@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.
- package/README.md +81 -15
- package/dist/agent/controller.d.ts +29 -1
- package/dist/agent/controller.js +123 -22
- package/dist/agent/model-profile-settings.d.ts +31 -0
- package/dist/agent/model-profile-settings.js +199 -0
- package/dist/agent/queued-messages.d.ts +12 -0
- package/dist/agent/queued-messages.js +18 -0
- package/dist/agent/reducer.d.ts +13 -1
- package/dist/agent/reducer.js +51 -12
- package/dist/cli.js +15 -1
- package/dist/commands/builtins.js +66 -39
- package/dist/commands/queue-command.d.ts +3 -0
- package/dist/commands/queue-command.js +5 -0
- package/dist/commands/registry.d.ts +3 -0
- package/dist/compaction/child.d.ts +4 -2
- package/dist/compaction/child.js +4 -1
- package/dist/compaction/compactor.d.ts +7 -2
- package/dist/compaction/compactor.js +13 -11
- package/dist/compaction/context-command.js +5 -0
- package/dist/compaction/wiring.d.ts +4 -3
- package/dist/compaction/wiring.js +6 -1
- package/dist/config/cli-commands.d.ts +2 -0
- package/dist/config/cli-commands.js +16 -4
- package/dist/config/load.d.ts +2 -1
- package/dist/config/load.js +15 -8
- package/dist/config/model-profile-resolution.d.ts +20 -0
- package/dist/config/model-profile-resolution.js +81 -0
- package/dist/config/model-profile-store.d.ts +67 -0
- package/dist/config/model-profile-store.js +153 -0
- package/dist/config/model-profiles.d.ts +44 -0
- package/dist/config/model-profiles.js +121 -0
- package/dist/config/schema.d.ts +9 -0
- package/dist/config/store.d.ts +11 -1
- package/dist/config/store.js +43 -27
- package/dist/fast/resolve.d.ts +2 -1
- package/dist/fast/resolve.js +6 -4
- package/dist/fast/wiring.d.ts +5 -3
- package/dist/fast/wiring.js +8 -4
- package/dist/input/stdin-filter.js +3 -2
- package/dist/logging/install.js +4 -2
- package/dist/logging/secret-registry.d.ts +2 -0
- package/dist/logging/secret-registry.js +18 -0
- package/dist/team/runtime.d.ts +2 -1
- package/dist/team/subagent.d.ts +3 -1
- package/dist/team/subagent.js +3 -1
- package/dist/ui/ActivityLine.d.ts +6 -70
- package/dist/ui/ActivityLine.js +12 -34
- package/dist/ui/App.d.ts +1 -0
- package/dist/ui/App.js +113 -205
- package/dist/ui/BottomStatusRow.d.ts +6 -86
- package/dist/ui/BottomStatusRow.js +11 -26
- package/dist/ui/Composer.d.ts +16 -83
- package/dist/ui/Composer.js +9 -100
- package/dist/ui/PromptInput.d.ts +4 -1
- package/dist/ui/PromptInput.js +41 -34
- package/dist/ui/QueuePanel.d.ts +8 -0
- package/dist/ui/QueuePanel.js +9 -0
- package/dist/ui/QueueStatusRow.d.ts +2 -0
- package/dist/ui/QueueStatusRow.js +1 -1
- package/dist/ui/StatusBar.d.ts +13 -149
- package/dist/ui/StatusBar.js +27 -142
- package/dist/ui/clipboard-task.d.ts +29 -0
- package/dist/ui/clipboard-task.js +66 -0
- package/dist/ui/clipboard.d.ts +29 -50
- package/dist/ui/clipboard.js +141 -75
- package/dist/ui/composer-limits.d.ts +3 -4
- package/dist/ui/composer-limits.js +4 -9
- package/dist/ui/composer-rows.d.ts +1 -1
- package/dist/ui/composer-rows.js +24 -137
- package/dist/ui/editor-navigation.d.ts +45 -0
- package/dist/ui/editor-navigation.js +85 -0
- package/dist/ui/editor-reducer.d.ts +4 -0
- package/dist/ui/editor-reducer.js +29 -15
- package/dist/ui/interaction-copy.d.ts +77 -0
- package/dist/ui/interaction-copy.js +40 -0
- package/dist/ui/interaction-hints.d.ts +18 -0
- package/dist/ui/interaction-hints.js +92 -0
- package/dist/ui/layout/budget.js +1 -1
- package/dist/ui/layout/queue-layout.d.ts +19 -0
- package/dist/ui/layout/queue-layout.js +26 -0
- package/dist/ui/layout/status-layout.d.ts +55 -0
- package/dist/ui/layout/status-layout.js +109 -0
- package/dist/ui/layout/todo-layout.d.ts +1 -0
- package/dist/ui/layout/todo-layout.js +1 -1
- package/dist/ui/overlays/HelpOverlay.js +8 -8
- package/dist/ui/overlays/ModelProfileEditor.d.ts +12 -0
- package/dist/ui/overlays/ModelProfileEditor.js +103 -0
- package/dist/ui/overlays/ModelProfilePicker.d.ts +35 -0
- package/dist/ui/overlays/ModelProfilePicker.js +167 -0
- package/dist/ui/overlays/QueueOverlay.d.ts +22 -0
- package/dist/ui/overlays/QueueOverlay.js +79 -0
- package/dist/ui/overlays/SettingsScreen.d.ts +9 -2
- package/dist/ui/overlays/SettingsScreen.js +347 -3
- package/dist/ui/overlays/model-profile-state.d.ts +74 -0
- package/dist/ui/overlays/model-profile-state.js +119 -0
- package/dist/ui/run-status-row.d.ts +7 -19
- package/dist/ui/run-status-row.js +2 -18
- package/dist/ui/selection/selection-controller.d.ts +2 -2
- package/dist/ui/selection/selection-controller.js +1 -0
- package/dist/ui/use-wheel-routing.js +2 -1
- 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 {
|
|
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
|
|
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,
|
|
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 {
|
|
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
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
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
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
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(
|
|
1498
|
+
toast(result.restartRequired ? 'warn' : 'success', modelSettingsResultMessage(result));
|
|
1499
|
+
return result;
|
|
1535
1500
|
};
|
|
1536
1501
|
const handleModelSelect = (provider, model) => {
|
|
1537
|
-
controller.
|
|
1538
|
-
|
|
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
|
-
|
|
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: '
|
|
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: '
|
|
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
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
const
|
|
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,
|
|
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
|
|
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,
|
|
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
|
-
//
|
|
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,
|
|
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:
|
|
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,
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
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
|
-
|
|
60
|
-
activity
|
|
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(
|
|
15
|
+
export declare function BottomStatusRow(props: BottomStatusRowProps): React.ReactElement;
|