@aragon-agent/cli 0.6.12 → 0.6.14

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 (119) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +108 -31
  3. package/dist/agent/controller.d.ts +38 -2
  4. package/dist/agent/controller.js +185 -34
  5. package/dist/agent/model-profile-settings.d.ts +31 -0
  6. package/dist/agent/model-profile-settings.js +208 -0
  7. package/dist/agent/queued-messages.d.ts +12 -0
  8. package/dist/agent/queued-messages.js +18 -0
  9. package/dist/agent/reducer.d.ts +13 -1
  10. package/dist/agent/reducer.js +52 -12
  11. package/dist/boot/guard.d.ts +3 -3
  12. package/dist/boot/guard.js +3 -3
  13. package/dist/cli.js +48 -16
  14. package/dist/commands/builtins.js +66 -39
  15. package/dist/commands/queue-command.d.ts +3 -0
  16. package/dist/commands/queue-command.js +5 -0
  17. package/dist/commands/registry.d.ts +3 -0
  18. package/dist/compaction/child.d.ts +4 -2
  19. package/dist/compaction/child.js +8 -1
  20. package/dist/compaction/compactor.d.ts +7 -2
  21. package/dist/compaction/compactor.js +21 -16
  22. package/dist/compaction/context-command.js +10 -3
  23. package/dist/compaction/meter.js +2 -0
  24. package/dist/compaction/pressure.d.ts +1 -0
  25. package/dist/compaction/pressure.js +1 -0
  26. package/dist/compaction/types.d.ts +2 -0
  27. package/dist/compaction/wiring.d.ts +4 -3
  28. package/dist/compaction/wiring.js +10 -2
  29. package/dist/config/cli-commands.d.ts +2 -0
  30. package/dist/config/cli-commands.js +16 -4
  31. package/dist/config/load.d.ts +5 -2
  32. package/dist/config/load.js +23 -12
  33. package/dist/config/model-profile-resolution.d.ts +20 -0
  34. package/dist/config/model-profile-resolution.js +81 -0
  35. package/dist/config/model-profile-store.d.ts +67 -0
  36. package/dist/config/model-profile-store.js +153 -0
  37. package/dist/config/model-profiles.d.ts +44 -0
  38. package/dist/config/model-profiles.js +121 -0
  39. package/dist/config/schema.d.ts +11 -2
  40. package/dist/config/store.d.ts +11 -1
  41. package/dist/config/store.js +43 -27
  42. package/dist/fast/resolve.d.ts +2 -1
  43. package/dist/fast/resolve.js +6 -4
  44. package/dist/fast/wiring.d.ts +5 -3
  45. package/dist/fast/wiring.js +8 -4
  46. package/dist/input/stdin-filter.js +3 -2
  47. package/dist/logging/install.js +4 -2
  48. package/dist/logging/secret-registry.d.ts +2 -0
  49. package/dist/logging/secret-registry.js +18 -0
  50. package/dist/team/runtime.d.ts +2 -1
  51. package/dist/team/subagent.d.ts +3 -1
  52. package/dist/team/subagent.js +3 -1
  53. package/dist/ui/ActivityLine.d.ts +6 -70
  54. package/dist/ui/ActivityLine.js +12 -34
  55. package/dist/ui/App.d.ts +1 -0
  56. package/dist/ui/App.js +116 -215
  57. package/dist/ui/BottomStatusRow.d.ts +6 -86
  58. package/dist/ui/BottomStatusRow.js +11 -26
  59. package/dist/ui/Composer.d.ts +16 -83
  60. package/dist/ui/Composer.js +9 -100
  61. package/dist/ui/Logo.d.ts +3 -5
  62. package/dist/ui/Logo.js +4 -8
  63. package/dist/ui/PromptInput.d.ts +4 -1
  64. package/dist/ui/PromptInput.js +44 -35
  65. package/dist/ui/QueuePanel.d.ts +8 -0
  66. package/dist/ui/QueuePanel.js +9 -0
  67. package/dist/ui/QueueStatusRow.d.ts +2 -0
  68. package/dist/ui/QueueStatusRow.js +1 -1
  69. package/dist/ui/StatusBar.d.ts +13 -149
  70. package/dist/ui/StatusBar.js +27 -142
  71. package/dist/ui/capabilities.d.ts +2 -0
  72. package/dist/ui/capabilities.js +6 -0
  73. package/dist/ui/clipboard-task.d.ts +29 -0
  74. package/dist/ui/clipboard-task.js +66 -0
  75. package/dist/ui/clipboard.d.ts +29 -50
  76. package/dist/ui/clipboard.js +141 -75
  77. package/dist/ui/color-runtime.d.ts +5 -0
  78. package/dist/ui/color-runtime.js +29 -0
  79. package/dist/ui/composer-limits.d.ts +3 -4
  80. package/dist/ui/composer-limits.js +4 -9
  81. package/dist/ui/composer-rows.d.ts +1 -1
  82. package/dist/ui/composer-rows.js +24 -137
  83. package/dist/ui/editor-navigation.d.ts +45 -0
  84. package/dist/ui/editor-navigation.js +85 -0
  85. package/dist/ui/editor-reducer.d.ts +4 -0
  86. package/dist/ui/editor-reducer.js +29 -15
  87. package/dist/ui/glyphs.js +10 -8
  88. package/dist/ui/ink-runtime.d.ts +3 -0
  89. package/dist/ui/ink-runtime.js +30 -0
  90. package/dist/ui/interaction-copy.d.ts +77 -0
  91. package/dist/ui/interaction-copy.js +40 -0
  92. package/dist/ui/interaction-hints.d.ts +18 -0
  93. package/dist/ui/interaction-hints.js +92 -0
  94. package/dist/ui/layout/budget.js +1 -1
  95. package/dist/ui/layout/queue-layout.d.ts +19 -0
  96. package/dist/ui/layout/queue-layout.js +26 -0
  97. package/dist/ui/layout/status-layout.d.ts +55 -0
  98. package/dist/ui/layout/status-layout.js +110 -0
  99. package/dist/ui/layout/todo-layout.d.ts +1 -0
  100. package/dist/ui/layout/todo-layout.js +1 -1
  101. package/dist/ui/overlays/HelpOverlay.js +8 -8
  102. package/dist/ui/overlays/ModelProfileEditor.d.ts +12 -0
  103. package/dist/ui/overlays/ModelProfileEditor.js +103 -0
  104. package/dist/ui/overlays/ModelProfilePicker.d.ts +35 -0
  105. package/dist/ui/overlays/ModelProfilePicker.js +167 -0
  106. package/dist/ui/overlays/QueueOverlay.d.ts +22 -0
  107. package/dist/ui/overlays/QueueOverlay.js +79 -0
  108. package/dist/ui/overlays/SettingsScreen.d.ts +9 -2
  109. package/dist/ui/overlays/SettingsScreen.js +347 -3
  110. package/dist/ui/overlays/model-profile-state.d.ts +74 -0
  111. package/dist/ui/overlays/model-profile-state.js +119 -0
  112. package/dist/ui/render-cache.d.ts +2 -2
  113. package/dist/ui/render-cache.js +4 -3
  114. package/dist/ui/run-status-row.d.ts +7 -19
  115. package/dist/ui/run-status-row.js +2 -18
  116. package/dist/ui/selection/selection-controller.d.ts +2 -2
  117. package/dist/ui/selection/selection-controller.js +1 -0
  118. package/dist/ui/use-wheel-routing.js +2 -1
  119. 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
