@aragon-agent/cli 0.6.35 → 0.6.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/README.md +3173 -3071
  3. package/dist/agent/agent-mode.d.ts +20 -4
  4. package/dist/agent/agent-mode.js +20 -4
  5. package/dist/agent/controller.d.ts +38 -3
  6. package/dist/agent/controller.js +64 -4
  7. package/dist/agent/headless.js +31 -0
  8. package/dist/agent/reducer.d.ts +13 -2
  9. package/dist/agent/reducer.js +1 -0
  10. package/dist/cli.js +12 -0
  11. package/dist/commands/agents-command.d.ts +15 -0
  12. package/dist/commands/agents-command.js +27 -0
  13. package/dist/commands/builtins.js +131 -4
  14. package/dist/commands/perf.d.ts +11 -0
  15. package/dist/commands/perf.js +2 -1
  16. package/dist/commands/registry.d.ts +14 -0
  17. package/dist/config/app-paths.d.ts +15 -0
  18. package/dist/config/app-paths.js +33 -2
  19. package/dist/config/env.d.ts +4 -1
  20. package/dist/config/env.js +28 -2
  21. package/dist/config/load.d.ts +10 -0
  22. package/dist/config/load.js +8 -1
  23. package/dist/config/schema.d.ts +68 -0
  24. package/dist/config/schema.js +39 -0
  25. package/dist/config/store.js +12 -1
  26. package/dist/config/ui-state.d.ts +18 -0
  27. package/dist/config/ui-state.js +9 -0
  28. package/dist/exec/events.d.ts +25 -0
  29. package/dist/exec/events.js +4 -0
  30. package/dist/exec/index.js +16 -1
  31. package/dist/exec/runner.js +10 -0
  32. package/dist/proc/limits.d.ts +16 -0
  33. package/dist/proc/limits.js +16 -0
  34. package/dist/proc/supervisor.d.ts +20 -1
  35. package/dist/proc/supervisor.js +42 -14
  36. package/dist/prompts/command.d.ts +34 -0
  37. package/dist/prompts/command.js +237 -0
  38. package/dist/prompts/copy.d.ts +89 -0
  39. package/dist/prompts/copy.js +131 -0
  40. package/dist/prompts/fill.d.ts +51 -0
  41. package/dist/prompts/fill.js +46 -0
  42. package/dist/prompts/limits.d.ts +53 -0
  43. package/dist/prompts/limits.js +53 -0
  44. package/dist/prompts/normalize.d.ts +53 -0
  45. package/dist/prompts/normalize.js +190 -0
  46. package/dist/prompts/resolve.d.ts +31 -0
  47. package/dist/prompts/resolve.js +60 -0
  48. package/dist/prompts/search.d.ts +58 -0
  49. package/dist/prompts/search.js +146 -0
  50. package/dist/prompts/store.d.ts +86 -0
  51. package/dist/prompts/store.js +453 -0
  52. package/dist/prompts/types.d.ts +204 -0
  53. package/dist/prompts/types.js +13 -0
  54. package/dist/session/checkpoint.d.ts +8 -1
  55. package/dist/session/checkpoint.js +6 -0
  56. package/dist/session/persist.d.ts +19 -1
  57. package/dist/session/persist.js +13 -1
  58. package/dist/session/startup-session.d.ts +7 -2
  59. package/dist/session/startup-session.js +5 -2
  60. package/dist/session/store.js +3 -0
  61. package/dist/session/validate-session.js +64 -1
  62. package/dist/team/limits.d.ts +25 -0
  63. package/dist/team/limits.js +26 -0
  64. package/dist/team/runtime.js +27 -2
  65. package/dist/team/settled.d.ts +57 -0
  66. package/dist/team/settled.js +87 -0
  67. package/dist/team/subagent.js +48 -2
  68. package/dist/team/timeline.d.ts +74 -0
  69. package/dist/team/timeline.js +101 -0
  70. package/dist/team/types.d.ts +24 -1
  71. package/dist/todo/acceptance.d.ts +120 -0
  72. package/dist/todo/acceptance.js +399 -0
  73. package/dist/todo/follow-through.d.ts +48 -23
  74. package/dist/todo/follow-through.js +110 -44
  75. package/dist/todo/limits.d.ts +62 -1
  76. package/dist/todo/limits.js +62 -1
  77. package/dist/todo/panel-rows.d.ts +29 -39
  78. package/dist/todo/panel-rows.js +37 -83
  79. package/dist/todo/plan-sections.d.ts +60 -0
  80. package/dist/todo/plan-sections.js +108 -0
  81. package/dist/todo/prompt.d.ts +17 -12
  82. package/dist/todo/prompt.js +59 -40
  83. package/dist/todo/row-window.d.ts +51 -0
  84. package/dist/todo/row-window.js +107 -0
  85. package/dist/todo/store.d.ts +139 -31
  86. package/dist/todo/store.js +271 -73
  87. package/dist/todo/todo-tool.d.ts +33 -8
  88. package/dist/todo/todo-tool.js +186 -30
  89. package/dist/todo/types.d.ts +58 -0
  90. package/dist/ui/App.js +310 -46
  91. package/dist/ui/Composer.d.ts +6 -2
  92. package/dist/ui/Composer.js +2 -2
  93. package/dist/ui/Header.d.ts +9 -1
  94. package/dist/ui/Header.js +2 -2
  95. package/dist/ui/PromptInput.d.ts +21 -1
  96. package/dist/ui/PromptInput.js +21 -1
  97. package/dist/ui/ScrollbackShell.d.ts +19 -19
  98. package/dist/ui/ScrollbackShell.js +8 -1
  99. package/dist/ui/StatusBar.d.ts +5 -0
  100. package/dist/ui/StatusBar.js +1 -0
  101. package/dist/ui/TeamPanel.d.ts +26 -44
  102. package/dist/ui/TeamPanel.js +46 -135
  103. package/dist/ui/TodoPanel.d.ts +17 -14
  104. package/dist/ui/TodoPanel.js +146 -38
  105. package/dist/ui/Transcript.js +1 -1
  106. package/dist/ui/composer-input.d.ts +10 -0
  107. package/dist/ui/composer-input.js +57 -24
  108. package/dist/ui/entries/TodoCard.d.ts +26 -11
  109. package/dist/ui/entries/TodoCard.js +56 -15
  110. package/dist/ui/glyphs.d.ts +19 -0
  111. package/dist/ui/glyphs.js +4 -0
  112. package/dist/ui/layout/agents-board.d.ts +102 -0
  113. package/dist/ui/layout/agents-board.js +389 -0
  114. package/dist/ui/layout/cells.d.ts +61 -0
  115. package/dist/ui/layout/cells.js +156 -0
  116. package/dist/ui/layout/header-hints.d.ts +13 -3
  117. package/dist/ui/layout/header-hints.js +48 -14
  118. package/dist/ui/layout/status-detail-layout.js +9 -0
  119. package/dist/ui/layout/status-layout.d.ts +13 -0
  120. package/dist/ui/layout/team-row.d.ts +139 -0
  121. package/dist/ui/layout/team-row.js +246 -0
  122. package/dist/ui/layout/todo-layout.d.ts +38 -0
  123. package/dist/ui/layout/todo-layout.js +75 -0
  124. package/dist/ui/layout/virtual-window.js +23 -3
  125. package/dist/ui/overlays/AgentsBoardOverlay.d.ts +57 -0
  126. package/dist/ui/overlays/AgentsBoardOverlay.js +183 -0
  127. package/dist/ui/overlays/HelpOverlay.js +17 -2
  128. package/dist/ui/overlays/PromptEditorPage.d.ts +48 -0
  129. package/dist/ui/overlays/PromptEditorPage.js +227 -0
  130. package/dist/ui/overlays/PromptLibraryOverlay.d.ts +37 -0
  131. package/dist/ui/overlays/PromptLibraryOverlay.js +191 -0
  132. package/dist/ui/overlays/PromptPicker.d.ts +36 -0
  133. package/dist/ui/overlays/PromptPicker.js +129 -0
  134. package/dist/ui/overlays/PromptTextArea.d.ts +86 -0
  135. package/dist/ui/overlays/PromptTextArea.js +141 -0
  136. package/dist/ui/overlays/prompt-library-state.d.ts +162 -0
  137. package/dist/ui/overlays/prompt-library-state.js +335 -0
  138. package/dist/ui/overlays/prompt-picker-state.d.ts +71 -0
  139. package/dist/ui/overlays/prompt-picker-state.js +98 -0
  140. package/dist/ui/scrollback/band.js +14 -1
  141. package/dist/ui/team-status.d.ts +25 -0
  142. package/dist/ui/team-status.js +31 -3
  143. package/dist/ui/use-startup-notices.d.ts +35 -1
  144. package/dist/ui/use-startup-notices.js +29 -2
  145. package/dist/ui/use-wheel-routing.js +9 -1
  146. package/package.json +2 -2
