@aragon-agent/cli 0.6.34 → 0.6.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +173 -7
  3. package/dist/agent/agent-mode.d.ts +20 -4
  4. package/dist/agent/agent-mode.js +20 -4
  5. package/dist/agent/controller.d.ts +67 -3
  6. package/dist/agent/controller.js +106 -5
  7. package/dist/agent/headless.js +31 -0
  8. package/dist/agent/kit-apply.d.ts +56 -0
  9. package/dist/agent/kit-apply.js +209 -0
  10. package/dist/agent/model-profile-settings.d.ts +28 -0
  11. package/dist/agent/model-profile-settings.js +142 -29
  12. package/dist/agent/reducer.d.ts +13 -2
  13. package/dist/agent/reducer.js +1 -0
  14. package/dist/cli.js +12 -0
  15. package/dist/commands/agents-command.d.ts +15 -0
  16. package/dist/commands/agents-command.js +27 -0
  17. package/dist/commands/builtins.js +135 -4
  18. package/dist/commands/kit-command.d.ts +20 -0
  19. package/dist/commands/kit-command.js +131 -0
  20. package/dist/commands/perf.d.ts +11 -0
  21. package/dist/commands/perf.js +2 -1
  22. package/dist/commands/registry.d.ts +14 -0
  23. package/dist/config/app-paths.d.ts +15 -0
  24. package/dist/config/app-paths.js +33 -2
  25. package/dist/config/env.d.ts +4 -1
  26. package/dist/config/env.js +28 -2
  27. package/dist/config/kit-resolution.d.ts +93 -0
  28. package/dist/config/kit-resolution.js +226 -0
  29. package/dist/config/load.d.ts +10 -0
  30. package/dist/config/load.js +8 -1
  31. package/dist/config/model-kits.d.ts +224 -0
  32. package/dist/config/model-kits.js +302 -0
  33. package/dist/config/model-profile-store.d.ts +25 -0
  34. package/dist/config/model-profiles.d.ts +41 -3
  35. package/dist/config/model-profiles.js +46 -5
  36. package/dist/config/schema.d.ts +84 -0
  37. package/dist/config/schema.js +39 -0
  38. package/dist/config/store.js +12 -1
  39. package/dist/config/ui-state.d.ts +18 -0
  40. package/dist/config/ui-state.js +9 -0
  41. package/dist/exec/events.d.ts +25 -0
  42. package/dist/exec/events.js +4 -0
  43. package/dist/exec/index.js +16 -1
  44. package/dist/exec/runner.js +10 -0
  45. package/dist/input/keymap.js +18 -4
  46. package/dist/proc/limits.d.ts +16 -0
  47. package/dist/proc/limits.js +16 -0
  48. package/dist/proc/supervisor.d.ts +20 -1
  49. package/dist/proc/supervisor.js +42 -14
  50. package/dist/prompts/command.d.ts +34 -0
  51. package/dist/prompts/command.js +237 -0
  52. package/dist/prompts/copy.d.ts +89 -0
  53. package/dist/prompts/copy.js +131 -0
  54. package/dist/prompts/fill.d.ts +51 -0
  55. package/dist/prompts/fill.js +46 -0
  56. package/dist/prompts/limits.d.ts +53 -0
  57. package/dist/prompts/limits.js +53 -0
  58. package/dist/prompts/normalize.d.ts +53 -0
  59. package/dist/prompts/normalize.js +190 -0
  60. package/dist/prompts/resolve.d.ts +31 -0
  61. package/dist/prompts/resolve.js +60 -0
  62. package/dist/prompts/search.d.ts +58 -0
  63. package/dist/prompts/search.js +146 -0
  64. package/dist/prompts/store.d.ts +86 -0
  65. package/dist/prompts/store.js +453 -0
  66. package/dist/prompts/types.d.ts +204 -0
  67. package/dist/prompts/types.js +13 -0
  68. package/dist/session/checkpoint.d.ts +8 -1
  69. package/dist/session/checkpoint.js +6 -0
  70. package/dist/session/persist.d.ts +19 -1
  71. package/dist/session/persist.js +13 -1
  72. package/dist/session/startup-session.d.ts +7 -2
  73. package/dist/session/startup-session.js +5 -2
  74. package/dist/session/store.js +3 -0
  75. package/dist/session/validate-session.js +64 -1
  76. package/dist/team/limits.d.ts +25 -0
  77. package/dist/team/limits.js +26 -0
  78. package/dist/team/runtime.js +27 -2
  79. package/dist/team/settled.d.ts +57 -0
  80. package/dist/team/settled.js +87 -0
  81. package/dist/team/subagent.js +48 -2
  82. package/dist/team/timeline.d.ts +74 -0
  83. package/dist/team/timeline.js +101 -0
  84. package/dist/team/types.d.ts +24 -1
  85. package/dist/todo/acceptance.d.ts +120 -0
  86. package/dist/todo/acceptance.js +399 -0
  87. package/dist/todo/follow-through.d.ts +48 -23
  88. package/dist/todo/follow-through.js +110 -44
  89. package/dist/todo/limits.d.ts +62 -1
  90. package/dist/todo/limits.js +62 -1
  91. package/dist/todo/panel-rows.d.ts +29 -39
  92. package/dist/todo/panel-rows.js +37 -83
  93. package/dist/todo/plan-sections.d.ts +60 -0
  94. package/dist/todo/plan-sections.js +108 -0
  95. package/dist/todo/prompt.d.ts +17 -12
  96. package/dist/todo/prompt.js +59 -40
  97. package/dist/todo/row-window.d.ts +51 -0
  98. package/dist/todo/row-window.js +107 -0
  99. package/dist/todo/store.d.ts +139 -31
  100. package/dist/todo/store.js +271 -73
  101. package/dist/todo/todo-tool.d.ts +33 -8
  102. package/dist/todo/todo-tool.js +186 -30
  103. package/dist/todo/types.d.ts +58 -0
  104. package/dist/ui/App.js +414 -54
  105. package/dist/ui/Composer.d.ts +6 -2
  106. package/dist/ui/Composer.js +2 -2
  107. package/dist/ui/Header.d.ts +17 -1
  108. package/dist/ui/Header.js +8 -2
  109. package/dist/ui/PromptInput.d.ts +21 -1
  110. package/dist/ui/PromptInput.js +22 -2
  111. package/dist/ui/ScrollbackShell.d.ts +22 -7
  112. package/dist/ui/ScrollbackShell.js +8 -1
  113. package/dist/ui/StatusBar.d.ts +18 -0
  114. package/dist/ui/StatusBar.js +11 -3
  115. package/dist/ui/TeamPanel.d.ts +35 -41
  116. package/dist/ui/TeamPanel.js +53 -163
  117. package/dist/ui/TodoPanel.d.ts +17 -14
  118. package/dist/ui/TodoPanel.js +146 -38
  119. package/dist/ui/Transcript.js +23 -3
  120. package/dist/ui/composer-input.d.ts +10 -0
  121. package/dist/ui/composer-input.js +57 -24
  122. package/dist/ui/entries/TeamCard.d.ts +18 -2
  123. package/dist/ui/entries/TeamCard.js +47 -13
  124. package/dist/ui/entries/TodoCard.d.ts +26 -11
  125. package/dist/ui/entries/TodoCard.js +56 -15
  126. package/dist/ui/glyphs.d.ts +19 -0
  127. package/dist/ui/glyphs.js +4 -0
  128. package/dist/ui/layout/agents-board.d.ts +102 -0
  129. package/dist/ui/layout/agents-board.js +389 -0
  130. package/dist/ui/layout/cells.d.ts +61 -0
  131. package/dist/ui/layout/cells.js +156 -0
  132. package/dist/ui/layout/header-hints.d.ts +23 -3
  133. package/dist/ui/layout/header-hints.js +73 -7
  134. package/dist/ui/layout/kit-chip.d.ts +50 -0
  135. package/dist/ui/layout/kit-chip.js +81 -0
  136. package/dist/ui/layout/status-detail-layout.js +9 -0
  137. package/dist/ui/layout/status-layout.d.ts +13 -0
  138. package/dist/ui/layout/team-row.d.ts +139 -0
  139. package/dist/ui/layout/team-row.js +246 -0
  140. package/dist/ui/layout/todo-layout.d.ts +38 -0
  141. package/dist/ui/layout/todo-layout.js +75 -0
  142. package/dist/ui/layout/virtual-window.js +32 -3
  143. package/dist/ui/overlays/AgentsBoardOverlay.d.ts +57 -0
  144. package/dist/ui/overlays/AgentsBoardOverlay.js +183 -0
  145. package/dist/ui/overlays/HelpOverlay.js +24 -2
  146. package/dist/ui/overlays/KitEditor.d.ts +23 -0
  147. package/dist/ui/overlays/KitEditor.js +101 -0
  148. package/dist/ui/overlays/KitManager.d.ts +23 -0
  149. package/dist/ui/overlays/KitManager.js +104 -0
  150. package/dist/ui/overlays/KitPicker.d.ts +49 -0
  151. package/dist/ui/overlays/KitPicker.js +58 -0
  152. package/dist/ui/overlays/ModelProfilePicker.js +16 -6
  153. package/dist/ui/overlays/PromptEditorPage.d.ts +48 -0
  154. package/dist/ui/overlays/PromptEditorPage.js +227 -0
  155. package/dist/ui/overlays/PromptLibraryOverlay.d.ts +37 -0
  156. package/dist/ui/overlays/PromptLibraryOverlay.js +191 -0
  157. package/dist/ui/overlays/PromptPicker.d.ts +36 -0
  158. package/dist/ui/overlays/PromptPicker.js +129 -0
  159. package/dist/ui/overlays/PromptTextArea.d.ts +86 -0
  160. package/dist/ui/overlays/PromptTextArea.js +141 -0
  161. package/dist/ui/overlays/SettingsScreen.js +139 -16
  162. package/dist/ui/overlays/kit-picker-state.d.ts +87 -0
  163. package/dist/ui/overlays/kit-picker-state.js +136 -0
  164. package/dist/ui/overlays/kit-settings-state.d.ts +129 -0
  165. package/dist/ui/overlays/kit-settings-state.js +267 -0
  166. package/dist/ui/overlays/model-profile-state.d.ts +26 -3
  167. package/dist/ui/overlays/model-profile-state.js +59 -6
  168. package/dist/ui/overlays/prompt-library-state.d.ts +162 -0
  169. package/dist/ui/overlays/prompt-library-state.js +335 -0
  170. package/dist/ui/overlays/prompt-picker-state.d.ts +71 -0
  171. package/dist/ui/overlays/prompt-picker-state.js +98 -0
  172. package/dist/ui/scrollback/LiveTail.d.ts +2 -2
  173. package/dist/ui/scrollback/LiveTail.js +7 -2
  174. package/dist/ui/scrollback/band.js +14 -1
  175. package/dist/ui/scrollback/live-preview.js +10 -3
  176. package/dist/ui/scrollback/tail-window.d.ts +12 -0
  177. package/dist/ui/scrollback/tail-window.js +17 -2
  178. package/dist/ui/scrollback/use-scrollback-view.js +7 -0
  179. package/dist/ui/team-status.d.ts +166 -0
  180. package/dist/ui/team-status.js +252 -0
  181. package/dist/ui/transcript-text.js +13 -4
  182. package/dist/ui/use-startup-notices.d.ts +35 -1
  183. package/dist/ui/use-startup-notices.js +29 -2
  184. package/dist/ui/use-wheel-routing.js +10 -2
  185. package/package.json +2 -2
