@aragon-agent/cli 0.6.8 → 0.6.10
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 +89 -44
- package/dist/agent/controller.d.ts +1 -1
- package/dist/agent/reducer.d.ts +31 -113
- package/dist/agent/reducer.js +41 -63
- package/dist/cli.js +53 -155
- package/dist/commands/builtins.d.ts +2 -1
- package/dist/commands/builtins.js +47 -2
- package/dist/commands/perf.d.ts +32 -2
- package/dist/commands/perf.js +32 -2
- package/dist/commands/registry.d.ts +2 -2
- 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/enter-sequences.d.ts +63 -0
- package/dist/input/enter-sequences.js +151 -0
- package/dist/input/limits.d.ts +11 -0
- package/dist/input/limits.js +11 -0
- package/dist/input/stdin-filter.js +19 -1
- package/dist/logging/install.d.ts +2 -2
- package/dist/logging/install.js +2 -2
- package/dist/session/persist.js +20 -0
- 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/ui/ActivityLine.d.ts +26 -34
- package/dist/ui/ActivityLine.js +30 -19
- package/dist/ui/App.d.ts +1 -41
- package/dist/ui/App.js +94 -129
- package/dist/ui/BottomStatusRow.d.ts +1 -3
- package/dist/ui/BottomStatusRow.js +4 -8
- package/dist/ui/Composer.d.ts +28 -1
- package/dist/ui/Composer.js +46 -17
- 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/PromptInput.d.ts +2 -28
- package/dist/ui/PromptInput.js +38 -9
- package/dist/ui/StatusBar.d.ts +0 -11
- 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 +15 -199
- package/dist/ui/enter-frames.d.ts +63 -0
- package/dist/ui/enter-frames.js +136 -0
- 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/QueuedEntry.d.ts +27 -0
- package/dist/ui/entries/QueuedEntry.js +30 -0
- 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 +36 -6
- package/dist/ui/glyphs.d.ts +9 -0
- package/dist/ui/glyphs.js +2 -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 +1 -10
- package/dist/ui/layout/AppShell.js +2 -5
- package/dist/ui/layout/OverlayFrame.d.ts +1 -25
- package/dist/ui/layout/OverlayFrame.js +0 -24
- package/dist/ui/layout/ScrollViewport.d.ts +3 -0
- package/dist/ui/layout/ScrollViewport.js +17 -5
- package/dist/ui/layout/frame.d.ts +0 -22
- package/dist/ui/layout/frame.js +0 -36
- package/dist/ui/layout/todo-layout.d.ts +1 -3
- package/dist/ui/layout/todo-layout.js +7 -8
- package/dist/ui/layout/viewport-geometry.d.ts +1 -1
- package/dist/ui/layout/viewport-geometry.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 +3 -1
- package/dist/ui/overlays/PlanReviewOverlay.js +5 -1
- package/dist/ui/overlays/QuestionOverlay.js +7 -2
- package/dist/ui/overlays/SettingsScreen.js +8 -1
- package/dist/ui/run-status-row.d.ts +61 -0
- package/dist/ui/run-status-row.js +70 -0
- package/dist/ui/selection/selection-controller.d.ts +23 -7
- package/dist/ui/selection/selection-controller.js +68 -16
- 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 +6 -58
- package/dist/ui/use-startup-notices.js +10 -57
- package/dist/ui/use-wheel-routing.d.ts +1 -1
- 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
|
@@ -7,7 +7,7 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } 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
11
|
import { advanceInterruptGesture, cancelInterruptConfirmation, } from '../input/interrupt-gesture.js';
|
|
12
12
|
import { initialViewState, reduceEvent, viewReducer, } from '../agent/reducer.js';
|
|
13
13
|
import { mergeDeltas } from '../agent/coalesce.js';
|
|
@@ -26,14 +26,12 @@ import { pickGlyphs } from './glyphs.js';
|
|
|
26
26
|
import { Header } from './Header.js';
|
|
27
27
|
import { pickHeaderVariant, pickOpenerVariant } from './Logo.js';
|
|
28
28
|
import { SessionOpener } from './SessionOpener.js';
|
|
29
|
-
import {
|
|
30
|
-
import { PromptInput } from './PromptInput.js';
|
|
29
|
+
import { TranscriptList } from './Transcript.js';
|
|
31
30
|
import { Composer } from './Composer.js';
|
|
32
31
|
import { StatusBar } from './StatusBar.js';
|
|
33
32
|
import { secondsLeft } from '../agent/retry-view.js';
|
|
34
33
|
import { TeamPanel } from './TeamPanel.js';
|
|
35
34
|
import { TodoPanel } from './TodoPanel.js';
|
|
36
|
-
import { TodoStrip } from './TodoStrip.js';
|
|
37
35
|
import { ActivityLine, liveSpinner } from './ActivityLine.js';
|
|
38
36
|
import { BottomStatusRow } from './BottomStatusRow.js';
|
|
39
37
|
import { UpdateLine } from './UpdateLine.js';
|
|
@@ -53,7 +51,7 @@ import { advanceBudget, decideFollowThrough, emptyBudget, } from '../todo/follow
|
|
|
53
51
|
import { useTerminalSize } from './layout/useTerminalSize.js';
|
|
54
52
|
import { useHeightStore } from './use-height-store.js';
|
|
55
53
|
import { useRenderGovernor } from './use-render-governor.js';
|
|
56
|
-
import { setPerfResetHook, setPerfSnapshotProvider } from '../commands/perf.js';
|
|
54
|
+
import { readScrollbarPerf, setPerfResetHook, setPerfSnapshotProvider, } from '../commands/perf.js';
|
|
57
55
|
import { MIN_FULLSCREEN_ROWS, MIN_FULLSCREEN_COLS } from './layout/frame.js';
|
|
58
56
|
import { HINT_MIN_ROWS, clampDraftRows, viewportRows as computeViewportRows } from './layout/budget.js';
|
|
59
57
|
import { OVERLAY_PAGE, useWheelRouting } from './use-wheel-routing.js';
|
|
@@ -70,6 +68,7 @@ import { CommandRegistry, runSlashInput } from '../commands/registry.js';
|
|
|
70
68
|
import { registerBuiltinCommands } from '../commands/builtins.js';
|
|
71
69
|
import { makeSkillsCommand, registerSkillCommands } from '../commands/skills.js';
|
|
72
70
|
import { MODE_LABEL, MODE_TOGGLE_KEYS, nextMode } from '../agent/agent-mode.js';
|
|
71
|
+
import { copyText } from './clipboard.js';
|
|
73
72
|
import { emptyTailState } from './layout/follow-state.js';
|
|
74
73
|
/**
|
|
75
74
|
* Decide what the `Max tokens` field means, WITHOUT letting a bad value in it
|
|
@@ -122,14 +121,18 @@ function liveServiceCount(services) {
|
|
|
122
121
|
function seedViewState(thinkingVisible) {
|
|
123
122
|
return initialViewState({ thinkingVisible });
|
|
124
123
|
}
|
|
125
|
-
export function App({ controller, version,
|
|
124
|
+
export function App({ controller, version, initialOverlay, initialPrompt, confirmBridge, humanInputBridge, mouseSource, vtInputWarning, updateBridge, pasteBridge, terminal, }) {
|
|
126
125
|
const { exit } = useApp();
|
|
127
126
|
const { stdout } = useStdout();
|
|
128
|
-
const fullscreen = mode === 'fullscreen';
|
|
129
127
|
const { rows, cols } = useTerminalSize();
|
|
130
|
-
const tooSmall =
|
|
128
|
+
const tooSmall = (rows < MIN_FULLSCREEN_ROWS || cols < MIN_FULLSCREEN_COLS);
|
|
131
129
|
const composerRef = useRef(null);
|
|
130
|
+
const activityRef = useRef(null);
|
|
132
131
|
const [cursorVisible, setCursorVisible] = useState(true);
|
|
132
|
+
// Whether the run status row above the input is inside the viewport. It is
|
|
133
|
+
// `true` while the row is not mounted (`elementVisible(null)`), so it is NEVER
|
|
134
|
+
// a decision on its own - always AND it with `runRowEnabled` (`runRowShown`).
|
|
135
|
+
const [activityRowVisible, setActivityRowVisible] = useState(true);
|
|
133
136
|
// --- Render governor (tui-render-performance L4). ------------------------
|
|
134
137
|
//
|
|
135
138
|
// FIRST, BEFORE ANY OTHER WORK IN THIS BODY. The hook stamps
|
|
@@ -236,16 +239,6 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
236
239
|
const servicesRef = useRef(services);
|
|
237
240
|
servicesRef.current = services;
|
|
238
241
|
const runBaselineOut = useRef(0);
|
|
239
|
-
/**
|
|
240
|
-
* Epoch ms the current run began — the activity phrase's seed.
|
|
241
|
-
*
|
|
242
|
-
* ASSIGNED IN RENDER SCOPE, NOT IN THE ELAPSED EFFECT (P1-7). That effect runs
|
|
243
|
-
* AFTER the render that first sees `status === 'running'`, so a ref it wrote
|
|
244
|
-
* would be one frame late — and the first frame of a run is exactly when the
|
|
245
|
-
* phrase is chosen. `Transcript.tsx` updates `highWater` / `prevLen` during
|
|
246
|
-
* render for the same reason: a value derived from a transition is needed by
|
|
247
|
-
* the very render that observes the transition.
|
|
248
|
-
*/
|
|
249
242
|
const runStartedAt = useRef(0);
|
|
250
243
|
const toastTimers = useRef(new Map());
|
|
251
244
|
/** Every outstanding `gate.request()`. Held in a ref, never in state: it is
|
|
@@ -499,14 +492,6 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
499
492
|
budgetRef.current = advanceBudget(budgetRef.current, todos, autoContinuationRef.current);
|
|
500
493
|
autoContinuationRef.current = false;
|
|
501
494
|
const decision = decideFollowThrough({
|
|
502
|
-
// FROM THE CONTROLLER, NOT FROM `cfg` (P1-1). This effect's dependency
|
|
503
|
-
// list never changes, so it closes over the `cfg` object from the render
|
|
504
|
-
// in which it mounted — and `setTodoConfig` builds a NEW object, which
|
|
505
|
-
// that closure will never see. `cfg.todo.followThrough` would therefore
|
|
506
|
-
// be pinned to the launch value for the whole session and
|
|
507
|
-
// `/todo follow auto` would report success and do nothing. Note the
|
|
508
|
-
// asymmetry with `showStrip` further down, which is right to use `cfg`
|
|
509
|
-
// because it runs in render scope where `cfg` is re-read every frame.
|
|
510
495
|
mode: controller.getTodoConfig().followThrough,
|
|
511
496
|
snapshot: todos,
|
|
512
497
|
runEnd: { ...endReasonRef.current },
|
|
@@ -570,21 +555,6 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
570
555
|
unsubscribe();
|
|
571
556
|
};
|
|
572
557
|
}, [controller, patchSource, flushPending, cancelPendingHuman, cancelFollowThrough, resetInterrupt]);
|
|
573
|
-
// --- Background services (background-service-supervision §3.8). ---------
|
|
574
|
-
//
|
|
575
|
-
// A SEPARATE SUBSCRIPTION, not a branch in the one above: `ProcEvent` is
|
|
576
|
-
// deliberately not a member of core's `AgentEvent` union (I-5 / D-1), so it
|
|
577
|
-
// cannot arrive through `controller.subscribe`.
|
|
578
|
-
//
|
|
579
|
-
// TWO SINKS PER EVENT, and they are not redundant. `setServices` holds the
|
|
580
|
-
// LIVE state the status chip, the composer hint and the Ctrl+C rung read;
|
|
581
|
-
// `dispatch` writes the TRANSCRIPT, which records events about a service
|
|
582
|
-
// rather than mirroring it (D-11). Feeding one from the other would make the
|
|
583
|
-
// chip lie about a card that has already been printed into `<Static>`.
|
|
584
|
-
//
|
|
585
|
-
// NOT ROUTED THROUGH THE STREAMING COALESCER. `output` is already coalesced at
|
|
586
|
-
// the SOURCE, inside the supervisor, because `/bg` and `bash_output` consume
|
|
587
|
-
// the same event (§6); a second buffer here would only add latency.
|
|
588
558
|
useEffect(() => {
|
|
589
559
|
return controller.subscribeProc((event) => {
|
|
590
560
|
const service = event.service;
|
|
@@ -666,9 +636,6 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
666
636
|
});
|
|
667
637
|
return () => {
|
|
668
638
|
unsubscribeTeam();
|
|
669
|
-
// Nothing else disposes the runtime on an inline-mode exit or a crash
|
|
670
|
-
// that unmounts the tree, and an orphaned child holds the event loop open
|
|
671
|
-
// (R-15). `cli.tsx` calls this too; `dispose()` is idempotent.
|
|
672
639
|
controller.dispose();
|
|
673
640
|
};
|
|
674
641
|
}, [controller]);
|
|
@@ -937,10 +904,8 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
937
904
|
// Ink's own patchConsole writes straight to stdout, which shifts the fixed
|
|
938
905
|
// frame by a row and breaks its line accounting for good.
|
|
939
906
|
useEffect(() => {
|
|
940
|
-
if (!fullscreen)
|
|
941
|
-
return undefined;
|
|
942
907
|
return installConsoleBridge((level, text) => dispatch({ type: 'notice', level, text }));
|
|
943
|
-
}, [
|
|
908
|
+
}, []);
|
|
944
909
|
// `enabled` is `mouseSource`, not `cfg.mouse`: a user whose terminal drops
|
|
945
910
|
// the events must never be shown advice about a mode not in effect (§6.1).
|
|
946
911
|
// `cli.tsx::wantMouse` now folds the platform check into the same expression,
|
|
@@ -963,21 +928,9 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
963
928
|
// `transcriptRetain` to `transcriptWindow` (tui-render-performance §5.1).
|
|
964
929
|
cfg.transcriptRetainRequested !== undefined
|
|
965
930
|
? { requested: cfg.transcriptRetainRequested, resolved: cfg.transcriptRetain }
|
|
966
|
-
: undefined,
|
|
967
|
-
// Its own one-shot key, NOT `mouseNoticeSeen`: everyone this notice is for
|
|
968
|
-
// has already run aragon at least once and already has that flag set. That
|
|
969
|
-
// key is now a VERSION for the same reason, one turn later — the audience
|
|
970
|
-
// for the text that finally names the fallback key is, again, exactly the
|
|
971
|
-
// set of users who already have the old flag on disk.
|
|
972
|
-
//
|
|
973
|
-
// `fullscreen` goes with it because only half of the damage is mode-
|
|
974
|
-
// independent: `Shift+Tab` is broken either way, but inline never asked for
|
|
975
|
-
// wheel reports and does not own the scroll keys, so it must not be told
|
|
976
|
-
// about either.
|
|
977
|
-
vtInputWarning
|
|
931
|
+
: undefined, vtInputWarning
|
|
978
932
|
? {
|
|
979
933
|
nodeVersion: vtInputWarning.nodeVersion,
|
|
980
|
-
fullscreen,
|
|
981
934
|
onSeen: () => setVtInputNoticeVersion(VT_INPUT_NOTICE_VERSION),
|
|
982
935
|
}
|
|
983
936
|
: undefined);
|
|
@@ -1415,7 +1368,12 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1415
1368
|
cancelConfirmation();
|
|
1416
1369
|
if (interactionPhase.current === 'running') {
|
|
1417
1370
|
controller.steer(message);
|
|
1418
|
-
|
|
1371
|
+
// THE TRANSCRIPT ENTRY IS THE FEEDBACK (tui-shift-enter-copy-queue
|
|
1372
|
+
// 5.3). A 2.5 s toast cannot satisfy "until the message is picked
|
|
1373
|
+
// up": the `queued` entry stays visible and is rewritten in place
|
|
1374
|
+
// as the user message it became on the next `turn_start`. Keeping
|
|
1375
|
+
// the toast too would confirm one event twice.
|
|
1376
|
+
dispatch({ type: 'steerQueued', text: message });
|
|
1419
1377
|
return;
|
|
1420
1378
|
}
|
|
1421
1379
|
if (interactionPhase.current === 'starting') {
|
|
@@ -1584,11 +1542,6 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1584
1542
|
}
|
|
1585
1543
|
resolveHuman({ kind: 'planDecision', decision: verdict.decision, feedback: verdict.feedback });
|
|
1586
1544
|
};
|
|
1587
|
-
// --- Mouse wheel routing (wheel-scrolls-transcript-only §4.1). Table and
|
|
1588
|
-
// coalescer live in the hook (D-13); only the wiring it cannot own stays here.
|
|
1589
|
-
// Inline arrows are fine — the hook re-reads its options through a ref every
|
|
1590
|
-
// render. The `Math.max(0, …)` mirrors the keyboard branch below, and for the
|
|
1591
|
-
// same reason: `OverlayFrame` only ever reports a clamp DOWNWARD.
|
|
1592
1545
|
const { clearCoalescer } = useWheelRouting({
|
|
1593
1546
|
mouseSource,
|
|
1594
1547
|
isPointerCaptured: () => terminal?.scrollbar?.controller?.isCaptured() ?? false,
|
|
@@ -1655,6 +1608,27 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1655
1608
|
// `Esc` IS DELIBERATELY NOT GIVEN A SECOND MEANING (§4.4.6). It aborts a
|
|
1656
1609
|
// run, and an emergency exit is the last key that should silently absorb a
|
|
1657
1610
|
// press; it clears the selection here like every other key and nothing more.
|
|
1611
|
+
//
|
|
1612
|
+
// THE ONE EXCEPTION, AND IT SITS AHEAD OF THE UNCONDITIONAL CLEAR
|
|
1613
|
+
// (tui-shift-enter-copy-queue 4.3): Ctrl+C over a RELEASED, non-empty
|
|
1614
|
+
// selection means COPY. Selecting is browsing; copying is committing,
|
|
1615
|
+
// and the commit key is the one a terminal user already reaches for.
|
|
1616
|
+
// `takeSelection` runs BEFORE `copyText` because the copy writes OSC
|
|
1617
|
+
// 52 through `writeForeign`, which invalidates the differ and would
|
|
1618
|
+
// otherwise re-clear this controller a tick later (R7). A failed copy
|
|
1619
|
+
// (`via === 'none'`) still reports through the shared toast funnel and
|
|
1620
|
+
// returns: the selection is already consumed, and the NEXT Ctrl+C
|
|
1621
|
+
// walks the original ladder.
|
|
1622
|
+
if (key.ctrl &&
|
|
1623
|
+
(input === 'c' || input === 'C') &&
|
|
1624
|
+
(selectionController?.hasPendingSelection() ?? false)) {
|
|
1625
|
+
const payload = selectionController.takeSelection();
|
|
1626
|
+
if (payload) {
|
|
1627
|
+
const via = copyText(payload.text, terminal?.writeForeign);
|
|
1628
|
+
selectionBridge?.onCopied?.(via, payload.lines, payload.text.length);
|
|
1629
|
+
return; // copied -- no service stop, no arm, no clear
|
|
1630
|
+
}
|
|
1631
|
+
}
|
|
1658
1632
|
selectionController?.clear();
|
|
1659
1633
|
if (key.ctrl && (input === 'c' || input === 'C')) {
|
|
1660
1634
|
// RUNG ONE: Ctrl+C acquires the meaning a terminal user already expects
|
|
@@ -1734,7 +1708,7 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1734
1708
|
return;
|
|
1735
1709
|
}
|
|
1736
1710
|
if (key.ctrl && (input === 'l' || input === 'L')) {
|
|
1737
|
-
|
|
1711
|
+
{
|
|
1738
1712
|
// Writing \x1B[2J here would erase the screen and then leave it blank:
|
|
1739
1713
|
// Ink sees no output change and skips the repaint (ink.js:132 +
|
|
1740
1714
|
// log-update.js:13), possibly forever on an idle empty session. Bump
|
|
@@ -1750,7 +1724,7 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1750
1724
|
}
|
|
1751
1725
|
return;
|
|
1752
1726
|
}
|
|
1753
|
-
|
|
1727
|
+
{
|
|
1754
1728
|
if (key.pageUp) {
|
|
1755
1729
|
scrollBy('pageUp');
|
|
1756
1730
|
return;
|
|
@@ -1771,17 +1745,10 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1771
1745
|
if (key.ctrl && (input === 't' || input === 'T')) {
|
|
1772
1746
|
const revealing = !stateRef.current.thinkingVisible;
|
|
1773
1747
|
dispatch({ type: 'toggleThinking' });
|
|
1774
|
-
// THE ACK IS MODE-AWARE FOR THE SAME REASON THE MARKER IS (D-16 / P0-2).
|
|
1775
|
-
// Inline mode cannot repaint what `<Static>` has already printed, so the
|
|
1776
|
-
// toast says what it DID rather than what the user hoped. The hide
|
|
1777
|
-
// direction needs no qualifier: hiding applies to everything drawn from
|
|
1778
|
-
// here on, and nothing is claimed about scrollback either way.
|
|
1779
1748
|
if (!revealing)
|
|
1780
1749
|
toast('info', 'Thinking hidden.');
|
|
1781
|
-
else if (fullscreen)
|
|
1782
|
-
toast('info', 'Thinking shown.');
|
|
1783
1750
|
else
|
|
1784
|
-
toast('info', 'Thinking shown
|
|
1751
|
+
toast('info', 'Thinking shown.');
|
|
1785
1752
|
return;
|
|
1786
1753
|
}
|
|
1787
1754
|
if (key.ctrl && (input === 'o' || input === 'O')) {
|
|
@@ -1927,40 +1894,25 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1927
1894
|
// milliseconds because `ToolCard` is `React.memo`'d with the default
|
|
1928
1895
|
// comparator; `Transcript` passes it on only to running tool entries.
|
|
1929
1896
|
const nowSec = running || compactionLive ? Math.floor(Date.now() / 1000) : undefined;
|
|
1930
|
-
// Row budget for the viewport and everything sized against it (§4.2a). The
|
|
1931
|
-
// inline path has no fixed frame, hence no height to fit into: `Infinity`
|
|
1932
|
-
// tells `OverlayFrame` to render everything and show no position indicator.
|
|
1933
|
-
// `draftRows` is the number `PromptInput` just reported for the rows it is
|
|
1934
|
-
// ACTUALLY RENDERING (I-8). Defaulting it to 1 is what makes the inline path
|
|
1935
|
-
// and every test that never types byte-identical to before.
|
|
1936
1897
|
const viewportBudget = computeViewportRows(rows, draftRows);
|
|
1937
|
-
const overlayMaxRows =
|
|
1898
|
+
const overlayMaxRows = viewportBudget;
|
|
1938
1899
|
// Renderers consume these same projections. Team capacity does not depend on
|
|
1939
1900
|
// popup occupancy, so a menu cannot toggle the team's collapsed state.
|
|
1940
|
-
|
|
1941
|
-
|
|
1901
|
+
// THE ONE CONDITION for "the hint row exists": it sizes the footer below AND
|
|
1902
|
+
// decides whether the run status row may replace that hint row while running,
|
|
1903
|
+
// so starting a run can never add or remove a footer row.
|
|
1904
|
+
const hintRowEnabled = rows >= HINT_MIN_ROWS && cfg.hints;
|
|
1905
|
+
const composerBaseRows = 2 + clampDraftRows(rows, draftRows) + Number(hintRowEnabled);
|
|
1942
1906
|
const teamLayout = buildTeamPanelLayout({
|
|
1943
1907
|
snapshot: state.team, terminalRows: rows, availableRows: Math.max(0, viewportBudget - composerBaseRows),
|
|
1944
1908
|
});
|
|
1945
1909
|
const railLayout = buildTodoRailLayout({
|
|
1946
|
-
|
|
1910
|
+
cols: cols - 1, panelEnabled: cfg.todo.panel, overlayOpen: false,
|
|
1947
1911
|
itemCount: state.todos?.items.length ?? 0, viewportBudget,
|
|
1948
1912
|
teamRows: teamLayout.rowCount, popupRows, composerBaseRows,
|
|
1949
1913
|
});
|
|
1950
1914
|
// Transcript and composer share contentCols; fixed chrome and overlays use terminal width.
|
|
1951
1915
|
const { visible: showRail, width: railWidth, rows: railRows, contentCols } = railLayout;
|
|
1952
|
-
// The inline plan strip (todo-plan-followthrough §3.7 / W2).
|
|
1953
|
-
//
|
|
1954
|
-
// DELIBERATELY NOT `!showRail`, which is true for FOUR different reasons and
|
|
1955
|
-
// three of them mean "do not draw this". `cfg.todo.panel` is included because
|
|
1956
|
-
// `--no-todo-panel` means "keep the planning, drop the display" and a strip is
|
|
1957
|
-
// a display. Overlays are NOT consulted: inline mode's overlays are part of
|
|
1958
|
-
// the document flow, not a modal that owns the screen.
|
|
1959
|
-
//
|
|
1960
|
-
// `cfg` is the right read here and `controller.getTodoConfig()` would be
|
|
1961
|
-
// needless — this runs in RENDER scope, where `cfg` is re-read every frame.
|
|
1962
|
-
// The asymmetry with the `agent_end` handler above is the whole of P1-1.
|
|
1963
|
-
const showStrip = !fullscreen && cfg.todo.panel && state.todos !== null;
|
|
1964
1916
|
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: {
|
|
1965
1917
|
provider: cfg.provider,
|
|
1966
1918
|
model: cfg.model,
|
|
@@ -1994,14 +1946,11 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
1994
1946
|
// Read from `cfg` (render scope, re-read every frame), so `/retry off`
|
|
1995
1947
|
// while the screen is open shows the new value on the next paint.
|
|
1996
1948
|
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;
|
|
1997
|
-
|
|
1998
|
-
// the viewport monotonic and the first submit jump-free (§4.2). Inline keeps
|
|
1999
|
-
// its v0.3.0 ternary and never calls `pickHeaderVariant`.
|
|
2000
|
-
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 }));
|
|
1949
|
+
const header = (_jsx(Header, { cwd: controller.getCwd(), provider: cfg.provider, model: cfg.model, hasKey: hasKey, variant: pickHeaderVariant(cols), theme: theme, caps: caps }));
|
|
2001
1950
|
// The wordmark + getting-started card now live INSIDE the viewport, as its
|
|
2002
1951
|
// first block of content, so they scroll away instead of being yanked out of
|
|
2003
1952
|
// the header the moment the first message lands.
|
|
2004
|
-
const opener = empty ? (_jsx(SessionOpener, { variant:
|
|
1953
|
+
const opener = empty ? (_jsx(SessionOpener, { variant: pickOpenerVariant(viewportBudget, contentCols, caps), version: version, cwd: controller.getCwd(), hasKey: hasKey, viewportRows: viewportBudget, theme: theme, caps: caps })) : null;
|
|
2005
1954
|
const density = cfg.density;
|
|
2006
1955
|
const transcriptWindow = cfg.transcriptWindow ?? DEFAULT_TRANSCRIPT_WINDOW;
|
|
2007
1956
|
// --- One spinner per run (single-spinner-while-running D-1 .. D-5). ------
|
|
@@ -2039,6 +1988,26 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
2039
1988
|
// threading the raw flag into a fifth consumer is the one silent way back to
|
|
2040
1989
|
// two spinners (R-2).
|
|
2041
1990
|
const viewReducedMotion = reducedMotion || activityVisible;
|
|
1991
|
+
// The run status row (spinner + phrase + queue/interrupt hint) replaces the idle
|
|
1992
|
+
// hint row ABOVE the input while running; it exists only when that hint row
|
|
1993
|
+
// would, so the footer height is identical with and without it. `running`, not
|
|
1994
|
+
// `activityVisible`: an overlay must not flip the Composer between two layouts.
|
|
1995
|
+
const runRowEnabled = running && hintRowEnabled;
|
|
1996
|
+
// The raw `reducedMotion` flag lives here and nowhere else (D-5).
|
|
1997
|
+
const runActivity = {
|
|
1998
|
+
startedAt: runStartedAt.current,
|
|
1999
|
+
elapsedMs,
|
|
2000
|
+
reducedMotion,
|
|
2001
|
+
runningTool,
|
|
2002
|
+
// THE ROW MUST NEVER SAY SOMETHING FALSE (§6.1). Compaction runs BEFORE
|
|
2003
|
+
// `turn_start`, so `status` is `'running'` with no tool in flight - precisely
|
|
2004
|
+
// the window in which the rotating phrase would claim the model is thinking
|
|
2005
|
+
// while it is actually waiting on a 30k-token summarization.
|
|
2006
|
+
compacting: state.compaction?.inFlight === true,
|
|
2007
|
+
};
|
|
2008
|
+
// The fixed bottom row takes over the life signal once the run row scrolls out
|
|
2009
|
+
// of the viewport; exactly one of the two ever animates.
|
|
2010
|
+
const runRowShown = runRowEnabled && activityRowVisible;
|
|
2042
2011
|
// Assigned during render so `/perf` reads the CURRENT frame (see `perfRef`).
|
|
2043
2012
|
// `mountedEntries` is the same number `render-budget.test.tsx` asserts (AC-9).
|
|
2044
2013
|
perfRef.current = {
|
|
@@ -2053,12 +2022,18 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
2053
2022
|
heightsMeasured: heights.stats().measured,
|
|
2054
2023
|
heightsEstimated: heights.stats().estimated,
|
|
2055
2024
|
cols: contentCols,
|
|
2056
|
-
mode:
|
|
2057
|
-
viewportRows:
|
|
2025
|
+
mode: 'fullscreen',
|
|
2026
|
+
viewportRows: viewportBudget,
|
|
2058
2027
|
offset: scrolledLines,
|
|
2028
|
+
// A getter: the bridge's `frameReady` and geometry change outside React renders.
|
|
2029
|
+
get scrollbar() {
|
|
2030
|
+
return readScrollbarPerf({
|
|
2031
|
+
bridge: terminal?.scrollbar, diffRender: cfg.diffRender, unicode: caps.unicode,
|
|
2032
|
+
});
|
|
2033
|
+
},
|
|
2059
2034
|
};
|
|
2060
2035
|
// Completion belongs to the document footer; cap it to keep the editor usable.
|
|
2061
|
-
const popupMaxRows =
|
|
2036
|
+
const popupMaxRows = Math.max(1, viewportBudget - 4);
|
|
2062
2037
|
// WHICH KEY THE HINT ROW NAMES (shift-tab-mode-toggle-still-dead-on-windows,
|
|
2063
2038
|
// C3-1). Derived from `vtInputWarning` rather than from a probe of its own,
|
|
2064
2039
|
// because that prop is already the single answer to "can this console deliver
|
|
@@ -2069,29 +2044,13 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
2069
2044
|
// Both keys always WORK (they share one branch in the input handler); this
|
|
2070
2045
|
// only decides which one the user is taught.
|
|
2071
2046
|
const modeToggleKey = vtInputWarning ? MODE_TOGGLE_KEYS.fallback : MODE_TOGGLE_KEYS.primary;
|
|
2072
|
-
const composer =
|
|
2073
|
-
|
|
2047
|
+
const composer = (_jsx(Composer, { measureRef: composerRef, runRow: runRowEnabled
|
|
2048
|
+
? { activity: runActivity, live: runRowShown && !overlayNode, rowRef: activityRef }
|
|
2049
|
+
: 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' }) }));
|
|
2050
|
+
const team = state.team ? (_jsx(TeamPanel, { snapshot: state.team, layout: teamLayout, rows: rows, cols: contentCols, reducedMotion: viewReducedMotion, theme: theme, caps: caps })) : null;
|
|
2074
2051
|
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 =
|
|
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."] }),
|
|
2077
|
-
// `!overlayNode` suppresses the line while an overlay owns the screen:
|
|
2078
|
-
// the full-screen branch replaces the whole viewport with the overlay,
|
|
2079
|
-
// and a working line under a settings screen is noise attached to a
|
|
2080
|
-
// surface the run is not visible on. (Inline overlays are part of the
|
|
2081
|
-
// document flow, so there the suppression is conservative rather than
|
|
2082
|
-
// necessary — P2-5.)
|
|
2083
|
-
//
|
|
2084
|
-
// THE CONDITION IS `activityVisible`, THE SAME CONST THE SUPPRESSION
|
|
2085
|
-
// SIGNAL IS DERIVED FROM (D-4): inlining it here again is how the two
|
|
2086
|
-
// drift apart, and the drift is silent in whichever branch is not
|
|
2087
|
-
// edited.
|
|
2088
|
-
activity: activityVisible ? (_jsx(ActivityLine, { startedAt: runStartedAt.current, elapsedMs: elapsedMs, reducedMotion: reducedMotion, runningTool: runningTool,
|
|
2089
|
-
// THE ROW MUST NEVER SAY SOMETHING FALSE (§6.1). Compaction runs
|
|
2090
|
-
// BEFORE `turn_start`, so `status` is `'running'` with no tool in
|
|
2091
|
-
// flight — precisely the window in which the rotating phrase
|
|
2092
|
-
// would claim the model is thinking while it is actually waiting
|
|
2093
|
-
// on a 30k-token summarization.
|
|
2094
|
-
compacting: state.compaction?.inFlight === true, theme: theme, caps: caps })) : null,
|
|
2052
|
+
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 })] }));
|
|
2053
|
+
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,
|
|
2095
2054
|
// THE FORM THE LIFE SIGNAL TAKES WHEN A TOAST HAS THE ROW
|
|
2096
2055
|
// (`activity-spinner-vanishes-behind-toast`). Passed UNCONDITIONALLY:
|
|
2097
2056
|
// `BottomStatusRow` reads it only when `activity` above is non-null,
|
|
@@ -2111,7 +2070,13 @@ export function App({ controller, version, mode, initialOverlay, initialPrompt,
|
|
|
2111
2070
|
// branch replaces the whole viewport with the overlay, and an update
|
|
2112
2071
|
// notice under a settings screen is noise attached to a surface it is
|
|
2113
2072
|
// not visible on.
|
|
2114
|
-
|
|
2073
|
+
//
|
|
2074
|
+
// `!running` (cli-auto-update D-2): the update notice stays silent for
|
|
2075
|
+
// the whole run. It used to be guaranteed by the activity line
|
|
2076
|
+
// out-ranking it inside this row; with the activity line moved above
|
|
2077
|
+
// the input, the row can be free during a run, so the silence is stated
|
|
2078
|
+
// here instead of inherited.
|
|
2079
|
+
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'
|
|
2115
2080
|
? 'Esc force-stop' : 'Esc x2 interrupt', model: cfg.model, provider: cfg.provider, usageTotal: state.usageTotal,
|
|
2116
2081
|
// ONE OBJECT INSTEAD OF FOUR PARALLEL PROPS (§4.3). The four were a
|
|
2117
2082
|
// projection of "many sources of truth" into the props layer, and a
|
|
@@ -54,9 +54,7 @@
|
|
|
54
54
|
import React from 'react';
|
|
55
55
|
import type { Theme } from './theme.js';
|
|
56
56
|
import type { Toast } from '../agent/reducer.js';
|
|
57
|
-
import type { RenderMode } from './layout/frame.js';
|
|
58
57
|
export interface BottomStatusRowProps {
|
|
59
|
-
mode: RenderMode;
|
|
60
58
|
toasts: Toast[];
|
|
61
59
|
/** `<ActivityLine>` while a run owns the screen, else `null`. */
|
|
62
60
|
activity: React.ReactNode;
|
|
@@ -94,4 +92,4 @@ export interface BottomStatusRowProps {
|
|
|
94
92
|
update?: React.ReactNode;
|
|
95
93
|
theme: Theme;
|
|
96
94
|
}
|
|
97
|
-
export declare function BottomStatusRow({
|
|
95
|
+
export declare function BottomStatusRow({ toasts, activity, activityGlyph, update, theme, }: BottomStatusRowProps): React.ReactElement | null;
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
3
|
import { ToastStack } from './ToastStack.js';
|
|
4
|
-
export function BottomStatusRow({
|
|
4
|
+
export function BottomStatusRow({ toasts, activity, activityGlyph, update, theme, }) {
|
|
5
5
|
// A toast wins the row outright — but a run in flight keeps its spinner on it.
|
|
6
6
|
if (toasts.length > 0) {
|
|
7
7
|
const spinner = activity ? activityGlyph : null;
|
|
8
8
|
// Byte-identical to the old branch whenever there is no live spinner to
|
|
9
9
|
// carry: no run, an ASCII terminal, or `reducedMotion`.
|
|
10
10
|
if (!spinner)
|
|
11
|
-
return _jsx(ToastStack, { toasts: toasts, theme: theme
|
|
12
|
-
const fullscreen = mode === 'fullscreen';
|
|
11
|
+
return _jsx(ToastStack, { toasts: toasts, theme: theme });
|
|
13
12
|
return (
|
|
14
13
|
// `flexDirection` SPELLED OUT even though `row` is Yoga's default: it is
|
|
15
14
|
// what keeps two children on ONE row, and the header above turns on that
|
|
@@ -17,15 +16,12 @@ export function BottomStatusRow({ mode, toasts, activity, activityGlyph, update,
|
|
|
17
16
|
// below that declares `column` — and "make them match" is a one-word edit
|
|
18
17
|
// that doubles the row, takes it from the transcript, and desynchronises
|
|
19
18
|
// every `viewportRows()` consumer without failing a type or a lint.
|
|
20
|
-
_jsxs(Box, { flexDirection: "row", flexShrink: 0, children: [_jsx(Box, { flexShrink: 0,
|
|
19
|
+
_jsxs(Box, { flexDirection: "row", flexShrink: 0, children: [_jsx(Box, { flexShrink: 0, children: _jsxs(Text, { color: theme.thinking, children: [' ', spinner] }) }), _jsx(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, overflow: "hidden", children: _jsx(ToastStack, { toasts: toasts, theme: theme }) })] }));
|
|
21
20
|
}
|
|
22
|
-
// Inline has no frame and therefore nothing to protect: `ToastStack`'s inline
|
|
23
|
-
// branch is already a conditional 0-3 row stack, and these are simply one more
|
|
24
|
-
// conditional row above it.
|
|
25
21
|
if (activity)
|
|
26
22
|
return _jsx(Box, { flexShrink: 0, children: activity });
|
|
27
23
|
if (update)
|
|
28
24
|
return _jsx(Box, { flexShrink: 0, children: update });
|
|
29
25
|
// The blank full-screen row that IS the budget must keep being emitted.
|
|
30
|
-
return _jsx(ToastStack, { toasts: toasts, theme: theme
|
|
26
|
+
return _jsx(ToastStack, { toasts: toasts, theme: theme });
|
|
31
27
|
}
|
package/dist/ui/Composer.d.ts
CHANGED
|
@@ -18,8 +18,24 @@ import type { Theme } from './theme.js';
|
|
|
18
18
|
import type { TermCapabilities } from './capabilities.js';
|
|
19
19
|
import { pickGlyphs } from './glyphs.js';
|
|
20
20
|
import { type CommandOption } from './PromptInput.js';
|
|
21
|
+
import { type RunActivity } from './run-status-row.js';
|
|
21
22
|
import { type AgentMode } from '../agent/agent-mode.js';
|
|
23
|
+
/**
|
|
24
|
+
* The run status row (tui-scrollbar-edge-and-run-row §3.3): the spinner, the phrase
|
|
25
|
+
* and the `queue / interrupt / exit` clauses on ONE row above the input box.
|
|
26
|
+
* `App` passes it only while a run is in flight AND the idle hint row would have
|
|
27
|
+
* been shown, so enabling it never changes the footer's height.
|
|
28
|
+
*/
|
|
29
|
+
export interface RunRowProps {
|
|
30
|
+
activity: RunActivity;
|
|
31
|
+
/** The row is in the viewport with no overlay: draw the animated spinner. */
|
|
32
|
+
live: boolean;
|
|
33
|
+
/** Measured by `ScrollViewport` to learn whether the row has scrolled away. */
|
|
34
|
+
rowRef?: React.RefObject<DOMElement>;
|
|
35
|
+
}
|
|
22
36
|
export interface ComposerProps {
|
|
37
|
+
/** Present only while the run status row replaces the hint row below the input. */
|
|
38
|
+
runRow?: RunRowProps | null;
|
|
23
39
|
cols?: number;
|
|
24
40
|
cursorVisible?: boolean;
|
|
25
41
|
onInteraction?: () => void;
|
|
@@ -96,6 +112,17 @@ export interface ComposerProps {
|
|
|
96
112
|
onSubmit: (text: string) => void;
|
|
97
113
|
onHelp?: () => void;
|
|
98
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* The running-state hint as separate clauses, most urgent first.
|
|
117
|
+
*
|
|
118
|
+
* `hintText` joins them (its output is pinned by AC-43), and the run status row
|
|
119
|
+
* consumes the SAME array so it can drop whole clauses from the tail on a narrow
|
|
120
|
+
* terminal instead of cutting `interrupt` in half. One source, two consumers.
|
|
121
|
+
*/
|
|
122
|
+
export declare function runningHintClauses(opts: {
|
|
123
|
+
glyphs: ReturnType<typeof pickGlyphs>;
|
|
124
|
+
services: number;
|
|
125
|
+
}): string[];
|
|
99
126
|
/**
|
|
100
127
|
* Build the hint row.
|
|
101
128
|
*
|
|
@@ -125,5 +152,5 @@ declare function hintText(opts: {
|
|
|
125
152
|
* than fishing the string out of a rendered frame.
|
|
126
153
|
*/
|
|
127
154
|
export declare const hintTextForTest: typeof hintText;
|
|
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;
|
|
155
|
+
export declare function Composer({ runRow, 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;
|
|
129
156
|
export {};
|
package/dist/ui/Composer.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
/**
|
|
3
3
|
* Composer (spec §4.7) — the thin shell around the existing `PromptInput`.
|
|
4
4
|
*
|
|
@@ -15,11 +15,33 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
15
15
|
*/
|
|
16
16
|
import { useCallback, useState } from 'react';
|
|
17
17
|
import { Box, Text } from 'ink';
|
|
18
|
+
import stringWidth from 'string-width';
|
|
18
19
|
import { pickGlyphs } from './glyphs.js';
|
|
19
20
|
import { HINT_FADE_AFTER } from '../config/schema.js';
|
|
20
21
|
import { PromptInput } from './PromptInput.js';
|
|
21
|
-
import { ModeChip } from './ModeChip.js';
|
|
22
|
+
import { ModeChip, modeChipCols } from './ModeChip.js';
|
|
23
|
+
import { ActivityLabel, resolveActivityLabel } from './ActivityLine.js';
|
|
24
|
+
import { RUN_ROW_LEAD, planRunRow } from './run-status-row.js';
|
|
22
25
|
import { MODE_LABEL, MODE_TOGGLE_KEYS, nextMode } from '../agent/agent-mode.js';
|
|
26
|
+
/**
|
|
27
|
+
* The running-state hint as separate clauses, most urgent first.
|
|
28
|
+
*
|
|
29
|
+
* `hintText` joins them (its output is pinned by AC-43), and the run status row
|
|
30
|
+
* consumes the SAME array so it can drop whole clauses from the tail on a narrow
|
|
31
|
+
* terminal instead of cutting `interrupt` in half. One source, two consumers.
|
|
32
|
+
*/
|
|
33
|
+
export function runningHintClauses(opts) {
|
|
34
|
+
const { glyphs, services } = opts;
|
|
35
|
+
// `queue`, NOT `steer` (tui-shift-enter-copy-queue 5.5): the gesture's
|
|
36
|
+
// consequence now has a name in the transcript (`Queue: ...`), and one
|
|
37
|
+
// behaviour with two names is undiscoverable.
|
|
38
|
+
const clauses = [`${glyphs.enterKey} queue`, `esc${glyphs.times}2 interrupt`];
|
|
39
|
+
// Confirmation is named even when service controls also need space.
|
|
40
|
+
if (services > 0)
|
|
41
|
+
clauses.push(`ctrl+c stop ${services}`);
|
|
42
|
+
clauses.push(`ctrl+c${glyphs.times}2 exit`);
|
|
43
|
+
return clauses;
|
|
44
|
+
}
|
|
23
45
|
/**
|
|
24
46
|
* Build the hint row.
|
|
25
47
|
*
|
|
@@ -40,18 +62,8 @@ function hintText(opts) {
|
|
|
40
62
|
// clauses are shown; the row is `wrap="truncate"` and they are ordered
|
|
41
63
|
// most-urgent-first, so a terminal too narrow for all of them drops the exit
|
|
42
64
|
// clause last rather than the abort key.
|
|
43
|
-
if (running)
|
|
44
|
-
|
|
45
|
-
return [
|
|
46
|
-
`${glyphs.enterKey} steer`,
|
|
47
|
-
// Confirmation is named even when service controls also need space.
|
|
48
|
-
`esc${glyphs.times}2 interrupt`,
|
|
49
|
-
`ctrl+c stop ${services}`,
|
|
50
|
-
`ctrl+c${glyphs.times}2 exit`,
|
|
51
|
-
].join(dot);
|
|
52
|
-
}
|
|
53
|
-
return [`${glyphs.enterKey} steer`, `esc${glyphs.times}2 interrupt`, `ctrl+c${glyphs.times}2 exit`].join(dot);
|
|
54
|
-
}
|
|
65
|
+
if (running)
|
|
66
|
+
return runningHintClauses({ glyphs, services }).join(dot);
|
|
55
67
|
// The hint names the DESTINATION, not the current state — the correct label
|
|
56
68
|
// for a toggle affordance, and the reason the mode word here is the OPPOSITE
|
|
57
69
|
// of the one on the chip beside it.
|
|
@@ -86,7 +98,21 @@ function hintText(opts) {
|
|
|
86
98
|
* than fishing the string out of a rendered frame.
|
|
87
99
|
*/
|
|
88
100
|
export const hintTextForTest = hintText;
|
|
89
|
-
|
|
101
|
+
function RunRow({ runRow, cols, services, agentMode, theme, caps, }) {
|
|
102
|
+
const glyphs = pickGlyphs(caps);
|
|
103
|
+
const separator = ` ${glyphs.midDot} `;
|
|
104
|
+
const clauses = runningHintClauses({ glyphs, services });
|
|
105
|
+
const plan = planRunRow({
|
|
106
|
+
cols,
|
|
107
|
+
// Spinner glyph + the space after it, then the label.
|
|
108
|
+
labelCols: 2 + stringWidth(resolveActivityLabel(runRow.activity, glyphs)),
|
|
109
|
+
hintClauseCols: clauses.map((clause) => stringWidth(clause)),
|
|
110
|
+
separatorCols: stringWidth(separator),
|
|
111
|
+
chipCols: modeChipCols(agentMode),
|
|
112
|
+
});
|
|
113
|
+
return (_jsxs(Box, { ref: runRow.rowRef, flexDirection: "row", flexShrink: 0, children: [_jsx(Box, { width: RUN_ROW_LEAD, flexShrink: 0, children: _jsx(Text, { children: ' ' }) }), _jsx(Box, { width: plan.labelCols, flexShrink: 0, children: _jsx(ActivityLabel, { ...runRow.activity, spinnerLive: runRow.live, theme: theme, caps: caps }) }), plan.hintClauses > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.hintFg ?? theme.muted, children: [separator, clauses.slice(0, plan.hintClauses).join(separator)] })), plan.chip && (_jsxs(_Fragment, { children: [_jsx(Box, { flexGrow: 1 }), _jsx(ModeChip, { mode: agentMode, theme: theme, caps: caps })] }))] }));
|
|
114
|
+
}
|
|
115
|
+
export function Composer({ runRow, 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, }) {
|
|
90
116
|
const [hasDraft, setHasDraft] = useState(false);
|
|
91
117
|
const onDraftChange = useCallback((next) => {
|
|
92
118
|
setHasDraft(next.hasDraft);
|
|
@@ -105,8 +131,11 @@ export function Composer({ cols, cursorVisible, onInteraction, measureRef, isAct
|
|
|
105
131
|
? theme.focusBorder ?? theme.primary
|
|
106
132
|
: theme.idleBorder ?? theme.border;
|
|
107
133
|
const glyphs = pickGlyphs(caps);
|
|
108
|
-
|
|
109
|
-
|
|
134
|
+
// While the run row is up it REPLACES the hint row below the input (same row
|
|
135
|
+
// count, so starting a run never moves the layout).
|
|
136
|
+
const runRowActive = running && !!runRow;
|
|
137
|
+
const visible = showHint && hintsEnabled && !runRowActive;
|
|
138
|
+
return (_jsxs(Box, { ref: measureRef, flexDirection: "column", flexShrink: 0, children: [runRowActive && (_jsx(RunRow, { runRow: runRow, cols: cols ?? process.stdout.columns ?? 80, services: services, agentMode: agentMode, theme: theme, caps: caps })), _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, borderColor: borderColor, onDraftChange: onDraftChange, onNotice: onNotice, scrolledLines: scrolledLines }), visible && (
|
|
110
139
|
// The chip rides the EXISTING hint row rather than adding one, so it
|
|
111
140
|
// costs zero rows on a 24-row terminal.
|
|
112
141
|
_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({
|