@aragon-agent/cli 0.5.1 → 0.5.3

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 (106) hide show
  1. package/CHANGELOG.md +117 -0
  2. package/README.md +155 -5
  3. package/dist/agent/controller.d.ts +51 -1
  4. package/dist/agent/controller.js +177 -2
  5. package/dist/agent/headless.d.ts +12 -0
  6. package/dist/agent/headless.js +41 -1
  7. package/dist/agent/reducer.d.ts +48 -0
  8. package/dist/agent/reducer.js +91 -2
  9. package/dist/agent/system-prompt.d.ts +17 -0
  10. package/dist/agent/system-prompt.js +4 -0
  11. package/dist/cli.js +131 -6
  12. package/dist/commands/builtins.js +82 -0
  13. package/dist/config/app-paths.d.ts +17 -0
  14. package/dist/config/app-paths.js +21 -0
  15. package/dist/config/cli-commands.d.ts +26 -2
  16. package/dist/config/cli-commands.js +63 -4
  17. package/dist/config/env.d.ts +2 -2
  18. package/dist/config/env.js +36 -2
  19. package/dist/config/history-commands.d.ts +20 -0
  20. package/dist/config/history-commands.js +71 -0
  21. package/dist/config/load.d.ts +15 -0
  22. package/dist/config/load.js +88 -5
  23. package/dist/config/migrate-state-out-of-config.d.ts +32 -0
  24. package/dist/config/migrate-state-out-of-config.js +85 -0
  25. package/dist/config/prompt-history.d.ts +86 -0
  26. package/dist/config/prompt-history.js +332 -0
  27. package/dist/config/schema.d.ts +125 -9
  28. package/dist/config/schema.js +90 -5
  29. package/dist/config/store.js +13 -4
  30. package/dist/config/ui-state.d.ts +52 -0
  31. package/dist/config/ui-state.js +149 -0
  32. package/dist/input/mouse-events.d.ts +61 -0
  33. package/dist/input/mouse-events.js +153 -0
  34. package/dist/input/stdin-mouse-filter.d.ts +42 -0
  35. package/dist/input/stdin-mouse-filter.js +153 -0
  36. package/dist/logging/install.d.ts +85 -0
  37. package/dist/logging/install.js +94 -0
  38. package/dist/logging/logger.d.ts +1 -1
  39. package/dist/session/persist.d.ts +16 -0
  40. package/dist/session/persist.js +19 -1
  41. package/dist/team/activity.d.ts +67 -0
  42. package/dist/team/activity.js +130 -0
  43. package/dist/team/bus.d.ts +190 -0
  44. package/dist/team/bus.js +366 -0
  45. package/dist/team/comm-tools.d.ts +61 -0
  46. package/dist/team/comm-tools.js +221 -0
  47. package/dist/team/human-queue.d.ts +55 -0
  48. package/dist/team/human-queue.js +72 -0
  49. package/dist/team/limits.d.ts +123 -0
  50. package/dist/team/limits.js +128 -0
  51. package/dist/team/normalize.d.ts +39 -0
  52. package/dist/team/normalize.js +106 -0
  53. package/dist/team/panel-rows.d.ts +48 -0
  54. package/dist/team/panel-rows.js +75 -0
  55. package/dist/team/prompt.d.ts +49 -0
  56. package/dist/team/prompt.js +87 -0
  57. package/dist/team/report.d.ts +58 -0
  58. package/dist/team/report.js +205 -0
  59. package/dist/team/retry.d.ts +48 -0
  60. package/dist/team/retry.js +56 -0
  61. package/dist/team/runtime.d.ts +122 -0
  62. package/dist/team/runtime.js +406 -0
  63. package/dist/team/subagent.d.ts +90 -0
  64. package/dist/team/subagent.js +294 -0
  65. package/dist/team/task-tool.d.ts +47 -0
  66. package/dist/team/task-tool.js +118 -0
  67. package/dist/team/types.d.ts +172 -0
  68. package/dist/team/types.js +13 -0
  69. package/dist/tools/index.d.ts +48 -2
  70. package/dist/tools/index.js +26 -1
  71. package/dist/ui/App.d.ts +9 -1
  72. package/dist/ui/App.js +140 -21
  73. package/dist/ui/Composer.d.ts +6 -1
  74. package/dist/ui/Composer.js +2 -2
  75. package/dist/ui/PromptInput.d.ts +15 -1
  76. package/dist/ui/PromptInput.js +38 -1
  77. package/dist/ui/StatusBar.d.ts +21 -0
  78. package/dist/ui/StatusBar.js +5 -2
  79. package/dist/ui/TeamPanel.d.ts +57 -0
  80. package/dist/ui/TeamPanel.js +134 -0
  81. package/dist/ui/Transcript.js +10 -0
  82. package/dist/ui/entries/TeamCard.d.ts +31 -0
  83. package/dist/ui/entries/TeamCard.js +39 -0
  84. package/dist/ui/glyphs.d.ts +22 -0
  85. package/dist/ui/glyphs.js +10 -0
  86. package/dist/ui/layout/AppShell.d.ts +20 -1
  87. package/dist/ui/layout/AppShell.js +39 -4
  88. package/dist/ui/layout/ScrollIndicator.d.ts +31 -0
  89. package/dist/ui/layout/ScrollIndicator.js +15 -0
  90. package/dist/ui/layout/ScrollViewport.d.ts +8 -2
  91. package/dist/ui/layout/ScrollViewport.js +24 -4
  92. package/dist/ui/layout/regions.d.ts +55 -0
  93. package/dist/ui/layout/regions.js +52 -0
  94. package/dist/ui/layout/scroll-indicator.d.ts +27 -0
  95. package/dist/ui/layout/scroll-indicator.js +35 -0
  96. package/dist/ui/layout/scroll.d.ts +9 -0
  97. package/dist/ui/layout/scroll.js +23 -0
  98. package/dist/ui/overlays/HelpOverlay.js +14 -0
  99. package/dist/ui/screen.d.ts +14 -1
  100. package/dist/ui/screen.js +37 -3
  101. package/dist/ui/transcript-text.js +20 -0
  102. package/dist/ui/use-startup-notices.d.ts +11 -1
  103. package/dist/ui/use-startup-notices.js +29 -2
  104. package/dist/ui/use-wheel-routing.d.ts +68 -0
  105. package/dist/ui/use-wheel-routing.js +139 -0
  106. package/package.json +2 -2