@@ -7,7 +8,6 @@ import { mergePendingEntries } from '../agent/queued-messages.js';
7
8
  * input / toast stack / status bar / overlays.
8
9
  */
9
10
  import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react';
10
- import process from 'node:process';
11
11
  import { Text, useApp, useInput, useStdout } from 'ink';
12
12
  import { advanceInterruptGesture, cancelInterruptConfirmation, } from '../input/interrupt-gesture.js';
13
13
  import { initialViewState, reduceEvent, viewReducer, } from '../agent/reducer.js';
@@ -19,9 +19,9 @@ import { appendPrompt, loadPromptHistory } from '../config/prompt-history.js';
19
19
  import { bumpSubmitCount, setMouseNoticeVersion, setVtInputNoticeVersion, } from '../config/ui-state.js';
20
20
  import { getLogger } from '../logging/logger.js';
21
21
  import { isTerminalStatus } from '../proc/types.js';
22
- import { getSecrets, registerSecret } from '../logging/secret-registry.js';
22
+ import { getSecrets } from '../logging/secret-registry.js';
23
23
  import { redactText } from '../logging/redact.js';
24
- import { detectCapabilities } from './capabilities.js';
24
+ import { resolveTuiCapabilities } from './capabilities.js';
25
25
  import { useStartupNotices, MOUSE_NOTICE_VERSION, VT_INPUT_NOTICE_VERSION, } from './use-startup-notices.js';
26
26
  import { getTheme } from './theme.js';
27
27
  import { pickGlyphs } from './glyphs.js';
@@ -34,7 +34,6 @@ import { StatusBar } from './StatusBar.js';
34
34
  import { secondsLeft } from '../agent/retry-view.js';
35
35
  import { TeamPanel } from './TeamPanel.js';
36
36
  import { TodoPanel } from './TodoPanel.js';
37
- import { ActivityLine, liveSpinner } from './ActivityLine.js';
38
37
  import { BottomStatusRow } from './BottomStatusRow.js';
39
38
  import { UpdateLine } from './UpdateLine.js';
40
39
  // The updater's two PURE modules. `shouldRenderUpdateLine` decides the row's
@@ -62,7 +61,7 @@ import { publishExitSnapshot } from './exit-snapshot.js';
62
61
  import { OverlayFrame } from './layout/OverlayFrame.js';
63
62
  import { helpRows } from './overlays/HelpOverlay.js';
64
63
  import { ModelPicker } from './overlays/ModelPicker.js';
65
- import { SettingsScreen, compactionSettingsFrom, fastSettingsFrom, readCompactionSettings, readFastSettings, } from './overlays/SettingsScreen.js';
64
+ import { SettingsScreen, compactionSettingsFrom, fastSettingsFrom, } from './overlays/SettingsScreen.js';
66
65
  import { ConfirmDialog } from './overlays/ConfirmDialog.js';
67
66
  import { QuestionOverlay } from './overlays/QuestionOverlay.js';
68
67
  import { PlanReviewOverlay } from './overlays/PlanReviewOverlay.js';
@@ -70,7 +69,11 @@ import { CommandRegistry, runSlashInput } from '../commands/registry.js';
70
69
  import { registerBuiltinCommands } from '../commands/builtins.js';
71
70
  import { makeSkillsCommand, registerSkillCommands } from '../commands/skills.js';
72
71
  import { MODE_LABEL, MODE_TOGGLE_KEYS, nextMode } from '../agent/agent-mode.js';
73
- import { copyText } from './clipboard.js';
72
+ import { createClipboardCoordinator } from './clipboard-task.js';
73
+ import { QueuePanel } from './QueuePanel.js';
74
+ import { QueueOverlay } from './overlays/QueueOverlay.js';
75
+ import { buildQueueLayout } from './layout/queue-layout.js';
76
+ import { interactionCopy } from './interaction-copy.js';
74
77
  import { emptyTailState } from './layout/follow-state.js';
