fix(ui): enable live tool call streaming in Control UI webchat#39036
fix(ui): enable live tool call streaming in Control UI webchat#39036jakepresent wants to merge 2 commits into
Conversation
Greptile SummaryThis PR fixes four independent bugs that collectively caused live tool-call streaming to be invisible in the Control UI webchat: missing Key findings:
Confidence Score: 4/5
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: dace5b42ae
ℹ️ 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".
| const sessionKey = typeof payload.sessionKey === "string" ? payload.sessionKey : undefined; | ||
| if (sessionKey && sessionKey !== host.sessionKey) { | ||
| return; |
There was a problem hiding this comment.
Keep tool events scoped to the active run
Filtering tool events only by sessionKey lets this handler accept events from any concurrent run in the same session, not just the run the chat view is currently streaming. The gateway intentionally registers one connection for other active runs in that session (for late-join behavior), so in concurrent-run scenarios this will merge unrelated tool cards into the current conversation and can prematurely clear the current chatStream when the first foreign tool event arrives. Please keep run-level scoping (or map server/internal run IDs back to the active client run) instead of session-only acceptance.
Useful? React with 👍 / 👎.
ad118eb to
4b4ef87
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4b4ef87abf
ℹ️ 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".
| host.chatStreamSegments.push({ | ||
| text: appHost.chatStream, | ||
| ts: appHost.chatStreamStartedAt ?? now, | ||
| }); | ||
| appHost.chatStream = null; |
There was a problem hiding this comment.
Preserve only post-tool text in live stream
When a tool starts, this code commits the current chatStream into chatStreamSegments and then resets chatStream to null, but subsequent chat deltas are cumulative assistant text for the whole run (the gateway emits merged text and handleChatEvent replaces chatStream with that full value). In any run where the assistant continues after a tool call, the pre-tool prefix is rendered twice—once as a committed segment and again at the start of the resumed live stream—so the interleaved transcript becomes incorrect during streaming.
Useful? React with 👍 / 👎.
| host.chatStreamSegments.push({ | ||
| text: appHost.chatStream, | ||
| ts: appHost.chatStreamStartedAt ?? now, |
There was a problem hiding this comment.
Bound committed stream segments by tool stream limit
Each new tool call appends a new entry to chatStreamSegments, but only toolStreamOrder is trimmed by TOOL_STREAM_LIMIT. In long tool-heavy runs, old tool cards are dropped while their paired stream segments remain indefinitely, causing unbounded growth in rendered items and leaving orphaned text segments with no corresponding tool card.
Useful? React with 👍 / 👎.
Tool call output (web_search, etc.) was invisible during streaming and only appeared after a manual page refresh. Four independent issues combined to cause this: 1. Control UI connected with caps: [] and never received tool events from the gateway's broadcastToConnIds path. Fix: register tool-events cap. 2. handleAgentEvent rejected all tool events due to runId mismatch: the client sets chatRunId to a client-generated UUID while tool events arrive with the server engine runId. Fix: filter by sessionKey only. 3. Tool cards were gated behind showThinking (intended for model reasoning tokens). When the user had showThinking disabled, tool cards and committed stream segments were hidden. Fix: render tool messages and stream segments unconditionally. 4. On run completion, resetToolStream cleared chatToolMessages, but shouldReloadHistoryForFinalEvent returned false for normal assistant messages, so tool cards were never repopulated from history. Fix: reload history when tool events were seen during the run. Return a flag to prevent the outer shouldReloadHistoryForFinalEvent path from issuing a duplicate loadChatHistory call. Additional improvements: - Commit streaming text to chatStreamSegments when a tool call starts, so assistant text before a tool card is preserved rather than replaced. - Interleave stream segments and tool messages by timestamp for correct visual ordering. - Clear chatToolMessages on history load to prevent duplicates. - Cancel pending sync timer in resetToolStream instead of flushing from empty state. Fixes openclaw#38888
4b4ef87 to
7f2c4f9
Compare
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 7f2c4f9600
ℹ️ 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".
| state.chatThinkingLevel = res.thinkingLevel ?? null; | ||
| // Clear streaming tool messages — history includes tool results inline, | ||
| // so keeping them would cause duplicates or wrong ordering. | ||
| (state as unknown as { chatToolMessages: unknown[] }).chatToolMessages = []; |
There was a problem hiding this comment.
Preserve tool stream state during history reload
loadChatHistory now unconditionally clears chatToolMessages, but the tab refresh path (refreshActiveTab → refreshChat) calls loadChatHistory without first resetting/rebuilding tool stream state. If a user switches away from chat and back while a tool-heavy run is still in progress, this wipes the currently streamed tool cards from the UI, and they may not reappear until another tool event arrives or the run finishes, causing missing live output in the transcript.
Useful? React with 👍 / 👎.
| activeMinutes: CHAT_SESSIONS_ACTIVE_MINUTES, | ||
| }); | ||
| } | ||
| return false; |
There was a problem hiding this comment.
Reload history after reset-command runs with tool events
In handleTerminalChatEvent, runs tracked in refreshSessionsAfterChat (the /new and /reset flow) always return without reloading chat history, even when hadToolEvents is true. Since resetToolStream already cleared live tool state and shouldReloadHistoryForFinalEvent is false for normal assistant finals, reset-command runs that continue with prompt text can lose their tool cards/segments after completion instead of being repopulated from persisted history.
Useful? React with 👍 / 👎.
Tool call output (web_search, etc.) was invisible during streaming and only appeared after a manual page refresh. Four independent issues combined to cause this: 1. Control UI connected with caps: [] and never received tool events from the gateway's broadcastToConnIds path. Fix: register tool-events cap. 2. handleAgentEvent rejected all tool events due to runId mismatch: the client sets chatRunId to a client-generated UUID while tool events arrive with the server engine runId. Fix: filter by sessionKey only. 3. Tool cards were gated behind showThinking (intended for model reasoning tokens). When the user had showThinking disabled, tool cards were hidden. Fix: render tool messages unconditionally. 4. On run completion, resetToolStream cleared chatToolMessages, but shouldReloadHistoryForFinalEvent returned false for normal assistant messages, so tool cards were never repopulated from history. Fix: reload history when tool events were seen during the run. Return a flag to prevent the outer shouldReloadHistoryForFinalEvent path from issuing a duplicate loadChatHistory call. 5. Reload history on each tool result event so persisted text and tool output replace any truncated streaming fragments mid-run. Additional improvements: - Clear chatToolMessages on history load to prevent duplicates. - Cancel pending sync timer in resetToolStream instead of flushing from empty state. Fixes openclaw#38888
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 391384e318
ℹ️ 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".
| agentPayload?.stream === "tool" && | ||
| typeof (agentPayload.data as Record<string, unknown>)?.phase === "string" && | ||
| (agentPayload.data as Record<string, unknown>).phase === "result" | ||
| ) { | ||
| void loadChatHistory(host as unknown as OpenClawApp); |
There was a problem hiding this comment.
Serialize tool-result history reload requests
This branch triggers loadChatHistory for every tool phase === "result" event, but each call is fire-and-forget and loadChatHistory unconditionally rewrites state.chatMessages when it resolves. In tool-heavy runs, multiple history requests can overlap; if an earlier request resolves after a later one, it can overwrite newer history with a stale snapshot and temporarily (or until manual refresh) hide newer tool output. Coalescing or sequencing these reloads would prevent out-of-order state rollback.
Useful? React with 👍 / 👎.
|
Superseded by a cleaner PR — same core fixes but slimmed down after iteration. |
Summary
Tool call output (e.g.
web_search,exec) in the Control UI webchat was invisible during streaming and only appeared after a manual page refresh. This PR fixes five independent issues that combined to cause this.Fixes #38888
Root Causes
1. Missing
tool-eventscapability (gateway.ts)Control UI connected with
caps: []and never received tool events from the gateway'sbroadcastToConnIdspath.Fix: Register
tool-eventscapability in the hello handshake.2. runId mismatch rejecting all tool events (app-tool-stream.ts)
handleAgentEventfiltered tool events bychatRunId, but the client setschatRunIdto a client-generated UUID (viagenerateUUIDinsendChatMessage), while tool events arrive with the server's engine runId. They could never match.Fix: Filter tool events by
sessionKeyonly, which is the correct scoping boundary.3. Tool cards gated behind
showThinking(views/chat.ts)Tool messages were only rendered inside
if (props.showThinking). This toggle controls model reasoning/thinking tokens, but tool calls are user-visible actions and should always render.Fix: Render tool messages unconditionally.
4. History not reloaded after tool runs (app-gateway.ts)
On run completion,
resetToolStreamclearedchatToolMessages, butshouldReloadHistoryForFinalEventreturnedfalsefor normal assistant messages, so tool cards were never repopulated from persisted history.Fix: Reload history when tool events were seen during the run. Return a flag to prevent the outer
shouldReloadHistoryForFinalEventpath from issuing a duplicateloadChatHistorycall.5. No mid-run history reload on tool results (app-gateway.ts)
During streaming, text fragments could be truncated because tool events arrive via a different pipeline than chat deltas. The server persists tool results as they happen, but the UI never fetched them mid-run.
Fix: Reload history on each tool
resultevent so the persisted text and tool output replace any truncated streaming state.Changes
ui/src/ui/gateway.tstool-eventscapabilityui/src/ui/app-tool-stream.tsresetToolStreamui/src/ui/app-gateway.tsui/src/ui/controllers/chat.tschatToolMessageson history load to prevent duplicatesui/src/ui/views/chat.tsshowThinkinggate)Testing
Tested manually on Windows with OpenClaw 2026.3.2:
web_search) - card renders live, history reloads with full text on resultshowThinkingoff - tool cards visible, reasoning hiddenshowThinkingon - tool cards + reasoning tokens visibleRelated