@@ -13,6 +13,19 @@ import type { AgentMode } from '../agent/agent-mode.js';
13
13
  export interface ConfirmRequest {
14
14
  tool: string;
15
15
  summary: string;
16
+ /**
17
+ * The tool call's own abort signal, when the executor supplied one.
18
+ *
19
+ * IT IS HERE BECAUSE `withConfirmation` IS THE ONLY PLACE THAT HAS IT. A
20
+ * confirm implementation that has to abandon a pending dialog — the subagent
21
+ * queue in `team/human-queue.ts` is the one that does (team-subagents §3.11 /
22
+ * AC-16) — cannot reach `ctx` any other way, and without it an `Esc` during a
23
+ * dispatch pops a fresh dialog for every request still sitting in the FIFO.
24
+ *
25
+ * Ignored by the interactive and headless confirms, which are resolved by
26
+ * their own channel when a run ends.
27
+ */
28
+ signal?: AbortSignal;
16
29
  }
17
30
  export interface BuiltinToolsOptions {
18
31
  getCwd: () => string;
@@ -72,6 +85,39 @@ export interface BuiltinToolsOptions {
72
85
  * burns a turn.
73
86
  */
74
87
  planTools?: AgentTool[];
88
+ /**
89
+ * Team-mode tools, appended verbatim. The lead gets `[task]`; a subagent gets
90
+ * `[team_send, team_wait]`.
91
+ *
92
+ * THIS OPTION EXISTS BECAUSE A TEST FORBIDS THE ALTERNATIVE (D-15 / P0-1).
93
+ * `tools.test.ts::C7` asserts that `HOST_TOOL_NAMES` equals WHAT THIS FACTORY
94
+ * ACTUALLY PRODUCES, so the moment `task` joined `HOST_TOOL_NAMES` (I-7),
95
+ * appending it in `AgentController` after the factory returned would turn C7
96
+ * red — with a failure message about two lists of names that says nothing
97
+ * about team mode.
98
+ *
99
+ * ONE option serves both sides on purpose: the only two call sites are the
100
+ * lead's controller and `createSubagent`, and a second option would just be a
101
+ * second name for "extra tools this caller supplies".
102
+ *
103
+ * An empty array (the `--no-team` path) leaves the tool list byte-identical to
104
+ * the pre-team build — AC-11, provable by object identity exactly as
105
+ * `--no-skills` is.
106
+ */
107
+ teamTools?: AgentTool[];
108
+ /**
109
+ * Tool names that skip the CEILING wrapper only (never the plan gate, never
110
+ * confirmation).
111
+ *
112
+ * It exists for a subagent's two comm tools and for nothing else (§3.11).
113
+ * `team_send` / `team_wait` are not in the LEAD's registered tool list, so
114
+ * evaluating them against the lead's `allowed-tools` ceiling would refuse them
115
+ * and dead-end the channel — the same dead end `SKILL_TOOL_FLOOR` exists to
116
+ * prevent one level up. Widening this set to anything that can write to disk
117
+ * would turn delegation into a ceiling bypass, which is exactly what §3.11
118
+ * re-applies the lead's policy to children to avoid.
119
+ */
120
+ policyExempt?: ReadonlySet<string>;
75
121
  }
76
122
  /**
77
123
  * Tools the ceiling always permits (D-G6). The ONLY place that knows which of
@@ -88,7 +134,7 @@ export interface BuiltinToolsOptions {
88
134
  * is the whole of what this mechanism is worth, and saying so plainly beats
89
135
  * implying it covers more.
90
136
  */
91
- export declare const SKILL_TOOL_FLOOR: readonly ["read_file", "list_dir", "glob", "grep", "skill", "skill_find", "ask_user", "submit_plan"];
137
+ export declare const SKILL_TOOL_FLOOR: readonly ["read_file", "list_dir", "glob", "grep", "skill", "skill_find", "ask_user", "submit_plan", "task"];
92
138
  /**
93
139
  * The five tools plan mode refuses: exactly the complement of `SKILL_TOOL_FLOOR`
94
140
  * within `HOST_TOOL_NAMES` — "the five that change something", the set this file
@@ -121,5 +167,5 @@ export declare const PLAN_MODE_BLOCKED_TOOLS: ReadonlySet<string>;
121
167
  * declaration MEANS, and that reading must not change with how the CLI was
122
168
  * started.
123
169
  */
124
- export declare const HOST_TOOL_NAMES: readonly ["read_file", "write_file", "edit_file", "list_dir", "glob", "grep", "bash", "skill", "skill_find", "skill_install", "skill_create", "ask_user", "submit_plan"];
170
+ export declare const HOST_TOOL_NAMES: readonly ["read_file", "write_file", "edit_file", "list_dir", "glob", "grep", "bash", "skill", "skill_find", "skill_install", "skill_create", "ask_user", "submit_plan", "task"];
125
171
  export declare function createBuiltinTools(options: BuiltinToolsOptions): AgentTool[];
@@ -46,6 +46,12 @@ export const SKILL_TOOL_FLOOR = [
46
46
  // "read-only".)
47
47
  'ask_user',
48
48
  'submit_plan',
49
+ // `task` joins the floor rather than the blocked set (D-5), and the choice is
50
+ // forced to be conscious by the partition test below. It cannot itself mutate
51
+ // anything: every child mutation passes through this same wrapper stack one
52
+ // level down, with the child inheriting `agentMode` through the same closure.
53
+ // Blocking it would forfeit parallel research, which is plan mode's best use.
54
+ 'task',
49
55
  ];
50
56
  /**
51
57
  * The five tools plan mode refuses: exactly the complement of `SKILL_TOOL_FLOOR`
@@ -99,6 +105,13 @@ export const HOST_TOOL_NAMES = [
99
105
  'skill_create',
100
106
  'ask_user',
101
107
  'submit_plan',
108
+ // `team_send` / `team_wait` are DELIBERATELY ABSENT (P2-5). This constant
109
+ // answers "can a skill's `allowed-tools` declaration ever take effect?", and
110
+ // those two are never registered on a lead — so a skill naming them genuinely
111
+ // IS unenforceable, and `aragon skills doctor` saying so is correct rather
112
+ // than a gap. They reach children through `policyExempt`, a different
113
+ // mechanism answering a different question.
114
+ 'task',
102
115
  ];
103
116
  function summarize(toolName, params) {
104
117
  if (toolName === 'bash')
@@ -118,6 +131,11 @@ function withConfirmation(tool, confirm) {
118
131
  const approved = await confirm({
119
132
  tool: tool.name,
120
133
  summary: summarize(tool.name, params),
134
+ // Forwarded, not consumed, here: this wrapper still awaits the answer,
135
+ // because a confirm that resolved itself on abort would let the tool run
136
+ // unapproved. It is the confirm IMPLEMENTATION that decides what an
137
+ // abort means (see `ConfirmRequest.signal`).
138
+ ...(ctx.signal ? { signal: ctx.signal } : {}),
121
139
  });
122
140
  if (!approved)
123
141
  return errorResult('Cancelled by user.');
@@ -209,6 +227,12 @@ export function createBuiltinTools(options) {
209
227
  if (options.planTools && options.planTools.length > 0) {
210
228
  tools = [...tools, ...options.planTools];
211
229
  }
230
+ // BEFORE both wrappers, so the lead's `task` is covered by the plan gate and
231
+ // the ceiling like everything else, and a child's comm tools are covered by
232
+ // everything except the ceiling (see `policyExempt`).
233
+ if (options.teamTools && options.teamTools.length > 0) {
234
+ tools = [...tools, ...options.teamTools];
235
+ }
212
236
  // No decision provider (`--no-skills`, or a caller that never enabled the
213
237
  // policy) means the ceiling adds nothing.
214
238
  //
@@ -223,7 +247,8 @@ export function createBuiltinTools(options) {
223
247
  // available ordering. The ceiling needs no I/O, so it goes before confirmation.
224
248
  if (options.toolPolicy) {
225
249
  const getDecision = options.toolPolicy;
226
- tools = tools.map((t) => withToolPolicy(t, getDecision, options.onToolPolicyEvent));
250
+ const exempt = options.policyExempt;
251
+ tools = tools.map((t) => exempt?.has(t.name) ? t : withToolPolicy(t, getDecision, options.onToolPolicyEvent));
227
252
  }
228
253
  // TWO OPTIONS, TWO INDEPENDENT GUARDS, NO SHARED EXIT.
229
254
  //
package/dist/ui/App.d.ts CHANGED
@@ -9,6 +9,8 @@ import type { AgentController } from '../agent/controller.js';
9
9
  import type { ConfirmRequest } from '../tools/index.js';
10
10
  import { type Overlay } from '../agent/reducer.js';
11
11
  import { type RenderMode } from './layout/frame.js';
12
+ import { type FrameGeometry } from './layout/regions.js';
13
+ import type { MouseSource } from '../input/stdin-mouse-filter.js';
12
14
  import type { HumanInputBridge } from '../tools/human-input.js';
13
15
  export interface ConfirmBridge {
14
16
  handler: ((req: ConfirmRequest) => Promise<boolean>) | null;
@@ -23,5 +25,11 @@ export interface AppProps {
23
25
  confirmBridge?: ConfirmBridge;
24
26
  /** The plan-mode human channel; absent in tests that do not exercise it. */
25
27
  humanInputBridge?: HumanInputBridge;
28
+ /** Wheel events, already stripped out of stdin. Absent whenever mouse support
29
+ * is off: inline mode, `--no-mouse`, a non-TTY, or a filter that failed to
30
+ * build (mouse-wheel-region-routing §4.2). */
31
+ mouseSource?: MouseSource;
32
+ /** Test-only seam (§8.2): frame geometry, bypassing `measureElement`. */
33
+ geometryOverride?: FrameGeometry;
26
34
  }
