@aragon-agent/cli 0.6.34 → 0.6.35

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 (64) hide show
  1. package/README.md +3071 -3007
  2. package/dist/agent/controller.d.ts +29 -0
  3. package/dist/agent/controller.js +42 -1
  4. package/dist/agent/kit-apply.d.ts +56 -0
  5. package/dist/agent/kit-apply.js +209 -0
  6. package/dist/agent/model-profile-settings.d.ts +28 -0
  7. package/dist/agent/model-profile-settings.js +142 -29
  8. package/dist/agent/reducer.d.ts +1 -1
  9. package/dist/commands/builtins.js +4 -0
  10. package/dist/commands/kit-command.d.ts +20 -0
  11. package/dist/commands/kit-command.js +131 -0
  12. package/dist/config/kit-resolution.d.ts +93 -0
  13. package/dist/config/kit-resolution.js +226 -0
  14. package/dist/config/model-kits.d.ts +224 -0
  15. package/dist/config/model-kits.js +302 -0
  16. package/dist/config/model-profile-store.d.ts +25 -0
  17. package/dist/config/model-profiles.d.ts +41 -3
  18. package/dist/config/model-profiles.js +46 -5
  19. package/dist/config/schema.d.ts +16 -0
  20. package/dist/input/keymap.js +18 -4
  21. package/dist/ui/App.js +108 -12
  22. package/dist/ui/Header.d.ts +9 -1
  23. package/dist/ui/Header.js +8 -2
  24. package/dist/ui/PromptInput.js +1 -1
  25. package/dist/ui/ScrollbackShell.d.ts +18 -3
  26. package/dist/ui/ScrollbackShell.js +1 -1
  27. package/dist/ui/StatusBar.d.ts +13 -0
  28. package/dist/ui/StatusBar.js +10 -3
  29. package/dist/ui/TeamPanel.d.ts +12 -0
  30. package/dist/ui/TeamPanel.js +10 -31
  31. package/dist/ui/Transcript.js +22 -2
  32. package/dist/ui/entries/TeamCard.d.ts +18 -2
  33. package/dist/ui/entries/TeamCard.js +47 -13
  34. package/dist/ui/layout/header-hints.d.ts +11 -1
  35. package/dist/ui/layout/header-hints.js +35 -3
  36. package/dist/ui/layout/kit-chip.d.ts +50 -0
  37. package/dist/ui/layout/kit-chip.js +81 -0
  38. package/dist/ui/layout/virtual-window.js +9 -0
  39. package/dist/ui/overlays/HelpOverlay.js +7 -0
  40. package/dist/ui/overlays/KitEditor.d.ts +23 -0
  41. package/dist/ui/overlays/KitEditor.js +101 -0
  42. package/dist/ui/overlays/KitManager.d.ts +23 -0
  43. package/dist/ui/overlays/KitManager.js +104 -0
  44. package/dist/ui/overlays/KitPicker.d.ts +49 -0
  45. package/dist/ui/overlays/KitPicker.js +58 -0
  46. package/dist/ui/overlays/ModelProfilePicker.js +16 -6
  47. package/dist/ui/overlays/SettingsScreen.js +139 -16
  48. package/dist/ui/overlays/kit-picker-state.d.ts +87 -0
  49. package/dist/ui/overlays/kit-picker-state.js +136 -0
  50. package/dist/ui/overlays/kit-settings-state.d.ts +129 -0
  51. package/dist/ui/overlays/kit-settings-state.js +267 -0
  52. package/dist/ui/overlays/model-profile-state.d.ts +26 -3
  53. package/dist/ui/overlays/model-profile-state.js +59 -6
  54. package/dist/ui/scrollback/LiveTail.d.ts +2 -2
  55. package/dist/ui/scrollback/LiveTail.js +7 -2
  56. package/dist/ui/scrollback/live-preview.js +10 -3
  57. package/dist/ui/scrollback/tail-window.d.ts +12 -0
  58. package/dist/ui/scrollback/tail-window.js +17 -2
  59. package/dist/ui/scrollback/use-scrollback-view.js +7 -0
  60. package/dist/ui/team-status.d.ts +141 -0
  61. package/dist/ui/team-status.js +224 -0
  62. package/dist/ui/transcript-text.js +13 -4
  63. package/dist/ui/use-wheel-routing.js +2 -2
  64. package/package.json +2 -2
