@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
@@ -52,6 +52,7 @@
52
52
  */
53
53
  import { useCallback, useEffect, useLayoutEffect, useMemo, useReducer, useRef } from 'react';
54
54
  import { estimateEntryRows } from '../layout/virtual-window.js';
55
+ import { isLiveTeamCard } from '../team-status.js';
55
56
  import { buildScrollbackBudget } from './budget.js';
56
57
  import { buildLivePreview } from './live-preview.js';
57
58
  import { SCROLLBACK_LIMITS } from './limits.js';
@@ -554,6 +555,12 @@ export function useScrollbackView(args) {
554
555
  if (index < 0)
555
556
  return false;
556
557
  const entry = list[index];
558
+ // A LIVE DISPATCH CARD IS REFUSED, NOT PRINTED (tui-subagent-double-roster A-2').
559
+ // `Ctrl+O` arrives mid-dispatch, `EntryView` suppresses this card, and a copy
560
+ // printed here would be a FROZEN SNAPSHOT of a card the roster is still rewriting -
561
+ // permanently in the scrollback, because printed is forever.
562
+ if (isLiveTeamCard(entry))
563
+ return false;
557
564
  // THE COPY IS RENDERED IN A MICROTASK, NOT IN THE KEY HANDLER THAT ASKED FOR IT.
558
565
  // `Ctrl+O` arrives on the stdin stream, and an offscreen render issued from there is
559
566
  // SCHEDULED rather than flushed - the reconciler sees a pending update, the frame the
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Team status — the ONE authority that answers "what state is this child in, and
3
+ * how is that drawn / counted / timed" (tui-subagent-double-roster A-1).
4
+ *
5
+ * WHY THIS FILE EXISTS. The phase -> glyph / colour / count question used to be
6
+ * answered by nine copies in seven files, and they disagreed. The transcript card
7
+ * asked `isOk(run) ? toolDone : toolError`, so six of the eight phases - every
8
+ * child that was still working - were painted with the FAILURE glyph, and an
9
+ * aborted child was counted as a failed one forever. That is not a palette
10
+ * mistake, it is a judgement implemented in the wrong number of places.
11
+ *
12
+ * THE VOCABULARY IS THE DOMAIN'S (analysis A-1' item 3). `ChildStatus` reuses
13
+ * `SubagentPhase`'s own words - `queued` / `running` / `done` / `failed` /
14
+ * `aborted` - and never invents a second dictionary (`pending` was proposed and
15
+ * rejected: `SubagentPhase` does not have it, and two words for one state is how
16
+ * the tenth copy gets written).
17
+ *
18
+ * FIVE STATES, NOT FOUR (A-1' item 2). `aborted` is NOT a failure. The runtime
19
+ * distinguishes them on purpose (`Runtime.settlePhase`: aborted first, then the
20
+ * timeout, then the error), and a user pressing Esc must not read their own
21
+ * action back as "3 failed". So `aborted` takes `glyphs.warn` and
22
+ * `theme.noticeWarn`, which collide with neither the done tick nor the failed
23
+ * cross in either glyph tier.
24
+ *
25
+ * ASCII ONLY: `src/ui/**` is inside `glyphs.test.ts`'s static scan, so every
26
+ * symbol here is injected through `pickGlyphs(caps)`.
27
+ */
28
+ import type { Entry } from '../agent/reducer.js';
29
+ import type { SubagentRun } from '../team/types.js';
30
+ import type { Glyphs } from './glyphs.js';
31
+ import type { Theme } from './theme.js';
32
+ /**
33
+ * The five readings a child can have on screen. A TOTAL mapping of
34
+ * `SubagentPhase`, which is why `childStatus` needs no guard clauses: `starting`,
35
+ * `thinking`, `tool` and `waiting` are all "this child is working right now", and
36
+ * the per-row activity column is what tells them apart.
37
+ */
38
+ export type ChildStatus = 'queued' | 'running' | 'done' | 'failed' | 'aborted';
39
+ /**
40
+ * The status of one child.
41
+ *
42
+ * A LOOKUP TABLE RATHER THAN A SWITCH, because the mapping is the whole content
43
+ * of this function: a `Record` keyed by the phase union is exhaustive at compile
44
+ * time (a new phase is a missing-key error) and cannot drift from the type.
45
+ */
46
+ export declare function childStatus(run: SubagentRun): ChildStatus;
47
+ /** Per-status totals of a roster. `total` is the roster size, not the sum. */
48
+ export interface RosterCounts {
49
+ queued: number;
50
+ running: number;
51
+ done: number;
52
+ failed: number;
53
+ aborted: number;
54
+ total: number;
55
+ }
56
+ /**
57
+ * Count a roster once, by status.
58
+ *
59
+ * THE HEADER, THE CARD HEADLINE AND THE STATUS CHIP ALL READ THIS. Before it
60
+ * existed the header filtered `phase !== 'queued' && ...` for "running" and the
61
+ * card subtracted `runs.length - isOk(runs)`, so the same dispatch reported
62
+ * `0 running` above two rows reading `queued` and `1 failed` for a child that had
63
+ * never started.
64
+ */
65
+ export declare function rosterCounts(runs: readonly SubagentRun[]): RosterCounts;
66
+ /** True while the child is doing something - the only state that animates. */
67
+ export declare function isRunningStatus(status: ChildStatus): boolean;
68
+ /** True once the child's outcome is final, whatever it is. */
69
+ export declare function isSettledStatus(status: ChildStatus): boolean;
70
+ /**
71
+ * The static marker for a status.
72
+ *
73
+ * `running` gets `spinnerStill` because the animated `<Spinner>` is the caller's
74
+ * job: it needs `reducedMotion` and `caps.unicode`, and a component that owns its
75
+ * own `ink-spinner` is what `spinner-census.test.ts` counts. The caller swaps this
76
+ * glyph for the spinner when it may animate.
77
+ */
78
+ export declare function statusGlyph(status: ChildStatus, glyphs: Glyphs): string;
79
+ /**
80
+ * The widest marker this terminal's tier can produce (1 in Unicode, 4 in ASCII).
81
+ *
82
+ * EXPORTED (tui-subagent-panel §4.2.1): the panel row's geometry has to know how
83
+ * many columns the marker cell spends before it can divide the rest, and the cell
84
+ * width is a property of the TIER rather than of a status. `statusMarkerCell`
85
+ * keeps using it, so the padder and the measurer can never disagree.
86
+ */
87
+ export declare function statusMarkerWidth(glyphs: Glyphs): number;
88
+ /**
89
+ * The marker padded to the tier's widest one, for the leading cell of a row.
90
+ *
91
+ * THE PADDING IS NOT COSMETIC (A-1'a): the ASCII tier spells `[ok]` (4), `[x]` (3)
92
+ * and `!` (1), so an unpadded marker cell starts every label at a different
93
+ * column and the roster stops reading as a table. `TodoPanel` pads its marker cell
94
+ * for the same reason. The Unicode tier's markers are all one cell wide, so the
95
+ * padding is a no-op there.
96
+ */
97
+ export declare function statusMarkerCell(status: ChildStatus, glyphs: Glyphs): string;
98
+ /**
99
+ * The colour of a status. One reading for the roster row, the card row and the
100
+ * card's rail marker.
101
+ *
102
+ * `aborted` deliberately does NOT share `toolError` with `failed`: an abort is the
103
+ * user's own Esc, and the analysis found that painting it red made a cancelled
104
+ * dispatch permanently unreadable in the history.
105
+ */
106
+ export declare function statusColor(status: ChildStatus, theme: Theme): string | undefined;
107
+ /**
108
+ * How long this child has been (or was) working, in ms.
109
+ *
110
+ * `endedAt ?? now`, NOT `endedAt ?? 0`. The transcript card used the latter, which
111
+ * made every RUNNING child read `0ms` while the roster beside it read `1m05s` for
112
+ * the same child - the pair of contradictory readouts the reporter screenshotted.
113
+ */
114
+ export declare function childElapsedMs(run: SubagentRun, now: number): number;
115
+ /**
116
+ * The roster header's count section, e.g. `2 running · 0 done · 1 queued`.
117
+ *
118
+ * `queued` IS COUNTED (the old `isRunning` excluded it, so a fresh dispatch
119
+ * announced `0 running · 0 done` above five rows that each said `queued`), and a
120
+ * non-zero bucket is always named even when nothing is running - a roster whose
121
+ * children are all `aborted` must say so rather than `0 running · 0 done`.
122
+ *
123
+ * WIDTH LADDER: narrow terminals drop the least urgent clause first, in the order
124
+ * `done`, `aborted`, `failed`, `queued`; `running` is never dropped. The order is
125
+ * a product decision, not a detail: while a dispatch is live, what is happening
126
+ * now outranks what has already finished, and the per-row activity column carries
127
+ * the finished children's own facts anyway.
128
+ */
129
+ export declare function rosterHeadline(counts: RosterCounts, glyphs: Glyphs, cols: number): string;
130
+ /**
131
+ * The settled card's outcome buckets, e.g. `2 ok, 1 aborted, 1 failed`.
132
+ *
133
+ * `ok` is always shown (it is the denominator a reader compares against) and a
134
+ * zero bucket is omitted, so a clean dispatch still reads `3 ok` and a failed one
135
+ * no longer claims that a cancelled child failed.
136
+ */
137
+ export declare function outcomePhrase(counts: RosterCounts): string;
138
+ /**
139
+ * Whether this entry is the transcript card of a dispatch that is STILL RUNNING,
140
+ * i.e. the second live surface the reporter saw beside the roster.
141
+ *
142
+ * ONE PREDICATE, FOUR CONSUMERS (analysis A-2'): the transcript's `case 'team'`
143
+ * and `estimateEntryRows` (so the virtual window and the frame agree), the tail
144
+ * window and `LiveTail` (so the pump's print plan and the box agree), and the
145
+ * `Ctrl+O` finder plus `reprintExpanded` (so a keypress cannot print a stale
146
+ * snapshot of a card that is still being rewritten). `state.entries` is NEVER
147
+ * filtered: the entry's lifecycle and `session/persist.ts`'s `interrupted`
148
+ * normalisation depend on it staying there.
149
+ */
150
+ export declare function isLiveTeamCard(entry: Entry): boolean;
151
+ /**
152
+ * The MOST RECENT dispatch card in the transcript, live or settled.
153
+ *
154
+ * ONE PREDICATE, TWO CONSUMERS (tui-subagent-panel §6.4): the `/agents` command
155
+ * and `App`'s `Ctrl+B` branch both have to answer "is there anything to show?"
156
+ * before opening the board, and the board itself falls back to this entry when a
157
+ * dispatch has already ended and `state.team` is `null` (`resolveBoardSource`).
158
+ * Asking the question in one place is what stops the key and the command from
159
+ * disagreeing about whether the board exists.
160
+ *
161
+ * Scans BACKWARDS and returns the first match: the newest card is the last one in
162
+ * `entries`, and a dispatch-heavy session can hold hundreds of them.
163
+ */
164
+ export declare function lastTeamEntry(entries: readonly Entry[]): Extract<Entry, {
165
+ kind: 'team';
166
+ }> | null;
@@ -0,0 +1,252 @@
1
+ /**
2
+ * Team status — the ONE authority that answers "what state is this child in, and
3
+ * how is that drawn / counted / timed" (tui-subagent-double-roster A-1).
4
+ *
5
+ * WHY THIS FILE EXISTS. The phase -> glyph / colour / count question used to be
6
+ * answered by nine copies in seven files, and they disagreed. The transcript card
7
+ * asked `isOk(run) ? toolDone : toolError`, so six of the eight phases - every
8
+ * child that was still working - were painted with the FAILURE glyph, and an
9
+ * aborted child was counted as a failed one forever. That is not a palette
10
+ * mistake, it is a judgement implemented in the wrong number of places.
11
+ *
12
+ * THE VOCABULARY IS THE DOMAIN'S (analysis A-1' item 3). `ChildStatus` reuses
13
+ * `SubagentPhase`'s own words - `queued` / `running` / `done` / `failed` /
14
+ * `aborted` - and never invents a second dictionary (`pending` was proposed and
15
+ * rejected: `SubagentPhase` does not have it, and two words for one state is how
16
+ * the tenth copy gets written).
17
+ *
18
+ * FIVE STATES, NOT FOUR (A-1' item 2). `aborted` is NOT a failure. The runtime
19
+ * distinguishes them on purpose (`Runtime.settlePhase`: aborted first, then the
20
+ * timeout, then the error), and a user pressing Esc must not read their own
21
+ * action back as "3 failed". So `aborted` takes `glyphs.warn` and
22
+ * `theme.noticeWarn`, which collide with neither the done tick nor the failed
23
+ * cross in either glyph tier.
24
+ *
25
+ * ASCII ONLY: `src/ui/**` is inside `glyphs.test.ts`'s static scan, so every
26
+ * symbol here is injected through `pickGlyphs(caps)`.
27
+ */
28
+ const STATUS_BY_PHASE = {
29
+ queued: 'queued',
30
+ starting: 'running',
31
+ thinking: 'running',
32
+ tool: 'running',
33
+ waiting: 'running',
34
+ done: 'done',
35
+ failed: 'failed',
36
+ aborted: 'aborted',
37
+ };
38
+ /**
39
+ * The status of one child.
40
+ *
41
+ * A LOOKUP TABLE RATHER THAN A SWITCH, because the mapping is the whole content
42
+ * of this function: a `Record` keyed by the phase union is exhaustive at compile
43
+ * time (a new phase is a missing-key error) and cannot drift from the type.
44
+ */
45
+ export function childStatus(run) {
46
+ return STATUS_BY_PHASE[run.phase];
47
+ }
48
+ /**
49
+ * Count a roster once, by status.
50
+ *
51
+ * THE HEADER, THE CARD HEADLINE AND THE STATUS CHIP ALL READ THIS. Before it
52
+ * existed the header filtered `phase !== 'queued' && ...` for "running" and the
53
+ * card subtracted `runs.length - isOk(runs)`, so the same dispatch reported
54
+ * `0 running` above two rows reading `queued` and `1 failed` for a child that had
55
+ * never started.
56
+ */
57
+ export function rosterCounts(runs) {
58
+ const counts = {
59
+ queued: 0, running: 0, done: 0, failed: 0, aborted: 0, total: runs.length,
60
+ };
61
+ for (const run of runs)
62
+ counts[childStatus(run)] += 1;
63
+ return counts;
64
+ }
65
+ /** True while the child is doing something - the only state that animates. */
66
+ export function isRunningStatus(status) {
67
+ return status === 'running';
68
+ }
69
+ /** True once the child's outcome is final, whatever it is. */
70
+ export function isSettledStatus(status) {
71
+ return status === 'done' || status === 'failed' || status === 'aborted';
72
+ }
73
+ /**
74
+ * The static marker for a status.
75
+ *
76
+ * `running` gets `spinnerStill` because the animated `<Spinner>` is the caller's
77
+ * job: it needs `reducedMotion` and `caps.unicode`, and a component that owns its
78
+ * own `ink-spinner` is what `spinner-census.test.ts` counts. The caller swaps this
79
+ * glyph for the spinner when it may animate.
80
+ */
81
+ export function statusGlyph(status, glyphs) {
82
+ switch (status) {
83
+ case 'queued':
84
+ return glyphs.toolPending;
85
+ case 'running':
86
+ return glyphs.spinnerStill;
87
+ case 'done':
88
+ return glyphs.toolDone;
89
+ case 'failed':
90
+ return glyphs.toolError;
91
+ case 'aborted':
92
+ return glyphs.warn;
93
+ }
94
+ }
95
+ /**
96
+ * The widest marker this terminal's tier can produce (1 in Unicode, 4 in ASCII).
97
+ *
98
+ * EXPORTED (tui-subagent-panel §4.2.1): the panel row's geometry has to know how
99
+ * many columns the marker cell spends before it can divide the rest, and the cell
100
+ * width is a property of the TIER rather than of a status. `statusMarkerCell`
101
+ * keeps using it, so the padder and the measurer can never disagree.
102
+ */
103
+ export function statusMarkerWidth(glyphs) {
104
+ return Math.max(glyphs.toolPending.length, glyphs.spinnerStill.length, glyphs.toolDone.length, glyphs.toolError.length, glyphs.warn.length);
105
+ }
106
+ /**
107
+ * The marker padded to the tier's widest one, for the leading cell of a row.
108
+ *
109
+ * THE PADDING IS NOT COSMETIC (A-1'a): the ASCII tier spells `[ok]` (4), `[x]` (3)
110
+ * and `!` (1), so an unpadded marker cell starts every label at a different
111
+ * column and the roster stops reading as a table. `TodoPanel` pads its marker cell
112
+ * for the same reason. The Unicode tier's markers are all one cell wide, so the
113
+ * padding is a no-op there.
114
+ */
115
+ export function statusMarkerCell(status, glyphs) {
116
+ return statusGlyph(status, glyphs).padEnd(statusMarkerWidth(glyphs));
117
+ }
118
+ /**
119
+ * The colour of a status. One reading for the roster row, the card row and the
120
+ * card's rail marker.
121
+ *
122
+ * `aborted` deliberately does NOT share `toolError` with `failed`: an abort is the
123
+ * user's own Esc, and the analysis found that painting it red made a cancelled
124
+ * dispatch permanently unreadable in the history.
125
+ */
126
+ export function statusColor(status, theme) {
127
+ switch (status) {
128
+ case 'queued':
129
+ return theme.toolPending;
130
+ case 'running':
131
+ return theme.toolRunning;
132
+ case 'done':
133
+ return theme.toolDone;
134
+ case 'failed':
135
+ return theme.toolError;
136
+ case 'aborted':
137
+ return theme.noticeWarn;
138
+ }
139
+ }
140
+ /**
141
+ * How long this child has been (or was) working, in ms.
142
+ *
143
+ * `endedAt ?? now`, NOT `endedAt ?? 0`. The transcript card used the latter, which
144
+ * made every RUNNING child read `0ms` while the roster beside it read `1m05s` for
145
+ * the same child - the pair of contradictory readouts the reporter screenshotted.
146
+ */
147
+ export function childElapsedMs(run, now) {
148
+ if (run.startedAt === undefined)
149
+ return 0;
150
+ return Math.max(0, (run.endedAt ?? now) - run.startedAt);
151
+ }
152
+ /**
153
+ * Columns the roster header spends on `team` (plus its marker) and on the
154
+ * incoming-message badge, so the count clauses are planned against what is left.
155
+ */
156
+ const HEADER_RESERVED_COLS = 16;
157
+ const HEADER_MIN_CLAUSE_COLS = 12;
158
+ function joinClauses(clauses, glyphs) {
159
+ return clauses.map((clause) => clause.text).join(` ${glyphs.midDot} `);
160
+ }
161
+ /** Drop the clause that can be spared first, keeping display order intact. */
162
+ function dropLowestPriority(clauses) {
163
+ let victim = 0;
164
+ for (let index = 1; index < clauses.length; index += 1) {
165
+ if (clauses[index].priority < clauses[victim].priority)
166
+ victim = index;
167
+ }
168
+ return clauses.filter((_, index) => index !== victim);
169
+ }
170
+ /**
171
+ * The roster header's count section, e.g. `2 running · 0 done · 1 queued`.
172
+ *
173
+ * `queued` IS COUNTED (the old `isRunning` excluded it, so a fresh dispatch
174
+ * announced `0 running · 0 done` above five rows that each said `queued`), and a
175
+ * non-zero bucket is always named even when nothing is running - a roster whose
176
+ * children are all `aborted` must say so rather than `0 running · 0 done`.
177
+ *
178
+ * WIDTH LADDER: narrow terminals drop the least urgent clause first, in the order
179
+ * `done`, `aborted`, `failed`, `queued`; `running` is never dropped. The order is
180
+ * a product decision, not a detail: while a dispatch is live, what is happening
181
+ * now outranks what has already finished, and the per-row activity column carries
182
+ * the finished children's own facts anyway.
183
+ */
184
+ export function rosterHeadline(counts, glyphs, cols) {
185
+ const clauses = [
186
+ { priority: 5, text: `${counts.running} running` },
187
+ { priority: 1, text: `${counts.done} done` },
188
+ ];
189
+ if (counts.queued > 0)
190
+ clauses.push({ priority: 4, text: `${counts.queued} queued` });
191
+ if (counts.failed > 0)
192
+ clauses.push({ priority: 3, text: `${counts.failed} failed` });
193
+ if (counts.aborted > 0)
194
+ clauses.push({ priority: 2, text: `${counts.aborted} aborted` });
195
+ const budget = Math.max(HEADER_MIN_CLAUSE_COLS, cols - HEADER_RESERVED_COLS);
196
+ let kept = clauses;
197
+ while (kept.length > 1 && joinClauses(kept, glyphs).length > budget) {
198
+ kept = dropLowestPriority(kept);
199
+ }
200
+ return joinClauses(kept, glyphs);
201
+ }
202
+ /**
203
+ * The settled card's outcome buckets, e.g. `2 ok, 1 aborted, 1 failed`.
204
+ *
205
+ * `ok` is always shown (it is the denominator a reader compares against) and a
206
+ * zero bucket is omitted, so a clean dispatch still reads `3 ok` and a failed one
207
+ * no longer claims that a cancelled child failed.
208
+ */
209
+ export function outcomePhrase(counts) {
210
+ const parts = [`${counts.done} ok`];
211
+ if (counts.aborted > 0)
212
+ parts.push(`${counts.aborted} aborted`);
213
+ if (counts.failed > 0)
214
+ parts.push(`${counts.failed} failed`);
215
+ return parts.join(', ');
216
+ }
217
+ /**
218
+ * Whether this entry is the transcript card of a dispatch that is STILL RUNNING,
219
+ * i.e. the second live surface the reporter saw beside the roster.
220
+ *
221
+ * ONE PREDICATE, FOUR CONSUMERS (analysis A-2'): the transcript's `case 'team'`
222
+ * and `estimateEntryRows` (so the virtual window and the frame agree), the tail
223
+ * window and `LiveTail` (so the pump's print plan and the box agree), and the
224
+ * `Ctrl+O` finder plus `reprintExpanded` (so a keypress cannot print a stale
225
+ * snapshot of a card that is still being rewritten). `state.entries` is NEVER
226
+ * filtered: the entry's lifecycle and `session/persist.ts`'s `interrupted`
227
+ * normalisation depend on it staying there.
228
+ */
229
+ export function isLiveTeamCard(entry) {
230
+ return entry.kind === 'team' && entry.active === true;
231
+ }
232
+ /**
233
+ * The MOST RECENT dispatch card in the transcript, live or settled.
234
+ *
235
+ * ONE PREDICATE, TWO CONSUMERS (tui-subagent-panel §6.4): the `/agents` command
236
+ * and `App`'s `Ctrl+B` branch both have to answer "is there anything to show?"
237
+ * before opening the board, and the board itself falls back to this entry when a
238
+ * dispatch has already ended and `state.team` is `null` (`resolveBoardSource`).
239
+ * Asking the question in one place is what stops the key and the command from
240
+ * disagreeing about whether the board exists.
241
+ *
242
+ * Scans BACKWARDS and returns the first match: the newest card is the last one in
243
+ * `entries`, and a dispatch-heavy session can hold hundreds of them.
244
+ */
245
+ export function lastTeamEntry(entries) {
246
+ for (let index = entries.length - 1; index >= 0; index -= 1) {
247
+ const entry = entries[index];
248
+ if (entry?.kind === 'team')
249
+ return entry;
250
+ }
251
+ return null;
252
+ }
@@ -1,5 +1,6 @@
1
1
  import { formatCost, formatDuration, formatTokens, promptTokensOf } from '../agent/usage.js';
2
2
  import { formatRetryLine } from '../agent/retry-view.js';
3
+ import { childStatus, outcomePhrase, rosterCounts, statusMarkerCell } from './team-status.js';
3
4
  import { expandTabs } from './display-tab.js';
4
5
  const DEFAULT_MAX_ENTRIES = 200;
5
6
  const CONTINUATION_INDENT = ' ';
@@ -101,15 +102,23 @@ function renderEntry(entry, glyphs) {
101
102
  // dispatch is often the most expensive thing in a session, so leaving it
102
103
  // out of the plain-text replay would make the exported transcript
103
104
  // unaccountable for most of its own cost.
104
- const ok = entry.runs.filter((r) => r.phase === 'done' && !r.error).length;
105
- const failed = entry.runs.length - ok;
105
+ // ONE COUNTING AUTHORITY (`team-status.ts`), so the replay cannot disagree with
106
+ // the card it replays: `runs.length - ok` counted a cancelled child as a failed
107
+ // one, and a still-working one as a failure (tui-subagent-double-roster E3/E8).
108
+ // A cancellation therefore gets its own bucket instead of inflating `failed`.
109
+ const counts = rosterCounts(entry.runs);
106
110
  const head = entry.active || (entry.aborted && entry.durationMs === undefined)
107
111
  ? 'interrupted'
108
- : `${entry.durationMs ?? 0}ms (${ok} ok, ${failed} failed)`;
112
+ : `${entry.durationMs ?? 0}ms (${outcomePhrase(counts)})`;
109
113
  const lines = [
110
114
  `${TOOL_INDENT}team ${entry.runs.length} subagents ${head}`,
111
115
  ...entry.runs.map((run) => {
112
- const glyph = run.phase === 'done' && !run.error ? glyphs.toolDone : glyphs.toolError;
116
+ // The same five-state judgement the card uses: only a real failure takes
117
+ // the cross, a cancelled child takes the warning and a running one the
118
+ // running marker. `statusMarkerCell`, NOT the bare glyph: the ASCII tier
119
+ // spells `[ok]`, `[x]` and `!` at three widths, and a replay whose label
120
+ // column shifts from row to row is unreadable.
121
+ const glyph = statusMarkerCell(childStatus(run), glyphs);
113
122
  const detail = run.error ? ` ${run.error}` : '';
114
123
  return `${TOOL_INDENT}${CONTINUATION_INDENT}${glyph} ${run.label} ${run.description}${detail}`;
115
124
  }),
@@ -109,4 +109,38 @@ export interface VtInputNoticeOptions {
109
109
  */
110
110
  onSeen: () => void;
111
111
  }
112
- export declare function useStartupNotices(dispatch: (action: ViewAction) => void, mouseNotice?: MouseNoticeOptions, retainNotice?: RetainNoticeOptions, vtInputNotice?: VtInputNoticeOptions, scrollModeNotice?: ScrollModeNoticeOptions): void;
112
+ /**
113
+ * The one-shot prompt-key migration notice (quick-prompt-library D-20).
114
+ *
115
+ * IT EXISTS BECAUSE A KEY MOVED: `Ctrl+P` now opens the prompt library and the mode
116
+ * fallback is `Ctrl+N`. Every existing user is in the audience, so silence is not an
117
+ * option - and, exactly like the two notices above, the gate is a VERSION so a later
118
+ * rewording can reach the people who learned the first one.
119
+ */
120
+ export interface PromptKeyNoticeOptions {
121
+ /** Record that `PROMPT_KEY_NOTICE_VERSION` was shown (`state.json`). */
122
+ onSeen: () => void;
123
+ }
124
+ /**
125
+ * The revision of the prompt-key notice a user must have seen for it to stay quiet
126
+ * (`state.json::promptKeyNoticeVersion`, `0` when absent). BUMP IT WHENEVER THE ADVICE
127
+ * CHANGES, never for a rewording.
128
+ */
129
+ export declare const PROMPT_KEY_NOTICE_VERSION = 1;
130
+ /**
131
+ * The startup notices, as ONE options object.
132
+ *
133
+ * SIX NOTICES, TWO PARAMETERS. This hook used to take five POSITIONAL arguments and
134
+ * this feature would have made it six, which `CLAUDE.md` forbids outright ("函数形参不超过
135
+ * 5 个") and which has no good answer anyway: a sixth positional boolean-shaped
136
+ * argument is unreadable at the call site and un-diffable in review. The object also
137
+ * makes an older caller that passes nothing byte-identical to today.
138
+ */
139
+ export interface StartupNoticeOptions {
140
+ mouse?: MouseNoticeOptions;
141
+ retain?: RetainNoticeOptions;
142
+ vtInput?: VtInputNoticeOptions;
143
+ scrollMode?: ScrollModeNoticeOptions;
144
+ promptKeys?: PromptKeyNoticeOptions;
145
+ }
146
+ export declare function useStartupNotices(dispatch: (action: ViewAction) => void, options: StartupNoticeOptions): void;
@@ -14,7 +14,8 @@
14
14
  import { useEffect, useRef } from 'react';
15
15
  import { MODE_TOGGLE_KEYS } from '../agent/agent-mode.js';
16
16
  import { readConfigFile } from '../config/store.js';
17
- import { getMouseNoticeVersion, getScrollModeNoticeVersion, getVtInputNoticeVersion, } from '../config/ui-state.js';
17
+ import { getMouseNoticeVersion, getPromptKeyNoticeVersion, getScrollModeNoticeVersion, getVtInputNoticeVersion, } from '../config/ui-state.js';
18
+ import { PROMPT_KEY_NOTICE_COPY } from '../prompts/copy.js';
18
19
  import { getLogger } from '../logging/logger.js';
19
20
  /**
20
21
  * The revision of the mouse notice a user must have seen for it to stay quiet
@@ -103,7 +104,14 @@ export function scrollModeNoticeText() {
103
104
  return ('Scrolling now uses your terminal: wheel, scrollbar, selection and search all work as ' +
104
105
  'usual. Prefer the old full-screen view? Run: aragon config set scrollMode app');
105
106
  }
106
- export function useStartupNotices(dispatch, mouseNotice, retainNotice, vtInputNotice, scrollModeNotice) {
107
+ /**
108
+ * The revision of the prompt-key notice a user must have seen for it to stay quiet
109
+ * (`state.json::promptKeyNoticeVersion`, `0` when absent). BUMP IT WHENEVER THE ADVICE
110
+ * CHANGES, never for a rewording.
111
+ */
112
+ export const PROMPT_KEY_NOTICE_VERSION = 1;
113
+ export function useStartupNotices(dispatch, options) {
114
+ const { mouse: mouseNotice, retain: retainNotice, vtInput: vtInputNotice, scrollMode: scrollModeNotice, promptKeys: promptKeyNotice, } = options;
107
115
  // The sink gives up after repeated write failures (a read-only log directory,
108
116
  // a full disk). Silently logging nothing is exactly the state a user must not
109
117
  // discover later, from an empty file, while trying to explain a bug.
@@ -193,6 +201,25 @@ export function useStartupNotices(dispatch, mouseNotice, retainNotice, vtInputNo
193
201
  dispatch({ type: 'notice', level: 'info', text: scrollModeNoticeText() });
194
202
  scrollNoticeRef.current?.onSeen();
195
203
  }, [scrollNoticeWanted, dispatch]);
204
+ // THE PROMPT-KEY MIGRATION (quick-prompt-library D-20). A durable `notice` and not
205
+ // a toast, for the reason every "you must learn a key" message in this file is one:
206
+ // a toast expires, and the whole mitigation for a moved key is that the sentence is
207
+ // READ. It queues LAST because it explains a key rather than making one work.
208
+ const promptNoticeRef = useRef(promptKeyNotice);
209
+ promptNoticeRef.current = promptKeyNotice;
210
+ const promptKeyWanted = promptKeyNotice !== undefined;
211
+ const promptKeyShown = useRef(false);
212
+ useEffect(() => {
213
+ if (!promptKeyWanted || promptKeyShown.current)
214
+ return;
215
+ // `>=`, not `!==`: a state file written by a NEWER build has already told this
216
+ // user everything the text below says.
217
+ if (getPromptKeyNoticeVersion() >= PROMPT_KEY_NOTICE_VERSION)
218
+ return;
219
+ promptKeyShown.current = true;
220
+ dispatch({ type: 'notice', level: 'info', text: PROMPT_KEY_NOTICE_COPY });
221
+ promptNoticeRef.current?.onSeen();
222
+ }, [promptKeyWanted, dispatch]);
196
223
  // A `warn` notice rather than a toast: it is about a value the user typed, so
197
224
  // it belongs in the transcript where it can be scrolled back to, not in a row
198
225
  // that expires. DEPENDS ON THE TWO SCALARS, never on the options object —
@@ -109,8 +109,16 @@ export function useWheelRouting(options) {
109
109
  if (overlay) {
110
110
  // Modes A only. `model` / `confirm` / `question` own their own keys and
111
111
  // scrolling them would leave an inconsistent cursor (R-11).
112
- const controlled = overlay === 'help' || overlay === 'settings'
113
- || overlay === 'plan' || overlay === 'queue';
112
+ const controlled = overlay === 'help' || overlay === 'settings' || overlay === 'plan'
113
+ || overlay === 'queue' || overlay === 'kit'
114
+ // The prompt surfaces (quick-prompt-library appendix B-2). Missing this line
115
+ // makes the wheel fall through to the transcript BEHIND an open overlay,
116
+ // which reads as a frozen list and a jumping transcript at the same time.
117
+ || overlay === 'prompts' || overlay === 'prompt-pick'
118
+ // The Agents board (tui-subagent-panel §6.5). Missing this line makes the
119
+ // wheel fall through to the transcript BEHIND the open board, which reads
120
+ // as a frozen list and a jumping transcript at the same time.
121
+ || overlay === 'agents';
114
122
  if (!controlled)
115
123
  return;
116
124
  // D-3: the row is not consulted here either. Behind an overlay the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aragon-agent/cli",
3
- "version": "0.6.34",
3
+ "version": "0.6.36",
4
4
  "description": "AragonAgent CLI — a Claude-Code / Codex-style interactive terminal UI (TUI) on top of the @aragon-agent/core engine. Installable and runnable via npm / npx.",
5
5
  "license": "MIT",
6
6
  "author": "AragonAgent contributors",
@@ -56,7 +56,7 @@
56
56
  "prepublishOnly": "npm run build"
57
57
  },
58
58
  "dependencies": {
59
- "@aragon-agent/core": "^0.2.46",
59
+ "@aragon-agent/core": "^0.2.48",
60
60
  "chalk": "^5.6.2",
61
61
  "cli-highlight": "^2.0.0",
62
62
  "commander": "^12.1.0",