27
- export declare function App({ controller, version, mode, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, }: AppProps): React.ReactElement;
35
+ export declare function App({ controller, version, mode, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, geometryOverride, }: AppProps): React.ReactElement;
package/dist/ui/App.js CHANGED
@@ -10,8 +10,11 @@ import process from 'node:process';
10
10
  import { Box, Text, useApp, useInput, useStdout } from 'ink';
11
11
  import { initialViewState, reduceEvent, viewReducer, } from '../agent/reducer.js';
12
12
  import { mergeDeltas } from '../agent/coalesce.js';
13
- import { coerceMaxTokens, DEFAULT_TRANSCRIPT_WINDOW, PROMPT_HISTORY_CAP, } from '../config/schema.js';
13
+ import { computeCost } from '../agent/usage.js';
14
+ import { coerceMaxTokens, DEFAULT_TRANSCRIPT_WINDOW, } from '../config/schema.js';
14
15
  import { updatePersistedConfig } from '../config/store.js';
16
+ import { appendPrompt, loadPromptHistory } from '../config/prompt-history.js';
17
+ import { bumpSubmitCount, setMouseNoticeSeen } from '../config/ui-state.js';
15
18
  import { getLogger } from '../logging/logger.js';
16
19
  import { registerSecret } from '../logging/secret-registry.js';
17
20
  import { detectCapabilities } from './capabilities.js';
@@ -25,12 +28,15 @@ import { Transcript, TranscriptList } from './Transcript.js';
25
28
  import { PromptInput } from './PromptInput.js';
26
29
  import { Composer } from './Composer.js';
27
30
  import { StatusBar } from './StatusBar.js';
31
+ import { TeamPanel } from './TeamPanel.js';
28
32
  import { ToastStack } from './ToastStack.js';
29
33
  import { AppShell } from './layout/AppShell.js';
30
34
  import { ScrollViewport } from './layout/ScrollViewport.js';
31
35
  import { useTerminalSize } from './layout/useTerminalSize.js';
32
36
  import { frameHeight, MIN_FULLSCREEN_ROWS } from './layout/frame.js';
33
37
  import { HINT_MIN_ROWS, viewportRows as computeViewportRows } from './layout/budget.js';
38
+ import { UNMEASURED_GEOMETRY } from './layout/regions.js';
39
+ import { OVERLAY_PAGE, useWheelRouting } from './use-wheel-routing.js';
34
40
  import { installConsoleBridge } from './console-bridge.js';
35
41
  import { publishExitSnapshot } from './exit-snapshot.js';
36
42
  import { OverlayFrame } from './layout/OverlayFrame.js';
@@ -46,9 +52,7 @@ import { makeSkillsCommand, registerSkillCommands } from '../commands/skills.js'
46
52
  import { MODE_LABEL, nextMode } from '../agent/agent-mode.js';
47
53
  /** Merge pending delta actions ~33 ms and flush the coalescer buffer. */
48
54
  const COALESCE_MS = 33;
