Skip to content

Delegated activity in the TUI, and a browser view that paints the same cards - #4

Open
grippado wants to merge 7 commits into
mainfrom
feat/subagent-workflow-activity
Open

grippado wants to merge 7 commits into
mainfrom
feat/subagent-workflow-activity

Conversation

@grippado

Copy link
Copy Markdown
Owner

What

Two things that turned out to be one thing.

Delegated activity (RFC 0004 + RFC 0005). The Claude adapter was parsing away session fields it already had on disk: subagents spawned via Task, multi-agent workflows, session lineage, entrypoint, agent type. Those now reach the cards, which render delegated work as sub-rows under the session that spawned it, with aggregated tree burn and a border that means something.

A browser view over the same board (RFC 0006). aitop --serve exposes a read-only SPA. It shipped with its own buildCards/sortCards in JavaScript, a second implementation of the Go join, and the copy had already drifted. internal/webserver now calls cards.BuildCards / cards.Sort / cards.NestByParent, the exact pipeline ui.Model.orderedCards runs, and the frontend became a paint layer. Whatever the TUI gains, the browser gains with it, structurally, which is how the delegated sub-rows above showed up in the browser without being written twice.

Contract

cards.Card's JSON tags are now public API, and they encode the honesty invariant:

  • a field whose zero means nothing was read is absent, so the renderer paints a dash;
  • a field whose zero is a real reading is always present: the gates themselves, token and cost figures, proc counts, depth. A rate limit is a pointer, so nil (no limit reported) and a present 0 do not collapse.

TestCardJSONContract pins both halves, and lives next to the struct so an edit that never runs the webserver's tests still trips it.

tool and sort moved to the query string, because both have to be resolved server-side: the representative session that absorbs a tool's unattributable processes is picked among the filtered sessions, and NestByParent needs parent-before-child ordering. Raw Sessions and Usage left the payload; they only fed the join that no longer happens.

Invariants held

Read-only, GET only, default bind 127.0.0.1. No new dependencies and no cgo: internal/ui/panes/cards pulls lipgloss, already a dependency, and not the Bubble Tea program tree.

Test

go build ./... and go test ./... green on macOS.

grippado and others added 7 commits July 9, 2026 23:07
Draft-only prep. Propose surfacing in-conversation Task subagents (context-keeper,
code-reviewer, etc.) as nested sub-rows under their parent session's card, sourced
from ~/.claude/projects/<cwd>/<parentSessionId>/subagents/agent-<id>.jsonl (model,
tokens, type/desc, running-vs-done). No PID/session file, so still never a
top-level card. Also scopes correcting RFC 0003's now-inaccurate known-gap note
("only inline in parent transcript" -> separate subagents/ file on Claude Code 2.1.x).

Not implemented; left as a starting point to continue later.

Co-Authored-By: Claude <noreply@anthropic.com>
…we were parsing away

Three layers the adapter could already reach and didn't:

- Layer 0: sessions/<pid>.json carries name/nameSource/jobId/parkedJobId/
  agent/entrypoint/version/startedAt. The jobId <-> parkedJobId pair is a
  file-backed lineage edge, stronger than RFC 0003's PPID walk.
- Layer 1 (RFC 0004): projects/<cwd>/<sid>/subagents/agent-*.jsonl plus the
  .meta.json sidecar, which is where type/description actually live.
- Layer 2 (RFC 0005): subagents/workflows/wf_*/ with journal.jsonl — the only
  source that makes running vs done a reading instead of an mtime guess — and
  the workflow script's own `export const meta`.

Per agent: model, effort, real cumulative burn (input+output, the only
additive figure), cwd, branch, last action, and the span from the transcript's
first/last timestamp. The opening timestamp is persisted in the tracker
because the reader tails by offset and would otherwise lose the head on the
second pass.

Honesty guards that cost real findings to get right: an interrupted agent is
its own state (the harness writes the turn, so it is read, not inferred); a
dead owner degrades to "" rather than to an invented "done"; a symlinked wf_*
dir belongs to the session that owns the bytes, which dedups the parked pair
and closes a symlink escape past the segment guard; and hostile sessionId/cwd
now route through one path choke point.

