0 · The point of the feature: what is running, under the spinner, collapsed
Nothing expanded. The exchange is closed, the pane's own working line says it is busy, and under it is every sub-agent this turn started — the ones still going and the ones that have landed. This is the running app, shot on the branch.

The rails are the file browser's. Rails moved out of FilesPane.tsx into its own component and both use it, with the six lines of .rails/.rail CSS untouched — one cell per depth, lines drawn with pseudo-elements, └ on the last child. Two details decide whether it reads as a tree: the rail cell is the width of the mark cell the working line's spinner hangs in, so the tree descends from that spinner rather than from the text column; and the elbow closes on whatever is actually last.

└ — a ├ at the bottom pointing at nothing reads as a row that failed to render.| decision | what it does | why |
|---|---|---|
| the strip is the unit | a finished sub-agent stays visible until the LAST one lands; then the whole strip goes and they are in the collapsed work | one appearance and one disappearance per turn, instead of one per sub-agent — the list a glance reads does not rearrange itself |
| no linger, no fade | deleted: 40 lines, a ref, a timer, a keyframe and a .leaving class | they existed to soften rows leaving one at a time, which no longer happens |
| capped at ten | over the cap the FINISHED rows give way, oldest first; the line says "…and 12 more — 12 done" | rows accumulate now: 22 in release-video's busiest turn, 40 in the-gatherer's. The strip's height stops growing instead of resizing on every completion, and "12 more running" would be a lie in that state |
| 15-minute silence rule | the mark goes inert and the row reads no write in 15m | the measured worst silence inside a run that finished normally is 601s; nine of release-video's 22 sub-agents have no outcome at all and would otherwise spin forever |
| the pane gates the word | if the pane is not running, nothing under it says "running" — and the strip is not drawn at all | a killed pane leaves sub-agents that never finish and never write again |
when the last one lands

8 sub-agents — and the rows are in the work.
Does that jump the reader? Measured, not argued: with the reader scrolled 400px up from the bottom, the strip's 164px of rows disappearing left scrollTop at 2578 and the reference prompt band on the same pixel. The strip is the last thing in the last exchange, so nothing above it can move; pinned to the bottom, the content slides down by the removed height and stays pinned — what already happens when the working line goes.
and the nested trace is a trace
The rail, the clamped task panel, the "show the whole task" button and the dashed state pills are deleted. What renders inside an opened row is an ordinary exchange: the task in the prompt band the reader already draws for a user prompt — because that is what it is — then steps, tools and timings exactly as at the top level. The step body's indent is the hierarchy; nothing else marks it.


Shot against a live pane whose transcript is a fixture in the real record shapes, so several states appear in one turn; the sidecars, the child transcripts, the descriptions and the notification numbers are real, and the pane really is running. One bug fell out of building it: the roster that supplies wrote 4m ago and the stalled state was fetched by an effect keyed on the pane's liveness, and every request was cancelled by its own cleanup before the answer landed. It lives in a module-level cache now.
0b · Codex, same rows, different plumbing
The first cut of this feature excluded Codex — one sub-agent thread on the machine to test against, and no terminal event found. Both halves of that are now false: a real run produced a parent and three children, and Codex records a completion event after all. The reader treats them exactly like Claude's, because the operator should not have to know which CLI produced a sub-agent.