49
- /** Rows a PgUp/PgDn moves inside a controlled overlay. */
50
- const OVERLAY_PAGE = 8;
51
- export function App({ controller, version, mode, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, }) {
55
+ export function App({ controller, version, mode, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, geometryOverride, }) {
52
56
  const { exit } = useApp();
53
57
  const { stdout } = useStdout();
54
58
  const fullscreen = mode === 'fullscreen';
@@ -58,9 +62,14 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
58
62
  const [confirmState, setConfirmState] = useState(null);
59
63
  /** The request the question / plan overlay is currently rendering. */
60
64
  const [humanRequest, setHumanRequest] = useState(null);
61
- const [promptHistory, setPromptHistory] = useState(() => controller.getConfig().promptHistory ?? []);
65
+ // Seeded from the history file, not from `CliConfig`: the list is no longer
66
+ // part of the resolved config, so every other `loadConfig()` caller is spared
67
+ // reading it (config-state-separation D-4).
68
+ const [promptHistory, setPromptHistory] = useState(() => loadPromptHistory());
62
69
  // Scroll plumbing: intent goes down, one derived display number comes back.
63
70
  const [scrollIntent, setScrollIntent] = useState();
71
+ /** One prompt-history step per wheel notch over the composer band (§4.7). */
72
+ const [historyIntent, setHistoryIntent] = useState();
64
73
  const [pinToBottomNonce, setPinToBottomNonce] = useState(0);
65
74
  const [scrolledLines, setScrolledLines] = useState(0);
66
75
  // I-5 (§4.13): Ctrl+L cannot repaint by writing escapes — Ink dedupes
@@ -98,6 +107,8 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
98
107
  /** Every outstanding `gate.request()`. Held in a ref, never in state: it is
99
108
  * bookkeeping for promises, and re-rendering on it would be noise. */
100
109
  const pendingHuman = useRef(new Set());
110
+ /** Written by `AppShell`'s layout effect, read at wheel-event time (§4.5). */
111
+ const geometryRef = useRef(UNMEASURED_GEOMETRY);
101
112
  /**
102
113
  * THE SINGLE SETTLE PATH for a human request. Idempotent per entry, and it
103
114
  * clears the overlay as its last act.
@@ -190,6 +201,62 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
190
201
  unsubscribe();
191
202
  };
192
203
  }, [controller, flushPending, cancelPendingHuman]);
204
+ // --- Team-mode event stream (team-subagents §5.2 / §3.9). --------------
205
+ //
206
+ // A SEPARATE SUBSCRIPTION, not a branch in the one above: `TeamEvent` is
207
+ // deliberately not a member of core's `AgentEvent` union (D-10 / I-5), so it
208
+ // cannot arrive through `controller.subscribe`.
209
+ //
210
+ // These actions are NOT routed through the streaming coalescer. That buffer
211
+ // exists to merge text/thinking deltas; `agent_update` is already coalesced at
212
+ // the source at 120 ms per child (§5.2), and pushing it through a second
213
+ // buffer would only delay the roster.
214
+ useEffect(() => {
215
+ const unsubscribeTeam = controller.subscribeTeam((event) => {
216
+ switch (event.type) {
217
+ case 'dispatch_start':
218
+ dispatch({
219
+ type: 'teamStart',
220
+ dispatchId: event.dispatchId,
221
+ requested: event.requested,
222
+ specs: event.specs,
223
+ });
224
+ break;
225
+ case 'agent_update': {
226
+ const snapshot = controller.getTeamSnapshot();
227
+ if (snapshot)
228
+ dispatch({ type: 'teamUpdate', snapshot });
229
+ break;
230
+ }
231
+ case 'message': {
232
+ const snapshot = controller.getTeamSnapshot();
233
+ if (snapshot)
234
+ dispatch({ type: 'teamUpdate', snapshot });
235
+ break;
236
+ }
237
+ case 'usage':
238
+ // Child spend is REAL SPEND and must reach the status bar, or the
239
+ // session cost readout under-reports by however much the team consumed
240
+ // — the single most misleading possible failure of this feature (R-5).
241
+ dispatch({
242
+ type: 'teamUsage',
243
+ usage: event.usage,
244
+ costDelta: computeCost(event.usage, controller.getModelInfo().cost),
245
+ });
246
+ break;
247
+ case 'dispatch_end':
248
+ dispatch({ type: 'teamEnd', outcome: event.outcome });
249
+ break;
250
+ }
251
+ });
252
+ return () => {
253
+ unsubscribeTeam();
254
+ // Nothing else disposes the runtime on an inline-mode exit or a crash
255
+ // that unmounts the tree, and an orphaned child holds the event loop open
256
+ // (R-15). `cli.tsx` calls this too; `dispose()` is idempotent.
257
+ controller.dispose();
258
+ };
259
+ }, [controller]);
193
260
  // --- Elapsed timer + tokens/sec baseline while running. ----------------
194
261
  useEffect(() => {
195
262
  if (state.status !== 'running') {
@@ -233,7 +300,14 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
233
300
  return undefined;
234
301
  return installConsoleBridge((level, text) => dispatch({ type: 'notice', level, text }));
235
302
  }, [fullscreen]);
236
- useStartupNotices(dispatch);
303
+ // `enabled` is `mouseSource`, not `cfg.mouse`: a user whose terminal drops
304
+ // the events must never be shown advice about a mode not in effect (§6.1).
305
+ useStartupNotices(dispatch, {
306
+ enabled: !!mouseSource,
307
+ // `state.json`, not `config.json`: whether a one-shot notice has been shown
308
+ // is bookkeeping, and the comment on the old persisted key said so.
309
+ onSeen: () => setMouseNoticeSeen(true),
310
+ });
237
311
  // --- Exit snapshot: the only way state reaches cli.tsx (§4.4). ----------
238
312
  useEffect(() => {
239
313
  publishExitSnapshot({
@@ -390,18 +464,25 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
390
464
  refreshSkills: () => setSkillsNonce((n) => n + 1),
391
465
  applyAgentMode: applyMode,
392
466
  });
467
+ /**
468
+ * Submitting a prompt no longer touches `config.json` at all — it appends one
469
+ * line to the history file and bumps one counter in `state.json`.
470
+ *
471
+ * `setPromptHistory` IS NOT OPTIONAL, and deleting it is the single easiest
472
+ * mistake to make here: `appendPrompt` keeps its own module-level array, but
473
+ * the `history` prop the composer recalls from is THIS React state. Without
474
+ * this line `↑` still works after a restart (the file is correct) and fails
475
+ * only for prompts submitted in the current session.
476
+ */
393
477
  const recordPrompt = (text) => {
394
- const next = [...promptHistory.filter((p) => p !== text), text].slice(-PROMPT_HISTORY_CAP);
395
- setPromptHistory(next);
396
- // The submit counter rides along with the history write rather than opening
397
- // a second I/O path (§4.6). It is read back through `controller.getConfig()`
398
- // and never enters React state — nothing re-renders on its account.
399
- const nextCount = (controller.getConfig().submitCount ?? 0) + 1;
400
- controller.setSubmitCount(nextCount);
401
- persistConfig({ promptHistory: next, submitCount: nextCount });
478
+ setPromptHistory(appendPrompt(text));
479
+ // Read back through `controller.getConfig()` and never entering React
480
+ // state, exactly as before — nothing re-renders on the counter's account.
481
+ controller.setSubmitCount(bumpSubmitCount());
402
482
  };
403
- const scrollBy = useCallback((kind) => {
404
- setScrollIntent((prev) => ({ kind, nonce: (prev?.nonce ?? 0) + 1 }));
483
+ /** `repeat` folds a coalesced wheel burst; the keyboard always sends one. */
484
+ const scrollBy = useCallback((kind, repeat = 1) => {
485
+ setScrollIntent((prev) => ({ kind, nonce: (prev?.nonce ?? 0) + 1, repeat }));
405
486
  }, []);
406
487
  /**
407
488
  * Send a message to the agent, bypassing slash parsing.
@@ -500,11 +581,30 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
500
581
  }
501
582
  resolveHuman({ kind: 'planDecision', decision: verdict.decision, feedback: verdict.feedback });
502
583
  };
584
+ // --- Mouse wheel region routing (§4.6). Table and coalescer live in the
585
+ // hook (D-13); only the wiring it cannot own stays here. Inline arrows are
586
+ // fine — the hook re-reads its options through a ref every render. The
587
+ // `Math.max(0, …)` mirrors the keyboard branch below, and for the same
588
+ // reason: `OverlayFrame` only ever reports a clamp DOWNWARD.
589
+ const { clearCoalescer } = useWheelRouting({
590
+ mouseSource,
591
+ geometryRef,
592
+ geometryOverride,
593
+ getOverlay: () => stateRef.current.overlay,
594
+ onScroll: scrollBy,
595
+ onOverlayScroll: (delta) => setOverlayScroll((n) => Math.max(0, n + delta)),
596
+ onHistoryStep: (dir) => setHistoryIntent((prev) => ({ dir, nonce: (prev?.nonce ?? 0) + 1 })),
597
+ });
503
598
  // A fresh overlay always opens at the top; carrying the previous overlay's
504
599
  // offset over would open Help mid-list for no reason the user can see.
600
+ //
601
+ // `clearCoalescer()` rides along here rather than adding a second subscriber
602
+ // to the same transition (§4.7 rule 2): an accumulated-but-unflushed
603
+ // transcript scroll must not survive an overlay opening.
505
604
  useEffect(() => {
506
605
  setOverlayScroll(0);
507
- }, [state.overlay]);
606
+ clearCoalescer();
607
+ }, [state.overlay, clearCoalescer]);
508
608
  // --- Initial overlay / prompt (after handlers are defined). ------------
509
609
  useEffect(() => {
510
610
  if (initialOverlay)
@@ -612,7 +712,12 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
612
712
  return;
613
713
  }
614
714
  if (key.ctrl && (input === 'o' || input === 'O')) {
615
- const last = [...stateRef.current.entries].reverse().find((e) => e.kind === 'tool');
715
+ // `team` joins `tool` here (team-subagents §6.3): a dispatch card collapses
716
+ // its per-agent summaries behind the same key, and `toggleExpand` is keyed
717
+ // on entry id so it needs no change.
718
+ const last = [...stateRef.current.entries]
719
+ .reverse()
720
+ .find((e) => e.kind === 'tool' || e.kind === 'team');
616
721
  if (last)
617
722
  dispatch({ type: 'toggleExpand', id: last.id });
618
723
  else
@@ -666,6 +771,12 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
666
771
  apiKey: '',
667
772
  logLevel: cfg.log.level,
668
773
  }, apiKeys: cfg.apiKeys, maxRows: overlayMaxRows, cols: cols, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps, onSave: handleSettingsSave })) : overlay === 'confirm' && confirmState ? (_jsx(ConfirmDialog, { state: confirmState, maxRows: overlayMaxRows, cols: cols, theme: theme, caps: caps, onClose: closeConfirm })) : overlay === 'question' && humanRequest?.kind === 'questions' ? (_jsx(QuestionOverlay, { questions: humanRequest.questions, maxRows: overlayMaxRows, cols: cols, theme: theme, caps: caps, onSubmit: (answers) => resolveHuman({ kind: 'answers', answers, cancelled: false }) })) : overlay === 'plan' && humanRequest?.kind === 'plan' ? (_jsx(PlanReviewOverlay, { plan: humanRequest.plan, maxRows: overlayMaxRows, cols: cols, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps, onVerdict: handlePlanVerdict })) : null;
774
+ // A MEASUREMENT MUST NEVER OUTLIVE THE FRAME IT DESCRIBED (P1-5): neither
775
+ // path below renders the full-screen `AppShell`, so nothing else would
776
+ // overwrite a stale `bottom`. See `UNMEASURED_GEOMETRY` for why there is no
777
+ // arithmetic fallback.
778
+ if (!fullscreen || rows < MIN_FULLSCREEN_ROWS)
779
+ geometryRef.current = UNMEASURED_GEOMETRY;
669
780
  // A resize can drop below the usable floor. Do NOT leave the alt-screen —
670
781
  // entering and leaving it on every drag is worse than a placeholder — and take
671
782
  // the height from `frameHeight()` like everything else, because this is the
@@ -683,11 +794,19 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
683
794
  // the header the moment the first message lands.
684
795
  const opener = empty ? (_jsx(SessionOpener, { variant: fullscreen ? pickOpenerVariant(viewportBudget, cols, caps) : 'none', version: version, cwd: controller.getCwd(), hasKey: hasKey, viewportRows: fullscreen ? viewportBudget : Number.POSITIVE_INFINITY, theme: theme, caps: caps })) : null;
685
796
  const density = cfg.density;
686
- const viewport = fullscreen ? (overlayNode ? (_jsx(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: overlayNode })) : (_jsxs(ScrollViewport, { intent: scrollIntent, pinToBottomNonce: pinToBottomNonce, onScrolledLinesChange: setScrolledLines, theme: theme, caps: caps, children: [opener, _jsx(TranscriptList, { entries: state.entries, expandedToolIds: state.expandedToolIds, thinkingVisible: state.thinkingVisible, reducedMotion: reducedMotion, density: density, theme: theme, caps: caps, windowSize: cfg.transcriptWindow ?? DEFAULT_TRANSCRIPT_WINDOW })] }))) : (_jsxs(_Fragment, { children: [opener, _jsx(Transcript, { entries: state.entries, expandedToolIds: state.expandedToolIds, thinkingVisible: state.thinkingVisible, reducedMotion: reducedMotion, density: density, theme: theme, caps: caps }), overlayNode] }));
797
+ const viewport = fullscreen ? (overlayNode ? (_jsx(Box, { flexDirection: "column", flexGrow: 1, overflow: "hidden", children: overlayNode })) : (_jsxs(ScrollViewport, { intent: scrollIntent, pinToBottomNonce: pinToBottomNonce, onScrolledLinesChange: setScrolledLines, showScrollIndicator: true, theme: theme, caps: caps, children: [opener, _jsx(TranscriptList, { entries: state.entries, expandedToolIds: state.expandedToolIds, thinkingVisible: state.thinkingVisible, reducedMotion: reducedMotion, density: density, theme: theme, caps: caps, windowSize: cfg.transcriptWindow ?? DEFAULT_TRANSCRIPT_WINDOW })] }))) : (_jsxs(_Fragment, { children: [opener, _jsx(Transcript, { entries: state.entries, expandedToolIds: state.expandedToolIds, thinkingVisible: state.thinkingVisible, reducedMotion: reducedMotion, density: density, theme: theme, caps: caps }), overlayNode] }));
687
798
  // The completion popup lives inside the bottom chrome and can add up to 9
688
799
  // rows; unbounded, it pushes the status bar past the frame height where
689
800
  // `overflow: hidden` clips it away (R-14 / M-14). Cap it against the viewport.
690
801
  const popupMaxRows = fullscreen ? Math.max(1, viewportBudget - 4) : undefined;
691
- const composer = fullscreen ? (_jsx(Composer, { isActive: overlay === null, running: state.status === 'running', history: promptHistory, commands: commandOptions, cwd: controller.getCwd(), showHint: rows >= HINT_MIN_ROWS, submitCount: cfg.submitCount, hintsEnabled: cfg.hints, agentMode: state.agentMode, popupMaxRows: popupMaxRows, theme: theme, caps: caps, onSubmit: (text) => void handleSubmit(text), onHelp: () => dispatch({ type: 'setOverlay', overlay: 'help' }) })) : (_jsx(Box, { marginTop: 1, children: _jsx(PromptInput, { isActive: overlay === null, running: state.status === 'running', history: promptHistory, commands: commandOptions, cwd: controller.getCwd(), agentMode: state.agentMode, theme: theme, caps: caps, onSubmit: (text) => void handleSubmit(text), onHelp: () => dispatch({ type: 'setOverlay', overlay: 'help' }) }) }));
692
- return (_jsx(AppShell, { mode: mode, rows: rows, cols: cols, header: header, viewport: viewport, toast: _jsx(ToastStack, { toasts: state.toasts, theme: theme, mode: mode }), composer: composer, status: _jsx(StatusBar, { model: cfg.model, provider: cfg.provider, usageTotal: state.usageTotal, contextTokens: state.contextTokens, contextWindow: modelInfo.contextWindow, contextWindowKnown: modelKnown, status: state.status, elapsedMs: elapsedMs, thinkingLevel: cfg.thinkingLevel, tokPerSec: tokPerSec, theme: theme, caps: caps, scrolledLines: scrolledLines, redrawNonce: redrawNonce, agentMode: state.agentMode, pendingAgentMode: state.pendingAgentMode }) }));
802
+ const composer = fullscreen ? (_jsx(Composer, { isActive: overlay === null, running: state.status === 'running', history: promptHistory, commands: commandOptions, cwd: controller.getCwd(), showHint: rows >= HINT_MIN_ROWS, submitCount: cfg.submitCount, hintsEnabled: cfg.hints, agentMode: state.agentMode, popupMaxRows: popupMaxRows, historyIntent: historyIntent, theme: theme, caps: caps, onSubmit: (text) => void handleSubmit(text), onHelp: () => dispatch({ type: 'setOverlay', overlay: 'help' }) })) : (_jsx(Box, { marginTop: 1, children: _jsx(PromptInput, { isActive: overlay === null, running: state.status === 'running', history: promptHistory, commands: commandOptions, cwd: controller.getCwd(), agentMode: state.agentMode, theme: theme, caps: caps, onSubmit: (text) => void handleSubmit(text), onHelp: () => dispatch({ type: 'setOverlay', overlay: 'help' }) }) }));
803
+ return (_jsx(AppShell, { mode: mode, rows: rows, cols: cols, geometryRef: geometryRef, header: header, viewport: viewport, team: state.team ? (_jsx(TeamPanel, { snapshot: state.team, rows: rows, cols: cols, reducedMotion: reducedMotion, theme: theme, caps: caps })) : null, toast: _jsx(ToastStack, { toasts: state.toasts, theme: theme, mode: mode }), composer: composer, status: _jsx(StatusBar, { model: cfg.model, provider: cfg.provider, usageTotal: state.usageTotal, contextTokens: state.contextTokens, contextWindow: modelInfo.contextWindow, contextWindowKnown: modelKnown, status: state.status, elapsedMs: elapsedMs, thinkingLevel: cfg.thinkingLevel, tokPerSec: tokPerSec, theme: theme, caps: caps, scrolledLines: scrolledLines, redrawNonce: redrawNonce, agentMode: state.agentMode, pendingAgentMode: state.pendingAgentMode, ...(state.team
804
+ ? {
805
+ teamActive: {
806
+ running: state.team.runs.filter((r) => r.phase !== 'queued' && r.phase !== 'done' &&
807
+ r.phase !== 'failed' && r.phase !== 'aborted').length,
808
+ total: state.team.runs.length,
809
+ },
810
+ }
811
+ : {}) }) }));
693
812
  }
@@ -35,9 +35,14 @@ export interface ComposerProps {
35
35
  /** Mirror of the controller's effective mode; drives the chip and the border. */
36
36
  agentMode: AgentMode;
37
37
  popupMaxRows?: number;
38
+ /** Pure pass-through to `PromptInput` (mouse-wheel-region-routing §5.6). */
39
+ historyIntent?: {
40
+ dir: 'up' | 'down';
41
+ nonce: number;
42
+ };
38
43
  theme: Theme;
39
44
  caps: TermCapabilities;
40
45
  onSubmit: (text: string) => void;
41
46
  onHelp?: () => void;
42
47
  }
43
- export declare function Composer({ isActive, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, popupMaxRows, theme, caps, onSubmit, onHelp, }: ComposerProps): React.ReactElement;
48
+ export declare function Composer({ isActive, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, popupMaxRows, historyIntent, theme, caps, onSubmit, onHelp, }: ComposerProps): React.ReactElement;
@@ -56,7 +56,7 @@ function hintText(running, submitCount, glyphs, mode) {
56
56
  '? help',
57
57
  ].join(dot);
58
58
  }
59
- export function Composer({ isActive, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, popupMaxRows, theme, caps, onSubmit, onHelp, }) {
59
+ export function Composer({ isActive, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, popupMaxRows, historyIntent, theme, caps, onSubmit, onHelp, }) {
60
60
  const [hasDraft, setHasDraft] = useState(false);
61
61
  const onDraftChange = useCallback((next) => setHasDraft(next), []);
62
62
  // Plan mode inserts ONE branch, above `draft` and below `running`. The
@@ -73,7 +73,7 @@ export function Composer({ isActive, running, history, commands, cwd, showHint,
73
73
  : theme.idleBorder ?? theme.border;
74
74
  const glyphs = pickGlyphs(caps);
75
75
  const visible = showHint && hintsEnabled;
76
- return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsx(PromptInput, { isActive: isActive, running: running, history: history, commands: commands, cwd: cwd, theme: theme, caps: caps, popupMaxRows: popupMaxRows, onSubmit: onSubmit, onHelp: onHelp, agentMode: agentMode, bordered: { color: borderColor }, onDraftChange: onDraftChange }), visible && (
76
+ return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsx(PromptInput, { isActive: isActive, running: running, history: history, commands: commands, cwd: cwd, theme: theme, caps: caps, popupMaxRows: popupMaxRows, onSubmit: onSubmit, onHelp: onHelp, agentMode: agentMode, bordered: { color: borderColor }, onDraftChange: onDraftChange, historyIntent: historyIntent }), visible && (
77
77
  // The chip rides the EXISTING hint row rather than adding one, so it
78
78
  // costs zero rows on a 24-row terminal.
79
79
  _jsxs(Box, { flexDirection: "row", flexShrink: 0, children: [_jsx(Text, { children: ' ' }), _jsx(ModeChip, { mode: agentMode, theme: theme, caps: caps }), _jsx(Text, { wrap: "truncate", color: theme.hintFg ?? theme.muted, children: hintText(running, submitCount, glyphs, agentMode) })] }))] }));
@@ -55,6 +55,20 @@ interface PromptInputProps {
55
55
  };
56
56
  /** Fires only when the draft flips empty ⇄ non-empty, for the border color. */
57
57
  onDraftChange?: (hasDraft: boolean) => void;
58
+ /**
59
+ * One prompt-history step, pushed by the wheel router when a notch lands on
60
+ * the composer band (mouse-wheel-region-routing §5.6). `nonce` is monotonic
61
+ * so two notches in the same direction are two steps.
62
+ *
63
+ * Applied through the SAME `verticalOrHistory` / popup-selection branches the
64
+ * keyboard uses, deliberately: it means the wheel and `↑`/`↓` cannot drift
65
+ * apart, and the "never clobber an in-progress draft" guard protects the
66
+ * wheel for free.
67
+ */
68
+ historyIntent?: {
69
+ dir: 'up' | 'down';
70
+ nonce: number;
71
+ };
58
72
  }
59
73
  /** Apply an edit intent as a pure buffer/cursor transform. */
60
74
  export declare function applyEdit(buffer: string, cursor: number, intent: EditIntent): {
@@ -84,5 +98,5 @@ export declare function fileTokenAt(buffer: string, cursor: number): {
84
98
  end: number;
85
99
  query: string;
86
100
  } | null;
87
- export declare function PromptInput({ isActive, running, history, commands, cwd, theme, caps, popupMaxRows, agentMode, onSubmit, onHelp, bordered, onDraftChange, }: PromptInputProps): React.ReactElement;
101
+ export declare function PromptInput({ isActive, running, history, commands, cwd, theme, caps, popupMaxRows, agentMode, onSubmit, onHelp, bordered, onDraftChange, historyIntent, }: PromptInputProps): React.ReactElement;
88
102
  export {};
@@ -127,7 +127,7 @@ function isControlSeq(input) {
127
127
  // ---------------------------------------------------------------------------
128
128
  // Component
129
129
  // ---------------------------------------------------------------------------
130
- export function PromptInput({ isActive, running, history, commands, cwd, theme, caps, popupMaxRows, agentMode, onSubmit, onHelp, bordered, onDraftChange, }) {
130
+ export function PromptInput({ isActive, running, history, commands, cwd, theme, caps, popupMaxRows, agentMode, onSubmit, onHelp, bordered, onDraftChange, historyIntent, }) {
131
131
  const [buffer, setBuffer] = useState('');
132
132
  const [cursor, setCursor] = useState(0);
133
133
  const [historyIndex, setHistoryIndex] = useState(null);
@@ -261,6 +261,43 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
261
261
  recallDown();
262
262
  }
263
263
  };
264
+ // --- Wheel-driven history step (mouse-wheel-region-routing §5.6). ---------
265
+ // The applier closes over the buffer, the cursor and the popup state, so it
266
+ // is re-created on every render and reached through a ref: depending on it
267
+ // directly would re-fire the effect on every keystroke.
268
+ const applyHistoryIntent = useRef(() => { });
269
+ applyHistoryIntent.current = (dir) => {
270
+ // Mirrors the keyboard branch exactly — with the completion popup open,
271
+ // Up/Down move the selection rather than recalling history.
272
+ if (popupItems.length > 0) {
273
+ setSel(dir === 'up'
274
+ ? Math.max(0, clampedSel - 1)
275
+ : Math.min(popupItems.length - 1, clampedSel + 1));
276
+ return;
277
+ }
278
+ verticalOrHistory(dir);
279
+ };
280
+ const historyIntentRef = useRef(historyIntent);
281
+ historyIntentRef.current = historyIntent;
282
+ // I-9: seeded from the incoming nonce during the FIRST render, so a mount
283
+ // that inherits a nonce cannot replay it. This component really does remount
284
+ // — `App` returns the terminal-too-small placeholder before `AppShell` —
285
+ // so without the guard, shrinking the window mid-wheel and growing it back
286
+ // recalls a history entry with no gesture behind it.
287
+ const lastHistoryNonce = useRef(historyIntent?.nonce);
288
+ const historyNonce = historyIntent?.nonce ?? 0;
289
+ useEffect(() => {
290
+ const current = historyIntentRef.current;
291
+ if (!current || !isActive)
292
+ return;
293
+ if (current.nonce === lastHistoryNonce.current)
294
+ return;
295
+ lastHistoryNonce.current = current.nonce;
296
+ applyHistoryIntent.current(current.dir);
297
+ // Keyed on the nonce alone, same reasoning as `ScrollViewport`'s intent
298
+ // effect: `historyIntent` is a fresh object on every parent render.
299
+ // eslint-disable-next-line react-hooks/exhaustive-deps
300
+ }, [historyNonce]);
264
301
  // --- Input handling. -----------------------------------------------------
265
302
  useInput((input, key) => {
266
303
  // FIRST STATEMENT, BEFORE THE POPUP BRANCH, AND IT HAS TO STAY THERE.
@@ -49,6 +49,27 @@ interface StatusBarProps {
49
49
  agentMode?: AgentMode;
50
50
  /** Non-null while a deferred `plan -> build` waits for `agent_end`. */
51
51
  pendingAgentMode?: AgentMode | null;
52
+ /**
53
+ * Live subagent counts (team-subagents §6.2), or absent when no dispatch is
54
+ * running — an ordinary session's bar is then unchanged.
55
+ *
56
+ * LEFT CLUSTER, and it degrades (D-20 / P2-1). The left cluster is
57
+ * `flexShrink={0}`, so every column it takes comes out of the context gauge
58
+ * and the cost readout opposite it; on an 80-column terminal the full
59
+ * `agents 3/5` visibly squeezes them, and the gauge is how a user notices they
60
+ * are about to run out of context. Below `TEAM_LIMITS.statusCompactCols` this
61
+ * therefore renders as `[3]` — the running count alone.
62
+ *
63
+ * This does NOT contradict the "no `cols >= N` breakpoint" rule above: that
64
+ * rule is written about `agentMode`, whose reason is that the mode must never
65
+ * be unreportable. The team counter has a second, fuller home in `TeamPanel`
66
+ * and a third in `TeamCard`; it is the one readout on this bar that can afford
67
+ * to degrade.
68
+ */
69
+ teamActive?: {
70
+ running: number;
71
+ total: number;
72
+ };
52
73
  }
53
74
  export declare function StatusBar(props: StatusBarProps): React.ReactElement;
54
75
  export {};
@@ -4,6 +4,7 @@ import { pickGlyphs } from './glyphs.js';
4
4
  import { formatCost, formatTokens, formatDuration } from '../agent/usage.js';
5
5
  import { buildGauge } from './gauge.js';
6
6
  import { MODE_LABEL } from '../agent/agent-mode.js';
7
+ import { TEAM_LIMITS } from '../team/limits.js';
7
8
  /**
8
9
  * I-5 — DO NOT "CLEAN THIS UP". Ink short-circuits identical output at TWO
9
10
  * gates (`ink.js:132` and `log-update.js:13`), so a fixed frame whose React
@@ -23,7 +24,7 @@ function redrawChar(nonce) {
23
24
  return nonce % 2 === 0 ? ' ' : NBSP;
24
25
  }
25
26
  export function StatusBar(props) {
26
- const { model, provider, usageTotal, contextTokens, contextWindow, contextWindowKnown, status, elapsedMs, thinkingLevel, tokPerSec, theme, caps, scrolledLines = 0, redrawNonce = 0, agentMode = 'build', pendingAgentMode = null, } = props;
27
+ const { model, provider, usageTotal, contextTokens, contextWindow, contextWindowKnown, status, elapsedMs, thinkingLevel, tokPerSec, theme, caps, scrolledLines = 0, redrawNonce = 0, agentMode = 'build', pendingAgentMode = null, teamActive, } = props;
27
28
  const { stdout } = useStdout();
28
29
  const cols = stdout?.columns ?? 80;
29
30
  const running = status === 'running';
@@ -47,5 +48,7 @@ export function StatusBar(props) {
47
48
  // and "? hel" rather than truncating cleanly at one end. Pinning the left
48
49
  // cluster keeps the run status and model intact and makes the degradation
49
50
  // land in one predictable place.
50
- _jsxs(Box, { flexDirection: "row", justifyContent: "space-between", flexShrink: 0, children: [_jsxs(Box, { flexDirection: "row", flexShrink: 0, children: [_jsxs(Text, { color: running ? theme.toolRunning : theme.toolDone, children: [redrawChar(redrawNonce), statusGlyph, " ", running ? 'running' : 'idle'] }), modeWord && (_jsxs(Text, { color: theme.accent, bold: true, children: [' ', modeWord] })), cols >= 60 && (_jsxs(Text, { color: theme.muted, children: [' ', provider, ":", model] })), thinkingLevel !== 'off' && cols >= 72 && (_jsxs(Text, { color: theme.accent, children: [" think:", thinkingLevel] }))] }), _jsxs(Box, { flexDirection: "row", children: [scrolledLines > 0 && (_jsxs(Text, { color: theme.noticeWarn, children: [glyphs.arrowUp, scrolledLines, ' '] })), cols >= 60 && (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: "[" }), _jsx(Text, { color: gauge.fillColor, children: gauge.filled }), _jsx(Text, { color: gauge.trackColor, children: gauge.empty }), _jsx(Text, { color: theme.muted, children: "] " })] })), _jsx(Text, { color: theme.muted, children: pctLabel }), cols >= 72 && _jsxs(Text, { color: theme.muted, children: [" ", tokens] }), _jsxs(Text, { color: theme.muted, children: [" ", formatCost(usageTotal.costUsd)] }), running && (_jsxs(Text, { color: theme.muted, children: [tokPerSec > 0 && cols >= 72 ? ` ${tokPerSec} tok/s` : '', " ", formatDuration(elapsedMs)] }))] })] }));
51
+ _jsxs(Box, { flexDirection: "row", justifyContent: "space-between", flexShrink: 0, children: [_jsxs(Box, { flexDirection: "row", flexShrink: 0, children: [_jsxs(Text, { color: running ? theme.toolRunning : theme.toolDone, children: [redrawChar(redrawNonce), statusGlyph, " ", running ? 'running' : 'idle'] }), modeWord && (_jsxs(Text, { color: theme.accent, bold: true, children: [' ', modeWord] })), teamActive && (_jsxs(Text, { color: theme.toolRunning, bold: true, children: [' ', cols >= TEAM_LIMITS.statusCompactCols
52
+ ? `agents ${teamActive.running}/${teamActive.total}`
53
+ : `[${teamActive.running}]`] })), cols >= 60 && (_jsxs(Text, { color: theme.muted, children: [' ', provider, ":", model] })), thinkingLevel !== 'off' && cols >= 72 && (_jsxs(Text, { color: theme.accent, children: [" think:", thinkingLevel] }))] }), _jsxs(Box, { flexDirection: "row", children: [scrolledLines > 0 && (_jsxs(Text, { color: theme.noticeWarn, children: [glyphs.arrowUp, scrolledLines, ' '] })), cols >= 60 && (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: "[" }), _jsx(Text, { color: gauge.fillColor, children: gauge.filled }), _jsx(Text, { color: gauge.trackColor, children: gauge.empty }), _jsx(Text, { color: theme.muted, children: "] " })] })), _jsx(Text, { color: theme.muted, children: pctLabel }), cols >= 72 && _jsxs(Text, { color: theme.muted, children: [" ", tokens] }), _jsxs(Text, { color: theme.muted, children: [" ", formatCost(usageTotal.costUsd)] }), running && (_jsxs(Text, { color: theme.muted, children: [tokPerSec > 0 && cols >= 72 ? ` ${tokPerSec} tok/s` : '', " ", formatDuration(elapsedMs)] }))] })] }));
51
54
  }