@@ -1,7 +1,9 @@
1
1
  import { type ModelProfile, type ModelProfilesConfig, type ModelRole } from '../../config/model-profiles.js';
2
+ import type { AgentKit } from '../../config/model-kits.js';
2
3
  import type { ModelSettingsDraft } from '../../config/model-profile-store.js';
3
4
  import type { SubagentModelMode } from '../../team/model.js';
4
- export type ProfilePage = 'root' | 'picker' | 'manager' | 'editor' | 'models' | 'delete-confirm' | 'discard-confirm';
5
+ import type { KitDraft } from './kit-settings-state.js';
6
+ export type ProfilePage = 'root' | 'picker' | 'manager' | 'editor' | 'models' | 'delete-confirm' | 'discard-confirm' | 'kits' | 'kit-edit' | 'kit-slot' | 'kit-delete';
5
7
  /**
6
8
  * Which role a PROFILE PICKER page is choosing for.
7
9
  *
@@ -9,8 +11,13 @@ export type ProfilePage = 'root' | 'picker' | 'manager' | 'editor' | 'models' |
9
11
  * `'main' | 'fast'` because it is the type basis of the config layer's
10
12
  * `${role}Id` template keys - widening it would invent a `subagentId` library
11
13
  * key nothing reads. The subagent picker writes `draft.patch.team` instead.
14
+ *
15
+ * `'binding'` IS A KIT SLOT CHOOSER (agent-model-kits §4.5): a plain profile
16
+ * list with no `Current custom` row, no built-in rows and one `Create profile`
17
+ * action. A kit slot can only ever name a profile, so offering "no profile" as a
18
+ * choice there would be offering an invalid kit.
12
19
  */
13
- export type ProfilePickerRole = ModelRole | 'subagent';
20
+ export type ProfilePickerRole = ModelRole | 'subagent' | 'binding';
14
21
  /** The picker's built-in `Fast tier model` row; never a profile id on disk. */
15
22
  export declare const SUBAGENT_FAST_ROW_ID = "__fast__";
