Skip to content

fix(chat): show provider reasoning in the timeline - #8628

Draft
kgarg2468 wants to merge 7 commits into
pingdotgg:mainfrom
kgarg2468:t3code/streaming-reasoning-transcript
Draft

fix(chat): show provider reasoning in the timeline#8628
kgarg2468 wants to merge 7 commits into
pingdotgg:mainfrom
kgarg2468:t3code/streaming-reasoning-transcript

Conversation

@kgarg2468

@kgarg2468 kgarg2468 commented Aug 29, 2026

Copy link
Copy Markdown

Provider reasoning output never reached any client. Ingestion forwarded only assistant_text. It dropped reasoning_text and reasoning_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 keyed reasoning:<threadId>:<turnId>:segment:<n>. Events without a turn ID use turnless in the turn slot.

Reasoning delivery follows the existing enableLegacyTokenStreaming setting. 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 Ns group as a collapsible row. A completed burst that took at least one second reads Thought for Ns. A shorter burst reads Thought.

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_ProjectionThreadMessagesChannel adds a nullable channel column to projection_thread_messages. A PRAGMA table_info check 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 summarized launch-argument workaround described in #5542.

Evidence

Before: the timeline on current main has no reasoning row.

Timeline on current main, with no reasoning row

After: the timeline on this branch has an expanded Thought for 7.7s row.

Timeline on this branch, showing an expanded Thought for 7.7s row

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_text and reasoning_summary_text. This change consumes both stream kinds. Cursor and Grok expose no reasoning stream, so there is nothing to show.

  • Contracts: The optional channel field 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 channel field for older clients, so they render reasoning as ordinary assistant text instead of failing. An older server rolled back onto events carrying channel still replays and starts.

  • Docs: docs/user/reasoning.md explains the shipped behavior, docs/user/providers-claude.md covers Claude's adaptive summarized thinking, and docs/internals/glossary.md defines 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, and threadReducer.test.ts, plus scoped typechecks for the touched packages. All passed.

Known boundary

The Claude adapter's content_block_start handler still does not register thinking blocks, so thinking deltas still carry no itemId. 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.display option, 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

  • Adds a reasoning message channel to the orchestration contracts, with a shared isReasoningMessage helper used across server, web, and mobile to classify these messages
  • Server ingestion (ProviderRuntimeIngestion) 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)
  • Assistant message timestamps are now monotonically increasing per thread via nextMessageStamp, which may advance timestamps beyond their original intendedAt
  • Persistence: migration 44 adds a nullable channel column to projection_thread_messages; projection, snapshot query, and repository layers persist and return it
  • Claude adapter now sends thinking: { type: "adaptive", display: "summarized" } by default on new queries unless explicitly disabled
  • Web timeline renders reasoning messages with a collapsible ReasoningTimelineRow (auto-expands while streaming, shows duration); reasoning messages are excluded from duration boundaries, terminal assistant selection, title formatting, and active-turn visibility
  • Client threadReducer and mobile buildThreadFeed exclude reasoning messages from turn settlement and feed derivation
  • Risk: hasAssistantMessageForTurn in ProviderRuntimeIngestion.ts and captureAndDispatchCheckpoint in CheckpointReactor.ts now 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. The channel column is nullable so existing rows are unaffected.
📊 Macroscope summarized 9b065e4. 21 files reviewed, 5 issues evaluated, 0 issues filtered, 5 comments posted

🗂️ Filtered Issues

kgarg2468 and others added 2 commits August 28, 2026 12:28
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>
@coderabbitai

coderabbitai Bot commented Aug 29, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 4bfa5174-d83c-496d-b3eb-58cead39a338

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Aug 29, 2026
Comment thread apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts
Comment thread apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts Outdated
Comment thread apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts Outdated
Comment thread apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts
Comment thread apps/web/src/components/chat/MessagesTimeline.logic.ts Outdated

@macroscopeapp macroscopeapp Bot 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.

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

Comment thread apps/web/src/components/chat/MessagesTimeline.tsx Outdated
@github-actions github-actions Bot added size:XL 500-999 changed lines (additions + deletions). and removed size:L 100-499 changed lines (additions + deletions). labels Aug 29, 2026
Comment thread apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts Outdated
Comment thread apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts

@macroscopeapp macroscopeapp Bot 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.

One finding in the web timeline fold logic; details inline.

Posted via Macroscope — UI Consistency

Comment thread apps/web/src/components/chat/MessagesTimeline.logic.ts
Comment thread apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts

@macroscopeapp macroscopeapp Bot 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.

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

Comment thread apps/web/src/components/chat/MessagesTimeline.logic.ts
Comment thread apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts Outdated

@macroscopeapp macroscopeapp Bot 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.

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

Comment on lines +744 to +746
return (
entry.message.role === "assistant" &&
!isReasoningMessage(entry.message) &&

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.

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.

Suggested change
return (
entry.message.role === "assistant" &&
!isReasoningMessage(entry.message) &&
return (
entry.message.role === "assistant" &&
(!isReasoningMessage(entry.message) || entry.message.streaming) &&

Posted via Macroscope — UI Consistency

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

Labels

size:XL 500-999 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant