@aragon-agent/cli 0.6.12 → 0.6.13

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 (101) hide show
  1. package/README.md +81 -15
  2. package/dist/agent/controller.d.ts +29 -1
  3. package/dist/agent/controller.js +123 -22
  4. package/dist/agent/model-profile-settings.d.ts +31 -0
  5. package/dist/agent/model-profile-settings.js +199 -0
  6. package/dist/agent/queued-messages.d.ts +12 -0
  7. package/dist/agent/queued-messages.js +18 -0
  8. package/dist/agent/reducer.d.ts +13 -1
  9. package/dist/agent/reducer.js +51 -12
  10. package/dist/cli.js +15 -1
  11. package/dist/commands/builtins.js +66 -39
  12. package/dist/commands/queue-command.d.ts +3 -0
  13. package/dist/commands/queue-command.js +5 -0
  14. package/dist/commands/registry.d.ts +3 -0
  15. package/dist/compaction/child.d.ts +4 -2
  16. package/dist/compaction/child.js +4 -1
  17. package/dist/compaction/compactor.d.ts +7 -2
  18. package/dist/compaction/compactor.js +13 -11
  19. package/dist/compaction/context-command.js +5 -0
  20. package/dist/compaction/wiring.d.ts +4 -3
  21. package/dist/compaction/wiring.js +6 -1
  22. package/dist/config/cli-commands.d.ts +2 -0
  23. package/dist/config/cli-commands.js +16 -4
  24. package/dist/config/load.d.ts +2 -1
  25. package/dist/config/load.js +15 -8
  26. package/dist/config/model-profile-resolution.d.ts +20 -0
  27. package/dist/config/model-profile-resolution.js +81 -0
  28. package/dist/config/model-profile-store.d.ts +67 -0
  29. package/dist/config/model-profile-store.js +153 -0
  30. package/dist/config/model-profiles.d.ts +44 -0
  31. package/dist/config/model-profiles.js +121 -0
  32. package/dist/config/schema.d.ts +9 -0
  33. package/dist/config/store.d.ts +11 -1
  34. package/dist/config/store.js +43 -27
  35. package/dist/fast/resolve.d.ts +2 -1
  36. package/dist/fast/resolve.js +6 -4
  37. package/dist/fast/wiring.d.ts +5 -3
  38. package/dist/fast/wiring.js +8 -4
  39. package/dist/input/stdin-filter.js +3 -2
  40. package/dist/logging/install.js +4 -2
  41. package/dist/logging/secret-registry.d.ts +2 -0
  42. package/dist/logging/secret-registry.js +18 -0
  43. package/dist/team/runtime.d.ts +2 -1
  44. package/dist/team/subagent.d.ts +3 -1
  45. package/dist/team/subagent.js +3 -1
  46. package/dist/ui/ActivityLine.d.ts +6 -70
  47. package/dist/ui/ActivityLine.js +12 -34
  48. package/dist/ui/App.d.ts +1 -0
  49. package/dist/ui/App.js +113 -205
  50. package/dist/ui/BottomStatusRow.d.ts +6 -86
  51. package/dist/ui/BottomStatusRow.js +11 -26
  52. package/dist/ui/Composer.d.ts +16 -83
  53. package/dist/ui/Composer.js +9 -100
  54. package/dist/ui/PromptInput.d.ts +4 -1
  55. package/dist/ui/PromptInput.js +41 -34
  56. package/dist/ui/QueuePanel.d.ts +8 -0
  57. package/dist/ui/QueuePanel.js +9 -0
  58. package/dist/ui/QueueStatusRow.d.ts +2 -0
  59. package/dist/ui/QueueStatusRow.js +1 -1
  60. package/dist/ui/StatusBar.d.ts +13 -149
  61. package/dist/ui/StatusBar.js +27 -142
  62. package/dist/ui/clipboard-task.d.ts +29 -0
  63. package/dist/ui/clipboard-task.js +66 -0
  64. package/dist/ui/clipboard.d.ts +29 -50
  65. package/dist/ui/clipboard.js +141 -75
  66. package/dist/ui/composer-limits.d.ts +3 -4
  67. package/dist/ui/composer-limits.js +4 -9
  68. package/dist/ui/composer-rows.d.ts +1 -1
  69. package/dist/ui/composer-rows.js +24 -137
  70. package/dist/ui/editor-navigation.d.ts +45 -0
  71. package/dist/ui/editor-navigation.js +85 -0
  72. package/dist/ui/editor-reducer.d.ts +4 -0
  73. package/dist/ui/editor-reducer.js +29 -15
  74. package/dist/ui/interaction-copy.d.ts +77 -0
  75. package/dist/ui/interaction-copy.js +40 -0
  76. package/dist/ui/interaction-hints.d.ts +18 -0
  77. package/dist/ui/interaction-hints.js +92 -0
  78. package/dist/ui/layout/budget.js +1 -1
  79. package/dist/ui/layout/queue-layout.d.ts +19 -0
  80. package/dist/ui/layout/queue-layout.js +26 -0
  81. package/dist/ui/layout/status-layout.d.ts +55 -0
  82. package/dist/ui/layout/status-layout.js +109 -0
  83. package/dist/ui/layout/todo-layout.d.ts +1 -0
  84. package/dist/ui/layout/todo-layout.js +1 -1
  85. package/dist/ui/overlays/HelpOverlay.js +8 -8
  86. package/dist/ui/overlays/ModelProfileEditor.d.ts +12 -0
  87. package/dist/ui/overlays/ModelProfileEditor.js +103 -0
  88. package/dist/ui/overlays/ModelProfilePicker.d.ts +35 -0
  89. package/dist/ui/overlays/ModelProfilePicker.js +167 -0
  90. package/dist/ui/overlays/QueueOverlay.d.ts +22 -0
  91. package/dist/ui/overlays/QueueOverlay.js +79 -0
  92. package/dist/ui/overlays/SettingsScreen.d.ts +9 -2
  93. package/dist/ui/overlays/SettingsScreen.js +347 -3
  94. package/dist/ui/overlays/model-profile-state.d.ts +74 -0
  95. package/dist/ui/overlays/model-profile-state.js +119 -0
  96. package/dist/ui/run-status-row.d.ts +7 -19
  97. package/dist/ui/run-status-row.js +2 -18
  98. package/dist/ui/selection/selection-controller.d.ts +2 -2
  99. package/dist/ui/selection/selection-controller.js +1 -0
  100. package/dist/ui/use-wheel-routing.js +2 -1
  101. package/package.json +2 -2
