@aragon-agent/cli 0.6.14 → 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 +14 -0
- package/README.md +250 -233
- package/dist/agent/controller.d.ts +27 -1
- package/dist/agent/controller.js +182 -45
- 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 +4 -0
- package/dist/agent/system-prompt.js +4 -0
- package/dist/commands/builtins.js +2 -2
- package/dist/compaction/child.d.ts +9 -1
- package/dist/compaction/child.js +39 -13
- package/dist/compaction/command.js +20 -10
- package/dist/compaction/compactor.d.ts +33 -204
- package/dist/compaction/compactor.js +311 -726
- 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 +53 -19
- package/dist/compaction/operation.d.ts +18 -0
- package/dist/compaction/operation.js +68 -0
- package/dist/compaction/pressure.d.ts +5 -20
- package/dist/compaction/pressure.js +13 -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 +21 -3
- package/dist/compaction/wiring.d.ts +10 -25
- package/dist/compaction/wiring.js +56 -66
- package/dist/config/schema.d.ts +1 -1
- 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 +218 -116
- 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/PromptInput.d.ts +3 -1
- package/dist/ui/PromptInput.js +32 -41
- 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/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/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 -84
- 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/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,9 +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';
|
|
10
|
+
import { useCallback, useEffect, useLayoutEffect, useMemo, useReducer, useRef, useState } from 'react';
|
|
11
11
|
import { Text, useApp, useInput, useStdout } from 'ink';
|
|
12
|
-
import { advanceInterruptGesture, cancelInterruptConfirmation, } from '../input/interrupt-gesture.js';
|
|
12
|
+
import { advanceInterruptGesture, INTERRUPT_CONFIRM_MS, cancelInterruptConfirmation, } from '../input/interrupt-gesture.js';
|
|
13
13
|
import { initialViewState, reduceEvent, viewReducer, } from '../agent/reducer.js';
|
|
14
14
|
import { mergeDeltas } from '../agent/coalesce.js';
|
|
15
15
|
import { computeCost } from '../agent/usage.js';
|
|
@@ -35,15 +35,8 @@ import { secondsLeft } from '../agent/retry-view.js';
|
|
|
35
35
|
import { TeamPanel } from './TeamPanel.js';
|
|
36
36
|
import { TodoPanel } from './TodoPanel.js';
|
|
37
37
|
import { BottomStatusRow } from './BottomStatusRow.js';
|
|
38
|
-
|
|
39
|
-
// The updater's two PURE modules. `shouldRenderUpdateLine` decides the row's
|
|
40
|
-
// presence AT THE CALL SITE (C-15), and `UPDATE_LIMITS` owns the width
|
|
41
|
-
// threshold - both are values, and both are resolved here rather than inside
|
|
42
|
-
// `UpdateLine` so that component's only edge into `update/` stays erasable by
|
|
43
|
-
// tsc (cli-auto-update section 3.1 rule 1 / IF-1). Neither module opens a
|
|
44
|
-
// socket, spawns a process or creates a timer.
|
|
38
|
+
// Only the updater's pure visibility predicate enters the status detail plan.
|
|
45
39
|
import { shouldRenderUpdateLine } from '../update/types.js';
|
|
46
|
-
import { UPDATE_LIMITS } from '../update/limits.js';
|
|
47
40
|
import { AppShell } from './layout/AppShell.js';
|
|
48
41
|
import { ScrollViewport } from './layout/ScrollViewport.js';
|
|
49
42
|
import { buildTodoRailLayout } from './layout/todo-layout.js';
|
|
@@ -53,8 +46,7 @@ import { useTerminalSize } from './layout/useTerminalSize.js';
|
|
|
53
46
|
import { useHeightStore } from './use-height-store.js';
|
|
54
47
|
import { useRenderGovernor } from './use-render-governor.js';
|
|
55
48
|
import { readScrollbarPerf, setPerfResetHook, setPerfSnapshotProvider, } from '../commands/perf.js';
|
|
56
|
-
import {
|
|
57
|
-
import { HINT_MIN_ROWS, clampDraftRows, viewportRows as computeViewportRows } from './layout/budget.js';
|
|
49
|
+
import { buildFrameBudget } from './layout/budget.js';
|
|
58
50
|
import { OVERLAY_PAGE, useWheelRouting } from './use-wheel-routing.js';
|
|
59
51
|
import { installConsoleBridge } from './console-bridge.js';
|
|
60
52
|
import { publishExitSnapshot } from './exit-snapshot.js';
|
|
@@ -74,6 +66,9 @@ import { QueuePanel } from './QueuePanel.js';
|
|
|
74
66
|
import { QueueOverlay } from './overlays/QueueOverlay.js';
|
|
75
67
|
import { buildQueueLayout } from './layout/queue-layout.js';
|
|
76
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';
|
|
77
72
|
import { emptyTailState } from './layout/follow-state.js';
|
|
78
73
|
/**
|
|
79
74
|
* Decide what the `Max tokens` field means, WITHOUT letting a bad value in it
|
|
@@ -129,15 +124,13 @@ function seedViewState(thinkingVisible) {
|
|
|
129
124
|
export function App({ controller, version, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, vtInputWarning, updateBridge, pasteBridge, terminal, }) {
|
|
130
125
|
const { exit } = useApp();
|
|
131
126
|
const { stdout } = useStdout();
|
|
132
|
-
const
|
|
133
|
-
const
|
|
134
|
-
const
|
|
135
|
-
const
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
// a decision on its own - always AND it with `runRowEnabled` (`runRowShown`).
|
|
140
|
-
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;
|
|
141
134
|
// --- Render governor (tui-render-performance L4). ------------------------
|
|
142
135
|
//
|
|
143
136
|
// FIRST, BEFORE ANY OTHER WORK IN THIS BODY. The hook stamps
|
|
@@ -155,7 +148,10 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
155
148
|
// frame 1. `seedViewState` is module-scope so the lazy initializer is a stable
|
|
156
149
|
// function reference.
|
|
157
150
|
const [state, dispatch] = useReducer(viewReducer, cfg.showThinking, seedViewState);
|
|
158
|
-
const
|
|
151
|
+
const metrics = useRef(createRunMetrics());
|
|
152
|
+
const manualMetrics = useRef(createRunMetrics());
|
|
153
|
+
const [, setMetricsClock] = useState(0);
|
|
154
|
+
const copyFeedback = useRef(null);
|
|
159
155
|
/**
|
|
160
156
|
* Forces the render that re-reads `Date.now()` for the live compaction card
|
|
161
157
|
* while the run is IDLE (hardening W5). The value itself is never read - the
|
|
@@ -220,6 +216,8 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
220
216
|
const interactionPhase = useRef(controller.isRunning() ? 'running' : 'idle');
|
|
221
217
|
const submissionSequence = useRef(0);
|
|
222
218
|
const mounted = useRef(true);
|
|
219
|
+
const switchingConversation = useRef(false);
|
|
220
|
+
const drainCompactionQueue = useRef(() => { });
|
|
223
221
|
/**
|
|
224
222
|
* Live service snapshots, keyed by id, for the status chip and the Ctrl+C rung.
|
|
225
223
|
*
|
|
@@ -239,7 +237,6 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
239
237
|
*/
|
|
240
238
|
const servicesRef = useRef(services);
|
|
241
239
|
servicesRef.current = services;
|
|
242
|
-
const runBaselineOut = useRef(0);
|
|
243
240
|
const runStartedAt = useRef(0);
|
|
244
241
|
const toastTimers = useRef(new Map());
|
|
245
242
|
/** Every outstanding `gate.request()`. Held in a ref, never in state: it is
|
|
@@ -313,11 +310,21 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
313
310
|
aborted: false,
|
|
314
311
|
errored: false,
|
|
315
312
|
});
|
|
313
|
+
const interruptTimer = useRef(null);
|
|
316
314
|
const resetInterrupt = useCallback(() => {
|
|
315
|
+
if (interruptTimer.current)
|
|
316
|
+
clearTimeout(interruptTimer.current);
|
|
317
|
+
interruptTimer.current = null;
|
|
317
318
|
interruptGesture.current = { phase: 'ready' };
|
|
318
319
|
}, []);
|
|
319
320
|
const cancelConfirmation = useCallback(() => {
|
|
321
|
+
if (interruptGesture.current.phase !== 'armed')
|
|
322
|
+
return;
|
|
320
323
|
interruptGesture.current = cancelInterruptConfirmation(interruptGesture.current);
|
|
324
|
+
if (interruptTimer.current)
|
|
325
|
+
clearTimeout(interruptTimer.current);
|
|
326
|
+
interruptTimer.current = null;
|
|
327
|
+
setMetricsClock(Date.now());
|
|
321
328
|
}, []);
|
|
322
329
|
useEffect(() => {
|
|
323
330
|
if (state.overlay)
|
|
@@ -328,12 +335,41 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
328
335
|
return () => {
|
|
329
336
|
mounted.current = false;
|
|
330
337
|
submissionSequence.current += 1;
|
|
331
|
-
if (interactionPhase.current === 'starting')
|
|
338
|
+
if (interactionPhase.current === 'starting' || controller.isCompactionBusy?.())
|
|
332
339
|
controller.abort();
|
|
333
340
|
interactionPhase.current = 'idle';
|
|
334
341
|
resetInterrupt();
|
|
335
342
|
};
|
|
336
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]);
|
|
337
373
|
/**
|
|
338
374
|
* Disarm an armed continuation. Idempotent; NEVER touches the budget — a
|
|
339
375
|
* cancelled continuation was never issued, so it must not be charged (AC-18).
|
|
@@ -447,8 +483,11 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
447
483
|
generation = controller.runGeneration;
|
|
448
484
|
return;
|
|
449
485
|
}
|
|
450
|
-
|
|
486
|
+
metrics.current = updateRunMetrics(metrics.current, event, Date.now());
|
|
487
|
+
if (event.type === 'agent_start') {
|
|
451
488
|
interactionPhase.current = 'running';
|
|
489
|
+
manualMetrics.current = createRunMetrics();
|
|
490
|
+
}
|
|
452
491
|
if (event.type === 'agent_end') {
|
|
453
492
|
interactionPhase.current = 'idle';
|
|
454
493
|
resetInterrupt();
|
|
@@ -522,7 +561,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
522
561
|
// a fired continuation would either be swallowed into
|
|
523
562
|
// `controller.steer()` (the quiet failure) or reach
|
|
524
563
|
// `controller.prompt()`, which REJECTS while running.
|
|
525
|
-
if (controller.isRunning())
|
|
564
|
+
if (controller.isRunning() || controller.isCompactionBusy?.())
|
|
526
565
|
return;
|
|
527
566
|
const live = controller.getTodoSnapshot();
|
|
528
567
|
if (!live || live.doneCount >= live.total)
|
|
@@ -769,6 +808,9 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
769
808
|
return controller.subscribeCompaction((event) => {
|
|
770
809
|
switch (event.type) {
|
|
771
810
|
case 'compaction_start':
|
|
811
|
+
if (interactionPhase.current === 'idle') {
|
|
812
|
+
manualMetrics.current = updateRunMetrics(createRunMetrics(), { type: 'agent_start' }, Date.now());
|
|
813
|
+
}
|
|
772
814
|
dispatch({
|
|
773
815
|
type: 'compactionStart',
|
|
774
816
|
index: event.index,
|
|
@@ -777,6 +819,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
777
819
|
});
|
|
778
820
|
break;
|
|
779
821
|
case 'compaction_end':
|
|
822
|
+
manualMetrics.current = freezeRunMetrics(manualMetrics.current, Date.now());
|
|
780
823
|
// THE CARD, AND NOTHING ELSE (context-usage-gauge-accuracy §3.3).
|
|
781
824
|
//
|
|
782
825
|
// A `contextTokensEstimated` dispatch used to live here, hand-building
|
|
@@ -798,10 +841,11 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
798
841
|
// same class of lie as the `$0.00` `pricingUnknown` exists to prevent,
|
|
799
842
|
// just in the other direction.
|
|
800
843
|
const snapshot = controller.getCompactionSnapshot();
|
|
801
|
-
const ref = controller.getCompactionSummarizerRef();
|
|
802
|
-
const costDelta =
|
|
844
|
+
const ref = event.modelRef ?? controller.getCompactionSummarizerRef();
|
|
845
|
+
const costDelta = (event.pricingUnknown ?? snapshot.pricingUnknown)
|
|
803
846
|
? 0
|
|
804
|
-
:
|
|
847
|
+
: event.costUsd ?? (ref
|
|
848
|
+
? computeCost(event.usage, controller.getModelInfoFor(ref).cost) : 0);
|
|
805
849
|
dispatch({ type: 'compactionUsage', usage: event.usage, costDelta });
|
|
806
850
|
break;
|
|
807
851
|
}
|
|
@@ -847,18 +891,21 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
847
891
|
}
|
|
848
892
|
});
|
|
849
893
|
}, [controller, flushPending, governorInterval]);
|
|
850
|
-
//
|
|
894
|
+
// Event boundaries own start/end; ticks only refresh the live projection.
|
|
851
895
|
useEffect(() => {
|
|
852
|
-
if (state.status !== 'running')
|
|
853
|
-
setElapsedMs(0);
|
|
896
|
+
if (state.status !== 'running')
|
|
854
897
|
return;
|
|
855
|
-
|
|
856
|
-
runBaselineOut.current = stateRef.current.usageTotal.outputTokens;
|
|
857
|
-
const startedAt = Date.now();
|
|
858
|
-
setElapsedMs(0);
|
|
859
|
-
const id = setInterval(() => setElapsedMs(Date.now() - startedAt), 200);
|
|
898
|
+
const id = setInterval(() => setMetricsClock(Date.now()), 200);
|
|
860
899
|
return () => clearInterval(id);
|
|
861
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]);
|
|
862
909
|
// --- Live compaction elapsed tick (hardening §3.6 / W5). ---------------
|
|
863
910
|
//
|
|
864
911
|
// ONLY WHILE A CARD IS LIVE AND THE RUN IS NOT, so an ordinary session has no
|
|
@@ -905,6 +952,10 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
905
952
|
for (const timer of toastTimers.current.values())
|
|
906
953
|
clearTimeout(timer);
|
|
907
954
|
toastTimers.current.clear();
|
|
955
|
+
if (ctrlCTimer.current)
|
|
956
|
+
clearTimeout(ctrlCTimer.current);
|
|
957
|
+
if (interruptTimer.current)
|
|
958
|
+
clearTimeout(interruptTimer.current);
|
|
908
959
|
}, []);
|
|
909
960
|
// --- Console bridge (fullscreen only, I-4). ----------------------------
|
|
910
961
|
// Ink's own patchConsole writes straight to stdout, which shifts the fixed
|
|
@@ -1077,45 +1128,12 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1077
1128
|
bridge.notify = null;
|
|
1078
1129
|
};
|
|
1079
1130
|
}, [pasteBridge]);
|
|
1080
|
-
//
|
|
1081
|
-
//
|
|
1082
|
-
// GROWTH IS IMMEDIATE, SHRINK IS DEFERRED BY ONE TICK (R-12). Growing late
|
|
1083
|
-
// overdraws the frame for a frame; shrinking early makes a `Backspace` that
|
|
1084
|
-
// crosses a wrap boundary bounce the whole transcript, and a run of deletions
|
|
1085
|
-
// bounce it repeatedly. Deferring coalesces the run into one move.
|
|
1086
|
-
//
|
|
1087
|
-
// The ref mirrors the state so the decision is made OUTSIDE the updater: React
|
|
1088
|
-
// may call an updater more than once, and scheduling a timer from inside one
|
|
1089
|
-
// would arm it twice.
|
|
1090
|
-
const [draftRows, setDraftRows] = useState(1);
|
|
1091
|
-
const [popupRows, setPopupRows] = useState(0);
|
|
1131
|
+
// Synchronous layout reports keep the fixed slot and its contents in one frame.
|
|
1092
1132
|
const onPopupRowsChange = useCallback((next) => {
|
|
1093
1133
|
setPopupRows(previous => previous === next ? previous : next);
|
|
1094
1134
|
}, []);
|
|
1095
|
-
const draftRowsRef = useRef(1);
|
|
1096
|
-
const shrinkTimer = useRef(null);
|
|
1097
1135
|
const onDraftRows = useCallback((next) => {
|
|
1098
|
-
|
|
1099
|
-
if (shrinkTimer.current) {
|
|
1100
|
-
clearTimeout(shrinkTimer.current);
|
|
1101
|
-
shrinkTimer.current = null;
|
|
1102
|
-
}
|
|
1103
|
-
if (wanted === draftRowsRef.current)
|
|
1104
|
-
return;
|
|
1105
|
-
if (wanted > draftRowsRef.current) {
|
|
1106
|
-
draftRowsRef.current = wanted;
|
|
1107
|
-
setDraftRows(wanted);
|
|
1108
|
-
return;
|
|
1109
|
-
}
|
|
1110
|
-
shrinkTimer.current = setTimeout(() => {
|
|
1111
|
-
shrinkTimer.current = null;
|
|
1112
|
-
draftRowsRef.current = wanted;
|
|
1113
|
-
setDraftRows(wanted);
|
|
1114
|
-
}, 0);
|
|
1115
|
-
}, []);
|
|
1116
|
-
useEffect(() => () => {
|
|
1117
|
-
if (shrinkTimer.current)
|
|
1118
|
-
clearTimeout(shrinkTimer.current);
|
|
1136
|
+
setDraftRows(previous => previous === next ? previous : next);
|
|
1119
1137
|
}, []);
|
|
1120
1138
|
// --- Auto-update bridge (cli-auto-update section 3.8 / 6.3). ------------
|
|
1121
1139
|
//
|
|
@@ -1171,26 +1189,26 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1171
1189
|
clipboard: { write: terminal?.writeForeign },
|
|
1172
1190
|
onRequest: () => {
|
|
1173
1191
|
ctrlCArmed.current = false;
|
|
1192
|
+
setMetricsClock(Date.now());
|
|
1174
1193
|
if (ctrlCTimer.current)
|
|
1175
1194
|
clearTimeout(ctrlCTimer.current);
|
|
1176
1195
|
ctrlCTimer.current = null;
|
|
1177
1196
|
},
|
|
1178
1197
|
onStateChange: setCopyState,
|
|
1179
1198
|
onBusy: ({ cleanupPending }) => toast('info', cleanupPending
|
|
1180
|
-
? '
|
|
1199
|
+
? 'Copy failed; cleaning up' : 'Copying'),
|
|
1181
1200
|
onResult: (result, request) => {
|
|
1182
1201
|
selectionBridge?.onCopied?.(result, request.lines, request.text.length);
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
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')
|
|
1192
1211
|
notify('error', text);
|
|
1193
|
-
}
|
|
1194
1212
|
},
|
|
1195
1213
|
}), []);
|
|
1196
1214
|
useEffect(() => () => copyCoordinator.dispose(), [copyCoordinator]);
|
|
@@ -1210,6 +1228,18 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1210
1228
|
// subscription on every render.
|
|
1211
1229
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1212
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]);
|
|
1213
1243
|
const selectionController = selectionBridge?.controller ?? null;
|
|
1214
1244
|
/**
|
|
1215
1245
|
* I-9, the DERIVED half — and the one that matters (D-14 / P1-4).
|
|
@@ -1231,6 +1261,12 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1231
1261
|
const onViewportShiftChange = useCallback(() => {
|
|
1232
1262
|
selectionController?.clear();
|
|
1233
1263
|
}, [selectionController]);
|
|
1264
|
+
const onGeometryChange = useCallback(() => {
|
|
1265
|
+
selectionController?.clear();
|
|
1266
|
+
terminal?.scrollbar?.controller?.cancel();
|
|
1267
|
+
terminal?.scrollbar?.invalidate();
|
|
1268
|
+
setSelectionHold(false);
|
|
1269
|
+
}, [selectionController, terminal]);
|
|
1234
1270
|
// I-9, the ENUMERATED half. `shiftUp` covers every movement of the rows and is
|
|
1235
1271
|
// wired straight into `ScrollViewport` below; these three are the cases that
|
|
1236
1272
|
// change the screen while leaving `shiftUp` untouched — a key that scrolls
|
|
@@ -1308,6 +1344,8 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1308
1344
|
state: stateRef.current,
|
|
1309
1345
|
dispatch: (action) => {
|
|
1310
1346
|
if (action.type === 'resetConversation' || action.type === 'restoreEntries') {
|
|
1347
|
+
metrics.current = createRunMetrics();
|
|
1348
|
+
manualMetrics.current = createRunMetrics();
|
|
1311
1349
|
setScrollResetKey((n) => n + 1);
|
|
1312
1350
|
const wasStarting = interactionPhase.current === 'starting';
|
|
1313
1351
|
submissionSequence.current += 1;
|
|
@@ -1397,7 +1435,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1397
1435
|
// must be the one that runs, whichever branch it takes.
|
|
1398
1436
|
cancelFollowThrough();
|
|
1399
1437
|
cancelConfirmation();
|
|
1400
|
-
if (interactionPhase.current === 'running') {
|
|
1438
|
+
if (interactionPhase.current === 'running' || controller.isCompactionBusy?.()) {
|
|
1401
1439
|
try {
|
|
1402
1440
|
const queueId = controller.queueUserMessage(message);
|
|
1403
1441
|
dispatch({ type: 'steerQueued', queueId, text: message });
|
|
@@ -1451,6 +1489,12 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1451
1489
|
};
|
|
1452
1490
|
submitRef.current = submitMessage;
|
|
1453
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
|
+
}
|
|
1454
1498
|
// Retain a recoverable command before any asynchronous handler can fail.
|
|
1455
1499
|
const safe = redactText(raw, getSecrets());
|
|
1456
1500
|
recordPrompt(safe);
|
|
@@ -1462,6 +1506,12 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1462
1506
|
const reason = error instanceof Error ? error.message : String(error);
|
|
1463
1507
|
notify('error', `Command failed: ${redactText(reason, getSecrets())}\nInput: ${safe}`);
|
|
1464
1508
|
}
|
|
1509
|
+
finally {
|
|
1510
|
+
if (switchWhileBusy) {
|
|
1511
|
+
switchingConversation.current = false;
|
|
1512
|
+
drainCompactionQueue.current();
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1465
1515
|
};
|
|
1466
1516
|
const handleSubmit = (raw) => {
|
|
1467
1517
|
const trimmed = raw.trimStart();
|
|
@@ -1612,6 +1662,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1612
1662
|
(selectionController?.hasPendingSelection() ?? false)) {
|
|
1613
1663
|
const payload = selectionController.takeSelection();
|
|
1614
1664
|
ctrlCArmed.current = false;
|
|
1665
|
+
setMetricsClock(Date.now());
|
|
1615
1666
|
if (ctrlCTimer.current)
|
|
1616
1667
|
clearTimeout(ctrlCTimer.current);
|
|
1617
1668
|
ctrlCTimer.current = null;
|
|
@@ -1647,14 +1698,25 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1647
1698
|
return;
|
|
1648
1699
|
}
|
|
1649
1700
|
ctrlCArmed.current = true;
|
|
1650
|
-
|
|
1701
|
+
// Gesture feedback follows its live state, without a second toast lifetime.
|
|
1702
|
+
setMetricsClock(value => value + 1);
|
|
1651
1703
|
if (ctrlCTimer.current)
|
|
1652
1704
|
clearTimeout(ctrlCTimer.current);
|
|
1653
1705
|
ctrlCTimer.current = setTimeout(() => {
|
|
1706
|
+
ctrlCTimer.current = null;
|
|
1654
1707
|
ctrlCArmed.current = false;
|
|
1708
|
+
setMetricsClock(value => value + 1);
|
|
1655
1709
|
}, 1500);
|
|
1656
1710
|
return;
|
|
1657
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;
|
|
1658
1720
|
// R-P1-7: the overlay branch MUST come before the transcript scroll branch
|
|
1659
1721
|
// and MUST return. `PgUp` used to fall through to `scrollBy()` while an
|
|
1660
1722
|
// overlay was open. The stable viewport ignores inactive intents, but routing
|
|
@@ -1796,6 +1858,10 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1796
1858
|
const handleComposerEscape = useCallback(() => {
|
|
1797
1859
|
if (stateRef.current.overlay)
|
|
1798
1860
|
return;
|
|
1861
|
+
if (controller.isCompactionBusy?.()) {
|
|
1862
|
+
controller.abort();
|
|
1863
|
+
return;
|
|
1864
|
+
}
|
|
1799
1865
|
if (interactionPhase.current === 'idle') {
|
|
1800
1866
|
if (followTimer.current) {
|
|
1801
1867
|
cancelFollowThrough();
|
|
@@ -1808,8 +1874,16 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1808
1874
|
const next = advanceInterruptGesture(interruptGesture.current, performance.now());
|
|
1809
1875
|
interruptGesture.current = next.state;
|
|
1810
1876
|
if (next.action === 'hint') {
|
|
1811
|
-
|
|
1812
|
-
|
|
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);
|
|
1813
1887
|
return;
|
|
1814
1888
|
}
|
|
1815
1889
|
endReasonRef.current.aborted = true;
|
|
@@ -1819,6 +1893,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1819
1893
|
interactionPhase.current = 'idle';
|
|
1820
1894
|
resetInterrupt();
|
|
1821
1895
|
autoContinuationRef.current = false;
|
|
1896
|
+
metrics.current = freezeRunMetrics(metrics.current, Date.now());
|
|
1822
1897
|
if (next.action === 'force-stop')
|
|
1823
1898
|
controller.forceStop();
|
|
1824
1899
|
else
|
|
@@ -1829,8 +1904,7 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1829
1904
|
}
|
|
1830
1905
|
controller.abort();
|
|
1831
1906
|
if (sequence === submissionSequence.current && interactionPhase.current === 'running') {
|
|
1832
|
-
|
|
1833
|
-
text: '\u6b63\u5728\u505c\u6b62' });
|
|
1907
|
+
setMetricsClock(value => value + 1);
|
|
1834
1908
|
}
|
|
1835
1909
|
}, [controller, cancelFollowThrough, resetInterrupt]);
|
|
1836
1910
|
// --- Render ------------------------------------------------------------
|
|
@@ -1848,9 +1922,9 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1848
1922
|
// RENDER SCOPE, so the chip and the composer hint agree with what the Ctrl+C
|
|
1849
1923
|
// handler will do on the very next keystroke.
|
|
1850
1924
|
const liveServices = liveServiceCount(services);
|
|
1851
|
-
const
|
|
1852
|
-
|
|
1853
|
-
|
|
1925
|
+
const projectedMetrics = projectRunMetrics(manualMetrics.current.startedAt !== null
|
|
1926
|
+
&& state.status !== 'running' ? manualMetrics.current : metrics.current, Date.now());
|
|
1927
|
+
const { elapsedMs, tokPerSec, speedKnown } = projectedMetrics;
|
|
1854
1928
|
// --- The activity line's seed (§3.2.3 / P1-7). --------------------------
|
|
1855
1929
|
//
|
|
1856
1930
|
// RENDER SCOPE. Without this the ref holds `0` on the first run (an arbitrary
|
|
@@ -1889,9 +1963,11 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1889
1963
|
// milliseconds because `ToolCard` is `React.memo`'d with the default
|
|
1890
1964
|
// comparator; `Transcript` passes it on only to running tool entries.
|
|
1891
1965
|
const nowSec = running || compactionLive ? Math.floor(Date.now() / 1000) : undefined;
|
|
1892
|
-
const
|
|
1966
|
+
const frameBudget = buildFrameBudget({ rows, cols, draftRows,
|
|
1967
|
+
popupRows: overlay ? 0 : popupRows, statusExpanded });
|
|
1968
|
+
const viewportBudget = frameBudget.viewportRows;
|
|
1893
1969
|
const overlayMaxRows = viewportBudget;
|
|
1894
|
-
const composerBaseRows =
|
|
1970
|
+
const composerBaseRows = 0;
|
|
1895
1971
|
const baseRail = buildTodoRailLayout({
|
|
1896
1972
|
cols: cols - 1, panelEnabled: cfg.todo.panel, overlayOpen: false,
|
|
1897
1973
|
itemCount: state.todos?.items.length ?? 0, viewportBudget,
|
|
@@ -1908,11 +1984,11 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1908
1984
|
const railLayout = buildTodoRailLayout({
|
|
1909
1985
|
cols: cols - 1, panelEnabled: cfg.todo.panel, overlayOpen: false,
|
|
1910
1986
|
itemCount: state.todos?.items.length ?? 0, viewportBudget,
|
|
1911
|
-
teamRows: teamLayout.rowCount, queueRows: queueLayout.rows, popupRows, composerBaseRows,
|
|
1987
|
+
teamRows: teamLayout.rowCount, queueRows: queueLayout.rows, popupRows: 0, composerBaseRows,
|
|
1912
1988
|
});
|
|
1913
1989
|
const { visible: showRail, width: railWidth, rows: railRows, contentCols } = railLayout;
|
|
1914
1990
|
const transcriptEntries = useMemo(() => selectTranscriptEntries(state.entries, state.pendingSteering), [state.entries, state.pendingSteering]);
|
|
1915
|
-
const overlayNode = overlay === 'queue' ? (_jsx(QueueOverlay, { pending: state.pendingSteering, cols: cols, maxRows: overlayMaxRows, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps })) : overlay === 'help' ? (_jsx(OverlayFrame, { title: "Help", hint: `Esc close ${glyphs.midDot} PgUp/PgDn scroll`, maxRows: overlayMaxRows, cols: cols, rows: helpRows(theme, caps), scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps })) : overlay === 'model' ? (_jsx(ModelPicker, { registry: controller.getModelRegistry(), currentProvider: cfg.provider, currentModel: cfg.model, maxRows: overlayMaxRows, cols: cols, theme: theme, caps: caps, onSelect: handleModelSelect })) : overlay === 'settings' ? (_jsx(SettingsScreen, { initial: {
|
|
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: {
|
|
1916
1992
|
provider: cfg.provider,
|
|
1917
1993
|
model: cfg.model,
|
|
1918
1994
|
baseUrl: cfg.baseUrl ?? '',
|
|
@@ -1944,8 +2020,8 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1944
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 }),
|
|
1945
2021
|
// Read from `cfg` (render scope, re-read every frame), so `/retry off`
|
|
1946
2022
|
// while the screen is open shows the new value on the next paint.
|
|
1947
|
-
retrySummary: cfg.retry.enabled && cfg.retry.maxRetries > 0 ? String(cfg.retry.maxRetries) : 'off' })) : overlay === 'confirm' && confirmState ? (_jsx(ConfirmDialog, { state: confirmState, maxRows: overlayMaxRows, cols: cols, theme: theme, caps: caps, onClose: closeConfirm })) : overlay === 'question' && humanRequest?.kind === 'questions' ? (_jsx(QuestionOverlay, { questions: humanRequest.questions, maxRows: overlayMaxRows, cols: cols, theme: theme, caps: caps, onSubmit: (answers) => resolveHuman({ kind: 'answers', answers, cancelled: false }) })) : overlay === 'plan' && humanRequest?.kind === 'plan' ? (_jsx(PlanReviewOverlay, { plan: humanRequest.plan, maxRows: overlayMaxRows, cols: cols, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps, onVerdict: handlePlanVerdict })) : null;
|
|
1948
|
-
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 }));
|
|
1949
2025
|
// The wordmark + getting-started card now live INSIDE the viewport, as its
|
|
1950
2026
|
// first block of content, so they scroll away instead of being yanked out of
|
|
1951
2027
|
// the header the moment the first message lands.
|
|
@@ -1978,8 +2054,8 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1978
2054
|
});
|
|
1979
2055
|
},
|
|
1980
2056
|
};
|
|
1981
|
-
// Completion
|
|
1982
|
-
const popupMaxRows =
|
|
2057
|
+
// Completion shares the fixed editor slot and its frame budget.
|
|
2058
|
+
const popupMaxRows = 6;
|
|
1983
2059
|
// WHICH KEY THE HINT ROW NAMES (shift-tab-mode-toggle-still-dead-on-windows,
|
|
1984
2060
|
// C3-1). Derived from `vtInputWarning` rather than from a probe of its own,
|
|
1985
2061
|
// because that prop is already the single answer to "can this console deliver
|
|
@@ -1990,21 +2066,47 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
1990
2066
|
// Both keys always WORK (they share one branch in the input handler); this
|
|
1991
2067
|
// only decides which one the user is taught.
|
|
1992
2068
|
const modeToggleKey = vtInputWarning ? MODE_TOGGLE_KEYS.fallback : MODE_TOGGLE_KEYS.primary;
|
|
1993
|
-
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' }) }));
|
|
1994
2106
|
const team = state.team ? (_jsx(TeamPanel, { snapshot: state.team, layout: teamLayout, rows: rows, cols: contentCols, reducedMotion: viewReducedMotion, theme: theme, caps: caps })) : null;
|
|
1995
2107
|
const rail = showRail && state.todos ? (_jsx(TodoPanel, { snapshot: state.todos, width: railWidth, rows: railRows, running: running, reducedMotion: viewReducedMotion, theme: theme, caps: caps })) : null;
|
|
1996
|
-
const viewport = (_jsxs(ScrollViewport, { active: overlay === null && !tooSmall, overlay:
|
|
1997
|
-
return (_jsx(AppShell, { inactive: tooSmall, placeholder: _jsxs(Text, { wrap: "truncate", color: theme.noticeWarn, children: [glyphs.warn, " Terminal too small - needs at least 40 columns and 12 rows."] }), rows: rows, cols: cols, header: header, viewport: viewport,
|
|
1998
|
-
: running ? 'running' : 'idle', cols, completion,
|
|
1999
|
-
selectionPending: selectionController?.hasPendingSelection() ?? false,
|
|
2000
|
-
copyInFlight: copyState.busy, copyCleanupPending: copyState.cleanupPending,
|
|
2001
|
-
overlay, services: liveServices, hintsEnabled: cfg.hints, modeToggleKey,
|
|
2002
|
-
interruptHint: interruptGesture.current.phase === 'stopping'
|
|
2003
|
-
? 'Esc force-stop' : interruptGesture.current.phase === 'armed'
|
|
2004
|
-
? 'Esc confirm' : 'Esc x2 interrupt',
|
|
2005
|
-
updateAvailable: !!updateSnapshot && !overlayNode && !running
|
|
2006
|
-
&& shouldRenderUpdateLine(updateSnapshot),
|
|
2007
|
-
}, update: updateSnapshot && !overlayNode && !running && shouldRenderUpdateLine(updateSnapshot) ? (_jsx(UpdateLine, { snapshot: updateSnapshot, compact: cols < UPDATE_LIMITS.statusCompactCols, theme: theme, caps: caps })) : null, theme: theme }), status: _jsx(StatusBar, { pendingSteering: state.pendingSteering, runPhase: state.runPhase, activeTool: state.activeTool, runOutcome: state.runOutcome, reducedMotion: reducedMotion, waitingForConfirmation: !!humanRequest || !!confirmState, stopping: interruptGesture.current.phase === 'stopping', interruptHint: interruptGesture.current.phase === 'stopping'
|
|
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'
|
|
2008
2110
|
? 'Esc force-stop' : 'Esc x2 interrupt', model: cfg.model, provider: cfg.provider, usageTotal: state.usageTotal,
|
|
2009
2111
|
// ONE OBJECT INSTEAD OF FOUR PARALLEL PROPS (§4.3). The four were a
|
|
2010
2112
|
// projection of "many sources of truth" into the props layer, and a
|
|
@@ -2022,9 +2124,9 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2022
2124
|
}
|
|
2023
2125
|
: {}), ...(state.todos && !showRail
|
|
2024
2126
|
? { todoActive: { done: state.todos.doneCount, total: state.todos.total } }
|
|
2025
|
-
: {}), ...(liveServices > 0 ? { servicesActive: { live: liveServices } } : {}), ...(state.fast?.live ? { fastActive: { inFlight: state.fast.inFlight } } : {}),
|
|
2026
|
-
|
|
2027
|
-
|
|
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()
|
|
2028
2130
|
? {
|
|
2029
2131
|
gaugeMarks: {
|
|
2030
2132
|
warn: cfg.compaction.warnThreshold * 100,
|