@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
@@ -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
  }
@@ -15,6 +15,7 @@ import React from 'react';
15
15
  import type { Theme } from '../theme.js';
16
16
  import type { TermCapabilities } from '../capabilities.js';
17
17
  import type { SubagentRun } from '../../team/types.js';
18
+ import { type RosterCounts } from '../team-status.js';
18
19
  export interface TeamCardProps {
19
20
  requested: number;
20
21
  runs: SubagentRun[];
@@ -25,9 +26,24 @@ export interface TeamCardProps {
25
26
  reducedMotion?: boolean;
26
27
  theme: Theme;
27
28
  caps: TermCapabilities;
29
+ /**
30
+ * Wall-clock seconds, set only while the dispatch is live (`nowSecFor`). WITHOUT
31
+ * IT a running child's elapsed time came from `(endedAt ?? 0) - startedAt` - a
32
+ * negative number clamped to `0ms`, the readout that contradicted the roster
33
+ * beside it (A-1 / E4).
34
+ */
35
+ nowSec?: number;
28
36
  }
29
- /** The card's aggregate colour: any failure dominates. */
30
- export declare function teamCardColor(runs: SubagentRun[], active: boolean, aborted: boolean, theme: Theme): string | undefined;
37
+ /**
38
+ * The card's aggregate colour.
39
+ *
40
+ * FAILURES DOMINATE, THEN A CANCELLATION, THEN A DISPATCH THAT NEVER SETTLED.
41
+ * The old version asked `runs.some((r) => !isOk(r))`, so an entry whose children
42
+ * were still working - and one the user had cancelled - was painted `toolError`
43
+ * (A-1 / E8). `RosterCounts` is passed IN rather than recomputed, so this colour
44
+ * and the headline cannot disagree inside one card.
45
+ */
46
+ export declare function teamCardColor(counts: RosterCounts, active: boolean, aborted: boolean, theme: Theme): string | undefined;
31
47
  declare function TeamCardImpl(props: TeamCardProps): React.ReactElement;
32
48
  /**
33
49
  * `React.memo` with the DEFAULT comparator (tui-render-performance L2 / R3).
@@ -18,37 +18,71 @@ import Spinner from 'ink-spinner';
18
18
  import { pickGlyphs } from '../glyphs.js';
19
19
  import { railBorderProps } from '../layout/Gutter.js';
20
20
  import { formatDuration } from '../../agent/usage.js';
21
- function isOk(run) {
22
- return run.phase === 'done' && !run.error;
23
- }
24
- /** The card's aggregate colour: any failure dominates. */
25
- export function teamCardColor(runs, active, aborted, theme) {
21
+ import { childElapsedMs, childStatus, outcomePhrase, rosterCounts, statusColor, statusMarkerCell, } from '../team-status.js';
22
+ /**
23
+ * The card's aggregate colour.
24
+ *
25
+ * FAILURES DOMINATE, THEN A CANCELLATION, THEN A DISPATCH THAT NEVER SETTLED.
26
+ * The old version asked `runs.some((r) => !isOk(r))`, so an entry whose children
27
+ * were still working - and one the user had cancelled - was painted `toolError`
28
+ * (A-1 / E8). `RosterCounts` is passed IN rather than recomputed, so this colour
29
+ * and the headline cannot disagree inside one card.
30
+ */
31
+ export function teamCardColor(counts, active, aborted, theme) {
26
32
  if (active)
27
33
  return theme.toolRunning;
28
- if (aborted || runs.some((r) => !isOk(r)))
34
+ if (counts.failed > 0)
29
35
  return theme.toolError;
36
+ if (aborted || counts.aborted > 0 || counts.queued > 0 || counts.running > 0) {
37
+ return theme.noticeWarn;
38
+ }
30
39
  return theme.toolDone;
31
40
  }
41
+ /** `nowSec` is seconds (`App`'s 1 Hz clock); a card measures in milliseconds. */
42
+ function liveClockMs(nowSec) {
43
+ return nowSec === undefined ? Date.now() : nowSec * 1000;
44
+ }
45
+ /**
46
+ * The elapsed column for one child: a MEASUREMENT, or an empty cell.
47
+ *
48
+ * `liveNow` is defined only while the dispatch is live, and that distinction is the
49
+ * whole point. A child restored by `/resume` keeps its `startedAt` and has no
50
+ * `endedAt` - `session/persist.ts` settles the ENTRY, it does not invent an end time
51
+ * for the run - so a figure computed against `Date.now()` would claim hours of work
52
+ * that never happened, next to an `interrupted (session resumed)` headline that says
53
+ * the opposite. An unmeasurable duration is left blank instead.
54
+ */
55
+ function elapsedCell(run, liveNow) {
56
+ const end = run.endedAt ?? liveNow;
57
+ if (run.startedAt === undefined || end === undefined)
58
+ return '';
59
+ return formatDuration(childElapsedMs(run, end));
60
+ }
32
61
  function TeamCardImpl(props) {
33
62
  const { requested, runs, aborted, durationMs, active, expanded, reducedMotion, theme, caps } = props;
34
63
  const glyphs = pickGlyphs(caps);
35
- const color = teamCardColor(runs, active, aborted, theme);
36
- const ok = runs.filter(isOk).length;
37
- const failed = runs.length - ok;
64
+ // ONE COUNTING AUTHORITY (A-1): `ok`/`failed` used to be `isOk(...)` and
65
+ // `runs.length - ok`, which counted a cancelled child - and a still-working one -
66
+ // as a failed one (E3 / E8).
67
+ const counts = rosterCounts(runs);
68
+ const color = teamCardColor(counts, active, aborted, theme);
69
+ // A CLOCK ONLY WHILE THE DISPATCH IS LIVE: a settled card has nothing left to time,
70
+ // and a clock in the settled path is how a resumed dispatch came to read like a run
71
+ // that is still going (`elapsedCell`).
72
+ const liveNow = active ? liveClockMs(props.nowSec) : undefined;
38
73
  // A dispatch entry restored from `/resume` is settled and aborted but has no
39
74
  // duration: nothing measured it, because the process it belonged to is gone.
40
75
  // Saying so plainly beats rendering `0.0s (0 ok, 3 failed)`, which reads like
41
76
  // a dispatch that ran and lost (§5.3 / P1-5).
42
77
  const interrupted = !active && aborted && durationMs === undefined;
43
- const settled = runs.filter((r) => r.phase === 'done' || r.phase === 'failed' || r.phase === 'aborted').length;
44
78
  const headline = interrupted
45
79
  ? 'interrupted (session resumed)'
46
80
  : active
47
- ? `${runs.length - settled} running`
48
- : `${formatDuration(durationMs ?? 0)} (${ok} ok, ${failed} failed)`;
81
+ ? `${counts.queued + counts.running} running`
82
+ : `${formatDuration(durationMs ?? 0)} (${outcomePhrase(counts)})`;
49
83
  const count = `${runs.length} ${runs.length === 1 ? 'subagent' : 'subagents'}`;
50
84
  const dropped = requested > runs.length ? ` (${runs.length} of ${requested} requested)` : '';
51
- return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: theme.accent, bold: true, children: "team" }), _jsxs(Text, { color: theme.muted, children: [' ', count, dropped, ' '] }), active && !reducedMotion && caps.unicode ? (_jsxs(Text, { color: color, children: [_jsx(Spinner, { type: "dots" }), " ", headline] })) : (_jsxs(Text, { color: color, children: [active ? `${glyphs.spinnerStill} ` : '', headline] }))] }), _jsxs(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, ...railBorderProps(glyphs.railVertical, theme.border), children: [runs.map((run) => (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: isOk(run) ? theme.toolDone : theme.toolError, children: isOk(run) ? glyphs.toolDone : glyphs.toolError }), _jsxs(Text, { color: theme.primary, children: [' ', run.label.padEnd(8).slice(0, 8)] }), _jsxs(Text, { color: theme.muted, children: [' ', run.description, ' ', run.startedAt ? formatDuration(Math.max(0, (run.endedAt ?? 0) - run.startedAt)) : '', ` ${run.turns} turns`, run.filesTouched.length > 0 ? ` ${run.filesTouched.length} files` : '', run.error ? ` ${run.error}` : ''] }), run.retry && (_jsxs(Text, { color: theme.noticeWarn, bold: true, children: [' ', "retry ", run.retry.attempt, "/", run.retry.maxRetries] }))] }, run.label))), expanded &&
85
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [_jsx(Text, { color: theme.accent, bold: true, children: "team" }), _jsxs(Text, { color: theme.muted, children: [' ', count, dropped, ' '] }), active && !reducedMotion && caps.unicode ? (_jsxs(Text, { color: color, children: [_jsx(Spinner, { type: "dots" }), " ", headline] })) : (_jsxs(Text, { color: color, children: [active ? `${glyphs.spinnerStill} ` : '', headline] }))] }), _jsxs(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, ...railBorderProps(glyphs.railVertical, theme.border), children: [runs.map((run) => (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: statusColor(childStatus(run), theme), children: statusMarkerCell(childStatus(run), glyphs) }), _jsxs(Text, { color: theme.primary, children: [' ', run.label.padEnd(8).slice(0, 8)] }), _jsxs(Text, { color: theme.muted, children: [' ', run.description, ' ', elapsedCell(run, liveNow), ` ${run.turns} turns`, run.filesTouched.length > 0 ? ` ${run.filesTouched.length} files` : '', run.error ? ` ${run.error}` : ''] }), run.retry && (_jsxs(Text, { color: theme.noticeWarn, bold: true, children: [' ', "retry ", run.retry.attempt, "/", run.retry.maxRetries] }))] }, run.label))), expanded &&
52
86
  runs.map((run) => (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { color: theme.accent, children: [run.label, " ", glyphs.midDot, " ", run.description] }), _jsx(Text, { color: theme.muted, children: run.summary ?? '(no summary)' })] }, `s-${run.label}`))), !expanded && !active && runs.length > 0 && (_jsxs(Text, { color: theme.muted, children: ["+", runs.length, " ", runs.length === 1 ? 'summary' : 'summaries', " (Ctrl+O)"] })), expanded && _jsx(Text, { color: theme.muted, children: "(Ctrl+O to collapse)" })] })] }));
53
87
  }
54
88
  /**
@@ -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: '*',
@@ -0,0 +1,102 @@
1
+ /**
2
+ * The Agents board's row model — pure projection, no Ink (tui-subagent-panel §4.4 / §7).
3
+ *
4
+ * WHY A PURE MODULE AND NOT JSX. The board's whole value is that it shows EVERY fact
5
+ * about every child, and the two things that can go wrong with that are both
6
+ * properties of a list: a row wider than the overlay (which silently desynchronises
7
+ * `OverlayFrame`'s position indicator) and a row count that grows without a bound.
8
+ * Both are assertable here, against plain strings, without a terminal - which is the
9
+ * same reason `layout/team-panel.ts` and `layout/budget.ts` are separate modules.
10
+ *
11
+ * LIVE-ONLY RULE (§4.4.2, RV-P0-2). The board has two sources with DIFFERENT fields:
12
+ * a live `TeamSnapshot` (has `messageCount`, `startedAt`, the activity prose and the
13
+ * timeline) and a settled `kind: 'team'` entry (has `durationMs` and the runs, and
14
+ * nothing else - `agent/reducer.ts`'s entry member carries no `messageCount` and no
15
+ * `startedAt`). A section whose source is missing is OMITTED, never filled with a
16
+ * zero: `mail 0` would be a lie, and an empty section title tells the reader nothing.
17
+ * The one section that survives both sources is `result` (`summary` / `error` is
18
+ * never stripped), which is precisely the fact a user opens the board to find.
19
+ *
20
+ * ASCII ONLY: `src/ui/**` is inside `glyphs.test.ts`'s static scan, so every symbol
21
+ * (the ellipsis, the separator, the cursor) arrives through `glyphs`.
22
+ */
23
+ import type { Entry } from '../../agent/reducer.js';
24
+ import type { SubagentRun, TeamSnapshot } from '../../team/types.js';
25
+ import type { Glyphs } from '../glyphs.js';
26
+ export type BoardRowKind = 'summary' | 'blank' | 'section' | 'child' | 'field' | 'event' | 'hint';
27
+ /** Which palette slot a row draws with. Mapped to `Theme` one level up, in the component. */
28
+ export type BoardTone = 'accent' | 'primary' | 'muted' | 'warn' | 'error' | 'done' | 'hint';
29
+ export interface BoardRow {
30
+ /** React key: stable and unique (`kind:subject:index`). */
31
+ key: string;
32
+ kind: BoardRowKind;
33
+ /** One already-folded line, safe to hand to `<Text wrap="truncate">`. */
34
+ text: string;
35
+ tone: BoardTone;
36
+ selected?: boolean;
37
+ }
38
+ export interface BoardSource {
39
+ dispatchId: string;
40
+ active: boolean;
41
+ /**
42
+ * When the dispatch started. **A settled `team` entry has no such field**
43
+ * (`agent/reducer.ts`'s entry member), so it is optional: with no `startedAt` the
44
+ * summary shows `durationMs` alone rather than inventing an absolute instant.
45
+ */
46
+ startedAt?: number;
47
+ endedAt?: number;
48
+ requested: number;
49
+ runs: readonly SubagentRun[];
50
+ /**
51
+ * Team-mail count. Live snapshots carry it; a settled entry does **not** - absent
52
+ * means the clause is omitted rather than rendered as `mail 0` (RV-P0-2).
53
+ */
54
+ messageCount?: number;
55
+ /** A settled entry's own total (`teamEnd` writes it). */
56
+ durationMs?: number;
57
+ }
58
+ /**
59
+ * The board's single source shape, resolved in ONE place.
60
+ *
61
+ * PRIORITY: live snapshot > (the component's frozen copy) > the most recent settled
62
+ * entry. This function answers the first and the third; the frozen copy belongs to
63
+ * the component because it only exists while a dispatch is being read (§4.4.5).
64
+ */
65
+ export declare function resolveBoardSource(input: {
66
+ team: TeamSnapshot | null;
67
+ entries: readonly Entry[];
68
+ }): BoardSource | null;
69
+ /**
70
+ * The child the board is showing, resolved from its LABEL every frame (D-8).
71
+ *
72
+ * Labels are the only stable handle: `panel-rows.ts` re-sorts the roster by phase, so
73
+ * an index would silently jump to a different child the moment one of them settles.
74
+ * Fallback order is "the first running child, else the first child", which is the
75
+ * one a reader opening the board mid-dispatch wants.
76
+ */
77
+ export declare function resolveSelection(runs: readonly SubagentRun[], label: string | null): SubagentRun | null;
78
+ export interface AgentBoardInput {
79
+ source: BoardSource;
80
+ /** Overlay content width. */
81
+ cols: number;
82
+ now: number;
83
+ selectedLabel: string | null;
84
+ glyphs: Glyphs;
85
+ native: boolean;
86
+ }
87
+ /**
88
+ * The board's rows, in the order a reader wants them: what the dispatch did, then who
89
+ * is in it, then everything about the selected child, then its history.
90
+ *
91
+ * Bounded by `TEAM_LIMITS.boardMaxRows`, and the truncation is ANNOUNCED rather than
92
+ * silent - a list that stops mid-fact with no notice reads as a rendering bug.
93
+ */
94
+ export declare function buildAgentBoardRows(input: AgentBoardInput): BoardRow[];
95
+ /**
96
+ * The facts, in a FIXED order, and with the zero-valued ones ABSENT.
97
+ *
98
+ * `supervisor` / `retries` / `compactions` / `truncated` only appear when they carry a
99
+ * fact; a row reading `supervisor 0` teaches a reader nothing and pushes the section
100
+ * a row down for every child that was never touched.
101
+ */
102
+ export declare function factPairs(run: SubagentRun, glyphs: Glyphs): [string, string][];