75
78
  /**
76
79
  * Decide what the `Max tokens` field means, WITHOUT letting a bad value in it
@@ -196,17 +199,13 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
196
199
  // closing an overlay cannot leave a stale offset behind, and reset whenever
197
200
  // the active overlay changes.
198
201
  const [overlayScroll, setOverlayScroll] = useState(0);
202
+ const [completion, setCompletion] = useState('none');
203
+ const [copyState, setCopyState] = useState({ busy: false, cleanupPending: false });
199
204
  // Bumped whenever the installed skill set changes. The command registry and
200
205
  // the completion list are memoized against it, so a skill installed mid-session
201
206
  // gets its `/<name>` command and its autocomplete entry immediately.
202
207
  const [skillsNonce, setSkillsNonce] = useState(0);
203
- const caps = useMemo(() => {
204
- const detected = detectCapabilities(process.env, stdout);
205
- return {
206
- colorLevel: cfg.color === false ? 0 : cfg.colorLevel ?? detected.colorLevel,
207
- unicode: cfg.unicode ?? detected.unicode,
208
- };
209
- }, [cfg.color, cfg.colorLevel, cfg.unicode, stdout]);
208
+ const caps = useMemo(() => resolveTuiCapabilities({}, cfg.color), [cfg.color]);
210
209
  const theme = useMemo(() => getTheme(cfg.theme, caps), [cfg.theme, caps]);
211
210
  const glyphs = useMemo(() => pickGlyphs(caps), [caps]);
212
211
  const reducedMotion = cfg.reducedMotion ?? false;
@@ -1168,22 +1167,36 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1168
1167
  // redraw when the frame differ has no cache to repaint from. Both are assigned
1169
1168
  // here and cleared on unmount, the same shape `updateBridge` uses.
1170
1169
  const selectionBridge = terminal?.selection;
1170
+ const copyCoordinator = useMemo(() => createClipboardCoordinator({
1171
+ clipboard: { write: terminal?.writeForeign },
1172
+ onRequest: () => {
1173
+ ctrlCArmed.current = false;
1174
+ if (ctrlCTimer.current)
1175
+ clearTimeout(ctrlCTimer.current);
1176
+ ctrlCTimer.current = null;
1177
+ },
1178
+ onStateChange: setCopyState,
1179
+ onBusy: ({ cleanupPending }) => toast('info', cleanupPending
1180
+ ? '\u590d\u5236\u5931\u8d25\uff0c\u6b63\u5728\u6e05\u7406' : '\u6b63\u5728\u590d\u5236'),
1181
+ onResult: (result, request) => {
1182
+ selectionBridge?.onCopied?.(result, request.lines, request.text.length);
1183
+ if (result.status === 'confirmed') {
1184
+ toast('success', `\u5df2\u590d\u5236 ${request.lines} \u884c`);
1185
+ }
1186
+ else if (result.status === 'sent') {
1187
+ toast('info', '\u5df2\u8bf7\u6c42\u7ec8\u7aef\u590d\u5236\uff0c\u7ed3\u679c\u672a\u786e\u8ba4');
1188
+ }
1189
+ else {
1190
+ const text = `${interactionCopy.copyFailed}\uff1a${interactionCopy.copyReasons[result.reason]}`;
1191
+ toast('warn', text);
1192
+ notify('error', text);
1193
+ }
1194
+ },
1195
+ }), []);
1196
+ useEffect(() => () => copyCoordinator.dispose(), [copyCoordinator]);
1171
1197
  useEffect(() => {
1172
1198
  if (!selectionBridge)
1173
1199
  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
1200
  // I-5 of `frame-differ.ts`: writing escapes here would be a foreign write.
1188
1201
  // The nonce is the carrier Ctrl+L already uses (§4.13).
1189
1202
  selectionBridge.requestRedraw = () => setRedrawNonce((n) => n + 1);
@@ -1290,6 +1303,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1290
1303
  };
1291
1304
  const makeCtx = (args) => ({
1292
1305
  args,
1306
+ requestCopy: copyCoordinator.requestCopy,
1293
1307
  controller,
1294
1308
  state: stateRef.current,
1295
1309
  dispatch: (action) => {
@@ -1372,6 +1386,9 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1372
1386
  * start with `/`, recurse into command dispatch.
1373
1387
  */
