fix(chat): show provider reasoning in the timeline - #8628
Conversation
Provider adapters already normalize thinking deltas to reasoning_text / reasoning_summary_text, but ingestion dropped them. Reasoning now rides the assistant message pipeline as an optional channel field: one segment slot per thread, buffered/streamed with the same delivery switch as assistant text, stamped with per-thread monotonic timestamps so live and reloaded order agree, and excluded from answer semantics (turn binding, checkpoints, titles, search, minimap). Web renders collapsible Thinking rows; mobile filters reasoning at derivation. Schema change ships as migration 044, idempotent for databases that predate it. Design debated with GPT-5.6 sol; implementation by GPT-5.6 sol via Codex CLI, Claude Opus 5, and Claude Fable 5. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…the timeline
Claude Code 2.1.248+ runs SDK sessions in the redacted-thinking phase:
thinking deltas stream with empty text and only estimated_tokens, so the
reasoning pipeline had nothing to ingest. Pass thinking: { type:
"adaptive", display: "summarized" } (the SDK's --thinking-display
flag) to request API-side thinking summaries, skipped when the thread's
thinking toggle is off.
Debugged and fixed by Claude Fable 5 in Claude Code.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
There was a problem hiding this comment.
Reviewed the web timeline changes for reasoning rows (MessagesTimeline.tsx, MessagesTimeline.logic.ts). The disclosure control matches the file's existing raw-button row pattern (TurnFoldTimelineRow), reasoning rows are correctly kept out of terminal-assistant meta, duration boundaries, minimap previews, and getItemType recycling buckets. One finding on the expanded thought body.
Posted via Macroscope — UI Consistency
There was a problem hiding this comment.
One finding in the web timeline fold logic; details inline.
Posted via Macroscope — UI Consistency
There was a problem hiding this comment.
One finding on the timeline fold derivation: reasoning entries now win the "first assistant entry stays visible" slot. Details inline.
Posted via Macroscope — UI Consistency
There was a problem hiding this comment.
One finding on the active-turn "Thinking" indicator. The two items flagged in earlier runs (turn-fold anchoring around a leading thought, and the missing break rule on the expanded thought body) are addressed in the current head.
Posted via Macroscope — UI Consistency
| return ( | ||
| entry.message.role === "assistant" && | ||
| !isReasoningMessage(entry.message) && |
There was a problem hiding this comment.
While a thought is still arriving, excluding it here keeps activeTurnHasVisibleContent false, so the working row keeps showThinking and renders the shimmering Thinking live-activity label directly above the streaming Thinking... reasoning row — two thinking indicators stacked for the same state (this also happens in the default buffered mode once a long thought spills its buffer).
Streamed reasoning text is visible provider output, like assistant commentary, so a streaming thought could count as visible content; empty thoughts stay excluded by the existing text check.
| return ( | |
| entry.message.role === "assistant" && | |
| !isReasoningMessage(entry.message) && | |
| return ( | |
| entry.message.role === "assistant" && | |
| (!isReasoningMessage(entry.message) || entry.message.streaming) && |
Posted via Macroscope — UI Consistency
Provider reasoning output never reached any client. Ingestion forwarded only
assistant_text. It droppedreasoning_textandreasoning_summary_text, so Claude, Codex, and OpenCode all lost their reasoning.This implements the fix discussed in discussion #8625 and addresses open issue #5542.
What changed
Reasoning now rides the existing assistant message pipeline through an optional
channel: "reasoning"field on assistant messages. There is no parallel message type or second store. One burst of thinking becomes one message keyedreasoning:<threadId>:<turnId>:segment:<n>. Events without a turn ID useturnlessin the turn slot.Reasoning delivery follows the existing
enableLegacyTokenStreamingsetting. There is no new delivery mode. Per-thread monotonic timestamps keep live ordering and reload ordering identical.The web client nests each reasoning message under the turn's
Worked for Nsgroup as a collapsible row. A completed burst that took at least one second readsThought for Ns. A shorter burst readsThought.Reasoning is excluded from answer semantics. It never settles a turn, lands in a checkpoint, supplies a thread title, appears in search, or appears in the minimap.
Migration
044_ProjectionThreadMessagesChanneladds a nullablechannelcolumn toprojection_thread_messages. APRAGMA table_infocheck guards the alteration. This makes the migration idempotent, which means running it twice has the same effect as running it once.The Claude adapter now sends
thinking: { type: "adaptive", display: "summarized" }unless thinking is explicitly disabled. Recent Claude Code versions otherwise stream redacted thinking that carries token estimates and no text. This removes the--thinking-display summarizedlaunch-argument workaround described in #5542.Evidence
Before: the timeline on current
mainhas no reasoning row.After: the timeline on this branch has an expanded
Thought for 7.7srow.Demo video: https://youtu.be/VuzwmRg24uM
Surfaces
Entry points: Only passive rendering in the chat view applies. There is no action to mirror in Settings, the command palette, or a keybinding.
Clients: Web renders the rows. Desktop gets them by wrapping web. Mobile filters reasoning out during derivation because it has no reasoning UI yet.
Providers: Claude, Codex, and OpenCode already normalize thinking deltas to
reasoning_textandreasoning_summary_text. This change consumes both stream kinds. Cursor and Grok expose no reasoning stream, so there is nothing to show.Contracts: The optional
channelfield crosses the typed message, command, event, persistence, and snapshot contracts.Reverse states: Each row can be expanded and collapsed. There is no persisted one-way state.
Connection modes and version skew: The existing assistant message path serves local, remote/relay, and tunnel connections. Schema decoding strips the unknown
channelfield for older clients, so they render reasoning as ordinary assistant text instead of failing. An older server rolled back onto events carryingchannelstill replays and starts.Docs:
docs/user/reasoning.mdexplains the shipped behavior,docs/user/providers-claude.mdcovers Claude's adaptive summarized thinking, anddocs/internals/glossary.mddefines the channel.Verification
Claude is verified end to end in a real client. Codex and OpenCode use the same ingestion path, and tests cover both reasoning stream kinds. Neither provider was manually driven.
Tests: focused runs of
threadActivity.test.ts,CheckpointReactor.test.ts,ProjectionPipeline.test.ts,ProjectionSnapshotQuery.test.ts,ProviderCommandReactor.test.ts,ProviderRuntimeIngestion.test.ts,projector.test.ts,044_ProjectionThreadMessagesChannel.test.ts,ClaudeAdapter.test.ts,MessagesTimeline.logic.test.ts, andthreadReducer.test.ts, plus scoped typechecks for the touched packages. All passed.Known boundary
The Claude adapter's
content_block_starthandler still does not registerthinkingblocks, so thinking deltas still carry noitemId. This design does not need that ID because it keys reasoning bursts by thread, turn, and segment. I can fix this at the source if maintainers prefer.Scope and split option
This PR is larger than the usual one-concern ideal. It has 906 additions and 65 deletions across 32 files, with roughly half of the additions in tests. It is rebased on current
main.The change splits into three pieces: the two-line Claude
thinking.displayoption, the server pipeline, and the web rows. I will reshape or split it on request.Model: GPT-5.6 Sol. Harness: Codex, orchestrated from Claude Code.
Note
Show provider reasoning messages in the chat timeline
reasoningmessage channel to the orchestration contracts, with a sharedisReasoningMessagehelper used across server, web, and mobile to classify these messagesProviderRuntimeIngestion) now emits reasoning-channel assistant deltas and completions, buffering or streaming based on delivery mode, and finalizes open segments on key lifecycle events (turn/session transitions, assistant messages, activities)nextMessageStamp, which may advance timestamps beyond their originalintendedAtchannelcolumn toprojection_thread_messages; projection, snapshot query, and repository layers persist and return itthinking: { type: "adaptive", display: "summarized" }by default on new queries unless explicitly disabledReasoningTimelineRow(auto-expands while streaming, shows duration); reasoning messages are excluded from duration boundaries, terminal assistant selection, title formatting, and active-turn visibilitythreadReducerand mobilebuildThreadFeedexclude reasoning messages from turn settlement and feed derivationhasAssistantMessageForTurninProviderRuntimeIngestion.tsandcaptureAndDispatchCheckpointinCheckpointReactor.tsnow filter out reasoning messages when selecting the assistant message for a turn; any code that relied on reasoning messages being counted as assistant messages will see different results. Thechannelcolumn is nullable so existing rows are unaffected.📊 Macroscope summarized 9b065e4. 21 files reviewed, 5 issues evaluated, 0 issues filtered, 5 comments posted
🗂️ Filtered Issues