package/dist/ui/App.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { mergePendingEntries } from '../agent/queued-messages.js';
2
+ import { modelSettingsResultMessage } from '../agent/model-profile-settings.js';
3
+ import { selectTranscriptEntries, mergePendingEntries } from '../agent/queued-messages.js';
3
4
  /**
4
5
  * App — the root Ink component. Owns the view reducer, subscribes to the
5
6
  * controller's event stream (with a streaming coalescer), wires global
@@ -19,7 +20,7 @@ import { appendPrompt, loadPromptHistory } from '../config/prompt-history.js';
19
20
  import { bumpSubmitCount, setMouseNoticeVersion, setVtInputNoticeVersion, } from '../config/ui-state.js';
20
21
  import { getLogger } from '../logging/logger.js';
21
22
  import { isTerminalStatus } from '../proc/types.js';
22
- import { getSecrets, registerSecret } from '../logging/secret-registry.js';
23
+ import { getSecrets } from '../logging/secret-registry.js';
23
24
  import { redactText } from '../logging/redact.js';
24
25
  import { detectCapabilities } from './capabilities.js';
25
26
  import { useStartupNotices, MOUSE_NOTICE_VERSION, VT_INPUT_NOTICE_VERSION, } from './use-startup-notices.js';
@@ -34,7 +35,6 @@ import { StatusBar } from './StatusBar.js';
34
35
  import { secondsLeft } from '../agent/retry-view.js';
35
36
  import { TeamPanel } from './TeamPanel.js';
36
37
  import { TodoPanel } from './TodoPanel.js';
37
- import { ActivityLine, liveSpinner } from './ActivityLine.js';
38
38
  import { BottomStatusRow } from './BottomStatusRow.js';
39
39
  import { UpdateLine } from './UpdateLine.js';
40
40
  // The updater's two PURE modules. `shouldRenderUpdateLine` decides the row's
@@ -62,7 +62,7 @@ import { publishExitSnapshot } from './exit-snapshot.js';
62
62
  import { OverlayFrame } from './layout/OverlayFrame.js';
63
63
  import { helpRows } from './overlays/HelpOverlay.js';
64
64
  import { ModelPicker } from './overlays/ModelPicker.js';
65
- import { SettingsScreen, compactionSettingsFrom, fastSettingsFrom, readCompactionSettings, readFastSettings, } from './overlays/SettingsScreen.js';
65
+ import { SettingsScreen, compactionSettingsFrom, fastSettingsFrom, } from './overlays/SettingsScreen.js';
66
66
  import { ConfirmDialog } from './overlays/ConfirmDialog.js';
67
67
  import { QuestionOverlay } from './overlays/QuestionOverlay.js';
68
68
  import { PlanReviewOverlay } from './overlays/PlanReviewOverlay.js';
@@ -70,7 +70,11 @@ import { CommandRegistry, runSlashInput } from '../commands/registry.js';
70
70
  import { registerBuiltinCommands } from '../commands/builtins.js';
71
71
  import { makeSkillsCommand, registerSkillCommands } from '../commands/skills.js';
72
72
  import { MODE_LABEL, MODE_TOGGLE_KEYS, nextMode } from '../agent/agent-mode.js';
73
- import { copyText } from './clipboard.js';
73
+ import { createClipboardCoordinator } from './clipboard-task.js';
74
+ import { QueuePanel } from './QueuePanel.js';
75
+ import { QueueOverlay } from './overlays/QueueOverlay.js';
76
+ import { buildQueueLayout } from './layout/queue-layout.js';
77
+ import { interactionCopy } from './interaction-copy.js';
74
78
  import { emptyTailState } from './layout/follow-state.js';
75
79
  /**
76
80
  * Decide what the `Max tokens` field means, WITHOUT letting a bad value in it
@@ -196,6 +200,8 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
196
200
  // closing an overlay cannot leave a stale offset behind, and reset whenever
197
201
  // the active overlay changes.
198
202
  const [overlayScroll, setOverlayScroll] = useState(0);
203
+ const [completion, setCompletion] = useState('none');
204
+ const [copyState, setCopyState] = useState({ busy: false, cleanupPending: false });
199
205
  // Bumped whenever the installed skill set changes. The command registry and
200
206
  // the completion list are memoized against it, so a skill installed mid-session
201
207
  // gets its `/<name>` command and its autocomplete entry immediately.
@@ -1168,22 +1174,36 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1168
1174
  // redraw when the frame differ has no cache to repaint from. Both are assigned
1169
1175
  // here and cleared on unmount, the same shape `updateBridge` uses.
1170
1176
  const selectionBridge = terminal?.selection;
1177
+ const copyCoordinator = useMemo(() => createClipboardCoordinator({
1178
+ clipboard: { write: terminal?.writeForeign },
1179
+ onRequest: () => {
1180
+ ctrlCArmed.current = false;
1181
+ if (ctrlCTimer.current)
1182
+ clearTimeout(ctrlCTimer.current);
1183
+ ctrlCTimer.current = null;
1184
+ },
1185
+ onStateChange: setCopyState,
1186
+ onBusy: ({ cleanupPending }) => toast('info', cleanupPending
1187
+ ? '\u590d\u5236\u5931\u8d25\uff0c\u6b63\u5728\u6e05\u7406' : '\u6b63\u5728\u590d\u5236'),
1188
+ onResult: (result, request) => {
1189
+ selectionBridge?.onCopied?.(result, request.lines, request.text.length);
1190
+ if (result.status === 'confirmed') {
1191
+ toast('success', `\u5df2\u590d\u5236 ${request.lines} \u884c`);
1192
+ }
1193
+ else if (result.status === 'sent') {
1194
+ toast('info', '\u5df2\u8bf7\u6c42\u7ec8\u7aef\u590d\u5236\uff0c\u7ed3\u679c\u672a\u786e\u8ba4');
1195
+ }
1196
+ else {
1197
+ const text = `${interactionCopy.copyFailed}\uff1a${interactionCopy.copyReasons[result.reason]}`;
1198
+ toast('warn', text);
1199
+ notify('error', text);
1200
+ }
1201
+ },
1202
+ }), []);
1203
+ useEffect(() => () => copyCoordinator.dispose(), [copyCoordinator]);
1171
1204
  useEffect(() => {
1172
1205
  if (!selectionBridge)
1173
1206
  return undefined;
1174
- selectionBridge.onCopied = (via, lines, chars) => {
1175
- // IT NAMES THE MECHANISM RATHER THAN CLAIMING SUCCESS (R-7). Neither OSC 52
1176
- // nor a spawned `xclip` is detectable: tmux without `set -g set-clipboard
1177
- // on` swallows the first silently, and the second is best-effort by
1178
- // construction. A toast that said "copied successfully" would be guessing.
1179
- const noun = lines === 1 ? 'line' : 'lines';
1180
- if (via === 'none') {
1181
- toast('warn', 'Nothing could be copied - no clipboard mechanism is available.');
1182
- return;
1183
- }
1184
- const how = via === 'osc52' ? 'terminal clipboard' : 'system clipboard';
1185
- toast('success', `Sent ${lines} ${noun} (${chars} chars) to the ${how}.`);
1186
- };
1187
1207
  // I-5 of `frame-differ.ts`: writing escapes here would be a foreign write.
1188
1208
  // The nonce is the carrier Ctrl+L already uses (§4.13).
1189
1209
  selectionBridge.requestRedraw = () => setRedrawNonce((n) => n + 1);
@@ -1290,6 +1310,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1290
1310
  };
1291
1311
  const makeCtx = (args) => ({
1292
1312
  args,
1313
+ requestCopy: copyCoordinator.requestCopy,
1293
1314
  controller,
1294
1315
  state: stateRef.current,
1295
1316
  dispatch: (action) => {
@@ -1372,6 +1393,9 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1372
1393
  * start with `/`, recurse into command dispatch.
1373
1394
  */
