Skip to content

feat(thought-stream): live plain-language tool-call feed during runs - #1314

Open
pedramamini wants to merge 1 commit into
rcfrom
feat/1312-live-tool-call-activity-feed
Open

feat(thought-stream): live plain-language tool-call feed during runs#1314
pedramamini wants to merge 1 commit into
rcfrom
feat/1312-live-tool-call-activity-feed

Conversation

@pedramamini

@pedramamini pedramamini commented Jul 26, 2026

Copy link
Copy Markdown
Collaborator

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's showThinking mode was switched on, and the in-chat listener (useAgentToolExecutionListener) matches the streaming id with REGEX_AI_TAB alone - 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:

3:42:07 PM  ⟳ Ran npm test
3:42:04 PM  ✓ Read src/renderer/components/ThoughtStreamPanel.tsx
3:42:01 PM  ✓ Searched for THOUGHT_BLOCK_GAP_MS
3:41:58 PM  ! Edited src/renderer/constants/themes.ts

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 (lowercase read/bash), Codex (shell/apply_patch/update_plan), Copilot (write_to_file), and MCP (mcp__server__tool) onto plain English. Unknown tools fall back to Used <name> rather than being silently dropped, so a new provider tool still shows up. Handles the raw-string input shape (Codex apply_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 as key=value, untruncated command, output preview). Both are documented in SHARED-UTILS.md with a note on which to reach for.