@@ -1,148 +1,20 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
3
  import Spinner from 'ink-spinner';
4
4
  import { pickGlyphs } from './glyphs.js';
5
+ import { childStatus, isRunningStatus, rosterCounts, rosterHeadline, statusColor, } from './team-status.js';
5
6
  import { formatDuration } from '../agent/usage.js';
6
- import { describeToolActivity, sanitizeActivity } from '../team/activity.js';
7
- import { TEAM_LIMITS } from '../team/limits.js';
8
7
  import { buildTeamPanelLayout } from './layout/team-panel.js';
9
- function isRunning(run) {
10
- return run.phase === 'starting' || run.phase === 'thinking' || run.phase === 'tool' ||
11
- run.phase === 'waiting';
12
- }
13
- function isSettled(run) {
14
- return run.phase === 'done' || run.phase === 'failed' || run.phase === 'aborted';
15
- }
16
- function statusColor(run, theme) {
17
- if (run.phase === 'failed' || run.phase === 'aborted')
18
- return theme.toolError;
19
- if (run.phase === 'done')
20
- return theme.toolDone;
21
- if (isRunning(run))
22
- return theme.toolRunning;
23
- return theme.muted;
24
- }
25
- /**
26
- * How many characters the activity column may spend at this width (P0-1).
27
- *
28
- * `TEAM_LIMITS.activityChars` is a STORAGE ceiling; a terminal has a COLUMN
29
- * budget, and the two are not the same number. The row is
30
- *
31
- * marker(3) + label(8) + description(flex) + 2 + activity + 2 + elapsed(~6)
32
- *
33
- * and the comment on the description cell below promises that activity and
34
- * elapsed never lose characters, i.e. that `description` absorbs ALL width
35
- * pressure. A 64-character prose tail plus `writing: ` is 73, which on an
36
- * 80-column terminal needs 94 columns before `description` gets one. The promise
37
- * cannot be kept by wishing; the column has to be sized from `cols`.
38
- *
39
- * `WIDE_CHAR_ALLOWANCE` is the concession to there being no display-width helper
40
- * in production code: this budget is denominated in code units while the
41
- * terminal is denominated in columns, and CJK and emoji occupy two columns per
42
- * unit, so the budget is halved against the reserve rather than spent to the
43
- * last cell. Erring small costs a few characters of prose; erring large costs
44
- * the elapsed column.
45
- */
46
- const ROW_RESERVED_COLS = 3 + 8 + 2 + 2 + 8 + 12; // marker, label, gaps, elapsed, min description
47
- const WIDE_CHAR_ALLOWANCE = 2;
48
- export function activityBudget(cols) {
49
- const spare = Math.floor((cols - ROW_RESERVED_COLS) / WIDE_CHAR_ALLOWANCE);
50
- return Math.max(TEAM_LIMITS.activityMinChars, Math.min(TEAM_LIMITS.activityChars, spare));
51
- }
52
- /**
53
- * The middle column: what this child is doing right now, in a few words.
54
- *
55
- * Two width policies, answering different questions. `wide` decides WHICH FACT
56
- * to show - a whole path or its basename, a whole command line or its program
57
- * name - mirroring `StatusBar`'s `statusCompactCols` degradation rather than
58
- * inventing a second threshold. `activityBudget(cols)` decides HOW MUCH OF IT
59
- * FITS, and it is a continuous function of width rather than a threshold,
60
- * because the failure it prevents - a row wider than the terminal - is
61
- * continuous.
62
- *
63
- * Exported for the same reason `buildSubagentTools` is: the width bound is the
64
- * property this round has to keep, and asserting it through a rendered frame
65
- * cannot distinguish "the arithmetic clamped it" from "Ink truncated it" - which
66
- * is precisely the confusion that let v1's fixed-width column look fine at 100
67
- * columns and break at 80.
68
- */
69
- export function activityLine(run, cols) {
70
- const wide = cols >= TEAM_LIMITS.activityWideCols;
71
- const budget = activityBudget(cols);
72
- switch (run.phase) {
73
- case 'queued':
74
- return 'queued';
75
- case 'starting':
76
- // The one row that legitimately restarts its clock, saying why (P2-4):
77
- // F-2 ranks the running group by `startedAt` and a retry re-stamps it.
78
- return run.retries ? 'starting (retry)' : 'starting';
79
- case 'thinking':
80
- // Clamped AT RENDER, against this terminal's budget. `run.activity` was
81
- // clamped at STORE against `activityChars`; that is a ceiling, not a fit.
82
- return run.activity ? `writing: ${sanitizeActivity(run.activity, budget)}` : 'thinking';
83
- case 'tool':
84
- case 'waiting':
85
- return run.lastTool
86
- ? sanitizeActivity(describeToolActivity(run.lastTool, run.activityArgs, wide), budget)
87
- : run.phase === 'waiting'
88
- ? 'waiting for mail'
89
- : 'tool';
90
- case 'done':
91
- // `compacted N` ON THE SETTLED ROW ONLY (hardening §3.4.4 / W3). A running
92
- // row is already competing for `activityBudget(cols)` with the child's own
93
- // prose, and a marker that displaces what the child is DOING right now
94
- // buys nothing - the dispatch report carries the same fact for good.
95
- return run.compactions
96
- ? `done ${run.turns} turns compacted ${run.compactions}`
97
- : `done ${run.turns} turns`;
98
- case 'failed':
99
- return 'failed';
100
- case 'aborted':
101
- return 'aborted';
102
- }
103
- }
104
- /** The past-tense word the supervisor badge and status line render. */
105
- function interventionWord(action) {
106
- switch (action) {
107
- case 'nudge':
108
- return 'nudged';
109
- case 'replace':
110
- return 'replaced';
111
- case 'abandon':
112
- return 'abandoned';
113
- default:
114
- return 'waited';
115
- }
116
- }
8
+ import { interventionWord, teamRowCells, teamRowGeometry, TEAM_CLOCK_CELLS, } from './layout/team-row.js';
9
+ import { fitCells } from './layout/cells.js';
117
10
  /**
118
- * The row's supervisor badge (subagent-overseer-v2 AC-5): what the supervisor
119
- * last did to THIS child and how long ago, e.g. `nudged 2m03s`.
120
- *
121
- * `lastIntervention.at` is ABSOLUTE epoch ms (R-P2-3), the same units as
122
- * `startedAt`, so this is one subtraction - no second clock convention.
11
+ * THE ACTIVITY / BADGE HELPERS LIVE IN `layout/team-row.ts` NOW (v2 implementation
12
+ * note, recorded in the spec's "implementation findings"). `teamRowCells` has to
13
+ * compose the activity text with the supervisor badge, so keeping those functions
14
+ * in this component would make the two modules import each other. They are
15
+ * re-exported here so every existing import site keeps its path.
123
16
  */
124
- export function overseerBadge(run, at) {
125
- const last = run.lastIntervention;
126
- if (last === undefined)
127
- return '';
128
- return `${interventionWord(last.action)} ${formatDuration(Math.max(0, at - last.at))}`;
129
- }
130
- /**
131
- * The activity column WITH the badge composed in (AC-5 / risk table): the
132
- * badge rides the SAME `activityBudget(cols)` allocation the prose uses, so
133
- * a row can never grow past its terminal; on a narrow terminal the badge
134
- * degrades to the action word alone - the who-acted fact outranks the
135
- * what-they-are-doing tail exactly when there is room for only one.
136
- */
137
- export function activityWithBadge(run, cols, at) {
138
- const last = run.lastIntervention;
139
- if (last === undefined)
140
- return activityLine(run, cols);
141
- const badge = overseerBadge(run, at);
142
- if (cols < TEAM_LIMITS.activityWideCols)
143
- return interventionWord(last.action);
144
- return sanitizeActivity(`${badge}; ${activityLine(run, cols)}`, activityBudget(cols));
145
- }
17
+ export { activityBudget, activityLine, activityWithBadge, overseerBadge, teamClockText, teamRowCells, teamRowGeometry, } from './layout/team-row.js';
146
18
  /**
147
19
  * The panel's one supervisor status line (AC-5): the most recent intervention
148
20
  * across the roster, human-phrased. Empty when the supervisor has not acted
@@ -164,6 +36,9 @@ export function supervisorStatusLine(runs, at) {
164
36
  const head = latest.reasonHead.length > 0 ? ` (${latest.reasonHead})` : '';
165
37
  return `supervisor: ${interventionWord(latest.action)} ${label} ${ago} ago${head}`;
166
38
  }
39
+ /** Columns the `ctrl+b` hint cell spends, and the width below which it is dropped. */
40
+ const HINT_CELLS = 'ctrl+b'.length;
41
+ const HINT_MIN_COLS = 100;
167
42
  export function TeamPanel({ snapshot, rows, cols, reducedMotion, theme, caps, now, layout, }) {
168
43
  const projection = layout ?? buildTeamPanelLayout({
169
44
  snapshot, terminalRows: rows, availableRows: Infinity,
@@ -172,42 +47,57 @@ export function TeamPanel({ snapshot, rows, cols, reducedMotion, theme, caps, no
172
47
  return null;
173
48
  const glyphs = pickGlyphs(caps);
174
49
  const at = now ?? Date.now();
175
- const running = snapshot.runs.filter(isRunning).length;
176
- const done = snapshot.runs.filter(isSettled).length;
50
+ // ONE COUNTING AUTHORITY (A-1): `queued` used to fall between `isRunning` and
51
+ // `isSettled`, so a fresh dispatch read `0 running 0 done` above rows that each
52
+ // said `queued`.
53
+ const counts = rosterCounts(snapshot.runs);
177
54
  const elapsed = formatDuration(Math.max(0, at - snapshot.startedAt));
178
- const header = (_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Text, { wrap: "truncate", color: theme.accent, bold: true, children: [glyphs.teamAgent, " team", ' ', _jsxs(Text, { color: theme.muted, children: [running, " running ", glyphs.midDot, " ", done, " done ", glyphs.midDot, " ", elapsed] })] }), snapshot.messageCount > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [glyphs.teamMail, " ", snapshot.messageCount] }))] }));
55
+ const g = teamRowGeometry(cols, glyphs);
56
+ // WIDTH IS BUDGETED ONCE (I-13). `rosterHeadline` subtracts its own
57
+ // HEADER_RESERVED_COLS(16) - it takes the ROW's column count, not a budget - so
58
+ // the caller may only take away the cells this feature ADDS. Subtracting 16 here
59
+ // too would spend the reserve twice and drop `done` / `queued` from a 100-column
60
+ // header that has room for them.
61
+ const showHint = cols >= HINT_MIN_COLS && !projection.collapsed;
62
+ const headlineCols = Math.max(0, cols - TEAM_CLOCK_CELLS - (showHint ? HINT_CELLS : 0));
63
+ const mailText = `${glyphs.teamMail} ${snapshot.messageCount}`;
64
+ // The badge takes its NATURAL width and the flexible left cell gives way: it is
65
+ // the only header cell whose content length varies, and pinning it to a computed
66
+ // width would clip `@ 12` on a row that has room for it.
67
+ const mailCells = Math.min(mailText.length, Math.max(0, cols - TEAM_CLOCK_CELLS));
68
+ /**
69
+ * The header is the row's own geometry one level up: a flexible left cell and
70
+ * fixed right-hand cells, so the dispatch clock and every child clock stand in
71
+ * the SAME column (I-5 / AC-2).
72
+ *
73
+ * THE ELAPSED TIME APPEARS ONLY IN THE CLOCK CELL (§4.3 / RV-P2-9). The old
74
+ * header also carried it inline among the count clauses; with a right-edge clock
75
+ * that would be the same number twice, and the pair could disagree for one frame
76
+ * every 200 ms.
77
+ */
78
+ const header = (_jsxs(Box, { flexDirection: "row", width: "100%", height: 1, flexShrink: 0, overflow: "hidden", children: [_jsx(Box, { flexGrow: 1, flexShrink: 1, overflow: "hidden", children: _jsxs(Text, { wrap: "truncate", color: theme.accent, bold: true, children: [glyphs.teamAgent, ' team ', _jsx(Text, { color: theme.muted, children: rosterHeadline(counts, glyphs, headlineCols) })] }) }), showHint && (_jsx(Box, { width: HINT_CELLS, flexShrink: 0, justifyContent: "flex-end", children: _jsx(Text, { color: theme.hintFg, children: "ctrl+b" }) })), snapshot.messageCount > 0 && (_jsx(Box, { width: mailCells, flexShrink: 0, justifyContent: "flex-end", children: _jsx(Text, { wrap: "truncate", color: theme.muted, children: mailText }) })), _jsx(Box, { width: TEAM_CLOCK_CELLS, flexShrink: 0, justifyContent: "flex-end", children: _jsx(Text, { wrap: "truncate", color: theme.accent, bold: true, children: elapsed }) })] }));
179
79
  // Short terminals or a long draft leave only the shared header projection,
