feat(chat): show live train-of-thought as one smooth-updating line (ENG-1108) - #511
feat(chat): show live train-of-thought as one smooth-updating line (ENG-1108)#511torrmal wants to merge 3 commits into
Conversation
…NG-1108) Reasoning/extended-thinking text that isn't part of the final answer no longer becomes its own permanent "Reasoning" step. Every resumed burst used to add a new frozen row to the steps list (stacking one after another) that never went away, even once the turn finished. Track the current burst as ephemeral `currentThought` state instead of a step: it updates in place as deltas stream in, resets (not appends) when a new burst starts after a tool call, and disappears entirely once the burst ends (tool call starts, body text starts) or the turn completes. Rendered as a single line at the bottom of the working-steps list, with a stable key so React updates it smoothly rather than remounting. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
848236e to
bd865c8
Compare
… + distinct thought styling (ENG-1108)
Follow-ups to the live train-of-thought work, from testing against real
turns:
- Narration reclassification: text that streams before a tool call is
preamble ("let me verify X first…"), not the final answer — anton's
tool loop keeps going after the tool result. It now moves into the
ephemeral thought line the moment the tool call starts, and only the
final round's text (no tool call after it) stays as the persisted
answer. Live streaming is preserved: the text still streams in real
time; it's just relocated once we learn it was preamble. A bare tool
start (no preamble) seals the current reasoning burst so a resumed one
starts fresh instead of appending.
- Orb stays anchored to the working header for the whole turn (thinking →
streaming → done), sharing isThinkingActive with the steps panel so the
two can't drift apart, and its anchor box now matches the step-icon
gutter so it lines up in the same column as the steps below it.
- The live thought line is styled distinct from a step row (pulsing dot +
italic shimmering text) so it reads as inner monologue, and the working
header no longer flickers to the thought text — it stays the working
message.
Note: this does NOT fix the same-answer-repeated-2-3x symptom — that's a
separate server-side issue (the completion verifier forcing unnecessary
continuations, each re-streaming a full answer). Tracked separately.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…-train-of-thought
pnewsam
left a comment
There was a problem hiding this comment.
Code review
Review type: COMMENT
The ephemeral currentThought state and reducer coverage are a solid foundation, but I found two important integration gaps and one moderate lifecycle issue: the parallel fallback buffer can duplicate or re-persist reclassified narration, thoughts before the first tool call remain hidden in the collapsed block, and post-tool reasoning can append to the previous narration burst.
Targeted reducer tests (14/14) and the full typecheck pass. The full suite reached 593 passing tests; 17 unrelated loopback-dependent tests failed because this review environment prohibits binding local ports.
| @@ -1752,6 +1752,7 @@ function AppCore() { | |||
| role: '_streaming', | |||
| content: streamState.bodyText || assistantContent, | |||
There was a problem hiding this comment.
Important: Clearing streamState.bodyText does not clear the parallel assistantContent buffer. Every output delta is also appended to assistantContent in onChunk, so after a tool start reclassifies the preamble and empties bodyText, this expression immediately falls back to the old preamble. The user can therefore see the same text both in the answer and in currentThought; if no later output arrives, finalContent persists it as the answer anyway.
This pattern appears in all four streaming paths. Please make one buffer the source of truth, or reset/segment the fallback buffer when the reducer reclassifies text.
| // header made the working message flicker/overwrite | ||
| // as each reasoning delta streamed in. | ||
| const active = [...(streamingMsg.steps || [])].reverse().find(s => s.status === 'in_progress'); | ||
| return active?.label || null; |
There was a problem hiding this comment.
Important: A thought arriving before the first tool call is not actually visible. With no inspectable steps, ThinkingBlock initializes collapsed and its auto-expand effect never fires; this expression then returns null, so the collapsed header only says “Thinking…” while the live thought line remains hidden.
Please include currentThought in the expansion condition, or use its truncated text as the collapsed label for this no-step state.
| _fullText: text, | ||
| }; | ||
| return { ...state, steps: [...state.steps, step] }; | ||
| const prevText = state.currentThought?.text || ''; |
There was a problem hiding this comment.
Moderate: When pre-tool narration exists, reclassifyPreambleOnToolStart stores it in currentThought, but the tool completion path does not clear it. The first post-tool reasoning delta therefore appends to that narration here (for example, Checking the docs.Now analyzing...) instead of starting a distinct burst.
Consider tracking reclassified narration separately or replacing it when reasoning resumes after the tool boundary.
Summary
Stacked on #510 (ENG-1107) — this PR will show that one's commit too until it merges to
staging.thought.progresswithsubtype: 'reasoning' | 'thinking') no longer becomes its own persisted "Reasoning" step in the steps list. Previously, every time thinking resumed after a tool call, a brand-new "Reasoning" row was pushed — so a turn with several tool calls ended up with multiple frozen, contentless "Reasoning" rows stacked in the list permanently, even after the turn finished.currentThought— an ephemeral{ text, startedAt } | nullburst tracked outside the persistedstepsarray inresponseStreamAdapter.js. It accumulates while a burst is live, and is cleared (not carried forward) whenever the burst ends: a tool call/scratchpad cell starts, body text starts streaming, or the turn completes/fails.ThinkingBlockrenders it as a single line at the bottom of the steps list with a stable React key, so it updates smoothly in place as new deltas arrive rather than stacking a new row per chunk or per burst — and disappears entirely once the burst ends or the turn is done, since it's never part of the persisted step history.currentLabelnow falls back to the live thought text when there's no active tool-call step to show a label for (e.g. reasoning happening before the very first tool call).WorkingIndicator(sameslotId) used to also render whenever there were no steps yet, which would double up withThinkingBlock's own header indicator once a thought burst existed with zero real steps.Linear: https://linear.app/mindsdb/issue/ENG-1108/show-live-train-of-thought-as-a-single-smooth-updating-line-not
Test plan
src/renderer/cowork/lib/responseStreamAdapter.test.js— covers burst accumulation, clearing on body-text/tool-call/scratchpad-start/completed/failed, and that a later burst doesn't leak text from an earlier onenpm run typecheckpassesnpm test— 570/570 passing (2 unrelated pre-existing suite failures from a missingremark-mathmodule in localnode_modules, not caused by this change)🤖 Generated with Claude Code