exec calls and the three wait_agent calls that follow, where they happened.
3 steps · 10 tools · 1m 18s · 7.6k tok — and its own report.This was a bug, and it was worth the operator catching it. Codex spawns a sub-agent with fork_turns: "all", so the child's rollout opens with the parent's entire conversation copied into it and only then the task it was given. Rendered whole — which is what the reader did — an expanded sub-agent replayed the parent's history: "the sub agents expanded seem to mirror the main agent trace". The boundary is the NEW_TASK hand-off addressed to that thread, and the parser drops everything before it, but only when that boundary is actually found: a hand-off outside the window means the child is shown whole rather than shown empty. Thirty turns of inherited history became three turns of its own work. The Claude path was checked for the same symptom with general-purpose children, which inherit nothing, and looked clean. It was not clean — see section 0c.
what codex actually records
| record | what it is |
|---|---|
collaboration.spawn_agent | the spawn. {"task_name":"pty_summary","fork_turns":"all","message":"gAAAA…"} — the message is encrypted, so there is no readable task text on this path |
| its output | {"task_name":"/root/pty_summary"} — an acknowledgement, not a result. Reading it as one marked every codex sub-agent finished the moment it started: Claude's launch-receipt trap, one shape further along |
wait_agent | {"message":"Wait completed.","timed_out":false} — names no agent, so it says something finished without saying what. Unusable as a per-agent signal |
agent_message | the completion. Message Type: FINAL_ANSWER · Sender: /root/pty_summary plus the payload it handed back — parent-recorded, names which child, timestamped. Codex's analogue of Claude's <task-notification> |
| the child's header | thread_source: "subagent", parent_thread_id, depth, agent_path, agent_nickname — a child is an ordinary rollout that says whose it is |
So the "no terminal event" finding was wrong, and worth saying why: the study looked for sub_agent_activity events, and CLI 0.149.1 emits zero of those in a run with three sub-agents. It emits agent_message instead. That means all three states rest on the same footing as Claude's and nothing has to be shown as unknowable — but the rule that gets there is different, and one Codex-specific trap nearly poisoned it: codex documents this exact message format in its own developer prompt (Message Type: MESSAGE | FINAL_ANSWER … Sender: <author>), in the same thread the real posts arrive in. A looser match reads the explanation as an event.
There is no shared id anywhere in the chain, so the join is the task name: spawn_agent's task_name ↔ the child's agent_path ↔ the post's Sender:. Exact, not a heuristic. And the roster is cheaper here than on Claude's path in one way and dearer in another: no sidecars to read, but it means checking the first line of each rollout in the tree rather than listing one directory.
0c · The same bug on Claude, one harness over — and a guard under it
The operator, on am-dev-3: "for claude it seems a bit recursive, still?" — a sub-agent row that expanded to the same row, with a byte-identical summary, and again below that. The tell was 1 sub-agent in the child's own summary line: it was being credited with itself as its child.
Reproduced against the operator's own run before anything was changed. That session's roster holds nine children and every one of them has agentType: "fork". Claude's subagent_type: "fork" inherits the parent's transcript exactly as codex's fork_turns: "all" does — and the last inherited record is the Agent call that created this fork, followed by its own launch receipt. Read whole, the child holds the call that made it, so it is its own child, and opening that row opens the same file. Fetched from am-dev-3 before the fix, child a69e9dcf… ("Research Meta news") began:
turn 0 assistant tool_use Agent · tool_result "Fork started — processing in background"
turn 1 system <fork-boilerplate> You are a worker fork. The transcript above is
the parent's history — inherited reference, not your situation.
The harness marks its own boundary, so the parser trims there, only when the boilerplate is actually in the window — a fork whose boundary is outside it is shown whole rather than shown empty. Two more defects fell out of the same data: Fork started — processing in background is a launch receipt, not a report, and reading it as one marked every fork finished the moment it started; and the count now excludes nothing and invents nothing, because the child no longer contains its own spawn.

4 steps · 3 sub-agents, and the open child's own line reads 1 step · 4 tools · 38s · 7.5k tok — no sub-agent of its own, its own WebSearch ×4, its own report. fork on the right is real data, not a leak: it is the subagent_type the parent asked for, shown for the same reason codex's Curie is.and a guard underneath, because the reader must never hang on bad data
The parser fix removes the cause. The guard removes the class: canOpenAgent(agentId, ancestors) refuses a row whose agent is already open above it, and refuses anything past MAX_NEST = 3 regardless — the deepest real nesting measured on this machine is 2. Such a row still renders, once, saying why it will not open. Shot against a deliberately self-referential fixture: a child transcript that names its own spawning call, which no harness writes any more.