180
- // which still answers whether anything is running and for how long.
80
+ // which still answers whether anything is running and for how long. The header
81
+ // keeps the count clauses AND the right-edge clock, so the collapsed form loses
82
+ // no information - only the per-child rows.
181
83
  if (projection.collapsed) {
182
- return _jsx(Box, { flexDirection: "column", flexShrink: 0, children: header });
84
+ return _jsx(Box, { flexDirection: "column", width: "100%", flexShrink: 0, children: header });
183
85
  }
184
86
  // RANKED, not sliced by array index (F-2). The slot pool hands out indices in
185
87
  // order, so `slice(0, 5)` showed the five children that finished FIRST and hid
186
88
  // everything still working behind `+N more` - at exactly the fan-out widths
187
89
  // the requirement permits.
188
90
  const { visible, hiddenTotal, hiddenRunning } = projection;
189
- return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [header, visible.map((run) => {
190
- const color = statusColor(run, theme);
91
+ const supervisorLine = supervisorStatusLine(snapshot.runs, at);
92
+ return (_jsxs(Box, { flexDirection: "column", width: "100%", flexShrink: 0, children: [header, visible.map((run) => {
93
+ const status = childStatus(run);
94
+ const color = statusColor(status, theme);
191
95
  // One spinner per RUNNING row, and the row cap is 5 precisely because
192
96
  // five is the documented maximum number of simultaneous spinners. Both
193
97
  // `reducedMotion` and a non-Unicode terminal fall back to the same
194
98
  // static marker, exactly as `AssistantEntry` and `ToolCard` already do.
195
- const animate = isRunning(run) && !reducedMotion && caps.unicode;
196
- return (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: color, children: [' ', animate ? _jsx(Spinner, { type: "dots" }) : marker(run, glyphs), ' '] }), _jsx(Text, { color: theme.primary, children: run.label.padEnd(8).slice(0, 8) }), _jsx(Box, { flexGrow: 1, flexShrink: 1, overflow: "hidden", children: _jsx(Text, { wrap: "truncate", color: theme.muted, children: run.description.replace(/\s+/g, ' ') }) }), _jsx(Box, { flexShrink: 1, overflow: "hidden", children: _jsxs(Text, { wrap: "truncate", color: color, children: [' ', activityWithBadge(run, cols, at)] }) }), _jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', run.startedAt ? formatDuration(Math.max(0, (run.endedAt ?? at) - run.startedAt)) : ''] })] }, run.label));
197
- }), hiddenTotal > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', "+", hiddenTotal, " more", hiddenRunning > 0 ? ` (${hiddenRunning} running)` : ''] })), (() => {
198
- // The panel's supervisor line (AC-5): one row, only when the
199
- // supervisor has actually acted - the per-row badges above already
200
- // carry the per-child facts.
201
- const line = supervisorStatusLine(snapshot.runs, at);
202
- return line ? (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', line] })) : null;
203
- })(), snapshot.lastMessage && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', glyphs.teamMail, " ", snapshot.lastMessage.from, " ", glyphs.arrowRight, " ", snapshot.lastMessage.to, ": ", snapshot.lastMessage.subject.replace(/\s+/g, ' ')] }))] }));
204
- }
205
- function marker(run, glyphs) {
206
- if (run.phase === 'done')
207
- return glyphs.toolDone;
208
- if (run.phase === 'failed' || run.phase === 'aborted')
209
- return glyphs.toolError;
210
- if (run.phase === 'queued')
211
- return glyphs.toolPending;
212
- return glyphs.spinnerStill;
99
+ const animate = isRunningStatus(status) && !reducedMotion && caps.unicode;
100
+ const cells = teamRowCells(run, cols, at, glyphs);
101
+ return (_jsxs(Box, { flexDirection: "row", width: "100%", height: 1, flexShrink: 0, overflow: "hidden", children: [_jsx(Box, { width: g.marker, flexShrink: 0, children: _jsxs(Text, { wrap: "truncate", color: color, children: [' ', animate ? _jsx(Spinner, { type: "dots" }) : cells.marker, ' '] }) }), _jsx(Box, { width: g.label, flexShrink: 0, children: _jsx(Text, { wrap: "truncate", color: theme.primary, children: cells.label }) }), _jsx(Box, { flexGrow: 1, flexShrink: 1, overflow: "hidden", children: _jsx(Text, { wrap: "truncate", color: theme.muted, children: cells.description }) }), _jsx(Box, { width: g.gap, flexShrink: 0, children: _jsx(Text, { children: " " }) }), _jsx(Box, { width: g.activity, flexShrink: 0, overflow: "hidden", children: _jsx(Text, { wrap: "truncate", color: color, children: cells.activity }) }), _jsx(Box, { width: g.clock, flexShrink: 0, justifyContent: "flex-end", children: _jsx(Text, { wrap: "truncate", color: theme.muted, children: cells.clock }) })] }, run.label));
102
+ }), hiddenTotal > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', "+", hiddenTotal, " more", hiddenRunning > 0 ? ` (${hiddenRunning} running)` : ''] })), supervisorLine.length > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', fitCells(supervisorLine, Math.max(0, cols - 2), glyphs.ellipsis)] })), snapshot.lastMessage && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', glyphs.teamMail, " ", snapshot.lastMessage.from, " ", glyphs.arrowRight, " ", snapshot.lastMessage.to, ": ", snapshot.lastMessage.subject.replace(/\s+/g, ' ')] }))] }));
213
103
  }
