@aragon-agent/cli 0.6.35 → 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 (146) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +3173 -3071
  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 +38 -3
  6. package/dist/agent/controller.js +64 -4
  7. package/dist/agent/headless.js +31 -0
  8. package/dist/agent/reducer.d.ts +13 -2
  9. package/dist/agent/reducer.js +1 -0
  10. package/dist/cli.js +12 -0
  11. package/dist/commands/agents-command.d.ts +15 -0
  12. package/dist/commands/agents-command.js +27 -0
  13. package/dist/commands/builtins.js +131 -4
  14. package/dist/commands/perf.d.ts +11 -0
  15. package/dist/commands/perf.js +2 -1
  16. package/dist/commands/registry.d.ts +14 -0
  17. package/dist/config/app-paths.d.ts +15 -0
  18. package/dist/config/app-paths.js +33 -2
  19. package/dist/config/env.d.ts +4 -1
  20. package/dist/config/env.js +28 -2
  21. package/dist/config/load.d.ts +10 -0
  22. package/dist/config/load.js +8 -1
  23. package/dist/config/schema.d.ts +68 -0
  24. package/dist/config/schema.js +39 -0
  25. package/dist/config/store.js +12 -1
  26. package/dist/config/ui-state.d.ts +18 -0
  27. package/dist/config/ui-state.js +9 -0
  28. package/dist/exec/events.d.ts +25 -0
  29. package/dist/exec/events.js +4 -0
  30. package/dist/exec/index.js +16 -1
  31. package/dist/exec/runner.js +10 -0
  32. package/dist/proc/limits.d.ts +16 -0
  33. package/dist/proc/limits.js +16 -0
  34. package/dist/proc/supervisor.d.ts +20 -1
  35. package/dist/proc/supervisor.js +42 -14
  36. package/dist/prompts/command.d.ts +34 -0
  37. package/dist/prompts/command.js +237 -0
  38. package/dist/prompts/copy.d.ts +89 -0
  39. package/dist/prompts/copy.js +131 -0
  40. package/dist/prompts/fill.d.ts +51 -0
  41. package/dist/prompts/fill.js +46 -0
  42. package/dist/prompts/limits.d.ts +53 -0
  43. package/dist/prompts/limits.js +53 -0
  44. package/dist/prompts/normalize.d.ts +53 -0
  45. package/dist/prompts/normalize.js +190 -0
  46. package/dist/prompts/resolve.d.ts +31 -0
  47. package/dist/prompts/resolve.js +60 -0
  48. package/dist/prompts/search.d.ts +58 -0
  49. package/dist/prompts/search.js +146 -0
  50. package/dist/prompts/store.d.ts +86 -0
  51. package/dist/prompts/store.js +453 -0
  52. package/dist/prompts/types.d.ts +204 -0
  53. package/dist/prompts/types.js +13 -0
  54. package/dist/session/checkpoint.d.ts +8 -1
  55. package/dist/session/checkpoint.js +6 -0
  56. package/dist/session/persist.d.ts +19 -1
  57. package/dist/session/persist.js +13 -1
  58. package/dist/session/startup-session.d.ts +7 -2
  59. package/dist/session/startup-session.js +5 -2
  60. package/dist/session/store.js +3 -0
  61. package/dist/session/validate-session.js +64 -1
  62. package/dist/team/limits.d.ts +25 -0
  63. package/dist/team/limits.js +26 -0
  64. package/dist/team/runtime.js +27 -2
  65. package/dist/team/settled.d.ts +57 -0
  66. package/dist/team/settled.js +87 -0
  67. package/dist/team/subagent.js +48 -2
  68. package/dist/team/timeline.d.ts +74 -0
  69. package/dist/team/timeline.js +101 -0
  70. package/dist/team/types.d.ts +24 -1
  71. package/dist/todo/acceptance.d.ts +120 -0
  72. package/dist/todo/acceptance.js +399 -0
  73. package/dist/todo/follow-through.d.ts +48 -23
  74. package/dist/todo/follow-through.js +110 -44
  75. package/dist/todo/limits.d.ts +62 -1
  76. package/dist/todo/limits.js +62 -1
  77. package/dist/todo/panel-rows.d.ts +29 -39
  78. package/dist/todo/panel-rows.js +37 -83
  79. package/dist/todo/plan-sections.d.ts +60 -0
  80. package/dist/todo/plan-sections.js +108 -0
  81. package/dist/todo/prompt.d.ts +17 -12
  82. package/dist/todo/prompt.js +59 -40
  83. package/dist/todo/row-window.d.ts +51 -0
  84. package/dist/todo/row-window.js +107 -0
  85. package/dist/todo/store.d.ts +139 -31
  86. package/dist/todo/store.js +271 -73
  87. package/dist/todo/todo-tool.d.ts +33 -8
  88. package/dist/todo/todo-tool.js +186 -30
  89. package/dist/todo/types.d.ts +58 -0
  90. package/dist/ui/App.js +310 -46
  91. package/dist/ui/Composer.d.ts +6 -2
  92. package/dist/ui/Composer.js +2 -2
  93. package/dist/ui/Header.d.ts +9 -1
  94. package/dist/ui/Header.js +2 -2
  95. package/dist/ui/PromptInput.d.ts +21 -1
  96. package/dist/ui/PromptInput.js +21 -1
  97. package/dist/ui/ScrollbackShell.d.ts +19 -19
  98. package/dist/ui/ScrollbackShell.js +8 -1
  99. package/dist/ui/StatusBar.d.ts +5 -0
  100. package/dist/ui/StatusBar.js +1 -0
  101. package/dist/ui/TeamPanel.d.ts +26 -44
  102. package/dist/ui/TeamPanel.js +46 -135
  103. package/dist/ui/TodoPanel.d.ts +17 -14
  104. package/dist/ui/TodoPanel.js +146 -38
  105. package/dist/ui/Transcript.js +1 -1
  106. package/dist/ui/composer-input.d.ts +10 -0
  107. package/dist/ui/composer-input.js +57 -24
  108. package/dist/ui/entries/TodoCard.d.ts +26 -11
  109. package/dist/ui/entries/TodoCard.js +56 -15
  110. package/dist/ui/glyphs.d.ts +19 -0
  111. package/dist/ui/glyphs.js +4 -0
  112. package/dist/ui/layout/agents-board.d.ts +102 -0
  113. package/dist/ui/layout/agents-board.js +389 -0
  114. package/dist/ui/layout/cells.d.ts +61 -0
  115. package/dist/ui/layout/cells.js +156 -0
  116. package/dist/ui/layout/header-hints.d.ts +13 -3
  117. package/dist/ui/layout/header-hints.js +48 -14
  118. package/dist/ui/layout/status-detail-layout.js +9 -0
  119. package/dist/ui/layout/status-layout.d.ts +13 -0
  120. package/dist/ui/layout/team-row.d.ts +139 -0
  121. package/dist/ui/layout/team-row.js +246 -0
  122. package/dist/ui/layout/todo-layout.d.ts +38 -0
  123. package/dist/ui/layout/todo-layout.js +75 -0
  124. package/dist/ui/layout/virtual-window.js +23 -3
  125. package/dist/ui/overlays/AgentsBoardOverlay.d.ts +57 -0
  126. package/dist/ui/overlays/AgentsBoardOverlay.js +183 -0
  127. package/dist/ui/overlays/HelpOverlay.js +17 -2
  128. package/dist/ui/overlays/PromptEditorPage.d.ts +48 -0
  129. package/dist/ui/overlays/PromptEditorPage.js +227 -0
  130. package/dist/ui/overlays/PromptLibraryOverlay.d.ts +37 -0
  131. package/dist/ui/overlays/PromptLibraryOverlay.js +191 -0
  132. package/dist/ui/overlays/PromptPicker.d.ts +36 -0
  133. package/dist/ui/overlays/PromptPicker.js +129 -0
  134. package/dist/ui/overlays/PromptTextArea.d.ts +86 -0
  135. package/dist/ui/overlays/PromptTextArea.js +141 -0
  136. package/dist/ui/overlays/prompt-library-state.d.ts +162 -0
  137. package/dist/ui/overlays/prompt-library-state.js +335 -0
  138. package/dist/ui/overlays/prompt-picker-state.d.ts +71 -0
  139. package/dist/ui/overlays/prompt-picker-state.js +98 -0
  140. package/dist/ui/scrollback/band.js +14 -1
  141. package/dist/ui/team-status.d.ts +25 -0
  142. package/dist/ui/team-status.js +31 -3
  143. package/dist/ui/use-startup-notices.d.ts +35 -1
  144. package/dist/ui/use-startup-notices.js +29 -2
  145. package/dist/ui/use-wheel-routing.js +9 -1
  146. package/package.json +2 -2