1374
1395
  const submitMessage = (message, opts = {}) => {
1396
+ if (controller.getConfig().modelSettingsRestartRequired) {
1397
+ return { accepted: false, reason: 'Settings require a restart before sending requests.' };
1398
+ }
1375
1399
  if (interactionPhase.current === 'starting') {
1376
1400
  return { accepted: false, reason: 'A run is still starting. Press Esc twice to cancel.' };
1377
1401
  }
@@ -1381,13 +1405,21 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1381
1405
  cancelFollowThrough();
1382
1406
  cancelConfirmation();
1383
1407
  if (interactionPhase.current === 'running') {
1384
- const queueId = controller.queueUserMessage(message);
1385
- dispatch({ type: 'steerQueued', queueId, text: message });
1386
- return { accepted: true };
1408
+ try {
1409
+ const queueId = controller.queueUserMessage(message);
1410
+ dispatch({ type: 'steerQueued', queueId, text: message });
1411
+ return { accepted: true };
1412
+ }
1413
+ catch (error) {
1414
+ const reason = error instanceof Error ? error.message : String(error);
1415
+ notify('error', reason);
1416
+ return { accepted: false, reason };
1417
+ }
1387
1418
  }
1388
1419
  resetInterrupt();
1389
1420
  const sequence = ++submissionSequence.current;
1390
1421
  interactionPhase.current = 'starting';
1422
+ dispatch({ type: 'runStarting' });
1391
1423
  // `userInitiated: false` skips TWO things and both are wanted: the
1392
1424
  // prompt-history append, and the `submitCount` bump that drives the
1393
1425
  // composer's hint fade. An auto-continuation demonstrates nothing about what
@@ -1401,6 +1433,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1401
1433
  dispatch({ type: 'submit', text: message });
1402
1434
  const pre = controller.preflight();
1403
1435
  if (!pre.ok) {
1436
+ dispatch({ type: 'runError' });
1404
1437
  notify('error', pre.message ?? 'Configuration error.');
1405
1438
  interactionPhase.current = 'idle';
1406
1439
  resetInterrupt();
@@ -1414,6 +1447,8 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1414
1447
  return;
1415
1448
  if (outcome?.status !== 'not-started')
1416
1449
  return;
1450
+ if (outcome.reason !== 'cancelled')
1451
+ dispatch({ type: 'runError' });
1417
1452
  interactionPhase.current = 'idle';
1418
1453
  resetInterrupt();
1419
1454
  autoContinuationRef.current = false;
@@ -1451,91 +1486,24 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1451
1486
  }
1452
1487
  return submitMessage(trimmed.startsWith('//') ? raw.replace('//', '/') : raw);
1453
1488
  };