@@ -1,5 +1,6 @@
1
1
  /**
2
- * TodoPanel — the right rail (todo-plan-execution §6.1).
2
+ * TodoPanel — the right rail (todo-plan-execution §6.1; two sections per
3
+ * todo-acceptance-criteria §6.9).
3
4
  *
4
5
  * ASCII ONLY: every glyph comes from `pickGlyphs`; a literal here bypasses the
5
6
  * capability probe and shows mojibake on a legacy console (`glyphs.ts` header).
@@ -8,22 +9,24 @@
8
9
  * this component is not rendered at all, so a session that never plans has zero
9
10
  * columns and zero layout shift (R-g's "no furniture when idle" / AC-24).
10
11
  *
12
+ * NO NEW PROP CARRIES THE ACCEPTANCE TABLE (§6.9.1): `snapshot.acceptance` is
13
+ * already inside `snapshot`, and `null` IS the projection of "the capability is
14
+ * off", because a disabled store commits an empty table. Keeping the props surface
15
+ * narrow is what keeps `App`'s two mount points untouched.
16
+ *
11
17
  * IT MUST BE MOUNTED IN `AppShell`'s MIDDLE BAND, NEVER IN THE BOTTOM CHROME
12
18
  * (C-6 / I-4). This is a COLUMN BESIDE the viewport, and the middle band is the
13
- * only row-wrapped region there is; moved down into the bottom chrome it would
14
- * stop being a rail and start eating viewport height, growing and shrinking the
19
+ * only row-wrapped region there is; moved down into the bottom chrome it would stop
20
+ * being a rail and start eating viewport height, growing and shrinking the
15
21
  * transcript every time the list changed length.
16
22
  *
17
23
  * WIDTH IS MEASURED IN COLUMNS, NEVER IN CODE UNITS (P2-3). This rail is 14-36
18
24
  * columns wide and the system prompt's last line is "Respond in the language the
19
- * user writes in", so CJK item text in an 18-column rail is the NORMAL case
20
- * rather than the edge case. Every text cell is `wrap="truncate"` and Ink's
21
- * truncation is display-width aware, so nothing can overflow the box — and NO
22
- * ARITHMETIC HERE MAY BUDGET USER TEXT BY `.length`. The count string and
23
- * `padStart(2)` cells below are ASCII counts and indices, which is why they are
24
- * safe; a "characters that fit" calculation over `content` would not be. That is
25
- * the whole of the lesson `TeamPanel`'s `WIDE_CHAR_ALLOWANCE` records at length,
26
- * and the mitigation here is to have no such budget at all.
25
+ * user writes in", so CJK text in an 18-column rail is the NORMAL case rather than
26
+ * the edge case. Every text cell is `wrap="truncate"` and Ink's truncation is
27
+ * display-width aware, so nothing can overflow the box - and NO ARITHMETIC HERE MAY
28
+ * BUDGET USER TEXT BY `.length`. The count strings and `padStart(2)` cells below are
29
+ * ASCII counts and indices, which is why they are safe.
27
30
  */
28
31
  import React from 'react';
29
32
  import type { Theme } from './theme.js';