1374
1388
  const submitMessage = (message, opts = {}) => {
1389
+ if (controller.getConfig().modelSettingsRestartRequired) {
1390
+ return { accepted: false, reason: 'Settings require a restart before sending requests.' };
1391
+ }
1375
1392
  if (interactionPhase.current === 'starting') {
1376
1393
  return { accepted: false, reason: 'A run is still starting. Press Esc twice to cancel.' };
1377
1394
  }
@@ -1381,13 +1398,21 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1381
1398
  cancelFollowThrough();
1382
1399
  cancelConfirmation();
1383
1400
  if (interactionPhase.current === 'running') {
1384
- const queueId = controller.queueUserMessage(message);
1385
- dispatch({ type: 'steerQueued', queueId, text: message });
1386
- return { accepted: true };
1401
+ try {
1402
+ const queueId = controller.queueUserMessage(message);
1403
+ dispatch({ type: 'steerQueued', queueId, text: message });
1404
+ return { accepted: true };
1405
+ }
1406
+ catch (error) {
1407
+ const reason = error instanceof Error ? error.message : String(error);
1408
+ notify('error', reason);
1409
+ return { accepted: false, reason };
1410
+ }
1387
1411
  }
1388
1412
  resetInterrupt();
1389
1413
  const sequence = ++submissionSequence.current;
1390
1414
  interactionPhase.current = 'starting';
1415
+ dispatch({ type: 'runStarting' });
1391
1416
  // `userInitiated: false` skips TWO things and both are wanted: the
1392
1417
  // prompt-history append, and the `submitCount` bump that drives the
1393
1418
  // composer's hint fade. An auto-continuation demonstrates nothing about what
@@ -1401,6 +1426,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1401
1426
  dispatch({ type: 'submit', text: message });
1402
1427
  const pre = controller.preflight();
1403
1428
  if (!pre.ok) {
1429
+ dispatch({ type: 'runError' });
1404
1430
  notify('error', pre.message ?? 'Configuration error.');
1405
1431
  interactionPhase.current = 'idle';
1406
1432
  resetInterrupt();
@@ -1414,6 +1440,8 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1414
1440
  return;
1415
1441
  if (outcome?.status !== 'not-started')
1416
1442
  return;
1443
+ if (outcome.reason !== 'cancelled')
1444
+ dispatch({ type: 'runError' });
1417
1445
  interactionPhase.current = 'idle';
1418
1446
  resetInterrupt();
1419
1447
  autoContinuationRef.current = false;
@@ -1451,91 +1479,24 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1451
1479
  }
1452
1480
  return submitMessage(trimmed.startsWith('//') ? raw.replace('//', '/') : raw);
1453
1481
  };
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) {
1482
+ const handleProfileSave = (draft) => {
1483
+ const result = controller.saveModelSettings(draft);
1484
+ if (!result.ok)
1485
+ return result;
1486
+ if (draft.patch.showThinking !== undefined
1487
+ && controller.getConfig().showThinking !== stateRef.current.thinkingVisible) {
1468
1488
  dispatch({ type: 'toggleThinking' });
1469
1489
  }
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
1490
  dispatch({ type: 'setOverlay', overlay: null });
1534
- toast(cap.toast?.[0] ?? 'success', cap.toast?.[1] ?? 'Settings saved.');
1491
+ toast(result.restartRequired ? 'warn' : 'success', modelSettingsResultMessage(result));
1492
+ return result;
1535
1493
  };
1536
1494
  const handleModelSelect = (provider, model) => {
1537
- controller.setModel(provider, model);
1538
- persistConfig({ provider, model });
1495
+ const result = controller.saveModelSettingsPatch({ provider, model });
1496
+ if (!result.ok) {
1497
+ toast('error', modelSettingsResultMessage(result));
1498
+ return;
1499
+ }
1539
1500
  dispatch({ type: 'setOverlay', overlay: null });
1540
1501
  toast('info', `Model set to ${provider}:${model}.`);
1541
1502
  };
@@ -1642,6 +1603,10 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1642
1603
  // (`via === 'none'`) still reports through the shared toast funnel and
1643
1604
  // returns: the selection is already consumed, and the NEXT Ctrl+C
1644
1605
  // walks the original ladder.
1606
+ if (key.ctrl && (input === 'c' || input === 'C') && copyCoordinator.isBusy()) {
1607
+ void copyCoordinator.requestCopy({ text: '', lines: 0 });
1608
+ return;
1609
+ }
1645
1610
  if (key.ctrl &&
1646
1611
  (input === 'c' || input === 'C') &&
1647
1612
  (selectionController?.hasPendingSelection() ?? false)) {
@@ -1651,8 +1616,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1651
1616
  clearTimeout(ctrlCTimer.current);
1652
1617
  ctrlCTimer.current = null;
1653
1618
  if (payload) {
1654
- const via = copyText(payload.text, terminal?.writeForeign);
1655
- selectionBridge?.onCopied?.(via, payload.lines, payload.text.length);
1619
+ void copyCoordinator.requestCopy({ text: payload.text, lines: payload.lines });
1656
1620
  }
1657
1621
  return; // A consumed copy intent never becomes exit or service stop.
1658
1622
  }
@@ -1699,7 +1663,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1699
1663
  const overlay = stateRef.current.overlay;
1700
1664
  // Modes A only. `model` / `confirm` manage their own keys; registering an
1701
1665
  // arrow handler for them would double every keypress (R-12).
1702
- const controlled = overlay === 'help' || overlay === 'settings' || overlay === 'plan';
1666
+ const controlled = overlay === 'help' || overlay === 'settings' || overlay === 'plan' || overlay === 'queue';
1703
1667
  if (key.pageUp) {
1704
1668
  if (controlled)
1705
1669
  setOverlayScroll((n) => Math.max(0, n - OVERLAY_PAGE));
@@ -1714,7 +1678,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1714
1678
  // worth more than scrolling six fields (§4.9). `question` keeps them for
1715
1679
  // its option cursor, for the same reason; `plan` has no cursor of its own
1716
1680
  // and scrolls a line at a time.
1717
- if ((overlay === 'help' || overlay === 'plan') && (key.upArrow || key.downArrow)) {
1681
+ if ((overlay === 'help' || overlay === 'plan' || overlay === 'queue') && (key.upArrow || key.downArrow)) {
1718
1682
  setOverlayScroll((n) => Math.max(0, n + (key.upArrow ? -1 : 1)));
1719
1683
  return;
1720
1684
  }
@@ -1809,6 +1773,8 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1809
1773
  }
1810
1774
  if (key.escape) {
1811
1775
  const overlay = stateRef.current.overlay;
1776
+ if (overlay === 'settings')
1777
+ return;
1812
1778
  if (overlay) {
1813
1779
  cancelConfirmation();
1814
1780
  if (overlay === 'confirm' && confirmRef.current) {
@@ -1843,7 +1809,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1843
1809
  interruptGesture.current = next.state;
1844
1810
  if (next.action === 'hint') {
1845
1811
  dispatch({ type: 'pushToast', level: 'info',
1846
- text: 'Press Esc again within 1.5s to interrupt.' });
1812
+ text: '\u4e2d\u65ad\u5df2\u51c6\u5907' });
1847
1813
  return;
1848
1814
  }
1849
1815
  endReasonRef.current.aborted = true;
@@ -1864,7 +1830,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1864
1830
  controller.abort();
1865
1831
  if (sequence === submissionSequence.current && interactionPhase.current === 'running') {
1866
1832
  dispatch({ type: 'pushToast', level: 'info',
1867
- text: 'Interrupt requested. Esc again to force-stop.' });
1833
+ text: '\u6b63\u5728\u505c\u6b62' });
1868
1834
  }
1869
1835
  }, [controller, cancelFollowThrough, resetInterrupt]);
