feat(thought-stream): live plain-language tool-call feed during runs - #1314
feat(thought-stream): live plain-language tool-call feed during runs#1314pedramamini wants to merge 1 commit into
Conversation
…1312) The Thought Stream captured only `process:thinking-chunk`, so it showed an agent's reasoning but never its actions. Tool calls did render in-chat, but only when a tab's `showThinking` mode was on, and the in-chat listener matches the streaming id with REGEX_AI_TAB alone - so an Auto Run's tool calls were dropped entirely. There was no surface where a user could watch what an agent was actually doing while it worked. Tool calls now stream into the Thought Stream alongside the reasoning, each reduced to ONE short plain-language line ("Read src/App.tsx", "Ran npm test", "Edited themes.ts") with a spinner while in flight and a check or warning when it ends. That makes the panel scannable for the reporter's actual goal: cost control - spotting an agent looping or grinding on an unproductive task and interrupting it before it burns more tokens. - `toolActivityLabel.ts`: `describeToolActivity()` normalizes tool names across Claude Code, OpenCode, Codex, Copilot, and MCP onto plain English. Unknown tools fall back to "Used <name>" rather than being dropped. Deliberately distinct from `summarizeToolInput()`, which builds the verbose in-chat cell. - `thoughtStreamStore`: `activities` buffer + `appendToolActivity()`, which merges a completion into its `running` entry (by `toolCallId`, or by newest matching running call in the same tab for providers like Codex that send none) so the feed lists actions, not state transitions. Entries keep their start timestamp so they don't jump when they finish. Capped at 2000/session. - `buildActivityFeed()`: merges thought blocks and tool calls into ONE chronological list, so a tool call appears between the two halves of the reasoning that produced it. - `useThoughtStreamToolListener`: taps `process:tool-execution` and resolves the id with `parseSessionId`, so Auto Run `-batch-` spawns are captured too. Same cheap `capturing[sessionId]` early-out as the thinking listener. - Panel renders tool rows, search matches the rendered line and the raw tool name, and the header counts "N thoughts - M actions". - Right Panel button becomes "View Activity"; in-app help and docs/autorun-playbooks.md updated to match. Closes #1312
📝 WalkthroughWalkthroughThe Thought Stream now captures tool execution events, converts them into concise labels, stores them per session, merges lifecycle updates, and renders tool calls interleaved with reasoning entries. UI copy, search behavior, tests, and developer documentation were updated accordingly. ChangesThought Stream Activity
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant AgentProcess
participant ToolListener
participant ThoughtStreamStore
participant ThoughtStreamPanel
AgentProcess->>ToolListener: Emit tool execution event
ToolListener->>ThoughtStreamStore: Normalize and append activity
ThoughtStreamStore->>ThoughtStreamStore: Merge lifecycle update
ThoughtStreamPanel->>ThoughtStreamStore: Build chronological feed
ThoughtStreamStore-->>ThoughtStreamPanel: Return thoughts and tool calls
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
Greptile SummaryAdds a live tool-call feed to the Thought Stream.
Confidence Score: 3/5The activity feed chronology should be fixed before merging because fast tool calls can be displayed after reasoning that actually followed them. Thought grouping ignores intervening tools, while feed sorting assigns each complete thought block its start time, so a tool occurring inside a block cannot be rendered between the surrounding reasoning chunks. Files Needing Attention: src/renderer/stores/thoughtStreamStore.ts Important Files Changed
Sequence DiagramsequenceDiagram
participant Agent
participant Main as Main process
participant Listener as Renderer listeners
participant Store as Thought Stream store
participant Panel as Thought Stream panel
Agent->>Main: Thinking chunks
Agent->>Main: Tool lifecycle events
Main-->>Listener: process:thinking-chunk
Main-->>Listener: process:tool-execution
Listener->>Store: Append thought
Listener->>Store: Append or merge tool activity
Store->>Panel: Build chronological activity feed
Panel-->>Panel: Search and render newest first
Reviews (1): Last reviewed commit: "feat(thought-stream): live plain-languag..." | Re-trigger Greptile |
| return items.sort((a, b) => { | ||
| if (a.timestamp !== b.timestamp) return a.timestamp - b.timestamp; | ||
| if (a.kind === b.kind) return 0; | ||
| return a.kind === 'thought' ? -1 : 1; | ||
| }); |
There was a problem hiding this comment.
Thought blocks hide tool ordering
When thought chunks immediately before and after a tool call are less than three seconds apart, groupThoughtsIntoBlocks combines them and this code sorts the complete block using only its start time. The tool therefore renders after reasoning that was emitted after the action, causing the activity feed to present the run in the wrong chronological order.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
src/renderer/stores/thoughtStreamStore.ts (1)
146-169: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueOptional: linear merge instead of a full sort.
Both inputs are already oldest-first, so this could be a two-pointer merge (O(n)) rather than an O(n log n) sort. With the caps (5000 thoughts + 2000 activities) this runs on every coalesced thinking flush in the panel's
useMemo. Not a correctness issue, and current behavior relies on stable sort which is guaranteed in ES2019+.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/renderer/stores/thoughtStreamStore.ts` around lines 146 - 169, Optionally replace the full sort in buildActivityFeed with a two-pointer linear merge, since blocks and activities are already oldest-first. Compare timestamps and preserve the existing tie-breaker by emitting thoughts before tools, while retaining all current items and ordering behavior.src/__tests__/renderer/hooks/agent/internal/useThoughtStreamToolListener.test.tsx (1)
155-169: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueOptional: cover the window-ownership gate.
Every test here exercises the capturing gate and
parseSessionId, but nothing asserts theuseOwnedSessionGate()early-return that drops broadcast events for agents this window does not own. A test stubbing the gate to reject would lock in that behavior.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/__tests__/renderer/hooks/agent/internal/useThoughtStreamToolListener.test.tsx` around lines 155 - 169, Add a test in the useThoughtStreamToolListener suite that stubs useOwnedSessionGate() to reject the agent/window ownership check, sends a broadcast tool event through toolHandler, and asserts no activity or session buffer is created. Keep the existing capturing and parseSessionId coverage unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/renderer/utils/toolActivityLabel.ts`:
- Around line 66-80: Update todoSummary so malformed array entries cannot cause
exceptions: guard each todo before reading status or label fields, while
preserving completed-count, current-item selection, and fallback label behavior
for valid entries. Keep the function’s existing undefined and task-count outputs
unchanged.
---
Nitpick comments:
In
`@src/__tests__/renderer/hooks/agent/internal/useThoughtStreamToolListener.test.tsx`:
- Around line 155-169: Add a test in the useThoughtStreamToolListener suite that
stubs useOwnedSessionGate() to reject the agent/window ownership check, sends a
broadcast tool event through toolHandler, and asserts no activity or session
buffer is created. Keep the existing capturing and parseSessionId coverage
unchanged.
In `@src/renderer/stores/thoughtStreamStore.ts`:
- Around line 146-169: Optionally replace the full sort in buildActivityFeed
with a two-pointer linear merge, since blocks and activities are already
oldest-first. Compare timestamps and preserve the existing tie-breaker by
emitting thoughts before tools, while retaining all current items and ordering
behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 86ab5dcc-6e47-4ca5-bf11-dbc3468bea94
📒 Files selected for processing (15)
CLAUDE.mddocs/agent-guides/SHARED-UTILS.mddocs/autorun-playbooks.mdsrc/__tests__/renderer/hooks/agent/internal/useThoughtStreamToolListener.test.tsxsrc/__tests__/renderer/hooks/useAgentListeners.test.tssrc/__tests__/renderer/stores/thoughtStreamStore.test.tssrc/__tests__/renderer/utils/toolActivityLabel.test.tssrc/renderer/components/AutoRun/AutoRunnerHelpModal.tsxsrc/renderer/components/RightPanel.tsxsrc/renderer/components/ThoughtStreamPanel.tsxsrc/renderer/global.d.tssrc/renderer/hooks/agent/internal/useThoughtStreamToolListener.tssrc/renderer/hooks/agent/useAgentListeners.tssrc/renderer/stores/thoughtStreamStore.tssrc/renderer/utils/toolActivityLabel.ts
| function todoSummary(value: unknown): string | undefined { | ||
| if (!Array.isArray(value) || value.length === 0) return undefined; | ||
| const todos = value as Array<{ | ||
| content?: string; | ||
| status?: string; | ||
| activeForm?: string; | ||
| step?: string; | ||
| }>; | ||
| const completed = todos.filter((t) => t.status === 'completed').length; | ||
| const current = todos.find((t) => t.status === 'in_progress'); | ||
| const label = | ||
| current?.activeForm || current?.content || current?.step || todos[0]?.content || todos[0]?.step; | ||
| if (!label) return `${todos.length} tasks`; | ||
| return `${label} (${completed}/${todos.length})`; | ||
| } |
There was a problem hiding this comment.
🩺 Stability & Availability | 🟠 Major | ⚡ Quick win
todoSummary can throw on a malformed todo/plan array, violating this module's "never throws" contract.
todos.filter((t) => t.status === 'completed') and todos.find((t) => t.status === 'in_progress') dereference t.status without a null check. The array comes straight from an external provider's tool-call payload (cast from unknown), so a malformed entry like [null, { status: 'completed' }] throws a TypeError here — undermining the explicit "unknown tool still gets a usable line" / never-throws design this file documents and tests (describeToolActivity never throws on a missing name or a null input`, tested for the top-level input but not for malformed array elements).
🐛 Proposed fix
- const completed = todos.filter((t) => t.status === 'completed').length;
- const current = todos.find((t) => t.status === 'in_progress');
+ const completed = todos.filter((t) => t?.status === 'completed').length;
+ const current = todos.find((t) => t?.status === 'in_progress');📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function todoSummary(value: unknown): string | undefined { | |
| if (!Array.isArray(value) || value.length === 0) return undefined; | |
| const todos = value as Array<{ | |
| content?: string; | |
| status?: string; | |
| activeForm?: string; | |
| step?: string; | |
| }>; | |
| const completed = todos.filter((t) => t.status === 'completed').length; | |
| const current = todos.find((t) => t.status === 'in_progress'); | |
| const label = | |
| current?.activeForm || current?.content || current?.step || todos[0]?.content || todos[0]?.step; | |
| if (!label) return `${todos.length} tasks`; | |
| return `${label} (${completed}/${todos.length})`; | |
| } | |
| function todoSummary(value: unknown): string | undefined { | |
| if (!Array.isArray(value) || value.length === 0) return undefined; | |
| const todos = value as Array<{ | |
| content?: string; | |
| status?: string; | |
| activeForm?: string; | |
| step?: string; | |
| }>; | |
| const completed = todos.filter((t) => t?.status === 'completed').length; | |
| const current = todos.find((t) => t?.status === 'in_progress'); | |
| const label = | |
| current?.activeForm || current?.content || current?.step || todos[0]?.content || todos[0]?.step; | |
| if (!label) return `${todos.length} tasks`; | |
| return `${label} (${completed}/${todos.length})`; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/renderer/utils/toolActivityLabel.ts` around lines 66 - 80, Update
todoSummary so malformed array entries cannot cause exceptions: guard each todo
before reading status or label fields, while preserving completed-count,
current-item selection, and fallback label behavior for valid entries. Keep the
function’s existing undefined and task-count outputs unchanged.
Closes #1312
Problem
The Thought Stream captured only
process:thinking-chunk, so it showed an agent's reasoning but never its actions. Tool calls did render in the chat transcript, but only when a tab'sshowThinkingmode was switched on, and the in-chat listener (useAgentToolExecutionListener) matches the streaming id withREGEX_AI_TABalone - so an Auto Run, which spawns as{sessionId}-batch-{ts}, had its tool calls dropped entirely.Net effect for the reporter: no surface anywhere that shows what an agent is actually doing while it works.
This is the follow-up explicitly deferred in #1231 / #1238, where the reasoning half shipped and "a count of background shell commands / running tool calls" was called out as separate work.
What this does
Tool calls now stream into the Thought Stream interleaved with the reasoning, each reduced to ONE short plain-language line:
A spinner marks a call still in flight; a check or a warning marks how it ended. The point is scannability, which is what the reporter's stated motivation needs: cost control - spotting an agent looping or grinding on an unproductive task and interrupting it before it burns more tokens.
Changes
src/renderer/utils/toolActivityLabel.ts(new)describeToolActivity(toolName, input)normalizes tool names across Claude Code (Read/Bash/MultiEdit), OpenCode (lowercaseread/bash), Codex (shell/apply_patch/update_plan), Copilot (write_to_file), and MCP (mcp__server__tool) onto plain English. Unknown tools fall back toUsed <name>rather than being silently dropped, so a new provider tool still shows up. Handles the raw-string input shape (Codexapply_patch) that would otherwise be iterated character-by-character, and argv-array commands.This is deliberately not
summarizeToolInput()- that one builds the verbose in-chat cell (every input key askey=value, untruncated command, output preview). Both are documented inSHARED-UTILS.mdwith a note on which to reach for.src/renderer/stores/thoughtStreamStore.tsactivitiesbuffer +appendToolActivity(). A tool call arrives as two or more events; the completion is merged into itsrunningentry so the feed lists actions, not state transitions. Matched bytoolCallIdwhere the provider sends one, else by newest still-running call with the same tool name in the same tab (the rule the in-chat listener already uses for Codex).MAX_ACTIVITIES_PER_SESSION(2000) with the existingtrimmedflag.buildActivityFeed()merges thought blocks and tool calls into ONE chronological list, so a tool call appears between the two halves of the reasoning that produced it. Ties break toward the tool (a dispatch instant is when the deciding block stops growing).src/renderer/hooks/agent/internal/useThoughtStreamToolListener.ts(new)Taps
process:tool-executionand resolves the id withparseSessionId, so Auto Run-batch-and synopsis spawns are captured too. Same cheapcapturing[sessionId]early-out as the thinking listener - does nothing unless a panel is open or minimized. No rAF coalescing: tool calls arrive per agent action, not per frame. Normalizes provider status wording (error->failed, missing -> still running).ThoughtStreamPanel.tsxRenders tool rows, search matches both the rendered line and the raw tool name, header counts "N thoughts · M actions". Search highlighting is done inline rather than through
<Markdown>- a shell command or glob pattern is not markdown and would be mangled by it.Copy/docs: Right Panel button becomes View Activity; in-app Auto Run help and
docs/autorun-playbooks.mdupdated.Testing
describeToolActivity: 20 cases across all five provider naming styles, plus the never-throws cases ('',null,undefined, array input).appendToolActivity: capture gating, merge-by-id, merge-by-newest-running, no cross-tab merge, start-timestamp preservation, cap/trim, parallel-run isolation.buildActivityFeed: interleaving, tie-break ordering, stable same-timestamp order.useThoughtStreamToolListener:-batch-capture (the case the in-chat listener misses),-ai-tab capture, completion merge, status normalization, not-capturing drop, cross-session isolation, unmount cleanup.useAgentListenersregistration counts updated:onToolExecutionnow has two subscribers, exactly mirroring the existingonThinkingChunkpattern. Added a note at the test mock that last-registration-wins the captured handler, so a future reorder has a breadcrumb.Full suite green: 34,720 passed, 108 skipped.
prettier --check ., all fourtscconfigs, andeslint src/clean.Note on the base branch
Based on
rc, notmain. The Thought Stream does not exist onmainat all -rcis 794 commits ahead and the whole surface this extends is rc-only, somainis not a viable base.Reviewer notes
Summary by CodeRabbit
New Features
Documentation