Skip to content

feat(coc): Agents canvas — spatial sub-agent run tree for chat#326

Merged
plusplusoneplusplus merged 11 commits into
mainfrom
coc/agents-canvas
Jun 14, 2026
Merged

feat(coc): Agents canvas — spatial sub-agent run tree for chat#326
plusplusoneplusplus merged 11 commits into
mainfrom
coc/agents-canvas

Conversation

@plusplusoneplusplus

Copy link
Copy Markdown
Owner

Summary

Adds an Agents view to the CoC chat: a pannable / zoomable spatial tree of a chat's recursive sub-agent runs, toggled from a Thread | Agents switch in the chat header. Ported from the coc-chat design handoff. The existing thread rendering is untouched — this is purely additive (a second view + a toggle).

All new UI lives under packages/coc/src/server/spa/client/react/features/chat/agent-canvas/. Distinct from the existing co-edited CanvasPanel side panel.

What's included (11 commits)

Core view

  • Thread | Agents segmented toggle in ChatHeader (new optional viewToggle slot); ChatDetail swaps the conversation row for <AgentCanvas> in Agents mode and hides thread-only flow cards. Thread untouched.
  • AgentCanvas — curved SVG edges + node cards (role glyph, name, live elapsed, spawn-count pill, status dot, progress bar). Pan/zoom via the shared useZoomPan hook.
  • Pure, tested data + layout layer: buildLayout/edgePath/spineVars (ported 1:1) and buildAgentRunTreeFromTurns, which derives the tree from the conversation's Task tool calls — no extra fetch.

Refinements (from review feedback)

  • Default view is 100% zoom, centered (new useZoomPan.centerContent); Fit button still fits the whole tree.
  • Toolbar % is a zoom-preset menu (25/50/75/100/150/200% + Fit) via new useZoomPan.zoomTo (zooms about the viewport center).
  • Clicking a sub-agent opens an inspector with its name / type / model / mode, task prompt, result, and children (drill-in); clicking the root closes it. An "Open in thread" button preserves jump-to-turn.
  • Deep-link: the view rides as ?view=agents on the chat hash, read on mount and written on toggle, so a shared/bookmarked URL opens straight into Agents.

Fixes folded into the series

  • Sub-agents persist after a chat completes — the persisted forge read model uses name/parameters (vs live toolName/args); the adapter now reads both.
  • Keep the non-empty-args snapshot when deduping — a terminal tool-complete often carries empty args, which was wiping out name/model/type.
  • Router strips a trailing ?query before parsing the path, so …/activity/<taskId>?view=agents no longer parses the query into the task id.

Why

The chat couldn't visualize recursive sub-agent runs. This makes the orchestrator → sub-agent tree legible at a glance and drillable, building on the existing chat UI as the design requested.

Testing

  • ~76 new unit/render tests (layout geometry, the data adapter incl. persisted/empty-args shapes, chatViewHash, useZoomPan center/zoomTo, AgentCanvas, AgentInspector, ChatViewToggle, Router query-strip).
  • npm run build:client passes; 0 new tsc errors (pre-existing client-tsconfig baseline unchanged) and 0 lint errors in changed files.
  • Verified against a live "run 3 subagents" chat: nodes resolve to time-agent-1/2/3 · explore · claude-sonnet-4.6 · background.