already open above, and does not open. Nesting depth stops at one.Both halves are tested: the trim against a fork fixture in the server suite (and the untrimmed read left alone, so nothing else changes shape), the guard against the self-referential case in the web suite, each mutation-checked by breaking the rule and watching the right check fail.
1 · The structure, before and after
#115 put the count in the summary line as its own button, so the row had two disclosures. The operator's point is that a sub-agent is not a separate category of thing — it is one of the steps. So the count becomes plain text in the line, and the single fold opens a step list in which the Agent steps are now live rows.
The count stays a count, not a status summary: what each one is doing is one row away, and the row is where state belongs. "2 still going" would fit in the line too, but it then repeats what the rows say two pixels below.
2 · Running, mid-turn, among ordinary steps
release-video's last turn, as it stands right now: eleven of the parent's own steps, then three sub-agents spawned within ninety seconds of each other, then the parent going back to work while they run. Two have reported; the third was spawned at 14:28 and is still writing.
Two facts on the running row, and neither is a verdict: 1h 10m is how long since it was spawned, wrote 2m ago is how long since its transcript last changed. Silence gets reported, never judged — measured gaps between consecutive records inside a live sub-agent reach 601s, so a rule that greys a row out after a minute would be wrong several times an hour.
Its own spinner sits next to the pane's working line, one indented and one not. Two things are working and both say so; the row's spinner is the same braille cell, in the same accent, so it reads as the same kind of statement.
3 · Several at once — and the number is smaller than it looks
In release-video they arrive in runs of two or three across 38 turns; per exchange the distribution is 4, 3, 3, 3, 3, 2, 2, 2, 2, 1, 1, 1. But that session is not the ceiling: the-gatherer spawns 40, 36, 22 and 12 in single exchanges (168 Agent calls across 219 turns). So both shapes are real — most turns spawn two or three, and a fan-out turn spawns dozens — which is why the collapsed strip is capped and the step list is not.
What this turn was under-reporting. Its own line says 53 tools. Its four sub-agents made 463 tool calls and spent 913k tokens between them — most of the turn's work, and none of it reachable from the reader before.
one thing this needs from the code
Consecutive calls to the same tool collapse into one row today — Read ×4 — and spawns arrive consecutively: in this very turn, three in a row at 12:43–12:44. Left as it is, they would render as a single Agent ×3 row with no state, no duration and nothing to open. Sub-agent steps have to be exempted from that grouping in stepsOf. Small change; it is the difference between this design and a row that says ×3.
when a turn spawns twenty
the-gatherer does, four times over. The proposal is the pattern the reader already uses for older turns: show the first few, then one line that opens the rest. The sub-agents stay chronological, and a pathological turn costs one row instead of twenty.
4 · All finished, collapsed
The state the reader is in most of the time: nothing spins, nothing is indented, and the line says how much happened underneath it.
Both grounds are built — drift-plus-noise at two amplitudes and the intricate grid. The grid one took an hour and a half; most of it was re-rendering at 60fps after the first pass read as static.
5 · One expanded: its task, then its trace
Opening a sub-agent row shows the two things the operator asked for. First the task it was given — the Agent call's own prompt, clamped. Then its trace, in the same rail the parent's steps use, because a sub-agent's transcript is an ordinary trace and the reader can already render one.
"completed" means it came back, not that it worked. This one's notification says completed; its report opens "I did not ship the rebuild — both fixes hit a hard stop." The row shows delivery, and the reason to open a row is that delivery and success are different things. Worth keeping the mark honest: ✓ is "handed something back", not "succeeded".
6 · Depth 2, which is where a design like this breaks
rl-llm-wiki ran three sub-agents; one of them ran five more. Inside an open row those five are one rail inside another, which holds. The question is what happens when you open one of the five: a third rail, inside a row already indented twice.
This session also shows an asymmetry worth knowing about before reading the rows: the parent pane recorded no completion for its three sub-agents, while the depth-1 agent recorded all five of its own. A notification lands in the transcript of whoever spawned the agent — so a missing outcome one level up says nothing about the level below.
Recommendation: (b), but only past depth 2. One rail inside a row is worth having — it is how you see that a sub-agent fanned out at all, and it reads fine. A third is where the text column starts paying for the structure, and it is unusable at phone width, where the reader already narrows its gutter to 8px. Swapping the body and showing a crumb keeps the depth reachable without spending indentation on it, and it reuses an idea the reader already has in the card's full history ↗. Depth 3 has not been observed here — 7 of 70 sub-agents are depth 2, none deeper — so this is a guard, not a common path.
7 · The unhappy state, which must not look like "running forever"
Not a rare case: of release-video's 22 sub-agents, nine have no recorded outcome — the notification never arrived, because the pane was restarted or the message was lost to a compaction. Their transcripts are 4–12 MB each and then stop. The reader has to say that plainly.
- The mark does not spin. A spinner is a claim about right now, and it is only earned while the pane's own process is alive. This pane has since been restarted, so nothing here is running.
- It is not a failure either. That third transcript is 9.6 MB of real work; nothing says it failed, only that nothing came back.
no resultis the honest word — dashed, not red. - Still openable. The transcript is on disk, and its last message is usually the report the notification failed to deliver. This is the case where opening the row is worth the most.
failed and killed are different states and read differently — red ✗, not a dash. This session's notifications include four of each, so both are real.
8 · What this asks of the code, if the operator likes it
- Drop the second disclosure. The count moves into the fold's own label;
SubAgentsButtonandSubAgentsListstop existing. - Exempt
Agentfrom step grouping instepsOf, so three consecutive spawns are three rows and notAgent ×3. - Teach the step row one new kind.
StepRowgains anagentcase: spinner-or-mark, the task as the detail line, the notification's facts on the right, and a body that is the task prompt plus the child trace #115 already fetches. - Keep the roster call. It resolves a spawn to its transcript when the launch receipt has scrolled out of the window, and supplies depth, size and last-write.
- Stop nesting at two. Depth 3 swaps the body and shows a crumb instead of indenting again.
Nothing here needs a new data source: every value on every row above is already on disk, and #115's two endpoints already serve it.
where the numbers came from
| panel | session · turn | real |
|---|---|---|
| 2 · running | release-video, turn 38 (live) | 6 tools, 42m, 87.2k tok; two notifications (154/136 calls), one spawn at 14:28 still writing |
| 3 · four at once | release-video, turn 24 | 53 tools, 2h 39m, 1.06M tok; four notifications: 169/218/39/37 calls, 331k/329k/131k/121k tok |
| 4 · collapsed | release-video, turn 21 | 11 tools, 1h 51m, 4.6M tok, two notifications |
| 5 · expanded | release-video, agent a441b23d… | 115 records, 37 tools, 708s; task text and report quoted from the transcript |
| 6 · depth 2 | rl-llm-wiki, agent a99944597… | five notifications inside the child (153/128/151/153/194s); depth-1 rows have none, and say so |
| 7 · no result | release-video, turn 14 | 81 tools, 58m, 1.33M tok; three spawns with no notification, last writes and sizes from stat |
| 3 · fallback | the-gatherer | 38 sub-agents in the session; the quoted row's notification is 172s, 13 calls, 41.7k tok |