@@ -35,9 +38,9 @@ export interface TodoPanelProps {
35
38
  width: number;
36
39
  /**
37
40
  * From `buildTodoRailLayout` after actual team/menu rows are subtracted.
38
- * (I-10 / P1-3). The gate that mounts this component and the number it lays
39
- * out against are ONE value computed once in `App`: a panel that mounts on one
40
- * number and lays out against another renders nothing and reports nothing.
41
+ * (I-10 / P1-3). The gate that mounts this component and the number it lays out
42
+ * against are ONE value computed once in `App`: a panel that mounts on one number
43
+ * and lays out against another renders nothing and reports nothing.
41
44
  */
42
45
  rows: number;
43
46
  /** Whether the agent is running; gates the spinner (never animate when idle). */
@@ -1,6 +1,7 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
2
  /**
3
- * TodoPanel — the right rail (todo-plan-execution §6.1).
3
+ * TodoPanel — the right rail (todo-plan-execution §6.1; two sections per
4
+ * todo-acceptance-criteria §6.9).
4
5
  *
5
6
  * ASCII ONLY: every glyph comes from `pickGlyphs`; a literal here bypasses the
6
7
  * capability probe and shows mojibake on a legacy console (`glyphs.ts` header).
@@ -9,22 +10,24 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
9
10
  * this component is not rendered at all, so a session that never plans has zero
10
11
  * columns and zero layout shift (R-g's "no furniture when idle" / AC-24).
11
12
  *
13
+ * NO NEW PROP CARRIES THE ACCEPTANCE TABLE (§6.9.1): `snapshot.acceptance` is
14
+ * already inside `snapshot`, and `null` IS the projection of "the capability is
15
+ * off", because a disabled store commits an empty table. Keeping the props surface
16
+ * narrow is what keeps `App`'s two mount points untouched.
17
+ *
12
18
  * IT MUST BE MOUNTED IN `AppShell`'s MIDDLE BAND, NEVER IN THE BOTTOM CHROME
13
19
  * (C-6 / I-4). This is a COLUMN BESIDE the viewport, and the middle band is the
14
- * only row-wrapped region there is; moved down into the bottom chrome it would
15
- * stop being a rail and start eating viewport height, growing and shrinking the
20
+ * only row-wrapped region there is; moved down into the bottom chrome it would stop
21
+ * being a rail and start eating viewport height, growing and shrinking the
16
22
  * transcript every time the list changed length.
17
23
  *
18
24
  * WIDTH IS MEASURED IN COLUMNS, NEVER IN CODE UNITS (P2-3). This rail is 14-36
19
25
  * columns wide and the system prompt's last line is "Respond in the language the
20
- * user writes in", so CJK item text in an 18-column rail is the NORMAL case
21
- * rather than the edge case. Every text cell is `wrap="truncate"` and Ink's
22
- * truncation is display-width aware, so nothing can overflow the box — and NO
23
- * ARITHMETIC HERE MAY BUDGET USER TEXT BY `.length`. The count string and
24
- * `padStart(2)` cells below are ASCII counts and indices, which is why they are
25
- * safe; a "characters that fit" calculation over `content` would not be. That is
26
- * the whole of the lesson `TeamPanel`'s `WIDE_CHAR_ALLOWANCE` records at length,
27
- * and the mitigation here is to have no such budget at all.
26
+ * user writes in", so CJK text in an 18-column rail is the NORMAL case rather than
27
+ * the edge case. Every text cell is `wrap="truncate"` and Ink's truncation is
28
+ * display-width aware, so nothing can overflow the box - and NO ARITHMETIC HERE MAY
29
+ * BUDGET USER TEXT BY `.length`. The count strings and `padStart(2)` cells below are
30
+ * ASCII counts and indices, which is why they are safe.
28
31
  */
29
32
  import { useMemo } from 'react';
30
33
  import { Box, Text } from 'ink';
@@ -34,18 +37,21 @@ import { buildGauge } from './gauge.js';
34
37
  import { railBorderProps } from './layout/Gutter.js';
35
38
  import { TODO_RAIL_INDEX_MIN_COLS } from './layout/rail.js';
36
39
  import { TODO_LIMITS } from '../todo/limits.js';
37
- import { selectCompactTodoRows, selectTodoRows } from '../todo/panel-rows.js';
38
- /** Header row + the blank row under the gauge. */
39
- const FIXED_ROWS = 2;
40
+ import { derivePlanPhase } from '../todo/acceptance.js';
41
+ import { buildPlanSections } from '../todo/plan-sections.js';
42
+ import { selectAcceptanceRows, selectCompactAcceptanceRows, selectCompactTodoRows, selectTodoRows, } from '../todo/panel-rows.js';
40
43
  /** The gauge is the one purely decorative element, so it is the first to go. */
41
44
  const GAUGE_MIN_ROWS = 8;
42
45
  const GAUGE_MIN_COLS = 20;
43
46
  /** Separator column + one space of padding, supplied by `railBorderProps`. */
44
47
  const CHROME_COLS = 2;
45
- /** Marker cell. Three, because the ASCII tier's `[x]` / `[ ]` are three wide. */
48
+ /** Marker cell. Three, because the ASCII tier's `[x]` / `[ ]` / `[!]` are three wide. */
46
49
  const MARKER_COLS = 3;
47
50
  /** Index cell: two digits plus the space that separates it from the text. */
48
51
  const INDEX_COLS = 3;
52
+ /** `ACCEPT` / `VERIFY` are both six columns, so switching phase shifts nothing (D-8). */
53
+ const ACCEPT_LABEL = 'ACCEPT';
54
+ const VERIFY_LABEL = 'VERIFY';
49
55
  function marker(item, glyphs) {
50
56
  // Ink's tokenizer otherwise treats the bare emoji-capable check as one cell,
51
57
  // while string-width counts two, adding an extra visible space to each row.
@@ -56,6 +62,21 @@ function marker(item, glyphs) {
56
62
  return glyphs.todoActive;
57
63
  return glyphs.todoPending;
58
64
  }
65
+ /**
66
+ * `passed` REUSES THE DONE GLYPH ON PURPOSE (D-9): it is the same settled positive,
67
+ * so two colour ramps and two shapes for one meaning would be learned twice for
68
+ * nothing. `failed` is the only new meaning, and it gets the only new glyph.
69
+ */
70
+ function acceptanceMarker(item, glyphs) {
71
+ if (item.status === 'passed') {
72
+ return glyphs.todoDone.length === 1 ? `${glyphs.todoDone}\uFE0F` : glyphs.todoDone;
73
+ }
74
+ if (item.status === 'failed')
75
+ return glyphs.todoFail;
76
+ if (item.status === 'in_progress')
77
+ return glyphs.todoActive;
78
+ return glyphs.todoPending;
79
+ }
59
80
  function rowColor(item, theme) {
60
81
  if (item.status === 'completed')
61
82
  return theme.muted;
@@ -63,44 +84,131 @@ function rowColor(item, theme) {
63
84
  return theme.primary;
64
85
  return theme.hintFg ?? theme.muted;
65
86
  }
87
+ function acceptanceRowColor(item, theme) {
88
+ if (item.status === 'failed')
89
+ return theme.toolError;
90
+ if (item.status === 'passed')
91
+ return theme.muted;
92
+ if (item.status === 'in_progress')
93
+ return theme.primary;
94
+ return theme.hintFg ?? theme.muted;
95
+ }
66
96
  export function TodoPanel({ snapshot, width, rows, running, reducedMotion, theme, caps, }) {
67
97
  const glyphs = pickGlyphs(caps);
68
- // Ink reapplies all four Yoga borders when the borderStyle object changes,
69
- // but unchanged borderTop/Bottom=false props are not reapplied on that update.
98
+ // Ink reapplies all four Yoga borders when the borderStyle object changes, but
99
+ // unchanged borderTop/Bottom=false props are not reapplied on that update.
70
100
  const border = useMemo(() => railBorderProps(glyphs.railVertical), [glyphs.railVertical]);
71
101
  if (snapshot.items.length === 0 || rows < TODO_LIMITS.panelMinRows)
72
102
  return null;
73
103
  const compact = rows < TODO_LIMITS.panelFullRows;
74
104
  const showGauge = width >= GAUGE_MIN_COLS && rows >= GAUGE_MIN_ROWS;
75
105
  const showIndex = !compact && width >= TODO_RAIL_INDEX_MIN_COLS;
76
- const compactFooter = compact && snapshot.items.length > rows - 1;
77
- const itemRows = compact ? rows - 1 - Number(compactFooter)
78
- : rows - FIXED_ROWS - Number(showGauge);
79
- const { visible, hiddenAbove, hiddenBelow } = compact
80
- ? selectCompactTodoRows(snapshot.items, itemRows)
81
- : selectTodoRows(snapshot.items, itemRows);
106
+ const acceptance = snapshot.acceptance;
107
+ const acceptanceItems = acceptance?.items ?? [];
108
+ const phase = derivePlanPhase(snapshot.items, acceptanceItems);
109
+ // EVERY ROW FIGURE COMES FROM ONE FUNCTION (§6.9.2). Not because arithmetic in a
110
+ // component is hard, but because `/perf` and the status-bar chip have to report
111
+ // the same answer, and a second copy is a second answer.
112
+ const sections = buildPlanSections({
113
+ rows,
114
+ width,
115
+ phase,
116
+ todoCount: snapshot.items.length,
117
+ acceptanceCount: acceptanceItems.length,
118
+ showGauge,
119
+ });
120
+ const compactFooter = sections.todoFooter;
121
+ const todoWindow = compact
122
+ ? selectCompactTodoRows(snapshot.items, sections.todoItemRows)
123
+ : selectTodoRows(snapshot.items, sections.todoItemRows);
124
+ const acceptanceSection = sections.acceptance;
125
+ const acceptanceWindow = acceptanceSection && acceptanceSection.itemRows > 0
126
+ ? compact
127
+ ? selectCompactAcceptanceRows(acceptanceItems, acceptanceSection.itemRows)
128
+ : selectAcceptanceRows(acceptanceItems, acceptanceSection.itemRows)
129
+ : { visible: [], hiddenAbove: 0, hiddenBelow: 0 };
82
130
  const pct = snapshot.total > 0 ? Math.round((snapshot.doneCount / snapshot.total) * 100) : 0;
83
131
  const gauge = buildGauge(pct, Math.max(4, width - CHROME_COLS - 2), theme, caps);
84
132
  const complete = snapshot.doneCount === snapshot.total;
85
133
  const countText = `${snapshot.doneCount}/${snapshot.total}`;
86
- const showDone = complete
87
- && width - CHROME_COLS >= 'TODO '.length + countText.length + ' done'.length;
134
+ const showDone = complete && fitsDone(width, countText, 'TODO');
88
135
  const count = `${countText}${showDone ? ' done' : ''}`;
136
+ // ONE CALL EACH: the text is both the header's right-hand cell and the width that
137
+ // cell is measured with, so computing it twice invites the two to drift.
138
+ const acceptanceCountLabel = acceptanceCountText(acceptance, width);
139
+ const acceptanceTone = acceptanceColor(acceptance, theme);
140
+ // The `summary` rung has NO `done` rung (§6.9.3): one muted line, or the error
141
+ // colour when something failed. Green is a verdict about a table the narrow rail is
142
+ // not showing, and claiming it in one line would be the only place in this feature
143
+ // where a colour says more than the text does.
144
+ const hasFailed = (acceptance?.failedCount ?? 0) > 0;
145
+ const acceptanceSummaryTone = hasFailed ? theme.toolError : theme.muted;
146
+ // `verify` collapses the step list to ONE row: every step is done, so seven rows
147
+ // of "completed" buy no information and cost the rows the eye needs (D-7). The
148
+ // marker cell is still reserved so the two sections stay column-aligned.
149
+ const showTodoItems = phase !== 'verify';
150
+ // THE `-N +M` FOOTER DESCRIBES A WINDOW THAT IS ON SCREEN. In `verify` the list is
151
+ // collapsed on purpose, so the marker would advertise rows the user cannot reach
152
+ // (`-0 +5` under "all steps complete" is the exact opposite of the sentence above
153
+ // it). The rows it would have occupied stay blank inside the fixed-height box.
154
+ const showCompactFooter = compactFooter && showTodoItems;
89
155
  return (
90
156
  // `flexShrink={0}` IS LOAD-BEARING (P1-4). Ink's `Box.defaultProps` is
91
- // `{ flexWrap:'nowrap', flexDirection:'row', flexGrow:0, flexShrink:1 }`, so
92
- // a `width={n}` box inside a shrinking row is SQUEEZED under pressure — and
93
- // every piece of arithmetic in `rail.ts` silently becomes a suggestion at
94
- // exactly the narrow widths where the guarantee matters. `ScrollIndicator`
95
- // spells the same pair one level down. Height uses the same row budget as
96
- // selection, so content cannot silently enlarge the middle band.
97
- _jsxs(Box, { flexDirection: "column", flexShrink: 0, width: width, height: rows, overflow: "hidden", paddingLeft: 1, ...border, borderColor: theme.border, children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsx(Box, { flexGrow: 1, flexShrink: 1, overflow: "hidden", children: _jsx(Text, { wrap: "truncate", color: theme.accent, bold: true, children: "TODO" }) }), _jsx(Box, { width: count.length, flexShrink: 0, children: _jsx(Text, { wrap: "truncate", color: complete ? theme.toolDone : theme.muted, children: count }) })] }), showGauge && (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: gauge.fillColor, children: gauge.filled }), _jsx(Text, { color: gauge.trackColor, children: gauge.empty })] })), !compact && _jsx(Text, { children: " " }), !compact && hiddenAbove > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', "+", hiddenAbove, " above"] })), visible.map(({ item, index }) => {
157
+ // `{ flexWrap:'nowrap', flexDirection:'row', flexGrow:0, flexShrink:1 }`, so a
158
+ // `width={n}` box inside a shrinking row is SQUEEZED under pressure - and every
159
+ // piece of arithmetic in `rail.ts` silently becomes a suggestion at exactly the
160
+ // narrow widths where the guarantee matters. `ScrollIndicator` spells the same
161
+ // pair one level down. Height uses the same row budget as selection, so content
162
+ // cannot silently enlarge the middle band.
163
+ _jsxs(Box, { flexDirection: "column", flexShrink: 0, width: width, height: rows, overflow: "hidden", paddingLeft: 1, ...border, borderColor: theme.border, children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsx(Box, { flexGrow: 1, flexShrink: 1, overflow: "hidden", children: _jsx(Text, { wrap: "truncate", color: theme.accent, bold: true, children: "TODO" }) }), _jsx(Box, { width: count.length, flexShrink: 0, children: _jsx(Text, { wrap: "truncate", color: complete ? theme.toolDone : theme.muted, children: count }) })] }), showGauge && (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: gauge.fillColor, children: gauge.filled }), _jsx(Text, { color: gauge.trackColor, children: gauge.empty })] })), !compact && _jsx(Text, { children: " " }), !compact && showTodoItems && todoWindow.hiddenAbove > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', "+", todoWindow.hiddenAbove, " above"] })), showTodoItems && todoWindow.visible.map(({ item, index }) => {
98
164
  const active = item.status === 'in_progress';
