Skip to content

feat(web): rich tool-call transcript — tool rows, inline diffs, thinking bursts, live status - #5471

Open
CuriosityOS wants to merge 2 commits into
pingdotgg:mainfrom
CuriosityOS:feat/tool-call-transcript
Open

feat(web): rich tool-call transcript — tool rows, inline diffs, thinking bursts, live status#5471
CuriosityOS wants to merge 2 commits into
pingdotgg:mainfrom
CuriosityOS:feat/tool-call-transcript

Conversation

@CuriosityOS

@CuriosityOS CuriosityOS commented Aug 6, 2026

Copy link
Copy Markdown

What

Upgrades the chat transcript's tool-call rendering to compact, information-dense rows (in the style of Claude Code's transcript):

  • Tool rows — each tool call renders as Tool(primary arg) with a status dot (running / done / error) and a ⎿ result one-liner, expandable for full input/output.
  • Inline diffs — file edits (Claude Edit/Write/MultiEdit, Codex native patches, ACP content diffs) render as inline unified diffs directly in the transcript, with add/remove line counts and theme-aware colors.
  • Thinking bursts — streamed reasoning deltas are grouped into a live "Thinking…" row that folds into a Thought for Xs entry when the burst ends, expandable to the reasoning text.
  • Live work status — the working indicator shows what the agent is doing right now (current tool + arg, or "Thinking…"), derived from recent activities and the streaming message.
  • Context-compaction rows — compaction starts render as a distinct shimmer row instead of disappearing.

How

Server

  • ClaudeAdapter forwards a bounded toolUseResult summary in toolData (structuredPatch capped at 400 lines, filePath kept) so the client can render edit diffs without replaying the file.
  • ProviderRuntimeIngestion tracks reasoning deltas into thinking-burst activities (throttled to 250ms, stable progress id per burst), forwards toolCallId/data on item events, and emits a context-compaction.started activity from session.state.changed.
  • ActivityPayloadProjection now retains tool-detail fields (toolName, input, result, toolUseResult, item, content, rawInput) in snapshots under a recursive clamp (4k-char strings, 100-item arrays, depth 6) instead of pruning them to one-line summaries — re-hydrated threads render the same rows the live stream did, with bounded row size. Live events were already unpruned; this only affects snapshot rows.

Web

  • session-logic normalizes Codex native tool items and ACP tool calls into one shape (tool name, input, diff hunks, result text), merges it into work-log entries, and adds deriveLiveWorkStatus.
  • MessagesTimeline renders the new rows; work groups keep their existing fold behavior (state tracked as collapsed-set with expanded default). Existing agent/subagent rendering (spawn CTA rows, agents panel, task linkage) is untouched.

Scope notes

  • No contract changes — new activity kinds ride the open kind string, and payload additions are additive.
  • Deliberately excluded from this PR to keep it reviewable: composer agent-status strip, scroll-behavior tweaks, plan-progress derivation.

Testing

  • New/updated unit tests: thinking-burst ingestion, snapshot clamp retention, tool metadata extraction (Claude/Codex/ACP), deriveLiveWorkStatus, timeline fold semantics, live-status row diffing.
  • Full web suite passes (207 files / 1845 tests); server suite passes apart from 3 pre-existing failures that require a non-root user (chmod-based permission tests).
  • Running in production on our fork for several weeks across Claude Code, Codex, and ACP providers.

🤖 Generated with Claude Code


Note

Medium Risk
Touches provider ingestion, activity projection, and large timeline UI paths; behavior is well-tested but regressions could affect transcript accuracy, snapshot size, or live status for all providers.

Overview
Delivers a richer chat transcript: compact Tool(arg) rows with status dots, result lines, inline file diffs, expandable “Thought for Xs” reasoning, context-compaction shimmer rows, and a phase-aware working indicator (thinking / running tool / writing).

Server: ProviderRuntimeIngestion collapses reasoning content.delta streams into throttled thinking.started / thinking.progress / thinking.completed activities (stable progress id, turn-aware close, stale turn.completed guard). Tool lifecycle events now carry toolCallId, more data on tool.started, and context-compaction.started from compacting session state. ClaudeAdapter attaches a bounded toolUseResult (structuredPatch + filePath) on edit tools. ActivityPayloadProjection retains and clamps tool-detail fields in snapshots instead of one-line summaries so re-hydrated threads match live rendering.