1454
- const handleSettingsSave = (values) => {
1455
- // An unusable cap must NOT discard the rest of the form: the same Enter
1456
- // press is often carrying a freshly pasted API key, and losing that to a
1457
- // typo in an unrelated field is the worst outcome available here.
1458
- const cap = resolveSettingsMaxTokens(values.maxTokens, controller.getConfig().maxTokens);
1459
- controller.setModel(values.provider, values.model, values.baseUrl || undefined);
1460
- controller.setThinkingLevel(values.thinkingLevel);
1461
- controller.setMaxTokens(cap.applied);
1462
- // THE SETTER AND THE PERSIST ARE BOTH REQUIRED, the pair `/todo panel`
1463
- // documents: persisting alone reports success and changes nothing until the
1464
- // next launch. `thinkingVisible` lives in `ViewState`, not in the config the
1465
- // controller holds, so the live half is a dispatch rather than a setter —
1466
- // and it is a no-op when the row already agrees with the current state.
1467
- if ((values.showThinking === 'on') !== stateRef.current.thinkingVisible) {
1489
+ const handleProfileSave = (draft) => {
1490
+ const result = controller.saveModelSettings(draft);
1491
+ if (!result.ok)
1492
+ return result;
1493
+ if (draft.patch.showThinking !== undefined
1494
+ && controller.getConfig().showThinking !== stateRef.current.thinkingVisible) {
1468
1495
  dispatch({ type: 'toggleThinking' });
1469
1496
  }
1470
- const key = values.apiKey.trim();
1471
- if (key.length > 0) {
1472
- // Registration site 4 (§4.4.3), and the one that matters most: this is how
1473
- // a key most often enters the process, and it happens long after
1474
- // `installLogging()` ran. A startup snapshot would miss it entirely, and
1475
- // the vendor regexes only cover keys that look like a vendor's.
1476
- registerSecret(key);
1477
- controller.setApiKey(values.provider, key);
1478
- }
1479
- const patch = {
1480
- provider: values.provider,
1481
- model: values.model,
1482
- baseUrl: values.baseUrl.trim() ? values.baseUrl.trim() : null,
1483
- thinkingLevel: values.thinkingLevel,
1484
- showThinking: values.showThinking === 'on',
1485
- // PERSIST ONLY, with no paired live setter — the store is allocated in
1486
- // `AgentController`'s constructor, so this row takes effect on the next
1487
- // launch. That is what manual row 8 asks for, and it is why the seed above
1488
- // reads `cfg` rather than `ViewState`.
1489
- liveToolOutput: values.liveToolOutput === 'on',
1490
- // Omitted entirely when the field was unusable, so the previous value
1491
- // survives instead of being overwritten with a guess.
1492
- ...(cap.persist !== 'skip' ? { maxTokens: cap.persist } : {}),
1493
- };
1494
- if (key.length > 0)
1495
- patch.apiKeys = { [values.provider]: key };
1496
- // A PARTIAL `log` section, safe only because `updatePersistedConfig` deep-
1497
- // merges it — a shallow merge here would reset `redactSecrets` every time
1498
- // someone touched the level.
1499
- patch.log = { level: values.logLevel };
1500
- // --- Fast tier (fast-model-tier §4.5) ---------------------------------
1501
- //
1502
- // THE SETTER AND THE PERSIST ARE BOTH REQUIRED, and this is the pair `/todo
1503
- // panel` documents (P1-2): `App` reads `controller.getConfig()` at render
1504
- // time, so persisting alone reports success and changes nothing until the
1505
- // next launch, while the setter alone forgets by morning.
1506
- //
1507
- // `setFastConfig` also re-resolves the tier, which matters HERE more than
1508
- // anywhere: this same Enter press may have changed the main provider, the
1509
- // main model or the API key, and `fast.provider: ''` inherits from all
1510
- // three (§3.2 rule 8 / RV-3).
1511
- const fastPatch = readFastSettings(values);
1512
- controller.setFastConfig(fastPatch);
1513
- patch.fast = fastPatch;
1514
- // --- Context compaction (context-auto-compaction §4.5) ------------------
1515
- //
1516
- // THE SAME SETTER-AND-PERSIST PAIR, and the same reason. `setCompactionConfig`
1517
- // additionally re-emits the snapshot, which is what moves the status chip and
1518
- // the gauge's marks without waiting for the next turn.
1519
- //
1520
- // `enabled` here flips the LIVE switch only. Whether the wiring exists at all
1521
- // was decided at construction and cannot be undone in this session (§3.2), so
1522
- // a user who turns it ON in a `--no-compaction` session gets the persisted
1523
- // value for next launch and nothing this session — which is exactly what
1524
- // `/compact on` reports in words.
1525
- const compactionPatch = readCompactionSettings(values);
1526
- controller.setCompactionConfig(compactionPatch);
1527
- if (compactionPatch.enabled !== undefined) {
1528
- controller.setCompactionEnabled(compactionPatch.enabled);
1529
- }
1530
- patch.compaction = compactionPatch;
1531
- persistConfig(patch);
1532
- getLogger().reconfigure({ ...controller.getConfig().log, level: values.logLevel });
1533
1497
  dispatch({ type: 'setOverlay', overlay: null });
1534
- toast(cap.toast?.[0] ?? 'success', cap.toast?.[1] ?? 'Settings saved.');
1498
+ toast(result.restartRequired ? 'warn' : 'success', modelSettingsResultMessage(result));
1499
+ return result;
1535
1500
  };
1536
1501
  const handleModelSelect = (provider, model) => {
1537
- controller.setModel(provider, model);
1538
- persistConfig({ provider, model });
1502
+ const result = controller.saveModelSettingsPatch({ provider, model });
1503
+ if (!result.ok) {
1504
+ toast('error', modelSettingsResultMessage(result));
1505
+ return;
1506
+ }
1539
1507
  dispatch({ type: 'setOverlay', overlay: null });
1540
1508
  toast('info', `Model set to ${provider}:${model}.`);
1541
1509
  };
@@ -1642,6 +1610,10 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1642
1610
  // (`via === 'none'`) still reports through the shared toast funnel and
1643
1611
  // returns: the selection is already consumed, and the NEXT Ctrl+C
1644
1612
  // walks the original ladder.
1613
+ if (key.ctrl && (input === 'c' || input === 'C') && copyCoordinator.isBusy()) {
1614
+ void copyCoordinator.requestCopy({ text: '', lines: 0 });
1615
+ return;
1616
+ }
1645
1617
  if (key.ctrl &&
1646
1618
  (input === 'c' || input === 'C') &&
1647
1619
  (selectionController?.hasPendingSelection() ?? false)) {
@@ -1651,8 +1623,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1651
1623
  clearTimeout(ctrlCTimer.current);
1652
1624
  ctrlCTimer.current = null;
1653
1625
  if (payload) {
1654
- const via = copyText(payload.text, terminal?.writeForeign);
1655
- selectionBridge?.onCopied?.(via, payload.lines, payload.text.length);
1626
+ void copyCoordinator.requestCopy({ text: payload.text, lines: payload.lines });
1656
1627
  }
1657
1628
  return; // A consumed copy intent never becomes exit or service stop.
1658
1629
  }
@@ -1699,7 +1670,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1699
1670
  const overlay = stateRef.current.overlay;
1700
1671
  // Modes A only. `model` / `confirm` manage their own keys; registering an
1701
1672
  // arrow handler for them would double every keypress (R-12).
1702
- const controlled = overlay === 'help' || overlay === 'settings' || overlay === 'plan';
1673
+ const controlled = overlay === 'help' || overlay === 'settings' || overlay === 'plan' || overlay === 'queue';
1703
1674
  if (key.pageUp) {
1704
1675
  if (controlled)
1705
1676
  setOverlayScroll((n) => Math.max(0, n - OVERLAY_PAGE));
@@ -1714,7 +1685,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1714
1685
  // worth more than scrolling six fields (§4.9). `question` keeps them for
1715
1686
  // its option cursor, for the same reason; `plan` has no cursor of its own
1716
1687
  // and scrolls a line at a time.
1717
- if ((overlay === 'help' || overlay === 'plan') && (key.upArrow || key.downArrow)) {
1688
+ if ((overlay === 'help' || overlay === 'plan' || overlay === 'queue') && (key.upArrow || key.downArrow)) {
1718
1689
  setOverlayScroll((n) => Math.max(0, n + (key.upArrow ? -1 : 1)));
1719
1690
  return;
1720
1691
  }
@@ -1809,6 +1780,8 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1809
1780
  }
1810
1781
  if (key.escape) {
1811
1782
  const overlay = stateRef.current.overlay;
1783
+ if (overlay === 'settings')
1784
+ return;
1812
1785
  if (overlay) {
1813
1786
  cancelConfirmation();
1814
1787
  if (overlay === 'confirm' && confirmRef.current) {
@@ -1843,7 +1816,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1843
1816
  interruptGesture.current = next.state;
1844
1817
  if (next.action === 'hint') {
1845
1818
  dispatch({ type: 'pushToast', level: 'info',
1846
- text: 'Press Esc again within 1.5s to interrupt.' });
1819
+ text: '\u4e2d\u65ad\u5df2\u51c6\u5907' });
1847
1820
  return;
1848
1821
  }
1849
1822
  endReasonRef.current.aborted = true;
@@ -1864,7 +1837,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1864
1837
  controller.abort();
1865
1838
  if (sequence === submissionSequence.current && interactionPhase.current === 'running') {
1866
1839
  dispatch({ type: 'pushToast', level: 'info',
1867
- text: 'Interrupt requested. Esc again to force-stop.' });
1840
+ text: '\u6b63\u5728\u505c\u6b62' });
1868
1841
  }
1869
1842
  }, [controller, cancelFollowThrough, resetInterrupt]);
1870
1843
  // --- Render ------------------------------------------------------------
@@ -1875,6 +1848,8 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1875
1848
  // which is what lets the user's `contextWindow` override reach the gauge at
