@aragon-agent/cli 0.6.13 → 0.6.15
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/CHANGELOG.md +41 -0
- package/README.md +250 -222
- package/dist/agent/controller.d.ts +36 -2
- package/dist/agent/controller.js +244 -57
- package/dist/agent/model-profile-settings.js +11 -2
- package/dist/agent/plan-prompt.d.ts +1 -1
- package/dist/agent/plan-prompt.js +4 -3
- package/dist/agent/project-guidance-prompt.d.ts +4 -0
- package/dist/agent/project-guidance-prompt.js +104 -0
- package/dist/agent/reducer.d.ts +2 -0
- package/dist/agent/reducer.js +5 -0
- package/dist/agent/system-prompt.js +4 -0
- package/dist/boot/guard.d.ts +3 -3
- package/dist/boot/guard.js +3 -3
- package/dist/cli.js +33 -15
- package/dist/commands/builtins.js +2 -2
- package/dist/compaction/child.d.ts +9 -1
- package/dist/compaction/child.js +43 -13
- package/dist/compaction/command.js +20 -10
- package/dist/compaction/compactor.d.ts +33 -204
- package/dist/compaction/compactor.js +311 -723
- package/dist/compaction/context-command.js +5 -3
- package/dist/compaction/digest.d.ts +15 -51
- package/dist/compaction/digest.js +113 -148
- package/dist/compaction/limits.d.ts +19 -142
- package/dist/compaction/limits.js +20 -147
- package/dist/compaction/memory-identity.d.ts +16 -0
- package/dist/compaction/memory-identity.js +63 -0
- package/dist/compaction/memory-input.d.ts +25 -0
- package/dist/compaction/memory-input.js +106 -0
- package/dist/compaction/memory-render.d.ts +4 -0
- package/dist/compaction/memory-render.js +47 -0
- package/dist/compaction/memory-validation.d.ts +21 -0
- package/dist/compaction/memory-validation.js +268 -0
- package/dist/compaction/memory.d.ts +79 -0
- package/dist/compaction/memory.js +118 -0
- package/dist/compaction/meter.d.ts +14 -10
- package/dist/compaction/meter.js +55 -19
- package/dist/compaction/operation.d.ts +18 -0
- package/dist/compaction/operation.js +68 -0
- package/dist/compaction/pressure.d.ts +6 -20
- package/dist/compaction/pressure.js +14 -23
- package/dist/compaction/summarize-call.d.ts +3 -16
- package/dist/compaction/summarize-call.js +20 -20
- package/dist/compaction/summary-prompt.d.ts +29 -66
- package/dist/compaction/summary-prompt.js +132 -178
- package/dist/compaction/types.d.ts +23 -3
- package/dist/compaction/wiring.d.ts +10 -25
- package/dist/compaction/wiring.js +60 -67
- package/dist/config/load.d.ts +3 -1
- package/dist/config/load.js +8 -4
- package/dist/config/schema.d.ts +3 -3
- package/dist/config/schema.js +1 -1
- package/dist/exec/events.d.ts +8 -0
- package/dist/exec/index.js +13 -6
- package/dist/exec/runner.d.ts +8 -0
- package/dist/exec/runner.js +26 -2
- package/dist/session/persist.d.ts +3 -0
- package/dist/session/persist.js +9 -0
- package/dist/session/store.js +2 -0
- package/dist/session/validate-session.js +34 -0
- package/dist/team/subagent.js +8 -0
- package/dist/ui/App.js +221 -126
- package/dist/ui/BottomStatusRow.d.ts +5 -10
- package/dist/ui/BottomStatusRow.js +7 -11
- package/dist/ui/Composer.d.ts +4 -2
- package/dist/ui/Composer.js +2 -2
- package/dist/ui/Header.d.ts +6 -2
- package/dist/ui/Header.js +7 -6
- package/dist/ui/Logo.d.ts +3 -5
- package/dist/ui/Logo.js +4 -8
- package/dist/ui/PromptInput.d.ts +3 -1
- package/dist/ui/PromptInput.js +35 -42
- package/dist/ui/StatusBar.d.ts +6 -0
- package/dist/ui/StatusBar.js +8 -7
- package/dist/ui/Transcript.js +1 -1
- package/dist/ui/capabilities.d.ts +2 -0
- package/dist/ui/capabilities.js +6 -0
- package/dist/ui/color-runtime.d.ts +5 -0
- package/dist/ui/color-runtime.js +29 -0
- package/dist/ui/entries/CompactionCard.d.ts +3 -1
- package/dist/ui/entries/CompactionCard.js +7 -5
- package/dist/ui/frame-differ.js +27 -0
- package/dist/ui/frame-observer.js +18 -5
- package/dist/ui/glyphs.js +10 -8
- package/dist/ui/ink-runtime.d.ts +3 -0
- package/dist/ui/ink-runtime.js +30 -0
- package/dist/ui/interaction-copy.d.ts +73 -73
- package/dist/ui/interaction-copy.js +30 -38
- package/dist/ui/interaction-hints.d.ts +8 -1
- package/dist/ui/interaction-hints.js +56 -69
- package/dist/ui/layout/AppShell.d.ts +6 -2
- package/dist/ui/layout/AppShell.js +2 -2
- package/dist/ui/layout/OverlayFrame.d.ts +2 -0
- package/dist/ui/layout/OverlayFrame.js +23 -14
- package/dist/ui/layout/ScrollViewport.d.ts +3 -1
- package/dist/ui/layout/ScrollViewport.js +10 -2
- package/dist/ui/layout/budget.d.ts +25 -26
- package/dist/ui/layout/budget.js +29 -22
- package/dist/ui/layout/document-layout.d.ts +2 -2
- package/dist/ui/layout/document-layout.js +2 -3
- package/dist/ui/layout/queue-layout.js +4 -4
- package/dist/ui/layout/status-detail-layout.d.ts +9 -0
- package/dist/ui/layout/status-detail-layout.js +56 -0
- package/dist/ui/layout/status-layout.d.ts +19 -2
- package/dist/ui/layout/status-layout.js +95 -83
- package/dist/ui/layout/wrap-rows.d.ts +2 -0
- package/dist/ui/layout/wrap-rows.js +21 -7
- package/dist/ui/overlays/ConfirmDialog.d.ts +2 -1
- package/dist/ui/overlays/ConfirmDialog.js +22 -3
- package/dist/ui/overlays/HelpOverlay.js +9 -2
- package/dist/ui/overlays/ModelPicker.d.ts +5 -8
- package/dist/ui/overlays/ModelPicker.js +44 -10
- package/dist/ui/overlays/ModelProfileEditor.js +2 -0
- package/dist/ui/overlays/ModelProfilePicker.d.ts +1 -0
- package/dist/ui/overlays/ModelProfilePicker.js +4 -2
- package/dist/ui/overlays/PlanReviewOverlay.d.ts +2 -1
- package/dist/ui/overlays/PlanReviewOverlay.js +7 -4
- package/dist/ui/overlays/QuestionOverlay.d.ts +2 -1
- package/dist/ui/overlays/QuestionOverlay.js +45 -32
- package/dist/ui/overlays/QueueOverlay.js +2 -2
- package/dist/ui/overlays/SettingsScreen.d.ts +1 -0
- package/dist/ui/overlays/SettingsScreen.js +10 -3
- package/dist/ui/render-cache.d.ts +2 -2
- package/dist/ui/render-cache.js +4 -3
- package/dist/ui/run-metrics.d.ts +23 -0
- package/dist/ui/run-metrics.js +27 -0
- package/dist/ui/status-feedback.d.ts +33 -0
- package/dist/ui/status-feedback.js +36 -0
- package/dist/ui/stdout-frame-writer.js +28 -0
- package/package.json +2 -2
package/dist/ui/App.js
CHANGED
|
@@ -7,10 +7,9 @@ import { selectTranscriptEntries, mergePendingEntries } from '../agent/queued-me
|
|
|
7
7
|
* keybindings, routes slash commands, and renders the header / transcript /
|
|
8
8
|
* input / toast stack / status bar / overlays.
|
|
9
9
|
*/
|
|
10
|
-
import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react';
|
|
11
|
-
import process from 'node:process';
|
|
10
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useReducer, useRef, useState } from 'react';
|
|
12
11
|
import { Text, useApp, useInput, useStdout } from 'ink';
|
|
13
|
-
import { advanceInterruptGesture, cancelInterruptConfirmation, } from '../input/interrupt-gesture.js';
|
|
12
|
+
import { advanceInterruptGesture, INTERRUPT_CONFIRM_MS, cancelInterruptConfirmation, } from '../input/interrupt-gesture.js';
|
|
14
13
|
import { initialViewState, reduceEvent, viewReducer, } from '../agent/reducer.js';
|
|
15
14
|
import { mergeDeltas } from '../agent/coalesce.js';
|
|
16
15
|
import { computeCost } from '../agent/usage.js';
|
|
@@ -22,7 +21,7 @@ import { getLogger } from '../logging/logger.js';
|
|
|
22
21
|
import { isTerminalStatus } from '../proc/types.js';
|
|
23
22
|
import { getSecrets } from '../logging/secret-registry.js';
|
|
24
23
|
import { redactText } from '../logging/redact.js';
|
|
25
|
-
import {
|
|
24
|
+
import { resolveTuiCapabilities } from './capabilities.js';
|
|
26
25
|
import { useStartupNotices, MOUSE_NOTICE_VERSION, VT_INPUT_NOTICE_VERSION, } from './use-startup-notices.js';
|
|
27
26
|
import { getTheme } from './theme.js';
|
|
28
27
|
import { pickGlyphs } from './glyphs.js';
|
|
@@ -36,15 +35,8 @@ import { secondsLeft } from '../agent/retry-view.js';
|
|
|
36
35
|
import { TeamPanel } from './TeamPanel.js';
|
|
37
36
|
import { TodoPanel } from './TodoPanel.js';
|
|
38
37
|
import { BottomStatusRow } from './BottomStatusRow.js';
|
|
39
|
-
|
|
40
|
-
// The updater's two PURE modules. `shouldRenderUpdateLine` decides the row's
|
|
41
|
-
// presence AT THE CALL SITE (C-15), and `UPDATE_LIMITS` owns the width
|
|
42
|
-
// threshold - both are values, and both are resolved here rather than inside
|
|
43
|
-
// `UpdateLine` so that component's only edge into `update/` stays erasable by
|
|
44
|
-
// tsc (cli-auto-update section 3.1 rule 1 / IF-1). Neither module opens a
|
|
45
|
-
// socket, spawns a process or creates a timer.
|
|
38
|
+
// Only the updater's pure visibility predicate enters the status detail plan.
|
|
46
39
|
import { shouldRenderUpdateLine } from '../update/types.js';
|
|
47
|
-
import { UPDATE_LIMITS } from '../update/limits.js';
|
|
48
40
|
import { AppShell } from './layout/AppShell.js';
|
|
49
41
|
import { ScrollViewport } from './layout/ScrollViewport.js';
|
|
50
42
|
import { buildTodoRailLayout } from './layout/todo-layout.js';
|
|
@@ -54,8 +46,7 @@ import { useTerminalSize } from './layout/useTerminalSize.js';
|
|
|
54
46
|
import { useHeightStore } from './use-height-store.js';
|
|
55
47
|
import { useRenderGovernor } from './use-render-governor.js';
|
|
56
48
|
import { readScrollbarPerf, setPerfResetHook, setPerfSnapshotProvider, } from '../commands/perf.js';
|
|
57
|
-
import {
|
|
58
|
-
import { HINT_MIN_ROWS, clampDraftRows, viewportRows as computeViewportRows } from './layout/budget.js';
|
|
49
|
+
import { buildFrameBudget } from './layout/budget.js';
|
|
59
50
|
import { OVERLAY_PAGE, useWheelRouting } from './use-wheel-routing.js';
|
|
60
51
|
import { installConsoleBridge } from './console-bridge.js';
|
|
61
52
|
import { publishExitSnapshot } from './exit-snapshot.js';
|
|
@@ -75,6 +66,9 @@ import { QueuePanel } from './QueuePanel.js';
|
|
|
75
66
|
import { QueueOverlay } from './overlays/QueueOverlay.js';
|
|
76
67
|
import { buildQueueLayout } from './layout/queue-layout.js';
|
|
77
68
|
import { interactionCopy } from './interaction-copy.js';
|
|
69
|
+
import { createRunMetrics, updateRunMetrics, freezeRunMetrics, projectRunMetrics } from './run-metrics.js';
|
|
70
|
+
import { projectStatusFeedback } from './status-feedback.js';
|
|
71
|
+
import { planStatusDetail } from './layout/status-detail-layout.js';
|
|
78
72
|
import { emptyTailState } from './layout/follow-state.js';
|
|
79
73
|
/**
|
|
80
74
|
* Decide what the `Max tokens` field means, WITHOUT letting a bad value in it
|
|
@@ -130,15 +124,13 @@ function seedViewState(thinkingVisible) {
|
|
|
130
124
|
export function App({ controller, version, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, vtInputWarning, updateBridge, pasteBridge, terminal, }) {
|
|
131
125
|
const { exit } = useApp();
|
|
132
126
|
const { stdout } = useStdout();
|
|
133
|
-
const
|
|
134
|
-
const
|
|
135
|
-
const
|
|
136
|
-
const
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
// a decision on its own - always AND it with `runRowEnabled` (`runRowShown`).
|
|
141
|
-
const [activityRowVisible, setActivityRowVisible] = useState(true);
|
|
127
|
+
const size = useTerminalSize();
|
|
128
|
+
const [draftRows, setDraftRows] = useState(1);
|
|
129
|
+
const [popupRows, setPopupRows] = useState(0);
|
|
130
|
+
const [statusExpanded, setStatusExpanded] = useState(false);
|
|
131
|
+
const dimensions = buildFrameBudget({ ...size, draftRows, popupRows, statusExpanded });
|
|
132
|
+
const { rows, cols } = dimensions;
|
|
133
|
+
const tooSmall = dimensions.inactive;
|
|
142
134
|
// --- Render governor (tui-render-performance L4). ------------------------
|
|
143
135
|
//
|
|
144
136
|
// FIRST, BEFORE ANY OTHER WORK IN THIS BODY. The hook stamps
|
|
@@ -156,7 +148,10 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
156
148
|
// frame 1. `seedViewState` is module-scope so the lazy initializer is a stable
|
|
157
149
|
// function reference.
|
|
158
150
|
const [state, dispatch] = useReducer(viewReducer, cfg.showThinking, seedViewState);
|
|
159
|
-
const
|
|
151
|
+
const metrics = useRef(createRunMetrics());
|
|
152
|
+
const manualMetrics = useRef(createRunMetrics());
|
|
153
|
+
const [, setMetricsClock] = useState(0);
|
|
154
|
+
const copyFeedback = useRef(null);
|
|
160
155
|
/**
|
|
161
156
|
* Forces the render that re-reads `Date.now()` for the live compaction card
|
|
162
157
|
* while the run is IDLE (hardening W5). The value itself is never read - the
|
|
@@ -206,13 +201,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
206
201
|
// the completion list are memoized against it, so a skill installed mid-session
|
|
207
202
|
// gets its `/<name>` command and its autocomplete entry immediately.
|
|
208
203
|
const [skillsNonce, setSkillsNonce] = useState(0);
|
|
209
|
-
const caps = useMemo(() => {
|
|
210
|
-
const detected = detectCapabilities(process.env, stdout);
|
|
211
|
-
return {
|
|
212
|
-
colorLevel: cfg.color === false ? 0 : cfg.colorLevel ?? detected.colorLevel,
|
|
213
|
-
unicode: cfg.unicode ?? detected.unicode,
|
|
214
|
-
};
|
|
215
|
-
}, [cfg.color, cfg.colorLevel, cfg.unicode, stdout]);
|
|
204
|
+
const caps = useMemo(() => resolveTuiCapabilities({}, cfg.color), [cfg.color]);
|
|
216
205
|
const theme = useMemo(() => getTheme(cfg.theme, caps), [cfg.theme, caps]);
|
|
217
206
|
const glyphs = useMemo(() => pickGlyphs(caps), [caps]);
|
|
218
207
|
const reducedMotion = cfg.reducedMotion ?? false;
|
|
@@ -227,6 +216,8 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
227
216
|
const interactionPhase = useRef(controller.isRunning() ? 'running' : 'idle');
|
|
228
217
|
const submissionSequence = useRef(0);
|
|
229
218
|
const mounted = useRef(true);
|
|
219
|
+
const switchingConversation = useRef(false);
|
|
220
|
+
const drainCompactionQueue = useRef(() => { });
|
|
230
221
|
/**
|
|
231
222
|
* Live service snapshots, keyed by id, for the status chip and the Ctrl+C rung.
|
|
232
223
|
*
|
|
@@ -246,7 +237,6 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
246
237
|
*/
|
|
247
238
|
const servicesRef = useRef(services);
|
|
248
239
|
servicesRef.current = services;
|
|
249
|
-
const runBaselineOut = useRef(0);
|
|
250
240
|
const runStartedAt = useRef(0);
|
|
251
241
|
const toastTimers = useRef(new Map());
|
|
252
242
|
/** Every outstanding `gate.request()`. Held in a ref, never in state: it is
|
|
@@ -320,11 +310,21 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
320
310
|
aborted: false,
|
|
321
311
|
errored: false,
|
|
322
312
|
});
|
|
313
|
+
const interruptTimer = useRef(null);
|
|
323
314
|
const resetInterrupt = useCallback(() => {
|
|
315
|
+
if (interruptTimer.current)
|
|
316
|
+
clearTimeout(interruptTimer.current);
|
|
317
|
+
interruptTimer.current = null;
|
|
324
318
|
interruptGesture.current = { phase: 'ready' };
|
|
325
319
|
}, []);
|
|
326
320
|
const cancelConfirmation = useCallback(() => {
|
|
321
|
+
if (interruptGesture.current.phase !== 'armed')
|
|
322
|
+
return;
|
|
327
323
|
interruptGesture.current = cancelInterruptConfirmation(interruptGesture.current);
|
|
324
|
+
if (interruptTimer.current)
|
|
325
|
+
clearTimeout(interruptTimer.current);
|
|
326
|
+
interruptTimer.current = null;
|
|
327
|
+
setMetricsClock(Date.now());
|
|
328
328
|
}, []);
|
|
329
329
|
useEffect(() => {
|
|
330
330
|
if (state.overlay)
|
|
@@ -335,12 +335,41 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
335
335
|
return () => {
|
|
336
336
|
mounted.current = false;
|
|
337
337
|
submissionSequence.current += 1;
|
|
338
|
-
if (interactionPhase.current === 'starting')
|
|
338
|
+
if (interactionPhase.current === 'starting' || controller.isCompactionBusy?.())
|
|
339
339
|
controller.abort();
|
|
340
340
|
interactionPhase.current = 'idle';
|
|
341
341
|
resetInterrupt();
|
|
342
342
|
};
|
|
343
343
|
}, [controller, resetInterrupt]);
|
|
344
|
+
useEffect(() => {
|
|
345
|
+
drainCompactionQueue.current = () => {
|
|
346
|
+
// A conversation switch owns the queue until its command has committed.
|
|
347
|
+
void Promise.resolve().then(async () => {
|
|
348
|
+
if (!mounted.current || switchingConversation.current
|
|
349
|
+
|| controller.isCompactionBusy() || controller.isRunning()
|
|
350
|
+
|| interactionPhase.current !== 'idle' || !controller.hasPendingUserMessages()
|
|
351
|
+
|| controller.areModelSettingsBlocked())
|
|
352
|
+
return;
|
|
353
|
+
const preflight = controller.preflight();
|
|
354
|
+
if (!preflight.ok) {
|
|
355
|
+
dispatch({ type: 'notice', level: 'error', text: preflight.message ?? 'Configuration error.' });
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
interactionPhase.current = 'starting';
|
|
359
|
+
try {
|
|
360
|
+
await controller.continue();
|
|
361
|
+
}
|
|
362
|
+
finally {
|
|
363
|
+
if (interactionPhase.current === 'starting')
|
|
364
|
+
interactionPhase.current = 'idle';
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
};
|
|
368
|
+
return controller.subscribeCompactionBusy?.((busy) => {
|
|
369
|
+
if (!busy)
|
|
370
|
+
drainCompactionQueue.current();
|
|
371
|
+
});
|
|
372
|
+
}, [controller]);
|
|
344
373
|
/**
|
|
345
374
|
* Disarm an armed continuation. Idempotent; NEVER touches the budget — a
|
|
346
375
|
* cancelled continuation was never issued, so it must not be charged (AC-18).
|
|
@@ -454,8 +483,11 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
454
483
|
generation = controller.runGeneration;
|
|
455
484
|
return;
|
|
456
485
|
}
|
|
457
|
-
|
|
486
|
+
metrics.current = updateRunMetrics(metrics.current, event, Date.now());
|
|
487
|
+
if (event.type === 'agent_start') {
|
|
458
488
|
interactionPhase.current = 'running';
|
|
489
|
+
manualMetrics.current = createRunMetrics();
|
|
490
|
+
}
|
|
459
491
|
if (event.type === 'agent_end') {
|
|
460
492
|
interactionPhase.current = 'idle';
|
|
461
493
|
resetInterrupt();
|
|
@@ -529,7 +561,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
529
561
|
// a fired continuation would either be swallowed into
|
|
530
562
|
// `controller.steer()` (the quiet failure) or reach
|
|
531
563
|
// `controller.prompt()`, which REJECTS while running.
|
|
532
|
-
if (controller.isRunning())
|
|
564
|
+
if (controller.isRunning() || controller.isCompactionBusy?.())
|
|
533
565
|
return;
|
|
534
566
|
const live = controller.getTodoSnapshot();
|
|
535
567
|
if (!live || live.doneCount >= live.total)
|
|
@@ -776,6 +808,9 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
776
808
|
return controller.subscribeCompaction((event) => {
|
|
777
809
|
switch (event.type) {
|
|
778
810
|
case 'compaction_start':
|
|
811
|
+
if (interactionPhase.current === 'idle') {
|
|
812
|
+
manualMetrics.current = updateRunMetrics(createRunMetrics(), { type: 'agent_start' }, Date.now());
|
|
813
|
+
}
|
|
779
814
|
dispatch({
|
|
780
815
|
type: 'compactionStart',
|
|
781
816
|
index: event.index,
|
|
@@ -784,6 +819,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
784
819
|
});
|
|
785
820
|
break;
|
|
786
821
|
case 'compaction_end':
|
|
822
|
+
manualMetrics.current = freezeRunMetrics(manualMetrics.current, Date.now());
|
|
787
823
|
// THE CARD, AND NOTHING ELSE (context-usage-gauge-accuracy §3.3).
|
|
788
824
|
//
|
|
789
825
|
// A `contextTokensEstimated` dispatch used to live here, hand-building
|
|
@@ -805,10 +841,11 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
805
841
|
// same class of lie as the `$0.00` `pricingUnknown` exists to prevent,
|
|
806
842
|
// just in the other direction.
|
|
807
843
|
const snapshot = controller.getCompactionSnapshot();
|
|
808
|
-
const ref = controller.getCompactionSummarizerRef();
|
|
809
|
-
const costDelta =
|
|
844
|
+
const ref = event.modelRef ?? controller.getCompactionSummarizerRef();
|
|
845
|
+
const costDelta = (event.pricingUnknown ?? snapshot.pricingUnknown)
|
|
810
846
|
? 0
|
|
811
|
-
:
|
|
847
|
+
: event.costUsd ?? (ref
|
|
848
|
+
? computeCost(event.usage, controller.getModelInfoFor(ref).cost) : 0);
|
|
812
849
|
dispatch({ type: 'compactionUsage', usage: event.usage, costDelta });
|
|
813
850
|
break;
|
|
814
851
|
}
|
|
@@ -854,18 +891,21 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
854
891
|
}
|
|
855
892
|
});
|
|
856
893
|
}, [controller, flushPending, governorInterval]);
|
|
857
|
-
//
|
|
894
|
+
// Event boundaries own start/end; ticks only refresh the live projection.
|
|
858
895
|
useEffect(() => {
|
|
859
|
-
if (state.status !== 'running')
|
|
860
|
-
setElapsedMs(0);
|
|
896
|
+
if (state.status !== 'running')
|
|
861
897
|
return;
|
|
862
|
-
|
|
863
|
-
runBaselineOut.current = stateRef.current.usageTotal.outputTokens;
|
|
864
|
-
const startedAt = Date.now();
|
|
865
|
-
setElapsedMs(0);
|
|
866
|
-
const id = setInterval(() => setElapsedMs(Date.now() - startedAt), 200);
|
|
898
|
+
const id = setInterval(() => setMetricsClock(Date.now()), 200);
|
|
867
899
|
return () => clearInterval(id);
|
|
868
900
|
}, [state.status]);
|
|
901
|
+
useLayoutEffect(() => {
|
|
902
|
+
if (manualMetrics.current.startedAt === null || !state.compactionEntryId)
|
|
903
|
+
return;
|
|
904
|
+
const entry = state.entries.find(item => item.id === state.compactionEntryId);
|
|
905
|
+
if (entry?.kind === 'compaction' && entry.startedAt !== undefined) {
|
|
906
|
+
manualMetrics.current = { ...manualMetrics.current, startedAt: entry.startedAt };
|
|
907
|
+
}
|
|
908
|
+
}, [state.compactionEntryId, state.entries]);
|
|
869
909
|
// --- Live compaction elapsed tick (hardening §3.6 / W5). ---------------
|
|
870
910
|
//
|
|
871
911
|
// ONLY WHILE A CARD IS LIVE AND THE RUN IS NOT, so an ordinary session has no
|
|
@@ -912,6 +952,10 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
912
952
|
for (const timer of toastTimers.current.values())
|
|
913
953
|
clearTimeout(timer);
|
|
914
954
|
toastTimers.current.clear();
|
|
955
|
+
if (ctrlCTimer.current)
|
|
956
|
+
clearTimeout(ctrlCTimer.current);
|
|
957
|
+
if (interruptTimer.current)
|
|
958
|
+
clearTimeout(interruptTimer.current);
|
|
915
959
|
}, []);
|
|
916
960
|
// --- Console bridge (fullscreen only, I-4). ----------------------------
|
|
917
961
|
// Ink's own patchConsole writes straight to stdout, which shifts the fixed
|
|
@@ -1084,45 +1128,12 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1084
1128
|
bridge.notify = null;
|
|
1085
1129
|
};
|
|
1086
1130
|
}, [pasteBridge]);
|
|
1087
|
-
//
|
|
1088
|
-
//
|
|
1089
|
-
// GROWTH IS IMMEDIATE, SHRINK IS DEFERRED BY ONE TICK (R-12). Growing late
|
|
1090
|
-
// overdraws the frame for a frame; shrinking early makes a `Backspace` that
|
|
1091
|
-
// crosses a wrap boundary bounce the whole transcript, and a run of deletions
|
|
1092
|
-
// bounce it repeatedly. Deferring coalesces the run into one move.
|
|
1093
|
-
//
|
|
1094
|
-
// The ref mirrors the state so the decision is made OUTSIDE the updater: React
|
|
1095
|
-
// may call an updater more than once, and scheduling a timer from inside one
|
|
1096
|
-
// would arm it twice.
|
|
1097
|
-
const [draftRows, setDraftRows] = useState(1);
|
|
1098
|
-
const [popupRows, setPopupRows] = useState(0);
|
|
1131
|
+
// Synchronous layout reports keep the fixed slot and its contents in one frame.
|
|
1099
1132
|
const onPopupRowsChange = useCallback((next) => {
|
|
1100
1133
|
setPopupRows(previous => previous === next ? previous : next);
|
|
1101
1134
|
}, []);
|
|
1102
|
-
const draftRowsRef = useRef(1);
|
|
1103
|
-
const shrinkTimer = useRef(null);
|
|
1104
1135
|
const onDraftRows = useCallback((next) => {
|
|
1105
|
-
|
|
1106
|
-
if (shrinkTimer.current) {
|
|
1107
|
-
clearTimeout(shrinkTimer.current);
|
|
1108
|
-
shrinkTimer.current = null;
|
|
1109
|
-
}
|
|
1110
|
-
if (wanted === draftRowsRef.current)
|
|
1111
|
-
return;
|
|
1112
|
-
if (wanted > draftRowsRef.current) {
|
|
1113
|
-
draftRowsRef.current = wanted;
|
|
1114
|
-
setDraftRows(wanted);
|
|
1115
|
-
return;
|
|
1116
|
-
}
|
|
1117
|
-
shrinkTimer.current = setTimeout(() => {
|
|
1118
|
-
shrinkTimer.current = null;
|
|
1119
|
-
draftRowsRef.current = wanted;
|
|
1120
|
-
setDraftRows(wanted);
|
|
1121
|
-
}, 0);
|
|
1122
|
-
}, []);
|
|
1123
|
-
useEffect(() => () => {
|
|
1124
|
-
if (shrinkTimer.current)
|
|
1125
|
-
clearTimeout(shrinkTimer.current);
|
|
1136
|
+
setDraftRows(previous => previous === next ? previous : next);
|
|
1126
1137
|
}, []);
|
|
1127
1138
|
// --- Auto-update bridge (cli-auto-update section 3.8 / 6.3). ------------
|
|
1128
1139
|
//
|
|
@@ -1178,26 +1189,26 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1178
1189
|
clipboard: { write: terminal?.writeForeign },
|
|
1179
1190
|
onRequest: () => {
|
|
1180
1191
|
ctrlCArmed.current = false;
|
|
1192
|
+
setMetricsClock(Date.now());
|
|
1181
1193
|
if (ctrlCTimer.current)
|
|
1182
1194
|
clearTimeout(ctrlCTimer.current);
|
|
1183
1195
|
ctrlCTimer.current = null;
|
|
1184
1196
|
},
|
|
1185
1197
|
onStateChange: setCopyState,
|
|
1186
1198
|
onBusy: ({ cleanupPending }) => toast('info', cleanupPending
|
|
1187
|
-
? '
|
|
1199
|
+
? 'Copy failed; cleaning up' : 'Copying'),
|
|
1188
1200
|
onResult: (result, request) => {
|
|
1189
1201
|
selectionBridge?.onCopied?.(result, request.lines, request.text.length);
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1202
|
+
const text = result.status === 'confirmed' ? `Copied ${request.lines} lines`
|
|
1203
|
+
: result.status === 'sent' ? 'Copy sent to terminal; not confirmed'
|
|
1204
|
+
: `${interactionCopy.copyFailed}: ${interactionCopy.copyReasons[result.reason]}`;
|
|
1205
|
+
copyFeedback.current = { result: {
|
|
1206
|
+
status: result.status === 'confirmed' || result.status === 'sent' ? result.status : 'error',
|
|
1207
|
+
text
|
|
1208
|
+
}, previousIds: new Set(stateRef.current.toasts.map(item => item.id)) };
|
|
1209
|
+
toast(result.status === 'confirmed' ? 'success' : result.status === 'sent' ? 'info' : 'warn', text);
|
|
1210
|
+
if (result.status !== 'confirmed' && result.status !== 'sent')
|
|
1199
1211
|
notify('error', text);
|
|
1200
|
-
}
|
|
1201
1212
|
},
|
|
1202
1213
|
}), []);
|
|
1203
1214
|
useEffect(() => () => copyCoordinator.dispose(), [copyCoordinator]);
|
|
@@ -1217,6 +1228,18 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1217
1228
|
// subscription on every render.
|
|
1218
1229
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1219
1230
|
}, [selectionBridge]);
|
|
1231
|
+
useLayoutEffect(() => {
|
|
1232
|
+
const feedback = copyFeedback.current;
|
|
1233
|
+
if (!feedback)
|
|
1234
|
+
return;
|
|
1235
|
+
if (!feedback.toastId) {
|
|
1236
|
+
feedback.toastId = state.toasts.find(item => !feedback.previousIds.has(item.id)
|
|
1237
|
+
&& item.text === feedback.result.text)?.id;
|
|
1238
|
+
}
|
|
1239
|
+
if (feedback.toastId && !state.toasts.some(item => item.id === feedback.toastId)) {
|
|
1240
|
+
copyFeedback.current = null;
|
|
1241
|
+
}
|
|
1242
|
+
}, [state.toasts]);
|
|
1220
1243
|
const selectionController = selectionBridge?.controller ?? null;
|
|
1221
1244
|
/**
|
|
1222
1245
|
* I-9, the DERIVED half — and the one that matters (D-14 / P1-4).
|
|
@@ -1238,6 +1261,12 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1238
1261
|
const onViewportShiftChange = useCallback(() => {
|
|
1239
1262
|
selectionController?.clear();
|
|
1240
1263
|
}, [selectionController]);
|
|
1264
|
+
const onGeometryChange = useCallback(() => {
|
|
1265
|
+
selectionController?.clear();
|
|
1266
|
+
terminal?.scrollbar?.controller?.cancel();
|
|
1267
|
+
terminal?.scrollbar?.invalidate();
|
|
1268
|
+
setSelectionHold(false);
|
|
1269
|
+
}, [selectionController, terminal]);
|
|
1241
1270
|
// I-9, the ENUMERATED half. `shiftUp` covers every movement of the rows and is
|
|
1242
1271
|
// wired straight into `ScrollViewport` below; these three are the cases that
|
|
1243
1272
|
// change the screen while leaving `shiftUp` untouched — a key that scrolls
|
|
@@ -1315,6 +1344,8 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1315
1344
|
state: stateRef.current,
|
|
1316
1345
|
dispatch: (action) => {
|
|
1317
1346
|
if (action.type === 'resetConversation' || action.type === 'restoreEntries') {
|
|
1347
|
+
metrics.current = createRunMetrics();
|
|
1348
|
+
manualMetrics.current = createRunMetrics();
|
|
1318
1349
|
setScrollResetKey((n) => n + 1);
|
|
1319
1350
|
const wasStarting = interactionPhase.current === 'starting';
|
|
1320
1351
|
submissionSequence.current += 1;
|
|
@@ -1404,7 +1435,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1404
1435
|
// must be the one that runs, whichever branch it takes.
|
|
1405
1436
|
cancelFollowThrough();
|
|
1406
1437
|
cancelConfirmation();
|
|
1407
|
-
if (interactionPhase.current === 'running') {
|
|
1438
|
+
if (interactionPhase.current === 'running' || controller.isCompactionBusy?.()) {
|
|
1408
1439
|
try {
|
|
1409
1440
|
const queueId = controller.queueUserMessage(message);
|
|
1410
1441
|
dispatch({ type: 'steerQueued', queueId, text: message });
|
|
@@ -1458,6 +1489,12 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1458
1489
|
};
|
|
1459
1490
|
submitRef.current = submitMessage;
|
|
1460
1491
|
const executeSlashInput = async (raw) => {
|
|
1492
|
+
const switchWhileBusy = controller.isCompactionBusy?.()
|
|
1493
|
+
&& /^\s*\/(clear|reset|resume)(?:\s|$)/.test(raw);
|
|
1494
|
+
if (switchWhileBusy) {
|
|
1495
|
+
switchingConversation.current = true;
|
|
1496
|
+
await controller.cancelCompaction();
|
|
1497
|
+
}
|
|
1461
1498
|
// Retain a recoverable command before any asynchronous handler can fail.
|
|
1462
1499
|
const safe = redactText(raw, getSecrets());
|
|
1463
1500
|
recordPrompt(safe);
|
|
@@ -1469,6 +1506,12 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1469
1506
|
const reason = error instanceof Error ? error.message : String(error);
|
|
1470
1507
|
notify('error', `Command failed: ${redactText(reason, getSecrets())}\nInput: ${safe}`);
|
|
1471
1508
|
}
|
|
1509
|
+
finally {
|
|
1510
|
+
if (switchWhileBusy) {
|
|
1511
|
+
switchingConversation.current = false;
|
|
1512
|
+
drainCompactionQueue.current();
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1472
1515
|
};
|
|
1473
1516
|
const handleSubmit = (raw) => {
|
|
1474
1517
|
const trimmed = raw.trimStart();
|
|
@@ -1619,6 +1662,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1619
1662
|
(selectionController?.hasPendingSelection() ?? false)) {
|
|
1620
1663
|
const payload = selectionController.takeSelection();
|
|
1621
1664
|
ctrlCArmed.current = false;
|
|
1665
|
+
setMetricsClock(Date.now());
|
|
1622
1666
|
if (ctrlCTimer.current)
|
|
1623
1667
|
clearTimeout(ctrlCTimer.current);
|
|
1624
1668
|
ctrlCTimer.current = null;
|
|
@@ -1654,14 +1698,25 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1654
1698
|
return;
|
|
1655
1699
|
}
|
|
1656
1700
|
ctrlCArmed.current = true;
|
|
1657
|
-
|
|
1701
|
+
// Gesture feedback follows its live state, without a second toast lifetime.
|
|
1702
|
+
setMetricsClock(value => value + 1);
|
|
1658
1703
|
if (ctrlCTimer.current)
|
|
1659
1704
|
clearTimeout(ctrlCTimer.current);
|
|
1660
1705
|
ctrlCTimer.current = setTimeout(() => {
|
|
1706
|
+
ctrlCTimer.current = null;
|
|
1661
1707
|
ctrlCArmed.current = false;
|
|
1708
|
+
setMetricsClock(value => value + 1);
|
|
1662
1709
|
}, 1500);
|
|
1663
1710
|
return;
|
|
1664
1711
|
}
|
|
1712
|
+
if (key.ctrl && input.toLowerCase() === 'g') {
|
|
1713
|
+
if (tooSmall || stateRef.current.overlay)
|
|
1714
|
+
return;
|
|
1715
|
+
setStatusExpanded(value => !value);
|
|
1716
|
+
return;
|
|
1717
|
+
}
|
|
1718
|
+
if (tooSmall)
|
|
1719
|
+
return;
|
|
1665
1720
|
// R-P1-7: the overlay branch MUST come before the transcript scroll branch
|
|
1666
1721
|
// and MUST return. `PgUp` used to fall through to `scrollBy()` while an
|
|
1667
1722
|
// overlay was open. The stable viewport ignores inactive intents, but routing
|
|
@@ -1803,6 +1858,10 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1803
1858
|
const handleComposerEscape = useCallback(() => {
|
|
1804
1859
|
if (stateRef.current.overlay)
|
|
1805
1860
|
return;
|
|
1861
|
+
if (controller.isCompactionBusy?.()) {
|
|
1862
|
+
controller.abort();
|
|
1863
|
+
return;
|
|
1864
|
+
}
|
|
1806
1865
|
if (interactionPhase.current === 'idle') {
|
|
1807
1866
|
if (followTimer.current) {
|
|
1808
1867
|
cancelFollowThrough();
|
|
@@ -1815,8 +1874,16 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1815
1874
|
const next = advanceInterruptGesture(interruptGesture.current, performance.now());
|
|
1816
1875
|
interruptGesture.current = next.state;
|
|
1817
1876
|
if (next.action === 'hint') {
|
|
1818
|
-
|
|
1819
|
-
|
|
1877
|
+
if (interruptTimer.current)
|
|
1878
|
+
clearTimeout(interruptTimer.current);
|
|
1879
|
+
interruptTimer.current = setTimeout(() => {
|
|
1880
|
+
interruptTimer.current = null;
|
|
1881
|
+
if (interruptGesture.current.phase !== 'armed')
|
|
1882
|
+
return;
|
|
1883
|
+
interruptGesture.current = { phase: 'ready' };
|
|
1884
|
+
setMetricsClock(Date.now());
|
|
1885
|
+
}, INTERRUPT_CONFIRM_MS + 1);
|
|
1886
|
+
setMetricsClock(value => value + 1);
|
|
1820
1887
|
return;
|
|
1821
1888
|
}
|
|
1822
1889
|
endReasonRef.current.aborted = true;
|
|
@@ -1826,6 +1893,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1826
1893
|
interactionPhase.current = 'idle';
|
|
1827
1894
|
resetInterrupt();
|
|
1828
1895
|
autoContinuationRef.current = false;
|
|
1896
|
+
metrics.current = freezeRunMetrics(metrics.current, Date.now());
|
|
1829
1897
|
if (next.action === 'force-stop')
|
|
1830
1898
|
controller.forceStop();
|
|
1831
1899
|
else
|
|
@@ -1836,8 +1904,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1836
1904
|
}
|
|
1837
1905
|
controller.abort();
|
|
1838
1906
|
if (sequence === submissionSequence.current && interactionPhase.current === 'running') {
|
|
1839
|
-
|
|
1840
|
-
text: '\u6b63\u5728\u505c\u6b62' });
|
|
1907
|
+
setMetricsClock(value => value + 1);
|
|
1841
1908
|
}
|
|
1842
1909
|
}, [controller, cancelFollowThrough, resetInterrupt]);
|
|
1843
1910
|
// --- Render ------------------------------------------------------------
|
|
@@ -1855,9 +1922,9 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1855
1922
|
// RENDER SCOPE, so the chip and the composer hint agree with what the Ctrl+C
|
|
1856
1923
|
// handler will do on the very next keystroke.
|
|
1857
1924
|
const liveServices = liveServiceCount(services);
|
|
1858
|
-
const
|
|
1859
|
-
|
|
1860
|
-
|
|
1925
|
+
const projectedMetrics = projectRunMetrics(manualMetrics.current.startedAt !== null
|
|
1926
|
+
&& state.status !== 'running' ? manualMetrics.current : metrics.current, Date.now());
|
|
1927
|
+
const { elapsedMs, tokPerSec, speedKnown } = projectedMetrics;
|
|
1861
1928
|
// --- The activity line's seed (§3.2.3 / P1-7). --------------------------
|
|
1862
1929
|
//
|
|
1863
1930
|
// RENDER SCOPE. Without this the ref holds `0` on the first run (an arbitrary
|
|
@@ -1896,9 +1963,11 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1896
1963
|
// milliseconds because `ToolCard` is `React.memo`'d with the default
|
|
1897
1964
|
// comparator; `Transcript` passes it on only to running tool entries.
|
|
1898
1965
|
const nowSec = running || compactionLive ? Math.floor(Date.now() / 1000) : undefined;
|
|
1899
|
-
const
|
|
1966
|
+
const frameBudget = buildFrameBudget({ rows, cols, draftRows,
|
|
1967
|
+
popupRows: overlay ? 0 : popupRows, statusExpanded });
|
|
1968
|
+
const viewportBudget = frameBudget.viewportRows;
|
|
1900
1969
|
const overlayMaxRows = viewportBudget;
|
|
1901
|
-
const composerBaseRows =
|
|
1970
|
+
const composerBaseRows = 0;
|
|
1902
1971
|
const baseRail = buildTodoRailLayout({
|
|
1903
1972
|
cols: cols - 1, panelEnabled: cfg.todo.panel, overlayOpen: false,
|
|
1904
1973
|
itemCount: state.todos?.items.length ?? 0, viewportBudget,
|
|
@@ -1915,11 +1984,11 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1915
1984
|
const railLayout = buildTodoRailLayout({
|
|
1916
1985
|
cols: cols - 1, panelEnabled: cfg.todo.panel, overlayOpen: false,
|
|
1917
1986
|
itemCount: state.todos?.items.length ?? 0, viewportBudget,
|
|
1918
|
-
teamRows: teamLayout.rowCount, queueRows: queueLayout.rows, popupRows, composerBaseRows,
|
|
1987
|
+
teamRows: teamLayout.rowCount, queueRows: queueLayout.rows, popupRows: 0, composerBaseRows,
|
|
1919
1988
|
});
|
|
1920
1989
|
const { visible: showRail, width: railWidth, rows: railRows, contentCols } = railLayout;
|
|
1921
1990
|
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: {
|
|
1991
|
+
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, { isActive: !tooSmall, registry: controller.getModelRegistry(), currentProvider: cfg.provider, currentModel: cfg.model, maxRows: overlayMaxRows, cols: cols, theme: theme, caps: caps, onSelect: handleModelSelect })) : overlay === 'settings' ? (_jsx(SettingsScreen, { isActive: !tooSmall, initial: {
|
|
1923
1992
|
provider: cfg.provider,
|
|
1924
1993
|
model: cfg.model,
|
|
1925
1994
|
baseUrl: cfg.baseUrl ?? '',
|
|
@@ -1951,12 +2020,12 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1951
2020
|
}, 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 }),
|
|
1952
2021
|
// Read from `cfg` (render scope, re-read every frame), so `/retry off`
|
|
1953
2022
|
// while the screen is open shows the new value on the next paint.
|
|
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;
|
|
1955
|
-
const header = (_jsx(Header, { cwd: controller.getCwd(), provider: cfg.provider, model: cfg.model, hasKey: hasKey, variant: pickHeaderVariant(cols), theme: theme, caps: caps }));
|
|
2023
|
+
retrySummary: cfg.retry.enabled && cfg.retry.maxRetries > 0 ? String(cfg.retry.maxRetries) : 'off' })) : overlay === 'confirm' && confirmState ? (_jsx(ConfirmDialog, { isActive: !tooSmall, state: confirmState, maxRows: overlayMaxRows, cols: cols, theme: theme, caps: caps, onClose: closeConfirm })) : overlay === 'question' && humanRequest?.kind === 'questions' ? (_jsx(QuestionOverlay, { isActive: !tooSmall, 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, { isActive: !tooSmall, plan: humanRequest.plan, maxRows: overlayMaxRows, cols: cols, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps, onVerdict: handlePlanVerdict })) : null;
|
|
2024
|
+
const header = (_jsx(Header, { columns: cols, statusExpanded: statusExpanded, scrolledLines: scrolledLines, overlayOpen: overlay !== null, cwd: controller.getCwd(), provider: cfg.provider, model: cfg.model, hasKey: hasKey, variant: pickHeaderVariant(cols), theme: theme, caps: caps }));
|
|
1956
2025
|
// The wordmark + getting-started card now live INSIDE the viewport, as its
|
|
1957
2026
|
// first block of content, so they scroll away instead of being yanked out of
|
|
1958
2027
|
// the header the moment the first message lands.
|
|
1959
|
-
const opener = empty ? (_jsx(SessionOpener, { variant: pickOpenerVariant(viewportBudget, contentCols
|
|
2028
|
+
const opener = empty ? (_jsx(SessionOpener, { variant: pickOpenerVariant(viewportBudget, contentCols), version: version, cwd: controller.getCwd(), hasKey: hasKey, viewportRows: viewportBudget, theme: theme, caps: caps })) : null;
|
|
1960
2029
|
const density = cfg.density;
|
|
1961
2030
|
const transcriptWindow = cfg.transcriptWindow ?? DEFAULT_TRANSCRIPT_WINDOW;
|
|
1962
2031
|
// The fixed status bar remains visible underneath every overlay and toast.
|
|
@@ -1985,8 +2054,8 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1985
2054
|
});
|
|
1986
2055
|
},
|
|
1987
2056
|
};
|
|
1988
|
-
// Completion
|
|
1989
|
-
const popupMaxRows =
|
|
2057
|
+
// Completion shares the fixed editor slot and its frame budget.
|
|
2058
|
+
const popupMaxRows = 6;
|
|
1990
2059
|
// WHICH KEY THE HINT ROW NAMES (shift-tab-mode-toggle-still-dead-on-windows,
|
|
1991
2060
|
// C3-1). Derived from `vtInputWarning` rather than from a probe of its own,
|
|
1992
2061
|
// because that prop is already the single answer to "can this console deliver
|
|
@@ -1997,21 +2066,47 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1997
2066
|
// Both keys always WORK (they share one branch in the input handler); this
|
|
1998
2067
|
// only decides which one the user is taught.
|
|
1999
2068
|
const modeToggleKey = vtInputWarning ? MODE_TOGGLE_KEYS.fallback : MODE_TOGGLE_KEYS.primary;
|
|
2000
|
-
const
|
|
2069
|
+
const pendingCopy = copyFeedback.current;
|
|
2070
|
+
const copyResult = pendingCopy && state.toasts.some(item => pendingCopy.toastId
|
|
2071
|
+
? item.id === pendingCopy.toastId
|
|
2072
|
+
: !pendingCopy.previousIds.has(item.id) && item.text === pendingCopy.result.text)
|
|
2073
|
+
? pendingCopy.result : undefined;
|
|
2074
|
+
const latestToast = state.toasts.at(-1);
|
|
2075
|
+
const feedbackProjection = projectStatusFeedback({
|
|
2076
|
+
interactionPhase: interactionPhase.current, interruptPhase: interruptGesture.current.phase,
|
|
2077
|
+
completion, overlay, copyState, copyResult, ctrlCArmed: ctrlCArmed.current,
|
|
2078
|
+
selectionPending: selectionController?.hasPendingSelection() ?? false, liveServices,
|
|
2079
|
+
toast: latestToast ? { text: latestToast.text,
|
|
2080
|
+
level: latestToast.level === 'success' ? 'info' : latestToast.level } : undefined,
|
|
2081
|
+
});
|
|
2082
|
+
const statusInput = {
|
|
2083
|
+
columns: cols, phase: state.runPhase, pendingCount: state.pendingSteering.length,
|
|
2084
|
+
context: state.context, usageTotal: state.usageTotal, thinkingLevel: cfg.thinkingLevel,
|
|
2085
|
+
elapsedMs, tokPerSec, speedKnown, activeTool: state.activeTool, runOutcome: state.runOutcome,
|
|
2086
|
+
waitingForConfirmation: !!humanRequest || !!confirmState,
|
|
2087
|
+
stopping: interruptGesture.current.phase === 'stopping',
|
|
2088
|
+
interruptPhase: interruptGesture.current.phase,
|
|
2089
|
+
compacting: compactionLive || !!state.compaction?.inFlight,
|
|
2090
|
+
mode: state.agentMode, pendingMode: state.pendingAgentMode,
|
|
2091
|
+
servicesActive: { live: liveServices }, ecoRung: governor.rung,
|
|
2092
|
+
teamActive: state.team ? { running: state.team.runs.filter(item => !['queued', 'done', 'failed', 'aborted'].includes(item.phase)).length,
|
|
2093
|
+
total: state.team.runs.length } : undefined,
|
|
2094
|
+
todoActive: state.todos ? { done: state.todos.doneCount, total: state.todos.total } : undefined,
|
|
2095
|
+
fastActive: state.fast?.live ? { inFlight: state.fast.inFlight } : undefined,
|
|
2096
|
+
...feedbackProjection,
|
|
2097
|
+
};
|
|
2098
|
+
const details = planStatusDetail({ status: statusInput, model: cfg.model, provider: cfg.provider,
|
|
2099
|
+
hints: { interactionPhase: interactionPhase.current, cols, completion, overlay,
|
|
2100
|
+
selectionPending: selectionController?.hasPendingSelection() ?? false,
|
|
2101
|
+
copyInFlight: copyState.busy, copyCleanupPending: copyState.cleanupPending,
|
|
2102
|
+
services: liveServices, hintsEnabled: cfg.hints, modeToggleKey,
|
|
2103
|
+
interruptPhase: interruptGesture.current.phase,
|
|
2104
|
+
updateAvailable: !!updateSnapshot && shouldRenderUpdateLine(updateSnapshot) } });
|
|
2105
|
+
const composer = (_jsx(Composer, { deleteDisambiguated: terminal?.deleteDisambiguated ?? false, onCompletionContextChange: setCompletion, cols: frameBudget.composerCols, terminalRows: rows, statusExpanded: statusExpanded, 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: false, submitCount: cfg.submitCount, hintsEnabled: cfg.hints, agentMode: state.agentMode, services: liveServices, modeToggleKey: modeToggleKey, popupMaxRows: popupMaxRows, popupMaxHeight: frameBudget.popupMaxHeight, onPopupRowsChange: onPopupRowsChange, onDraftRows: onDraftRows, onNotice: notify, theme: theme, caps: caps, onSubmit: handleSubmit, onHelp: () => dispatch({ type: 'setOverlay', overlay: 'help' }) }));
|
|
2001
2106
|
const team = state.team ? (_jsx(TeamPanel, { snapshot: state.team, layout: teamLayout, rows: rows, cols: contentCols, reducedMotion: viewReducedMotion, theme: theme, caps: caps })) : null;
|
|
2002
2107
|
const rail = showRail && state.todos ? (_jsx(TodoPanel, { snapshot: state.todos, width: railWidth, rows: railRows, running: running, reducedMotion: viewReducedMotion, theme: theme, caps: caps })) : null;
|
|
2003
|
-
const viewport = (_jsxs(ScrollViewport, { active: overlay === null && !tooSmall, overlay:
|
|
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,
|
|
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'
|
|
2108
|
+
const viewport = (_jsxs(ScrollViewport, { active: overlay === null && !tooSmall, overlay: overlayNode, footer: _jsxs(_Fragment, { children: [team, _jsx(QueuePanel, { layout: queueLayout, theme: theme })] }), rail: rail, scrollbar: terminal?.scrollbar, resetKey: scrollResetKey, intent: scrollIntent, pinToBottomNonce: pinToBottomNonce, onScrolledLinesChange: setScrolledLines, onViewportShiftChange: onViewportShiftChange, onGeometryChange: onGeometryChange, geometryKey: `${rows}:${cols}:${viewportBudget}:${frameBudget.composerSlotRows}:${frameBudget.statusRows}:${contentCols}:${overlay}:${tooSmall}`, 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 })] }));
|
|
2109
|
+
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, viewportRows: frameBudget.viewportRows, composer: composer, composerSlotRows: frameBudget.composerSlotRows, statusRows: frameBudget.statusRows, details: _jsx(BottomStatusRow, { plan: details, columns: cols, theme: theme }), status: _jsx(StatusBar, { columns: cols, speedKnown: speedKnown, ...feedbackProjection, interruptPhase: interruptGesture.current.phase, 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'
|
|
2015
2110
|
? 'Esc force-stop' : 'Esc x2 interrupt', model: cfg.model, provider: cfg.provider, usageTotal: state.usageTotal,
|
|
2016
2111
|
// ONE OBJECT INSTEAD OF FOUR PARALLEL PROPS (§4.3). The four were a
|
|
2017
2112
|
// projection of "many sources of truth" into the props layer, and a
|
|
@@ -2029,9 +2124,9 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2029
2124
|
}
|
|
2030
2125
|
: {}), ...(state.todos && !showRail
|
|
2031
2126
|
? { todoActive: { done: state.todos.doneCount, total: state.todos.total } }
|
|
2032
|
-
: {}), ...(liveServices > 0 ? { servicesActive: { live: liveServices } } : {}), ...(state.fast?.live ? { fastActive: { inFlight: state.fast.inFlight } } : {}),
|
|
2033
|
-
|
|
2034
|
-
|
|
2127
|
+
: {}), ...(liveServices > 0 ? { servicesActive: { live: liveServices } } : {}), ...(state.fast?.live ? { fastActive: { inFlight: state.fast.inFlight } } : {}),
|
|
2128
|
+
// Manual compaction starts before its next snapshot is published.
|
|
2129
|
+
compactionActive: { inFlight: statusInput.compacting ?? false }, ...(controller.isCompactionEnabled()
|
|
2035
2130
|
? {
|
|
2036
2131
|
gaugeMarks: {
|
|
2037
2132
|
warn: cfg.compaction.warnThreshold * 100,
|