Reviewer notes

  • The tree is currently depth-1 (the current conversation's Task calls); AgentRunNode supports arbitrary depth, so deeper recursion (via child processes) can be layered on later without touching the canvas.
  • The prototype's clock scrubber/replay was intentionally dropped — the real app is live-streaming.
  • Knowledge doc dashboard-spa.md updated; no changeset added (repo has no active per-PR changeset convention).

🤖 Generated with Claude Code

plusplusoneplusplus and others added 11 commits June 13, 2026 13:50
Foundation for a spatial "Agents" view of a chat's recursive sub-agent
runs (ported from the coc-chat design). Pure, UI-free modules so the
layout math and data adapter are unit-tested in isolation:

- types.ts — AgentRunNode tree (orchestrator root → sub-agent runs).
- layout.ts — tidy left→right tree layout (buildLayout), curved edge
  paths (edgePath), and per-depth spine colors (spineVars), ported
  verbatim from the prototype so spatial output matches.
- buildAgentRunTree.ts — adapts real conversation turns into the tree:
  the orchestrator is the root, each `Task` tool call becomes a
  sub-agent child (name/role from args, status/timing from the call),
  deduped across toolCalls + timeline and ordered by start time.

Tests cover layout geometry/edges/spine cycling and the adapter's
field mapping, prompt-name fallback, dedup, root-status derivation,
and ordering. No UI is wired yet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The pannable/zoomable spatial tree view, ported from the coc-chat
design and adapted to the app:

- AgentCanvas.tsx — renders the run tree from buildLayout: curved SVG
  edges (animated for running, dashed for queued) + absolutely-placed
  HTML node cards. Each card shows a role glyph, name, role · live
  elapsed, a spawn-count pill, a status dot, and a progress bar. Pan/
  zoom is delegated to the shared useZoomPan hook; auto-fit on mount
  and on resize until the user takes over, with a re-arming Fit button.
  A live 1s clock ticks running nodes' elapsed time; an empty state
  shows when the chat spawned no sub-agents.
- icons.tsx — line-icon set (roles, spawn, zoom, view nav) ported from
  the design, plus a keyword→role glyph picker for free-form agent types.
- agent-canvas.css — design's canvas styles, namespaced under
  `.agent-canvas` with light/dark token sets (dark via `.dark`).
- index.ts — barrel for the feature.

Render tests cover node rendering, the spawn pill, empty state,
selection highlight, onSelect, the zoom toolbar, and status flags.
Not wired into the chat yet.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds a Thread | Agents segmented toggle to the chat top bar and renders
the sub-agent canvas as a second view, leaving the thread untouched.

- ChatViewToggle — segmented control (Thread / Agents) styled with the
  app's light/dark tokens; exposed through a new optional `viewToggle`
  slot on ChatHeader.
- ChatDetail — owns `view` state (resets to thread on chat switch).
  Builds the agent tree from the loaded turns via
  buildAgentRunTreeFromTurns and, in Agents mode, swaps the
  ConversationArea inner row for <AgentCanvas>, keeping the composer/
  scratchpad and hiding thread-only flow cards (Ralph start, Implement
  plan). The toggle is hidden in the floating variant and while
  loading/pending.
- Node click → findTurnIndexForRun maps the run back to its issuing
  turn, switches to the thread, and scrolls to it.

Tests: findTurnIndexForRun (toolCalls/timeline/turnIndex/missing) and
ChatViewToggle (render, aria-pressed, onChange). build:client passes;
no new tsc/lint errors. Knowledge doc (dashboard-spa.md) updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Live (SSE) tool calls carry `toolName`, but the persisted forge read
model (loaded by refreshConversation after completion) carries `name`
(and may use `parameters` instead of `args`). The agent-tree adapter
read `tc.toolName` directly, so once a chat finished and its turns were
refreshed from the server, `Task` calls were no longer recognized and
every sub-agent vanished from the canvas — leaving just the root.

Detect the tool name via `toolName ?? name` and read args via
`args ?? parameters`, so sub-agents render identically mid-run and after
completion. Tests cover the persisted (`name`) shape and `parameters`
fallback.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The canvas opened auto-fit (scaled down to fit the whole tree). Default
to 100% zoom with the content centered in the viewport instead, so nodes
render at full size on open and the user pans to explore.

- useZoomPan: add `centerContent(scale = 1)` — sets the given scale and
  centers content in the container (additive; existing consumers
  unaffected).
- AgentCanvas: use centerContent(1) for the default/auto view (mount,
  tree growth, resize) until the user takes over. The Fit button still
  zooms to fit the whole tree and now sticks (counts as an interaction)
  rather than re-arming auto-center.

Tests cover centerContent (centering math, no-container no-op, scale
clamping).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Thread/Agents view is now shareable/bookmarkable. In the main inline
chat context the active view rides as a `?view=agents` query param on the
chat hash (`#repos/<ws>/<tab>/<taskId>?view=agents`):

- chatViewHash.ts — pure read/apply helpers for the `view` param
  (preserve path, leading #, and other params).
- ChatDetail — initializes `view` from the hash on mount (so a shared
  URL reopens straight into the canvas) and mirrors it back via
  history.replaceState on toggle. Gated to variant 'inline' &
  non-standalone. The per-chat reset honors a deep-linked view on first
  mount (ChatDetail is keyed per task, so the initializer re-reads the
  hash for each chat).
- Router — `parseActivityDeepLink` strips a trailing `?query` before
  extracting the taskId, so the new param never corrupts chat routing.

Tests cover chatViewHash (read/apply/round-trip, param preservation) and
the Router taskId query-strip. dashboard-spa.md updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The toolbar's % label is now a dropdown of preset zoom levels
(25/50/75/100/150/200%) plus "Fit to screen". Picking a level zooms
about the viewport center.

- useZoomPan: add `zoomTo(scale)` — zooms to an exact scale while
  keeping the world point under the viewport center fixed (additive).
- AgentCanvas: the % becomes a menu button; the active level is marked;
  the menu closes on pick or outside click. Picking a level counts as a
  user interaction (stops auto-centering).

Tests cover zoomTo (center-fixed math, clamping, no-container) and the
menu (opens from %, lists presets + Fit, closes on pick).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Clicking a sub-agent node now opens a detail panel instead of jumping
back to the thread.

- AgentInspector — right-side panel showing the run's role, status,
  elapsed/duration, the task prompt it was handed, its result/conclusion,
  and its child runs (clickable to drill in). An explicit "Open in
  thread" button preserves the jump-to-turn action.
- AgentCanvas owns selection internally: clicking a sub-agent opens the
  inspector and highlights the node; clicking the orchestrator root or
  the close button dismisses it; the legend hides while it's open. New
  `onOpenInThread` prop replaces the old `onSelect`/`selectedId` props.
- buildAgentRunTree now captures each run's `prompt` (task) and `result`
  for the inspector; duration helpers handle a 0 timestamp.
- ChatDetail passes `onOpenInThread` (the former jump-to-turn handler);
  the internal selected-agent state is gone.
- icons: add Clock + X.

Tests cover the inspector (status/duration/task/result, child drill-in,
close, root has no result/open-in-thread) and the canvas wiring (open on
sub-agent click, highlight, close on root/close, onOpenInThread).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Task tool's args carry richer fields than were surfaced. Capture and
display them:

- buildAgentRunTree: read `name` (the agent's name) as the node title
  (falling back to description/prompt), plus `model`, `mode`, and keep
  `description` when it adds something beyond the name.
- AgentInspector: add a details list (Model / Mode / Summary) under the
  status meta. Type is the role shown in the header, name is the title.

So a node like { name: "time-agent-1", agent_type: "explore",
model: "claude-sonnet-4.6", mode: "background", description: "Query
current time" } now reads clearly in the panel.

Tests cover the new arg capture (name title, model/mode/description) and
the inspector rendering them.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… args

Each Task call appears three times in a completed conversation: toolCalls
(full args), timeline tool-start (full args), and timeline tool-complete
(same terminal status, but EMPTY args). The dedup preferred the latest
equal-rank snapshot — the empty-args tool-complete — so every node fell
back to "sub-agent"/"agent" with no model/mode.

collectToolCalls now keeps whichever snapshot has non-empty args
(nonEmptyArgs helper), so the name/type/model/mode survive. Verified
against the live "3 subagents" chat: nodes now resolve to
time-agent-1/2/3 · explore · claude-sonnet-4.6 · background.

Regression test replicates the three-snapshot shape.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Router's hashchange handler split the raw hash into path parts
without removing a trailing `?query`, so a deep-link like
`#repos/<ws>/activity/<taskId>?view=agents` parsed the task id as
`<taskId>?view=agents` — the wrong id, which broke landing directly on
the Agents view (and any other `?`-carrying chat deep-link).

Strip the query once where the handler reads the hash, so every routed
segment (repoId, sub-tab, taskId) is clean. The query stays on
`location.hash`, so components that read it directly (ChatDetail's
`?view`, TaskPreview's `?mode`) still work. Mirrors the existing
`parseActivityDeepLink` query-strip (already tested).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@plusplusoneplusplus plusplusoneplusplus merged commit fefa37b into main Jun 14, 2026
36 checks passed
@plusplusoneplusplus plusplusoneplusplus deleted the coc/agents-canvas branch June 14, 2026 04:38
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