1870
1836
  // --- Render ------------------------------------------------------------
@@ -1875,6 +1841,8 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1875
1841
  // which is what lets the user's `contextWindow` override reach the gauge at
1876
1842
  // all. Nothing else in this render read them.
1877
1843
  const overlay = state.overlay;
1844
+ const settingsDraft = useMemo(() => overlay === 'settings'
1845
+ ? controller.createModelSettingsDraft() : undefined, [controller, overlay]);
1878
1846
  const hasKey = controller.hasApiKey();
1879
1847
  const empty = state.entries.length === 0;
1880
1848
  // RENDER SCOPE, so the chip and the composer hint agree with what the Ctrl+C
@@ -1923,24 +1891,28 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1923
1891
  const nowSec = running || compactionLive ? Math.floor(Date.now() / 1000) : undefined;
1924
1892
  const viewportBudget = computeViewportRows(rows, draftRows);
1925
1893
  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);
1894
+ const composerBaseRows = 2 + clampDraftRows(rows, draftRows);
1895
+ const baseRail = buildTodoRailLayout({
1896
+ cols: cols - 1, panelEnabled: cfg.todo.panel, overlayOpen: false,
1897
+ itemCount: state.todos?.items.length ?? 0, viewportBudget,
1898
+ teamRows: 0, popupRows: 0, composerBaseRows,
1899
+ });
1900
+ const queueLayout = useMemo(() => buildQueueLayout({
1901
+ pending: state.pendingSteering, columns: baseRail.contentCols, terminalRows: rows,
1902
+ availableRows: Math.max(0, viewportBudget - composerBaseRows - 1), paused: !running,
1903
+ }), [state.pendingSteering, baseRail.contentCols, rows, viewportBudget, composerBaseRows, running]);
1933
1904
  const teamLayout = buildTeamPanelLayout({
1934
- snapshot: state.team, terminalRows: rows, availableRows: Math.max(0, viewportBudget - composerBaseRows),
1905
+ snapshot: state.team, terminalRows: rows,
1906
+ availableRows: Math.max(0, viewportBudget - composerBaseRows - queueLayout.rows - 1),
1935
1907
  });
