refactor(sidebar): one subagent row for CLI and structured children, shared with the chat strip - #22565
refactor(sidebar): one subagent row for CLI and structured children, shared with the chat strip#22565brennanb2025 wants to merge 9 commits into
Conversation
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
- One row model (
src/shared/agent-child-row-model.ts):buildAgentChildRowModelsderives a row per host child view (top-level = the main agent's children, owned work nested), andbuildLegacyAgentChildRowModelsreproduces today's inputs from asubagentssnapshot. Display state comes from the shared child fold, freshness fromresolveAgentChildWorkFreshness, and the detail string in the same order a CLI agent row decides it. - One component + formatter (
AgentChildRowContent.tsx,agent-child-row-text.ts): the dot, name and detail piece the sidebar's compact row and the chat strip both render, with words reused from the CLI row's own formatters (formatAgentToolPreview,agentStateLabel('monitoring'),agentNoUpdateLabel,formatAgentTypeLabel). - Sidebar (
worktree-subagent-child-rows.ts,worktree-card-compact-agent-row.tsx,worktree-card-agent-summary.ts): readsentry.childrenwhen a host publishes views andsubagentsotherwise; no longer collapsesmonitoring → working/done → idleor borrows the parent clock for the child (evidenceObservedAt= the child'sobservedAt); flattens to agent rows carrying the model aschildRow. - Chat strip (
NativeChatBackgroundTasksStatus.tsx,background-task-roster.ts): new optionalchildViewsprop builds rows from views (agents, shells, monitors) with owned work nested under its owner; settled rows show "ended N ago"; stop is keyed on the provider id. - Dot rule + display split (
agent-row-dot-state.ts,agent-status-child-work-display.ts):agentRowDisplayDotStateunifies the dashboard/summary dot (child state first, then interrupt, then state), andderiveAgentChildDisplayState/agentChildWorkOwnedLiveness/AgentChildDisplayStatemove out of the view module so the renderer no longer reaches host-only code. - Supporting: type-only
AgentStatusEntry.children;components.agentChildRow.ended/endedAgoadded to all six catalogs; first-commit fallback goldens pin legacy pixels, and the parity test pins the two surfaces to one expected table across every state.
I read the full diff and ran the new/changed suites (agent-child-row-model, agent-child-row-parity, subagent-child-row-fallback-pixels, worktree-subagent-child-rows, background-task-roster) plus renderer-node-builtin-boundary: 76 tests pass, including the renderer boot boundary the split was made for. I also traced the legacy paths against DashboardAgentRow, worktree-card-compact-agent-row, lastEnteredDoneAt, agent-row-tool-preview and the roster header to confirm the pixel-parity claim holds.
ℹ️ Cross-surface parity is only asserted while the transport is live
The sidebar passes real freshness into the shared model (parentIsFresh, parentEntry.subagentObservation, parentEntry.updatedAt), while the strip hardcodes STRIP_ROW_CONTEXT as parentEvidenceFresh: true, transportObservation: 'live'. So for a given set of views, the same child can read unverifiable in the sidebar and working/monitoring in the strip once contact is lost — the one divergence the shared record was meant to remove. The parity test cannot catch this because both sidebarRows and stripRows only ever exercise fresh contexts, and its unverifiable scenario is a host-reported state rather than a freshness verdict. This is documented in the PR body as a follow-up, so nothing to change here; worth keeping the caveat explicit next to the parity-assertion claim so a future reader does not over-trust it.
Technical details
# Cross-surface freshness divergence is outside the parity test's reach
## Affected sites
- `src/renderer/src/components/sidebar/worktree-subagent-child-rows.ts:107` — context built from `args.parentIsFresh` and `parentEntry.subagentObservation`.
- `src/renderer/src/components/native-chat/background-task-roster.ts:136` — `STRIP_ROW_CONTEXT` pins freshness/transport live.
- `src/renderer/src/components/agent-child-row-parity.test.tsx:115,130` — `sidebarRows`/`stripRows` take no freshness input, so parity is only ever asserted fresh.
## Required outcome
- No change required for this PR. When C5 wires the strip to a contact/liveness verdict, both surfaces should feed the same verdict into `buildAgentChildRowModels`; until then the parity test's "same by construction" claim is scoped to a live transport.
## Open questions for the human
- Does C5's strip channel carry a contact-loss verdict, or will the strip remain permanently `live` and accept the divergence?DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
Since the prior review at 47f8ddbd1d, one commit (3422ccaf4c) lands the review fixes:
- The strip reads its parent's verdict.
agentChildRowContextForParentis now the one context a parent row gives its children; the sidebar uses it, and the strip takes an optionalchildRowContextthreaded intobuildBackgroundTaskGroupsFromViews(defaultREPORTED_ROW_CONTEXTkeeps today's "claims stand as reported" until C5 passes one). - The full row matches the child row. Its message line comes from
agentChildRowMessageLine(the child's message or "Ended"), an unlabeled child falls back to its display state, and its dot comes fromagentRowDisplayDotState. - Failed children count.
failedjoinsSUMMARY_STATE_ORDER, so a failed child is no longer dropped from the summary groups and counts. - Coverage. The parity test now asserts the full sidebar row for every state, a live parked child, an unlabeled child, and a lost or stale parent on both surfaces.
I read the new commit end to end, traced the touched consumers (DashboardAgentRow, the compact row, worktree-card-agent-summary, background-task-roster, worktree-subagent-child-rows) and ran the six affected suites (agent-child-row-parity, subagent-child-row-fallback-pixels, worktree-card-agent-summary, background-task-roster, agent-child-row-model, agent-finished-timestamp): 75 tests pass. The full-row "Ended" assertion and the lost/stale-parent parity assertions each fail without their corresponding change, so the coverage is real. The dot/lastEnteredDoneAt change from 47f8ddbd1d stays behavior-preserving on the legacy path because a legacy subagents snapshot can never carry state done (AgentSubagentState excludes it), which the fallback goldens confirm.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
f098d8d to
c99160f
Compare
3422cca to
78e73c6
Compare
c99160f to
8e22ed0
Compare
78e73c6 to
04d1d40
Compare
There was a problem hiding this comment.
✅ No new issues found.
Reviewed changes
Since the prior pullfrog review at 3422ccaf4c, the branch rebased onto a newer brennanb2025/c4-codex-producer and one commit lands on top:
- Extracted the subagent snapshot module (
src/shared/agent-status-subagent-snapshot.ts, new):AgentSubagentState,AgentSubagentSnapshot,AGENT_STATUS_MAX_SUBAGENTS, the admission normalizer (renamednormalizeAgentSubagentsField) andagentSubagentsEqualmove out ofagent-status-types.ts, which re-exports them so every importer is unchanged. This bringsagent-status-types.tsback under themax-linescap. - Moved the shared field caps (
AGENT_STATUS_TOOL_INPUT_MAX_LENGTH,AGENT_TYPE_MAX_LENGTH,AGENT_MODEL_MAX_LENGTH) intoagent-status-field-normalization.ts, re-exported fromagent-status-types.ts. - Rebase-only churn: the six older commits were rebased (new SHAs, identical diffs); the
src/main/codex/*movement the range-diff shows is base-branch progress, not this PR's work.
I read the incremental range-diff and the full diff end to end, traced every importer of the moved symbols, and confirmed the extraction is behavior-preserving: the three caps stay in live use inside agent-status-types.ts, the new module adds no import cycle, and no import site breaks. pnpm test src/shared/agent-status-types.test.ts src/shared/agent-status-child-work-view.test.ts passes (88 tests), pnpm run typecheck:node exits 0, and oxlint reports 0 findings on the three changed files.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
There was a problem hiding this comment.
ℹ️ No critical issues — one defer-able observation inline. Tests and typechecks for this delta are green.
Reviewed changes
Since the prior pullfrog review at 04d1d403f0, two commits land on top:
- One legacy builder family (
src/shared/agent-child-row-model.ts): the strip's task-roster rows now come frombuildLegacyTaskRowModelsbeside the view and snapshot builders, sharingusableAgentChildLabeland the one detail rule;background-task-roster.tsgroups them and substitutes the kind label only for an unnamed legacy row. - One owner for the run-state word (
src/shared/agent-status-child-work-display.ts):agentChildRunStateFor(failed→blocked, cancelled→idle) moves into the shared display module; the sidebar row state and the strip header both delegate to it. - One reader clock (
src/shared/agent-child-row-model.ts):AgentChildRowContextgainshostClockOffsetMs;agentChildRowContextForParentmeasures the parent on the reader's receipt clock androwFromViewmoves a child'sobservedAtonto it, so a mirrored parent's children age against one machine. - Frozen settled rows (
background-task-roster.ts,NativeChatBackgroundTasksStatus.tsx): a settled row shows its run frozen atsettledAtinstead of a growing "ended N ago", and settled rows no longer arm the 1 Hz tick; theendedAgostring is removed from all six catalogs. - Tests: the three builders, the mirrored reader clock, and the frozen strip are pinned, plus a lifecycle-word test tying the sidebar row state to the strip header.
I read the incremental range-diff, both new commits, and the full diff end to end; ran the six affected suites, the renderer boundary test, pnpm run typecheck:node / typecheck:web (both exit 0), pnpm run verify:localization-catalog, and a broader renderer/shared run (5234 passed, 1 expected fail). One defer-able observation inline.
DeepSeek Flash (free via Pullfrog for OSS) | 𝕏
| ), | ||
| firstObservedAt: view.firstObservedAt, | ||
| observedAt: view.observedAt, | ||
| recencyAt: view.observedAt + context.hostClockOffsetMs, |
There was a problem hiding this comment.
recencyAt is the only host-stamped clock this moves onto the reader's clock — firstObservedAt above it and settledAt below stay on the host's, so for a mirrored parent every age computed from them still subtracts across the two machines. That is the strip's live elapsed and single-command header elapsed, and the sidebar's started/done times; the hostClockOffsetMs comment's “no age subtracts across two machines” holds only for the “No update” reading. Latent until #22614 publishes views, so nothing to fix in this PR's shipped behavior, but worth mapping these with the same offset.
Technical details
# Only `recencyAt` is moved onto the reader clock
## Affected sites
- `src/shared/agent-child-row-model.ts:240` — `firstObservedAt` copied raw from the host view.
- `src/shared/agent-child-row-model.ts:243` — `settledAt` copied raw from the host view.
- `src/renderer/src/components/native-chat/background-task-roster.ts:178` — `backgroundTaskElapsedLabel(row.firstObservedAt, now)` subtracts a host stamp from the reader's `now` for a live row (a host running fast renders `0s`).
- `src/renderer/src/components/native-chat/background-task-header-content.ts:170` — the same subtraction for a single live command's header elapsed.
- `src/renderer/src/components/sidebar/worktree-subagent-child-rows.ts:38,48` — the synthetic entry's `startedAt`/`stateStartedAt` come from `firstObservedAt`; `lastEnteredDoneAt` reads `childRow.settledAt`, which the full and compact rows then subtract from `now`.
## Required outcome
- For a parent with `mirroredEvidenceReceivedAt` set, a child's `now - firstObservedAt` and `now - settledAt` must read on the reader's clock, matching the “No update” reading. Mapping both fields with the same `hostClockOffsetMs` keeps the skew-free `settledAt - firstObservedAt` run duration.
## Open questions for the human
- Are these fields left on the host clock deliberately (e.g. #22614 or the producer re-stamps them), or should the model map them the way it maps `recencyAt`?8e22ed0 to
08ca92b
Compare
b4cf0d2 to
5406ab2
Compare
08ca92b to
358d1a9
Compare
5406ab2 to
359483b
Compare
358d1a9 to
1db34d1
Compare
359483b to
f02c2e1
Compare
1db34d1 to
36242df
Compare
f02c2e1 to
d30ea85
Compare
36242df to
bc928a2
Compare
d30ea85 to
a26d58b
Compare
bc928a2 to
307537d
Compare
a26d58b to
871c4b9
Compare
307537d to
e7239f1
Compare
…acy shapes Captured on the unmodified renderer: the compact and full sidebar child rows built from a legacy subagents snapshot, and the expanded chat strip built from a legacy background-task roster. A host that sends only these shapes must keep rendering exactly these rows.
…shared with the chat strip A child row's dot, name and detail are now decided once, by a shared row model (src/shared/agent-child-row-model.ts), and rendered by one piece (AgentChildRowContent) that both the sidebar's child rows and the chat strip use. The model reads the host's child views when a session publishes them and today's legacy shapes otherwise (the subagents snapshot for the sidebar, the task roster for the strip), so old hosts and CLI panes render exactly as before. From views it keeps what the legacy path lost: a finished subagent whose shell still runs reads monitoring through the shared child fold, a settled child reads by its outcome, the tool it runs shows as the CLI row shows it, and each row keeps its own clock. In the strip, work a child owns renders nested beneath it.
A row-model table for every display state, and a parity table that renders the same child views through the compact sidebar row and the chat strip and asserts both show the same dot, name and detail. Covers a finished subagent whose shell still runs (monitoring on both, no stale tool text), sibling rows with their own clocks, a settled row timed from when it ended, and owned shells nested under their owner in the strip. The strip's view input is named childViews so it cannot be mistaken for React children.
The row model imported deriveAgentChildDisplayState from the view module, whose owner resolution reaches status subjects and, through them, agent-hook-relay's node:crypto. The renderer cannot evaluate that chunk, so the app booted blank (the renderer node-builtin boundary test fails on the previous commit). The display derivation (agentChildWorkOwnedLiveness, deriveAgentChildDisplayState, AgentChildDisplayState) now lives in agent-status-child-work-display.ts, which imports only the fold, liveness and the one-pass grouping both modules share (grouped-by.ts); the view module keeps the host-side projection.
…says Ended Review fixes: - The strip's view path took no freshness input, so once views are wired the same lost child would read unverifiable in the sidebar and working in the strip. agentChildRowContextForParent builds the one context a parent row gives its children; the sidebar uses it, and buildBackgroundTaskGroupsFromViews / the strip's new optional childRowContext prop accept it (absent: claims stand as reported, as before). - The full sidebar row showed no word for a child that ended with no outcome; its message line now reads the row model's (the message, or Ended). An unlabeled child falls back to its display state there too. - The summary order now includes failed, so a failed row is never dropped from the counts. - Parity now covers the full row for every state, a live parked child, an unlabeled child, and a lost or stale parent on both surfaces.
…equality get their own module agent-status-types.ts crossed the file-size limit once the row gained child views beside the main agent fact. The subagent snapshot shape, its admission normalization and the array equality move into agent-status-subagent-snapshot.ts (re-exported, so importers are unchanged); the three field caps it shares with the row move to the field normalization.
…n settled rows - The chat strip's task-roster rows are built in the shared row model beside the other two builders, with one placeholder set and the one detail rule. The module header states when each legacy builder is deleted. - A child's "No update" duration reads the parent's reader clock (receipt time for a mirrored parent); a view child's own stamp is moved onto that clock. The full sidebar row reads the same value as the compact row. - The failed->blocked / interrupted->idle collapse has one owner, shared by the sidebar row state and the strip header. - A settled strip row shows its run frozen at settledAt instead of a growing "ended N ago", so a strip of only finished work never wakes the 1 Hz tick.
871c4b9 to
05308c3
Compare

ELI5
A subagent shows up in two places: as an indented row under its parent in the sidebar, and as a row in the chat's background-task strip. Until now each place decided on its own what the row says, and they disagreed. A subagent that finished but left a dev server running vanished from the sidebar, and the strip showed it as "done" with the dev server in a separate group. Finished and failed subagents never showed in the sidebar at all, neither surface said which tool a subagent was running, and under a quiet parent every sibling showed the parent's clock instead of its own. This PR makes one piece of code decide what a subagent row says (the dot, the name and the detail line) and one component draw it in both places. A subagent row from a chat session now reads exactly like a subagent row from a CLI agent, including the yellow monitoring icon.
Nothing on screen changes at this PR's position, deliberately. This PR moves both surfaces onto one row model and pins today's rendering byte-for-byte (the first commit's golden, captured on the unmodified renderer). The host publishes no child views until #22614, which flips both surfaces to the records and makes every screenshot in this body live. Merging this PR alone is a pure, pixel-identical refactor.
The row code is provider-agnostic, but rows only exist where a producer registers children: Claude and Codex multi-agent v2 render after #22614, while Codex's default mode (GPT-5.5, which announces helpers only through
collabAgentToolCall) shows no rows until #22619 also lands.Stacked on #22553 (Codex producer, head
f098d8db9e), which is stacked on #22536 and #22521. The base branch isbrennanb2025/c4-codex-producer. This PR takes over the display half of #21400 (a subagent row's own clock). The field half of #21400 is the record'sobservedAt(#22521), and its CLI clock stamps belong to a follow-up PR (CLI subagent clocks), not open yet.Merge order
What Changed
One row model, shared by every surface (
src/shared/agent-child-row-model.ts)AgentChildRowModelholds what one child row shows:displayState(anAgentStateDotstate),name, a structureddetail, the clocks (firstObservedAt,observedAt,recencyAt,settledAt),totalTokens,canStop,settled, andowned(the work this child owns, nested).deriveAgentChildDisplayStateplus the child's owned liveness. That is the same fold that decides a CLI parent's row, so an idle or finished child whose own shell still runs readsmonitoring. Top-level rows are the main agent's children. A child's shells and nested agents hang under it inowned.resolveAgentChildWorkFreshness. Any live claim (working, monitoring, waiting, blocked) under a stale parent or a lost transport readsunverifiable. A settled outcome is history and is never rewritten.agentChildRowContextForParentbuilds the one context a parent row gives its children: its freshness verdict, itssubagentObservation, and its clock. Every surface that lists one parent's children builds the context there, so a lost child reads the same everywhere.unverifiablemonitoringworking/waitingTool: input, else the last message, else the roleblocked/done/failedinterrupted(cancelled)idle, settled with outcome unknownidle, live (parked)subagentssnapshot (an old host, or any CLI pane): the same derivation, with today's inputs. There is no clock of the child's own (recency is the parent's, as today), no outcome and no operation.One component, rendered by both surfaces (
AgentChildRowContent)The component draws the dot, the name and the detail. Only the surface tone, the separator and the hover title are passed in by the surface. The words come from one formatter,
agent-child-row-text.ts, which reuses the CLI row's own phrasing (formatAgentToolPreview,agentStateLabel('monitoring'),agentNoUpdateLabel,formatAgentTypeLabel).buildSubagentChildRows): builds its context withagentChildRowContextForParent(parentEntry, parentIsFresh). It readsentry.childrenwhen a host publishes views, andsubagentsotherwise. It stops collapsingmonitoring → workinganddone → idle, and stops using the parent's clock as the child's (evidenceObservedAt= the child'sobservedAt). The row'sDashboardAgentRowdata now carries the model aschildRow. The compact row rendersAgentChildRowContentfor it. The full row still renders through the CLI parent's own code, fed by the synthetic entry:workingMode: 'monitoring', andtoolName/toolInputfrom the operation. FromchildRowdirectly it takes the dot, the fallback label for an unlabeled child, and the message line (agentChildRowMessageLine: the child's message, or "Ended"). Shells and monitors are not listed in the sidebar; they appear through their owner's dot, as today.NativeChatBackgroundTasksStatus): every row, agents, shells and monitors, rendersAgentChildRowContent. Two new optional props.childViewsbuilds the rows from views, with owned rows nested under their owner.childRowContext?: AgentChildRowContextis the session's parent-row context, the same one its sidebar rows read, passed tobuildBackgroundTaskGroupsFromViews(views, context?). Without it, every live claim stands as reported, which is the only behaviour today. Without it, rows come fromtasks/settledTasksexactly as before. The kind icon, tokens, elapsed time and Stop stay local to the strip. A settled row whose host reports when it settled shows its run frozen atsettledAt(see Architecture review). The groups and the header are unchanged: the header counts top-level rows, and a failed child reads as the host'sblockedand a cancelled one asidle, the words an old strip uses for them.lastEnteredDoneAtfor a subagent row is the child'ssettledAt, so both sidebar modes time a finished child from when it ended.Supporting changes
AgentStatusEntry.children?: AgentChildWorkView[]: a type-only field. No producer writes it and no parser admits it; feat(native-chat): the chat strip and the sidebar read the host's child records #22614 populates it.deriveAgentChildDisplayState,agentChildWorkOwnedLivenessandAgentChildDisplayStatemoved fromagent-status-child-work-view.tsto a newagent-status-child-work-display.ts. The view module's owner resolution reachesagent-status-subject→agent-status-run→agent-hook-relay(node:crypto). Importing the display functions from there made the renderer boot blank in the dev app. The existingrenderer-node-builtin-boundarytest fails on the commit before the split. The view module keeps the host-side projection. Three test files from feat(agent-status): child work records say what the child is doing, how it ended, and when #22521/feat(native-chat): Codex sessions write their subagents into the host status store #22553 changed only their import lines.agentRowDisplayDotState(agent)is the one dot rule for a dashboard row: a child's own state first, then an interrupted turn, then its state.getAgentDotStateand the full row both use it. The compact summary's state order now includesfailed, so a failed row can never drop out of the counts.components.agentChildRow.ended("Ended"). Added toen.jsonby the sync script and by hand toes/fr/ja/ko/zh.en-runtime-required.jsonneeds no entry (literal fallbacks).Why
A subagent has been materialised differently for each surface. The sidebar converted the strip's DTO and lost facts on the way (
monitoring → working,done → idle, the parent's clock instead of the child's). The strip had its own row, with its own words. Unifying the record host-side (#22521, #22536, #22553) does not help if each surface still derives its own row from it. This PR puts that derivation in one model and one component, so both surfaces read the same row from the same record, and a structured subagent reads the same as a CLI one. For a structured child the sidebar and strip rows are the same row by construction. The parity test below asserts it for every state.Alternatives considered:
interrupted: truefor a cancel). Rejected: the CLI wording "Interrupted by user" would overclaim for a child a parent cancelled, and there is no failed or "Ended" state to map onto. The row carries its own display state, androw.statekeeps the lifecycle word every other reader of a CLI row understands (failed → blocked,cancelled/unknown → idle).Linked Issue
None — part of the structured chat status/orchestration program.
Visual Proof
Captured with the
$electronskill: a dev instance launched withORCA_BACKGROUND_LAUNCH=1on a throwaway profile, Playwright over CDP, and screenshots of the hidden window. No window was shown and no OS input was used. The fixture store: one folder workspace with three agent panes seeded throughsetAgentStatus.Bash: npm test -- parser(started 12m ago); working withRead: src/parser/index.ts(3m); finished while itsnpm run devshell still runs; finished; failed; cancelled.subagents.The chat strip is the real
NativeChatBackgroundTasksStatus, mounted into the running renderer with the same children, because no structured session was running. Before is the base commit (1df7e358c0), fed the legacy shapes the shared legacy projections derive from the same views. After is this branch, fed the views. Both runs used the same seed script. The shots predate the review-fix commit3422ccaf4c; no scenario that commit changes is in frame.What each pair shows (images below):
*-04-sidebar-card.png)general-purpose/Explore; its four settled children are absent. B's children both read "No update in 40m" (the parent's clock).Run the parser tests - Bash: npm test -- parser;Summarize the grammar - Grammar has 42 productions(green check, 6m);Monitoring background tasks - Start the dev server(yellow Activity icon, no tool text);Fuzz the tokenizer - Exit code 1: fuzz target crashed(red);Benchmark the lexer(interrupted);Map call sites - Read: src/parser/index.ts. B: "No update in 1m" and "No update in 25m".Explore,Rename token kinds - general-purpose,Approve the write - Agent*-05-sidebar-card-full-mode.png)Bash npm test -- parser), result and error lines, the monitoring icon on the dev-server child, finish times*-02-strip-top.png,*-03-strip-bottom.png)Start the dev serverreads done (green check); its shell sits in a separate Shell group;Fuzz the tokenizer · failed; no tool, result or error text.Run the parser tests · Bash: npm test -- parser;Monitoring background tasks · Start the dev server, withnpm run devnested under it;Fuzz the tokenizer · Exit code 1: fuzz target crashed; settled rows said "ended Nm Ns ago" at this capture; they now show a frozen run duration (see Architecture review).*-01-window-compact.png,*-06-window-full-mode.png)Testing
All tests were run with
env -u ORCA_STRUCTURED_SESSION, rebased onto #22553'sf098d8db9e. The broad run below is at47f8ddbd1d. After the review fixes (3422ccaf4c), the sidebar, native-chat and dashboard suites, the row-model test and the boundary test pass: 4,766 tests, all green.pnpm tc:node,tc:webandtc:cliall exit 0.src/shared/agent-status*,src/shared/agent-lead-status-fold,src/main/codex,src/renderer/src/components/{sidebar,native-chat,dashboard},src/renderer/src/lib,src/renderer/src/i18n, and the renderer boundary test. 1,355 files, 13,308 tests: 13,292 passed, 1 expected-fail, 14 skipped, 1 failed. The two timing tests that failed across two runs of that set,codex-session-index-heal-state(75 s) andagent-resume-launch-target(cmd.exe case, 30 s), both pass when run alone (18/18). The branch does not touch either file.subagent-child-row-fallback-pixels.test.tsxis the first commit, captured on the unmodified renderer. It pins the compact rows (fresh, stale parent, lost transport), the full rows, and the expanded strip built from legacysubagentsandtasks/settledTasks, as markup snapshots plus row text. It passes unchanged after the refactor, so old hosts and CLI panes render identical markup.agent-child-row-model.test.ts(21 tests): the display table for every display state; tool text suppressed while monitoring, even with a stale operation present; freshness (a stale parent or lost transport makes live claims unverifiable and leaves settled outcomes alone); sibling clocks; nesting and host order; names and roles; stop eligibility; the legacy path.agent-child-row-parity.test.tsx(21 tests): the same views rendered through the compact sidebar row and through the chat strip give the same dot, name and detail for all 12 states (including a live parked child), each also checked against a literal expected table. The full sidebar row is checked for every state too: its dot label, its name, and the detail it shows ("Ended" included), or the text it must hide. Also covered:agentChildRowContextForParentof the same parent);settledAton both surfaces;worktree-card-agent-summary.test.ts(+1): a failed child stays in the summary groups and counts.HEAD. The test set is the new tests, the existing child-row, strip and summary tests, and the renderer boundary test (99 tests).monitoring → workingcollapse restored in the sidebar child rowssettledAtas a child's finish timefailedremoved from the summary orderQuality. The changed-code gate reports 0 new findings in every category since
f098d8db9e. Fulloxlinton the 22 changed TS/TSX files exits 0, and a 1,200-line probe confirms it reportsmax-lines.pnpm run audit:anti-slopexits 0. All fourverify:localization-*scripts pass, andoxfmt --checkpasses on the six catalogs.pnpm-lock.yamlis absent from the range, and no docs were added.Platforms: macOS only (tests and the hidden-window dev instance). The change is renderer and shared presentation code with no platform branches.
I manually tested these changes locally
Automated tests added/updated, or explained why not below
Review
Not verified:
entry.childrenandchildViewsare populated only by tests and by the screenshot fixture. feat(native-chat): the chat strip and the sidebar read the host's child records #22614 wires the status summary and the strip channel to them. The renderer store's live-entry builder (agent-status-live-entry-builder.ts) copies fields explicitly, so feat(native-chat): the chat strip and the sidebar read the host's child records #22614 must carrychildrenthrough it and its equality check.childRowContextyet. feat(native-chat): the chat strip and the sidebar read the host's child records #22614 must pass it wherever it passeschildViews, built withagentChildRowContextForParentfrom the session's own status row: that row'sisExplicitAgentStatusFresh(…)verdict, and itssubagentObservationfromfeed.getSessionObservation. Without it the strip cannot decay a lost child, and the sidebar would.subagentsand the dashboard board readsrow.state, and neither changes for legacy input. On the view path the board maps a failed child toblockedand a cancelled or unknown one toidle.Decisions and deviations:
Edit: src/a.ts) with the "Waiting for input" dot label. That matches the CLI row, where the plan table said "Waiting for input" + tool name.childWorkId; the legacy path keeps today's provider ids. A resumed child (settled → live, same id, generation + 1) keeps its row. When feat(native-chat): the chat strip and the sidebar read the host's child records #22614 switches a session from legacy to views, a row's key changes once: a Codex agent's viewproviderIdis its thread id, notcodex-agent:<thread>. Stop namesproviderId; Codex agents are not stoppable, so no stop call sees the difference.Architecture review
An architecture pass at
04d1d403f0asked for four changes and one note. Commits21fcd597e6andb4cf0d2a91make them.1. One legacy builder family, one label rule, one detail rule. The strip used to build its old-host rows by hand in
background-task-roster.ts. Those rows had their own placeholder set and their own three-state detail rule. That made three builders ofAgentChildRowModelwith two copies of the naming rule. One child whose description was the placeholdertaskgot three names:''from views (the surface then showed its state),taskfrom the sidebar's snapshot path, andBackground agentfrom the strip's roster path. Now:buildLegacyTaskRowModels(tasks, settledTasks)lives inagent-child-row-model.tsbeside the other two builders. It also keeps the resumed-task dedupe (the live row wins).usableAgentChildLabel, with one placeholder set. The transcript row'sresolveBackgroundTaskNameuses it too.agentChildRowDetail. A roster row declaresevidence: 'run-state': the host reported only a run state, so the row says that state's reason word or nothing. That is exactly today's output.childrenand no supported paired host predates that. The roster builder goes once no supported paired host predates child views on the session feed.taskand roleExplore, and renders it four ways: sidebar from views, sidebar from the snapshot, strip from views, strip from the roster. All four give the same dot, name and detail. Before this change the snapshot path readtask - Explore.2. "No update in Nm" uses the parent's reader clock.
agentChildRowContextForParenttookupdatedAt, which is the host's wall clock. For a mirrored parent, the parent row decays on this machine's receipt time (agentStatusEvidenceObservedAt), while its children subtracted the host's clock from this machine'snow. With a host running 20 minutes fast, the parent read "No update in 3m" and its snapshot children read 23m. Now:agentStatusEvidenceObservedAt(parent). ItsPickis widened toevidenceObservedAtandmirroredEvidenceReceivedAt.hostClockOffsetMs: the reader's clock minus the host's, measured at that evidence, and 0 for a parent observed on this machine. A view child's ownobservedAtis moved onto the reader's clock by that offset. Its age is then the host-clock age plus the receipt-clock age, and nothing is subtracted across two machines.observedAtitself keeps the host's clock.agentChildRowNoUpdateLabel), not from the synthetic entry, so the compact row and the full row agree.3. One display-to-legacy state collapse.
failed → blockedandinterrupted → idlewere written twice: the sidebar'sagentRowStateForand the strip header'sheaderRunState. NowagentChildRunStateForinagent-status-child-work-display.tsowns it. The sidebar adds only its own step on top: monitoring becomesworkingplusworkingMode.4. Settled rows freeze. A settled strip row used to show "ended 6m 7s ago", and that label grew at 1 Hz. So an expanded strip holding only finished work re-rendered every second, forever. Now:
settledAt(settledAt − firstObservedAt). That is the live row's elapsed clock, stopped when the child finished.components.agentChildRow.endedAgokey is removed from all six catalogs; this PR had added it.5. Freshness input is still computed per surface (note, no change).
agentChildRowContextForParentunifies how the context is built, butparentEvidenceFreshis still passed in by each surface from its ownuseNow(30_000)tick. The sidebar does this inWorktreeCardAgents, and #22614 does it in its session-context hook with the same rule and threshold. Near the 30-minute boundary, the two surfaces can disagree for up to one tick (about 30 s). The parity tests pass the same boolean to both surfaces, so they cannot see this. For structured sessions this barely matters:structuredHostOwnedmakes the clock test always fresh, which leavestransportObservationas the real verdict, and both surfaces read that one store fact. Computing the verdict once per entry in the store would remove the duplicated input. That is new machinery and is not done here.Deviations, recorded:
buildBackgroundTaskGroups, on the legacy path only, after the shared label rule has run. Codex publishes unlabeled agent tasks today (codex-background-task-tracker.ts), so making this row read by its state would change old-host pixels. The first commit's golden forbids that. So the four-way test uses a child that has a usable label, and the unlabeled case stays one step different between host generations, as it already was.run-stateevidence says only the reason word, because the roster carries no role. So a new-host and an old-host row for the same unlabeled agent differ in their trail ("Working · Agent" versus "Background agent"). This is pinned by the legacy golden.firstObservedAtfrom this machine'snow. A mirrored strip would show that skew. No caller passes views from another host yet (feat(native-chat): the chat strip and the sidebar read the host's child records #22614). If one does, the samehostClockOffsetMsapplies.Validation for these commits:
pnpm tc:node,tc:webandtc:cliexit 0. Fullpnpm exec oxlintexits 0. The changed-code gate reports 0 new findings.audit:anti-slopexits 0.pnpm-lock.yamlis not in the range, and no files are added.src/shared/agent-status*, the row model,src/main/codex,src/renderer/src/components/{sidebar,native-chat,dashboard}, the parity test,src/renderer/src/lib,src/renderer/src/i18n): 1,370 files and 13,480 tests. 13,465 passed, 1 expected-fail and 13 skipped. One failed: the palette-match performance budget, a timing test in an untouched file, which passes alone (9/9). The legacy fallback goldens pass unchanged.HEAD:description ?? agentTypenamerun-statearm deleted from the detail ruleparent.updatedAtfailed → doneb4cf0d2a91.nowIn the first run of the tick ablation,
WorktreeCard.pr-displayalso failed once, under load. The rerun at the final head reddened only the intended test, and that file passes alone.Visual proof for these commits. Captured with the
$electronskill: a hidden dev instance withORCA_BACKGROUND_LAUNCH=1, a throwaway profile, and Playwright over CDP. The fixture store is the same one as above. Three real strips are mounted in the running renderer: the six-child views strip, the same children filtered to settled only, and an old-host roster with a placeholder-named task. Before is04d1d403f0; after isb4cf0d2a91. Over a 3.2 s window, the settled-only strip's text changed before (ended 6m 8s ago→ended 6m 11s ago) and did not change after (5m 0sthroughout). The roster strip is identical in both, including the unlabeledBackground agent · no contactrow.Not verified for these commits: no mirrored (paired or SSH) host was run, so the skew fix is proven by tests only. There was no Linux, Windows or mobile run. The sidebar's placeholder-name fix is pinned by tests and not shown in a screenshot.
Agent skill upstream boundary
docs/reference/agent-skill-sharing-upstream-boundary.mdand copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.Notes
AgentStatusEntry.childrenis a type-only optional field that nothing publishes or parses yet. The strip'schildViewsandchildRowContextare optional props no caller passes yet. Old hosts, and new hosts before feat(native-chat): the chat strip and the sidebar read the host's child records #22614, send only the legacy shapes, which render as before (pinned by the first commit).Checklist
N/Awith reasonpnpm lint,pnpm typecheck,pnpm test, andpnpm buildpass (or CI will cover; local preferred)