Co-Authored-By: Claude <noreply@anthropic.com>
… means something

The card grows a block below its footer: one ◇ row per subagent, one ⧉ row per
workflow with its description and phases, and one row per workflow agent with
model, context, burn, span and state. Cards became variable-height in LIST, so
CardHeightOf() is now the source of truth and model.go sums it instead of
multiplying by CardHeight.

A bg card whose own transcript is empty no longer reads as an empty card: it
shows the burn aggregated from the tree hanging under it, in its own labelled
block, never merged into the own-transcript reading. TreeLinked names the
session the tree belongs to, including in GRID where there are no sub-rows to
carry the attribution.

The border animates a purple gradient while — and only while — a workflow
agent is genuinely running per its journal. Liveness outranks selection, which
moved to a marker in the header, because making the card under the cursor the
one card that can never animate hid the effect exactly where it was wanted.
Truecolor ramp with an ANSI-256 fallback, frame passed in so the render stays
pure and testable.

A settled workflow folds to its headline and description at rest, with any
interruption promoted onto that line so a fold can never lose bad news;
selecting the card still opens everything. J/K scroll inside a selected card,
because a card taller than the viewport had an unreachable tail.

Co-Authored-By: Claude <noreply@anthropic.com>
…nce claims

Records what the three implementation rounds found on disk, including the parts
that must stay "—" and why: the agent label and the per-phase progress are
runtime-only (every .meta.json of every recent run, one of them mid-flight,
carries nothing but agentType/spawnDepth, and journal.key is a one-way hash),
and using the prompt's opening line as a label was tried and rejected because
agents of one workflow share the preamble.

Also corrects the claim in ADAPTER.md that a sidechain "exists only as
isSidechain:true lines inside the parent's transcript". On 2.1.220 the parent
transcript holds zero such lines — the data lives in separate files. Kept as a
recorded lesson rather than a silent edit, same as the 0/204 fix in 0003.

Co-Authored-By: Claude <noreply@anthropic.com>
Merges the unmerged browser branch, which forked from the same base as this
one. No file overlaps: the web view lives entirely in internal/webserver/,
plus the --serve/--addr flags in main.go.

The RFC it shipped as 0004 collides with 0004-subagent-activity, which is
already cross-referenced from ADAPTER.md, the README and code comments — so
the web view is renumbered to 0006 in the following commit.

The frontend is still a hand-written JS port of the pre-0004 BuildCards, so it
renders none of the activity this branch added. Serving the Go-built cards is
the next step.

Co-Authored-By: Claude <noreply@anthropic.com>
Both branches independently claimed 0004. The subagent-activity RFC keeps the
number: it is already cross-referenced from the README, ADAPTER.md, RFC 0005
and code comments, so moving it would break more links than it fixes.

Co-Authored-By: Claude <noreply@anthropic.com>
The browser view had its own buildCards/sortCards in JavaScript, a second
implementation of the Go join that had already drifted from the original.
internal/webserver now calls cards.BuildCards / cards.Sort / cards.NestByParent
- the exact pipeline ui.Model.orderedCards runs - and emits sorted,
parent-before-child cards. The frontend became a paint layer, so it inherits
delegated activity (subagent and workflow sub-rows, tree burn) for free.

Card's JSON tags are now a public contract encoding the honesty invariant: a
field whose zero means "nothing was read" is absent so the renderer paints a
dash, and a field whose zero is a real reading (gates, token and cost figures,
proc counts, depth) is always present. TestCardJSONContract pins both halves
next to the struct.

tool and sort moved to the query string because both have to be resolved
server-side: the representative session that absorbs a tool's unattributable
processes is picked among the filtered sessions, and NestByParent requires
parent-before-child ordering. The raw Sessions and Usage arrays left the
payload - they only fed the client-side join.

Co-Authored-By: Claude <noreply@anthropic.com>
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