Web: session-logic normalizes Claude / Codex / ACP tool payloads into toolName, toolInput, toolDiff, toolResultText, maps completed thinking to work-log rows, and adds deriveLiveWorkStatus (with optional Claude silent-thinking fallback). MessagesTimeline renders the new row types and passes live status into the working row; work groups default expanded (track collapsedWorkGroupIds).

Reviewed by Cursor Bugbot for commit dc67dd4. Bugbot is set up for automated code reviews on this repo. Configure here.

Note

Add rich tool-call transcript with inline diffs, thinking bursts, and live status to chat timeline

  • Server-side ingestion in ProviderRuntimeIngestion.ts now coalesces reasoning deltas into thinking.started, throttled thinking.progress, and thinking.completed activities, and emits a context-compaction.started activity when the provider reports compaction.
  • ActivityPayloadProjection.ts and ClaudeAdapter.ts now retain and clamp structured tool detail fields (input, result, structuredPatch) in projected activity payloads.
  • session-logic.ts adds deriveLiveWorkStatus to compute a live phase indicator (Thinking / Running / Editing) from activities and streaming state, and enriches WorkLogEntry with toolName, toolInput, toolDiff, and toolResultText.
  • MessagesTimeline.tsx renders tool rows with a standardized header, inline unified diffs with expand/collapse, a ThinkingWorkEntryRow for completed reasoning bursts, a ContextCompactionRow divider, and a LiveThinkingStream with auto-scroll in the live working indicator.
  • Work groups in the timeline now default to expanded and only collapse when toggled (inverted from previous opt-in expand behavior).
  • Behavioral Change: collapsedWorkGroupIds replaces expandedWorkGroupIds in deriveMessagesTimelineRows; callers that previously passed expanded IDs must invert their state.

Macroscope summarized dc67dd4.

…ing bursts, live status

Renders provider tool calls as compact Claude Code-style rows in the chat
transcript: a Tool(arg) header with a status dot, a one-line result summary,
and inline unified diffs for file edits. Adds streamed thinking bursts
(grouped reasoning deltas with a "Thought for Xs" completed row), a live
work-status line under the working indicator, and context-compaction rows.

Server side: ClaudeAdapter forwards a bounded toolUseResult summary
(structuredPatch capped at 400 lines) in toolData; ingestion tracks
reasoning deltas into thinking-burst activities, forwards toolCallId/data on
item events, and surfaces compaction starts; snapshot payload projection now
retains tool detail fields under a recursive clamp (4k strings / 100-item
arrays / depth 6) so re-hydrated threads render the same rows as the live
stream.

Web side: session-logic normalizes Codex native items and ACP tool calls
into a shared shape (tool name, input, diff hunks, result text), derives a
live work status, and folds thinking bursts into the work log; the timeline
renders the new rows with expand-on-click detail while keeping existing
agent/subagent rendering untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 6, 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: 8c95af66-dbe9-49e5-88f7-2734e6abddf4

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

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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:XXL 1,000+ changed lines (additions + deletions). labels Aug 6, 2026
Comment thread apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts
return hunk ? capToolDiff(filePath, [hunk]) : null;
}

const content = typeof toolInput.content === "string" ? toolInput.content : 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.

🟡 Medium src/session-logic.ts:1808

extractToolDiff treats every content-based write/write_file/update_file call as a file creation, prefixing every line with +. When such a tool overwrites an existing file, the transcript reports the entire new content as additions and omits all removed lines, producing incorrect add/remove counts. Additionally, an empty content value causes extractToolDiff to return null, dropping the diff for a valid overwrite of an existing file with empty contents. This happens because the content branch at line 1809 unconditionally builds all-+ lines and returns null when the resulting array is empty, with no awareness of prior file state. Consider tracking whether the target file already existed and emitting old content as - lines (or marking the hunk as an overwrite) so edits to existing files render correctly.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/session-logic.ts around line 1808:

`extractToolDiff` treats every `content`-based `write`/`write_file`/`update_file` call as a file creation, prefixing every line with `+`. When such a tool overwrites an existing file, the transcript reports the entire new content as additions and omits all removed lines, producing incorrect add/remove counts. Additionally, an empty `content` value causes `extractToolDiff` to return `null`, dropping the diff for a valid overwrite of an existing file with empty contents. This happens because the `content` branch at line 1809 unconditionally builds all-`+` lines and returns `null` when the resulting array is empty, with no awareness of prior file state. Consider tracking whether the target file already existed and emitting old content as `-` lines (or marking the hunk as an overwrite) so edits to existing files render correctly.

