@aragon-agent/cli 0.6.7 → 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/README.md +72 -43
- package/dist/agent/controller.d.ts +10 -6
- 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/cli.js +83 -53
- package/dist/commands/builtins.js +3 -1
- package/dist/commands/registry.d.ts +2 -3
- 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/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 +166 -128
- package/dist/ui/Composer.d.ts +11 -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 +9 -6
- package/dist/ui/PromptInput.js +34 -14
- package/dist/ui/StatusBar.d.ts +1 -0
- package/dist/ui/StatusBar.js +14 -8
- package/dist/ui/Transcript.js +1 -0
- package/dist/ui/composer-rows.js +50 -9
- 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/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/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 +1 -0
- package/dist/ui/layout/todo-layout.js +3 -2
- 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/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,6 +8,7 @@ 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';
|
|
@@ -53,8 +54,8 @@ import { useTerminalSize } from './layout/useTerminalSize.js';
|
|
|
53
54
|
import { useHeightStore } from './use-height-store.js';
|
|
54
55
|
import { useRenderGovernor } from './use-render-governor.js';
|
|
55
56
|
import { setPerfResetHook, setPerfSnapshotProvider } from '../commands/perf.js';
|
|
56
|
-
import {
|
|
57
|
-
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';
|
|
58
59
|
import { OVERLAY_PAGE, useWheelRouting } from './use-wheel-routing.js';
|
|
59
60
|
import { installConsoleBridge } from './console-bridge.js';
|
|
60
61
|
import { publishExitSnapshot } from './exit-snapshot.js';
|
|
@@ -105,15 +106,6 @@ export function resolveSettingsMaxTokens(raw, previous) {
|
|
|
105
106
|
toast: ['warn', 'Max tokens must be a number or "auto".'],
|
|
106
107
|
};
|
|
107
108
|
}
|
|
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
109
|
/** Services in a non-terminal state, from the live snapshot map. */
|
|
118
110
|
function liveServiceCount(services) {
|
|
119
111
|
let n = 0;
|
|
@@ -135,6 +127,9 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
135
127
|
const { stdout } = useStdout();
|
|
136
128
|
const fullscreen = mode === 'fullscreen';
|
|
137
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);
|
|
138
133
|
// --- Render governor (tui-render-performance L4). ------------------------
|
|
139
134
|
//
|
|
140
135
|
// FIRST, BEFORE ANY OTHER WORK IN THIS BODY. The hook stamps
|
|
@@ -177,6 +172,7 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
177
172
|
// Scroll plumbing: intent goes down, one derived display number comes back.
|
|
178
173
|
const [scrollIntent, setScrollIntent] = useState();
|
|
179
174
|
const [pinToBottomNonce, setPinToBottomNonce] = useState(0);
|
|
175
|
+
const [scrollResetKey, setScrollResetKey] = useState(0);
|
|
180
176
|
const [scrolledLines, setScrolledLines] = useState(0);
|
|
181
177
|
/**
|
|
182
178
|
* True while a selection drag holds the viewport
|
|
@@ -216,18 +212,10 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
216
212
|
confirmRef.current = confirmState;
|
|
217
213
|
const ctrlCArmed = useRef(false);
|
|
218
214
|
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);
|
|
215
|
+
const interruptGesture = useRef({ phase: 'ready' });
|
|
216
|
+
const interactionPhase = useRef(controller.isRunning() ? 'running' : 'idle');
|
|
217
|
+
const submissionSequence = useRef(0);
|
|
218
|
+
const mounted = useRef(true);
|
|
231
219
|
/**
|
|
232
220
|
* Live service snapshots, keyed by id, for the status chip and the Ctrl+C rung.
|
|
233
221
|
*
|
|
@@ -331,20 +319,27 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
331
319
|
aborted: false,
|
|
332
320
|
errored: false,
|
|
333
321
|
});
|
|
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
|
-
}
|
|
322
|
+
const resetInterrupt = useCallback(() => {
|
|
323
|
+
interruptGesture.current = { phase: 'ready' };
|
|
347
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]);
|
|
348
343
|
/**
|
|
349
344
|
* Disarm an armed continuation. Idempotent; NEVER touches the budget — a
|
|
350
345
|
* cancelled continuation was never issued, so it must not be charged (AC-18).
|
|
@@ -437,7 +432,7 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
437
432
|
// emits while it unwinds - `turn_start`, `tool_call_start`, `turn_end`,
|
|
438
433
|
// `agent_end` - belongs to the run the user just abandoned. `runStart` /
|
|
439
434
|
// `turnStart` would set `status: 'running'` again, bouncing the view straight
|
|
440
|
-
// back out of the `idle`
|
|
435
|
+
// back out of the `idle` force-stop just gave them, and stray entries would
|
|
441
436
|
// append to a transcript they believe is finished.
|
|
442
437
|
//
|
|
443
438
|
// A LOCAL `let`, RE-READ RATHER THAN CAPTURED ONCE: the generation this
|
|
@@ -453,6 +448,12 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
453
448
|
generation = controller.runGeneration;
|
|
454
449
|
return;
|
|
455
450
|
}
|
|
451
|
+
if (event.type === 'agent_start')
|
|
452
|
+
interactionPhase.current = 'running';
|
|
453
|
+
if (event.type === 'agent_end') {
|
|
454
|
+
interactionPhase.current = 'idle';
|
|
455
|
+
resetInterrupt();
|
|
456
|
+
}
|
|
456
457
|
const cost = controller.getModelInfo().cost;
|
|
457
458
|
for (const action of reduceEvent(event, cost, patchSource)) {
|
|
458
459
|
// MUTATED HERE, ONE INSTANT BEFORE THE ACTION THAT SETS
|
|
@@ -479,11 +480,6 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
479
480
|
}
|
|
480
481
|
if (event.type !== 'agent_end')
|
|
481
482
|
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
483
|
// NO OVERLAY OUTLIVES ITS RUN (P1-4). `ctx.signal` already covers the
|
|
488
484
|
// abort path, so this is DELIBERATE REDUNDANCY: it makes "the card cannot
|
|
489
485
|
// survive the run that opened it" a property of the App's lifecycle
|
|
@@ -551,7 +547,7 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
551
547
|
done: live.doneCount,
|
|
552
548
|
mode: 'auto',
|
|
553
549
|
});
|
|
554
|
-
submitRef.current(decision.message, { userInitiated: false });
|
|
550
|
+
submitRef.current(decision.message, { userInitiated: false, todoPolicy: 'continue' });
|
|
555
551
|
}, decision.graceMs);
|
|
556
552
|
}
|
|
557
553
|
// A deferred `plan -> build` becomes real here and nowhere else (§3.2).
|
|
@@ -573,7 +569,7 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
573
569
|
cancelFollowThrough();
|
|
574
570
|
unsubscribe();
|
|
575
571
|
};
|
|
576
|
-
}, [controller, patchSource, flushPending, cancelPendingHuman, cancelFollowThrough,
|
|
572
|
+
}, [controller, patchSource, flushPending, cancelPendingHuman, cancelFollowThrough, resetInterrupt]);
|
|
577
573
|
// --- Background services (background-service-supervision §3.8). ---------
|
|
578
574
|
//
|
|
579
575
|
// A SEPARATE SUBSCRIPTION, not a branch in the one above: `ProcEvent` is
|
|
@@ -1278,9 +1274,9 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1278
1274
|
return;
|
|
1279
1275
|
// An overlay replaces the viewport, so a selection over it would be a
|
|
1280
1276
|
// highlight over rows that are no longer there (N4).
|
|
1281
|
-
selectionController.setEnabled(state.overlay === null);
|
|
1277
|
+
selectionController.setEnabled(state.overlay === null && !tooSmall);
|
|
1282
1278
|
selectionController.clear();
|
|
1283
|
-
}, [selectionController, state.overlay]);
|
|
1279
|
+
}, [selectionController, state.overlay, tooSmall]);
|
|
1284
1280
|
const persistConfig = (patch) => {
|
|
1285
1281
|
try {
|
|
1286
1282
|
updatePersistedConfig(patch);
|
|
@@ -1336,7 +1332,22 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1336
1332
|
args,
|
|
1337
1333
|
controller,
|
|
1338
1334
|
state: stateRef.current,
|
|
1339
|
-
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
|
+
},
|
|
1340
1351
|
setOverlay: (overlay) => dispatch({ type: 'setOverlay', overlay }),
|
|
1341
1352
|
notify,
|
|
1342
1353
|
toast,
|
|
@@ -1401,11 +1412,19 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1401
1412
|
// line, before the steering branch: a message typed during the grace window
|
|
1402
1413
|
// must be the one that runs, whichever branch it takes.
|
|
1403
1414
|
cancelFollowThrough();
|
|
1404
|
-
|
|
1415
|
+
cancelConfirmation();
|
|
1416
|
+
if (interactionPhase.current === 'running') {
|
|
1405
1417
|
controller.steer(message);
|
|
1406
1418
|
toast('info', 'Steering queued.');
|
|
1407
1419
|
return;
|
|
1408
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';
|
|
1409
1428
|
// `userInitiated: false` skips TWO things and both are wanted: the
|
|
1410
1429
|
// prompt-history append, and the `submitCount` bump that drives the
|
|
1411
1430
|
// composer's hint fade. An auto-continuation demonstrates nothing about what
|
|
@@ -1420,14 +1439,30 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1420
1439
|
const pre = controller.preflight();
|
|
1421
1440
|
if (!pre.ok) {
|
|
1422
1441
|
notify('error', pre.message ?? 'Configuration error.');
|
|
1442
|
+
interactionPhase.current = 'idle';
|
|
1443
|
+
resetInterrupt();
|
|
1444
|
+
autoContinuationRef.current = false;
|
|
1423
1445
|
dispatch({ type: 'runEnd' }); // reset status; nothing was started
|
|
1424
1446
|
return;
|
|
1425
1447
|
}
|
|
1426
1448
|
// Fire-and-forget: events drive the UI. prompt() never rejects.
|
|
1427
|
-
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
|
+
});
|
|
1428
1459
|
};
|
|
1429
1460
|
submitRef.current = submitMessage;
|
|
1430
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
|
+
}
|
|
1431
1466
|
// Submitting is an unconditional "take me to the newest output" (§4.5).
|
|
1432
1467
|
setPinToBottomNonce((n) => n + 1);
|
|
1433
1468
|
const handled = await runSlashInput(registry, raw, makeCtx);
|
|
@@ -1556,10 +1591,34 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1556
1591
|
// same reason: `OverlayFrame` only ever reports a clamp DOWNWARD.
|
|
1557
1592
|
const { clearCoalescer } = useWheelRouting({
|
|
1558
1593
|
mouseSource,
|
|
1594
|
+
isPointerCaptured: () => terminal?.scrollbar?.controller?.isCaptured() ?? false,
|
|
1595
|
+
isActive: () => !tooSmall,
|
|
1559
1596
|
getOverlay: () => stateRef.current.overlay,
|
|
1560
1597
|
onScroll: scrollBy,
|
|
1561
1598
|
onOverlayScroll: (delta) => setOverlayScroll((n) => Math.max(0, n + delta)),
|
|
1562
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]);
|
|
1563
1622
|
// A fresh overlay always opens at the top; carrying the previous overlay's
|
|
1564
1623
|
// offset over would open Help mid-list for no reason the user can see.
|
|
1565
1624
|
//
|
|
@@ -1585,6 +1644,8 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1585
1644
|
}, []);
|
|
1586
1645
|
// --- Global keys -------------------------------------------------------
|
|
1587
1646
|
useInput((input, key) => {
|
|
1647
|
+
if (!key.escape)
|
|
1648
|
+
cancelConfirmation();
|
|
1588
1649
|
// I-9, the third clear trigger, and FIRST IN THE HANDLER so no `return`
|
|
1589
1650
|
// below can skip it. Any key at all drops the selection: most of them move
|
|
1590
1651
|
// the rows, and the ones that do not have still ended the gesture. It is
|
|
@@ -1631,10 +1692,8 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1631
1692
|
}
|
|
1632
1693
|
// R-P1-7: the overlay branch MUST come before the transcript scroll branch
|
|
1633
1694
|
// 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.
|
|
1695
|
+
// overlay was open. The stable viewport ignores inactive intents, but routing
|
|
1696
|
+
// must still give overlays exclusive ownership of their paging keys.
|
|
1638
1697
|
if (stateRef.current.overlay) {
|
|
1639
1698
|
const overlay = stateRef.current.overlay;
|
|
1640
1699
|
// Modes A only. `model` / `confirm` manage their own keys; registering an
|
|
@@ -1757,6 +1816,7 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1757
1816
|
if (key.escape) {
|
|
1758
1817
|
const overlay = stateRef.current.overlay;
|
|
1759
1818
|
if (overlay) {
|
|
1819
|
+
cancelConfirmation();
|
|
1760
1820
|
if (overlay === 'confirm' && confirmRef.current) {
|
|
1761
1821
|
confirmRef.current.resolve(false);
|
|
1762
1822
|
setConfirmState(null);
|
|
@@ -1770,65 +1830,49 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1770
1830
|
dispatch({ type: 'setOverlay', overlay: null });
|
|
1771
1831
|
return;
|
|
1772
1832
|
}
|
|
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".
|
|
1833
|
+
return;
|
|
1834
|
+
}
|
|
1835
|
+
});
|
|
1836
|
+
const handleComposerEscape = useCallback(() => {
|
|
1837
|
+
if (stateRef.current.overlay)
|
|
1838
|
+
return;
|
|
1839
|
+
if (interactionPhase.current === 'idle') {
|
|
1825
1840
|
if (followTimer.current) {
|
|
1826
1841
|
cancelFollowThrough();
|
|
1827
|
-
|
|
1842
|
+
dispatch({ type: 'pushToast', level: 'info', text: 'Auto-continue cancelled.' });
|
|
1828
1843
|
}
|
|
1829
1844
|
return;
|
|
1830
1845
|
}
|
|
1831
|
-
|
|
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]);
|
|
1832
1876
|
// --- Render ------------------------------------------------------------
|
|
1833
1877
|
// `const modelInfo = controller.getModelInfo()` AND ITS `modelKnown` COMPANION
|
|
1834
1878
|
// USED TO LIVE HERE (context-usage-gauge-accuracy §4.3). They fed the status
|
|
@@ -1893,16 +1937,17 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1893
1937
|
const overlayMaxRows = fullscreen ? viewportBudget : Number.POSITIVE_INFINITY;
|
|
1894
1938
|
// Renderers consume these same projections. Team capacity does not depend on
|
|
1895
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);
|
|
1896
1942
|
const teamLayout = buildTeamPanelLayout({
|
|
1897
|
-
snapshot: state.team, terminalRows: rows, availableRows: viewportBudget,
|
|
1943
|
+
snapshot: state.team, terminalRows: rows, availableRows: Math.max(0, viewportBudget - composerBaseRows),
|
|
1898
1944
|
});
|
|
1899
1945
|
const railLayout = buildTodoRailLayout({
|
|
1900
|
-
mode, cols, panelEnabled: cfg.todo.panel, overlayOpen:
|
|
1946
|
+
mode, cols: fullscreen ? cols - 1 : cols, panelEnabled: cfg.todo.panel, overlayOpen: false,
|
|
1901
1947
|
itemCount: state.todos?.items.length ?? 0, viewportBudget,
|
|
1902
|
-
teamRows: teamLayout.rowCount, popupRows,
|
|
1948
|
+
teamRows: teamLayout.rowCount, popupRows, composerBaseRows,
|
|
1903
1949
|
});
|
|
1904
|
-
//
|
|
1905
|
-
// overlays keep the full terminal width. Overlay visibility is in the gate.
|
|
1950
|
+
// Transcript and composer share contentCols; fixed chrome and overlays use terminal width.
|
|
1906
1951
|
const { visible: showRail, width: railWidth, rows: railRows, contentCols } = railLayout;
|
|
1907
1952
|
// The inline plan strip (todo-plan-followthrough §3.7 / W2).
|
|
1908
1953
|
//
|
|
@@ -1949,14 +1994,6 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1949
1994
|
// Read from `cfg` (render scope, re-read every frame), so `/retry off`
|
|
1950
1995
|
// while the screen is open shows the new value on the next paint.
|
|
1951
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;
|
|
1952
|
-
// A resize can drop below the usable floor. Do NOT leave the alt-screen —
|
|
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
1997
|
// Full-screen: a constant one-row brand bar at every size, which is what makes
|
|
1961
1998
|
// the viewport monotonic and the first submit jump-free (§4.2). Inline keeps
|
|
1962
1999
|
// its v0.3.0 ternary and never calls `pickHeaderVariant`.
|
|
@@ -2020,10 +2057,7 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
2020
2057
|
viewportRows: fullscreen ? viewportBudget : rows,
|
|
2021
2058
|
offset: scrolledLines,
|
|
2022
2059
|
};
|
|
2023
|
-
|
|
2024
|
-
// The completion popup lives inside the bottom chrome and can add up to 9
|
|
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.
|
|
2060
|
+
// Completion belongs to the document footer; cap it to keep the editor usable.
|
|
2027
2061
|
const popupMaxRows = fullscreen ? Math.max(1, viewportBudget - 4) : undefined;
|
|
2028
2062
|
// WHICH KEY THE HINT ROW NAMES (shift-tab-mode-toggle-still-dead-on-windows,
|
|
2029
2063
|
// C3-1). Derived from `vtInputWarning` rather than from a probe of its own,
|
|
@@ -2035,8 +2069,11 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
2035
2069
|
// Both keys always WORK (they share one branch in the input handler); this
|
|
2036
2070
|
// only decides which one the user is taught.
|
|
2037
2071
|
const modeToggleKey = vtInputWarning ? MODE_TOGGLE_KEYS.fallback : MODE_TOGGLE_KEYS.primary;
|
|
2038
|
-
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, 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, { 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' }) }) }));
|
|
2039
|
-
|
|
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,
|
|
2040
2077
|
// `!overlayNode` suppresses the line while an overlay owns the screen:
|
|
2041
2078
|
// the full-screen branch replaces the whole viewport with the overlay,
|
|
2042
2079
|
// and a working line under a settings screen is noise attached to a
|
|
@@ -2074,7 +2111,8 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
2074
2111
|
// branch replaces the whole viewport with the overlay, and an update
|
|
2075
2112
|
// notice under a settings screen is noise attached to a surface it is
|
|
2076
2113
|
// not visible on.
|
|
2077
|
-
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,
|
|
2078
2116
|
// ONE OBJECT INSTEAD OF FOUR PARALLEL PROPS (§4.3). The four were a
|
|
2079
2117
|
// projection of "many sources of truth" into the props layer, and a
|
|
2080
2118
|
// caller could update three of them; a single reading cannot be
|
package/dist/ui/Composer.d.ts
CHANGED
|
@@ -9,20 +9,26 @@
|
|
|
9
9
|
* "the input is no longer empty", and routing that through App would re-render
|
|
10
10
|
* the transcript on the first keystroke of every message.
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* them 31 columns wide, were both redundant and the reason the status bar had a
|
|
15
|
-
* `cols >= 110` breakpoint.
|
|
12
|
+
* The fixed status row retains emergency and history-navigation hints while
|
|
13
|
+
* this footer scrolls outside the viewport. Editor state stays mounted.
|
|
16
14
|
*/
|
|
17
15
|
import React from 'react';
|
|
16
|
+
import { type DOMElement } from 'ink';
|
|
18
17
|
import type { Theme } from './theme.js';
|
|
19
18
|
import type { TermCapabilities } from './capabilities.js';
|
|
20
19
|
import { pickGlyphs } from './glyphs.js';
|
|
21
20
|
import { type CommandOption } from './PromptInput.js';
|
|
22
21
|
import { type AgentMode } from '../agent/agent-mode.js';
|
|
23
22
|
export interface ComposerProps {
|
|
23
|
+
cols?: number;
|
|
24
|
+
cursorVisible?: boolean;
|
|
25
|
+
onInteraction?: () => void;
|
|
26
|
+
measureRef?: React.RefObject<DOMElement>;
|
|
24
27
|
/** False while an overlay owns the keyboard (the input is visually blurred). */
|
|
25
28
|
isActive: boolean;
|
|
29
|
+
reducedMotion?: boolean;
|
|
30
|
+
onEscape?: () => void;
|
|
31
|
+
onEscapeDismiss?: () => void;
|
|
26
32
|
running: boolean;
|
|
27
33
|
history: string[];
|
|
28
34
|
commands: CommandOption[];
|
|
@@ -119,5 +125,5 @@ declare function hintText(opts: {
|
|
|
119
125
|
* than fishing the string out of a rendered frame.
|
|
120
126
|
*/
|
|
121
127
|
export declare const hintTextForTest: typeof hintText;
|
|
122
|
-
export declare function Composer({ isActive, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, services, modeToggleKey, popupMaxRows, popupMaxHeight, onPopupRowsChange, scrolledLines, onDraftRows, onNotice, theme, caps, onSubmit, onHelp, }: ComposerProps): React.ReactElement;
|
|
128
|
+
export declare function Composer({ cols, cursorVisible, onInteraction, measureRef, isActive, reducedMotion, onEscape, onEscapeDismiss, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, services, modeToggleKey, popupMaxRows, popupMaxHeight, onPopupRowsChange, scrolledLines, onDraftRows, onNotice, theme, caps, onSubmit, onHelp, }: ComposerProps): React.ReactElement;
|
|
123
129
|
export {};
|
package/dist/ui/Composer.js
CHANGED
|
@@ -10,10 +10,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
10
10
|
* "the input is no longer empty", and routing that through App would re-render
|
|
11
11
|
* the transcript on the first keystroke of every message.
|
|
12
12
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* them 31 columns wide, were both redundant and the reason the status bar had a
|
|
16
|
-
* `cols >= 110` breakpoint.
|
|
13
|
+
* The fixed status row retains emergency and history-navigation hints while
|
|
14
|
+
* this footer scrolls outside the viewport. Editor state stays mounted.
|
|
17
15
|
*/
|
|
18
16
|
import { useCallback, useState } from 'react';
|
|
19
17
|
import { Box, Text } from 'ink';
|
|
@@ -46,15 +44,13 @@ function hintText(opts) {
|
|
|
46
44
|
if (services > 0) {
|
|
47
45
|
return [
|
|
48
46
|
`${glyphs.enterKey} steer`,
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
// is left exactly where the reported screenshot left them.
|
|
52
|
-
`esc abort${glyphs.times}2 force`,
|
|
47
|
+
// Confirmation is named even when service controls also need space.
|
|
48
|
+
`esc${glyphs.times}2 interrupt`,
|
|
53
49
|
`ctrl+c stop ${services}`,
|
|
54
50
|
`ctrl+c${glyphs.times}2 exit`,
|
|
55
51
|
].join(dot);
|
|
56
52
|
}
|
|
57
|
-
return [`${glyphs.enterKey} steer`,
|
|
53
|
+
return [`${glyphs.enterKey} steer`, `esc${glyphs.times}2 interrupt`, `ctrl+c${glyphs.times}2 exit`].join(dot);
|
|
58
54
|
}
|
|
59
55
|
// The hint names the DESTINATION, not the current state — the correct label
|
|
60
56
|
// for a toggle affordance, and the reason the mode word here is the OPPOSITE
|
|
@@ -90,7 +86,7 @@ function hintText(opts) {
|
|
|
90
86
|
* than fishing the string out of a rendered frame.
|
|
91
87
|
*/
|
|
92
88
|
export const hintTextForTest = hintText;
|
|
93
|
-
export function Composer({ isActive, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, services = 0, modeToggleKey = MODE_TOGGLE_KEYS.primary, popupMaxRows, popupMaxHeight, onPopupRowsChange, scrolledLines = 0, onDraftRows, onNotice, theme, caps, onSubmit, onHelp, }) {
|
|
89
|
+
export function Composer({ cols, cursorVisible, onInteraction, measureRef, isActive, reducedMotion, onEscape, onEscapeDismiss, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, services = 0, modeToggleKey = MODE_TOGGLE_KEYS.primary, popupMaxRows, popupMaxHeight, onPopupRowsChange, scrolledLines = 0, onDraftRows, onNotice, theme, caps, onSubmit, onHelp, }) {
|
|
94
90
|
const [hasDraft, setHasDraft] = useState(false);
|
|
95
91
|
const onDraftChange = useCallback((next) => {
|
|
96
92
|
setHasDraft(next.hasDraft);
|
|
@@ -110,7 +106,7 @@ export function Composer({ isActive, running, history, commands, cwd, showHint,
|
|
|
110
106
|
: theme.idleBorder ?? theme.border;
|
|
111
107
|
const glyphs = pickGlyphs(caps);
|
|
112
108
|
const visible = showHint && hintsEnabled;
|
|
113
|
-
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsx(PromptInput, { isActive: isActive, running: running, history: history, commands: commands, cwd: cwd, theme: theme, caps: caps, popupMaxRows: popupMaxRows, popupMaxHeight: popupMaxHeight, onPopupRowsChange: onPopupRowsChange, onSubmit: onSubmit, onHelp: onHelp, agentMode: agentMode, bordered: { color: borderColor }, onDraftChange: onDraftChange, onNotice: onNotice, scrolledLines: scrolledLines }), visible && (
|
|
109
|
+
return (_jsxs(Box, { ref: measureRef, flexDirection: "column", flexShrink: 0, children: [_jsx(PromptInput, { cols: cols, cursorVisible: cursorVisible, onInteraction: onInteraction, isActive: isActive, reducedMotion: reducedMotion, onEscape: onEscape, onEscapeDismiss: onEscapeDismiss, running: running, history: history, commands: commands, cwd: cwd, theme: theme, caps: caps, popupMaxRows: popupMaxRows, popupMaxHeight: popupMaxHeight, onPopupRowsChange: onPopupRowsChange, onSubmit: onSubmit, onHelp: onHelp, agentMode: agentMode, bordered: { color: borderColor }, onDraftChange: onDraftChange, onNotice: onNotice, scrolledLines: scrolledLines }), visible && (
|
|
114
110
|
// The chip rides the EXISTING hint row rather than adding one, so it
|
|
115
111
|
// costs zero rows on a 24-row terminal.
|
|
116
112
|
_jsxs(Box, { flexDirection: "row", flexShrink: 0, children: [_jsx(Text, { children: ' ' }), _jsx(ModeChip, { mode: agentMode, theme: theme, caps: caps }), _jsx(Text, { wrap: "truncate", color: theme.hintFg ?? theme.muted, children: hintText({
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
export interface PromptCaretProps {
|
|
3
|
+
text: string;
|
|
4
|
+
active: boolean;
|
|
5
|
+
reducedMotion: boolean;
|
|
6
|
+
colorLevel: 0 | 1 | 2 | 3;
|
|
7
|
+
color?: string;
|
|
8
|
+
resetKey: object;
|
|
9
|
+
}
|
|
10
|
+
/** The only blinking state belongs to this leaf, never to the editor. */
|
|
11
|
+
export declare function PromptCaret({ text, active, reducedMotion, colorLevel, color, resetKey, }: PromptCaretProps): React.ReactElement;
|