@@ -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
  }
@@ -119,7 +119,7 @@ function EntryViewImpl({ entry, prev, expanded, thinkingVisible, reducedMotion,
119
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
120
  }
121
121
  case 'todo':
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, 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 }));
123
123
  case 'retry':
124
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,
125
125
  // READ HERE, not threaded from `App`, and that is what keeps the memo
@@ -3,10 +3,20 @@ export interface ComposerSubmitResult {
3
3
  accepted: boolean;
4
4
  reason?: string;
5
5
  }
6
+ /**
7
+ * A fill carries the prompt body; `submit` folds "insert and send" into the SAME
8
+ * transaction, so `Ctrl+Enter` in the picker is literally the Enter key one step
9
+ * later rather than a second submission path (quick-prompt-library P0-1 / I-9).
10
+ */
6
11
  export type ComposerInputIntent = InputSegment | {
7
12
  kind: 'newline';
8
13
  } | {
9
14
  kind: 'submit';
15
+ } | {
16
+ kind: 'fill';
17
+ text: string;
18
+ mode: 'insert' | 'replace';
19
+ submit: boolean;
10
20
  };
11
21
  export interface ComposerInputOptions {
12
22
  editor: EditorState;
@@ -1,5 +1,28 @@
1
1
  import { editorReducer } from './editor-reducer.js';
2
+ import { planPromptFill } from '../prompts/fill.js';
2
3
  const MORE_INPUT_NOTICE = 'More input is kept in the draft; press Enter to send.';
4
+ /**
5
+ * Apply one fill to one draft. TWO CALLS PER INTENT, on purpose: the same plan runs
6
+ * against the accepted and the rejected draft (P0-1), so a submission the host
7
+ * refuses leaves the FILLED text in the composer rather than the text from before
8
+ * the fill. Doing it once and sharing the result would silently drop the prompt the
9
+ * user just chose, which is the data loss this design exists to prevent.
10
+ *
11
+ * THE REDUCER OWNS THE RESULT. `planPromptFill` says what to insert; `insert` and
12
+ * `replace` decide the caret, the paste-token snapping and the draft-flag reset,
13
+ * exactly as they do for typing.
14
+ */
15
+ function applyFill(editor, intent) {
16
+ const plan = planPromptFill({
17
+ draft: editor.buffer,
18
+ cursor: editor.cursor,
19
+ content: intent.text,
20
+ mode: intent.mode,
21
+ });
22
+ return plan.mode === 'replace'
23
+ ? editorReducer(editor, { type: 'replace', buffer: plan.text, cursor: plan.cursor })
24
+ : editorReducer(editor, { type: 'insert', text: plan.text });
25
+ }
3
26
  /**
4
27
  * Plan both accepted and rejected drafts before any submission side effect.
5
28
  * Uses caller-assigned paste IDs and pure callbacks; never mutates the source editor.
@@ -13,34 +36,44 @@ export function planComposerInput(options) {
13
36
  let submission;
14
37
  let notice;
15
38
  for (const intent of options.intents) {
16
- if (intent.kind === 'submit') {
17
- const text = options.resolveSubmit(nextEditor);
18
- if (submission !== undefined) {
19
- notice = MORE_INPUT_NOTICE;
20
- }
21
- else if (text !== null && text.trim().length > 0) {
22
- submission = text;
23
- nextEditor = editorReducer(nextEditor, { type: 'clear' });
24
- acceptedBase = nextEditor;
25
- acceptedPastes = [];
39
+ if (intent.kind === 'fill') {
40
+ nextEditor = applyFill(nextEditor, intent);
41
+ rejectedEditor = applyFill(rejectedEditor, intent);
42
+ // A plain fill stops here: the draft is filled, nothing is sent (AC-16).
43
+ if (!intent.submit)
44
+ continue;
45
+ }
46
+ else if (intent.kind !== 'submit') {
47
+ const segment = intent.kind === 'newline'
48
+ ? { kind: 'text', text: '\n' } : intent;
49
+ const segments = [segment];
50
+ if (segment.kind === 'paste') {
51
+ // Inline pastes have no map entry; count every paste in this callback as before.
52
+ acceptedPastes.push(segment);
53
+ rejectedPastes.push(segment);
54
+ const refusal = options.checkPasteLimit(acceptedPastes, acceptedBase)
55
+ ?? options.checkPasteLimit(rejectedPastes, options.editor);
56
+ if (refusal) {
57
+ return { nextEditor: options.editor, rejectedEditor: options.editor, refusal };
58
+ }
26
59
  }
60
+ nextEditor = editorReducer(nextEditor, { type: 'input', segments });
61
+ rejectedEditor = editorReducer(rejectedEditor, { type: 'input', segments });
27
62
  continue;
28
63
  }
29
- const segment = intent.kind === 'newline'
30
- ? { kind: 'text', text: '\n' } : intent;
31
- const segments = [segment];
32
- if (segment.kind === 'paste') {
33
- // Inline pastes have no map entry; count every paste in this callback as before.
34
- acceptedPastes.push(segment);
35
- rejectedPastes.push(segment);
36
- const refusal = options.checkPasteLimit(acceptedPastes, acceptedBase)
37
- ?? options.checkPasteLimit(rejectedPastes, options.editor);
38
- if (refusal) {
39
- return { nextEditor: options.editor, rejectedEditor: options.editor, refusal };
40
- }
64
+ // THE ONE SUBMIT PATH. A `fill` with `submit` and a bare `submit` reach this
65
+ // block and nothing else, so queueing, paste expansion and the
66
+ // accepted/rejected contract are inherited rather than re-implemented.
67
+ const text = options.resolveSubmit(nextEditor);
68
+ if (submission !== undefined) {
69
+ notice = MORE_INPUT_NOTICE;
70
+ }
71
+ else if (text !== null && text.trim().length > 0) {
72
+ submission = text;
73
+ nextEditor = editorReducer(nextEditor, { type: 'clear' });
74
+ acceptedBase = nextEditor;
75
+ acceptedPastes = [];
41
76
  }
42
- nextEditor = editorReducer(nextEditor, { type: 'input', segments });
43
- rejectedEditor = editorReducer(rejectedEditor, { type: 'input', segments });
44
77
  }
45
78
  return { nextEditor, rejectedEditor, submission, notice };
46
79
  }
@@ -1,25 +1,39 @@
1
1
  /**
2
- * TodoCard — the transcript checklist entry (todo-plan-execution §6.2).
2
+ * TodoCard — the transcript checklist entry (todo-plan-execution §6.2;
3
+ * todo-acceptance-criteria §6.10).
3
4
  *
4
5
  * ASCII ONLY: every glyph comes from `pickGlyphs`.
5
6
  *
6
- * ONE CARD PER USER TURN, REWRITTEN IN PLACE. The rail is EPHEMERAL and this is
7
- * the HISTORY, exactly as `TeamPanel` and `TeamCard` divide the same work. It
8
- * renders through the shared `EntryFrame` supplied by `EntryView`, so a plan
9
- * reads as one more step in the transcript rather than a second kind of object.
7
+ * ONE CARD PER USER TURN, REWRITTEN IN PLACE. The rail is EPHEMERAL and this is the
8
+ * HISTORY, exactly as `TeamPanel` and `TeamCard` divide the same work. It renders
9
+ * through the shared `EntryFrame` supplied by `EntryView`, so a plan reads as one
10
+ * more step in the transcript rather than a second kind of object.
10
11
  *
11
12
  * THE FULL LIST, NO WINDOWING (the transcript scrolls; the rail does not), and
12
- * NEVER ANIMATED even while `live` — a resumed session would otherwise spin
13
- * forever on a card whose run ended days ago.
13
+ * NEVER ANIMATED even while `live` — a resumed session would otherwise spin forever
14
+ * on a card whose run ended days ago.
15
+ *
16
+ * THE ACCEPTANCE BLOCK ADDS ROWS, NOT A CARD (§6.10): same frame, same marker
17
+ * column, one block underneath. Only a FAILED criterion gets an evidence line
18
+ * (D-14), which is what keeps the height exactly `1 + items.length + failedCount`.
14
19
  */
15
20
  import React from 'react';
16
21
  import type { Theme } from '../theme.js';
17
22
  import type { TermCapabilities } from '../capabilities.js';
18
- import type { TodoItem } from '../../todo/types.js';
23
+ import type { AcceptanceSnapshot, TodoItem } from '../../todo/types.js';
19
24
  export interface TodoCardProps {
20
25
  items: TodoItem[];
21
26
  doneCount: number;
22
27
  total: number;
28
+ /**
29
+ * The acceptance projection (todo-acceptance-criteria §6.10).
30
+ *
31
+ * OPTIONAL, unlike `TodoSnapshot.acceptance` (P1-4): a `TodoSnapshot` has exactly
32
+ * one writer and can fail closed, while this entry's sources include transcript
33
+ * entries read back from a session file written before the feature existed. A
34
+ * required field there would be typed "present" and be `undefined` at runtime.
35
+ */
36
+ acceptance?: AcceptanceSnapshot | null;
23
37
  live: boolean;
24
38
  /** Saved mid-run and resumed: the run behind this card died with the process. */
25
39
  interrupted?: boolean;
@@ -31,9 +45,10 @@ export declare function todoCardColor(doneCount: number, total: number, live: bo
31
45
  declare function TodoCardImpl(props: TodoCardProps): React.ReactElement;
32
46
  /**
33
47
  * `React.memo` with the DEFAULT comparator (tui-render-performance L2 / R3).
34
- * `mapEntry` already preserves object identity for untouched entries, so the
35
- * array props below are stable references on a settled card, and `theme` /
36
- * `caps` are `useMemo`d in `App.tsx` (I-L2-1).
48
+ * `mapEntry` already preserves object identity for untouched entries, so the array
49
+ * props below are stable references on a settled card, and `theme` / `caps` are
50
+ * `useMemo`d in `App.tsx` (I-L2-1). `acceptance` is a NEW object whenever a verdict
51
+ * lands and the SAME reference otherwise, which is exactly what the comparator wants.
37
52
  */
38
53
  export declare const TodoCard: React.MemoExoticComponent<typeof TodoCardImpl>;
39
54
  export {};
@@ -1,22 +1,28 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  /**
3
- * TodoCard — the transcript checklist entry (todo-plan-execution §6.2).
3
+ * TodoCard — the transcript checklist entry (todo-plan-execution §6.2;
4
+ * todo-acceptance-criteria §6.10).
4
5
  *
5
6
  * ASCII ONLY: every glyph comes from `pickGlyphs`.
6
7
  *
7
- * ONE CARD PER USER TURN, REWRITTEN IN PLACE. The rail is EPHEMERAL and this is
8
- * the HISTORY, exactly as `TeamPanel` and `TeamCard` divide the same work. It
9
- * renders through the shared `EntryFrame` supplied by `EntryView`, so a plan
10
- * reads as one more step in the transcript rather than a second kind of object.
8
+ * ONE CARD PER USER TURN, REWRITTEN IN PLACE. The rail is EPHEMERAL and this is the
9
+ * HISTORY, exactly as `TeamPanel` and `TeamCard` divide the same work. It renders
10
+ * through the shared `EntryFrame` supplied by `EntryView`, so a plan reads as one
11
+ * more step in the transcript rather than a second kind of object.
11
12
  *
12
13
  * THE FULL LIST, NO WINDOWING (the transcript scrolls; the rail does not), and
13
- * NEVER ANIMATED even while `live` — a resumed session would otherwise spin
14
- * forever on a card whose run ended days ago.
14
+ * NEVER ANIMATED even while `live` — a resumed session would otherwise spin forever
15
+ * on a card whose run ended days ago.
16
+ *
17
+ * THE ACCEPTANCE BLOCK ADDS ROWS, NOT A CARD (§6.10): same frame, same marker
18
+ * column, one block underneath. Only a FAILED criterion gets an evidence line
19
+ * (D-14), which is what keeps the height exactly `1 + items.length + failedCount`.
15
20
  */
16
21
  import React from 'react';
17
22
  import { Box, Text } from 'ink';
18
23
  import { pickGlyphs } from '../glyphs.js';
19
24
  import { railBorderProps } from '../layout/Gutter.js';
25
+ import { derivePlanPhase } from '../../todo/acceptance.js';
20
26
  /** The card's aggregate colour: complete is done, anything else is in flight. */
21
27
  export function todoCardColor(doneCount, total, live, theme) {
22
28
  if (live)
@@ -39,19 +45,54 @@ function rowColor(item, theme) {
39
45
  return theme.primary;
40
46
  return theme.hintFg ?? theme.muted;
41
47
  }
48
+ /** `passed` reuses the done glyph; `failed` is the one new meaning (D-9). */
49
+ function acceptanceMarker(item, glyphs) {
50
+ if (item.status === 'passed')
51
+ return glyphs.todoDone;
52
+ if (item.status === 'failed')
53
+ return glyphs.todoFail;
54
+ if (item.status === 'in_progress')
55
+ return glyphs.todoActive;
56
+ return glyphs.todoPending;
57
+ }
58
+ function acceptanceRowColor(item, theme) {
59
+ if (item.status === 'failed')
60
+ return theme.toolError;
61
+ if (item.status === 'passed')
62
+ return theme.muted;
63
+ if (item.status === 'in_progress')
64
+ return theme.primary;
65
+ return theme.hintFg ?? theme.muted;
66
+ }
67
+ /**
68
+ * All passed reads as done, any failure as an error, otherwise muted; while the run
69
+ * is live and the plan is in VERIFY it reads as running - the same expression family
70
+ * `todoCardColor` uses one level up.
71
+ */
72
+ function acceptanceTitleColor(acceptance, items, live, theme) {
73
+ if (acceptance.failedCount > 0)
74
+ return theme.toolError;
75
+ if (live && derivePlanPhase(items, acceptance.items) === 'verify')
76
+ return theme.toolRunning;
77
+ if (acceptance.total > 0 && acceptance.checkedCount === acceptance.total)
78
+ return theme.toolDone;
79
+ return theme.muted;
80
+ }
42
81
  function TodoCardImpl(props) {
43
- const { items, doneCount, total, live, interrupted, theme, caps } = props;
82
+ const { items, doneCount, total, acceptance, live, interrupted, theme, caps } = props;
44
83
  const glyphs = pickGlyphs(caps);
84
+ const criteria = acceptance && acceptance.total > 0 ? acceptance : null;
45
85
  return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: theme.accent, bold: true, children: "Todos" }), _jsxs(Text, { color: todoCardColor(doneCount, total, live, theme), children: [' ', doneCount, "/", total] }), interrupted && (
46
- // A resumed `2/7` would otherwise read as a run still in flight
47
- // (P2-6). Saying so plainly is the same fix `TeamCard` makes for a
48
- // dispatch that never got a duration.
49
- _jsxs(Text, { color: theme.muted, children: [' ', "interrupted (session resumed)"] }))] }), _jsx(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, ...railBorderProps(glyphs.railVertical, theme.border), children: items.map((item, index) => (_jsxs(Text, { wrap: "truncate", color: rowColor(item, theme), children: [marker(item, glyphs).padEnd(3), item.status === 'in_progress' ? item.activeForm : item.content] }, `${index}-${item.content}`))) })] }));
86
+ // A resumed `2/7` would otherwise read as a run still in flight (P2-6).
87
+ // Saying so plainly is the same fix `TeamCard` makes for a dispatch that
88
+ // never got a duration.
89
+ _jsxs(Text, { color: theme.muted, children: [' ', "interrupted (session resumed)"] }))] }), _jsx(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, ...railBorderProps(glyphs.railVertical, theme.border), children: items.map((item, index) => (_jsxs(Text, { wrap: "truncate", color: rowColor(item, theme), children: [marker(item, glyphs).padEnd(3), item.status === 'in_progress' ? item.activeForm : item.content] }, `${index}-${item.content}`))) }), criteria && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: theme.accent, bold: true, children: "Acceptance" }), _jsxs(Text, { color: acceptanceTitleColor(criteria, items, live, theme), children: [' ', criteria.checkedCount, "/", criteria.total, " checked", criteria.failedCount > 0 ? ` !${criteria.failedCount} failed` : ''] })] }), _jsx(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, ...railBorderProps(glyphs.railVertical, theme.border), children: criteria.items.map((item, index) => (_jsxs(React.Fragment, { children: [_jsxs(Text, { wrap: "truncate", color: acceptanceRowColor(item, theme), children: [acceptanceMarker(item, glyphs).padEnd(3), item.content] }), item.status === 'failed' && item.evidence.length > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', item.evidence] }))] }, `acc-${index}-${item.content}`))) })] }))] }));
50
90
  }
51
91
  /**
52
92
  * `React.memo` with the DEFAULT comparator (tui-render-performance L2 / R3).
53
- * `mapEntry` already preserves object identity for untouched entries, so the
54
- * array props below are stable references on a settled card, and `theme` /
55
- * `caps` are `useMemo`d in `App.tsx` (I-L2-1).
93
+ * `mapEntry` already preserves object identity for untouched entries, so the array
94
+ * props below are stable references on a settled card, and `theme` / `caps` are
95
+ * `useMemo`d in `App.tsx` (I-L2-1). `acceptance` is a NEW object whenever a verdict
96
+ * lands and the SAME reference otherwise, which is exactly what the comparator wants.
56
97
  */
57
98
  export const TodoCard = React.memo(TodoCardImpl);
@@ -86,6 +86,15 @@ export interface Glyphs {
86
86
  scrollThumb: string;
87
87
  /** Redaction dot for `maskSecret` in the settings screen (§4.1 tier B). */
88
88
  maskDot: string;
89
+ /**
90
+ * The Agents board's selection cursor (tui-subagent-panel D-14).
91
+ *
92
+ * BYTE-IDENTICAL to `todoActive`, and still a field of its own: the two carry
93
+ * different meanings - "the item the run is on" versus "the child this panel is
94
+ * showing" - and reusing the todo glyph would restyle the board the day someone
95
+ * redraws the TODO rail. `scrollTrack` / `scrollThumb` above are the same precedent.
96
+ */
97
+ cursor: string;
89
98
  /**
90
99
  * Team mode (team-subagents §6.1). The rail marker for a dispatch card and
91
100
  * the prefix on the panel's incoming-message preview.
@@ -108,6 +117,16 @@ export interface Glyphs {
108
117
  todoPending: string;
109
118
  todoActive: string;
110
119
  todoDone: string;
120
+ /**
121
+ * The ONLY glyph this feature adds (todo-acceptance-criteria D-9): `passed` is a
122
+ * settled positive and reuses `todoDone`, while `failed` is a genuinely new
123
+ * meaning.
124
+ *
125
+ * The ASCII tier must still tell "done" from "failed", so `[!]` joins `[x]` - the
126
+ * same three columns, which is what keeps the mixed-width list aligned with no
127
+ * per-tier branch.
128
+ */
129
+ todoFail: string;
111
130
  /**
112
131
  * API retry (llm-api-retry-backoff §6.5). The rail marker on the retry card.
113
132
  *
package/dist/ui/glyphs.js CHANGED
@@ -60,12 +60,14 @@ const UNICODE_GLYPHS = {
60
60
  scrollTrack: '│',
61
61
  scrollThumb: '█',
62
62
  maskDot: '•',
63
+ cursor: '▸',
63
64
  boxStyle: 'round',
64
65
  teamAgent: '◆',
65
66
  teamMail: '⇄',
66
67
  todoPending: '○',
67
68
  todoActive: '▸',
68
69
  todoDone: '✓',
70
+ todoFail: '✗',
69
71
  retry: '↻',
70
72
  compaction: '⤓',
71
73
  queued: '◷',
@@ -124,12 +126,14 @@ const ASCII_GLYPHS = {
124
126
  scrollTrack: '|',
125
127
  scrollThumb: '#',
126
128
  maskDot: '*',
129
+ cursor: '>',
127
130
  boxStyle: 'classic',
128
131
  teamAgent: '*',
129
132
  teamMail: '@',
130
133
  todoPending: '[ ]',
131
134
  todoActive: '>',
132
135
  todoDone: '[x]',
136
+ todoFail: '[!]',
133
137
  retry: '[r]',
134
138
  compaction: '[c]',
135
139
  queued: '*',