Repository navigation
Agents panel: see what subagents are doing, plus Tasks tool support - #324
Merged
Merged
Conversation
Two displays for work happening outside the main transcript. Agents panel (new `agents` right-column panel). `collectAgents` in `shared/state/json-claude-agents.ts` derives every sub-agent a session launched — synchronous or background, at any nesting depth — from entries + backgroundAgents. Nothing new goes over the wire. The panel lists them running-first with a live "currently" line, elapsed time and status; clicking one opens a peek flyout with its whole activity feed, following the tail as it streams, and nested agents link through. A dead agent is reported as `interrupted` rather than left spinning: a sync sub-agent can't outlive its parent's turn, so no result once the turn is over means the run was cut short. Tasks tool (TaskCreate/TaskUpdate). Ness only knew TodoWrite, so a session driving the newer Tasks tool showed an empty Todos panel and raw JSON in the transcript. Unlike TodoWrite these mutate one task at a time, so `collectTasks` replays the calls instead of reading the last one — recovering ids from the create results, mirroring the blocks/blockedBy graph, and surfacing owner and activeForm. The Todos panel renders Tasks when present and falls back to TodoWrite; keeping the `todos` panel key means saved per-repo panel config still applies. Both panels derive off entry count rather than session identity, which churns per streamed token — a full walk per token is CLAUDE.md anti-pattern #4. The agents panel adds a 500ms tick for in-place streaming updates, gated on something actually being live. 41 new reducer-level tests; full suite green.
…panel The agents list is never purged — it grows for the life of a session — and the panel re-derives on a 500ms tick while anything is live. So every tick was rebuilding the step feed of every agent the session ever launched, including ones that finished long ago and can never change. Allocation churn rather than a leak, but it scales with session length and it's the renderer-side GC churn main-process telemetry can't see. collectAgents now takes the caller's previous result and hands settled agents straight back by reference, skipping their step rebuild and keeping object identity stable so React skips those rows too. Reuse is refused for a settled agent whose result hasn't landed yet, because task_notification dispatches the status and the real summary as two events — caching in between would pin the launch stub as the report forever. The approval-bubbling pass no longer assigns onto summaries in place: a reused entry is the same object the caller still holds, so mutating it would write through to their copy. It computes a set and rebuilds only the entries whose value actually changed. This is a live path, not a hypothetical — a background child outlives its parent's turn, so a finished parent can still acquire a pending approval from below. Panel-side, finished agents now collapse behind a "show N more" toggle after the first five. They're history — the transcript keeps their Task cards — and dozens of them would push the live agents the panel exists to surface off the top.
…gent list Claude Code emits system/task_started for EVERY sub-agent, synchronous ones included. Both the Agents panel and the transcript's Task card treated the presence of that lifecycle record as proof of detachment, so every single agent got a "background" badge — verified against a session whose seven Agent calls all passed no run_in_background at all. On the Task card it also drove the spinner off the wrong lifecycle. Only the caller's own run_in_background decides it now. The record is still used for usage totals and completion time, which means synchronous agents gain token/tool/duration figures they never showed. The panel also listed nested agents inline, indented. Sorting is by status then recency, so a child routinely sorted *above* its parent: an indented row pointing at nothing, promising a tree the ordering doesn't deliver. The list is flat now — top-level agents, plus anything running at any depth so nothing live is ever hidden — and a parent shows a nested count instead. A running nested agent names its parent on its secondary line. Full hierarchy stays in the peek, which links through. Also: rows carry a tooltip explaining what the bare duration means, and the header reads "N running" / "N done" rather than an unlabelled count.
Clicking a row now toggles it open with a ▸/▾ disclosure, matching the idiom ToolGroup, TaskCard and ContextPanel already use, instead of jumping straight to the modal. Expanded shows the tail of the agent's step feed, its report once finished, and a link through to the full peek — which is still where the whole feed and the parent→child hierarchy live. Several rows can be open at once, since comparing two running agents side by side is the main reason to open one. The panel's max height grows while anything is expanded; an expanded row is about twelve lines and at the collapsed height it would be a scrollbar showing three. The collapsed "currently" line stays for running agents, so the panel still reads at a glance, and is dropped when expanded because the feed below ends on that same step.
The chevron now reveals an agent's nested agents rather than its step feed, and clicking the row still opens its history in the peek — the file-tree split, where disclosure is structure and the label is the thing. Replaces the previous reading, where ▸ expanded an inline step list and the peek had to be reached through a link. This also retires the flatten-the-list workaround. Nested agents were being hoisted to the top level because sorting by status then recency put a child above its parent, leaving an indented row pointing at nothing. Rendering an actual tree means a child sits under its parent by construction, so the indent finally means what it looks like, and nothing has to be hidden to keep the ordering honest. buildAgentTree regroups the flat result, preserving running-first order within each sibling group and promoting an orphan (parent truncated out of the buffer) to a root rather than dropping it. It also reports which agents have a running descendant: those default to open so a live agent is never buried in a collapsed parent, with the user's own click overriding that — recorded as overrides rather than as the state, so auto-expand can't fight someone who deliberately closed a row. Only top-level rows are capped by "show N more finished". A nested one is revealed by its parent's chevron, so hiding it behind a second cap would make the tree lie about what it contains.
Running agents showed a green tick. Claude Code runs EVERY sub-agent detached — verified across two live sessions, where all 24 Agent calls resolved immediately with "Async agent launched successfully" and not one passed run_in_background. There is no synchronous path: the tool_result is always a stub, superseded later by the real answer on task_notification. The previous commit keyed status off run_in_background, so with that flag absent every agent took the result-based branch and the stub read as a successful result — complete the instant it launched. These are two questions with two sources, and conflating them has now broken this in both directions: did the user ASK for detachment -> run_in_background (the badge) has the agent FINISHED -> task_* lifecycle (the status) Status comes from the lifecycle record whenever there is one, falling back to the tool_result only when there isn't — a build predating the records, a seeded transcript, or a launch that failed outright (unknown subagent_type) and so never started a task. The stub is recognised and dropped, so it can neither settle an agent nor be rendered as its report. Same fix in TaskCard, which had both bugs too. Also: `interrupted` is no longer cacheable by the reuse path. It's inferred from !busy rather than reported, so caching it froze an agent that way permanently instead of letting it return to running.
The Todos panel stopped appearing. Keying its memo on entries.length assumed a tool call always appends an entry — it doesn't. assistantBlockAppended adds the tool_use block to the assistant entry already in flight, so a TodoWrite / TaskCreate call lands without the array growing and the panel never re-derives. It sat empty for the whole turn. Before memoization it derived every render, which is why this only broke when the memo went in. The Agents panel had the identical flaw, hidden by its 500ms tick papering over it within half a second. Both now share useSessionRevision, which keys on a constant-time signature — session id, entry count, and the last entry's id and block count — so an appended block registers immediately. The slow poll stays as a backstop for mutations the signature can't see: a tool_result patched into an existing block, or a block appended to an entry that isn't last because a sub-agent is interleaving. Those now land within half a second rather than never. Walking the entries to hash them properly would cost exactly what the memo exists to avoid, hence a signature rather than real change detection, with the poll covering the gap.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Chat mode had no way to see what sub-agents were up to. A session that fans out a dozen of them showed a dozen collapsed Task cards scattered through the transcript, and once you scrolled past, nothing told you what was still running.
Agents panel
A new
agentsright-column panel.collectAgents(shared/state/json-claude-agents.ts) derives every sub-agent a session launched, at any depth, fromentries+backgroundAgents— a derived selector, so nothing new goes over the wire for a second client.▸/▾chevron reveals nested agents under their parent; clicking the label opens that agent's history in a peek flyout. Rows with a running descendant default to open so a live agent is never buried, with the user's click overriding that — stored as an override rather than as the state, so auto-expand can't reopen a row someone deliberately closed.Tasks tool
Ness only knew
TodoWrite, so a session driving the newer Tasks tool showed an empty Todos panel and raw JSON in the transcript. UnlikeTodoWrite,TaskCreate/TaskUpdatemutate one task at a time, socollectTasksreplays the calls instead of reading the last one — recovering ids from the create results, mirroring theblocks/blockedBygraph, and surfacingownerandactiveForm. The panel renders Tasks when present and falls back to TodoWrite; keeping thetodospanel key means saved per-repo panel config still applies.TaskCreate/TaskUpdatealso get a real card instead ofGenericToolCard.Two bugs this surfaced in existing code
Both came from conflating two questions that have two different sources. Every sub-agent runs detached — verified across two live sessions, where all 24
Agentcalls resolved immediately with"Async agent launched successfully"and not one passedrun_in_background.run_in_backgroundBGbadgetask_started/task_notificationTaskCardkeyed the badge off the presence of a lifecycle record. Claude Code emitstask_startedfor every sub-agent, so every Task card was labelled "background".Performance
The agents list is never purged — it grows for the life of a session — and the panel re-derives on a 500 ms tick while anything is live, so every tick was rebuilding the step feed of agents that finished long ago.
collectAgentsnow takes the caller's previous result and hands settled agents back by reference, which also keeps their object identity stable so React skips those rows. Reuse is refused for an agent whose report hasn't landed yet (task_notificationsends status and summary as two events), and forinterrupted, which is inferred from!busyrather than reported.Both panels derive off entry count rather than session identity, which churns per streamed token — a full walk per token is CLAUDE.md anti-pattern #4.
Verification
npm run typecheck,npx electron-vite buildandnpx vitest runall clean — 1817 tests, 59 new, covering nesting depth, orphan promotion, sibling ordering, running-ancestor marking, stub handling, the reuse cache and its mutation hazards, and the Tasks replay.Exercised in dev against live sessions, which is how the badge, status and tree-ordering bugs were found. The reducer-level logic is well covered; the React components have no automated tests (this repo has no component-test setup), so the panel bodies rest on manual use.