99
- // The spinner is gated on `running`: a spinner while the agent is idle
100
- // is a lie about the state of the world, and this panel outlives the run
101
- // that filled it. Reduced motion and a non-Unicode terminal fall back to
102
- // the same static marker, exactly as `AssistantEntry` and `TeamPanel` do.
165
+ // The spinner is gated on `running`: a spinner while the agent is idle is a
166
+ // lie about the state of the world, and this panel outlives the run that
167
+ // filled it. Reduced motion and a non-Unicode terminal fall back to the same
168
+ // static marker, exactly as `AssistantEntry` and `TeamPanel` do.
103
169
  const animate = active && running && !reducedMotion && caps.unicode;
104
170
  return (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { flexShrink: 0, width: MARKER_COLS, children: _jsx(Text, { color: rowColor(item, theme), bold: active, children: animate ? _jsx(Spinner, { type: "dots" }) : marker(item, glyphs) }) }), showIndex && (_jsx(Box, { flexShrink: 0, width: INDEX_COLS, children: _jsx(Text, { color: theme.muted, children: String(index + 1).padStart(2) }) })), _jsx(Box, { flexGrow: 1, flexShrink: 1, overflow: "hidden", children: _jsx(Text, { wrap: "truncate", color: rowColor(item, theme), bold: active, children: active ? item.activeForm : item.content }) })] }, `${index}-${item.content}`));