Comment on lines +991 to +992
thinkingBurstByThreadId.delete(threadId);
const durationMs = Math.max(0, Date.parse(burst.lastEventAt) - Date.parse(burst.startedAt));

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.

🟡 Medium Layers/ProviderRuntimeIngestion.ts:991

closeThinkingBurst deletes the burst from thinkingBurstByThreadId before dispatching the final thinking.progress and thinking.completed activities. If either orchestrationEngine.dispatch call fails, processInputSafely catches and logs the error, but the burst state is already deleted — so a later reasoning event can't retry the close, leaving the persisted thinking.progress activity permanently uncompleted. Consider moving thinkingBurstByThreadId.delete(threadId) to after both dispatches succeed.

Suggested change
thinkingBurstByThreadId.delete(threadId);
const durationMs = Math.max(0, Date.parse(burst.lastEventAt) - Date.parse(burst.startedAt));
const durationMs = Math.max(0, Date.parse(burst.lastEventAt) - Date.parse(burst.startedAt));
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts around lines 991-992:

`closeThinkingBurst` deletes the burst from `thinkingBurstByThreadId` before dispatching the final `thinking.progress` and `thinking.completed` activities. If either `orchestrationEngine.dispatch` call fails, `processInputSafely` catches and logs the error, but the burst state is already deleted — so a later reasoning event can't retry the close, leaving the persisted `thinking.progress` activity permanently uncompleted. Consider moving `thinkingBurstByThreadId.delete(threadId)` to after both dispatches succeed.

Comment on lines +271 to +275
case "request.opened":
case "user-input.requested":
case "turn.completed":
case "turn.aborted":
case "session.exited":

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.

🟡 Medium Layers/ProviderRuntimeIngestion.ts:271

eventEndsThinkingBurst returns false for runtime.error and terminal session.state.changed events, so an open thinking burst is never closed when the session aborts during reasoning. The burst stays in thinkingBurstByThreadId and the transcript shows a live "Thinking" row indefinitely until some unrelated later event happens to close it. The function should also return true for these terminal/error cases so thinking.completed is emitted and the burst is flushed.

    case "request.opened":
    case "user-input.requested":
+    case "runtime.error":
+    case "session.state.changed":
    case "turn.completed":
    case "turn.aborted":
    case "session.exited":
🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts around lines 271-275:

`eventEndsThinkingBurst` returns `false` for `runtime.error` and terminal `session.state.changed` events, so an open thinking burst is never closed when the session aborts during reasoning. The burst stays in `thinkingBurstByThreadId` and the transcript shows a live "Thinking" row indefinitely until some unrelated later event happens to close it. The function should also return `true` for these terminal/error cases so `thinking.completed` is emitted and the burst is flushed.

Comment thread apps/server/src/orchestration/ActivityPayloadProjection.ts
Effect.map((uuid) => CommandId.make(`provider:${event.eventId}:${tag}:${uuid}`)),
);

const thinkingBurstByThreadId = new Map<ThreadId, ThinkingBurstState>();

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.

🟡 Medium Layers/ProviderRuntimeIngestion.ts:939

If the server restarts while a thinking burst is in progress, the persisted activity stream is left with a thinking.progress (or thinking.started) row that never gets a matching thinking.completed row. The burst state lives only in the in-memory thinkingBurstByThreadId map, so after a restart the map is empty and no closing event can be synthesized for the orphaned burst. The client transcript is left showing a permanently live "Thinking" entry, and any later reasoning deltas start a separate burst instead of continuing the original one. Consider persisting enough burst state to reconstruct the open burst on startup, or emitting a terminal row on startup for any thinking.started/thinking.progress activity that has no corresponding thinking.completed.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts around line 939:

If the server restarts while a thinking burst is in progress, the persisted activity stream is left with a `thinking.progress` (or `thinking.started`) row that never gets a matching `thinking.completed` row. The burst state lives only in the in-memory `thinkingBurstByThreadId` map, so after a restart the map is empty and no closing event can be synthesized for the orphaned burst. The client transcript is left showing a permanently live "Thinking" entry, and any later reasoning deltas start a separate burst instead of continuing the original one. Consider persisting enough burst state to reconstruct the open burst on startup, or emitting a terminal row on startup for any `thinking.started`/`thinking.progress` activity that has no corresponding `thinking.completed`.