1876
1849
  // all. Nothing else in this render read them.
1877
1850
  const overlay = state.overlay;
1851
+ const settingsDraft = useMemo(() => overlay === 'settings'
1852
+ ? controller.createModelSettingsDraft() : undefined, [controller, overlay]);
1878
1853
  const hasKey = controller.hasApiKey();
1879
1854
  const empty = state.entries.length === 0;
1880
1855
  // RENDER SCOPE, so the chip and the composer hint agree with what the Ctrl+C
@@ -1923,24 +1898,28 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1923
1898
  const nowSec = running || compactionLive ? Math.floor(Date.now() / 1000) : undefined;
1924
1899
  const viewportBudget = computeViewportRows(rows, draftRows);
1925
1900
  const overlayMaxRows = viewportBudget;
1926
- // Renderers consume these same projections. Team capacity does not depend on
1927
- // popup occupancy, so a menu cannot toggle the team's collapsed state.
1928
- // THE ONE CONDITION for "the hint row exists": it sizes the footer below AND
1929
- // decides whether the run status row may replace that hint row while running,
1930
- // so starting a run can never add or remove a footer row.
1931
- const hintRowEnabled = rows >= HINT_MIN_ROWS && cfg.hints;
1932
- const composerBaseRows = 2 + clampDraftRows(rows, draftRows) + Number(hintRowEnabled);
1901
+ const composerBaseRows = 2 + clampDraftRows(rows, draftRows);
1902
+ const baseRail = buildTodoRailLayout({
1903
+ cols: cols - 1, panelEnabled: cfg.todo.panel, overlayOpen: false,
1904
+ itemCount: state.todos?.items.length ?? 0, viewportBudget,
1905
+ teamRows: 0, popupRows: 0, composerBaseRows,
1906
+ });
1907
+ const queueLayout = useMemo(() => buildQueueLayout({
1908
+ pending: state.pendingSteering, columns: baseRail.contentCols, terminalRows: rows,
1909
+ availableRows: Math.max(0, viewportBudget - composerBaseRows - 1), paused: !running,
1910
+ }), [state.pendingSteering, baseRail.contentCols, rows, viewportBudget, composerBaseRows, running]);
1933
1911
  const teamLayout = buildTeamPanelLayout({
1934
- snapshot: state.team, terminalRows: rows, availableRows: Math.max(0, viewportBudget - composerBaseRows),
1912
+ snapshot: state.team, terminalRows: rows,
1913
+ availableRows: Math.max(0, viewportBudget - composerBaseRows - queueLayout.rows - 1),
1935
1914
  });
1936
1915
  const railLayout = buildTodoRailLayout({
1937
1916
  cols: cols - 1, panelEnabled: cfg.todo.panel, overlayOpen: false,
1938
1917
  itemCount: state.todos?.items.length ?? 0, viewportBudget,
1939
- teamRows: teamLayout.rowCount, popupRows, composerBaseRows,
1918
+ teamRows: teamLayout.rowCount, queueRows: queueLayout.rows, popupRows, composerBaseRows,
1940
1919
  });
1941
- // Transcript and composer share contentCols; fixed chrome and overlays use terminal width.
1942
1920
  const { visible: showRail, width: railWidth, rows: railRows, contentCols } = railLayout;