@@ -102,8 +102,28 @@ interface PromptInputProps {
102
102
  * paste payloads with it.
103
103
  */
104
104
  draftClearNonce?: number;
105
+ /**
106
+ * App-owned ONE-SHOT fill (quick-prompt-library D-6 / §4.6).
107
+ *
108
+ * The payload-carrying sibling of `draftClearNonce`: `{ nonce, text, submit }`, with
109
+ * the same monotonic-nonce discipline, because a nonce is the only thing that makes
110
+ * an effect idempotent under StrictMode and re-renders (I-8). It is applied through
111
+ * `transact`, NOT by dispatching and calling `onSubmit` here: the composer has
112
+ * exactly one submission path and a fill must not become the second one (P0-1 / I-9).
113
+ */
114
+ draftFill?: DraftFill;
115
+ /** `prompts.insertMode`: what a fill does to a draft that already has text. */
116
+ insertMode?: 'insert' | 'replace';
105
117
  scrolledLines?: number;
106
118
  }
119
+ /** A one-shot "put this text in the draft" payload (quick-prompt-library §4.6). */
120
+ export interface DraftFill {
121
+ /** Monotonic; the same value is applied at most once. */
122
+ nonce: number;
123
+ text: string;
124
+ /** `true` = apply and submit in ONE transaction (the picker's `Ctrl+Enter`). */
125
+ submit?: boolean;
126
+ }
107
127
  export interface ScrollChip {
108
128
  text: string;
109
129
  /** FIXED width, so the number gaining a digit cannot re-wrap the draft (P2-4). */
@@ -168,5 +188,5 @@ export declare function fileTokenAt(buffer: string, cursor: number): {
168
188
  end: number;
169
189
  query: string;
170
190
  } | null;
171
- export declare function PromptInput({ cols: colsProp, terminalRows: terminalRowsProp, statusExpanded, cursorVisible, onInteraction, isActive, running, history, commands, cwd, theme, caps, popupMaxRows, popupMaxHeight, onPopupRowsChange, onCompletionContextChange, deleteDisambiguated, agentMode, onSubmit, onHelp, onEscape, onEscapeDismiss, reducedMotion, borderColor, onDraftChange, draftClearNonce, onNotice, }: PromptInputProps): React.ReactElement;
191
+ export declare function PromptInput({ cols: colsProp, terminalRows: terminalRowsProp, statusExpanded, cursorVisible, onInteraction, isActive, running, history, commands, cwd, theme, caps, popupMaxRows, popupMaxHeight, onPopupRowsChange, onCompletionContextChange, deleteDisambiguated, agentMode, onSubmit, onHelp, onEscape, onEscapeDismiss, reducedMotion, borderColor, onDraftChange, draftClearNonce, draftFill, insertMode, onNotice, }: PromptInputProps): React.ReactElement;
172
192
  export {};
@@ -269,7 +269,7 @@ function isControlSeq(input) {
269
269
  // ---------------------------------------------------------------------------
270
270
  // Component
271
271
  // ---------------------------------------------------------------------------
272
- export function PromptInput({ cols: colsProp, terminalRows: terminalRowsProp, statusExpanded = false, cursorVisible = true, onInteraction, isActive, running, history, commands, cwd, theme, caps, popupMaxRows, popupMaxHeight, onPopupRowsChange, onCompletionContextChange, deleteDisambiguated = false, agentMode, onSubmit, onHelp, onEscape, onEscapeDismiss, reducedMotion = false, borderColor, onDraftChange, draftClearNonce, onNotice, }) {
272
+ export function PromptInput({ cols: colsProp, terminalRows: terminalRowsProp, statusExpanded = false, cursorVisible = true, onInteraction, isActive, running, history, commands, cwd, theme, caps, popupMaxRows, popupMaxHeight, onPopupRowsChange, onCompletionContextChange, deleteDisambiguated = false, agentMode, onSubmit, onHelp, onEscape, onEscapeDismiss, reducedMotion = false, borderColor, onDraftChange, draftClearNonce, draftFill, insertMode = 'insert', onNotice, }) {
273
273
  const { stdout } = useStdout();
274
274
  const [editor, dispatch] = useReducer(editorReducer, INITIAL_EDITOR_STATE);
275
275
  const { buffer, cursor, historyIndex, dismissed, sel } = editor;
@@ -466,6 +466,26 @@ export function PromptInput({ cols: colsProp, terminalRows: terminalRowsProp, st
466
466
  onNotice?.('warn', plan.notice);
467
467
  }
468
468
  };
469
+ // --- App-requested fill (quick-prompt-library §4.6 / D-6). -----------------
470
+ //
471
+ // THE NONCE IS THE DEPENDENCY, NOT THE TEXT, for the reason the clear effect above
472
+ // records: two fills of the same prompt in a row must both apply, and keying on the
473
+ // payload would silently drop the second one. `transact` is deliberately absent from
474
+ // the dependency list - it closes over THIS render's `editor`, and depending on it
475
+ // would re-run (and re-fill) the effect on every keystroke.
476
+ const appliedFill = useRef(0);
477
+ useEffect(() => {
478
+ if (!draftFill || draftFill.nonce <= appliedFill.current)
479
+ return;
480
+ appliedFill.current = draftFill.nonce;
481
+ transact([{
482
+ kind: 'fill',
483
+ text: draftFill.text,
484
+ mode: insertMode,
485
+ submit: draftFill.submit === true,
486
+ }]);
487
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- see the note above
488
+ }, [draftFill, insertMode]);
469
489
  const recallUp = () => {
470
490
  if (history.length === 0)
471
491
  return;
@@ -18,22 +18,22 @@
18
18
  * transcript; without one the rail and the roster simply had nowhere to go, so the
19
19
  * default mode showed neither. The band is a row pool inside the live region:
20
20
  *
21
- * +---------------------------------------------+-----------------+
22
- * | tail window (height = band.previewRows) | TODO rail |
23
- * +---------------------------------------------+ (height = |
24
- * | team roster (height = band.teamRows) | band.railRows) |
25
- * +---------------------------------------------+-----------------+
21
+ * +----------------------------------------------+----------------+
22
+ * | tail window (height = band.previewRows) | TODO rail |
23
+ * +----------------------------------------------+ (height = |
24
+ * | team roster (width = contentCols, height = band.teamRows) |
25
+ * +----------------------------------------------------------------+
26
26
  *
27
- * THE ROSTER IS THE LEFT COLUMN'S BOTTOM SLOT, not its top one
28
- * (tui-subagent-double-roster A-3). `band.ts` allocates the same two heights either
29
- * way - only the drawing order changed - so every row number, and every test that
30
- * asserts one, is untouched. What moves is the reading: the roster is transient
31
- * status about the run in progress, so it belongs against the composer (where the
32
- * user's eyes already are, and where full-screen mode's viewport `footer` puts it),
33
- * and the conversation's own tail stays above it. As a bonus the rows a short tail
34
- * does not use now fall between the tail and the roster instead of between the tail
35
- * and the input box, which is the "blank space above the composer" shape the
36
- * plan-A-prime work removed one level up.
27
+ * THE ROSTER IS A FULL-WIDTH SLOT BELOW THE ROW REGION
28
+ * (tui-subagent-panel §4.1.2 / D-2). It used to be the band's LEFT COLUMN bottom
29
+ * slot, which capped its width at `previewCols`: with the TODO rail visible that is
30
+ * 84 columns on a 100-column terminal, so the row's right-aligned clock could never
31
+ * reach the screen's edge however carefully the row itself was laid out. It is now a
32
+ * SIBLING of the row region and spans `contentCols`, so the clock lands on the last
33
+ * column the terminal can draw. The READING is unchanged, and it is what
34
+ * `tui-subagent-double-roster` A-3 established: the roster is transient status about
35
+ * the run in progress, so it belongs against the composer, where the user's eyes
36
+ * already are and where full-screen mode's viewport `footer` puts it.
37
37
  *
38
38
  * Every box in it has an explicit `height` and `width` plus `overflow="hidden"`, and
39
39
  * that is a correctness requirement rather than tidiness: a box that grows with its
@@ -88,12 +88,12 @@ export interface ScrollbackShellProps {
88
88
  /** Real components for the tail window (`LiveTail`). */
89
89
  tail?: React.ReactNode;
90
90
  /**
91
- * The subagent roster: the BOTTOM slot of the band's left column, i.e. the row
92
- * closest to the composer. It is the only live subagent surface during a
93
- * dispatch - the transcript's `team` card is suppressed while it is active.
91
+ * The subagent roster: the band's FULL-WIDTH bottom slot, i.e. the rows closest to
92
+ * the composer. It is the only live subagent surface during a dispatch - the
93
+ * transcript's `team` card is suppressed while it is active.
94
94
  */
95
95
  team?: React.ReactNode;
96
- /** The TODO rail, drawn as the band's right-hand column. */
96
+ /** The TODO rail, drawn as the row region's right-hand column. */
97
97
  rail?: React.ReactNode;
98
98
  composer: React.ReactNode;
99
99
  status: React.ReactNode;
@@ -11,9 +11,16 @@ export function ScrollbackShell({ budget, header, overlay, liveTailLines, opener
11
11
  // fixed (`flex-start`); `tail` alone may fall back to `flex-end` when it overflows.
12
12
  const leftColumn = opener ?? tail;
13
13
  const tailLines = liveTailLines.slice(-budget.liveTailRows);
14
+ // I-1 (tui-subagent-panel §4.1.3): `rowRegion + band.teamRows <= band.bandRows`. In
15
+ // `fill` mode `previewRows === railRows === bandRows - teamRows`, so the two heights
16
+ // add back up to the band exactly; `band.ts` owns both numbers and this is their
17
+ // only consumer.
18
+ const rowRegion = Math.max(band.previewRows, band.railRows);
14
19
  // The overlay and the band are alternatives, not siblings: the band reports
15
20
  // `bandRows = remaining` for an overlay frame, so drawing both would double the
16
21
  // region's height and take the frame to the terminal height (the `ESC[3J` case).
17
- const top = band.reason === 'overlay' ? (_jsx(Box, { flexDirection: "column", height: budget.overlayRows, flexShrink: 0, overflow: "hidden", children: overlay })) : band.bandRows > 0 ? (_jsxs(Box, { flexDirection: "row", height: band.bandRows, flexShrink: 0, overflow: "hidden", children: [_jsxs(Box, { flexDirection: "column", width: budget.previewCols, flexShrink: 0, overflow: "hidden", children: [band.previewRows > 0 && (_jsx(Box, { flexDirection: "column", height: band.previewRows, flexShrink: 0, justifyContent: "flex-start", overflow: "hidden", children: leftColumn ?? tailLines.map((line, index) => (_jsx(Text, { wrap: "truncate", color: theme.muted, children: line }, index))) })), band.teamRows > 0 && (_jsx(Box, { height: band.teamRows, flexShrink: 0, overflow: "hidden", children: team }))] }), band.railRows > 0 && (_jsx(Box, { height: band.railRows, width: band.railCols, flexShrink: 0, overflow: "hidden", children: rail }))] })) : null;
22
+ const top = band.reason === 'overlay' ? (_jsx(Box, { flexDirection: "column", height: budget.overlayRows, flexShrink: 0, overflow: "hidden", children: overlay })) : band.bandRows > 0 ? (
23
+ // ROW REGION ABOVE, FULL-WIDTH ROSTER BELOW (tui-subagent-panel §4.1.2 / D-2).
24
+ _jsxs(Box, { flexDirection: "column", height: band.bandRows, flexShrink: 0, overflow: "hidden", children: [_jsxs(Box, { flexDirection: "row", height: rowRegion, flexShrink: 0, overflow: "hidden", children: [_jsx(Box, { flexDirection: "column", width: budget.previewCols, height: rowRegion, flexShrink: 0, overflow: "hidden", children: band.previewRows > 0 && (_jsx(Box, { flexDirection: "column", height: band.previewRows, flexShrink: 0, justifyContent: "flex-start", overflow: "hidden", children: leftColumn ?? tailLines.map((line, index) => (_jsx(Text, { wrap: "truncate", color: theme.muted, children: line }, index))) })) }), band.railRows > 0 && (_jsx(Box, { height: band.railRows, width: band.railCols, flexShrink: 0, overflow: "hidden", children: rail }))] }), band.teamRows > 0 && (_jsx(Box, { width: width, height: band.teamRows, flexShrink: 0, overflow: "hidden", children: team }))] })) : null;
18
25
  return (_jsxs(Box, { flexDirection: "column", height: budget.liveRows, width: width, overflow: "hidden", flexShrink: 0, children: [budget.headerRows > 0 && (_jsx(Box, { height: budget.headerRows, flexShrink: 0, overflow: "hidden", children: header })), top, _jsx(Box, { flexDirection: "column", height: budget.composerSlotRows, justifyContent: "flex-end", flexShrink: 0, overflow: "hidden", children: composer }), _jsx(Box, { height: budget.statusRows > 0 ? 1 : 0, flexShrink: 0, overflow: "hidden", children: status }), budget.statusRows === 2 && (_jsx(Box, { height: 1, flexShrink: 0, overflow: "hidden", children: details }))] }));
19
26
  }
@@ -73,6 +73,11 @@ export interface StatusBarProps {
73
73
  done: number;
74
74
  total: number;
75
75
  };
76
+ acceptanceActive?: {
77
+ checked: number;
78
+ total: number;
79
+ failed: number;
80
+ };
76
81
  servicesActive?: {
77
82
  live: number;
78
83
  };
@@ -41,6 +41,7 @@ export function StatusBar(props) {
41
41
  compacting: props.compactionActive?.inFlight, retryActive: props.retryActive,
42
42
  mode: props.agentMode, pendingMode: props.pendingAgentMode, servicesActive: props.servicesActive,
43
43
  teamActive: props.teamActive, todoActive: props.todoActive,
44
+ ...(props.acceptanceActive ? { acceptanceActive: props.acceptanceActive } : {}),
44
45
  ecoRung: props.ecoRung, fastActive: props.fastActive, scrolledLines: props.scrolledLines,
45
46
  });
46
47
  const tones = { normal: props.theme.primary, muted: props.theme.muted,
@@ -20,9 +20,18 @@
20
20
  * transcript's `team` card is suppressed while its dispatch is active
21
21
  * (`team-status.ts::isLiveTeamCard`), so this roster is what the user watches
22
22
  * during a dispatch and the card becomes the history afterwards. Native mode
23
- * draws it as the BOTTOM slot of the band's left column (`ScrollbackShell`),
24
- * i.e. directly above the composer; full-screen mode draws it in the viewport
25
- * footer, which is already above the composer.
23
+ * draws it as the band's BOTTOM, FULL-WIDTH slot (`ScrollbackShell`), i.e.
24
+ * directly above the composer and spanning the TODO rail's columns as well; the
25
+ * full-screen mode draws it in the viewport footer, which is already above the
26
+ * composer.
27
+ *
28
+ * THE ROW IS A TABLE, NOT A PARAGRAPH (tui-subagent-panel §0.1 / §4.2). The
29
+ * columns are placed by COMPUTED widths (`layout/team-row.ts`) inside fixed-width
30
+ * boxes, never by `flexGrow`: a fixed-height parent box makes Yoga size a column
31
+ * to its widest child, so the old row measured 48 columns inside a 100-column
32
+ * terminal and the clock stopped beside the description instead of at the right
33
+ * edge. The description is the only flexible cell, which is what keeps the two
34
+ * right-hand cells at their absolute offsets.
26
35
  *
27
36
  * EVERY PHASE -> GLYPH / COLOUR / COUNT DECISION COMES FROM `team-status.ts`.
28
37
  * The local copies this file used to carry are gone on purpose: the transcript
@@ -33,6 +42,20 @@ import type { Theme } from './theme.js';
33
42
  import type { TermCapabilities } from './capabilities.js';
34
43
  import { type TeamPanelLayout } from './layout/team-panel.js';
35
44
  import type { SubagentRun, TeamSnapshot } from '../team/types.js';
45
+ /**
46
+ * THE ACTIVITY / BADGE HELPERS LIVE IN `layout/team-row.ts` NOW (v2 implementation
47
+ * note, recorded in the spec's "implementation findings"). `teamRowCells` has to
48
+ * compose the activity text with the supervisor badge, so keeping those functions
49
+ * in this component would make the two modules import each other. They are
50
+ * re-exported here so every existing import site keeps its path.
51
+ */
52
+ export { activityBudget, activityLine, activityWithBadge, overseerBadge, teamClockText, teamRowCells, teamRowGeometry, } from './layout/team-row.js';
53
+ /**
54
+ * The panel's one supervisor status line (AC-5): the most recent intervention
55
+ * across the roster, human-phrased. Empty when the supervisor has not acted
56
+ * yet - furniture that says nothing is worse than no line.
57
+ */
58
+ export declare function supervisorStatusLine(runs: SubagentRun[], at: number): string;
36
59
  export interface TeamPanelProps {
37
60
  snapshot: TeamSnapshot;
38
61
  /** Terminal rows, for the one-line collapse below `panelCollapseRows`. */
@@ -50,45 +73,4 @@ export interface TeamPanelProps {
50
73
  now?: number;
51
74
  layout?: TeamPanelLayout;
52
75
  }
53
- export declare function activityBudget(cols: number): number;
54
- /**
55
- * The middle column: what this child is doing right now, in a few words.
56
- *
57
- * Two width policies, answering different questions. `wide` decides WHICH FACT
58
- * to show - a whole path or its basename, a whole command line or its program
59
- * name - mirroring `StatusBar`'s `statusCompactCols` degradation rather than
60
- * inventing a second threshold. `activityBudget(cols)` decides HOW MUCH OF IT
61
- * FITS, and it is a continuous function of width rather than a threshold,
62
- * because the failure it prevents - a row wider than the terminal - is
63
- * continuous.
64
- *
65
- * Exported for the same reason `buildSubagentTools` is: the width bound is the
66
- * property this round has to keep, and asserting it through a rendered frame
67
- * cannot distinguish "the arithmetic clamped it" from "Ink truncated it" - which
68
- * is precisely the confusion that let v1's fixed-width column look fine at 100
69
- * columns and break at 80.
70
- */
71
- export declare function activityLine(run: SubagentRun, cols: number): string;
72
- /**
73
- * The row's supervisor badge (subagent-overseer-v2 AC-5): what the supervisor
74
- * last did to THIS child and how long ago, e.g. `nudged 2m03s`.
75
- *
76
- * `lastIntervention.at` is ABSOLUTE epoch ms (R-P2-3), the same units as
77
- * `startedAt`, so this is one subtraction - no second clock convention.
78
- */
79
- export declare function overseerBadge(run: SubagentRun, at: number): string;
80
- /**
81
- * The activity column WITH the badge composed in (AC-5 / risk table): the
82
- * badge rides the SAME `activityBudget(cols)` allocation the prose uses, so
83
- * a row can never grow past its terminal; on a narrow terminal the badge
84
- * degrades to the action word alone - the who-acted fact outranks the
85
- * what-they-are-doing tail exactly when there is room for only one.
86
- */
87
- export declare function activityWithBadge(run: SubagentRun, cols: number, at: number): string;
88
- /**
89
- * The panel's one supervisor status line (AC-5): the most recent intervention
90
- * across the roster, human-phrased. Empty when the supervisor has not acted
91
- * yet - furniture that says nothing is worse than no line.
92
- */
93
- export declare function supervisorStatusLine(runs: SubagentRun[], at: number): string;
94
76
  export declare function TeamPanel({ snapshot, rows, cols, reducedMotion, theme, caps, now, layout, }: TeamPanelProps): React.ReactElement | null;
@@ -1,133 +1,20 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
3
  import Spinner from 'ink-spinner';
4
4
  import { pickGlyphs } from './glyphs.js';
5
- import { childElapsedMs, childStatus, isRunningStatus, rosterCounts, rosterHeadline, statusColor, statusMarkerCell, } from './team-status.js';
5
+ import { childStatus, isRunningStatus, rosterCounts, rosterHeadline, statusColor, } from './team-status.js';
6
6
  import { formatDuration } from '../agent/usage.js';
7
- import { describeToolActivity, sanitizeActivity } from '../team/activity.js';
8
- import { TEAM_LIMITS } from '../team/limits.js';
9
7
  import { buildTeamPanelLayout } from './layout/team-panel.js';
8
+ import { interventionWord, teamRowCells, teamRowGeometry, TEAM_CLOCK_CELLS, } from './layout/team-row.js';
9
+ import { fitCells } from './layout/cells.js';
10
10
  /**
11
- * How many characters the activity column may spend at this width (P0-1).
12
- *
13
- * `TEAM_LIMITS.activityChars` is a STORAGE ceiling; a terminal has a COLUMN
14
- * budget, and the two are not the same number. The row is
15
- *
16
- * marker(3) + label(8) + description(flex) + 2 + activity + 2 + elapsed(~6)
17
- *
18
- * and the comment on the description cell below promises that activity and
19
- * elapsed never lose characters, i.e. that `description` absorbs ALL width
20
- * pressure. A 64-character prose tail plus `writing: ` is 73, which on an
21
- * 80-column terminal needs 94 columns before `description` gets one. The promise
22
- * cannot be kept by wishing; the column has to be sized from `cols`.
23
- *
24
- * `WIDE_CHAR_ALLOWANCE` is the concession to there being no display-width helper
25
- * in production code: this budget is denominated in code units while the
26
- * terminal is denominated in columns, and CJK and emoji occupy two columns per
27
- * unit, so the budget is halved against the reserve rather than spent to the
28
- * last cell. Erring small costs a few characters of prose; erring large costs
29
- * the elapsed column.
11
+ * THE ACTIVITY / BADGE HELPERS LIVE IN `layout/team-row.ts` NOW (v2 implementation
12
+ * note, recorded in the spec's "implementation findings"). `teamRowCells` has to
13
+ * compose the activity text with the supervisor badge, so keeping those functions
14
+ * in this component would make the two modules import each other. They are
15
+ * re-exported here so every existing import site keeps its path.
30
16
  */
31
- const ROW_RESERVED_COLS = 3 + 8 + 2 + 2 + 8 + 12; // marker, label, gaps, elapsed, min description
32
- const WIDE_CHAR_ALLOWANCE = 2;
33
- export function activityBudget(cols) {
34
- const spare = Math.floor((cols - ROW_RESERVED_COLS) / WIDE_CHAR_ALLOWANCE);
35
- return Math.max(TEAM_LIMITS.activityMinChars, Math.min(TEAM_LIMITS.activityChars, spare));
36
- }
37
- /**
38
- * The middle column: what this child is doing right now, in a few words.
39
- *
40
- * Two width policies, answering different questions. `wide` decides WHICH FACT
41
- * to show - a whole path or its basename, a whole command line or its program
42
- * name - mirroring `StatusBar`'s `statusCompactCols` degradation rather than
43
- * inventing a second threshold. `activityBudget(cols)` decides HOW MUCH OF IT
44
- * FITS, and it is a continuous function of width rather than a threshold,
45
- * because the failure it prevents - a row wider than the terminal - is
46
- * continuous.
47
- *
48
- * Exported for the same reason `buildSubagentTools` is: the width bound is the
49
- * property this round has to keep, and asserting it through a rendered frame
50
- * cannot distinguish "the arithmetic clamped it" from "Ink truncated it" - which
51
- * is precisely the confusion that let v1's fixed-width column look fine at 100
52
- * columns and break at 80.
53
- */
54
- export function activityLine(run, cols) {
55
- const wide = cols >= TEAM_LIMITS.activityWideCols;
56
- const budget = activityBudget(cols);
57
- switch (run.phase) {
58
- case 'queued':
59
- return 'queued';
60
- case 'starting':
61
- // The one row that legitimately restarts its clock, saying why (P2-4):
62
- // F-2 ranks the running group by `startedAt` and a retry re-stamps it.
63
- return run.retries ? 'starting (retry)' : 'starting';
64
- case 'thinking':
65
- // Clamped AT RENDER, against this terminal's budget. `run.activity` was
66
- // clamped at STORE against `activityChars`; that is a ceiling, not a fit.
67
- return run.activity ? `writing: ${sanitizeActivity(run.activity, budget)}` : 'thinking';
68
- case 'tool':
69
- case 'waiting':
70
- return run.lastTool
71
- ? sanitizeActivity(describeToolActivity(run.lastTool, run.activityArgs, wide), budget)
72
- : run.phase === 'waiting'
73
- ? 'waiting for mail'
74
- : 'tool';
75
- case 'done':
76
- // `compacted N` ON THE SETTLED ROW ONLY (hardening §3.4.4 / W3). A running
77
- // row is already competing for `activityBudget(cols)` with the child's own
78
- // prose, and a marker that displaces what the child is DOING right now
79
- // buys nothing - the dispatch report carries the same fact for good.
80
- return run.compactions
81
- ? `done ${run.turns} turns compacted ${run.compactions}`
82
- : `done ${run.turns} turns`;
83
- case 'failed':
84
- return 'failed';
85
- case 'aborted':
86
- return 'aborted';
87
- }
88
- }
89
- /** The past-tense word the supervisor badge and status line render. */
90
- function interventionWord(action) {
91
- switch (action) {
92
- case 'nudge':
93
- return 'nudged';
94
- case 'replace':
95
- return 'replaced';
96
- case 'abandon':
97
- return 'abandoned';
98
- default:
99
- return 'waited';
100
- }
101
- }
102
- /**
103
- * The row's supervisor badge (subagent-overseer-v2 AC-5): what the supervisor
104
- * last did to THIS child and how long ago, e.g. `nudged 2m03s`.
105
- *
106
- * `lastIntervention.at` is ABSOLUTE epoch ms (R-P2-3), the same units as
107
- * `startedAt`, so this is one subtraction - no second clock convention.
108
- */
109
- export function overseerBadge(run, at) {
110
- const last = run.lastIntervention;
111
- if (last === undefined)
112
- return '';
113
- return `${interventionWord(last.action)} ${formatDuration(Math.max(0, at - last.at))}`;
114
- }
115
- /**
116
- * The activity column WITH the badge composed in (AC-5 / risk table): the
117
- * badge rides the SAME `activityBudget(cols)` allocation the prose uses, so
118
- * a row can never grow past its terminal; on a narrow terminal the badge
119
- * degrades to the action word alone - the who-acted fact outranks the
120
- * what-they-are-doing tail exactly when there is room for only one.
121
- */
122
- export function activityWithBadge(run, cols, at) {
123
- const last = run.lastIntervention;
124
- if (last === undefined)
125
- return activityLine(run, cols);
126
- const badge = overseerBadge(run, at);
127
- if (cols < TEAM_LIMITS.activityWideCols)
128
- return interventionWord(last.action);
129
- return sanitizeActivity(`${badge}; ${activityLine(run, cols)}`, activityBudget(cols));
130
- }
17
+ export { activityBudget, activityLine, activityWithBadge, overseerBadge, teamClockText, teamRowCells, teamRowGeometry, } from './layout/team-row.js';
131
18
  /**
132
19
  * The panel's one supervisor status line (AC-5): the most recent intervention
133
20
  * across the roster, human-phrased. Empty when the supervisor has not acted
@@ -149,6 +36,9 @@ export function supervisorStatusLine(runs, at) {
149
36
  const head = latest.reasonHead.length > 0 ? ` (${latest.reasonHead})` : '';
150
37
  return `supervisor: ${interventionWord(latest.action)} ${label} ${ago} ago${head}`;
151
38
  }
39
+ /** Columns the `ctrl+b` hint cell spends, and the width below which it is dropped. */
40
+ const HINT_CELLS = 'ctrl+b'.length;
41
+ const HINT_MIN_COLS = 100;
152
42
  export function TeamPanel({ snapshot, rows, cols, reducedMotion, theme, caps, now, layout, }) {
153
43
  const projection = layout ?? buildTeamPanelLayout({
154
44
  snapshot, terminalRows: rows, availableRows: Infinity,
@@ -162,18 +52,44 @@ export function TeamPanel({ snapshot, rows, cols, reducedMotion, theme, caps, no
162
52
  // said `queued`.
163
53
  const counts = rosterCounts(snapshot.runs);
164
54
  const elapsed = formatDuration(Math.max(0, at - snapshot.startedAt));
165
- const header = (_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", children: [_jsxs(Text, { wrap: "truncate", color: theme.accent, bold: true, children: [glyphs.teamAgent, " team", ' ', _jsxs(Text, { color: theme.muted, children: [rosterHeadline(counts, glyphs, cols), " ", glyphs.midDot, " ", elapsed] })] }), snapshot.messageCount > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [glyphs.teamMail, " ", snapshot.messageCount] }))] }));
55
+ const g = teamRowGeometry(cols, glyphs);
56
+ // WIDTH IS BUDGETED ONCE (I-13). `rosterHeadline` subtracts its own
57
+ // HEADER_RESERVED_COLS(16) - it takes the ROW's column count, not a budget - so
58
+ // the caller may only take away the cells this feature ADDS. Subtracting 16 here
59
+ // too would spend the reserve twice and drop `done` / `queued` from a 100-column
60
+ // header that has room for them.
61
+ const showHint = cols >= HINT_MIN_COLS && !projection.collapsed;
62
+ const headlineCols = Math.max(0, cols - TEAM_CLOCK_CELLS - (showHint ? HINT_CELLS : 0));
63
+ const mailText = `${glyphs.teamMail} ${snapshot.messageCount}`;
64
+ // The badge takes its NATURAL width and the flexible left cell gives way: it is
65
+ // the only header cell whose content length varies, and pinning it to a computed
66
+ // width would clip `@ 12` on a row that has room for it.
67
+ const mailCells = Math.min(mailText.length, Math.max(0, cols - TEAM_CLOCK_CELLS));
68
+ /**
69
+ * The header is the row's own geometry one level up: a flexible left cell and
70
+ * fixed right-hand cells, so the dispatch clock and every child clock stand in
71
+ * the SAME column (I-5 / AC-2).
72
+ *
73
+ * THE ELAPSED TIME APPEARS ONLY IN THE CLOCK CELL (§4.3 / RV-P2-9). The old
74
+ * header also carried it inline among the count clauses; with a right-edge clock
75
+ * that would be the same number twice, and the pair could disagree for one frame
76
+ * every 200 ms.
77
+ */
78
+ const header = (_jsxs(Box, { flexDirection: "row", width: "100%", height: 1, flexShrink: 0, overflow: "hidden", children: [_jsx(Box, { flexGrow: 1, flexShrink: 1, overflow: "hidden", children: _jsxs(Text, { wrap: "truncate", color: theme.accent, bold: true, children: [glyphs.teamAgent, ' team ', _jsx(Text, { color: theme.muted, children: rosterHeadline(counts, glyphs, headlineCols) })] }) }), showHint && (_jsx(Box, { width: HINT_CELLS, flexShrink: 0, justifyContent: "flex-end", children: _jsx(Text, { color: theme.hintFg, children: "ctrl+b" }) })), snapshot.messageCount > 0 && (_jsx(Box, { width: mailCells, flexShrink: 0, justifyContent: "flex-end", children: _jsx(Text, { wrap: "truncate", color: theme.muted, children: mailText }) })), _jsx(Box, { width: TEAM_CLOCK_CELLS, flexShrink: 0, justifyContent: "flex-end", children: _jsx(Text, { wrap: "truncate", color: theme.accent, bold: true, children: elapsed }) })] }));
166
79
  // Short terminals or a long draft leave only the shared header projection,
167
- // which still answers whether anything is running and for how long.
80
+ // which still answers whether anything is running and for how long. The header
81
+ // keeps the count clauses AND the right-edge clock, so the collapsed form loses
82
+ // no information - only the per-child rows.
168
83
  if (projection.collapsed) {
169
- return _jsx(Box, { flexDirection: "column", flexShrink: 0, children: header });
84
+ return _jsx(Box, { flexDirection: "column", width: "100%", flexShrink: 0, children: header });
170
85
  }
171
86
  // RANKED, not sliced by array index (F-2). The slot pool hands out indices in
172
87
  // order, so `slice(0, 5)` showed the five children that finished FIRST and hid
173
88
  // everything still working behind `+N more` - at exactly the fan-out widths
174
89
  // the requirement permits.
175
90
  const { visible, hiddenTotal, hiddenRunning } = projection;
176
- return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [header, visible.map((run) => {
91
+ const supervisorLine = supervisorStatusLine(snapshot.runs, at);
92
+ return (_jsxs(Box, { flexDirection: "column", width: "100%", flexShrink: 0, children: [header, visible.map((run) => {
177
93
  const status = childStatus(run);
178
94
  const color = statusColor(status, theme);
179
95
  // One spinner per RUNNING row, and the row cap is 5 precisely because
@@ -181,12 +97,7 @@ export function TeamPanel({ snapshot, rows, cols, reducedMotion, theme, caps, no
181
97
  // `reducedMotion` and a non-Unicode terminal fall back to the same
182
98
  // static marker, exactly as `AssistantEntry` and `ToolCard` already do.
183
99
  const animate = isRunningStatus(status) && !reducedMotion && caps.unicode;
184
- return (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: color, children: [' ', animate ? _jsx(Spinner, { type: "dots" }) : statusMarkerCell(status, glyphs), ' '] }), _jsx(Text, { color: theme.primary, children: run.label.padEnd(8).slice(0, 8) }), _jsx(Box, { flexGrow: 1, flexShrink: 1, overflow: "hidden", children: _jsx(Text, { wrap: "truncate", color: theme.muted, children: run.description.replace(/\s+/g, ' ') }) }), _jsx(Box, { flexShrink: 1, overflow: "hidden", children: _jsxs(Text, { wrap: "truncate", color: color, children: [' ', activityWithBadge(run, cols, at)] }) }), _jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', run.startedAt ? formatDuration(childElapsedMs(run, at)) : ''] })] }, run.label));
185
- }), hiddenTotal > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', "+", hiddenTotal, " more", hiddenRunning > 0 ? ` (${hiddenRunning} running)` : ''] })), (() => {
186
- // The panel's supervisor line (AC-5): one row, only when the
187
- // supervisor has actually acted - the per-row badges above already
188
- // carry the per-child facts.
189
- const line = supervisorStatusLine(snapshot.runs, at);
190
- return line ? (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', line] })) : null;
191
- })(), snapshot.lastMessage && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', glyphs.teamMail, " ", snapshot.lastMessage.from, " ", glyphs.arrowRight, " ", snapshot.lastMessage.to, ": ", snapshot.lastMessage.subject.replace(/\s+/g, ' ')] }))] }));
100
+ const cells = teamRowCells(run, cols, at, glyphs);
101
+ return (_jsxs(Box, { flexDirection: "row", width: "100%", height: 1, flexShrink: 0, overflow: "hidden", children: [_jsx(Box, { width: g.marker, flexShrink: 0, children: _jsxs(Text, { wrap: "truncate", color: color, children: [' ', animate ? _jsx(Spinner, { type: "dots" }) : cells.marker, ' '] }) }), _jsx(Box, { width: g.label, flexShrink: 0, children: _jsx(Text, { wrap: "truncate", color: theme.primary, children: cells.label }) }), _jsx(Box, { flexGrow: 1, flexShrink: 1, overflow: "hidden", children: _jsx(Text, { wrap: "truncate", color: theme.muted, children: cells.description }) }), _jsx(Box, { width: g.gap, flexShrink: 0, children: _jsx(Text, { children: " " }) }), _jsx(Box, { width: g.activity, flexShrink: 0, overflow: "hidden", children: _jsx(Text, { wrap: "truncate", color: color, children: cells.activity }) }), _jsx(Box, { width: g.clock, flexShrink: 0, justifyContent: "flex-end", children: _jsx(Text, { wrap: "truncate", color: theme.muted, children: cells.clock }) })] }, run.label));
102
+ }), hiddenTotal > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', "+", hiddenTotal, " more", hiddenRunning > 0 ? ` (${hiddenRunning} running)` : ''] })), supervisorLine.length > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', fitCells(supervisorLine, Math.max(0, cols - 2), glyphs.ellipsis)] })), snapshot.lastMessage && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [' ', glyphs.teamMail, " ", snapshot.lastMessage.from, " ", glyphs.arrowRight, " ", snapshot.lastMessage.to, ": ", snapshot.lastMessage.subject.replace(/\s+/g, ' ')] }))] }));
192
103
  }
@@ -1,5 +1,6 @@
1
1
  /**
2
- * TodoPanel — the right rail (todo-plan-execution §6.1).
2
+ * TodoPanel — the right rail (todo-plan-execution §6.1; two sections per
3
+ * todo-acceptance-criteria §6.9).
3
4
  *
4
5
  * ASCII ONLY: every glyph comes from `pickGlyphs`; a literal here bypasses the
5
6
  * capability probe and shows mojibake on a legacy console (`glyphs.ts` header).
@@ -8,22 +9,24 @@
8
9
  * this component is not rendered at all, so a session that never plans has zero
9
10
  * columns and zero layout shift (R-g's "no furniture when idle" / AC-24).
10
11
  *
12
+ * NO NEW PROP CARRIES THE ACCEPTANCE TABLE (§6.9.1): `snapshot.acceptance` is
13
+ * already inside `snapshot`, and `null` IS the projection of "the capability is
14
+ * off", because a disabled store commits an empty table. Keeping the props surface
15
+ * narrow is what keeps `App`'s two mount points untouched.
16
+ *
11
17
  * IT MUST BE MOUNTED IN `AppShell`'s MIDDLE BAND, NEVER IN THE BOTTOM CHROME
12
18
  * (C-6 / I-4). This is a COLUMN BESIDE the viewport, and the middle band is the
13
- * only row-wrapped region there is; moved down into the bottom chrome it would
14
- * stop being a rail and start eating viewport height, growing and shrinking the
19
+ * only row-wrapped region there is; moved down into the bottom chrome it would stop
20
+ * being a rail and start eating viewport height, growing and shrinking the
15
21
  * transcript every time the list changed length.
16
22
  *
17
23
  * WIDTH IS MEASURED IN COLUMNS, NEVER IN CODE UNITS (P2-3). This rail is 14-36
18
24
  * columns wide and the system prompt's last line is "Respond in the language the
19
- * user writes in", so CJK item text in an 18-column rail is the NORMAL case
20
- * rather than the edge case. Every text cell is `wrap="truncate"` and Ink's
21
- * truncation is display-width aware, so nothing can overflow the box — and NO
22
- * ARITHMETIC HERE MAY BUDGET USER TEXT BY `.length`. The count string and
23
- * `padStart(2)` cells below are ASCII counts and indices, which is why they are
24
- * safe; a "characters that fit" calculation over `content` would not be. That is
25
- * the whole of the lesson `TeamPanel`'s `WIDE_CHAR_ALLOWANCE` records at length,
26
- * and the mitigation here is to have no such budget at all.
25
+ * user writes in", so CJK text in an 18-column rail is the NORMAL case rather than
26
+ * the edge case. Every text cell is `wrap="truncate"` and Ink's truncation is
27
+ * display-width aware, so nothing can overflow the box - and NO ARITHMETIC HERE MAY
28
+ * BUDGET USER TEXT BY `.length`. The count strings and `padStart(2)` cells below are
29
+ * ASCII counts and indices, which is why they are safe.
27
30
  */
28
31
  import React from 'react';
29
32
  import type { Theme } from './theme.js';
@@ -35,9 +38,9 @@ export interface TodoPanelProps {
35
38
  width: number;
36
39
  /**
37
40
  * From `buildTodoRailLayout` after actual team/menu rows are subtracted.
38
- * (I-10 / P1-3). The gate that mounts this component and the number it lays
39
- * out against are ONE value computed once in `App`: a panel that mounts on one
40
- * number and lays out against another renders nothing and reports nothing.
41
+ * (I-10 / P1-3). The gate that mounts this component and the number it lays out
42
+ * against are ONE value computed once in `App`: a panel that mounts on one number
43
+ * and lays out against another renders nothing and reports nothing.
41
44
  */
42
45
  rows: number;
43
46
  /** Whether the agent is running; gates the spinner (never animate when idle). */