@aragon-agent/cli 0.6.4 → 0.6.8
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 +37 -0
- package/README.md +163 -37
- package/dist/agent/controller.d.ts +49 -8
- package/dist/agent/controller.js +160 -48
- package/dist/agent/headless.d.ts +2 -1
- package/dist/agent/prompt-options.d.ts +15 -0
- package/dist/agent/prompt-options.js +1 -0
- package/dist/agent/reducer.d.ts +33 -19
- package/dist/agent/reducer.js +62 -71
- package/dist/agent/usage.d.ts +34 -0
- package/dist/agent/usage.js +43 -0
- package/dist/cli.js +131 -54
- package/dist/commands/builtins.js +12 -1
- package/dist/commands/registry.d.ts +2 -3
- package/dist/compaction/compactor.d.ts +60 -24
- package/dist/compaction/compactor.js +89 -61
- package/dist/compaction/context-command.d.ts +33 -0
- package/dist/compaction/context-command.js +166 -0
- package/dist/compaction/meter.d.ts +251 -0
- package/dist/compaction/meter.js +428 -0
- package/dist/compaction/pressure.d.ts +6 -0
- package/dist/compaction/pressure.js +4 -0
- package/dist/compaction/types.d.ts +41 -0
- package/dist/compaction/wiring.d.ts +47 -0
- package/dist/compaction/wiring.js +88 -27
- package/dist/config/cli-commands.js +5 -0
- package/dist/config/env.js +15 -1
- package/dist/config/load.d.ts +17 -0
- package/dist/config/load.js +45 -1
- package/dist/config/schema.d.ts +44 -0
- package/dist/config/schema.js +36 -0
- package/dist/config/store.js +27 -1
- package/dist/exec/events.d.ts +138 -1
- package/dist/exec/events.js +42 -0
- package/dist/exec/index.js +43 -6
- package/dist/exec/progress-reporter.d.ts +23 -0
- package/dist/exec/progress-reporter.js +88 -0
- package/dist/exec/runner.d.ts +34 -0
- package/dist/exec/runner.js +137 -25
- package/dist/fast/reviewer.d.ts +19 -1
- package/dist/fast/reviewer.js +22 -0
- package/dist/fast/types.d.ts +15 -0
- package/dist/fast/wiring.d.ts +10 -1
- package/dist/fast/wiring.js +18 -1
- package/dist/input/interrupt-gesture.d.ts +18 -0
- package/dist/input/interrupt-gesture.js +19 -0
- package/dist/input/pointer-router.d.ts +11 -0
- package/dist/input/pointer-router.js +14 -0
- package/dist/todo/limits.d.ts +4 -17
- package/dist/todo/limits.js +4 -17
- package/dist/todo/panel-rows.d.ts +8 -6
- package/dist/todo/panel-rows.js +19 -6
- package/dist/todo/store.d.ts +8 -13
- package/dist/todo/store.js +13 -14
- package/dist/todo/types.d.ts +2 -1
- package/dist/ui/App.d.ts +2 -0
- package/dist/ui/App.js +233 -183
- package/dist/ui/AutocompletePopup.d.ts +3 -1
- package/dist/ui/AutocompletePopup.js +9 -11
- package/dist/ui/BottomStatusRow.d.ts +1 -1
- package/dist/ui/Composer.d.ts +13 -5
- package/dist/ui/Composer.js +7 -11
- package/dist/ui/PromptCaret.d.ts +11 -0
- package/dist/ui/PromptCaret.js +31 -0
- package/dist/ui/PromptInput.d.ts +12 -6
- package/dist/ui/PromptInput.js +50 -16
- package/dist/ui/StatusBar.d.ts +22 -15
- package/dist/ui/StatusBar.js +61 -16
- package/dist/ui/TeamPanel.d.ts +3 -1
- package/dist/ui/TeamPanel.js +13 -9
- package/dist/ui/TodoPanel.d.ts +5 -5
- package/dist/ui/TodoPanel.js +56 -24
- package/dist/ui/Transcript.js +1 -0
- package/dist/ui/UpdateLine.d.ts +1 -1
- package/dist/ui/capabilities.js +5 -5
- package/dist/ui/composer-rows.js +50 -9
- package/dist/ui/exit-snapshot.d.ts +9 -0
- package/dist/ui/frame-differ.js +2 -19
- package/dist/ui/frame-observer.d.ts +17 -0
- package/dist/ui/frame-observer.js +81 -0
- package/dist/ui/frame-parser.d.ts +7 -0
- package/dist/ui/frame-parser.js +18 -0
- package/dist/ui/layout/AppShell.d.ts +7 -52
- package/dist/ui/layout/AppShell.js +2 -2
- package/dist/ui/layout/ScrollIndicator.d.ts +4 -3
- package/dist/ui/layout/ScrollIndicator.js +4 -4
- package/dist/ui/layout/ScrollViewport.d.ts +13 -66
- package/dist/ui/layout/ScrollViewport.js +183 -185
- package/dist/ui/layout/autocomplete.d.ts +18 -0
- package/dist/ui/layout/autocomplete.js +26 -0
- package/dist/ui/layout/budget.d.ts +3 -51
- package/dist/ui/layout/budget.js +4 -53
- package/dist/ui/layout/document-layout.d.ts +13 -0
- package/dist/ui/layout/document-layout.js +11 -0
- package/dist/ui/layout/follow-state.d.ts +1 -0
- package/dist/ui/layout/follow-state.js +1 -3
- package/dist/ui/layout/rail.d.ts +4 -53
- package/dist/ui/layout/rail.js +10 -62
- package/dist/ui/layout/scroll-indicator.d.ts +1 -1
- package/dist/ui/layout/scroll-indicator.js +3 -2
- package/dist/ui/layout/scrollbar-geometry.d.ts +17 -0
- package/dist/ui/layout/scrollbar-geometry.js +13 -0
- package/dist/ui/layout/team-panel.d.ts +18 -0
- package/dist/ui/layout/team-panel.js +22 -0
- package/dist/ui/layout/todo-layout.d.ts +25 -0
- package/dist/ui/layout/todo-layout.js +28 -0
- package/dist/ui/layout/viewport-geometry.d.ts +1 -0
- package/dist/ui/layout/viewport-geometry.js +1 -0
- package/dist/ui/layout/virtual-window.d.ts +1 -0
- package/dist/ui/layout/virtual-window.js +1 -1
- package/dist/ui/overlays/HelpOverlay.js +5 -2
- package/dist/ui/screen.d.ts +1 -1
- package/dist/ui/screen.js +4 -6
- package/dist/ui/scrollbar-controller.d.ts +30 -0
- package/dist/ui/scrollbar-controller.js +124 -0
- package/dist/ui/transcript-text.js +6 -2
- package/dist/ui/use-wheel-routing.d.ts +2 -0
- package/dist/ui/use-wheel-routing.js +6 -0
- package/package.json +2 -2
package/dist/ui/App.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx,
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
3
|
* App — the root Ink component. Owns the view reducer, subscribes to the
|
|
4
4
|
* controller's event stream (with a streaming coalescer), wires global
|
|
@@ -8,10 +8,10 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
8
8
|
import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react';
|
|
9
9
|
import process from 'node:process';
|
|
10
10
|
import { Box, Text, useApp, useInput, useStdout } from 'ink';
|
|
11
|
+
import { advanceInterruptGesture, cancelInterruptConfirmation, } from '../input/interrupt-gesture.js';
|
|
11
12
|
import { initialViewState, reduceEvent, viewReducer, } from '../agent/reducer.js';
|
|
12
13
|
import { mergeDeltas } from '../agent/coalesce.js';
|
|
13
14
|
import { computeCost } from '../agent/usage.js';
|
|
14
|
-
import { isApproximate } from '../compaction/pressure.js';
|
|
15
15
|
import { parseMaxTokensInput, DEFAULT_TRANSCRIPT_RETAIN, DEFAULT_TRANSCRIPT_WINDOW, } from '../config/schema.js';
|
|
16
16
|
import { updatePersistedConfig } from '../config/store.js';
|
|
17
17
|
import { appendPrompt, loadPromptHistory } from '../config/prompt-history.js';
|
|
@@ -47,15 +47,15 @@ import { shouldRenderUpdateLine } from '../update/types.js';
|
|
|
47
47
|
import { UPDATE_LIMITS } from '../update/limits.js';
|
|
48
48
|
import { AppShell } from './layout/AppShell.js';
|
|
49
49
|
import { ScrollViewport } from './layout/ScrollViewport.js';
|
|
50
|
-
import {
|
|
51
|
-
import {
|
|
50
|
+
import { buildTodoRailLayout } from './layout/todo-layout.js';
|
|
51
|
+
import { buildTeamPanelLayout } from './layout/team-panel.js';
|
|
52
52
|
import { advanceBudget, decideFollowThrough, emptyBudget, } from '../todo/follow-through.js';
|
|
53
53
|
import { useTerminalSize } from './layout/useTerminalSize.js';
|
|
54
54
|
import { useHeightStore } from './use-height-store.js';
|
|
55
55
|
import { useRenderGovernor } from './use-render-governor.js';
|
|
56
56
|
import { setPerfResetHook, setPerfSnapshotProvider } from '../commands/perf.js';
|
|
57
|
-
import {
|
|
58
|
-
import { HINT_MIN_ROWS, viewportRows as computeViewportRows } from './layout/budget.js';
|
|
57
|
+
import { MIN_FULLSCREEN_ROWS, MIN_FULLSCREEN_COLS } from './layout/frame.js';
|
|
58
|
+
import { HINT_MIN_ROWS, clampDraftRows, viewportRows as computeViewportRows } from './layout/budget.js';
|
|
59
59
|
import { OVERLAY_PAGE, useWheelRouting } from './use-wheel-routing.js';
|
|
60
60
|
import { installConsoleBridge } from './console-bridge.js';
|
|
61
61
|
import { publishExitSnapshot } from './exit-snapshot.js';
|
|
@@ -106,15 +106,6 @@ export function resolveSettingsMaxTokens(raw, previous) {
|
|
|
106
106
|
toast: ['warn', 'Max tokens must be a number or "auto".'],
|
|
107
107
|
};
|
|
108
108
|
}
|
|
109
|
-
/**
|
|
110
|
-
* How long rung two of the Esc ladder stays armed after rung one (§5.5).
|
|
111
|
-
*
|
|
112
|
-
* Four seconds: long enough that a user who watched the abort fail can react,
|
|
113
|
-
* short enough that a stray Esc minutes later cannot force-stop a healthy run.
|
|
114
|
-
* `runEnd` / `agent_end` disarm it too, so the window is an upper bound rather
|
|
115
|
-
* than the only protection (R-9).
|
|
116
|
-
*/
|
|
117
|
-
const ESC_ARM_MS = 4000;
|
|
118
109
|
/** Services in a non-terminal state, from the live snapshot map. */
|
|
119
110
|
function liveServiceCount(services) {
|
|
120
111
|
let n = 0;
|
|
@@ -136,6 +127,9 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
136
127
|
const { stdout } = useStdout();
|
|
137
128
|
const fullscreen = mode === 'fullscreen';
|
|
138
129
|
const { rows, cols } = useTerminalSize();
|
|
130
|
+
const tooSmall = fullscreen && (rows < MIN_FULLSCREEN_ROWS || cols < MIN_FULLSCREEN_COLS);
|
|
131
|
+
const composerRef = useRef(null);
|
|
132
|
+
const [cursorVisible, setCursorVisible] = useState(true);
|
|
139
133
|
// --- Render governor (tui-render-performance L4). ------------------------
|
|
140
134
|
//
|
|
141
135
|
// FIRST, BEFORE ANY OTHER WORK IN THIS BODY. The hook stamps
|
|
@@ -178,6 +172,7 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
178
172
|
// Scroll plumbing: intent goes down, one derived display number comes back.
|
|
179
173
|
const [scrollIntent, setScrollIntent] = useState();
|
|
180
174
|
const [pinToBottomNonce, setPinToBottomNonce] = useState(0);
|
|
175
|
+
const [scrollResetKey, setScrollResetKey] = useState(0);
|
|
181
176
|
const [scrolledLines, setScrolledLines] = useState(0);
|
|
182
177
|
/**
|
|
183
178
|
* True while a selection drag holds the viewport
|
|
@@ -217,18 +212,10 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
217
212
|
confirmRef.current = confirmState;
|
|
218
213
|
const ctrlCArmed = useRef(false);
|
|
219
214
|
const ctrlCTimer = useRef(null);
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
* halves matter: a healthy run that ended between the two presses must not be
|
|
225
|
-
* force-stoppable retroactively, and in every healthy run the second press
|
|
226
|
-
* lands on the idle branch and does nothing at all (R-9). Rung two only ever
|
|
227
|
-
* fires while the run is STILL running after rung one - i.e. only when the
|
|
228
|
-
* first press provably failed.
|
|
229
|
-
*/
|
|
230
|
-
const escArmed = useRef(false);
|
|
231
|
-
const escArmTimer = useRef(null);
|
|
215
|
+
const interruptGesture = useRef({ phase: 'ready' });
|
|
216
|
+
const interactionPhase = useRef(controller.isRunning() ? 'running' : 'idle');
|
|
217
|
+
const submissionSequence = useRef(0);
|
|
218
|
+
const mounted = useRef(true);
|
|
232
219
|
/**
|
|
233
220
|
* Live service snapshots, keyed by id, for the status chip and the Ctrl+C rung.
|
|
234
221
|
*
|
|
@@ -332,20 +319,27 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
332
319
|
aborted: false,
|
|
333
320
|
errored: false,
|
|
334
321
|
});
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
*
|
|
338
|
-
* A `useCallback` so the event subscription's dependency list can name it, and
|
|
339
|
-
* so it is one function rather than three copies of "clear the flag and the
|
|
340
|
-
* timer" that can drift.
|
|
341
|
-
*/
|
|
342
|
-
const disarmEsc = useCallback(() => {
|
|
343
|
-
escArmed.current = false;
|
|
344
|
-
if (escArmTimer.current) {
|
|
345
|
-
clearTimeout(escArmTimer.current);
|
|
346
|
-
escArmTimer.current = null;
|
|
347
|
-
}
|
|
322
|
+
const resetInterrupt = useCallback(() => {
|
|
323
|
+
interruptGesture.current = { phase: 'ready' };
|
|
348
324
|
}, []);
|
|
325
|
+
const cancelConfirmation = useCallback(() => {
|
|
326
|
+
interruptGesture.current = cancelInterruptConfirmation(interruptGesture.current);
|
|
327
|
+
}, []);
|
|
328
|
+
useEffect(() => {
|
|
329
|
+
if (state.overlay)
|
|
330
|
+
cancelConfirmation();
|
|
331
|
+
}, [state.overlay, cancelConfirmation]);
|
|
332
|
+
useEffect(() => {
|
|
333
|
+
mounted.current = true;
|
|
334
|
+
return () => {
|
|
335
|
+
mounted.current = false;
|
|
336
|
+
submissionSequence.current += 1;
|
|
337
|
+
if (interactionPhase.current === 'starting')
|
|
338
|
+
controller.abort();
|
|
339
|
+
interactionPhase.current = 'idle';
|
|
340
|
+
resetInterrupt();
|
|
341
|
+
};
|
|
342
|
+
}, [controller, resetInterrupt]);
|
|
349
343
|
/**
|
|
350
344
|
* Disarm an armed continuation. Idempotent; NEVER touches the budget — a
|
|
351
345
|
* cancelled continuation was never issued, so it must not be charged (AC-18).
|
|
@@ -438,7 +432,7 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
438
432
|
// emits while it unwinds - `turn_start`, `tool_call_start`, `turn_end`,
|
|
439
433
|
// `agent_end` - belongs to the run the user just abandoned. `runStart` /
|
|
440
434
|
// `turnStart` would set `status: 'running'` again, bouncing the view straight
|
|
441
|
-
// back out of the `idle`
|
|
435
|
+
// back out of the `idle` force-stop just gave them, and stray entries would
|
|
442
436
|
// append to a transcript they believe is finished.
|
|
443
437
|
//
|
|
444
438
|
// A LOCAL `let`, RE-READ RATHER THAN CAPTURED ONCE: the generation this
|
|
@@ -454,6 +448,12 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
454
448
|
generation = controller.runGeneration;
|
|
455
449
|
return;
|
|
456
450
|
}
|
|
451
|
+
if (event.type === 'agent_start')
|
|
452
|
+
interactionPhase.current = 'running';
|
|
453
|
+
if (event.type === 'agent_end') {
|
|
454
|
+
interactionPhase.current = 'idle';
|
|
455
|
+
resetInterrupt();
|
|
456
|
+
}
|
|
457
457
|
const cost = controller.getModelInfo().cost;
|
|
458
458
|
for (const action of reduceEvent(event, cost, patchSource)) {
|
|
459
459
|
// MUTATED HERE, ONE INSTANT BEFORE THE ACTION THAT SETS
|
|
@@ -480,11 +480,6 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
480
480
|
}
|
|
481
481
|
if (event.type !== 'agent_end')
|
|
482
482
|
return;
|
|
483
|
-
// RUNG TWO DISARMS WITH THE RUN (§3.6 / R-9). A healthy run that ended
|
|
484
|
-
// between the two Esc presses must not be force-stoppable retroactively:
|
|
485
|
-
// the second press then lands on the idle branch, which is what makes the
|
|
486
|
-
// ladder free to take rather than a footgun.
|
|
487
|
-
disarmEsc();
|
|
488
483
|
// NO OVERLAY OUTLIVES ITS RUN (P1-4). `ctx.signal` already covers the
|
|
489
484
|
// abort path, so this is DELIBERATE REDUNDANCY: it makes "the card cannot
|
|
490
485
|
// survive the run that opened it" a property of the App's lifecycle
|
|
@@ -552,7 +547,7 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
552
547
|
done: live.doneCount,
|
|
553
548
|
mode: 'auto',
|
|
554
549
|
});
|
|
555
|
-
submitRef.current(decision.message, { userInitiated: false });
|
|
550
|
+
submitRef.current(decision.message, { userInitiated: false, todoPolicy: 'continue' });
|
|
556
551
|
}, decision.graceMs);
|
|
557
552
|
}
|
|
558
553
|
// A deferred `plan -> build` becomes real here and nowhere else (§3.2).
|
|
@@ -574,7 +569,7 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
574
569
|
cancelFollowThrough();
|
|
575
570
|
unsubscribe();
|
|
576
571
|
};
|
|
577
|
-
}, [controller, patchSource, flushPending, cancelPendingHuman, cancelFollowThrough,
|
|
572
|
+
}, [controller, patchSource, flushPending, cancelPendingHuman, cancelFollowThrough, resetInterrupt]);
|
|
578
573
|
// --- Background services (background-service-supervision §3.8). ---------
|
|
579
574
|
//
|
|
580
575
|
// A SEPARATE SUBSCRIPTION, not a branch in the one above: `ProcEvent` is
|
|
@@ -751,6 +746,27 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
751
746
|
}
|
|
752
747
|
});
|
|
753
748
|
}, [controller]);
|
|
749
|
+
// --- Context occupancy (context-usage-gauge-accuracy §3.3 / I-1). --------
|
|
750
|
+
//
|
|
751
|
+
// A SEVENTH SUBSCRIPTION, AND THE SOLE DISPATCHER OF `contextUsage`. The gauge
|
|
752
|
+
// used to be written from three reducer branches fed by two upstreams, which
|
|
753
|
+
// is what let a stale `snapshot` bounce the bar back to the pre-compaction
|
|
754
|
+
// figure one statement after a compaction correctly dropped it (P0-1). One
|
|
755
|
+
// writer makes that unrepresentable.
|
|
756
|
+
//
|
|
757
|
+
// IT SUBSCRIBES UNCONDITIONALLY. `subscribeContextUsage` is not a compaction
|
|
758
|
+
// forwarder - the meter exists in every session, including one started with
|
|
759
|
+
// `--no-compaction`, which is precisely the session whose gauge previously had
|
|
760
|
+
// one sample per turn and read 0 % after a `/resume`.
|
|
761
|
+
useEffect(() => {
|
|
762
|
+
// SEED ON MOUNT so a resumed session shows a real occupancy on frame 1
|
|
763
|
+
// rather than after its first completed turn. `getContextUsage()` measures
|
|
764
|
+
// on demand, so this works before any event has ever arrived.
|
|
765
|
+
dispatch({ type: 'contextUsage', snapshot: controller.getContextUsage() });
|
|
766
|
+
return controller.subscribeContextUsage((snapshot) => {
|
|
767
|
+
dispatch({ type: 'contextUsage', snapshot });
|
|
768
|
+
});
|
|
769
|
+
}, [controller]);
|
|
754
770
|
// --- Compaction event stream (context-auto-compaction §5.2). -------------
|
|
755
771
|
//
|
|
756
772
|
// A SIXTH SUBSCRIPTION, for the reason the team, todo and fast ones record:
|
|
@@ -787,23 +803,20 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
787
803
|
model: event.model,
|
|
788
804
|
});
|
|
789
805
|
break;
|
|
790
|
-
case 'compaction_end':
|
|
806
|
+
case 'compaction_end':
|
|
807
|
+
// THE CARD, AND NOTHING ELSE (context-usage-gauge-accuracy §3.3).
|
|
808
|
+
//
|
|
809
|
+
// A `contextTokensEstimated` dispatch used to live here, hand-building
|
|
810
|
+
// the post-compaction occupancy out of `record.tokensAfter` plus the
|
|
811
|
+
// snapshot's `estimateOffset`. It is gone because the gauge now has ONE
|
|
812
|
+
// writer: `ContextMeter` learns about the splice from
|
|
813
|
+
// `CompactionWiring.settlePending` itself and re-measures, so the bar
|
|
814
|
+
// falls in the same frame WITHOUT this branch and without the `snapshot`
|
|
815
|
+
// branch below racing it. Those two dispatches were the second and
|
|
816
|
+
// third writers of one number, and the loser of that race was the
|
|
817
|
+
// correct value (P0-1).
|
|
791
818
|
dispatch({ type: 'compactionEnd', record: event.record });
|
|
792
|
-
// THE GAUGE MUST FALL IMMEDIATELY (§6.2 / AC-5), and it must fall to a
|
|
793
|
-
// number in the SAME UNIT as the one it replaces (P1-11). The value
|
|
794
|
-
// being overwritten came from `occupiedTokens(usage)` — a provider
|
|
795
|
-
// measurement that includes the tool schemas — while
|
|
796
|
-
// `record.tokensAfter` comes from core's `estimatePromptTokens`, which
|
|
797
|
-
// never counts them. Writing the raw estimate makes the bar fall too
|
|
798
|
-
// far and then visibly climb again on the next `turn_end` for no reason
|
|
799
|
-
// the user can see, which is the same is-this-thing-working doubt the
|
|
800
|
-
// immediate drop exists to prevent, one turn later.
|
|
801
|
-
if (event.record.applied) {
|
|
802
|
-
const offset = controller.getCompactionSnapshot().pressure.estimateOffset ?? 0;
|
|
803
|
-
dispatch({ type: 'contextTokensEstimated', tokens: event.record.tokensAfter + offset });
|
|
804
|
-
}
|
|
805
819
|
break;
|
|
806
|
-
}
|
|
807
820
|
case 'usage': {
|
|
808
821
|
// Compaction spend is REAL SPEND at the SUMMARIZER's price (§6.4 /
|
|
809
822
|
// AC-15), which may be neither the lead's nor the fast tier's — so the
|
|
@@ -820,24 +833,12 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
820
833
|
break;
|
|
821
834
|
}
|
|
822
835
|
case 'snapshot':
|
|
836
|
+
// THE CHIP, THE COUNTS AND THE SUMMARIZER (§3.3). The occupancy that
|
|
837
|
+
// rides along on `snapshot.pressure` is deliberately NOT dispatched
|
|
838
|
+
// here: the meter that produced it publishes it on its own channel, and
|
|
839
|
+
// a second dispatch of the same number is the second writer this
|
|
840
|
+
// feature removed.
|
|
823
841
|
dispatch({ type: 'compactionSnapshot', snapshot: event.snapshot });
|
|
824
|
-
// ONE NUMBER ON SCREEN AND IN THE TRIGGER (R-11 / hardening §3.2.4).
|
|
825
|
-
// The checkpoint's occupancy is a measured base PLUS an estimate of
|
|
826
|
-
// what was appended after it, and without this the gauge would still
|
|
827
|
-
// show the bare base from the last `turn_end` - so a user watching
|
|
828
|
-
// 76 % would see compaction fire and conclude the feature is broken.
|
|
829
|
-
//
|
|
830
|
-
// `contextTokensEstimated` IS REUSED RATHER THAN A SECOND WRITER ADDED:
|
|
831
|
-
// it is the one existing action for "this figure is derived", and it
|
|
832
|
-
// already sets the `~`. Gated on `isApproximate`, which is precisely
|
|
833
|
-
// the case it was built for - a wholly measured figure belongs to
|
|
834
|
-
// `turnEnd`.
|
|
835
|
-
if (isApproximate(event.snapshot.pressure)) {
|
|
836
|
-
dispatch({
|
|
837
|
-
type: 'contextTokensEstimated',
|
|
838
|
-
tokens: event.snapshot.pressure.occupied,
|
|
839
|
-
});
|
|
840
|
-
}
|
|
841
842
|
break;
|
|
842
843
|
}
|
|
843
844
|
});
|
|
@@ -1128,6 +1129,10 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1128
1129
|
// may call an updater more than once, and scheduling a timer from inside one
|
|
1129
1130
|
// would arm it twice.
|
|
1130
1131
|
const [draftRows, setDraftRows] = useState(1);
|
|
1132
|
+
const [popupRows, setPopupRows] = useState(0);
|
|
1133
|
+
const onPopupRowsChange = useCallback((next) => {
|
|
1134
|
+
setPopupRows(previous => previous === next ? previous : next);
|
|
1135
|
+
}, []);
|
|
1131
1136
|
const draftRowsRef = useRef(1);
|
|
1132
1137
|
const shrinkTimer = useRef(null);
|
|
1133
1138
|
const onDraftRows = useCallback((next) => {
|
|
@@ -1269,9 +1274,9 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1269
1274
|
return;
|
|
1270
1275
|
// An overlay replaces the viewport, so a selection over it would be a
|
|
1271
1276
|
// highlight over rows that are no longer there (N4).
|
|
1272
|
-
selectionController.setEnabled(state.overlay === null);
|
|
1277
|
+
selectionController.setEnabled(state.overlay === null && !tooSmall);
|
|
1273
1278
|
selectionController.clear();
|
|
1274
|
-
}, [selectionController, state.overlay]);
|
|
1279
|
+
}, [selectionController, state.overlay, tooSmall]);
|
|
1275
1280
|
const persistConfig = (patch) => {
|
|
1276
1281
|
try {
|
|
1277
1282
|
updatePersistedConfig(patch);
|
|
@@ -1327,7 +1332,22 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1327
1332
|
args,
|
|
1328
1333
|
controller,
|
|
1329
1334
|
state: stateRef.current,
|
|
1330
|
-
dispatch
|
|
1335
|
+
dispatch: (action) => {
|
|
1336
|
+
if (action.type === 'resetConversation' || action.type === 'restoreEntries') {
|
|
1337
|
+
setScrollResetKey((n) => n + 1);
|
|
1338
|
+
const wasStarting = interactionPhase.current === 'starting';
|
|
1339
|
+
submissionSequence.current += 1;
|
|
1340
|
+
interactionPhase.current = 'idle';
|
|
1341
|
+
resetInterrupt();
|
|
1342
|
+
cancelFollowThrough();
|
|
1343
|
+
autoContinuationRef.current = false;
|
|
1344
|
+
if (wasStarting) {
|
|
1345
|
+
dispatch({ type: 'abortMark' });
|
|
1346
|
+
dispatch({ type: 'runEnd' });
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
dispatch(action);
|
|
1350
|
+
},
|
|
1331
1351
|
setOverlay: (overlay) => dispatch({ type: 'setOverlay', overlay }),
|
|
1332
1352
|
notify,
|
|
1333
1353
|
toast,
|
|
@@ -1392,11 +1412,19 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1392
1412
|
// line, before the steering branch: a message typed during the grace window
|
|
1393
1413
|
// must be the one that runs, whichever branch it takes.
|
|
1394
1414
|
cancelFollowThrough();
|
|
1395
|
-
|
|
1415
|
+
cancelConfirmation();
|
|
1416
|
+
if (interactionPhase.current === 'running') {
|
|
1396
1417
|
controller.steer(message);
|
|
1397
1418
|
toast('info', 'Steering queued.');
|
|
1398
1419
|
return;
|
|
1399
1420
|
}
|
|
1421
|
+
if (interactionPhase.current === 'starting') {
|
|
1422
|
+
toast('info', 'A run is still starting. Press Esc twice to cancel.');
|
|
1423
|
+
return;
|
|
1424
|
+
}
|
|
1425
|
+
resetInterrupt();
|
|
1426
|
+
const sequence = ++submissionSequence.current;
|
|
1427
|
+
interactionPhase.current = 'starting';
|
|
1400
1428
|
// `userInitiated: false` skips TWO things and both are wanted: the
|
|
1401
1429
|
// prompt-history append, and the `submitCount` bump that drives the
|
|
1402
1430
|
// composer's hint fade. An auto-continuation demonstrates nothing about what
|
|
@@ -1411,14 +1439,30 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1411
1439
|
const pre = controller.preflight();
|
|
1412
1440
|
if (!pre.ok) {
|
|
1413
1441
|
notify('error', pre.message ?? 'Configuration error.');
|
|
1442
|
+
interactionPhase.current = 'idle';
|
|
1443
|
+
resetInterrupt();
|
|
1444
|
+
autoContinuationRef.current = false;
|
|
1414
1445
|
dispatch({ type: 'runEnd' }); // reset status; nothing was started
|
|
1415
1446
|
return;
|
|
1416
1447
|
}
|
|
1417
1448
|
// Fire-and-forget: events drive the UI. prompt() never rejects.
|
|
1418
|
-
void controller.prompt(message)
|
|
1449
|
+
void controller.prompt(message, { todoPolicy: opts.todoPolicy ?? 'new-task' }).then((outcome) => {
|
|
1450
|
+
if (!mounted.current || sequence !== submissionSequence.current)
|
|
1451
|
+
return;
|
|
1452
|
+
if (outcome?.status !== 'not-started')
|
|
1453
|
+
return;
|
|
1454
|
+
interactionPhase.current = 'idle';
|
|
1455
|
+
resetInterrupt();
|
|
1456
|
+
autoContinuationRef.current = false;
|
|
1457
|
+
dispatch({ type: 'runEnd' });
|
|
1458
|
+
});
|
|
1419
1459
|
};
|
|
1420
1460
|
submitRef.current = submitMessage;
|
|
1421
1461
|
const handleSubmit = async (raw) => {
|
|
1462
|
+
if (interactionPhase.current === 'running' && /^\s*\/(reset|resume)(?:\s|$)/.test(raw)) {
|
|
1463
|
+
notify('warn', 'Interrupt the current run before switching conversations.');
|
|
1464
|
+
return;
|
|
1465
|
+
}
|
|
1422
1466
|
// Submitting is an unconditional "take me to the newest output" (§4.5).
|
|
1423
1467
|
setPinToBottomNonce((n) => n + 1);
|
|
1424
1468
|
const handled = await runSlashInput(registry, raw, makeCtx);
|
|
@@ -1547,10 +1591,34 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1547
1591
|
// same reason: `OverlayFrame` only ever reports a clamp DOWNWARD.
|
|
1548
1592
|
const { clearCoalescer } = useWheelRouting({
|
|
1549
1593
|
mouseSource,
|
|
1594
|
+
isPointerCaptured: () => terminal?.scrollbar?.controller?.isCaptured() ?? false,
|
|
1595
|
+
isActive: () => !tooSmall,
|
|
1550
1596
|
getOverlay: () => stateRef.current.overlay,
|
|
1551
1597
|
onScroll: scrollBy,
|
|
1552
1598
|
onOverlayScroll: (delta) => setOverlayScroll((n) => Math.max(0, n + delta)),
|
|
1553
1599
|
});
|
|
1600
|
+
const returnToComposer = useCallback(() => {
|
|
1601
|
+
clearCoalescer();
|
|
1602
|
+
terminal?.scrollbar?.controller?.cancel();
|
|
1603
|
+
if (scrolledLines > 0)
|
|
1604
|
+
setPinToBottomNonce((n) => n + 1);
|
|
1605
|
+
}, [clearCoalescer, terminal, scrolledLines]);
|
|
1606
|
+
useEffect(() => {
|
|
1607
|
+
stdout.on('resize', clearCoalescer);
|
|
1608
|
+
return () => { stdout.off('resize', clearCoalescer); };
|
|
1609
|
+
}, [stdout, clearCoalescer]);
|
|
1610
|
+
useEffect(() => {
|
|
1611
|
+
const bridge = terminal?.scrollbar;
|
|
1612
|
+
if (!bridge)
|
|
1613
|
+
return;
|
|
1614
|
+
bridge.onStart = () => { clearCoalescer(); terminal?.selection?.controller?.clear(); };
|
|
1615
|
+
bridge.requestRedraw = () => setRedrawNonce((n) => n + 1);
|
|
1616
|
+
return () => { bridge.onStart = undefined; bridge.requestRedraw = undefined; };
|
|
1617
|
+
}, [terminal, clearCoalescer]);
|
|
1618
|
+
useEffect(() => {
|
|
1619
|
+
clearCoalescer();
|
|
1620
|
+
terminal?.scrollbar?.controller?.cancel();
|
|
1621
|
+
}, [rows, cols, tooSmall, state.overlay, clearCoalescer, terminal]);
|
|
1554
1622
|
// A fresh overlay always opens at the top; carrying the previous overlay's
|
|
1555
1623
|
// offset over would open Help mid-list for no reason the user can see.
|
|
1556
1624
|
//
|
|
@@ -1576,6 +1644,8 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1576
1644
|
}, []);
|
|
1577
1645
|
// --- Global keys -------------------------------------------------------
|
|
1578
1646
|
useInput((input, key) => {
|
|
1647
|
+
if (!key.escape)
|
|
1648
|
+
cancelConfirmation();
|
|
1579
1649
|
// I-9, the third clear trigger, and FIRST IN THE HANDLER so no `return`
|
|
1580
1650
|
// below can skip it. Any key at all drops the selection: most of them move
|
|
1581
1651
|
// the rows, and the ones that do not have still ended the gesture. It is
|
|
@@ -1622,10 +1692,8 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1622
1692
|
}
|
|
1623
1693
|
// R-P1-7: the overlay branch MUST come before the transcript scroll branch
|
|
1624
1694
|
// and MUST return. `PgUp` used to fall through to `scrollBy()` while an
|
|
1625
|
-
// overlay was open
|
|
1626
|
-
//
|
|
1627
|
-
// applied when the overlay closed and the transcript jumped a page on its
|
|
1628
|
-
// own. Even overlays that ignore the key must swallow it here.
|
|
1695
|
+
// overlay was open. The stable viewport ignores inactive intents, but routing
|
|
1696
|
+
// must still give overlays exclusive ownership of their paging keys.
|
|
1629
1697
|
if (stateRef.current.overlay) {
|
|
1630
1698
|
const overlay = stateRef.current.overlay;
|
|
1631
1699
|
// Modes A only. `model` / `confirm` manage their own keys; registering an
|
|
@@ -1748,6 +1816,7 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1748
1816
|
if (key.escape) {
|
|
1749
1817
|
const overlay = stateRef.current.overlay;
|
|
1750
1818
|
if (overlay) {
|
|
1819
|
+
cancelConfirmation();
|
|
1751
1820
|
if (overlay === 'confirm' && confirmRef.current) {
|
|
1752
1821
|
confirmRef.current.resolve(false);
|
|
1753
1822
|
setConfirmState(null);
|
|
@@ -1761,68 +1830,56 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1761
1830
|
dispatch({ type: 'setOverlay', overlay: null });
|
|
1762
1831
|
return;
|
|
1763
1832
|
}
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
// `bash` child (which is what actually unblocks a wedged
|
|
1772
|
-
// `await tool.execute`) and bumps the run generation so the unwinding
|
|
1773
|
-
// engine can no longer move the view. The local `runEnd` is dispatched
|
|
1774
|
-
// UNCONDITIONALLY on top: even if the engine is blocked somewhere
|
|
1775
|
-
// nothing can reach, the view returns to `idle`, the composer is usable,
|
|
1776
|
-
// and the reducer releases every live tail. Making the view's usability
|
|
1777
|
-
// conditional on the engine recovering is what produced the reported
|
|
1778
|
-
// screenshot (D-6).
|
|
1779
|
-
//
|
|
1780
|
-
// AND THE NEXT MESSAGE MUST STILL WORK. `AgentController.prompt()` waits
|
|
1781
|
-
// a still-running engine out and never rejects (I-8) - without that,
|
|
1782
|
-
// "Esc, Esc, then type" would reach `Agent.prompt()`'s synchronous throw,
|
|
1783
|
-
// become an unhandled rejection, and EXIT THE PROCESS (P0-1).
|
|
1784
|
-
if (escArmed.current) {
|
|
1785
|
-
disarmEsc();
|
|
1786
|
-
controller.forceStop();
|
|
1787
|
-
endReasonRef.current.aborted = true;
|
|
1788
|
-
dispatch({ type: 'abortMark' });
|
|
1789
|
-
dispatch({ type: 'runEnd' });
|
|
1790
|
-
toast('warn', 'Force-stopped.');
|
|
1791
|
-
return;
|
|
1792
|
-
}
|
|
1793
|
-
controller.abort();
|
|
1794
|
-
// Next to `abortMark`, in the same callback, for the reason
|
|
1795
|
-
// `endReasonRef`'s own comment gives (C-11). D-5 turns this into
|
|
1796
|
-
// silence at `agent_end`: the user's Esc is the statement, and
|
|
1797
|
-
// restating what they interrupted is the CLI arguing.
|
|
1798
|
-
endReasonRef.current.aborted = true;
|
|
1799
|
-
dispatch({ type: 'abortMark' });
|
|
1800
|
-
escArmed.current = true;
|
|
1801
|
-
if (escArmTimer.current)
|
|
1802
|
-
clearTimeout(escArmTimer.current);
|
|
1803
|
-
escArmTimer.current = setTimeout(() => {
|
|
1804
|
-
escArmed.current = false;
|
|
1805
|
-
escArmTimer.current = null;
|
|
1806
|
-
}, ESC_ARM_MS);
|
|
1807
|
-
toast('info', 'Run aborted. Esc again to force-stop.');
|
|
1808
|
-
return;
|
|
1809
|
-
}
|
|
1810
|
-
// IDLE. Today this branch does nothing at all, which is what makes it free
|
|
1811
|
-
// to take (R-4). OVERLAY PRECEDENCE IS UNCHANGED AND DELIBERATE (P2-9):
|
|
1812
|
-
// the block above returns while an overlay is open, so with one open the
|
|
1813
|
-
// first Esc closes the overlay and the second cancels the continuation.
|
|
1814
|
-
// Reaching past it would make "close this dialog" silently also mean
|
|
1815
|
-
// "abandon the plan".
|
|
1833
|
+
return;
|
|
1834
|
+
}
|
|
1835
|
+
});
|
|
1836
|
+
const handleComposerEscape = useCallback(() => {
|
|
1837
|
+
if (stateRef.current.overlay)
|
|
1838
|
+
return;
|
|
1839
|
+
if (interactionPhase.current === 'idle') {
|
|
1816
1840
|
if (followTimer.current) {
|
|
1817
1841
|
cancelFollowThrough();
|
|
1818
|
-
|
|
1842
|
+
dispatch({ type: 'pushToast', level: 'info', text: 'Auto-continue cancelled.' });
|
|
1819
1843
|
}
|
|
1820
1844
|
return;
|
|
1821
1845
|
}
|
|
1822
|
-
|
|
1846
|
+
const sequence = submissionSequence.current;
|
|
1847
|
+
const phase = interactionPhase.current;
|
|
1848
|
+
const next = advanceInterruptGesture(interruptGesture.current, performance.now());
|
|
1849
|
+
interruptGesture.current = next.state;
|
|
1850
|
+
if (next.action === 'hint') {
|
|
1851
|
+
dispatch({ type: 'pushToast', level: 'info',
|
|
1852
|
+
text: 'Press Esc again within 1.5s to interrupt.' });
|
|
1853
|
+
return;
|
|
1854
|
+
}
|
|
1855
|
+
endReasonRef.current.aborted = true;
|
|
1856
|
+
dispatch({ type: 'abortMark' });
|
|
1857
|
+
if (next.action === 'force-stop' || phase === 'starting') {
|
|
1858
|
+
submissionSequence.current += 1;
|
|
1859
|
+
interactionPhase.current = 'idle';
|
|
1860
|
+
resetInterrupt();
|
|
1861
|
+
autoContinuationRef.current = false;
|
|
1862
|
+
if (next.action === 'force-stop')
|
|
1863
|
+
controller.forceStop();
|
|
1864
|
+
else
|
|
1865
|
+
controller.abort();
|
|
1866
|
+
dispatch({ type: 'runEnd' });
|
|
1867
|
+
dispatch({ type: 'pushToast', level: 'warn', text: next.action === 'force-stop' ? 'Force-stopped.' : 'Pending run cancelled.' });
|
|
1868
|
+
return;
|
|
1869
|
+
}
|
|
1870
|
+
controller.abort();
|
|
1871
|
+
if (sequence === submissionSequence.current && interactionPhase.current === 'running') {
|
|
1872
|
+
dispatch({ type: 'pushToast', level: 'info',
|
|
1873
|
+
text: 'Interrupt requested. Esc again to force-stop.' });
|
|
1874
|
+
}
|
|
1875
|
+
}, [controller, cancelFollowThrough, resetInterrupt]);
|
|
1823
1876
|
// --- Render ------------------------------------------------------------
|
|
1824
|
-
const modelInfo = controller.getModelInfo()
|
|
1825
|
-
|
|
1877
|
+
// `const modelInfo = controller.getModelInfo()` AND ITS `modelKnown` COMPANION
|
|
1878
|
+
// USED TO LIVE HERE (context-usage-gauge-accuracy §4.3). They fed the status
|
|
1879
|
+
// bar's `contextWindow` / `contextWindowKnown` props, which no longer exist:
|
|
1880
|
+
// the denominator and its trustworthiness are resolved inside `ContextMeter`,
|
|
1881
|
+
// which is what lets the user's `contextWindow` override reach the gauge at
|
|
1882
|
+
// all. Nothing else in this render read them.
|
|
1826
1883
|
const overlay = state.overlay;
|
|
1827
1884
|
const hasKey = controller.hasApiKey();
|
|
1828
1885
|
const empty = state.entries.length === 0;
|
|
@@ -1878,22 +1935,20 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1878
1935
|
// and every test that never types byte-identical to before.
|
|
1879
1936
|
const viewportBudget = computeViewportRows(rows, draftRows);
|
|
1880
1937
|
const overlayMaxRows = fullscreen ? viewportBudget : Number.POSITIVE_INFINITY;
|
|
1881
|
-
//
|
|
1882
|
-
//
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
railWidth > 0 && // too narrow (under 80 columns)
|
|
1896
|
-
railRows >= TODO_LIMITS.panelMinRows; // too short to say anything useful
|
|
1938
|
+
// Renderers consume these same projections. Team capacity does not depend on
|
|
1939
|
+
// popup occupancy, so a menu cannot toggle the team's collapsed state.
|
|
1940
|
+
const composerBaseRows = 2 + clampDraftRows(rows, draftRows)
|
|
1941
|
+
+ Number(rows >= HINT_MIN_ROWS && cfg.hints);
|
|
1942
|
+
const teamLayout = buildTeamPanelLayout({
|
|
1943
|
+
snapshot: state.team, terminalRows: rows, availableRows: Math.max(0, viewportBudget - composerBaseRows),
|
|
1944
|
+
});
|
|
1945
|
+
const railLayout = buildTodoRailLayout({
|
|
1946
|
+
mode, cols: fullscreen ? cols - 1 : cols, panelEnabled: cfg.todo.panel, overlayOpen: false,
|
|
1947
|
+
itemCount: state.todos?.items.length ?? 0, viewportBudget,
|
|
1948
|
+
teamRows: teamLayout.rowCount, popupRows, composerBaseRows,
|
|
1949
|
+
});
|
|
1950
|
+
// Transcript and composer share contentCols; fixed chrome and overlays use terminal width.
|
|
1951
|
+
const { visible: showRail, width: railWidth, rows: railRows, contentCols } = railLayout;
|
|
1897
1952
|
// The inline plan strip (todo-plan-followthrough §3.7 / W2).
|
|
1898
1953
|
//
|
|
1899
1954
|
// DELIBERATELY NOT `!showRail`, which is true for FOUR different reasons and
|
|
@@ -1906,12 +1961,6 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1906
1961
|
// needless — this runs in RENDER scope, where `cfg` is re-read every frame.
|
|
1907
1962
|
// The asymmetry with the `agent_end` handler above is the whole of P1-1.
|
|
1908
1963
|
const showStrip = !fullscreen && cfg.todo.panel && state.todos !== null;
|
|
1909
|
-
// Every consumer INSIDE the viewport slot switches to this; the header,
|
|
1910
|
-
// composer, status bar, toasts and every overlay keep the full frame width
|
|
1911
|
-
// (C-7). Overlays keep it because `showRail` already includes `overlay ===
|
|
1912
|
-
// null`: splitting the screen with a status panel behind a modal is both worse
|
|
1913
|
-
// design and a second source of width-contract bugs in six components.
|
|
1914
|
-
const contentCols = cols - (showRail ? railWidth : 0);
|
|
1915
1964
|
const overlayNode = overlay === 'help' ? (_jsx(OverlayFrame, { title: "Help", hint: `Esc close ${glyphs.midDot} PgUp/PgDn scroll`, maxRows: overlayMaxRows, cols: cols, rows: helpRows(theme, caps), scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps })) : overlay === 'model' ? (_jsx(ModelPicker, { registry: controller.getModelRegistry(), currentProvider: cfg.provider, currentModel: cfg.model, maxRows: overlayMaxRows, cols: cols, theme: theme, caps: caps, onSelect: handleModelSelect })) : overlay === 'settings' ? (_jsx(SettingsScreen, { initial: {
|
|
1916
1965
|
provider: cfg.provider,
|
|
1917
1966
|
model: cfg.model,
|
|
@@ -1945,14 +1994,6 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1945
1994
|
// Read from `cfg` (render scope, re-read every frame), so `/retry off`
|
|
1946
1995
|
// while the screen is open shows the new value on the next paint.
|
|
1947
1996
|
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
|
-
// A resize can drop below the usable floor. Do NOT leave the alt-screen —
|
|
1949
|
-
// entering and leaving it on every drag is worse than a placeholder — and take
|
|
1950
|
-
// the height from `frameHeight()` like everything else, because this is the
|
|
1951
|
-
// one path that reaches rows < 12 and a hard-coded height here would land on
|
|
1952
|
-
// `outputHeight >= rows` exactly when the user is dragging the window (§4.11).
|
|
1953
|
-
if (fullscreen && rows < MIN_FULLSCREEN_ROWS) {
|
|
1954
|
-
return (_jsx(Box, { flexDirection: "column", height: frameHeight(rows), width: cols, overflow: "hidden", children: _jsxs(Text, { wrap: "truncate", color: theme.noticeWarn, children: [glyphs.warn, " Terminal too small - needs at least ", MIN_FULLSCREEN_ROWS, " rows."] }) }));
|
|
1955
|
-
}
|
|
1956
1997
|
// Full-screen: a constant one-row brand bar at every size, which is what makes
|
|
1957
1998
|
// the viewport monotonic and the first submit jump-free (§4.2). Inline keeps
|
|
1958
1999
|
// its v0.3.0 ternary and never calls `pickHeaderVariant`.
|
|
@@ -2016,10 +2057,7 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
2016
2057
|
viewportRows: fullscreen ? viewportBudget : rows,
|
|
2017
2058
|
offset: scrolledLines,
|
|
2018
2059
|
};
|
|
2019
|
-
|
|
2020
|
-
// The completion popup lives inside the bottom chrome and can add up to 9
|
|
2021
|
-
// rows; unbounded, it pushes the status bar past the frame height where
|
|
2022
|
-
// `overflow: hidden` clips it away (R-14 / M-14). Cap it against the viewport.
|
|
2060
|
+
// Completion belongs to the document footer; cap it to keep the editor usable.
|
|
2023
2061
|
const popupMaxRows = fullscreen ? Math.max(1, viewportBudget - 4) : undefined;
|
|
2024
2062
|
// WHICH KEY THE HINT ROW NAMES (shift-tab-mode-toggle-still-dead-on-windows,
|
|
2025
2063
|
// C3-1). Derived from `vtInputWarning` rather than from a probe of its own,
|
|
@@ -2031,8 +2069,11 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
2031
2069
|
// Both keys always WORK (they share one branch in the input handler); this
|
|
2032
2070
|
// only decides which one the user is taught.
|
|
2033
2071
|
const modeToggleKey = vtInputWarning ? MODE_TOGGLE_KEYS.fallback : MODE_TOGGLE_KEYS.primary;
|
|
2034
|
-
const composer = fullscreen ? (_jsx(Composer, { isActive: overlay === null, running: state.status === 'running', history: promptHistory, commands: commandOptions, cwd: controller.getCwd(), showHint: rows >= HINT_MIN_ROWS, submitCount: cfg.submitCount, hintsEnabled: cfg.hints, agentMode: state.agentMode, services: liveServices, modeToggleKey: modeToggleKey, popupMaxRows: popupMaxRows, scrolledLines: scrolledLines, onDraftRows: onDraftRows, onNotice: notify, theme: theme, caps: caps, onSubmit: (text) => void handleSubmit(text), onHelp: () => dispatch({ type: 'setOverlay', overlay: 'help' }) })) : (_jsx(Box, { marginTop: 1, children: _jsx(PromptInput, { isActive: overlay === null, running: state.status === 'running', history: promptHistory, commands: commandOptions, cwd: controller.getCwd(), agentMode: state.agentMode, theme: theme, caps: caps, onSubmit: (text) => void handleSubmit(text), onHelp: () => dispatch({ type: 'setOverlay', overlay: 'help' }) }) }));
|
|
2035
|
-
|
|
2072
|
+
const composer = fullscreen ? (_jsx(Composer, { measureRef: composerRef, cols: contentCols, cursorVisible: cursorVisible, onInteraction: returnToComposer, reducedMotion: cfg.reducedMotion ?? false, onEscape: handleComposerEscape, onEscapeDismiss: cancelConfirmation, isActive: overlay === null && !tooSmall, running: state.status === 'running', history: promptHistory, commands: commandOptions, cwd: controller.getCwd(), showHint: rows >= HINT_MIN_ROWS, submitCount: cfg.submitCount, hintsEnabled: cfg.hints, agentMode: state.agentMode, services: liveServices, modeToggleKey: modeToggleKey, popupMaxRows: popupMaxRows, popupMaxHeight: railLayout.popupMaxHeight, onPopupRowsChange: onPopupRowsChange, scrolledLines: scrolledLines, onDraftRows: onDraftRows, onNotice: notify, theme: theme, caps: caps, onSubmit: (text) => void handleSubmit(text), onHelp: () => dispatch({ type: 'setOverlay', overlay: 'help' }) })) : (_jsx(Box, { marginTop: 1, children: _jsx(PromptInput, { reducedMotion: cfg.reducedMotion ?? false, onEscape: handleComposerEscape, onEscapeDismiss: cancelConfirmation, isActive: overlay === null && !tooSmall, running: state.status === 'running', history: promptHistory, commands: commandOptions, cwd: controller.getCwd(), agentMode: state.agentMode, theme: theme, caps: caps, onSubmit: (text) => void handleSubmit(text), onHelp: () => dispatch({ type: 'setOverlay', overlay: 'help' }) }) }));
|
|
2073
|
+
const team = state.team ? (_jsx(TeamPanel, { snapshot: state.team, layout: fullscreen ? teamLayout : undefined, rows: rows, cols: fullscreen ? contentCols : cols, reducedMotion: viewReducedMotion, theme: theme, caps: caps })) : null;
|
|
2074
|
+
const rail = showRail && state.todos ? (_jsx(TodoPanel, { snapshot: state.todos, width: railWidth, rows: railRows, running: running, reducedMotion: viewReducedMotion, theme: theme, caps: caps })) : null;
|
|
2075
|
+
const viewport = fullscreen ? (_jsxs(ScrollViewport, { active: overlay === null && !tooSmall, overlay: tooSmall ? null : overlayNode, footer: _jsxs(_Fragment, { children: [team, composer] }), rail: rail, scrollbar: terminal?.scrollbar, composerRef: composerRef, onComposerVisibilityChange: setCursorVisible, resetKey: scrollResetKey, intent: scrollIntent, pinToBottomNonce: pinToBottomNonce, onScrolledLinesChange: setScrolledLines, onViewportShiftChange: onViewportShiftChange, tailRowsRef: tailSink, hold: selectionHold, resumeMs: cfg.scrollResumeMs, showScrollIndicator: true, cols: contentCols, theme: theme, caps: caps, children: [opener, _jsx(TranscriptList, { entries: state.entries, expandedToolIds: state.expandedToolIds, thinkingVisible: state.thinkingVisible, reducedMotion: viewReducedMotion, density: density, mode: mode, nowSec: nowSec, theme: theme, caps: caps, windowSize: transcriptWindow, cols: contentCols, heights: heights, mountedSink: mountedCount, tailSink: tailSink })] })) : (_jsxs(_Fragment, { children: [opener, _jsx(Transcript, { entries: state.entries, expandedToolIds: state.expandedToolIds, thinkingVisible: state.thinkingVisible, reducedMotion: viewReducedMotion, density: density, mode: mode, nowSec: nowSec, theme: theme, caps: caps }), overlayNode] }));
|
|
2076
|
+
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."] }), mode: mode, rows: rows, cols: cols, header: header, viewport: viewport, team: team, strip: showStrip && state.todos ? (_jsx(TodoStrip, { snapshot: state.todos, cols: cols, theme: theme, caps: caps })) : null, toast: _jsx(BottomStatusRow, { mode: mode, toasts: state.toasts,
|
|
2036
2077
|
// `!overlayNode` suppresses the line while an overlay owns the screen:
|
|
2037
2078
|
// the full-screen branch replaces the whole viewport with the overlay,
|
|
2038
2079
|
// and a working line under a settings screen is noise attached to a
|
|
@@ -2070,7 +2111,15 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
2070
2111
|
// branch replaces the whole viewport with the overlay, and an update
|
|
2071
2112
|
// notice under a settings screen is noise attached to a surface it is
|
|
2072
2113
|
// not visible on.
|
|
2073
|
-
update: updateSnapshot && !overlayNode && shouldRenderUpdateLine(updateSnapshot) ? (_jsx(UpdateLine, { snapshot: updateSnapshot, compact: cols < UPDATE_LIMITS.statusCompactCols, theme: theme, caps: caps })) : null, theme: theme }), composer: composer, status: _jsx(StatusBar, {
|
|
2114
|
+
update: updateSnapshot && !overlayNode && shouldRenderUpdateLine(updateSnapshot) ? (_jsx(UpdateLine, { snapshot: updateSnapshot, compact: cols < UPDATE_LIMITS.statusCompactCols, theme: theme, caps: caps })) : null, theme: theme }), composer: composer, status: _jsx(StatusBar, { interruptHint: interruptGesture.current.phase === 'stopping'
|
|
2115
|
+
? 'Esc force-stop' : 'Esc x2 interrupt', model: cfg.model, provider: cfg.provider, usageTotal: state.usageTotal,
|
|
2116
|
+
// ONE OBJECT INSTEAD OF FOUR PARALLEL PROPS (§4.3). The four were a
|
|
2117
|
+
// projection of "many sources of truth" into the props layer, and a
|
|
2118
|
+
// caller could update three of them; a single reading cannot be
|
|
2119
|
+
// partially stale. The window and its `known` flag now travel INSIDE
|
|
2120
|
+
// it, resolved by the meter - which is what makes the user's
|
|
2121
|
+
// `contextWindow` override reach the bar.
|
|
2122
|
+
context: state.context, status: state.status, elapsedMs: elapsedMs, thinkingLevel: cfg.thinkingLevel, tokPerSec: tokPerSec, theme: theme, caps: caps, scrolledLines: scrolledLines, redrawNonce: redrawNonce, agentMode: state.agentMode, pendingAgentMode: state.pendingAgentMode, ecoRung: governor.rung, ...(state.team
|
|
2074
2123
|
? {
|
|
2075
2124
|
teamActive: {
|
|
2076
2125
|
running: state.team.runs.filter((r) => r.phase !== 'queued' && r.phase !== 'done' &&
|
|
@@ -2081,8 +2130,9 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
2081
2130
|
: {}), ...(state.todos && !showRail
|
|
2082
2131
|
? { todoActive: { done: state.todos.doneCount, total: state.todos.total } }
|
|
2083
2132
|
: {}), ...(liveServices > 0 ? { servicesActive: { live: liveServices } } : {}), ...(state.fast?.live ? { fastActive: { inFlight: state.fast.inFlight } } : {}), ...(state.compaction?.live
|
|
2133
|
+
? { compactionActive: { inFlight: state.compaction.inFlight } }
|
|
2134
|
+
: {}), ...(controller.isCompactionEnabled()
|
|
2084
2135
|
? {
|
|
2085
|
-
compactionActive: { inFlight: state.compaction.inFlight },
|
|
2086
2136
|
gaugeMarks: {
|
|
2087
2137
|
warn: cfg.compaction.warnThreshold * 100,
|
|
2088
2138
|
high: cfg.compaction.threshold * 100,
|