Skip to content

feat(chat): show live train-of-thought as one smooth-updating line (ENG-1108) - #511

Open
torrmal wants to merge 3 commits into
stagingfrom
feat/eng-1108-live-train-of-thought
Open

feat(chat): show live train-of-thought as one smooth-updating line (ENG-1108)#511
torrmal wants to merge 3 commits into
stagingfrom
feat/eng-1108-live-train-of-thought

Conversation

@torrmal

@torrmal torrmal commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Summary

Stacked on #510 (ENG-1107) — this PR will show that one's commit too until it merges to staging.

  • Reasoning/extended-thinking text (thought.progress with subtype: '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.
  • Introduced currentThought — an ephemeral { text, startedAt } | null burst tracked outside the persisted steps array in responseStreamAdapter.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.
  • ThinkingBlock renders 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.
  • The collapsed header's currentLabel now 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).
  • Fixed a latent slot collision: the "bridge state" placeholder WorkingIndicator (same slotId) used to also render whenever there were no steps yet, which would double up with ThinkingBlock'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

  • Added 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 one
  • npm run typecheck passes
  • npm test — 570/570 passing (2 unrelated pre-existing suite failures from a missing remark-math module in local node_modules, not caused by this change)

🤖 Generated with Claude Code

…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>
@torrmal
torrmal force-pushed the feat/eng-1108-live-train-of-thought branch from 848236e to bd865c8 Compare July 28, 2026 00:37
torrmal and others added 2 commits July 29, 2026 09:26
… + 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>
@torrmal
torrmal requested a review from pnewsam July 29, 2026 00:55

@pnewsam pnewsam left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 || '';

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants