@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
@@ -49,6 +49,34 @@ export const HEADER_HINT_LIMITS = {
49
49
  /** The verbatim clause text (spec 3.8, appendix A). */
50
50
  const BRANCH_CLAUSE = { collapsed: '^L branch', expanded: '^L new branch' };
51
51
  const INDEX_CLAUSE = { collapsed: '^I Index', expanded: '^I build index' };
52
+ /**
53
+ * THE KIT CLAUSE HAS NO COLLAPSED FORM (agent-model-kits D-11).
54
+ *
55
+ * The requirement says the `ctrl+k` hint appears AFTER `ctrl+G`; the collapsed
56
+ * row is the narrowest budget in the header, and `^K` is a low-frequency action.
57
+ * Spending 13 columns of the everyday row on it would push `provider:model`
58
+ * leftward on every launch for a key pressed twice a session.
59
+ */
60
+ const KIT_CLAUSE = { expanded: '^K switch kit' };
61
+ /**
62
+ * THE PROMPT LIBRARY KEY HAS EXACTLY ONE FORM, AND IT IS THE SHORT ONE
63
+ * (quick-prompt-library D-15).
64
+ *
65
+ * `^P` is a REQUIREMENT (the user must be able to see that `ctrl+P` exists), and
66
+ * the collapsed row is where that requirement is met. The expanded row's job is to
67
+ * reveal MORE keys than the collapsed one, so inserting a 17-cell
68
+ * `^P prompt library` before `^I` there would trade `^I` and `^K` away on an
69
+ * 80-column terminal - a user who pressed `^G` asking for more would be shown less.
70
+ *
71
+ * The second thing this constant's position encodes is an explicit REVISION of the
72
+ * `RV-P1-2` ordering constraint for the COLLAPSED ladder: `^P` sits before `^I`
73
+ * there, so `^I Index` now appears from 66 columns instead of 54. That is a
74
+ * deliberate, documented loss: `^I` is a once-per-project action and `^P` is the
75
+ * entry point this feature is about. The expanded ladder keeps its full existing
76
+ * order, so `RV-P1-2` still holds there and `^I` never disappears from a row where
77
+ * it used to fit.
78
+ */
79
+ const PROMPTS_CLAUSE = '^P prompts';
52
80
  const DETAIL_CLAUSE = { collapsed: '^G more', expanded: '^G less' };
53
81
  /** L3 gives the key name and nothing else: discoverability at the last width. */
54
82
  const BRANCH_KEY_ONLY = '^L';
@@ -60,8 +88,9 @@ const SCROLL_CHIP_MAX = 9999;
60
88
  /**
61
89
  * Plan the right-hand hints.
62
90
  *
63
- * The four inputs are exactly the props `Header` already receives - the call site
64
- * does not change, only the arithmetic behind it (spec 3.11 step 5).
91
+ * The inputs are the props `Header` receives plus the two switches that decide which
92
+ * clauses exist at all - the call site does not change, only the arithmetic behind it
93
+ * (spec 3.11 step 5).
65
94
  */
66
95
  export function planHeaderHints(input) {
67
96
  // NORMALISED HERE, NOT TRUSTED (R-14). `Header` defaults to 80, but a test or a
@@ -77,7 +106,7 @@ export function planHeaderHints(input) {
77
106
  const detail = input.statusExpanded ? DETAILS_ON : DETAILS_OFF;
78
107
  return buildPlan([{ id: 'detail', text: detail }], scrollCells, true);
79
108
  }
80
- const ladder = ladderFor(input.statusExpanded);
109
+ const ladder = ladderFor(input.statusExpanded, input.kitHint !== false, input.promptsHint !== false);
81
110
  // THE SEPARATOR IS SPENT OUT OF THE SAME ROW (defect 12): left half + gap + this
82
111
  // column have to add up to `columns`, so the gap comes off the budget instead of
83
112
  // being assumed free.
@@ -94,8 +123,18 @@ export function planHeaderHints(input) {
94
123
  // shrinks (to zero), and `overflow="hidden"` keeps that from overflowing.
95
124
  return buildPlan(ladder[ladder.length - 1] ?? [], scrollCells, false);
96
125
  }
97
- /** The four rungs, widest first. Built per call because the widths are text-derived. */
98
- function ladderFor(statusExpanded) {
126
+ /**
127
+ * The rungs, widest first. Built per call because the widths are text-derived.
128
+ *
129
+ * THE EXPANDED ORDER `branch -> index -> kit -> detail` IS A REGRESSION
130
+ * CONSTRAINT, NOT A PREFERENCE (RV-P1-2). With `kit` AFTER `index`, rungs E2,
131
+ * E4, E5 and E6 are exactly today's four expanded rungs, so every existing
132
+ * width assertion for 48-78 columns keeps its old answer and `^I` never
133
+ * disappears from a row where it used to fit. Putting `^K` first would have cost
134
+ * `^I` everything from 64 to 78 columns - a permanent loss, paid for a
135
+ * low-frequency key.
136
+ */
137
+ function ladderFor(statusExpanded, kitHint, promptsHint) {
99
138
  const branch = {
100
139
  id: 'branch',
101
140
  text: statusExpanded ? BRANCH_CLAUSE.expanded : BRANCH_CLAUSE.collapsed,
@@ -108,12 +147,39 @@ function ladderFor(statusExpanded) {
108
147
  id: 'detail',
109
148
  text: statusExpanded ? DETAIL_CLAUSE.expanded : DETAIL_CLAUSE.collapsed,
110
149
  };
111
- return [
150
+ const branchKey = { id: 'branch', text: BRANCH_KEY_ONLY };
151
+ const prompts = { id: 'prompts', text: PROMPTS_CLAUSE };
152
+ const kit = { id: 'kit', text: KIT_CLAUSE.expanded };
153
+ // THE LADDERS ARE WRITTEN WITH EVERY CLAUSE IN PLACE AND THEN FILTERED, rather
154
+ // than branched per width. Two independently written tables drift: the day one of
155
+ // them gains a rung, the `prompts.hint=false` / `kitHint=false` degradations stop
156
+ // being "the same row minus that clause" and nothing would notice. Filtering a
157
+ // single table makes that property structural.
158
+ const keep = (clause) => (promptsHint || clause.id !== 'prompts') && (kitHint || clause.id !== 'kit');
159
+ // COLLAPSED (six rungs). `^P prompts` sits between the branch and the index, and
160
+ // the third rung is where `^L` loses its label in favour of `^P` - see the note
161
+ // on `PROMPTS_CLAUSE` above for the revision of `RV-P1-2` this encodes.
162
+ const collapsed = [
163
+ [branch, prompts, index, detail],
164
+ [branch, prompts, detail],
165
+ [branchKey, prompts, detail],
166
+ [branch, detail],
167
+ [branchKey, detail],
168
+ [detail],
169
+ ];
170
+ // EXPANDED (seven rungs). `^P` appears in the WIDEST rung only, and AFTER `^K`,
171
+ // so E2..E7 - the rungs that decide what a 34..90 column terminal shows - are
172
+ // byte-identical to the pre-feature answers.
173
+ const expanded = [
174
+ [branch, index, kit, prompts, detail],
175
+ [branch, index, kit, detail],
112
176
  [branch, index, detail],
177
+ [branch, kit, detail],
113
178
  [branch, detail],
114
- [{ id: 'branch', text: BRANCH_KEY_ONLY }, detail],
179
+ [branchKey, detail],
115
180
  [detail],
116
181
  ];
182
+ return (statusExpanded ? expanded : collapsed).map((rung) => rung.filter(keep));
117
183
  }
118
184
  /**
119
185
  * Compose the final string. The chip carries its OWN trailing spacing and is the
@@ -0,0 +1,50 @@
1
+ /**
2
+ * The status bar's kit chip: pure width planning, no JSX
3
+ * (agent-model-kits §4.8 / D-12).
4
+ *
5
+ * ASCII ONLY - `src/ui/**` is inside `glyphs.test.ts::inScope`.
6
+ *
7
+ * WHY A PLANNER, AND WHY IT IS THE SAME SHAPE AS `session-chip.ts`: the status
8
+ * row is measured cell by cell, and anything appended "just to the right" eats
9
+ * `phase` - the one field a user needs while a run is in flight. The chip
10
+ * therefore competes for the SAME budget as every other field, and the rung that
11
+ * survives is computed rather than hand-tuned.
12
+ *
13
+ * ONE COMPARISON, NOT A THRESHOLD TABLE: `columns - (cells + gap) >=
14
+ * minStatusCells`. The width table in the spec is what that comparison WORKS
15
+ * OUT TO. `minStatusCells` is deliberately the same 34 as the session chip's -
16
+ * both answer "how much room must the left half keep", and two numbers for one
17
+ * question is how a "fit" and a "don't fit" end up disagreeing.
18
+ */
19
+ export declare const KIT_CHIP_LIMITS: {
20
+ /** Blank cells between the chip and the field run. */
21
+ readonly gapCells: 2;
22
+ /** The floor the left half must keep; asserted equal to the session chip's. */
23
+ readonly minStatusCells: 34;
24
+ /** Below this the `kit` label is dropped before the name is shortened. */
25
+ readonly labelMinCols: 72;
26
+ /** The shortest unique prefix worth showing; below it the chip hides. */
27
+ readonly minNameChars: 4;
28
+ };
29
+ export interface KitChipPlan {
30
+ visible: boolean;
31
+ text: string;
32
+ cells: number;
33
+ gap: number;
34
+ }
35
+ export interface KitChipInput {
36
+ columns: number;
37
+ /**
38
+ * The kit in effect, or absent/null when there is none.
39
+ *
40
+ * ABSENT IS NOT EMPTY: with no kit the plan is hidden and the row's output is
41
+ * byte-identical to the pre-feature build (I-9).
42
+ */
43
+ kit?: {
44
+ name: string;
45
+ modified: boolean;
46
+ otherNames: readonly string[];
47
+ } | null | undefined;
48
+ }
49
+ /** Plan the chip for `columns` and this session's kit. */
50
+ export declare function planKitChip(input: KitChipInput): KitChipPlan;
@@ -0,0 +1,81 @@
1
+ /**
2
+ * The status bar's kit chip: pure width planning, no JSX
3
+ * (agent-model-kits §4.8 / D-12).
4
+ *
5
+ * ASCII ONLY - `src/ui/**` is inside `glyphs.test.ts::inScope`.
6
+ *
7
+ * WHY A PLANNER, AND WHY IT IS THE SAME SHAPE AS `session-chip.ts`: the status
8
+ * row is measured cell by cell, and anything appended "just to the right" eats
9
+ * `phase` - the one field a user needs while a run is in flight. The chip
10
+ * therefore competes for the SAME budget as every other field, and the rung that
11
+ * survives is computed rather than hand-tuned.
12
+ *
13
+ * ONE COMPARISON, NOT A THRESHOLD TABLE: `columns - (cells + gap) >=
14
+ * minStatusCells`. The width table in the spec is what that comparison WORKS
15
+ * OUT TO. `minStatusCells` is deliberately the same 34 as the session chip's -
16
+ * both answer "how much room must the left half keep", and two numbers for one
17
+ * question is how a "fit" and a "don't fit" end up disagreeing.
18
+ */
19
+ import stringWidth from 'string-width';
20
+ export const KIT_CHIP_LIMITS = {
21
+ /** Blank cells between the chip and the field run. */
22
+ gapCells: 2,
23
+ /** The floor the left half must keep; asserted equal to the session chip's. */
24
+ minStatusCells: 34,
25
+ /** Below this the `kit` label is dropped before the name is shortened. */
26
+ labelMinCols: 72,
27
+ /** The shortest unique prefix worth showing; below it the chip hides. */
28
+ minNameChars: 4,
29
+ };
30
+ const HIDDEN = { visible: false, text: '', cells: 0, gap: 0 };
31
+ /** The shortest prefix of `name` that no sibling name also starts with. */
32
+ function uniqueLength(name, others) {
33
+ const points = [...name];
34
+ const start = Math.min(KIT_CHIP_LIMITS.minNameChars, points.length);
35
+ const shared = (length) => {
36
+ const prefix = points.slice(0, length).join('');
37
+ return others.some((other) => [...other].slice(0, length).join('') === prefix);
38
+ };
39
+ let length = start;
40
+ while (length < points.length && shared(length))
41
+ length += 1;
42
+ return Math.max(length, start);
43
+ }
44
+ /** Plan the chip for `columns` and this session's kit. */
45
+ export function planKitChip(input) {
46
+ const kit = input.kit;
47
+ if (!kit)
48
+ return HIDDEN;
49
+ const name = kit.name.trim();
50
+ if (!name)
51
+ return HIDDEN;
52
+ const columns = Number.isFinite(input.columns) ? Math.floor(input.columns) : 80;
53
+ const gap = KIT_CHIP_LIMITS.gapCells;
54
+ const fits = (text) => columns - (stringWidth(text) + gap) >= KIT_CHIP_LIMITS.minStatusCells;
55
+ const mark = kit.modified ? '~' : '';
56
+ const plan = (text) => ({ visible: true, text, cells: stringWidth(text), gap });
57
+ // Widest rung first, then drop the label, then shorten the name, then hide.
58
+ // THE ORDER IS THE DECISION: a reader cares WHICH kit is in effect before they
59
+ // care that it is a kit, so the name is the last thing to go.
60
+ //
61
+ // `KIT_CHIP_LIMITS.labelMinCols` NEEDS NO BRANCH: a labelled rung is 45 cells
62
+ // for a 40-character name, so `fits` already rejects it below ~81 columns. The
63
+ // constant is the documented intent (labels belong to wide rows), measured
64
+ // rather than hard-coded into the ladder.
65
+ const points = [...name];
66
+ const shortest = uniqueLength(name, kit.otherNames);
67
+ const rungs = [];
68
+ if (kit.modified)
69
+ rungs.push(`kit ${name}~`);
70
+ rungs.push(`kit ${name}`);
71
+ if (kit.modified)
72
+ rungs.push(`${name}~`);
73
+ rungs.push(name);
74
+ for (let length = points.length - 1; length >= shortest; length -= 1) {
75
+ rungs.push(`${points.slice(0, length).join('')}${mark}`);
76
+ }
77
+ for (const text of rungs)
78
+ if (fits(text))
79
+ return plan(text);
80
+ return HIDDEN;
81
+ }
@@ -55,6 +55,15 @@ export function planStatusDetail(input) {
55
55
  add(statusField('team', 'Team ' + status.teamActive.running + '/' + status.teamActive.total));
56
56
  if (status.todoActive)
57
57
  add(statusField('todo', 'Todo ' + status.todoActive.done + '/' + status.todoActive.total));
58
+ // AFTER `todo`, AND THE ORDER IS THE PRIORITY (D-12): `add()` only appends while
59
+ // the whole row still fits, so a later field is dropped FIRST. `Todo 3/7` is the
60
+ // more important of the two numbers, so it goes first and survives the narrower
61
+ // terminal; `Accept 2/5` degrades before it.
62
+ if (status.acceptanceActive) {
63
+ const acceptance = status.acceptanceActive;
64
+ add(statusField('acceptance', 'Accept ' + acceptance.checked + '/' + acceptance.total
65
+ + (acceptance.failed > 0 ? ' !' + acceptance.failed : ''), acceptance.failed > 0 ? 'error' : 'normal'));
66
+ }
58
67
  add(statusField('model', input.provider + ':' + input.model));
59
68
  if (status.fastActive)
60
69
  add(statusField('fast', 'Fast' + (status.fastActive.inFlight ? ' active' : '')));
@@ -51,6 +51,19 @@ export interface StatusLayoutInput {
51
51
  done: number;
52
52
  total: number;
53
53
  };
54
+ /**
55
+ * The acceptance counter (todo-acceptance-criteria §6.11).
56
+ *
57
+ * A FALLBACK, exactly like `todoActive`: present only when the rail is NOT already
58
+ * reporting the number, because printing `2/5` twice in one row is the defect
59
+ * RV-P2-4 records. Whether it is present is decided by `acceptanceChip()` in
60
+ * `ui/layout/todo-layout.ts`, which owns "what did the rail actually draw".
61
+ */
62
+ acceptanceActive?: {
63
+ checked: number;
64
+ total: number;
65
+ failed: number;
66
+ };
54
67
  ecoRung?: number;
55
68
  fastActive?: {
56
69
  inFlight: boolean;
@@ -0,0 +1,139 @@
1
+ /**
2
+ * Panel row geometry — the ONE authority for how a subagent row is measured and
3
+ * what it says (tui-subagent-panel §4.2 / §6.1).
4
+ *
5
+ * WHY THIS FILE EXISTS. The roster used to place its right-hand columns by
6
+ * `flexGrow`, and a fixed-height parent box makes Yoga stretch the column to its
7
+ * widest CHILD instead of to the available width: on a 100-column terminal inside
8
+ * the band slot the row measured 48 (spec §0.1, cases 3/4), so `grep 22.1s` sat
9
+ * hard against the description and the right half of the row was blank. Flex
10
+ * cannot fix that, because the number it needs - "how wide is this terminal" - is
11
+ * not part of the layout it is given. So the widths are COMPUTED here, by a total
12
+ * pure function, and the component only places fixed-width boxes at the offsets
13
+ * this module names.
14
+ *
15
+ * THE FIVE CELLS, in drawing order:
16
+ *
17
+ * marker(1+status+1) label(8) description(flex) gap(2) activity(n) clock(8)
18
+ *
19
+ * `description` is the ONLY flexible cell, and it is the one that absorbs every
20
+ * width pressure - the same degradation discipline `StatusBar` documents for its
21
+ * left cluster. The two right-hand cells are fixed and `flexShrink={0}`, so their
22
+ * absolute offsets follow from `rowWidth = parentWidth` and the sum of the fixed
23
+ * cells in front of them, and never from how a particular frame allocates slack.
24
+ *
25
+ * ASCII ONLY: `src/ui/**` is inside `glyphs.test.ts`'s static scan.
26
+ *
27
+ * THE ACTIVITY HELPERS LIVE HERE TOO (v2 implementation note, recorded in the
28
+ * spec's "implementation findings"). `teamRowCells` has to compose the activity
29
+ * text with the supervisor badge, which is what `activityWithBadge` already does;
30
+ * leaving those in `TeamPanel.tsx` would make this module and the component
31
+ * import each other. `TeamPanel` therefore RE-EXPORTS every symbol below, so the
32
+ * existing import sites (three test files) keep their import path unchanged.
33
+ */
34
+ import type { OverseerAction, SubagentRun } from '../../team/types.js';
35
+ import type { Glyphs } from '../glyphs.js';
36
+ /** The clock cell: wide enough for `125m07s` (7) with one column of headroom. */
37
+ export declare const TEAM_CLOCK_CELLS = 8;
38
+ /** `run.label.padEnd(8).slice(0,8)` — unchanged from the row this replaces. */
39
+ export declare const TEAM_LABEL_CELLS = 8;
40
+ /** The two spaces in front of the activity cell. */
41
+ export declare const TEAM_GAP_CELLS = 2;
42
+ /**
43
+ * The description column's floor: below this the row is unreadable, so the
44
+ * activity cell gives way instead (`activityDropCols`).
45
+ */
46
+ export declare const TEAM_MIN_DESCRIPTION_CELLS = 12;
47
+ export interface TeamRowGeometry {
48
+ marker: number;
49
+ label: number;
50
+ gap: number;
51
+ activity: number;
52
+ clock: number;
53
+ /** The description column's reserved MINIMUM; the real width comes from flex. */
54
+ minDescription: number;
55
+ }
56
+ /**
57
+ * How many characters the activity cell may spend at this width.
58
+ *
59
+ * `TEAM_LIMITS.activityChars` is a STORAGE ceiling; a terminal has a COLUMN
60
+ * budget, and the two are not the same number. Below
61
+ * `TEAM_LIMITS.activityDropCols` the cell gives way ENTIRELY (returns 0) rather
62
+ * than shrinking to its 12-character floor: at 40 columns those twelve characters
63
+ * would push the description below its own floor, and "who is doing what" outranks
64
+ * "what they are writing right now" exactly when there is room for one of them.
65
+ * That is a semantic change to a shipped assertion (`activityBudget(40)` used to
66
+ * return 12) and is recorded in the spec's §10 collateral table.
67
+ *
68
+ * `markerCells` defaults to the Unicode tier so the three existing single-argument
69
+ * call sites keep compiling; the panel passes the real tier's width.
70
+ */
71
+ export declare function activityBudget(cols: number, markerCells?: number): number;
72
+ /**
73
+ * The row's cell widths. A TOTAL function: every cell is a non-negative integer
74
+ * and the five of them never sum past `cols`, for any `cols >= 0` and either
75
+ * glyph tier (I-3, `team-row-geometry.test.ts`'s property test).
76
+ *
77
+ * THE LADDER IS THE SPECIFICATION (§4.2.2), in the order the cells give way:
78
+ * 1. a wide enough terminal gets the business budget;
79
+ * 2. below `activityDropCols` the activity cell is 0;
80
+ * 3. description still negative -> the gap goes;
81
+ * 4. still negative -> the label shrinks to `max(3, cols - marker - clock)`;
82
+ * 5. still negative -> the clock shrinks (`cols < marker + 3 + clock`);
83
+ * 6. the residue (a terminal narrower than the marker itself) is clamped so the
84
+ * sum stays inside `cols` — unreachable on a real device, where native mode
85
+ * degrades to the placeholder below 40x12, but the function is total because
86
+ * the property test runs `cols = 1..400`.
87
+ */
88
+ export declare function teamRowGeometry(cols: number, glyphs: Glyphs): TeamRowGeometry;
89
+ /**
90
+ * The clock cell's text: this child's elapsed time, or empty when it never
91
+ * started (a `queued` row has no clock to show, and `0ms` would be a lie).
92
+ */
93
+ export declare function teamClockText(run: SubagentRun, at: number): string;
94
+ /** The past-tense word the supervisor badge and status line render. */
95
+ export declare function interventionWord(action: OverseerAction): string;
96
+ /**
97
+ * The row's supervisor badge (subagent-overseer-v2 AC-5): what the supervisor
98
+ * last did to THIS child and how long ago, e.g. `nudged 2m03s`.
99
+ *
100
+ * `lastIntervention.at` is ABSOLUTE epoch ms (R-P2-3), the same units as
101
+ * `startedAt`, so this is one subtraction - no second clock convention.
102
+ */
103
+ export declare function overseerBadge(run: SubagentRun, at: number): string;
104
+ /**
105
+ * The middle cell: what this child is doing right now, in a few words.
106
+ *
107
+ * Two width policies, answering different questions. `wide` decides WHICH FACT to
108
+ * show - a whole path or its basename, a whole command line or its program name -
109
+ * mirroring `StatusBar`'s `statusCompactCols` degradation rather than inventing a
110
+ * second threshold. `activityBudget(cols)` decides HOW MUCH OF IT FITS, and it is
111
+ * a continuous function of width rather than a threshold, because the failure it
112
+ * prevents - a row wider than the terminal - is continuous.
113
+ */
114
+ export declare function activityLine(run: SubagentRun, cols: number, markerCells?: number): string;
115
+ /**
116
+ * The activity cell WITH the badge composed in (AC-5 / risk table): the badge
117
+ * rides the SAME allocation the prose uses, so a row can never grow past its
118
+ * terminal; on a narrow terminal the badge degrades to the action word alone -
119
+ * the who-acted fact outranks the what-they-are-doing tail exactly when there is
120
+ * room for only one.
121
+ */
122
+ export declare function activityWithBadge(run: SubagentRun, cols: number, at: number, markerCells?: number): string;
123
+ /** The five text fragments of one row, each already clamped to its cell's policy. */
124
+ export interface TeamRowCells {
125
+ marker: string;
126
+ label: string;
127
+ description: string;
128
+ activity: string;
129
+ clock: string;
130
+ }
131
+ /**
132
+ * The five text fragments of one row.
133
+ *
134
+ * The description is normalized here rather than in the component because
135
+ * truncation preserves explicit newlines and the band reserves exactly ONE row
136
+ * for a child: a description carrying a newline would make the row two rows tall
137
+ * and every row below it would land one row lower than `band.teamRows` allows.
138
+ */
139
+ export declare function teamRowCells(run: SubagentRun, cols: number, at: number, glyphs: Glyphs): TeamRowCells;
@@ -0,0 +1,246 @@
1
+ /**
2
+ * Panel row geometry — the ONE authority for how a subagent row is measured and
3
+ * what it says (tui-subagent-panel §4.2 / §6.1).
4
+ *
5
+ * WHY THIS FILE EXISTS. The roster used to place its right-hand columns by
6
+ * `flexGrow`, and a fixed-height parent box makes Yoga stretch the column to its
7
+ * widest CHILD instead of to the available width: on a 100-column terminal inside
8
+ * the band slot the row measured 48 (spec §0.1, cases 3/4), so `grep 22.1s` sat
9
+ * hard against the description and the right half of the row was blank. Flex
10
+ * cannot fix that, because the number it needs - "how wide is this terminal" - is
11
+ * not part of the layout it is given. So the widths are COMPUTED here, by a total
12
+ * pure function, and the component only places fixed-width boxes at the offsets
13
+ * this module names.
14
+ *
15
+ * THE FIVE CELLS, in drawing order:
16
+ *
17
+ * marker(1+status+1) label(8) description(flex) gap(2) activity(n) clock(8)
18
+ *
19
+ * `description` is the ONLY flexible cell, and it is the one that absorbs every
20
+ * width pressure - the same degradation discipline `StatusBar` documents for its
21
+ * left cluster. The two right-hand cells are fixed and `flexShrink={0}`, so their
22
+ * absolute offsets follow from `rowWidth = parentWidth` and the sum of the fixed
23
+ * cells in front of them, and never from how a particular frame allocates slack.
24
+ *
25
+ * ASCII ONLY: `src/ui/**` is inside `glyphs.test.ts`'s static scan.
26
+ *
27
+ * THE ACTIVITY HELPERS LIVE HERE TOO (v2 implementation note, recorded in the
28
+ * spec's "implementation findings"). `teamRowCells` has to compose the activity
29
+ * text with the supervisor badge, which is what `activityWithBadge` already does;
30
+ * leaving those in `TeamPanel.tsx` would make this module and the component
31
+ * import each other. `TeamPanel` therefore RE-EXPORTS every symbol below, so the
32
+ * existing import sites (three test files) keep their import path unchanged.
33
+ */
34
+ import { describeToolActivity, sanitizeActivity } from '../../team/activity.js';
35
+ import { TEAM_LIMITS } from '../../team/limits.js';
36
+ import { formatDuration } from '../../agent/usage.js';
37
+ import { fitCells } from './cells.js';
38
+ import { childElapsedMs, childStatus, statusMarkerCell, statusMarkerWidth } from '../team-status.js';
39
+ /** The clock cell: wide enough for `125m07s` (7) with one column of headroom. */
40
+ export const TEAM_CLOCK_CELLS = 8;
41
+ /** `run.label.padEnd(8).slice(0,8)` — unchanged from the row this replaces. */
42
+ export const TEAM_LABEL_CELLS = 8;
43
+ /** The two spaces in front of the activity cell. */
44
+ export const TEAM_GAP_CELLS = 2;
45
+ /**
46
+ * The description column's floor: below this the row is unreadable, so the
47
+ * activity cell gives way instead (`activityDropCols`).
48
+ */
49
+ export const TEAM_MIN_DESCRIPTION_CELLS = 12;
50
+ /**
51
+ * The concession to code units vs terminal columns, inherited verbatim from the
52
+ * `WIDE_CHAR_ALLOWANCE` comment on the row this replaces: this budget is
53
+ * denominated in code units while the terminal is denominated in columns, and CJK
54
+ * and emoji occupy two columns per unit, so the budget is halved against the
55
+ * reserve rather than spent to the last cell. Erring small costs a few characters
56
+ * of prose; erring large costs the clock column.
57
+ */
58
+ const WIDE_CHAR_ALLOWANCE = 2;
59
+ /** The marker cell of the Unicode tier, used as the default by callers that pass no tier. */
60
+ const DEFAULT_MARKER_CELLS = 3;
61
+ function nonNegativeInt(value) {
62
+ return Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
63
+ }
64
+ /**
65
+ * How many characters the activity cell may spend at this width.
66
+ *
67
+ * `TEAM_LIMITS.activityChars` is a STORAGE ceiling; a terminal has a COLUMN
68
+ * budget, and the two are not the same number. Below
69
+ * `TEAM_LIMITS.activityDropCols` the cell gives way ENTIRELY (returns 0) rather
70
+ * than shrinking to its 12-character floor: at 40 columns those twelve characters
71
+ * would push the description below its own floor, and "who is doing what" outranks
72
+ * "what they are writing right now" exactly when there is room for one of them.
73
+ * That is a semantic change to a shipped assertion (`activityBudget(40)` used to
74
+ * return 12) and is recorded in the spec's §10 collateral table.
75
+ *
76
+ * `markerCells` defaults to the Unicode tier so the three existing single-argument
77
+ * call sites keep compiling; the panel passes the real tier's width.
78
+ */
79
+ export function activityBudget(cols, markerCells = DEFAULT_MARKER_CELLS) {
80
+ if (!Number.isFinite(cols) || cols < TEAM_LIMITS.activityDropCols)
81
+ return 0;
82
+ const fixed = markerCells + TEAM_LABEL_CELLS + TEAM_GAP_CELLS + TEAM_CLOCK_CELLS
83
+ + TEAM_MIN_DESCRIPTION_CELLS;
84
+ const spare = Math.floor((cols - fixed) / WIDE_CHAR_ALLOWANCE);
85
+ return Math.max(TEAM_LIMITS.activityMinChars, Math.min(TEAM_LIMITS.activityChars, spare));
86
+ }
87
+ /**
88
+ * The row's cell widths. A TOTAL function: every cell is a non-negative integer
89
+ * and the five of them never sum past `cols`, for any `cols >= 0` and either
90
+ * glyph tier (I-3, `team-row-geometry.test.ts`'s property test).
91
+ *
92
+ * THE LADDER IS THE SPECIFICATION (§4.2.2), in the order the cells give way:
93
+ * 1. a wide enough terminal gets the business budget;
94
+ * 2. below `activityDropCols` the activity cell is 0;
95
+ * 3. description still negative -> the gap goes;
96
+ * 4. still negative -> the label shrinks to `max(3, cols - marker - clock)`;
97
+ * 5. still negative -> the clock shrinks (`cols < marker + 3 + clock`);
98
+ * 6. the residue (a terminal narrower than the marker itself) is clamped so the
99
+ * sum stays inside `cols` — unreachable on a real device, where native mode
100
+ * degrades to the placeholder below 40x12, but the function is total because
101
+ * the property test runs `cols = 1..400`.
102
+ */
103
+ export function teamRowGeometry(cols, glyphs) {
104
+ const width = nonNegativeInt(cols);
105
+ const markerCells = 1 + statusMarkerWidth(glyphs) + 1;
106
+ const cells = {
107
+ marker: Math.min(markerCells, width),
108
+ label: TEAM_LABEL_CELLS,
109
+ gap: TEAM_GAP_CELLS,
110
+ activity: activityBudget(width, markerCells),
111
+ clock: TEAM_CLOCK_CELLS,
112
+ minDescription: TEAM_MIN_DESCRIPTION_CELLS,
113
+ };
114
+ const sum = () => cells.marker + cells.label + cells.gap + cells.activity + cells.clock;
115
+ if (sum() > width)
116
+ cells.gap = 0;
117
+ if (sum() > width)
118
+ cells.label = Math.max(3, width - cells.marker - cells.clock);
119
+ if (sum() > width)
120
+ cells.clock = Math.max(0, Math.min(TEAM_CLOCK_CELLS, width - cells.marker - cells.label));
121
+ if (sum() > width)
122
+ cells.label = Math.max(0, Math.min(cells.label, width - cells.marker));
123
+ cells.minDescription = sum() >= width ? 0 : TEAM_MIN_DESCRIPTION_CELLS;
124
+ return cells;
125
+ }
126
+ /**
127
+ * The clock cell's text: this child's elapsed time, or empty when it never
128
+ * started (a `queued` row has no clock to show, and `0ms` would be a lie).
129
+ */
130
+ export function teamClockText(run, at) {
131
+ return run.startedAt === undefined ? '' : formatDuration(childElapsedMs(run, at));
132
+ }
133
+ /** The past-tense word the supervisor badge and status line render. */
134
+ export function interventionWord(action) {
135
+ switch (action) {
136
+ case 'nudge':
137
+ return 'nudged';
138
+ case 'replace':
139
+ return 'replaced';
140
+ case 'abandon':
141
+ return 'abandoned';
142
+ default:
143
+ return 'waited';
144
+ }
145
+ }
146
+ /**
147
+ * The row's supervisor badge (subagent-overseer-v2 AC-5): what the supervisor
148
+ * last did to THIS child and how long ago, e.g. `nudged 2m03s`.
149
+ *
150
+ * `lastIntervention.at` is ABSOLUTE epoch ms (R-P2-3), the same units as
151
+ * `startedAt`, so this is one subtraction - no second clock convention.
152
+ */
153
+ export function overseerBadge(run, at) {
154
+ const last = run.lastIntervention;
155
+ if (last === undefined)
156
+ return '';
157
+ return `${interventionWord(last.action)} ${formatDuration(Math.max(0, at - last.at))}`;
158
+ }
159
+ /**
160
+ * The middle cell: what this child is doing right now, in a few words.
161
+ *
162
+ * Two width policies, answering different questions. `wide` decides WHICH FACT to
163
+ * show - a whole path or its basename, a whole command line or its program name -
164
+ * mirroring `StatusBar`'s `statusCompactCols` degradation rather than inventing a
165
+ * second threshold. `activityBudget(cols)` decides HOW MUCH OF IT FITS, and it is
166
+ * a continuous function of width rather than a threshold, because the failure it
167
+ * prevents - a row wider than the terminal - is continuous.
168
+ */
169
+ export function activityLine(run, cols, markerCells) {
170
+ const wide = cols >= TEAM_LIMITS.activityWideCols;
171
+ const budget = activityBudget(cols, markerCells);
172
+ switch (run.phase) {
173
+ case 'queued':
174
+ return 'queued';
175
+ case 'starting':
176
+ // The one row that legitimately restarts its clock, saying why (P2-4):
177
+ // F-2 ranks the running group by `startedAt` and a retry re-stamps it.
178
+ return run.retries ? 'starting (retry)' : 'starting';
179
+ case 'thinking':
180
+ // Clamped AT RENDER, against this terminal's budget. `run.activity` was
181
+ // clamped at STORE against `activityChars`; that is a ceiling, not a fit.
182
+ return run.activity ? `writing: ${sanitizeActivity(run.activity, budget)}` : 'thinking';
183
+ case 'tool':
184
+ case 'waiting':
185
+ return run.lastTool
186
+ ? sanitizeActivity(describeToolActivity(run.lastTool, run.activityArgs, wide), budget)
187
+ : run.phase === 'waiting'
188
+ ? 'waiting for mail'
189
+ : 'tool';
190
+ case 'done':
191
+ // `compacted N` ON THE SETTLED ROW ONLY (hardening §3.4.4 / W3). A running
192
+ // row is already competing for `activityBudget(cols)` with the child's own
193
+ // prose, and a marker that displaces what the child is DOING right now
194
+ // buys nothing - the dispatch report carries the same fact for good.
195
+ return run.compactions
196
+ ? `done ${run.turns} turns compacted ${run.compactions}`
197
+ : `done ${run.turns} turns`;
198
+ case 'failed':
199
+ return 'failed';
200
+ case 'aborted':
201
+ return 'aborted';
202
+ }
203
+ }
204
+ /**
205
+ * The activity cell WITH the badge composed in (AC-5 / risk table): the badge
206
+ * rides the SAME allocation the prose uses, so a row can never grow past its
207
+ * terminal; on a narrow terminal the badge degrades to the action word alone -
208
+ * the who-acted fact outranks the what-they-are-doing tail exactly when there is
209
+ * room for only one.
210
+ */
211
+ export function activityWithBadge(run, cols, at, markerCells) {
212
+ const last = run.lastIntervention;
213
+ if (last === undefined)
214
+ return activityLine(run, cols, markerCells);
215
+ const badge = overseerBadge(run, at);
216
+ if (cols < TEAM_LIMITS.activityWideCols)
217
+ return interventionWord(last.action);
218
+ return sanitizeActivity(`${badge}; ${activityLine(run, cols, markerCells)}`, activityBudget(cols, markerCells));
219
+ }
220
+ /**
221
+ * The five text fragments of one row.
222
+ *
223
+ * The description is normalized here rather than in the component because
224
+ * truncation preserves explicit newlines and the band reserves exactly ONE row
225
+ * for a child: a description carrying a newline would make the row two rows tall
226
+ * and every row below it would land one row lower than `band.teamRows` allows.
227
+ */
228
+ export function teamRowCells(run, cols, at, glyphs) {
229
+ const geometry = teamRowGeometry(cols, glyphs);
230
+ // THE TEXT IS CLAMPED TO THE SAME WIDTH THE GEOMETRY ALLOCATED, so a cell's content
231
+ // measures no more than its box. `activityWithBadge` already clamps to the business
232
+ // budget; this is the belt-and-braces that also covers the two cases the budget alone
233
+ // cannot express - a ZERO-width cell (below `activityDropCols`, where the answer is
234
+ // "no activity column at all", not "a nine-character word in a zero-column box") and
235
+ // a wide character straddling the edge.
236
+ const activity = geometry.activity === 0
237
+ ? ''
238
+ : fitCells(activityWithBadge(run, cols, at, geometry.marker), geometry.activity, glyphs.ellipsis);
239
+ return {
240
+ marker: statusMarkerCell(childStatus(run), glyphs),
241
+ label: run.label.padEnd(TEAM_LABEL_CELLS).slice(0, TEAM_LABEL_CELLS),
242
+ description: run.description.replace(/\s+/g, ' '),
243
+ activity,
244
+ clock: teamClockText(run, at),
245
+ };
246
+ }