16
23
  export interface ProfileEditorDraft {
@@ -30,13 +37,27 @@ export interface ModelProfileState {
30
37
  page: ProfilePage;
31
38
  role: ProfilePickerRole;
32
39
  editor?: ProfileEditorDraft;
33
- returnPage: 'picker' | 'manager';
40
+ /**
41
+ * Where the editor and the delete confirmation hand control back.
42
+ *
43
+ * `'kit-slot'` JOINS `'picker'` (agent-model-kits §4.5): the kit slot chooser
44
+ * is a profile picker too, and its `Create profile` action must return to the
45
+ * slot it was creating for rather than to the profile manager.
46
+ */
47
+ returnPage: 'picker' | 'manager' | 'kit-slot';
34
48
  bindNew: boolean;
35
49
  dirty: boolean;
36
50
  error?: string;
37
51
  deleteId?: string;
38
52
  /** Who referenced the entry being deleted ('Main + Subagent', or ''). */
39
53
  deleteUsers?: string;
54
+ /**
55
+ * The kit editor's own draft (agent-model-kits §4.5), owned by `kitReducer`.
56
+ *
57
+ * ONE FIELD, NOT A SECOND STATE OBJECT: the page axis stays shared, so `Save`
58
+ * and `Esc` mean the same thing on every page of this screen.
59
+ */
60
+ kitDraft?: KitDraft;
40
61
  }
41
62
  export type ModelProfileAction = {
42
63
  type: 'page';
@@ -90,6 +111,8 @@ export declare function finishProfileEditor(editor: ProfileEditorDraft): {
90
111
  };
91
112
  /** Make an isolated root draft; opening the screen never mutates live config. */
92
113
  export declare function createModelProfileState(draft: ModelSettingsDraft): ModelProfileState;
114
+ /** The kits whose three slots name `id`, for the delete confirmation (D-9). */
115
+ export declare function kitsUsingProfile(profiles: ModelProfilesConfig | undefined, id: string): AgentKit[];
93
116
  /** Describe references using text so ASCII and no-color modes remain usable. */
94
117
  export declare function profileUsers(profiles: ModelProfilesConfig | undefined, id: string, subagentProfileId?: string | null): string;
95
118
  /**
@@ -41,10 +41,38 @@ export function createModelProfileState(draft) {
41
41
  return { draft: copy, page: 'root', role: 'main', returnPage: 'manager',
42
42
  bindNew: false, dirty: false };
43
43
  }
44
+ /** The kits whose three slots name `id`, for the delete confirmation (D-9). */
45
+ export function kitsUsingProfile(profiles, id) {
46
+ return (profiles?.kits ?? []).filter((kit) => kit.mainProfileId === id
47
+ || kit.fastProfileId === id
48
+ || (kit.subagent.model === 'profile' && kit.subagent.modelProfileId === id));
49
+ }
50
+ /**
51
+ * Drop `slot` from the session kit (§4.3 row 2 / AC-38).
52
+ *
53
+ * THE DRAFT IS NARROWED HERE, NOT ONLY AT SAVE TIME. `releasedSlots` already
54
+ * strips the same slot from the transaction before it commits, but the ROOT PAGE
55
+ * paints the kit's effective value for every HELD slot - so without this line a
56
+ * user picks a profile and the row keeps showing the one they just replaced,
57
+ * which is the most confusing thing a settings screen can do. Releasing the last
58
+ * slot ends the kit: `held: []` is not a state any surface can render.
59
+ */
60
+ function releaseHeldSlot(draft, slot) {
61
+ const current = draft.sessionKit;
62
+ if (!current || !current.held.includes(slot))
63
+ return draft;
64
+ const held = current.held.filter((item) => item !== slot);
65
+ return { ...draft, sessionKit: held.length ? { id: current.id, held } : null };
66
+ }
44
67
  /** Describe references using text so ASCII and no-color modes remain usable. */
45
68
  export function profileUsers(profiles, id, subagentProfileId) {
69
+ // THE KITS ARE REFERENCES TOO (D-9 / RV-P2-6): "Main + kit "Deep work"" is what
70
+ // makes the confirmation honest, because deleting this entry also deletes that
71
+ // kit. The empty case must stay empty - a leading ` + ` is what a naive
72
+ // `join` after an unconditional push produces.
46
73
  return [profiles?.mainId === id ? 'Main' : '', profiles?.fastId === id ? 'Fast' : '',
47
- subagentProfileId === id ? 'Subagent' : '']
74
+ subagentProfileId === id ? 'Subagent' : '',
75
+ ...kitsUsingProfile(profiles, id).map((kit) => `kit "${kit.name}"`)]
48
76
  .filter(Boolean).join(' + ');
49
77
  }
50
78
  /**
@@ -155,14 +183,27 @@ export function modelProfileReducer(state, action) {
155
183
  ? { model: 'fast', modelProfileId: null }
156
184
  : { model: 'profile', modelProfileId: action.id };
157
185
  return { ...state, page: 'root', dirty: true, error: undefined,
158
- draft: { ...state.draft, patch: { ...state.draft.patch, team } } };
186
+ draft: releaseHeldSlot({ ...state.draft,
187
+ patch: { ...state.draft.patch, team } }, 'subagent') };
159
188
  }
189
+ // `'binding'` NEVER REACHES THIS REDUCER: a kit slot's choice is applied
190
+ // by `kitReducer` (`kit-main` / `kit-fast`), because the value belongs to
191
+ // an editor draft rather than to a `${role}Id` library key. Returning the
192
+ // state unchanged is the honest answer to an impossible action.
193
+ if (action.role === 'binding')
194
+ return state;
160
195
  return { ...state, page: 'root', dirty: true, error: undefined,
161
- draft: { ...state.draft, profiles: { ...library, [`${action.role}Id`]: action.id },
162
- activateRoles: [...new Set([...state.draft.activateRoles, action.role])] } };
196
+ draft: releaseHeldSlot({ ...state.draft,
197
+ profiles: { ...library, [`${action.role}Id`]: action.id },
198
+ activateRoles: [...new Set([...state.draft.activateRoles, action.role])] }, action.role) };
163
199
  }
164
200
  case 'edit': return { ...state, editor: createProfileEditor(action.profile), page: 'editor',
165
- returnPage: state.page === 'picker' ? 'picker' : 'manager',
201
+ // A PROFILE BORN IN A KIT SLOT RETURNS TO THAT SLOT (§4.5): the picker's
202
+ // `Create profile` action is the only way to invent one and it is offered on
203
+ // the kit's slot chooser - landing in the profile MANAGER there would
204
+ // abandon the half-built kit behind a page the user never asked for.
205
+ returnPage: state.page === 'picker' || state.page === 'kit-slot'
206
+ ? state.page : 'manager',
166
207
  bindNew: action.bindNew ?? false, error: undefined };
167
208
  case 'editor-field': return editField(state, action);
168
209
  case 'retain-key': return state.editor?.original.apiKey ? { ...state,
@@ -206,17 +247,29 @@ export function modelProfileReducer(state, action) {
206
247
  // when it named THIS entry. `removeModelProfile`'s throw stays as the
207
248
  // bottom-line defense - the UI has already unbound, so it cannot fire
208
249
  // on this path.
250
+ //
251
+ // AND THE KITS GO WITH IT (agent-model-kits D-9 / I-8): a kit naming a
252
+ // deleted profile can neither be applied nor repaired from any screen, so
253
+ // leaving it behind would be leaving a permanent error. The confirmation
254
+ // page has already named them, so this is the action the user agreed to.
255
+ const doomed = kitsUsingProfile(library, id);
256
+ const defaultKitKept = doomed.some((kit) => kit.id === (library.defaultKitId ?? null))
257
+ ? null : library.defaultKitId;
258
+ const sessionReleased = doomed.some((kit) => kit.id === state.draft.sessionKit?.id)
259
+ ? null : state.draft.sessionKit;
209
260
  const profiles = {
210
261
  ...library,
211
262
  mainId: library.mainId === id ? null : library.mainId,
212
263
  fastId: library.fastId === id ? null : library.fastId,
213
264
  entries: library.entries.filter((entry) => entry.id !== id),
265
+ kits: (library.kits ?? []).filter((kit) => !doomed.includes(kit)),
266
+ defaultKitId: defaultKitKept,
214
267
  };
215
268
  const teamReset = draftSubagentProfileId(state.draft) === id
216
269
  ? { team: { model: 'main', modelProfileId: null } }
217
270
  : {};
218
271
  return { ...state, page: 'manager', dirty: true,
219
- draft: { ...state.draft, profiles,
272
+ draft: { ...state.draft, profiles, sessionKit: sessionReleased,
220
273
  patch: { ...state.draft.patch, ...teamReset } },
221
274
  deleteId: undefined, deleteUsers: undefined,
222
275
  // No orphan editor for a deleted entry (3.7).
@@ -4,8 +4,8 @@
4
4
  *
5
5
  * WHY THIS FILE EXISTS. Native scroll mode cannot print an entry until it settles, so
6
6
  * the live region used to show `buildLivePreview`'s plain-text projection - and for a
7
- * running dispatch that projection is four grey rows saying `Team: 2/4 running`. That
8
- * is the "simple look" the feature was opened for. A text-only region can never fix
7
+ * running dispatch that projection was four grey rows saying `Team: 2/4 running`. It
8
+ * was the "simple look" the feature was opened for. A text-only region can never fix
9
9
  * it, which is why the region's content type changed from `readonly string[]` to a
10
10
  * fixed-height box that can hold components.
11
11
  *
@@ -3,7 +3,7 @@ import { Box } from 'ink';
3
3
  import { EntryView, nowSecFor } from '../Transcript.js';
4
4
  import { estimateEntryRows } from '../layout/virtual-window.js';
5
5
  import { SCROLLBACK_LIMITS } from './limits.js';
6
- import { isExcluded } from './settled-boundary.js';
6
+ import { isSlotless } from './tail-window.js';
7
7
  import { resolveTailAnchor } from './tail-anchor.js';
8
8
  /**
9
9
  * The last `maxEntries` printable entries, each with its predecessor.
@@ -13,7 +13,12 @@ import { resolveTailAnchor } from './tail-anchor.js';
13
13
  * separation row the transcript shows above the same card.
14
14
  */
15
15
  export function selectTailEntries(entries, maxEntries) {
16
- const printable = entries.filter((entry) => !isExcluded(entry));
16
+ // A LIVE DISPATCH CARD TAKES NO SLOT (tui-subagent-double-roster A-2): the roster
17
+ // draws it and `EntryView` returns null for it, so the window must not spend one of
18
+ // its slots on an entry that is not on screen. `isSlotless` is `tail-window.ts`'s
19
+ // predicate - the one `selectTailWindow` and the print pump apply - so the box and
20
+ // `planOverflowPrint` cannot drift into different windows.
21
+ const printable = entries.filter((entry) => !isSlotless(entry));
17
22
  const limit = Number.isFinite(maxEntries) ? Math.max(0, Math.floor(maxEntries)) : 0;
18
23
  if (limit === 0)
19
24
  return [];
@@ -20,6 +20,7 @@
20
20
  import { clampLiveText } from '../live-clamp.js';
21
21
  import { wrapToRows } from '../layout/wrap-rows.js';
22
22
  import { isExcluded } from './settled-boundary.js';
23
+ import { isLiveTeamCard, outcomePhrase, rosterCounts } from '../team-status.js';
23
24
  import { clipLine } from './writer-shapes.js';
24
25
  const MAX_TEAM_RUN_ROWS = 3;
25
26
  const TOOL_LIVE_ROWS = 6;
@@ -56,8 +57,10 @@ function toolRows(entry, cols) {
56
57
  return out;
57
58
  }
58
59
  function teamRows(entry, cols) {
59
- const running = entry.runs.filter((run) => !['queued', 'done', 'failed', 'aborted'].includes(run.phase)).length;
60
- const out = [fit(`Team: ${running}/${entry.runs.length} running`, cols)];
60
+ // ONLY A SETTLED CARD REACHES HERE (the live one projects nothing, below), so the
61
+ // outcome phrase - `team-status.ts`'s own sentence - replaces an always-zero fraction.
62
+ const counts = rosterCounts(entry.runs);
63
+ const out = [fit(`Team: ${outcomePhrase(counts)}`, cols)];
61
64
  for (const run of entry.runs.slice(0, MAX_TEAM_RUN_ROWS)) {
62
65
  out.push(fit(` ${run.label} ${run.phase}`, cols));
63
66
  }
@@ -71,7 +74,11 @@ function entryRows(entry, input) {
71
74
  case 'tool':
72
75
  return toolRows(entry, cols);
73
76
  case 'team':
74
- return teamRows(entry, cols);
77
+ // THE LIVE DISPATCH HAS ONE SURFACE (tui-subagent-double-roster A-2): this
78
+ // projection is the text fallback for the same slot `LiveTail` fills, so a
79
+ // `Team: n/m running` line here would be the second live block again, one level
80
+ // down. A settled card still projects normally.
81
+ return isLiveTeamCard(entry) ? [] : teamRows(entry, cols);
75
82
  case 'todo':
76
83
  return [fit(`Todo: ${entry.doneCount}/${entry.total}`, cols)];
77
84
  case 'compaction':
@@ -27,6 +27,18 @@
27
27
  * closes, and `/perf`'s `window=<start>/<n> head=<rows>` is how it stays observable.
28
28
  */
29
29
  import type { Entry } from '../../agent/reducer.js';
30
+ /**
31
+ * Entries that take NO slot in the window: they neither print nor take a row.
32
+ *
33
+ * A LIVE DISPATCH CARD JOINS THE NON-TERMINAL SERVICE CARD HERE
34
+ * (tui-subagent-double-roster A-2'): the roster draws the live dispatch and the
35
+ * transcript draws nothing, so the window must not spend one of its slots on an
36
+ * entry that is not on screen. THE PRINT PUMP AND THE BOX MUST AGREE ABOUT SLOTS:
37
+ * so both window functions below skip it, and `LiveTail.selectTailEntries` - the
38
+ * window function the box renders - imports this one instead of spelling the
39
+ * composition out a second time.
40
+ */
41
+ export declare function isSlotless(entry: Entry): boolean;
30
42
  export interface TailWindow {
31
43
  /** First index of the window inside the full entry list. */
32
44
  start: number;
@@ -27,6 +27,21 @@
27
27
  * closes, and `/perf`'s `window=<start>/<n> head=<rows>` is how it stays observable.
28
28
  */
29
29
  import { isExcluded, settledCount } from './settled-boundary.js';
30
+ import { isLiveTeamCard } from '../team-status.js';
31
+ /**
32
+ * Entries that take NO slot in the window: they neither print nor take a row.
33
+ *
34
+ * A LIVE DISPATCH CARD JOINS THE NON-TERMINAL SERVICE CARD HERE
35
+ * (tui-subagent-double-roster A-2'): the roster draws the live dispatch and the
36
+ * transcript draws nothing, so the window must not spend one of its slots on an
37
+ * entry that is not on screen. THE PRINT PUMP AND THE BOX MUST AGREE ABOUT SLOTS:
38
+ * so both window functions below skip it, and `LiveTail.selectTailEntries` - the
39
+ * window function the box renders - imports this one instead of spelling the
40
+ * composition out a second time.
41
+ */
42
+ export function isSlotless(entry) {
43
+ return isExcluded(entry) || isLiveTeamCard(entry);
44
+ }
30
45
  /**
31
46
  * The index of the first entry that is still on screen: count backwards up to
32
47
  * `maxEntries` PRINTABLE entries. Everything before it has left the region and is the
@@ -42,7 +57,7 @@ export function tailWindowStart(entries, maxEntries) {
42
57
  return entries.length;
43
58
  let seen = 0;
44
59
  for (let i = entries.length - 1; i >= 0; i -= 1) {
45
- if (isExcluded(entries[i]))
60
+ if (isSlotless(entries[i]))
46
61
  continue;
47
62
  seen += 1;
48
63
  if (seen === limit)
@@ -54,7 +69,7 @@ export function tailWindowStart(entries, maxEntries) {
54
69
  /** `entries.slice(start)` with the entries that never print dropped. */
55
70
  export function selectTailWindow(entries, maxEntries) {
56
71
  const start = tailWindowStart(entries, maxEntries);
57
- return { start, entries: entries.slice(start).filter((entry) => !isExcluded(entry)) };
72
+ return { start, entries: entries.slice(start).filter((entry) => !isSlotless(entry)) };
58
73
  }
59
74
  /**
60
75
  * The print boundary: never print what the region is still showing.
@@ -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,141 @@
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 marker padded to the tier's widest one, for the leading cell of a row.
81
+ *
82
+ * THE PADDING IS NOT COSMETIC (A-1'a): the ASCII tier spells `[ok]` (4), `[x]` (3)
83
+ * and `!` (1), so an unpadded marker cell starts every label at a different
84
+ * column and the roster stops reading as a table. `TodoPanel` pads its marker cell
85
+ * for the same reason. The Unicode tier's markers are all one cell wide, so the
86
+ * padding is a no-op there.
87
+ */
88
+ export declare function statusMarkerCell(status: ChildStatus, glyphs: Glyphs): string;
89
+ /**
90
+ * The colour of a status. One reading for the roster row, the card row and the
91
+ * card's rail marker.
92
+ *
93
+ * `aborted` deliberately does NOT share `toolError` with `failed`: an abort is the
94
+ * user's own Esc, and the analysis found that painting it red made a cancelled
95
+ * dispatch permanently unreadable in the history.
96
+ */
97
+ export declare function statusColor(status: ChildStatus, theme: Theme): string | undefined;
98
+ /**
99
+ * How long this child has been (or was) working, in ms.
100
+ *
101
+ * `endedAt ?? now`, NOT `endedAt ?? 0`. The transcript card used the latter, which
102
+ * made every RUNNING child read `0ms` while the roster beside it read `1m05s` for
103
+ * the same child - the pair of contradictory readouts the reporter screenshotted.
104
+ */
105
+ export declare function childElapsedMs(run: SubagentRun, now: number): number;
106
+ /**
107
+ * The roster header's count section, e.g. `2 running · 0 done · 1 queued`.
108
+ *
109
+ * `queued` IS COUNTED (the old `isRunning` excluded it, so a fresh dispatch
110
+ * announced `0 running · 0 done` above five rows that each said `queued`), and a
111
+ * non-zero bucket is always named even when nothing is running - a roster whose
112
+ * children are all `aborted` must say so rather than `0 running · 0 done`.
113
+ *
114
+ * WIDTH LADDER: narrow terminals drop the least urgent clause first, in the order
115
+ * `done`, `aborted`, `failed`, `queued`; `running` is never dropped. The order is
116
+ * a product decision, not a detail: while a dispatch is live, what is happening
117
+ * now outranks what has already finished, and the per-row activity column carries
118
+ * the finished children's own facts anyway.
119
+ */
120
+ export declare function rosterHeadline(counts: RosterCounts, glyphs: Glyphs, cols: number): string;
121
+ /**
122
+ * The settled card's outcome buckets, e.g. `2 ok, 1 aborted, 1 failed`.
123
+ *
124
+ * `ok` is always shown (it is the denominator a reader compares against) and a
125
+ * zero bucket is omitted, so a clean dispatch still reads `3 ok` and a failed one
126
+ * no longer claims that a cancelled child failed.
127
+ */
128
+ export declare function outcomePhrase(counts: RosterCounts): string;
129
+ /**
130
+ * Whether this entry is the transcript card of a dispatch that is STILL RUNNING,
131
+ * i.e. the second live surface the reporter saw beside the roster.
132
+ *
133
+ * ONE PREDICATE, FOUR CONSUMERS (analysis A-2'): the transcript's `case 'team'`
134
+ * and `estimateEntryRows` (so the virtual window and the frame agree), the tail
135
+ * window and `LiveTail` (so the pump's print plan and the box agree), and the
136
+ * `Ctrl+O` finder plus `reprintExpanded` (so a keypress cannot print a stale
137
+ * snapshot of a card that is still being rewritten). `state.entries` is NEVER
138
+ * filtered: the entry's lifecycle and `session/persist.ts`'s `interrupted`
139
+ * normalisation depend on it staying there.
140
+ */
141
+ export declare function isLiveTeamCard(entry: Entry): boolean;