src/renderer/stores/thoughtStreamStore.ts

  • activities buffer + appendToolActivity(). A tool call arrives as two or more events; the completion is merged into its running entry so the feed lists actions, not state transitions. Matched by toolCallId where 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).
  • Merged entries keep their start timestamp, so a line doesn't jump to the top of the feed when it finishes.
  • Capped at MAX_ACTIVITIES_PER_SESSION (2000) with the existing trimmed flag.
  • 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-execution and resolves the id with parseSessionId, so Auto Run -batch- and synopsis spawns are captured too. Same cheap capturing[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.tsx
Renders 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.md updated.

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.
  • useAgentListeners registration counts updated: onToolExecution now has two subscribers, exactly mirroring the existing onThinkingChunk pattern. 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 four tsc configs, and eslint src/ clean.

Note on the base branch

Based on rc, not main. The Thought Stream does not exist on main at all - rc is 794 commits ahead and the whole surface this extends is rc-only, so main is not a viable base.

Reviewer notes

  • Capture starts when the panel is opened, so the feed covers activity from that point forward - unchanged from how the thinking capture already behaves, but worth confirming that's the right default rather than always-on capture.
  • The status-pill entry point from feat(thinking): open live Thought Stream from the status pill (#1231) #1238 opens this same panel, so the reporter's screenshot surface gets the feed for free.

Summary by CodeRabbit

  • New Features

    • Auto Run’s Thought Stream now displays a unified, chronological activity feed combining agent thoughts and tool calls.
    • Tool calls show plain-language descriptions, targets, timestamps, and running, completed, or failed statuses.
    • Search now covers both thoughts and tool-call activity, including raw tool names.
    • Renamed the activity viewer action to View Activity and updated related help text.
  • Documentation

    • Expanded guidance for using and understanding the Thought Stream and activity-labeling utilities.

…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
@coderabbitai

coderabbitai Bot commented Jul 26, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The 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.

Changes

Thought Stream Activity

Layer / File(s) Summary
Tool activity labels and normalization
src/renderer/utils/toolActivityLabel.ts, src/__tests__/renderer/utils/toolActivityLabel.test.ts, docs/agent-guides/SHARED-UTILS.md
Adds provider-agnostic labels for file, shell, edit, search, planning, delegation, MCP, and unknown tool calls, with truncation and invalid-input handling.
Activity buffering and chronological feeds
src/renderer/stores/thoughtStreamStore.ts, src/__tests__/renderer/stores/thoughtStreamStore.test.ts
Adds per-session activities, status and tool-call correlation, retention limits, clearing behavior, and buildActivityFeed ordering.
IPC capture and listener wiring
src/renderer/global.d.ts, src/renderer/hooks/agent/internal/useThoughtStreamToolListener.ts, src/renderer/hooks/agent/useAgentListeners.ts, src/__tests__/renderer/hooks/agent/internal/useThoughtStreamToolListener.test.tsx, src/__tests__/renderer/hooks/useAgentListeners.test.ts
Subscribes to tool execution events, scopes them to owned captured sessions, normalizes statuses, and routes them into the store.
Unified activity UI and documentation
src/renderer/components/ThoughtStreamPanel.tsx, src/renderer/components/RightPanel.tsx, src/renderer/components/AutoRun/AutoRunnerHelpModal.tsx, docs/autorun-playbooks.md, CLAUDE.md
Displays interleaved thoughts and tool calls with status indicators, search matching, counts, updated labels, and expanded file references.

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
Loading

Possibly related PRs

Suggested reviewers: reachrazamair

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the main change: a live plain-language tool-call feed during runs.
Linked Issues check ✅ Passed The PR implements the requested live, plain-language tool-call feed during runs and matches the issue’s capture and concise status requirements.
Out of Scope Changes check ✅ Passed The changes stay focused on the thought-stream activity feed, supporting labels, docs, and tests, with no obvious unrelated additions.
Docstring Coverage ✅ Passed Docstring coverage is 85.71% which is sufficient. The required threshold is 80.00%.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/1312-live-tool-call-activity-feed

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.

@greptile-apps

greptile-apps Bot commented Jul 26, 2026

Copy link
Copy Markdown

Greptile Summary

Adds a live tool-call feed to the Thought Stream.

  • Captures tool lifecycle events for interactive, Auto Run, and synopsis streams.
  • Converts provider-specific tool calls into concise plain-language labels.
  • Merges tool actions with reasoning blocks in the searchable activity panel.
  • Updates tests, renderer API types, UI copy, and documentation.

Confidence Score: 3/5

The 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

Filename Overview
src/renderer/stores/thoughtStreamStore.ts Adds tool activity storage, lifecycle merging, retention, and combined feed construction, but tools cannot be correctly positioned inside a grouped thought block.
src/renderer/hooks/agent/internal/useThoughtStreamToolListener.ts Adds ownership-scoped capture of raw tool events with session parsing, status normalization, and store dispatch.
src/renderer/utils/toolActivityLabel.ts Adds defensive provider-normalized plain-language labels for tool inputs.
src/renderer/components/ThoughtStreamPanel.tsx Renders searchable tool rows alongside reasoning and displays action counts and lifecycle status.
src/renderer/hooks/agent/useAgentListeners.ts Registers the new Thought Stream tool listener with the existing agent listener composition.

Sequence Diagram

sequenceDiagram
    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
Loading

Reviews (1): Last reviewed commit: "feat(thought-stream): live plain-languag..." | Re-trigger Greptile

Comment on lines +164 to +168
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;
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/renderer/stores/thoughtStreamStore.ts (1)

146-169: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Optional: 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 value

Optional: cover the window-ownership gate.

Every test here exercises the capturing gate and parseSessionId, but nothing asserts the useOwnedSessionGate() 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

📥 Commits

Reviewing files that changed from the base of the PR and between 2aaf7a4 and 0ca6b4b.

📒 Files selected for processing (15)
  • CLAUDE.md
  • docs/agent-guides/SHARED-UTILS.md
  • docs/autorun-playbooks.md
  • src/__tests__/renderer/hooks/agent/internal/useThoughtStreamToolListener.test.tsx
  • src/__tests__/renderer/hooks/useAgentListeners.test.ts
  • src/__tests__/renderer/stores/thoughtStreamStore.test.ts
  • src/__tests__/renderer/utils/toolActivityLabel.test.ts
  • src/renderer/components/AutoRun/AutoRunnerHelpModal.tsx
  • src/renderer/components/RightPanel.tsx
  • src/renderer/components/ThoughtStreamPanel.tsx
  • src/renderer/global.d.ts
  • src/renderer/hooks/agent/internal/useThoughtStreamToolListener.ts
  • src/renderer/hooks/agent/useAgentListeners.ts
  • src/renderer/stores/thoughtStreamStore.ts
  • src/renderer/utils/toolActivityLabel.ts

Comment on lines +66 to +80
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})`;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 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.

Suggested change
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.

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.

1 participant