Comment thread apps/web/src/session-logic.ts Outdated
* so edits render like Claude's structured patches instead of a full-file
* remove/add wall.
*/
function contextualDiffHunk(oldText: string, newText: string): WorkLogToolDiffHunk | 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.

🟡 Medium src/session-logic.ts:1979

contextualDiffHunk treats everything between the first and last changed line as a single remove/add block. For an ACP edit touching two distant locations in the same file, every unchanged line between them is emitted as both removed and added, inflating add/remove counts. When the file exceeds MAX_TOOL_DIFF_LINES, capToolDiff can exhaust the cap on these false deletions and drop the real additions or the second edit, producing materially incorrect inline diffs. Consider splitting disjoint changes into separate hunks or computing a real line diff instead of collapsing the middle span into one replacement.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/session-logic.ts around line 1979:

`contextualDiffHunk` treats everything between the first and last changed line as a single remove/add block. For an ACP edit touching two distant locations in the same file, every unchanged line between them is emitted as both removed and added, inflating add/remove counts. When the file exceeds `MAX_TOOL_DIFF_LINES`, `capToolDiff` can exhaust the cap on these false deletions and drop the real additions or the second edit, producing materially incorrect inline diffs. Consider splitting disjoint changes into separate hunks or computing a real line diff instead of collapsing the middle span into one replacement.

Comment thread apps/web/src/session-logic.ts
Comment thread apps/web/src/session-logic.ts
Comment thread apps/server/src/provider/Layers/ClaudeAdapter.ts

@cursor cursor 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.

Cursor Bugbot has reviewed your changes using high effort and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit ad731ab. Configure here.

Comment thread apps/server/src/orchestration/ActivityPayloadProjection.ts
Comment thread apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts
Comment thread apps/web/src/session-logic.ts
@macroscopeapp

macroscopeapp Bot commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

7 blocking correctness issues found. This PR introduces a significant new feature (rich tool-call transcript with thinking bursts, inline diffs, live status) with substantial new UI components and server-side state management. Additionally, 7 unresolved review comments identify logic bugs in the implementation. New features of this scope with outstanding issues warrant human review.

You can customize Macroscope's approvability policy. Learn more.

…ttribution and stale-turn handling

Server:
- Cap thinking-burst text at 32k chars so burst state and each persisted
  progress/completed payload stay bounded (chars keeps counting past the cap).
- Close open thinking bursts on runtime.error; ignore turn.completed/aborted
  events for a different turn than the open burst's so stale replays don't
  split reasoning into premature "Thought for Xs" rows.
- Delete burst state only after both close dispatches succeed so a failed
  close can be retried (stable activity ids make retries idempotent).
- clampToolDetail: cap object key count (100) alongside string/array/depth.
- Retain ACP `locations` in projected payloads — the client reads the primary
  file-path argument from it.
- Cap structuredPatch line length (500 chars) in the forwarded toolUseResult.

Web:
- ACP contextual diffs now split on unique anchor lines, so two edits at
  distant locations render as separate hunks instead of one replacement block
  that double-counts every unchanged line between them.
- Multi-file ACP/Codex diffs only include hunks for the first file — other
  files' hunks no longer render under the wrong filename.
- deriveLiveWorkStatus ignores unstamped activities from before the running
  turn started, so an open tool/thinking row left by an interrupted earlier
  turn can't resurface as current status.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@CuriosityOS

Copy link
Copy Markdown
Author

Addressed the review findings in dc67dd4:

Fixed

  • Unbounded thinking-burst text (macroscope, high): burst text is now capped at 32k chars, bounding both heap state and each persisted progress/completed payload; chars keeps counting past the cap so the summary stays accurate.
  • clampToolDetail object width (macroscope, high): added a 100-key cap per object alongside the string/array/depth limits.
  • structuredPatch line length (macroscope, high): each forwarded patch line is now capped at 500 chars.
  • Stale turn.completed closes the active burst (bugbot, high): turn lifecycle events now only close a burst belonging to that turn.
  • Burst state deleted before dispatch (macroscope): state is deleted only after both close dispatches succeed; stable activity ids make retries idempotent.
  • Burst never closed on abort (macroscope): runtime.error now ends an open burst. session.state.changed was left out deliberately — it also fires for non-terminal transitions (e.g. compaction) mid-reasoning, and the terminal paths are covered by turn.aborted/session.exited/runtime.error.
  • Projection drops ACP locations (bugbot): locations added to the retained keys.
  • Multi-file diff misattribution (bugbot + macroscope): both Codex fileChange and ACP diff-content normalization now only include hunks for the first file, instead of rendering later files' hunks under the first file's path.
  • contextualDiffHunk inflated counts (macroscope): rewritten to recursively split on lines unique to both sides, so distant edits in one ACP blob become separate contextual hunks instead of one replacement block that double-counts every unchanged line between them.
  • Live status leaks stale unstamped activities (macroscope): deriveLiveWorkStatus now ignores null-turnId activities created before the running turn's first stamped activity (still accepting them for providers that never stamp turn ids, where no better boundary exists).

Each fix has a regression test (stale-turn burst survival, anchor-split hunks, multi-file attribution, stale live-status filtering).

Not changed, with rationale

  • Write-tool overwrites rendered as all-additions: for content-only write tools the client has no prior file contents, so a real old/new diff isn't derivable; when the provider supplies old text (structuredPatch, ACP oldText) we already render a true diff. Rendering the written content as additions matches the tool's semantics.
  • Server restart orphans an open thinking.progress row: orphaned rows are invisible by construction — the transcript skips thinking.started/thinking.progress rows, and the live indicator only renders while a turn is running. A fresh burst starting after restart is the intended behavior.

🤖 Generated with Claude Code

* Reduce them to contextual hunks so edits render like Claude's structured
* patches instead of a full-file remove/add wall.
*/
function contextualDiffHunks(oldText: string, newText: string): WorkLogToolDiffHunk[] {

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.

🟡 Medium src/session-logic.ts:2087

contextualDiffHunks renders no diff for an edit that only adds or removes the file's trailing newline — for example oldText = "const x = 1;\n" and newText = "const x = 1;". splitDiffContent strips the terminal empty element, so both inputs become identical arrays; collectContextualDiffBlocks returns no blocks and the function returns [], dropping the edit from the UI. Consider preserving the trailing-newline difference so such edits still produce a hunk.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/session-logic.ts around line 2087:

`contextualDiffHunks` renders no diff for an edit that only adds or removes the file's trailing newline — for example `oldText = "const x = 1;\n"` and `newText = "const x = 1;"`. `splitDiffContent` strips the terminal empty element, so both inputs become identical arrays; `collectContextualDiffBlocks` returns no blocks and the function returns `[]`, dropping the edit from the UI. Consider preserving the trailing-newline difference so such edits still produce a hunk.

// the burst-completion activity was lost.
openThinking = null;
const entry = toDerivedWorkLogEntry(activity);
const key = entry.toolCallId ?? entry.collapseKey ?? entry.id;

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.

🟡 Medium src/session-logic.ts:942

In deriveLiveWorkStatus, tools that never get a toolCallId stay open after tool.completed: a tool.started event is keyed in openToolsByKey by entry.id (because tool.started rows never receive a collapseKey, and the key fallback is entry.toolCallId ?? entry.collapseKey ?? entry.id), but the matching tool.completed row computes its own entry.id, so openToolsByKey.delete(key) misses and the completed tool is never removed. The live-work indicator then keeps showing a completed tool as "Running ..." instead of dropping it. Consider keying both tool.started and tool.completed by a stable field (e.g. toolCallId first, then a collapseKey derived consistently for both lifecycle stages) so delete hits the same entry.

🤖 Copy this AI Prompt to have your agent fix this:
In file @apps/web/src/session-logic.ts around line 942:

In `deriveLiveWorkStatus`, tools that never get a `toolCallId` stay open after `tool.completed`: a `tool.started` event is keyed in `openToolsByKey` by `entry.id` (because `tool.started` rows never receive a `collapseKey`, and the key fallback is `entry.toolCallId ?? entry.collapseKey ?? entry.id`), but the matching `tool.completed` row computes its own `entry.id`, so `openToolsByKey.delete(key)` misses and the completed tool is never removed. The live-work indicator then keeps showing a completed tool as "Running ..." instead of dropping it. Consider keying both `tool.started` and `tool.completed` by a stable field (e.g. `toolCallId` first, then a `collapseKey` derived consistently for both lifecycle stages) so `delete` hits the same entry.

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

Labels

size:XXL 1,000+ 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