fix(webchat): clear chatStream before updating chatMessages to prevent duplicate rendering#34164
fix(webchat): clear chatStream before updating chatMessages to prevent duplicate rendering#34164qiyuey wants to merge 1 commit into
Conversation
* feat(ios): add live activity connection status and cleanup Add lock-screen/Dynamic Island connection health states and prune duplicate/stale activities before reuse. This intentionally excludes AI/title generation and heavier UX rewrites from #27488. Co-authored-by: leepokai <1663017+leepokai@users.noreply.github.com> * fix(ios): treat ended live activities as inactive * chore(changelog): add PR reference and author thanks --------- Co-authored-by: leepokai <1663017+leepokai@users.noreply.github.com>
Greptile SummaryThis PR fixes a duplicate-rendering bug in the webchat controller where Lit's async batching could trigger a render cycle after However, the PR also includes 12 unrelated iOS files implementing a new Live Activity feature (Lock Screen / Dynamic Island connection status). The changelog fragment for these iOS changes explicitly references PR #33591, not this one, strongly suggesting they were accidentally included here. Key observations:
Confidence Score: 3/5
Last reviewed commit: 0e97e38 |
| @@ -0,0 +1 @@ | |||
| - iOS: add Live Activity connection status (connecting/idle/disconnected) on Lock Screen and Dynamic Island, and clean up duplicate/stale activities before starting a new one (#33591) (thanks @mbelinky, @leepokai) | |||
There was a problem hiding this comment.
iOS changes appear unrelated to this PR's stated scope
This changelog entry references #33591 and describes an entirely new iOS Live Activity feature (Lock Screen + Dynamic Island connection status). The PR title and description focus exclusively on a webchat duplicate-rendering fix in ui/src/ui/controllers/chat.ts.
The 12 iOS files added/modified here (LiveActivityManager.swift, OpenClawActivityAttributes.swift, OpenClawLiveActivity.swift, NodeAppModel.swift, project.yml, Signing.xcconfig, Info.plist, etc.) appear to belong to PR #33591, not this fix. Bundling two unrelated features makes the diff harder to review, complicates bisecting if a regression is introduced, and makes targeted rollback impossible.
Please confirm whether these iOS changes were intentionally included or if they were accidentally carried over from another branch.
Prompt To Fix With AI
This is a comment left during a code review.
Path: changelog/fragments/ios-live-activity-status-cleanup.md
Line: 1
Comment:
**iOS changes appear unrelated to this PR's stated scope**
This changelog entry references `#33591` and describes an entirely new iOS Live Activity feature (Lock Screen + Dynamic Island connection status). The PR title and description focus exclusively on a webchat duplicate-rendering fix in `ui/src/ui/controllers/chat.ts`.
The 12 iOS files added/modified here (`LiveActivityManager.swift`, `OpenClawActivityAttributes.swift`, `OpenClawLiveActivity.swift`, `NodeAppModel.swift`, `project.yml`, `Signing.xcconfig`, `Info.plist`, etc.) appear to belong to PR #33591, not this fix. Bundling two unrelated features makes the diff harder to review, complicates bisecting if a regression is introduced, and makes targeted rollback impossible.
Please confirm whether these iOS changes were intentionally included or if they were accidentally carried over from another branch.
How can I resolve this? If you propose a fix, please make it concise.| func handleDisconnect() { | ||
| self.updateCurrent(state: self.disconnectedState()) | ||
| } |
There was a problem hiding this comment.
Live Activity is never explicitly ended
handleDisconnect() updates the activity state to "Disconnected" but never calls activity.end(...). This means the Live Activity banner will persist on the Lock Screen and Dynamic Island indefinitely — iOS will eventually clean it up (after its staleness timeout, typically up to 4–8 hours), but users will continue to see "Disconnected" long after the session is gone.
Consider adding an explicit endActivity() method that calls activity.end(dismissalPolicy: .immediate) and hooking it into the user-initiated disconnect or app termination flow if a clean dismissal is desired.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/ios/Sources/LiveActivity/LiveActivityManager.swift
Line: 64-66
Comment:
**Live Activity is never explicitly ended**
`handleDisconnect()` updates the activity state to "Disconnected" but never calls `activity.end(...)`. This means the Live Activity banner will persist on the Lock Screen and Dynamic Island indefinitely — iOS will eventually clean it up (after its staleness timeout, typically up to 4–8 hours), but users will continue to see "Disconnected" long after the session is gone.
Consider adding an explicit `endActivity()` method that calls `activity.end(dismissalPolicy: .immediate)` and hooking it into the user-initiated disconnect or app termination flow if a clean dismissal is desired.
How can I resolve this? If you propose a fix, please make it concise.| ActivityWidget/OpenClawActivityWidgetBundle.swift | ||
| ActivityWidget/OpenClawLiveActivity.swift |
There was a problem hiding this comment.
Widget extension @main entry-point included in app source list
ActivityWidget/OpenClawActivityWidgetBundle.swift is decorated with @main and is the entry point for the OpenClawActivityWidget extension target. If SwiftSources.input.xcfilelist is consumed by a build phase that compiles sources into the main app module, this will produce a compile error because Swift only allows one @main per module.
If this file list is exclusively used by analysis tools (e.g., SwiftLint, SwiftFormat), this is harmless. Please add a comment clarifying the intended use, or remove the extension entry-point files if compilation is involved.
Prompt To Fix With AI
This is a comment left during a code review.
Path: apps/ios/SwiftSources.input.xcfilelist
Line: 67-68
Comment:
**Widget extension `@main` entry-point included in app source list**
`ActivityWidget/OpenClawActivityWidgetBundle.swift` is decorated with `@main` and is the entry point for the `OpenClawActivityWidget` extension target. If `SwiftSources.input.xcfilelist` is consumed by a build phase that compiles sources into the main app module, this will produce a compile error because Swift only allows one `@main` per module.
If this file list is exclusively used by analysis tools (e.g., SwiftLint, SwiftFormat), this is harmless. Please add a comment clarifying the intended use, or remove the extension entry-point files if compilation is involved.
How can I resolve this? If you propose a fix, please make it concise.|
Closing: branch accidentally included an upstream commit due to a bad git amend. Will reopen with a clean branch. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0e97e38d43
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| await self.refreshAgentsFromGateway() | ||
| await self.refreshShareRouteFromGateway() | ||
| await self.startVoiceWakeSync() | ||
| await MainActor.run { LiveActivityManager.shared.handleReconnect() } |
There was a problem hiding this comment.
Create Live Activity before applying reconnect state
The reconnect callback can run before any activity exists, because handleReconnect() is called from the operator session while activity creation only happens in the node loop. In that ordering, handleReconnect() becomes a no-op (updateCurrent returns when currentActivity is nil), then the node loop later sets the activity to connecting, and no subsequent transition moves it to idle, so users can be left with a stuck “Connecting...” Live Activity despite a healthy connection.
Useful? React with 👍 / 👎.
| Task { | ||
| await activity.update(ActivityContent(state: state, staleDate: nil)) |
There was a problem hiding this comment.
Serialize Live Activity state updates
Each status transition is dispatched via an unstructured fire-and-forget Task, so rapid transitions (for example disconnect followed immediately by reconnect) can complete out of order and leave the Live Activity showing stale state. Because updates are not awaited or sequenced, whichever async activity.update(...) finishes last wins, even if it was triggered earlier.
Useful? React with 👍 / 👎.
Problem
In the Control UI webchat, assistant replies appear duplicated on every message (issue #5964).
Root Cause
In
handleChatEvent(), thefinalandabortedevent handlers updatedchatMessagesbefore clearingchatStream. Because Lit batches reactive property updates asynchronously, a render cycle could be triggered afterchatMessageswas updated but beforechatStreamwas cleared. This causedbuildChatItems()to include the same assistant message in both the history list (fromchatMessages) and the live stream bubble (fromchatStream), visually appearing as a duplicate.Fix
Capture the stream text in a local snapshot, clear
chatStream/chatRunId/chatStreamStartedAtfirst, then append tochatMessages. This guarantees that any render triggered by thechatMessageswrite seeschatStreamasnull.The same fix is applied to both
finalandabortedevent paths.Changes
ui/src/ui/controllers/chat.ts: swap update order infinalandabortedbranchesui/src/ui/controllers/chat.test.ts: add 2 regression tests that verifychatStreamisnullat the exact momentchatMessagesis writtenTesting
Fixes #5964