@aragon-agent/cli 0.6.7 → 0.6.9
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 +27 -0
- package/README.md +107 -77
- package/dist/agent/controller.d.ts +11 -7
- package/dist/agent/controller.js +35 -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 +7 -113
- package/dist/agent/reducer.js +0 -62
- package/dist/cli.js +111 -182
- package/dist/commands/builtins.js +3 -1
- package/dist/commands/perf.d.ts +32 -2
- package/dist/commands/perf.js +32 -2
- package/dist/commands/registry.d.ts +4 -5
- package/dist/config/env.js +0 -130
- package/dist/config/load.d.ts +0 -2
- package/dist/config/load.js +2 -31
- package/dist/config/schema.d.ts +1 -14
- package/dist/config/schema.js +0 -1
- package/dist/config/store.js +2 -0
- 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/logging/install.d.ts +2 -2
- package/dist/logging/install.js +2 -2
- package/dist/todo/follow-through.d.ts +0 -4
- package/dist/todo/follow-through.js +0 -4
- package/dist/todo/limits.d.ts +0 -10
- package/dist/todo/limits.js +0 -10
- package/dist/todo/prompt.d.ts +0 -15
- 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/ActivityLine.d.ts +26 -34
- package/dist/ui/ActivityLine.js +30 -19
- package/dist/ui/App.d.ts +3 -41
- package/dist/ui/App.js +223 -247
- package/dist/ui/BottomStatusRow.d.ts +1 -3
- package/dist/ui/BottomStatusRow.js +4 -8
- package/dist/ui/Composer.d.ts +38 -5
- package/dist/ui/Composer.js +45 -23
- package/dist/ui/Header.d.ts +2 -16
- package/dist/ui/Header.js +3 -18
- package/dist/ui/Logo.d.ts +1 -17
- package/dist/ui/Logo.js +0 -16
- package/dist/ui/ModeChip.d.ts +7 -18
- package/dist/ui/ModeChip.js +9 -0
- package/dist/ui/PromptCaret.d.ts +11 -0
- package/dist/ui/PromptCaret.js +31 -0
- package/dist/ui/PromptInput.d.ts +10 -33
- package/dist/ui/PromptInput.js +37 -20
- package/dist/ui/StatusBar.d.ts +1 -11
- package/dist/ui/StatusBar.js +14 -8
- package/dist/ui/ToastStack.d.ts +2 -18
- package/dist/ui/ToastStack.js +4 -11
- package/dist/ui/Transcript.d.ts +5 -71
- package/dist/ui/Transcript.js +10 -199
- package/dist/ui/composer-rows.js +50 -9
- package/dist/ui/entries/AssistantEntry.d.ts +0 -23
- package/dist/ui/entries/AssistantEntry.js +3 -4
- package/dist/ui/entries/DiffView.d.ts +1 -7
- package/dist/ui/entries/DiffView.js +2 -2
- package/dist/ui/entries/ToolCard.d.ts +0 -6
- package/dist/ui/entries/ToolCard.js +4 -13
- package/dist/ui/frame-differ.d.ts +13 -0
- package/dist/ui/frame-differ.js +37 -24
- 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/ink-runtime.d.ts +1 -0
- package/dist/ui/ink-runtime.js +21 -0
- package/dist/ui/layout/AppShell.d.ts +4 -58
- package/dist/ui/layout/AppShell.js +3 -6
- package/dist/ui/layout/OverlayFrame.d.ts +1 -25
- package/dist/ui/layout/OverlayFrame.js +0 -24
- package/dist/ui/layout/ScrollIndicator.d.ts +4 -3
- package/dist/ui/layout/ScrollIndicator.js +4 -4
- package/dist/ui/layout/ScrollViewport.d.ts +16 -66
- package/dist/ui/layout/ScrollViewport.js +195 -185
- 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/frame.d.ts +0 -22
- package/dist/ui/layout/frame.js +0 -36
- 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/todo-layout.d.ts +2 -3
- package/dist/ui/layout/todo-layout.js +9 -9
- package/dist/ui/layout/viewport-geometry.d.ts +2 -1
- package/dist/ui/layout/viewport-geometry.js +2 -1
- package/dist/ui/layout/virtual-window.d.ts +1 -0
- package/dist/ui/layout/virtual-window.js +1 -1
- package/dist/ui/live-clamp.d.ts +1 -29
- package/dist/ui/live-clamp.js +1 -29
- package/dist/ui/overlays/HelpOverlay.js +6 -2
- package/dist/ui/run-status-row.d.ts +61 -0
- package/dist/ui/run-status-row.js +70 -0
- 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.d.ts +0 -8
- package/dist/ui/transcript-text.js +0 -8
- package/dist/ui/use-startup-notices.d.ts +1 -57
- package/dist/ui/use-startup-notices.js +3 -54
- package/dist/ui/use-wheel-routing.d.ts +3 -1
- package/dist/ui/use-wheel-routing.js +6 -0
- package/package.json +2 -2
- package/dist/ui/TodoStrip.d.ts +0 -36
- package/dist/ui/TodoStrip.js +0 -32
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
|
|
@@ -7,7 +7,8 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
|
|
|
7
7
|
*/
|
|
8
8
|
import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react';
|
|
9
9
|
import process from 'node:process';
|
|
10
|
-
import {
|
|
10
|
+
import { 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';
|
|
@@ -25,14 +26,12 @@ import { pickGlyphs } from './glyphs.js';
|
|
|
25
26
|
import { Header } from './Header.js';
|
|
26
27
|
import { pickHeaderVariant, pickOpenerVariant } from './Logo.js';
|
|
27
28
|
import { SessionOpener } from './SessionOpener.js';
|
|
28
|
-
import {
|
|
29
|
-
import { PromptInput } from './PromptInput.js';
|
|
29
|
+
import { TranscriptList } from './Transcript.js';
|
|
30
30
|
import { Composer } from './Composer.js';
|
|
31
31
|
import { StatusBar } from './StatusBar.js';
|
|
32
32
|
import { secondsLeft } from '../agent/retry-view.js';
|
|
33
33
|
import { TeamPanel } from './TeamPanel.js';
|
|
34
34
|
import { TodoPanel } from './TodoPanel.js';
|
|
35
|
-
import { TodoStrip } from './TodoStrip.js';
|
|
36
35
|
import { ActivityLine, liveSpinner } from './ActivityLine.js';
|
|
37
36
|
import { BottomStatusRow } from './BottomStatusRow.js';
|
|
38
37
|
import { UpdateLine } from './UpdateLine.js';
|
|
@@ -52,9 +51,9 @@ import { advanceBudget, decideFollowThrough, emptyBudget, } from '../todo/follow
|
|
|
52
51
|
import { useTerminalSize } from './layout/useTerminalSize.js';
|
|
53
52
|
import { useHeightStore } from './use-height-store.js';
|
|
54
53
|
import { useRenderGovernor } from './use-render-governor.js';
|
|
55
|
-
import { setPerfResetHook, setPerfSnapshotProvider } from '../commands/perf.js';
|
|
56
|
-
import {
|
|
57
|
-
import { HINT_MIN_ROWS, viewportRows as computeViewportRows } from './layout/budget.js';
|
|
54
|
+
import { readScrollbarPerf, setPerfResetHook, setPerfSnapshotProvider, } from '../commands/perf.js';
|
|
55
|
+
import { MIN_FULLSCREEN_ROWS, MIN_FULLSCREEN_COLS } from './layout/frame.js';
|
|
56
|
+
import { HINT_MIN_ROWS, clampDraftRows, viewportRows as computeViewportRows } from './layout/budget.js';
|
|
58
57
|
import { OVERLAY_PAGE, useWheelRouting } from './use-wheel-routing.js';
|
|
59
58
|
import { installConsoleBridge } from './console-bridge.js';
|
|
60
59
|
import { publishExitSnapshot } from './exit-snapshot.js';
|
|
@@ -105,15 +104,6 @@ export function resolveSettingsMaxTokens(raw, previous) {
|
|
|
105
104
|
toast: ['warn', 'Max tokens must be a number or "auto".'],
|
|
106
105
|
};
|
|
107
106
|
}
|
|
108
|
-
/**
|
|
109
|
-
* How long rung two of the Esc ladder stays armed after rung one (§5.5).
|
|
110
|
-
*
|
|
111
|
-
* Four seconds: long enough that a user who watched the abort fail can react,
|
|
112
|
-
* short enough that a stray Esc minutes later cannot force-stop a healthy run.
|
|
113
|
-
* `runEnd` / `agent_end` disarm it too, so the window is an upper bound rather
|
|
114
|
-
* than the only protection (R-9).
|
|
115
|
-
*/
|
|
116
|
-
const ESC_ARM_MS = 4000;
|
|
117
107
|
/** Services in a non-terminal state, from the live snapshot map. */
|
|
118
108
|
function liveServiceCount(services) {
|
|
119
109
|
let n = 0;
|
|
@@ -130,11 +120,18 @@ function liveServiceCount(services) {
|
|
|
130
120
|
function seedViewState(thinkingVisible) {
|
|
131
121
|
return initialViewState({ thinkingVisible });
|
|
132
122
|
}
|
|
133
|
-
export function App({ controller, version,
|
|
123
|
+
export function App({ controller, version, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, vtInputWarning, updateBridge, pasteBridge, terminal, }) {
|
|
134
124
|
const { exit } = useApp();
|
|
135
125
|
const { stdout } = useStdout();
|
|
136
|
-
const fullscreen = mode === 'fullscreen';
|
|
137
126
|
const { rows, cols } = useTerminalSize();
|
|
127
|
+
const tooSmall = (rows < MIN_FULLSCREEN_ROWS || cols < MIN_FULLSCREEN_COLS);
|
|
128
|
+
const composerRef = useRef(null);
|
|
129
|
+
const activityRef = useRef(null);
|
|
130
|
+
const [cursorVisible, setCursorVisible] = useState(true);
|
|
131
|
+
// Whether the run status row above the input is inside the viewport. It is
|
|
132
|
+
// `true` while the row is not mounted (`elementVisible(null)`), so it is NEVER
|
|
133
|
+
// a decision on its own - always AND it with `runRowEnabled` (`runRowShown`).
|
|
134
|
+
const [activityRowVisible, setActivityRowVisible] = useState(true);
|
|
138
135
|
// --- Render governor (tui-render-performance L4). ------------------------
|
|
139
136
|
//
|
|
140
137
|
// FIRST, BEFORE ANY OTHER WORK IN THIS BODY. The hook stamps
|
|
@@ -177,6 +174,7 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
177
174
|
// Scroll plumbing: intent goes down, one derived display number comes back.
|
|
178
175
|
const [scrollIntent, setScrollIntent] = useState();
|
|
179
176
|
const [pinToBottomNonce, setPinToBottomNonce] = useState(0);
|
|
177
|
+
const [scrollResetKey, setScrollResetKey] = useState(0);
|
|
180
178
|
const [scrolledLines, setScrolledLines] = useState(0);
|
|
181
179
|
/**
|
|
182
180
|
* True while a selection drag holds the viewport
|
|
@@ -216,18 +214,10 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
216
214
|
confirmRef.current = confirmState;
|
|
217
215
|
const ctrlCArmed = useRef(false);
|
|
218
216
|
const ctrlCTimer = useRef(null);
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
* halves matter: a healthy run that ended between the two presses must not be
|
|
224
|
-
* force-stoppable retroactively, and in every healthy run the second press
|
|
225
|
-
* lands on the idle branch and does nothing at all (R-9). Rung two only ever
|
|
226
|
-
* fires while the run is STILL running after rung one - i.e. only when the
|
|
227
|
-
* first press provably failed.
|
|
228
|
-
*/
|
|
229
|
-
const escArmed = useRef(false);
|
|
230
|
-
const escArmTimer = useRef(null);
|
|
217
|
+
const interruptGesture = useRef({ phase: 'ready' });
|
|
218
|
+
const interactionPhase = useRef(controller.isRunning() ? 'running' : 'idle');
|
|
219
|
+
const submissionSequence = useRef(0);
|
|
220
|
+
const mounted = useRef(true);
|
|
231
221
|
/**
|
|
232
222
|
* Live service snapshots, keyed by id, for the status chip and the Ctrl+C rung.
|
|
233
223
|
*
|
|
@@ -248,16 +238,6 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
248
238
|
const servicesRef = useRef(services);
|
|
249
239
|
servicesRef.current = services;
|
|
250
240
|
const runBaselineOut = useRef(0);
|
|
251
|
-
/**
|
|
252
|
-
* Epoch ms the current run began — the activity phrase's seed.
|
|
253
|
-
*
|
|
254
|
-
* ASSIGNED IN RENDER SCOPE, NOT IN THE ELAPSED EFFECT (P1-7). That effect runs
|
|
255
|
-
* AFTER the render that first sees `status === 'running'`, so a ref it wrote
|
|
256
|
-
* would be one frame late — and the first frame of a run is exactly when the
|
|
257
|
-
* phrase is chosen. `Transcript.tsx` updates `highWater` / `prevLen` during
|
|
258
|
-
* render for the same reason: a value derived from a transition is needed by
|
|
259
|
-
* the very render that observes the transition.
|
|
260
|
-
*/
|
|
261
241
|
const runStartedAt = useRef(0);
|
|
262
242
|
const toastTimers = useRef(new Map());
|
|
263
243
|
/** Every outstanding `gate.request()`. Held in a ref, never in state: it is
|
|
@@ -331,20 +311,27 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
331
311
|
aborted: false,
|
|
332
312
|
errored: false,
|
|
333
313
|
});
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
*
|
|
337
|
-
* A `useCallback` so the event subscription's dependency list can name it, and
|
|
338
|
-
* so it is one function rather than three copies of "clear the flag and the
|
|
339
|
-
* timer" that can drift.
|
|
340
|
-
*/
|
|
341
|
-
const disarmEsc = useCallback(() => {
|
|
342
|
-
escArmed.current = false;
|
|
343
|
-
if (escArmTimer.current) {
|
|
344
|
-
clearTimeout(escArmTimer.current);
|
|
345
|
-
escArmTimer.current = null;
|
|
346
|
-
}
|
|
314
|
+
const resetInterrupt = useCallback(() => {
|
|
315
|
+
interruptGesture.current = { phase: 'ready' };
|
|
347
316
|
}, []);
|
|
317
|
+
const cancelConfirmation = useCallback(() => {
|
|
318
|
+
interruptGesture.current = cancelInterruptConfirmation(interruptGesture.current);
|
|
319
|
+
}, []);
|
|
320
|
+
useEffect(() => {
|
|
321
|
+
if (state.overlay)
|
|
322
|
+
cancelConfirmation();
|
|
323
|
+
}, [state.overlay, cancelConfirmation]);
|
|
324
|
+
useEffect(() => {
|
|
325
|
+
mounted.current = true;
|
|
326
|
+
return () => {
|
|
327
|
+
mounted.current = false;
|
|
328
|
+
submissionSequence.current += 1;
|
|
329
|
+
if (interactionPhase.current === 'starting')
|
|
330
|
+
controller.abort();
|
|
331
|
+
interactionPhase.current = 'idle';
|
|
332
|
+
resetInterrupt();
|
|
333
|
+
};
|
|
334
|
+
}, [controller, resetInterrupt]);
|
|
348
335
|
/**
|
|
349
336
|
* Disarm an armed continuation. Idempotent; NEVER touches the budget — a
|
|
350
337
|
* cancelled continuation was never issued, so it must not be charged (AC-18).
|
|
@@ -437,7 +424,7 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
437
424
|
// emits while it unwinds - `turn_start`, `tool_call_start`, `turn_end`,
|
|
438
425
|
// `agent_end` - belongs to the run the user just abandoned. `runStart` /
|
|
439
426
|
// `turnStart` would set `status: 'running'` again, bouncing the view straight
|
|
440
|
-
// back out of the `idle`
|
|
427
|
+
// back out of the `idle` force-stop just gave them, and stray entries would
|
|
441
428
|
// append to a transcript they believe is finished.
|
|
442
429
|
//
|
|
443
430
|
// A LOCAL `let`, RE-READ RATHER THAN CAPTURED ONCE: the generation this
|
|
@@ -453,6 +440,12 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
453
440
|
generation = controller.runGeneration;
|
|
454
441
|
return;
|
|
455
442
|
}
|
|
443
|
+
if (event.type === 'agent_start')
|
|
444
|
+
interactionPhase.current = 'running';
|
|
445
|
+
if (event.type === 'agent_end') {
|
|
446
|
+
interactionPhase.current = 'idle';
|
|
447
|
+
resetInterrupt();
|
|
448
|
+
}
|
|
456
449
|
const cost = controller.getModelInfo().cost;
|
|
457
450
|
for (const action of reduceEvent(event, cost, patchSource)) {
|
|
458
451
|
// MUTATED HERE, ONE INSTANT BEFORE THE ACTION THAT SETS
|
|
@@ -479,11 +472,6 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
479
472
|
}
|
|
480
473
|
if (event.type !== 'agent_end')
|
|
481
474
|
return;
|
|
482
|
-
// RUNG TWO DISARMS WITH THE RUN (§3.6 / R-9). A healthy run that ended
|
|
483
|
-
// between the two Esc presses must not be force-stoppable retroactively:
|
|
484
|
-
// the second press then lands on the idle branch, which is what makes the
|
|
485
|
-
// ladder free to take rather than a footgun.
|
|
486
|
-
disarmEsc();
|
|
487
475
|
// NO OVERLAY OUTLIVES ITS RUN (P1-4). `ctx.signal` already covers the
|
|
488
476
|
// abort path, so this is DELIBERATE REDUNDANCY: it makes "the card cannot
|
|
489
477
|
// survive the run that opened it" a property of the App's lifecycle
|
|
@@ -503,14 +491,6 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
503
491
|
budgetRef.current = advanceBudget(budgetRef.current, todos, autoContinuationRef.current);
|
|
504
492
|
autoContinuationRef.current = false;
|
|
505
493
|
const decision = decideFollowThrough({
|
|
506
|
-
// FROM THE CONTROLLER, NOT FROM `cfg` (P1-1). This effect's dependency
|
|
507
|
-
// list never changes, so it closes over the `cfg` object from the render
|
|
508
|
-
// in which it mounted — and `setTodoConfig` builds a NEW object, which
|
|
509
|
-
// that closure will never see. `cfg.todo.followThrough` would therefore
|
|
510
|
-
// be pinned to the launch value for the whole session and
|
|
511
|
-
// `/todo follow auto` would report success and do nothing. Note the
|
|
512
|
-
// asymmetry with `showStrip` further down, which is right to use `cfg`
|
|
513
|
-
// because it runs in render scope where `cfg` is re-read every frame.
|
|
514
494
|
mode: controller.getTodoConfig().followThrough,
|
|
515
495
|
snapshot: todos,
|
|
516
496
|
runEnd: { ...endReasonRef.current },
|
|
@@ -551,7 +531,7 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
551
531
|
done: live.doneCount,
|
|
552
532
|
mode: 'auto',
|
|
553
533
|
});
|
|
554
|
-
submitRef.current(decision.message, { userInitiated: false });
|
|
534
|
+
submitRef.current(decision.message, { userInitiated: false, todoPolicy: 'continue' });
|
|
555
535
|
}, decision.graceMs);
|
|
556
536
|
}
|
|
557
537
|
// A deferred `plan -> build` becomes real here and nowhere else (§3.2).
|
|
@@ -573,22 +553,7 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
573
553
|
cancelFollowThrough();
|
|
574
554
|
unsubscribe();
|
|
575
555
|
};
|
|
576
|
-
}, [controller, patchSource, flushPending, cancelPendingHuman, cancelFollowThrough,
|
|
577
|
-
// --- Background services (background-service-supervision §3.8). ---------
|
|
578
|
-
//
|
|
579
|
-
// A SEPARATE SUBSCRIPTION, not a branch in the one above: `ProcEvent` is
|
|
580
|
-
// deliberately not a member of core's `AgentEvent` union (I-5 / D-1), so it
|
|
581
|
-
// cannot arrive through `controller.subscribe`.
|
|
582
|
-
//
|
|
583
|
-
// TWO SINKS PER EVENT, and they are not redundant. `setServices` holds the
|
|
584
|
-
// LIVE state the status chip, the composer hint and the Ctrl+C rung read;
|
|
585
|
-
// `dispatch` writes the TRANSCRIPT, which records events about a service
|
|
586
|
-
// rather than mirroring it (D-11). Feeding one from the other would make the
|
|
587
|
-
// chip lie about a card that has already been printed into `<Static>`.
|
|
588
|
-
//
|
|
589
|
-
// NOT ROUTED THROUGH THE STREAMING COALESCER. `output` is already coalesced at
|
|
590
|
-
// the SOURCE, inside the supervisor, because `/bg` and `bash_output` consume
|
|
591
|
-
// the same event (§6); a second buffer here would only add latency.
|
|
556
|
+
}, [controller, patchSource, flushPending, cancelPendingHuman, cancelFollowThrough, resetInterrupt]);
|
|
592
557
|
useEffect(() => {
|
|
593
558
|
return controller.subscribeProc((event) => {
|
|
594
559
|
const service = event.service;
|
|
@@ -670,9 +635,6 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
670
635
|
});
|
|
671
636
|
return () => {
|
|
672
637
|
unsubscribeTeam();
|
|
673
|
-
// Nothing else disposes the runtime on an inline-mode exit or a crash
|
|
674
|
-
// that unmounts the tree, and an orphaned child holds the event loop open
|
|
675
|
-
// (R-15). `cli.tsx` calls this too; `dispose()` is idempotent.
|
|
676
638
|
controller.dispose();
|
|
677
639
|
};
|
|
678
640
|
}, [controller]);
|
|
@@ -941,10 +903,8 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
941
903
|
// Ink's own patchConsole writes straight to stdout, which shifts the fixed
|
|
942
904
|
// frame by a row and breaks its line accounting for good.
|
|
943
905
|
useEffect(() => {
|
|
944
|
-
if (!fullscreen)
|
|
945
|
-
return undefined;
|
|
946
906
|
return installConsoleBridge((level, text) => dispatch({ type: 'notice', level, text }));
|
|
947
|
-
}, [
|
|
907
|
+
}, []);
|
|
948
908
|
// `enabled` is `mouseSource`, not `cfg.mouse`: a user whose terminal drops
|
|
949
909
|
// the events must never be shown advice about a mode not in effect (§6.1).
|
|
950
910
|
// `cli.tsx::wantMouse` now folds the platform check into the same expression,
|
|
@@ -967,21 +927,9 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
967
927
|
// `transcriptRetain` to `transcriptWindow` (tui-render-performance §5.1).
|
|
968
928
|
cfg.transcriptRetainRequested !== undefined
|
|
969
929
|
? { requested: cfg.transcriptRetainRequested, resolved: cfg.transcriptRetain }
|
|
970
|
-
: undefined,
|
|
971
|
-
// Its own one-shot key, NOT `mouseNoticeSeen`: everyone this notice is for
|
|
972
|
-
// has already run aragon at least once and already has that flag set. That
|
|
973
|
-
// key is now a VERSION for the same reason, one turn later — the audience
|
|
974
|
-
// for the text that finally names the fallback key is, again, exactly the
|
|
975
|
-
// set of users who already have the old flag on disk.
|
|
976
|
-
//
|
|
977
|
-
// `fullscreen` goes with it because only half of the damage is mode-
|
|
978
|
-
// independent: `Shift+Tab` is broken either way, but inline never asked for
|
|
979
|
-
// wheel reports and does not own the scroll keys, so it must not be told
|
|
980
|
-
// about either.
|
|
981
|
-
vtInputWarning
|
|
930
|
+
: undefined, vtInputWarning
|
|
982
931
|
? {
|
|
983
932
|
nodeVersion: vtInputWarning.nodeVersion,
|
|
984
|
-
fullscreen,
|
|
985
933
|
onSeen: () => setVtInputNoticeVersion(VT_INPUT_NOTICE_VERSION),
|
|
986
934
|
}
|
|
987
935
|
: undefined);
|
|
@@ -1278,9 +1226,9 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1278
1226
|
return;
|
|
1279
1227
|
// An overlay replaces the viewport, so a selection over it would be a
|
|
1280
1228
|
// highlight over rows that are no longer there (N4).
|
|
1281
|
-
selectionController.setEnabled(state.overlay === null);
|
|
1229
|
+
selectionController.setEnabled(state.overlay === null && !tooSmall);
|
|
1282
1230
|
selectionController.clear();
|
|
1283
|
-
}, [selectionController, state.overlay]);
|
|
1231
|
+
}, [selectionController, state.overlay, tooSmall]);
|
|
1284
1232
|
const persistConfig = (patch) => {
|
|
1285
1233
|
try {
|
|
1286
1234
|
updatePersistedConfig(patch);
|
|
@@ -1336,7 +1284,22 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1336
1284
|
args,
|
|
1337
1285
|
controller,
|
|
1338
1286
|
state: stateRef.current,
|
|
1339
|
-
dispatch
|
|
1287
|
+
dispatch: (action) => {
|
|
1288
|
+
if (action.type === 'resetConversation' || action.type === 'restoreEntries') {
|
|
1289
|
+
setScrollResetKey((n) => n + 1);
|
|
1290
|
+
const wasStarting = interactionPhase.current === 'starting';
|
|
1291
|
+
submissionSequence.current += 1;
|
|
1292
|
+
interactionPhase.current = 'idle';
|
|
1293
|
+
resetInterrupt();
|
|
1294
|
+
cancelFollowThrough();
|
|
1295
|
+
autoContinuationRef.current = false;
|
|
1296
|
+
if (wasStarting) {
|
|
1297
|
+
dispatch({ type: 'abortMark' });
|
|
1298
|
+
dispatch({ type: 'runEnd' });
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
dispatch(action);
|
|
1302
|
+
},
|
|
1340
1303
|
setOverlay: (overlay) => dispatch({ type: 'setOverlay', overlay }),
|
|
1341
1304
|
notify,
|
|
1342
1305
|
toast,
|
|
@@ -1401,11 +1364,19 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1401
1364
|
// line, before the steering branch: a message typed during the grace window
|
|
1402
1365
|
// must be the one that runs, whichever branch it takes.
|
|
1403
1366
|
cancelFollowThrough();
|
|
1404
|
-
|
|
1367
|
+
cancelConfirmation();
|
|
1368
|
+
if (interactionPhase.current === 'running') {
|
|
1405
1369
|
controller.steer(message);
|
|
1406
1370
|
toast('info', 'Steering queued.');
|
|
1407
1371
|
return;
|
|
1408
1372
|
}
|
|
1373
|
+
if (interactionPhase.current === 'starting') {
|
|
1374
|
+
toast('info', 'A run is still starting. Press Esc twice to cancel.');
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
resetInterrupt();
|
|
1378
|
+
const sequence = ++submissionSequence.current;
|
|
1379
|
+
interactionPhase.current = 'starting';
|
|
1409
1380
|
// `userInitiated: false` skips TWO things and both are wanted: the
|
|
1410
1381
|
// prompt-history append, and the `submitCount` bump that drives the
|
|
1411
1382
|
// composer's hint fade. An auto-continuation demonstrates nothing about what
|
|
@@ -1420,14 +1391,30 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1420
1391
|
const pre = controller.preflight();
|
|
1421
1392
|
if (!pre.ok) {
|
|
1422
1393
|
notify('error', pre.message ?? 'Configuration error.');
|
|
1394
|
+
interactionPhase.current = 'idle';
|
|
1395
|
+
resetInterrupt();
|
|
1396
|
+
autoContinuationRef.current = false;
|
|
1423
1397
|
dispatch({ type: 'runEnd' }); // reset status; nothing was started
|
|
1424
1398
|
return;
|
|
1425
1399
|
}
|
|
1426
1400
|
// Fire-and-forget: events drive the UI. prompt() never rejects.
|
|
1427
|
-
void controller.prompt(message)
|
|
1401
|
+
void controller.prompt(message, { todoPolicy: opts.todoPolicy ?? 'new-task' }).then((outcome) => {
|
|
1402
|
+
if (!mounted.current || sequence !== submissionSequence.current)
|
|
1403
|
+
return;
|
|
1404
|
+
if (outcome?.status !== 'not-started')
|
|
1405
|
+
return;
|
|
1406
|
+
interactionPhase.current = 'idle';
|
|
1407
|
+
resetInterrupt();
|
|
1408
|
+
autoContinuationRef.current = false;
|
|
1409
|
+
dispatch({ type: 'runEnd' });
|
|
1410
|
+
});
|
|
1428
1411
|
};
|
|
1429
1412
|
submitRef.current = submitMessage;
|
|
1430
1413
|
const handleSubmit = async (raw) => {
|
|
1414
|
+
if (interactionPhase.current === 'running' && /^\s*\/(reset|resume)(?:\s|$)/.test(raw)) {
|
|
1415
|
+
notify('warn', 'Interrupt the current run before switching conversations.');
|
|
1416
|
+
return;
|
|
1417
|
+
}
|
|
1431
1418
|
// Submitting is an unconditional "take me to the newest output" (§4.5).
|
|
1432
1419
|
setPinToBottomNonce((n) => n + 1);
|
|
1433
1420
|
const handled = await runSlashInput(registry, raw, makeCtx);
|
|
@@ -1549,17 +1536,36 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1549
1536
|
}
|
|
1550
1537
|
resolveHuman({ kind: 'planDecision', decision: verdict.decision, feedback: verdict.feedback });
|
|
1551
1538
|
};
|
|
1552
|
-
// --- Mouse wheel routing (wheel-scrolls-transcript-only §4.1). Table and
|
|
1553
|
-
// coalescer live in the hook (D-13); only the wiring it cannot own stays here.
|
|
1554
|
-
// Inline arrows are fine — the hook re-reads its options through a ref every
|
|
1555
|
-
// render. The `Math.max(0, …)` mirrors the keyboard branch below, and for the
|
|
1556
|
-
// same reason: `OverlayFrame` only ever reports a clamp DOWNWARD.
|
|
1557
1539
|
const { clearCoalescer } = useWheelRouting({
|
|
1558
1540
|
mouseSource,
|
|
1541
|
+
isPointerCaptured: () => terminal?.scrollbar?.controller?.isCaptured() ?? false,
|
|
1542
|
+
isActive: () => !tooSmall,
|
|
1559
1543
|
getOverlay: () => stateRef.current.overlay,
|
|
1560
1544
|
onScroll: scrollBy,
|
|
1561
1545
|
onOverlayScroll: (delta) => setOverlayScroll((n) => Math.max(0, n + delta)),
|
|
1562
1546
|
});
|
|
1547
|
+
const returnToComposer = useCallback(() => {
|
|
1548
|
+
clearCoalescer();
|
|
1549
|
+
terminal?.scrollbar?.controller?.cancel();
|
|
1550
|
+
if (scrolledLines > 0)
|
|
1551
|
+
setPinToBottomNonce((n) => n + 1);
|
|
1552
|
+
}, [clearCoalescer, terminal, scrolledLines]);
|
|
1553
|
+
useEffect(() => {
|
|
1554
|
+
stdout.on('resize', clearCoalescer);
|
|
1555
|
+
return () => { stdout.off('resize', clearCoalescer); };
|
|
1556
|
+
}, [stdout, clearCoalescer]);
|
|
1557
|
+
useEffect(() => {
|
|
1558
|
+
const bridge = terminal?.scrollbar;
|
|
1559
|
+
if (!bridge)
|
|
1560
|
+
return;
|
|
1561
|
+
bridge.onStart = () => { clearCoalescer(); terminal?.selection?.controller?.clear(); };
|
|
1562
|
+
bridge.requestRedraw = () => setRedrawNonce((n) => n + 1);
|
|
1563
|
+
return () => { bridge.onStart = undefined; bridge.requestRedraw = undefined; };
|
|
1564
|
+
}, [terminal, clearCoalescer]);
|
|
1565
|
+
useEffect(() => {
|
|
1566
|
+
clearCoalescer();
|
|
1567
|
+
terminal?.scrollbar?.controller?.cancel();
|
|
1568
|
+
}, [rows, cols, tooSmall, state.overlay, clearCoalescer, terminal]);
|
|
1563
1569
|
// A fresh overlay always opens at the top; carrying the previous overlay's
|
|
1564
1570
|
// offset over would open Help mid-list for no reason the user can see.
|
|
1565
1571
|
//
|
|
@@ -1585,6 +1591,8 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1585
1591
|
}, []);
|
|
1586
1592
|
// --- Global keys -------------------------------------------------------
|
|
1587
1593
|
useInput((input, key) => {
|
|
1594
|
+
if (!key.escape)
|
|
1595
|
+
cancelConfirmation();
|
|
1588
1596
|
// I-9, the third clear trigger, and FIRST IN THE HANDLER so no `return`
|
|
1589
1597
|
// below can skip it. Any key at all drops the selection: most of them move
|
|
1590
1598
|
// the rows, and the ones that do not have still ended the gesture. It is
|
|
@@ -1631,10 +1639,8 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1631
1639
|
}
|
|
1632
1640
|
// R-P1-7: the overlay branch MUST come before the transcript scroll branch
|
|
1633
1641
|
// and MUST return. `PgUp` used to fall through to `scrollBy()` while an
|
|
1634
|
-
// overlay was open
|
|
1635
|
-
//
|
|
1636
|
-
// applied when the overlay closed and the transcript jumped a page on its
|
|
1637
|
-
// own. Even overlays that ignore the key must swallow it here.
|
|
1642
|
+
// overlay was open. The stable viewport ignores inactive intents, but routing
|
|
1643
|
+
// must still give overlays exclusive ownership of their paging keys.
|
|
1638
1644
|
if (stateRef.current.overlay) {
|
|
1639
1645
|
const overlay = stateRef.current.overlay;
|
|
1640
1646
|
// Modes A only. `model` / `confirm` manage their own keys; registering an
|
|
@@ -1675,7 +1681,7 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1675
1681
|
return;
|
|
1676
1682
|
}
|
|
1677
1683
|
if (key.ctrl && (input === 'l' || input === 'L')) {
|
|
1678
|
-
|
|
1684
|
+
{
|
|
1679
1685
|
// Writing \x1B[2J here would erase the screen and then leave it blank:
|
|
1680
1686
|
// Ink sees no output change and skips the repaint (ink.js:132 +
|
|
1681
1687
|
// log-update.js:13), possibly forever on an idle empty session. Bump
|
|
@@ -1691,7 +1697,7 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1691
1697
|
}
|
|
1692
1698
|
return;
|
|
1693
1699
|
}
|
|
1694
|
-
|
|
1700
|
+
{
|
|
1695
1701
|
if (key.pageUp) {
|
|
1696
1702
|
scrollBy('pageUp');
|
|
1697
1703
|
return;
|
|
@@ -1712,17 +1718,10 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1712
1718
|
if (key.ctrl && (input === 't' || input === 'T')) {
|
|
1713
1719
|
const revealing = !stateRef.current.thinkingVisible;
|
|
1714
1720
|
dispatch({ type: 'toggleThinking' });
|
|
1715
|
-
// THE ACK IS MODE-AWARE FOR THE SAME REASON THE MARKER IS (D-16 / P0-2).
|
|
1716
|
-
// Inline mode cannot repaint what `<Static>` has already printed, so the
|
|
1717
|
-
// toast says what it DID rather than what the user hoped. The hide
|
|
1718
|
-
// direction needs no qualifier: hiding applies to everything drawn from
|
|
1719
|
-
// here on, and nothing is claimed about scrollback either way.
|
|
1720
1721
|
if (!revealing)
|
|
1721
1722
|
toast('info', 'Thinking hidden.');
|
|
1722
|
-
else if (fullscreen)
|
|
1723
|
-
toast('info', 'Thinking shown.');
|
|
1724
1723
|
else
|
|
1725
|
-
toast('info', 'Thinking shown
|
|
1724
|
+
toast('info', 'Thinking shown.');
|
|
1726
1725
|
return;
|
|
1727
1726
|
}
|
|
1728
1727
|
if (key.ctrl && (input === 'o' || input === 'O')) {
|
|
@@ -1757,6 +1756,7 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1757
1756
|
if (key.escape) {
|
|
1758
1757
|
const overlay = stateRef.current.overlay;
|
|
1759
1758
|
if (overlay) {
|
|
1759
|
+
cancelConfirmation();
|
|
1760
1760
|
if (overlay === 'confirm' && confirmRef.current) {
|
|
1761
1761
|
confirmRef.current.resolve(false);
|
|
1762
1762
|
setConfirmState(null);
|
|
@@ -1770,65 +1770,49 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1770
1770
|
dispatch({ type: 'setOverlay', overlay: null });
|
|
1771
1771
|
return;
|
|
1772
1772
|
}
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
// `bash` child (which is what actually unblocks a wedged
|
|
1781
|
-
// `await tool.execute`) and bumps the run generation so the unwinding
|
|
1782
|
-
// engine can no longer move the view. The local `runEnd` is dispatched
|
|
1783
|
-
// UNCONDITIONALLY on top: even if the engine is blocked somewhere
|
|
1784
|
-
// nothing can reach, the view returns to `idle`, the composer is usable,
|
|
1785
|
-
// and the reducer releases every live tail. Making the view's usability
|
|
1786
|
-
// conditional on the engine recovering is what produced the reported
|
|
1787
|
-
// screenshot (D-6).
|
|
1788
|
-
//
|
|
1789
|
-
// AND THE NEXT MESSAGE MUST STILL WORK. `AgentController.prompt()` waits
|
|
1790
|
-
// a still-running engine out and never rejects (I-8) - without that,
|
|
1791
|
-
// "Esc, Esc, then type" would reach `Agent.prompt()`'s synchronous throw,
|
|
1792
|
-
// become an unhandled rejection, and EXIT THE PROCESS (P0-1).
|
|
1793
|
-
if (escArmed.current) {
|
|
1794
|
-
disarmEsc();
|
|
1795
|
-
controller.forceStop();
|
|
1796
|
-
endReasonRef.current.aborted = true;
|
|
1797
|
-
dispatch({ type: 'abortMark' });
|
|
1798
|
-
dispatch({ type: 'runEnd' });
|
|
1799
|
-
toast('warn', 'Force-stopped.');
|
|
1800
|
-
return;
|
|
1801
|
-
}
|
|
1802
|
-
controller.abort();
|
|
1803
|
-
// Next to `abortMark`, in the same callback, for the reason
|
|
1804
|
-
// `endReasonRef`'s own comment gives (C-11). D-5 turns this into
|
|
1805
|
-
// silence at `agent_end`: the user's Esc is the statement, and
|
|
1806
|
-
// restating what they interrupted is the CLI arguing.
|
|
1807
|
-
endReasonRef.current.aborted = true;
|
|
1808
|
-
dispatch({ type: 'abortMark' });
|
|
1809
|
-
escArmed.current = true;
|
|
1810
|
-
if (escArmTimer.current)
|
|
1811
|
-
clearTimeout(escArmTimer.current);
|
|
1812
|
-
escArmTimer.current = setTimeout(() => {
|
|
1813
|
-
escArmed.current = false;
|
|
1814
|
-
escArmTimer.current = null;
|
|
1815
|
-
}, ESC_ARM_MS);
|
|
1816
|
-
toast('info', 'Run aborted. Esc again to force-stop.');
|
|
1817
|
-
return;
|
|
1818
|
-
}
|
|
1819
|
-
// IDLE. Today this branch does nothing at all, which is what makes it free
|
|
1820
|
-
// to take (R-4). OVERLAY PRECEDENCE IS UNCHANGED AND DELIBERATE (P2-9):
|
|
1821
|
-
// the block above returns while an overlay is open, so with one open the
|
|
1822
|
-
// first Esc closes the overlay and the second cancels the continuation.
|
|
1823
|
-
// Reaching past it would make "close this dialog" silently also mean
|
|
1824
|
-
// "abandon the plan".
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1775
|
+
});
|
|
1776
|
+
const handleComposerEscape = useCallback(() => {
|
|
1777
|
+
if (stateRef.current.overlay)
|
|
1778
|
+
return;
|
|
1779
|
+
if (interactionPhase.current === 'idle') {
|
|
1825
1780
|
if (followTimer.current) {
|
|
1826
1781
|
cancelFollowThrough();
|
|
1827
|
-
|
|
1782
|
+
dispatch({ type: 'pushToast', level: 'info', text: 'Auto-continue cancelled.' });
|
|
1828
1783
|
}
|
|
1829
1784
|
return;
|
|
1830
1785
|
}
|
|
1831
|
-
|
|
1786
|
+
const sequence = submissionSequence.current;
|
|
1787
|
+
const phase = interactionPhase.current;
|
|
1788
|
+
const next = advanceInterruptGesture(interruptGesture.current, performance.now());
|
|
1789
|
+
interruptGesture.current = next.state;
|
|
1790
|
+
if (next.action === 'hint') {
|
|
1791
|
+
dispatch({ type: 'pushToast', level: 'info',
|
|
1792
|
+
text: 'Press Esc again within 1.5s to interrupt.' });
|
|
1793
|
+
return;
|
|
1794
|
+
}
|
|
1795
|
+
endReasonRef.current.aborted = true;
|
|
1796
|
+
dispatch({ type: 'abortMark' });
|
|
1797
|
+
if (next.action === 'force-stop' || phase === 'starting') {
|
|
1798
|
+
submissionSequence.current += 1;
|
|
1799
|
+
interactionPhase.current = 'idle';
|
|
1800
|
+
resetInterrupt();
|
|
1801
|
+
autoContinuationRef.current = false;
|
|
1802
|
+
if (next.action === 'force-stop')
|
|
1803
|
+
controller.forceStop();
|
|
1804
|
+
else
|
|
1805
|
+
controller.abort();
|
|
1806
|
+
dispatch({ type: 'runEnd' });
|
|
1807
|
+
dispatch({ type: 'pushToast', level: 'warn', text: next.action === 'force-stop' ? 'Force-stopped.' : 'Pending run cancelled.' });
|
|
1808
|
+
return;
|
|
1809
|
+
}
|
|
1810
|
+
controller.abort();
|
|
1811
|
+
if (sequence === submissionSequence.current && interactionPhase.current === 'running') {
|
|
1812
|
+
dispatch({ type: 'pushToast', level: 'info',
|
|
1813
|
+
text: 'Interrupt requested. Esc again to force-stop.' });
|
|
1814
|
+
}
|
|
1815
|
+
}, [controller, cancelFollowThrough, resetInterrupt]);
|
|
1832
1816
|
// --- Render ------------------------------------------------------------
|
|
1833
1817
|
// `const modelInfo = controller.getModelInfo()` AND ITS `modelKnown` COMPANION
|
|
1834
1818
|
// USED TO LIVE HERE (context-usage-gauge-accuracy §4.3). They fed the status
|
|
@@ -1883,39 +1867,25 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1883
1867
|
// milliseconds because `ToolCard` is `React.memo`'d with the default
|
|
1884
1868
|
// comparator; `Transcript` passes it on only to running tool entries.
|
|
1885
1869
|
const nowSec = running || compactionLive ? Math.floor(Date.now() / 1000) : undefined;
|
|
1886
|
-
// Row budget for the viewport and everything sized against it (§4.2a). The
|
|
1887
|
-
// inline path has no fixed frame, hence no height to fit into: `Infinity`
|
|
1888
|
-
// tells `OverlayFrame` to render everything and show no position indicator.
|
|
1889
|
-
// `draftRows` is the number `PromptInput` just reported for the rows it is
|
|
1890
|
-
// ACTUALLY RENDERING (I-8). Defaulting it to 1 is what makes the inline path
|
|
1891
|
-
// and every test that never types byte-identical to before.
|
|
1892
1870
|
const viewportBudget = computeViewportRows(rows, draftRows);
|
|
1893
|
-
const overlayMaxRows =
|
|
1871
|
+
const overlayMaxRows = viewportBudget;
|
|
1894
1872
|
// Renderers consume these same projections. Team capacity does not depend on
|
|
1895
1873
|
// popup occupancy, so a menu cannot toggle the team's collapsed state.
|
|
1874
|
+
// THE ONE CONDITION for "the hint row exists": it sizes the footer below AND
|
|
1875
|
+
// decides whether the run status row may replace that hint row while running,
|
|
1876
|
+
// so starting a run can never add or remove a footer row.
|
|
1877
|
+
const hintRowEnabled = rows >= HINT_MIN_ROWS && cfg.hints;
|
|
1878
|
+
const composerBaseRows = 2 + clampDraftRows(rows, draftRows) + Number(hintRowEnabled);
|
|
1896
1879
|
const teamLayout = buildTeamPanelLayout({
|
|
1897
|
-
snapshot: state.team, terminalRows: rows, availableRows: viewportBudget,
|
|
1880
|
+
snapshot: state.team, terminalRows: rows, availableRows: Math.max(0, viewportBudget - composerBaseRows),
|
|
1898
1881
|
});
|
|
1899
1882
|
const railLayout = buildTodoRailLayout({
|
|
1900
|
-
|
|
1883
|
+
cols: cols - 1, panelEnabled: cfg.todo.panel, overlayOpen: false,
|
|
1901
1884
|
itemCount: state.todos?.items.length ?? 0, viewportBudget,
|
|
1902
|
-
teamRows: teamLayout.rowCount, popupRows,
|
|
1885
|
+
teamRows: teamLayout.rowCount, popupRows, composerBaseRows,
|
|
1903
1886
|
});
|
|
1904
|
-
//
|
|
1905
|
-
// overlays keep the full terminal width. Overlay visibility is in the gate.
|
|
1887
|
+
// Transcript and composer share contentCols; fixed chrome and overlays use terminal width.
|
|
1906
1888
|
const { visible: showRail, width: railWidth, rows: railRows, contentCols } = railLayout;
|
|
1907
|
-
// The inline plan strip (todo-plan-followthrough §3.7 / W2).
|
|
1908
|
-
//
|
|
1909
|
-
// DELIBERATELY NOT `!showRail`, which is true for FOUR different reasons and
|
|
1910
|
-
// three of them mean "do not draw this". `cfg.todo.panel` is included because
|
|
1911
|
-
// `--no-todo-panel` means "keep the planning, drop the display" and a strip is
|
|
1912
|
-
// a display. Overlays are NOT consulted: inline mode's overlays are part of
|
|
1913
|
-
// the document flow, not a modal that owns the screen.
|
|
1914
|
-
//
|
|
1915
|
-
// `cfg` is the right read here and `controller.getTodoConfig()` would be
|
|
1916
|
-
// needless — this runs in RENDER scope, where `cfg` is re-read every frame.
|
|
1917
|
-
// The asymmetry with the `agent_end` handler above is the whole of P1-1.
|
|
1918
|
-
const showStrip = !fullscreen && cfg.todo.panel && state.todos !== null;
|
|
1919
1889
|
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: {
|
|
1920
1890
|
provider: cfg.provider,
|
|
1921
1891
|
model: cfg.model,
|
|
@@ -1949,22 +1919,11 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1949
1919
|
// Read from `cfg` (render scope, re-read every frame), so `/retry off`
|
|
1950
1920
|
// while the screen is open shows the new value on the next paint.
|
|
1951
1921
|
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;
|
|
1952
|
-
|
|
1953
|
-
// entering and leaving it on every drag is worse than a placeholder — and take
|
|
1954
|
-
// the height from `frameHeight()` like everything else, because this is the
|
|
1955
|
-
// one path that reaches rows < 12 and a hard-coded height here would land on
|
|
1956
|
-
// `outputHeight >= rows` exactly when the user is dragging the window (§4.11).
|
|
1957
|
-
if (fullscreen && rows < MIN_FULLSCREEN_ROWS) {
|
|
1958
|
-
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."] }) }));
|
|
1959
|
-
}
|
|
1960
|
-
// Full-screen: a constant one-row brand bar at every size, which is what makes
|
|
1961
|
-
// the viewport monotonic and the first submit jump-free (§4.2). Inline keeps
|
|
1962
|
-
// its v0.3.0 ternary and never calls `pickHeaderVariant`.
|
|
1963
|
-
const header = (_jsx(Header, { version: version, cwd: controller.getCwd(), provider: cfg.provider, model: cfg.model, hasKey: hasKey, variant: fullscreen ? pickHeaderVariant(cols) : empty ? 'banner' : 'bar', theme: theme, caps: caps }));
|
|
1922
|
+
const header = (_jsx(Header, { cwd: controller.getCwd(), provider: cfg.provider, model: cfg.model, hasKey: hasKey, variant: pickHeaderVariant(cols), theme: theme, caps: caps }));
|
|
1964
1923
|
// The wordmark + getting-started card now live INSIDE the viewport, as its
|
|
1965
1924
|
// first block of content, so they scroll away instead of being yanked out of
|
|
1966
1925
|
// the header the moment the first message lands.
|
|
1967
|
-
const opener = empty ? (_jsx(SessionOpener, { variant:
|
|
1926
|
+
const opener = empty ? (_jsx(SessionOpener, { variant: pickOpenerVariant(viewportBudget, contentCols, caps), version: version, cwd: controller.getCwd(), hasKey: hasKey, viewportRows: viewportBudget, theme: theme, caps: caps })) : null;
|
|
1968
1927
|
const density = cfg.density;
|
|
1969
1928
|
const transcriptWindow = cfg.transcriptWindow ?? DEFAULT_TRANSCRIPT_WINDOW;
|
|
1970
1929
|
// --- One spinner per run (single-spinner-while-running D-1 .. D-5). ------
|
|
@@ -2002,6 +1961,26 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
2002
1961
|
// threading the raw flag into a fifth consumer is the one silent way back to
|
|
2003
1962
|
// two spinners (R-2).
|
|
2004
1963
|
const viewReducedMotion = reducedMotion || activityVisible;
|
|
1964
|
+
// The run status row (spinner + phrase + steer/interrupt hint) replaces the idle
|
|
1965
|
+
// hint row ABOVE the input while running; it exists only when that hint row
|
|
1966
|
+
// would, so the footer height is identical with and without it. `running`, not
|
|
1967
|
+
// `activityVisible`: an overlay must not flip the Composer between two layouts.
|
|
1968
|
+
const runRowEnabled = running && hintRowEnabled;
|
|
1969
|
+
// The raw `reducedMotion` flag lives here and nowhere else (D-5).
|
|
1970
|
+
const runActivity = {
|
|
1971
|
+
startedAt: runStartedAt.current,
|
|
1972
|
+
elapsedMs,
|
|
1973
|
+
reducedMotion,
|
|
1974
|
+
runningTool,
|
|
1975
|
+
// THE ROW MUST NEVER SAY SOMETHING FALSE (§6.1). Compaction runs BEFORE
|
|
1976
|
+
// `turn_start`, so `status` is `'running'` with no tool in flight - precisely
|
|
1977
|
+
// the window in which the rotating phrase would claim the model is thinking
|
|
1978
|
+
// while it is actually waiting on a 30k-token summarization.
|
|
1979
|
+
compacting: state.compaction?.inFlight === true,
|
|
1980
|
+
};
|
|
1981
|
+
// The fixed bottom row takes over the life signal once the run row scrolls out
|
|
1982
|
+
// of the viewport; exactly one of the two ever animates.
|
|
1983
|
+
const runRowShown = runRowEnabled && activityRowVisible;
|
|
2005
1984
|
// Assigned during render so `/perf` reads the CURRENT frame (see `perfRef`).
|
|
2006
1985
|
// `mountedEntries` is the same number `render-budget.test.tsx` asserts (AC-9).
|
|
2007
1986
|
perfRef.current = {
|
|
@@ -2016,15 +1995,18 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
2016
1995
|
heightsMeasured: heights.stats().measured,
|
|
2017
1996
|
heightsEstimated: heights.stats().estimated,
|
|
2018
1997
|
cols: contentCols,
|
|
2019
|
-
mode:
|
|
2020
|
-
viewportRows:
|
|
1998
|
+
mode: 'fullscreen',
|
|
1999
|
+
viewportRows: viewportBudget,
|
|
2021
2000
|
offset: scrolledLines,
|
|
2001
|
+
// A getter: the bridge's `frameReady` and geometry change outside React renders.
|
|
2002
|
+
get scrollbar() {
|
|
2003
|
+
return readScrollbarPerf({
|
|
2004
|
+
bridge: terminal?.scrollbar, diffRender: cfg.diffRender, unicode: caps.unicode,
|
|
2005
|
+
});
|
|
2006
|
+
},
|
|
2022
2007
|
};
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
// rows; unbounded, it pushes the status bar past the frame height where
|
|
2026
|
-
// `overflow: hidden` clips it away (R-14 / M-14). Cap it against the viewport.
|
|
2027
|
-
const popupMaxRows = fullscreen ? Math.max(1, viewportBudget - 4) : undefined;
|
|
2008
|
+
// Completion belongs to the document footer; cap it to keep the editor usable.
|
|
2009
|
+
const popupMaxRows = Math.max(1, viewportBudget - 4);
|
|
2028
2010
|
// WHICH KEY THE HINT ROW NAMES (shift-tab-mode-toggle-still-dead-on-windows,
|
|
2029
2011
|
// C3-1). Derived from `vtInputWarning` rather than from a probe of its own,
|
|
2030
2012
|
// because that prop is already the single answer to "can this console deliver
|
|
@@ -2035,26 +2017,13 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
2035
2017
|
// Both keys always WORK (they share one branch in the input handler); this
|
|
2036
2018
|
// only decides which one the user is taught.
|
|
2037
2019
|
const modeToggleKey = vtInputWarning ? MODE_TOGGLE_KEYS.fallback : MODE_TOGGLE_KEYS.primary;
|
|
2038
|
-
const composer =
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
// necessary — P2-5.)
|
|
2046
|
-
//
|
|
2047
|
-
// THE CONDITION IS `activityVisible`, THE SAME CONST THE SUPPRESSION
|
|
2048
|
-
// SIGNAL IS DERIVED FROM (D-4): inlining it here again is how the two
|
|
2049
|
-
// drift apart, and the drift is silent in whichever branch is not
|
|
2050
|
-
// edited.
|
|
2051
|
-
activity: activityVisible ? (_jsx(ActivityLine, { startedAt: runStartedAt.current, elapsedMs: elapsedMs, reducedMotion: reducedMotion, runningTool: runningTool,
|
|
2052
|
-
// THE ROW MUST NEVER SAY SOMETHING FALSE (§6.1). Compaction runs
|
|
2053
|
-
// BEFORE `turn_start`, so `status` is `'running'` with no tool in
|
|
2054
|
-
// flight — precisely the window in which the rotating phrase
|
|
2055
|
-
// would claim the model is thinking while it is actually waiting
|
|
2056
|
-
// on a 30k-token summarization.
|
|
2057
|
-
compacting: state.compaction?.inFlight === true, theme: theme, caps: caps })) : null,
|
|
2020
|
+
const composer = (_jsx(Composer, { measureRef: composerRef, runRow: runRowEnabled
|
|
2021
|
+
? { activity: runActivity, live: runRowShown && !overlayNode, rowRef: activityRef }
|
|
2022
|
+
: null, cols: contentCols, cursorVisible: cursorVisible, onInteraction: returnToComposer, reducedMotion: cfg.reducedMotion ?? false, onEscape: handleComposerEscape, onEscapeDismiss: cancelConfirmation, isActive: overlay === null && !tooSmall, running: state.status === 'running', history: promptHistory, commands: commandOptions, cwd: controller.getCwd(), showHint: rows >= HINT_MIN_ROWS, submitCount: cfg.submitCount, hintsEnabled: cfg.hints, agentMode: state.agentMode, services: liveServices, modeToggleKey: modeToggleKey, popupMaxRows: popupMaxRows, popupMaxHeight: railLayout.popupMaxHeight, onPopupRowsChange: onPopupRowsChange, scrolledLines: scrolledLines, onDraftRows: onDraftRows, onNotice: notify, theme: theme, caps: caps, onSubmit: (text) => void handleSubmit(text), onHelp: () => dispatch({ type: 'setOverlay', overlay: 'help' }) }));
|
|
2023
|
+
const team = state.team ? (_jsx(TeamPanel, { snapshot: state.team, layout: teamLayout, rows: rows, cols: contentCols, reducedMotion: viewReducedMotion, theme: theme, caps: caps })) : null;
|
|
2024
|
+
const rail = showRail && state.todos ? (_jsx(TodoPanel, { snapshot: state.todos, width: railWidth, rows: railRows, running: running, reducedMotion: viewReducedMotion, theme: theme, caps: caps })) : null;
|
|
2025
|
+
const viewport = (_jsxs(ScrollViewport, { active: overlay === null && !tooSmall, overlay: tooSmall ? null : overlayNode, footer: _jsxs(_Fragment, { children: [team, composer] }), rail: rail, scrollbar: terminal?.scrollbar, composerRef: composerRef, onComposerVisibilityChange: setCursorVisible, activityRef: activityRef, onActivityVisibilityChange: setActivityRowVisible, resetKey: scrollResetKey, intent: scrollIntent, pinToBottomNonce: pinToBottomNonce, onScrolledLinesChange: setScrolledLines, onViewportShiftChange: onViewportShiftChange, tailRowsRef: tailSink, hold: selectionHold, resumeMs: cfg.scrollResumeMs, showScrollIndicator: true, cols: contentCols, theme: theme, caps: caps, children: [opener, _jsx(TranscriptList, { entries: state.entries, expandedToolIds: state.expandedToolIds, thinkingVisible: state.thinkingVisible, reducedMotion: viewReducedMotion, density: density, nowSec: nowSec, theme: theme, caps: caps, windowSize: transcriptWindow, cols: contentCols, heights: heights, mountedSink: mountedCount, tailSink: tailSink })] }));
|
|
2026
|
+
return (_jsx(AppShell, { inactive: tooSmall, placeholder: _jsxs(Text, { wrap: "truncate", color: theme.noticeWarn, children: [glyphs.warn, " Terminal too small - needs at least 40 columns and 12 rows."] }), rows: rows, cols: cols, header: header, viewport: viewport, toast: _jsx(BottomStatusRow, { toasts: state.toasts, activity: activityVisible && !runRowShown ? (_jsx(ActivityLine, { ...runActivity, theme: theme, caps: caps })) : null,
|
|
2058
2027
|
// THE FORM THE LIFE SIGNAL TAKES WHEN A TOAST HAS THE ROW
|
|
2059
2028
|
// (`activity-spinner-vanishes-behind-toast`). Passed UNCONDITIONALLY:
|
|
2060
2029
|
// `BottomStatusRow` reads it only when `activity` above is non-null,
|
|
@@ -2074,7 +2043,14 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
2074
2043
|
// branch replaces the whole viewport with the overlay, and an update
|
|
2075
2044
|
// notice under a settings screen is noise attached to a surface it is
|
|
2076
2045
|
// not visible on.
|
|
2077
|
-
|
|
2046
|
+
//
|
|
2047
|
+
// `!running` (cli-auto-update D-2): the update notice stays silent for
|
|
2048
|
+
// the whole run. It used to be guaranteed by the activity line
|
|
2049
|
+
// out-ranking it inside this row; with the activity line moved above
|
|
2050
|
+
// the input, the row can be free during a run, so the silence is stated
|
|
2051
|
+
// here instead of inherited.
|
|
2052
|
+
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, { interruptHint: interruptGesture.current.phase === 'stopping'
|
|
2053
|
+
? 'Esc force-stop' : 'Esc x2 interrupt', model: cfg.model, provider: cfg.provider, usageTotal: state.usageTotal,
|
|
2078
2054
|
// ONE OBJECT INSTEAD OF FOUR PARALLEL PROPS (§4.3). The four were a
|
|
2079
2055
|
// projection of "many sources of truth" into the props layer, and a
|
|
2080
2056
|
// caller could update three of them; a single reading cannot be
|