105
- }), compactFooter && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: ["-", hiddenAbove, " +", hiddenBelow] })), !compact && hiddenBelow > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', "+", hiddenBelow, " more"] }))] }, glyphs.railVertical));
171
+ }), phase === 'verify' && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', "all steps complete"] })), showCompactFooter && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: ["-", todoWindow.hiddenAbove, " +", todoWindow.hiddenBelow] })), !compact && showTodoItems && todoWindow.hiddenBelow > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', "+", todoWindow.hiddenBelow, " more"] })), acceptanceSection && acceptanceSection.reason === 'summary' && (_jsxs(Text, { wrap: "truncate", color: acceptanceSummaryTone, children: [' ', "AC ", summaryCount(acceptance)] })), acceptanceSection && acceptanceSection.reason === 'visible' && (_jsxs(_Fragment, { children: [_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsx(Box, { flexGrow: 1, flexShrink: 1, overflow: "hidden", children: _jsx(Text, { wrap: "truncate", color: theme.accent, bold: true, children: phase === 'verify' ? VERIFY_LABEL : ACCEPT_LABEL }) }), _jsx(Box, { width: acceptanceCountLabel.length, flexShrink: 0, children: _jsx(Text, { wrap: "truncate", color: acceptanceTone, children: acceptanceCountLabel }) })] }), acceptanceWindow.visible.map(({ item, index }) => {
172
+ const active = item.status === 'in_progress';
173
+ const animate = active && running && !reducedMotion && caps.unicode;
174
+ return (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { flexShrink: 0, width: MARKER_COLS + 1, children: _jsx(Text, { color: acceptanceRowColor(item, theme), bold: active, children: animate ? _jsx(Spinner, { type: "dots" }) : acceptanceMarker(item, glyphs) }) }), _jsx(Box, { flexGrow: 1, flexShrink: 1, overflow: "hidden", children: _jsx(Text, { wrap: "truncate", color: acceptanceRowColor(item, theme), bold: active, children: item.content }) })] }, `acc-${index}-${item.content}`));
175
+ })] }))] }, glyphs.railVertical));
176
+ }
177
+ /**
178
+ * Does the section's count fit beside its label? The same width guard the TODO
179
+ * header uses for ` done`, so a narrow rail drops the suffix rather than the number.
180
+ */
181
+ function fitsDone(width, countText, label) {
182
+ return width - CHROME_COLS >= label.length + 1 + countText.length + ' done'.length;
183
+ }
184
+ /** `<checked>/<total>`, ` !<failed>`, and ` done` when every criterion passed. */
185
+ function acceptanceCountText(acceptance, width) {
186
+ if (!acceptance)
187
+ return '';
188
+ const base = `${acceptance.checkedCount}/${acceptance.total}`;
189
+ const failed = acceptance.failedCount > 0 ? ` !${acceptance.failedCount}` : '';
190
+ const allPassed = acceptance.checkedCount === acceptance.total && acceptance.failedCount === 0;
191
+ const done = allPassed && fitsDone(width, base, VERIFY_LABEL) ? ' done' : '';
192
+ return `${base}${failed}${done}`;
193
+ }
194
+ /** The `summary` rung's single line: `2/5`, plus ` !1` when something failed. */
195
+ function summaryCount(acceptance) {
196
+ if (!acceptance)
197
+ return '';
198
+ return `${acceptance.checkedCount}/${acceptance.total}` +
199
+ (acceptance.failedCount > 0 ? ` !${acceptance.failedCount}` : '');
200
+ }
201
+ /**
202
+ * All passed reads as done, any failure reads as an error, everything else is
203
+ * muted - the same three-rung colour ladder the TODO header uses.
204
+ */
205
+ function acceptanceColor(acceptance, theme) {
206
+ if (!acceptance)
207
+ return theme.muted;
208
+ if (acceptance.failedCount > 0)
209
+ return theme.toolError;
210
+ if (acceptance.total > 0 && acceptance.checkedCount === acceptance.total) {
211
+ return theme.toolDone;
212
+ }
213
+ return theme.muted;
106
214
  }
@@ -12,6 +12,7 @@ import { QueuedEntry } from './entries/QueuedEntry.js';
12
12
  import { AssistantEntry } from './entries/AssistantEntry.js';
13
13
  import { ToolCard, statusColor, toolEntryGlyph } from './entries/ToolCard.js';
14
14
  import { TeamCard, teamCardColor } from './entries/TeamCard.js';
15
+ import { isLiveTeamCard, rosterCounts } from './team-status.js';
15
16
  import { TodoCard, todoCardColor } from './entries/TodoCard.js';
16
17
  import { RetryCard, retryCardColor } from './entries/RetryCard.js';
17
18
  import { FastCard, fastCardColor } from './entries/FastCard.js';
@@ -42,6 +43,12 @@ export function nowSecFor(entry, nowSec) {
42
43
  // function protects is unchanged for every other card, forever.
43
44
  if (entry.kind === 'compaction' && entry.live)
44
45
  return nowSec;
46
+ // THE FOURTH CONSUMER IS A LIVE DISPATCH CARD (tui-subagent-double-roster A-1):
47
+ // a child's elapsed time must advance while it works, and `TeamCard` reads this
48
+ // same 1 Hz clock instead of a second `Date.now()` per render. A suppressed card
49
+ // never mounts, so this only keeps the reading honest for a directly mounted one.
50
+ if (entry.kind === 'team' && entry.active)
51
+ return nowSec;
45
52
  // THE THIRD CONSUMER IS A LIVE JOB (long-task-jobs D-8), and it is the reason
46
53
  // this stays a single 1 Hz clock: a task's elapsed time and its `no output`
47
54
  // badge both have to move, and a per-card `setInterval` would be one timer per
@@ -96,10 +103,23 @@ function EntryViewImpl({ entry, prev, expanded, thinkingVisible, reducedMotion,
96
103
  // comparator sees a fresh `[]` as a changed prop, which is why that
97
104
  // file already keeps a shared `EMPTY_LINES`.
98
105
  live: entry.live, lastOutputAt: entry.lastOutputAt, nowSec: nowSec, theme: theme, caps: caps }));
99
- case 'team':
100
- return frame(glyphs.teamAgent, teamCardColor(entry.runs, entry.active, entry.aborted, theme), _jsx(TeamCard, { requested: entry.requested, runs: entry.runs, aborted: entry.aborted, durationMs: entry.durationMs, active: entry.active, expanded: expanded, reducedMotion: reducedMotion, theme: theme, caps: caps }));
106
+ case 'team': {
107
+ // THE LIVE DISPATCH HAS EXACTLY ONE SURFACE (tui-subagent-double-roster A-2):
108
+ // while `active`, the roster (`TeamPanel`) is the only live subagent block, so
109
+ // the transcript draws nothing for this entry and the card appears - once - as
110
+ // history the moment the dispatch settles. Returning BEFORE `frame(...)` draws
111
+ // neither the rail marker nor the separation row.
112
+ //
113
+ // `renderer.tsx`'s offscreen measurement goes through THIS function too, which is
114
+ // what makes the native print pump's `measureWindow` agree with the frame for
115
+ // free. `estimateEntryRows` carries the same predicate (0 rows), so no other
116
+ // arithmetic has to know about the suppression.
117
+ if (isLiveTeamCard(entry))
118
+ return null;
119
+ return frame(glyphs.teamAgent, teamCardColor(rosterCounts(entry.runs), entry.active, entry.aborted, theme), _jsx(TeamCard, { requested: entry.requested, runs: entry.runs, aborted: entry.aborted, durationMs: entry.durationMs, active: entry.active, expanded: expanded, reducedMotion: reducedMotion, theme: theme, caps: caps, nowSec: nowSecFor(entry, nowSec) }));
120
+ }
101
121
  case 'todo':
102
- return frame(toolGlyph(glyphs, 'todo_write'), todoCardColor(entry.doneCount, entry.total, entry.live, theme), _jsx(TodoCard, { items: entry.items, doneCount: entry.doneCount, total: entry.total, live: entry.live, interrupted: entry.interrupted, theme: theme, caps: caps }));
122
+ return frame(toolGlyph(glyphs, 'todo_write'), todoCardColor(entry.doneCount, entry.total, entry.live, theme), _jsx(TodoCard, { items: entry.items, doneCount: entry.doneCount, total: entry.total, acceptance: entry.acceptance ?? null, live: entry.live, interrupted: entry.interrupted, theme: theme, caps: caps }));
103
123
  case 'retry':
104
124
  return frame(entry.phase === 'exhausted' ? glyphs.error : glyphs.retry, retryCardColor(entry.phase, theme), _jsx(RetryCard, { attempt: entry.attempt, maxRetries: entry.maxRetries, errorType: entry.errorType, phase: entry.phase, resumeAt: entry.resumeAt, totalRetries: entry.totalRetries, elapsedMs: entry.elapsedMs,
105
125
  // READ HERE, not threaded from `App`, and that is what keeps the memo