1936
1908
  const railLayout = buildTodoRailLayout({
1937
1909
  cols: cols - 1, panelEnabled: cfg.todo.panel, overlayOpen: false,
1938
1910
  itemCount: state.todos?.items.length ?? 0, viewportBudget,
1939
- teamRows: teamLayout.rowCount, popupRows, composerBaseRows,
1911
+ teamRows: teamLayout.rowCount, queueRows: queueLayout.rows, popupRows, composerBaseRows,
1940
1912
  });
1941
- // Transcript and composer share contentCols; fixed chrome and overlays use terminal width.
1942
1913
  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: {
1914
+ const transcriptEntries = useMemo(() => selectTranscriptEntries(state.entries, state.pendingSteering), [state.entries, state.pendingSteering]);
1915
+ 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
1916
  provider: cfg.provider,
1945
1917
  model: cfg.model,
1946
1918
  baseUrl: cfg.baseUrl ?? '',
@@ -1969,7 +1941,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1969
1941
  // the save handler cannot disagree about what `(inherit)` means.
1970
1942
  ...fastSettingsFrom(cfg.fast),
1971
1943
  ...compactionSettingsFrom(cfg.compaction),
1972
- }, apiKeys: cfg.apiKeys, maxRows: overlayMaxRows, cols: cols, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps, onSave: handleSettingsSave,
1944
+ }, 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
1945
  // Read from `cfg` (render scope, re-read every frame), so `/retry off`
1974
1946
  // while the screen is open shows the new value on the next paint.
1975
1947
  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;
@@ -1977,64 +1949,11 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
1977
1949
  // The wordmark + getting-started card now live INSIDE the viewport, as its
1978
1950
  // first block of content, so they scroll away instead of being yanked out of
1979
1951
  // the header the moment the first message lands.
1980
- const opener = empty ? (_jsx(SessionOpener, { variant: pickOpenerVariant(viewportBudget, contentCols, caps), version: version, cwd: controller.getCwd(), hasKey: hasKey, viewportRows: viewportBudget, theme: theme, caps: caps })) : null;
1952
+ const opener = empty ? (_jsx(SessionOpener, { variant: pickOpenerVariant(viewportBudget, contentCols), version: version, cwd: controller.getCwd(), hasKey: hasKey, viewportRows: viewportBudget, theme: theme, caps: caps })) : null;
1981
1953
  const density = cfg.density;
1982
1954
  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;
1955
+ // The fixed status bar remains visible underneath every overlay and toast.
1956
+ const viewReducedMotion = reducedMotion || running || state.runPhase === 'starting' || compactionLive;
2038
1957
  // Assigned during render so `/perf` reads the CURRENT frame (see `perfRef`).
2039
1958
  // `mountedEntries` is the same number `render-budget.test.tsx` asserts (AC-9).
2040
1959
  perfRef.current = {
@@ -2071,39 +1990,21 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
2071
1990
  // Both keys always WORK (they share one branch in the input handler); this
2072
1991
  // only decides which one the user is taught.
2073
1992
  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' }) }));
1993
+ 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
1994
  const team = state.team ? (_jsx(TeamPanel, { snapshot: state.team, layout: teamLayout, rows: rows, cols: contentCols, reducedMotion: viewReducedMotion, theme: theme, caps: caps })) : null;
2078
1995
  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'
1996
+ 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 })] }));
1997
+ 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'
1998
+ : running ? 'running' : 'idle', cols, completion,
1999
+ selectionPending: selectionController?.hasPendingSelection() ?? false,
2000
+ copyInFlight: copyState.busy, copyCleanupPending: copyState.cleanupPending,
2001
+ overlay, services: liveServices, hintsEnabled: cfg.hints, modeToggleKey,
2002
+ interruptHint: interruptGesture.current.phase === 'stopping'
2003
+ ? 'Esc force-stop' : interruptGesture.current.phase === 'armed'
2004
+ ? 'Esc confirm' : 'Esc x2 interrupt',
2005
+ updateAvailable: !!updateSnapshot && !overlayNode && !running
2006
+ && shouldRenderUpdateLine(updateSnapshot),
2007
+ }, 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
2008
  ? 'Esc force-stop' : 'Esc x2 interrupt', model: cfg.model, provider: cfg.provider, usageTotal: state.usageTotal,
2108
2009
  // ONE OBJECT INSTEAD OF FOUR PARALLEL PROPS (§4.3). The four were a
2109
2010
  // projection of "many sources of truth" into the props layer, and a