1943
- const overlayNode = overlay === 'help' ? (_jsx(OverlayFrame, { title: "Help", hint: `Esc close ${glyphs.midDot} PgUp/PgDn scroll`, maxRows: overlayMaxRows, cols: cols, rows: helpRows(theme, caps), scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps })) : overlay === 'model' ? (_jsx(ModelPicker, { registry: controller.getModelRegistry(), currentProvider: cfg.provider, currentModel: cfg.model, maxRows: overlayMaxRows, cols: cols, theme: theme, caps: caps, onSelect: handleModelSelect })) : overlay === 'settings' ? (_jsx(SettingsScreen, { initial: {
1921
+ const transcriptEntries = useMemo(() => selectTranscriptEntries(state.entries, state.pendingSteering), [state.entries, state.pendingSteering]);
1922
+ const overlayNode = overlay === 'queue' ? (_jsx(QueueOverlay, { pending: state.pendingSteering, cols: cols, maxRows: overlayMaxRows, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps })) : overlay === 'help' ? (_jsx(OverlayFrame, { title: "Help", hint: `Esc close ${glyphs.midDot} PgUp/PgDn scroll`, maxRows: overlayMaxRows, cols: cols, rows: helpRows(theme, caps), scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps })) : overlay === 'model' ? (_jsx(ModelPicker, { registry: controller.getModelRegistry(), currentProvider: cfg.provider, currentModel: cfg.model, maxRows: overlayMaxRows, cols: cols, theme: theme, caps: caps, onSelect: handleModelSelect })) : overlay === 'settings' ? (_jsx(SettingsScreen, { initial: {
1944
1923
  provider: cfg.provider,
1945
1924
  model: cfg.model,
1946
1925
  baseUrl: cfg.baseUrl ?? '',
@@ -1969,7 +1948,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1969
1948
  // the save handler cannot disagree about what `(inherit)` means.
1970
1949
  ...fastSettingsFrom(cfg.fast),
1971
1950
  ...compactionSettingsFrom(cfg.compaction),
1972
- }, apiKeys: cfg.apiKeys, maxRows: overlayMaxRows, cols: cols, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps, onSave: handleSettingsSave,
1951
+ }, apiKeys: cfg.apiKeys, maxRows: overlayMaxRows, cols: cols, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps, draft: settingsDraft, onProfileSave: handleProfileSave, onReload: () => controller.createModelSettingsDraft(), onClose: () => dispatch({ type: 'setOverlay', overlay: null }),
1973
1952
  // Read from `cfg` (render scope, re-read every frame), so `/retry off`
1974
1953
  // while the screen is open shows the new value on the next paint.
1975
1954
  retrySummary: cfg.retry.enabled && cfg.retry.maxRetries > 0 ? String(cfg.retry.maxRetries) : 'off' })) : 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;
@@ -1980,61 +1959,8 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1980
1959
  const opener = empty ? (_jsx(SessionOpener, { variant: pickOpenerVariant(viewportBudget, contentCols, caps), version: version, cwd: controller.getCwd(), hasKey: hasKey, viewportRows: viewportBudget, theme: theme, caps: caps })) : null;
1981
1960
  const density = cfg.density;
1982
1961
  const transcriptWindow = cfg.transcriptWindow ?? DEFAULT_TRANSCRIPT_WINDOW;
1983
- // --- One spinner per run (single-spinner-while-running D-1 .. D-5). ------
1984
- //
1985
- // THE MOUNT CONDITION, NAMED ONCE AND USED TWICE (D-4). This const both gates
1986
- // the `<ActivityLine>` element below and feeds the derivation on the next
1987
- // line, so "the line is up" and "every other site is still" can never
1988
- // disagree. Two copies of the same boolean expression is exactly how that
1989
- // invariant becomes false in six months, silently and in only one branch.
1990
- //
1991
- // NOT `running` ALONE (D-3). While an overlay owns the screen the activity row
1992
- // is suppressed, so keying suppression off `running` would leave the frame
1993
- // with ZERO life signals: the bar shows state, this row shows life. Never
1994
- // trade one duplicate for one absence.
1995
- //
1996
- // THE CLAIM ABOVE WAS ONCE FALSE, AND THAT WAS THE WHOLE OF
1997
- // `activity-spinner-vanishes-behind-toast`. `BottomStatusRow` — not this const
1998
- // — decides what the row actually shows, and a toast used to take the row
1999
- // outright, so for its 2.5 s TTL this said "the line is up" while nothing was
2000
- // mounted and all seven other sites sat still. The answer was NOT to enumerate
2001
- // `&& !state.toasts.length` here (the next occupant reopens it, and the seven
2002
- // sites animating out of phase is the defect this whole feature removed):
2003
- // `BottomStatusRow` now carries the bare spinner alongside the toast, so the
2004
- // signal is mounted exactly when this const is true and the claim holds by
2005
- // construction.
2006
- const activityVisible = running && !overlayNode;
2007
- // The FOUR VIEW CONSUMERS (transcript x 2, team panel, todo panel) take this;
2008
- // the activity line keeps the raw config flag (D-5), because it reads that
2009
- // flag a second time to gate phrase rotation and the widened value would
2010
- // freeze the phrase at the first word of every run.
2011
- //
2012
- // So below `App` the prop now means "this frame does not animate here" rather
2013
- // than "the user asked for reduced motion". `cfg.reducedMotion` (`:279`) stays
2014
- // the sole config source, and `spinner-census.test.ts` pins this wiring —
2015
- // threading the raw flag into a fifth consumer is the one silent way back to
2016
- // two spinners (R-2).
2017
- const viewReducedMotion = reducedMotion || activityVisible;
2018
- // The run status row (spinner + phrase + queue/interrupt hint) replaces the idle
2019
- // hint row ABOVE the input while running; it exists only when that hint row
2020
- // would, so the footer height is identical with and without it. `running`, not
2021
- // `activityVisible`: an overlay must not flip the Composer between two layouts.
2022
- const runRowEnabled = running && hintRowEnabled;
2023
- // The raw `reducedMotion` flag lives here and nowhere else (D-5).
2024
- const runActivity = {
2025
- startedAt: runStartedAt.current,
2026
- elapsedMs,
2027
- reducedMotion,
2028
- runningTool,
2029
- // THE ROW MUST NEVER SAY SOMETHING FALSE (§6.1). Compaction runs BEFORE
2030
- // `turn_start`, so `status` is `'running'` with no tool in flight - precisely
2031
- // the window in which the rotating phrase would claim the model is thinking
2032
- // while it is actually waiting on a 30k-token summarization.
2033
- compacting: state.compaction?.inFlight === true,
2034
- };
2035
- // The fixed bottom row takes over the life signal once the run row scrolls out
2036
- // of the viewport; exactly one of the two ever animates.
2037
- const runRowShown = runRowEnabled && activityRowVisible;
1962
+ // The fixed status bar remains visible underneath every overlay and toast.
1963
+ const viewReducedMotion = reducedMotion || running || state.runPhase === 'starting' || compactionLive;
2038
1964
  // Assigned during render so `/perf` reads the CURRENT frame (see `perfRef`).
2039
1965
  // `mountedEntries` is the same number `render-budget.test.tsx` asserts (AC-9).
2040
1966
  perfRef.current = {
@@ -2071,39 +1997,21 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
2071
1997
  // Both keys always WORK (they share one branch in the input handler); this
2072
1998
  // only decides which one the user is taught.
2073
1999
  const modeToggleKey = vtInputWarning ? MODE_TOGGLE_KEYS.fallback : MODE_TOGGLE_KEYS.primary;
2074
- const composer = (_jsx(Composer, { measureRef: composerRef, runRow: runRowEnabled
2075
- ? { activity: runActivity, live: runRowShown && !overlayNode, rowRef: activityRef }
2076
- : null, cols: contentCols, cursorVisible: cursorVisible, onInteraction: returnToComposer, reducedMotion: cfg.reducedMotion ?? false, onEscape: handleComposerEscape, onEscapeDismiss: cancelConfirmation, isActive: overlay === null && !tooSmall, 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, services: liveServices, modeToggleKey: modeToggleKey, popupMaxRows: popupMaxRows, popupMaxHeight: railLayout.popupMaxHeight, onPopupRowsChange: onPopupRowsChange, scrolledLines: scrolledLines, onDraftRows: onDraftRows, onNotice: notify, theme: theme, caps: caps, onSubmit: handleSubmit, onHelp: () => dispatch({ type: 'setOverlay', overlay: 'help' }) }));
2000
+ const composer = (_jsx(Composer, { measureRef: composerRef, deleteDisambiguated: terminal?.deleteDisambiguated ?? false, onCompletionContextChange: setCompletion, cols: contentCols, cursorVisible: cursorVisible, onInteraction: returnToComposer, reducedMotion: cfg.reducedMotion ?? false, onEscape: handleComposerEscape, onEscapeDismiss: cancelConfirmation, isActive: overlay === null && !tooSmall, 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, services: liveServices, modeToggleKey: modeToggleKey, popupMaxRows: popupMaxRows, popupMaxHeight: railLayout.popupMaxHeight, onPopupRowsChange: onPopupRowsChange, scrolledLines: scrolledLines, onDraftRows: onDraftRows, onNotice: notify, theme: theme, caps: caps, onSubmit: handleSubmit, onHelp: () => dispatch({ type: 'setOverlay', overlay: 'help' }) }));
2077
2001
  const team = state.team ? (_jsx(TeamPanel, { snapshot: state.team, layout: teamLayout, rows: rows, cols: contentCols, reducedMotion: viewReducedMotion, theme: theme, caps: caps })) : null;
2078
2002
  const rail = showRail && state.todos ? (_jsx(TodoPanel, { snapshot: state.todos, width: railWidth, rows: railRows, running: running, reducedMotion: viewReducedMotion, theme: theme, caps: caps })) : null;
2079
- const viewport = (_jsxs(ScrollViewport, { active: overlay === null && !tooSmall, overlay: tooSmall ? null : overlayNode, footer: _jsxs(_Fragment, { children: [team, composer] }), rail: rail, scrollbar: terminal?.scrollbar, composerRef: composerRef, onComposerVisibilityChange: setCursorVisible, activityRef: activityRef, onActivityVisibilityChange: setActivityRowVisible, resetKey: scrollResetKey, intent: scrollIntent, pinToBottomNonce: pinToBottomNonce, onScrolledLinesChange: setScrolledLines, onViewportShiftChange: onViewportShiftChange, tailRowsRef: tailSink, hold: selectionHold, resumeMs: cfg.scrollResumeMs, showScrollIndicator: true, cols: contentCols, theme: theme, caps: caps, children: [opener, _jsx(TranscriptList, { entries: state.entries, expandedToolIds: state.expandedToolIds, thinkingVisible: state.thinkingVisible, reducedMotion: viewReducedMotion, density: density, nowSec: nowSec, theme: theme, caps: caps, windowSize: transcriptWindow, cols: contentCols, heights: heights, mountedSink: mountedCount, tailSink: tailSink })] }));
2080
- return (_jsx(AppShell, { inactive: tooSmall, placeholder: _jsxs(Text, { wrap: "truncate", color: theme.noticeWarn, children: [glyphs.warn, " Terminal too small - needs at least 40 columns and 12 rows."] }), rows: rows, cols: cols, header: header, viewport: viewport, toast: _jsx(BottomStatusRow, { toasts: state.toasts, activity: activityVisible && !runRowShown ? (_jsx(ActivityLine, { ...runActivity, theme: theme, caps: caps })) : null,
2081
- // THE FORM THE LIFE SIGNAL TAKES WHEN A TOAST HAS THE ROW
2082
- // (`activity-spinner-vanishes-behind-toast`). Passed UNCONDITIONALLY:
2083
- // `BottomStatusRow` reads it only when `activity` above is non-null,
2084
- // so `activityVisible` stays the one mount condition and this cannot
2085
- // become a second copy of it that drifts. `null` here means the
2086
- // spinner would be static anyway (ASCII tier, or reduced motion), and
2087
- // those builds must come out unchanged.
2088
- activityGlyph: liveSpinner(reducedMotion, caps),
2089
- // PRESENCE IS DECIDED HERE, NOT BY `UpdateLine` RETURNING `null`
2090
- // (C-15 / cli-auto-update P0-1 / D-19). `BottomStatusRow` tests this
2091
- // prop for truthiness, and a React element is truthy however it
2092
- // renders - so a component that rendered nothing would give the row
2093
- // ZERO rows and silently unbalance the frame budget. Same rule the
2094
- // activity line above follows with `running && !overlayNode`.
2095
- //
2096
- // `!overlayNode` for the same reason it appears there: the full-screen
2097
- // branch replaces the whole viewport with the overlay, and an update
2098
- // notice under a settings screen is noise attached to a surface it is
2099
- // not visible on.
2100
- //
2101
- // `!running` (cli-auto-update D-2): the update notice stays silent for
2102
- // the whole run. It used to be guaranteed by the activity line
2103
- // out-ranking it inside this row; with the activity line moved above
2104
- // the input, the row can be free during a run, so the silence is stated
2105
- // here instead of inherited.
2106
- update: updateSnapshot && !overlayNode && !running && shouldRenderUpdateLine(updateSnapshot) ? (_jsx(UpdateLine, { snapshot: updateSnapshot, compact: cols < UPDATE_LIMITS.statusCompactCols, theme: theme, caps: caps })) : null, theme: theme }), status: _jsx(StatusBar, { pendingSteering: state.overlay ? undefined : state.pendingSteering, interruptHint: interruptGesture.current.phase === 'stopping'
2003
+ const viewport = (_jsxs(ScrollViewport, { active: overlay === null && !tooSmall, overlay: tooSmall ? null : overlayNode, footer: _jsxs(_Fragment, { children: [team, _jsx(QueuePanel, { layout: queueLayout, theme: theme }), composer] }), rail: rail, scrollbar: terminal?.scrollbar, composerRef: composerRef, onComposerVisibilityChange: setCursorVisible, activityRef: activityRef, onActivityVisibilityChange: setActivityRowVisible, resetKey: scrollResetKey, intent: scrollIntent, pinToBottomNonce: pinToBottomNonce, onScrolledLinesChange: setScrolledLines, onViewportShiftChange: onViewportShiftChange, tailRowsRef: tailSink, hold: selectionHold, resumeMs: cfg.scrollResumeMs, showScrollIndicator: true, cols: contentCols, theme: theme, caps: caps, children: [opener, _jsx(TranscriptList, { entries: transcriptEntries, expandedToolIds: state.expandedToolIds, thinkingVisible: state.thinkingVisible, reducedMotion: viewReducedMotion, density: density, nowSec: nowSec, theme: theme, caps: caps, windowSize: transcriptWindow, cols: contentCols, heights: heights, mountedSink: mountedCount, tailSink: tailSink })] }));
2004
+ return (_jsx(AppShell, { inactive: tooSmall, placeholder: _jsxs(Text, { wrap: "truncate", color: theme.noticeWarn, children: [glyphs.warn, " Terminal too small - needs at least 40 columns and 12 rows."] }), rows: rows, cols: cols, header: header, viewport: viewport, toast: _jsx(BottomStatusRow, { toasts: state.toasts, hints: { interactionPhase: state.runPhase === 'starting' ? 'starting'
2005
+ : running ? 'running' : 'idle', cols, completion,
2006
+ selectionPending: selectionController?.hasPendingSelection() ?? false,
2007
+ copyInFlight: copyState.busy, copyCleanupPending: copyState.cleanupPending,
2008
+ overlay, services: liveServices, hintsEnabled: cfg.hints, modeToggleKey,
2009
+ interruptHint: interruptGesture.current.phase === 'stopping'
2010
+ ? 'Esc force-stop' : interruptGesture.current.phase === 'armed'
2011
+ ? 'Esc confirm' : 'Esc x2 interrupt',
2012
+ updateAvailable: !!updateSnapshot && !overlayNode && !running
2013
+ && shouldRenderUpdateLine(updateSnapshot),
2014
+ }, update: updateSnapshot && !overlayNode && !running && shouldRenderUpdateLine(updateSnapshot) ? (_jsx(UpdateLine, { snapshot: updateSnapshot, compact: cols < UPDATE_LIMITS.statusCompactCols, theme: theme, caps: caps })) : null, theme: theme }), status: _jsx(StatusBar, { pendingSteering: state.pendingSteering, runPhase: state.runPhase, activeTool: state.activeTool, runOutcome: state.runOutcome, reducedMotion: reducedMotion, waitingForConfirmation: !!humanRequest || !!confirmState, stopping: interruptGesture.current.phase === 'stopping', interruptHint: interruptGesture.current.phase === 'stopping'
2107
2015
  ? 'Esc force-stop' : 'Esc x2 interrupt', model: cfg.model, provider: cfg.provider, usageTotal: state.usageTotal,
2108
2016
  // ONE OBJECT INSTEAD OF FOUR PARALLEL PROPS (§4.3). The four were a
2109
2017
  // projection of "many sources of truth" into the props layer, and a
@@ -1,95 +1,15 @@
1
- /**
2
- * The bottom status row — the one already-budgeted row above the composer, now
3
- * shared by two occupants (agent-activity-presentation §3.2.3 / D-17 / P0-1).
4
- *
5
- * THE ACTIVITY LINE IS NOT A NEW `AppShell` SLOT, AND THIS FILE IS WHY.
6
- *
7
- * `viewportRows()` (`layout/budget.ts`) is the ONE function that says how many
8
- * rows the viewport gets, and it enumerates exactly `header + toast + composer +
9
- * status`. In full-screen `AppShell`'s root is `height={frameHeight(rows)}` with
10
- * `overflow="hidden"`, the bottom chrome box is `flexShrink={0}` and the viewport
11
- * box is its only `flexShrink={1}` child — so an extra chrome row does not make
12
- * the frame taller, Yoga takes the row out of the TRANSCRIPT. Meanwhile
13
- * `viewportRows()` keeps returning the old number to `ScrollViewport`,
14
- * `selectWindow`'s spacers, `overlayMaxRows`, `popupMaxRows` and `buildTodoRailLayout`.
15
- * The transcript would draw one row shorter than every consumer believes, ONLY
16
- * WHILE A RUN IS IN FLIGHT, so the layout would shift on submit and shift back on
17
- * `agent_end`.
18
- *
19
- * `ToastStack.tsx` already legislates this for itself: *"in `fullscreen` this
20
- * ALWAYS occupies exactly one row — an empty row when there is nothing to say.
21
- * Rendering conditionally would make the viewport height (and therefore the whole
22
- * transcript) jump every time a toast appears or expires."* A run starts and ends
23
- * far more often than a toast appears and expires, so the activity line is the
24
- * STRONGER case for that rule, not an exception to it.
25
- *
26
- * Sharing the row means `budget.ts` and `AppShell.tsx` come out of this round
27
- * UNMODIFIED — and `budget.test.ts` asserts that they did (AC-8a, DoD #8).
28
- *
29
- * PRECEDENCE: toast > activity > update > blank. A toast wins because a
30
- * transient ack is a RESPONSE TO THE USER, and the row nearest the input belongs
31
- * to it. The update line is LAST because it is the only one of the three that is
32
- * PERSISTENT (cli-auto-update D-2): it loses nothing by waiting for the run to
33
- * end, whereas an activity line deferred is an activity line that never renders.
34
- * That also means a user who is actively working never sees the updater at all
35
- * until they stop, which is the whole of the silence requirement.
36
- *
37
- * THE TOAST TAKES THE ROW BUT NOT THE SPINNER
38
- * (`activity-spinner-vanishes-behind-toast`). "Toast wins" used to mean the
39
- * activity line was never mounted while an ack was up — and because `App`
40
- * suppresses the other seven animated sites for exactly as long as it BELIEVES
41
- * this row is showing one, a mid-run toast left the whole frame with ZERO
42
- * animations for its 2.5 s TTL, indistinguishable from a hung process. Steering
43
- * and plan approval both ack mid-run, so a long run hit it repeatedly.
44
- *
45
- * The row still belongs to the toast: the spinner joins it as a bare glyph, no
46
- * phrase and no clock, so `bottom-status-row.test.tsx`'s "the ack keeps its
47
- * words" assertion and the toast's own width are both untouched. A row-direction
48
- * box holding two one-row children is STILL ONE ROW, which is what the note
49
- * above actually forbids disturbing — `budget.ts` and `AppShell.tsx` stay out of
50
- * it. And because the glyph is mounted under precisely the condition `App` uses
51
- * to suppress the others, "the line is up" and "every other site is still" agree
52
- * again by construction rather than by enumeration of what can pre-empt the row.
53
- */
1
+ /** The permanent action/feedback row. StatusBar owns all activity animation. */
54
2
  import React from 'react';
55
3
  import type { Theme } from './theme.js';
56
4
  import type { Toast } from '../agent/reducer.js';
5
+ import { type ActionHintInput } from './interaction-hints.js';
57
6
  export interface BottomStatusRowProps {
58
7
  toasts: Toast[];
59
- /** `<ActivityLine>` while a run owns the screen, else `null`. */
60
- activity: React.ReactNode;
61
- /**
62
- * The bare animated spinner (`liveSpinner`), or `null` when it would be a
63
- * STATIC glyph — an ASCII terminal, or `reducedMotion`.
64
- *
65
- * IT IS NOT A SECOND MOUNT CONDITION. `activity` alone decides whether a life
66
- * signal belongs on this row; this prop only says which FORM the signal takes,
67
- * and it is read only when `activity` is non-null. So a caller cannot make the
68
- * two disagree, which is the failure mode that produced this prop.
69
- *
70
- * `null` means PREFIX NOTHING, not "prefix the still glyph". A static `·` in
71
- * front of every toast would be a visible change to ASCII terminals and to
72
- * users who asked for stillness — neither of whom ever had the dead-frame bug,
73
- * and both of whom must come out byte-identical.
74
- */
8
+ hints?: ActionHintInput;
9
+ /** Legacy assembly props are accepted but never mount an activity spinner. */
10
+ activity?: React.ReactNode;
75
11
  activityGlyph?: React.ReactElement | null;
76
- /**
77
- * `<UpdateLine>` when the updater has something to say, else `null` — AND THE
78
- * CALLER DECIDES WHICH (C-15 / cli-auto-update P0-1).
79
- *
80
- * `if (update)` below tests a REACT ELEMENT, and an element whose own render
81
- * returns `null` is still truthy: that branch would emit a box of ZERO rows,
82
- * the frame would keep its fixed height, Yoga would hand the row to the
83
- * transcript, and `viewportRows()` would go on reporting the old number to
84
- * five consumers. So `UpdateLine` is typed `React.ReactElement` and `App`
85
- * gates it with `shouldRenderUpdateLine(snapshot)`.
86
- *
87
- * OPTIONAL, because `bottom-status-row.test.tsx` and the `App.tsx` fixtures
88
- * construct this component without it and the test tree is inside
89
- * `tsconfig.test.json` (`typecheck-scope.test.ts`); a required prop turns
90
- * `npm run typecheck` red for a reason unrelated to this feature (P2-1).
91
- */
92
12
  update?: React.ReactNode;
93
13
  theme: Theme;
94
14
  }
95
- export declare function BottomStatusRow({ toasts, activity, activityGlyph, update, theme, }: BottomStatusRowProps): React.ReactElement | null;
15
+ export declare function BottomStatusRow(props: BottomStatusRowProps): React.ReactElement;