@aragon-agent/cli 0.5.0 → 0.5.2
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 +158 -0
- package/README.md +254 -7
- package/dist/agent/agent-mode.d.ts +53 -0
- package/dist/agent/agent-mode.js +69 -0
- package/dist/agent/controller.d.ts +69 -0
- package/dist/agent/controller.js +154 -1
- package/dist/agent/plan-prompt.d.ts +18 -0
- package/dist/agent/plan-prompt.js +93 -0
- package/dist/agent/reducer.d.ts +14 -1
- package/dist/agent/reducer.js +9 -0
- package/dist/agent/system-prompt.d.ts +15 -0
- package/dist/agent/system-prompt.js +8 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +247 -23
- package/dist/commands/builtins.js +60 -0
- package/dist/commands/registry.d.ts +17 -0
- package/dist/config/app-paths.d.ts +71 -4
- package/dist/config/app-paths.js +139 -8
- package/dist/config/cli-commands.d.ts +43 -0
- package/dist/config/cli-commands.js +196 -0
- package/dist/config/env.d.ts +7 -1
- package/dist/config/env.js +53 -1
- package/dist/config/load.d.ts +10 -0
- package/dist/config/load.js +82 -2
- package/dist/config/migrate-home.d.ts +63 -0
- package/dist/config/migrate-home.js +165 -0
- package/dist/config/migrate-legacy-state.d.ts +10 -0
- package/dist/config/migrate-legacy-state.js +15 -6
- package/dist/config/schema.d.ts +81 -0
- package/dist/config/schema.js +85 -0
- package/dist/config/store.d.ts +30 -12
- package/dist/config/store.js +47 -35
- package/dist/input/mouse-events.d.ts +61 -0
- package/dist/input/mouse-events.js +153 -0
- package/dist/input/stdin-mouse-filter.d.ts +42 -0
- package/dist/input/stdin-mouse-filter.js +153 -0
- package/dist/logging/cli-commands.d.ts +25 -0
- package/dist/logging/cli-commands.js +221 -0
- package/dist/logging/file-sink.d.ts +114 -0
- package/dist/logging/file-sink.js +326 -0
- package/dist/logging/install.d.ts +163 -0
- package/dist/logging/install.js +302 -0
- package/dist/logging/levels.d.ts +40 -0
- package/dist/logging/levels.js +59 -0
- package/dist/logging/logger.d.ts +110 -0
- package/dist/logging/logger.js +237 -0
- package/dist/logging/redact.d.ts +28 -0
- package/dist/logging/redact.js +90 -0
- package/dist/logging/secret-registry.d.ts +30 -0
- package/dist/logging/secret-registry.js +55 -0
- package/dist/skills/paths.d.ts +2 -4
- package/dist/skills/paths.js +7 -9
- package/dist/tools/human-input.d.ts +179 -0
- package/dist/tools/human-input.js +227 -0
- package/dist/tools/index.d.ts +35 -3
- package/dist/tools/index.js +96 -9
- package/dist/tools/plan-tools.d.ts +64 -0
- package/dist/tools/plan-tools.js +198 -0
- package/dist/ui/App.d.ts +12 -1
- package/dist/ui/App.js +223 -17
- package/dist/ui/Composer.d.ts +9 -1
- package/dist/ui/Composer.js +26 -7
- package/dist/ui/ModeChip.d.ts +29 -0
- package/dist/ui/ModeChip.js +15 -0
- package/dist/ui/PromptInput.d.ts +22 -1
- package/dist/ui/PromptInput.js +49 -2
- package/dist/ui/StatusBar.d.ts +15 -0
- package/dist/ui/StatusBar.js +10 -2
- package/dist/ui/entries/ToolPreview.d.ts +2 -0
- package/dist/ui/entries/ToolPreview.js +64 -0
- package/dist/ui/glyphs.d.ts +21 -0
- package/dist/ui/glyphs.js +12 -0
- package/dist/ui/layout/AppShell.d.ts +10 -1
- package/dist/ui/layout/AppShell.js +38 -3
- package/dist/ui/layout/ScrollIndicator.d.ts +31 -0
- package/dist/ui/layout/ScrollIndicator.js +15 -0
- package/dist/ui/layout/ScrollViewport.d.ts +8 -2
- package/dist/ui/layout/ScrollViewport.js +24 -4
- package/dist/ui/layout/regions.d.ts +55 -0
- package/dist/ui/layout/regions.js +52 -0
- package/dist/ui/layout/scroll-indicator.d.ts +27 -0
- package/dist/ui/layout/scroll-indicator.js +35 -0
- package/dist/ui/layout/scroll.d.ts +9 -0
- package/dist/ui/layout/scroll.js +23 -0
- package/dist/ui/layout/wrap-rows.d.ts +24 -0
- package/dist/ui/layout/wrap-rows.js +74 -0
- package/dist/ui/overlays/HelpOverlay.js +15 -0
- package/dist/ui/overlays/PlanReviewOverlay.d.ts +50 -0
- package/dist/ui/overlays/PlanReviewOverlay.js +117 -0
- package/dist/ui/overlays/QuestionOverlay.d.ts +30 -0
- package/dist/ui/overlays/QuestionOverlay.js +193 -0
- package/dist/ui/overlays/SettingsScreen.d.ts +7 -0
- package/dist/ui/overlays/SettingsScreen.js +8 -1
- package/dist/ui/screen.d.ts +14 -1
- package/dist/ui/screen.js +37 -3
- package/dist/ui/use-startup-notices.d.ts +25 -0
- package/dist/ui/use-startup-notices.js +64 -0
- package/dist/ui/use-wheel-routing.d.ts +68 -0
- package/dist/ui/use-wheel-routing.js +139 -0
- package/package.json +3 -2
- package/runtime/insecure-tls-warning.cjs +62 -0
- package/runtime/insecure-tls-warning.d.cts +5 -0
package/dist/ui/Composer.d.ts
CHANGED
|
@@ -18,6 +18,7 @@ import React from 'react';
|
|
|
18
18
|
import type { Theme } from './theme.js';
|
|
19
19
|
import type { TermCapabilities } from './capabilities.js';
|
|
20
20
|
import { type CommandOption } from './PromptInput.js';
|
|
21
|
+
import { type AgentMode } from '../agent/agent-mode.js';
|
|
21
22
|
export interface ComposerProps {
|
|
22
23
|
/** False while an overlay owns the keyboard (the input is visually blurred). */
|
|
23
24
|
isActive: boolean;
|
|
@@ -31,10 +32,17 @@ export interface ComposerProps {
|
|
|
31
32
|
submitCount: number;
|
|
32
33
|
/** User opt-out (`--no-hints` / `hints: false`). */
|
|
33
34
|
hintsEnabled: boolean;
|
|
35
|
+
/** Mirror of the controller's effective mode; drives the chip and the border. */
|
|
36
|
+
agentMode: AgentMode;
|
|
34
37
|
popupMaxRows?: number;
|
|
38
|
+
/** Pure pass-through to `PromptInput` (mouse-wheel-region-routing §5.6). */
|
|
39
|
+
historyIntent?: {
|
|
40
|
+
dir: 'up' | 'down';
|
|
41
|
+
nonce: number;
|
|
42
|
+
};
|
|
35
43
|
theme: Theme;
|
|
36
44
|
caps: TermCapabilities;
|
|
37
45
|
onSubmit: (text: string) => void;
|
|
38
46
|
onHelp?: () => void;
|
|
39
47
|
}
|
|
40
|
-
export declare function Composer({ isActive, running, history, commands, cwd, showHint, submitCount, hintsEnabled, popupMaxRows, theme, caps, onSubmit, onHelp, }: ComposerProps): React.ReactElement;
|
|
48
|
+
export declare function Composer({ isActive, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, popupMaxRows, historyIntent, theme, caps, onSubmit, onHelp, }: ComposerProps): React.ReactElement;
|
package/dist/ui/Composer.js
CHANGED
|
@@ -20,6 +20,8 @@ import { Box, Text } from 'ink';
|
|
|
20
20
|
import { pickGlyphs } from './glyphs.js';
|
|
21
21
|
import { HINT_FADE_AFTER } from '../config/schema.js';
|
|
22
22
|
import { PromptInput } from './PromptInput.js';
|
|
23
|
+
import { ModeChip } from './ModeChip.js';
|
|
24
|
+
import { MODE_LABEL, nextMode } from '../agent/agent-mode.js';
|
|
23
25
|
/**
|
|
24
26
|
* Build the hint row.
|
|
25
27
|
*
|
|
@@ -30,32 +32,49 @@ import { PromptInput } from './PromptInput.js';
|
|
|
30
32
|
* Progressive disclosure is for teaching discoverable things, not for retiring
|
|
31
33
|
* an emergency exit (§4.6 / A-14 / M-10b).
|
|
32
34
|
*/
|
|
33
|
-
function hintText(running, submitCount, glyphs) {
|
|
35
|
+
function hintText(running, submitCount, glyphs, mode) {
|
|
34
36
|
const dot = ` ${glyphs.midDot} `;
|
|
35
37
|
if (running) {
|
|
36
38
|
return [`${glyphs.enterKey} steer`, 'esc abort', `ctrl+c${glyphs.times}2 exit`].join(dot);
|
|
37
39
|
}
|
|
40
|
+
// The hint names the DESTINATION, not the current state — the correct label
|
|
41
|
+
// for a toggle affordance, and the reason the mode word here is the OPPOSITE
|
|
42
|
+
// of the one on the chip beside it.
|
|
43
|
+
const toggle = `shift+tab ${MODE_LABEL[nextMode(mode)].toLowerCase()}`;
|
|
44
|
+
// The faded form KEEPS the mode toggle. Progressive disclosure exists to
|
|
45
|
+
// retire things the user has demonstrably learned, and `submitCount` counts
|
|
46
|
+
// submissions — a user can send fifty messages without ever discovering
|
|
47
|
+
// Shift+Tab. Same argument that keeps `esc abort` un-faded above.
|
|
38
48
|
if (submitCount >= HINT_FADE_AFTER)
|
|
39
|
-
return '? help';
|
|
49
|
+
return [toggle, '? help'].join(dot);
|
|
40
50
|
return [
|
|
41
51
|
`${glyphs.enterKey} send`,
|
|
42
52
|
`${glyphs.shiftEnter} newline`,
|
|
43
53
|
'/ commands',
|
|
44
54
|
'@ files',
|
|
55
|
+
toggle,
|
|
45
56
|
'? help',
|
|
46
57
|
].join(dot);
|
|
47
58
|
}
|
|
48
|
-
export function Composer({ isActive, running, history, commands, cwd, showHint, submitCount, hintsEnabled, popupMaxRows, theme, caps, onSubmit, onHelp, }) {
|
|
59
|
+
export function Composer({ isActive, running, history, commands, cwd, showHint, submitCount, hintsEnabled, agentMode, popupMaxRows, historyIntent, theme, caps, onSubmit, onHelp, }) {
|
|
49
60
|
const [hasDraft, setHasDraft] = useState(false);
|
|
50
61
|
const onDraftChange = useCallback((next) => setHasDraft(next), []);
|
|
62
|
+
// Plan mode inserts ONE branch, above `draft` and below `running`. The
|
|
63
|
+
// ordering is load-bearing: an in-flight run's border must stay `toolRunning`,
|
|
64
|
+
// because "is it running" is more urgent than "which mode".
|
|
51
65
|
const borderColor = !isActive
|
|
52
66
|
? theme.toolPending
|
|
53
67
|
: running
|
|
54
68
|
? theme.toolRunning
|
|
55
|
-
:
|
|
56
|
-
? theme.
|
|
57
|
-
:
|
|
69
|
+
: agentMode === 'plan'
|
|
70
|
+
? theme.accent
|
|
71
|
+
: hasDraft
|
|
72
|
+
? theme.focusBorder ?? theme.primary
|
|
73
|
+
: theme.idleBorder ?? theme.border;
|
|
58
74
|
const glyphs = pickGlyphs(caps);
|
|
59
75
|
const visible = showHint && hintsEnabled;
|
|
60
|
-
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, onSubmit: onSubmit, onHelp: onHelp, bordered: { color: borderColor }, onDraftChange: onDraftChange }), visible && (
|
|
76
|
+
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, onSubmit: onSubmit, onHelp: onHelp, agentMode: agentMode, bordered: { color: borderColor }, onDraftChange: onDraftChange, historyIntent: historyIntent }), visible && (
|
|
77
|
+
// The chip rides the EXISTING hint row rather than adding one, so it
|
|
78
|
+
// costs zero rows on a 24-row terminal.
|
|
79
|
+
_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(running, submitCount, glyphs, agentMode) })] }))] }));
|
|
61
80
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ModeChip — the `PLAN` badge on the composer's hint row (plan-mode §6.1).
|
|
3
|
+
*
|
|
4
|
+
* `theme.chip = { fg, bg }` exists in every palette and was, until now, unused.
|
|
5
|
+
* A mode badge is what it was for.
|
|
6
|
+
*
|
|
7
|
+
* SHOWN ONLY IN PLAN MODE. `BUILD` is the default, and a badge that is always on
|
|
8
|
+
* is furniture rather than signal; it also means the Build-mode composer renders
|
|
9
|
+
* exactly as it did before this feature existed.
|
|
10
|
+
*
|
|
11
|
+
* THE CHIP IS AN ENHANCEMENT, NOT THE GUARANTEE (P1-6). It rides the hint row,
|
|
12
|
+
* which disappears on a short terminal (`showHint`), under `hints: false`, and
|
|
13
|
+
* in inline render mode (which mounts a bare `PromptInput` and never mounts
|
|
14
|
+
* `Composer` at all). The GUARANTEED carrier is the status-bar word, which
|
|
15
|
+
* renders in both `AppShell` branches and has no opt-out. If you ever move the
|
|
16
|
+
* mode indicator, that is the constraint to preserve: the status bar must always
|
|
17
|
+
* name a non-default mode; the chip may.
|
|
18
|
+
*/
|
|
19
|
+
import React from 'react';
|
|
20
|
+
import type { Theme } from './theme.js';
|
|
21
|
+
import type { TermCapabilities } from './capabilities.js';
|
|
22
|
+
import { type AgentMode } from '../agent/agent-mode.js';
|
|
23
|
+
interface ModeChipProps {
|
|
24
|
+
mode: AgentMode;
|
|
25
|
+
theme: Theme;
|
|
26
|
+
caps: TermCapabilities;
|
|
27
|
+
}
|
|
28
|
+
export declare function ModeChip({ mode, theme, caps }: ModeChipProps): React.ReactElement | null;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { MODE_LABEL } from '../agent/agent-mode.js';
|
|
4
|
+
export function ModeChip({ mode, theme, caps }) {
|
|
5
|
+
if (mode !== 'plan')
|
|
6
|
+
return null;
|
|
7
|
+
const label = MODE_LABEL[mode];
|
|
8
|
+
// `Box backgroundColor` degrades to plain text at colorLevel 0, where the chip
|
|
9
|
+
// would become invisible against the hint row. Brackets carry the same
|
|
10
|
+
// "this is a badge" reading with no color at all.
|
|
11
|
+
if (caps.colorLevel === 0) {
|
|
12
|
+
return (_jsxs(Text, { wrap: "truncate", bold: true, children: ["[", label, "]", ' '] }));
|
|
13
|
+
}
|
|
14
|
+
return (_jsx(Box, { flexShrink: 0, marginRight: 1, children: _jsxs(Text, { backgroundColor: theme.chip.bg, color: theme.chip.fg, bold: true, children: [' ', label, ' '] }) }));
|
|
15
|
+
}
|
package/dist/ui/PromptInput.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ import type { Theme } from './theme.js';
|
|
|
13
13
|
import type { TermCapabilities } from './capabilities.js';
|
|
14
14
|
import { type EditIntent } from '../input/keymap.js';
|
|
15
15
|
import { type Suggestion } from './AutocompletePopup.js';
|
|
16
|
+
import type { AgentMode } from '../agent/agent-mode.js';
|
|
16
17
|
export interface CommandOption {
|
|
17
18
|
name: string;
|
|
18
19
|
description: string;
|
|
@@ -37,6 +38,12 @@ interface PromptInputProps {
|
|
|
37
38
|
popupMaxRows?: number;
|
|
38
39
|
onSubmit: (text: string) => void;
|
|
39
40
|
onHelp?: () => void;
|
|
41
|
+
/**
|
|
42
|
+
* Mirror of the session mode. Read for the placeholder only — the toggle
|
|
43
|
+
* itself belongs to `App`, and this component's job is to get out of its way
|
|
44
|
+
* (see the first statement of the input handler).
|
|
45
|
+
*/
|
|
46
|
+
agentMode?: AgentMode;
|
|
40
47
|
/**
|
|
41
48
|
* Set by `Composer` in full-screen mode: draw the rounded frame around the
|
|
42
49
|
* input row only, so the autocomplete popup lands OUTSIDE it (spec §4.7)
|
|
@@ -48,6 +55,20 @@ interface PromptInputProps {
|
|
|
48
55
|
};
|
|
49
56
|
/** Fires only when the draft flips empty ⇄ non-empty, for the border color. */
|
|
50
57
|
onDraftChange?: (hasDraft: boolean) => void;
|
|
58
|
+
/**
|
|
59
|
+
* One prompt-history step, pushed by the wheel router when a notch lands on
|
|
60
|
+
* the composer band (mouse-wheel-region-routing §5.6). `nonce` is monotonic
|
|
61
|
+
* so two notches in the same direction are two steps.
|
|
62
|
+
*
|
|
63
|
+
* Applied through the SAME `verticalOrHistory` / popup-selection branches the
|
|
64
|
+
* keyboard uses, deliberately: it means the wheel and `↑`/`↓` cannot drift
|
|
65
|
+
* apart, and the "never clobber an in-progress draft" guard protects the
|
|
66
|
+
* wheel for free.
|
|
67
|
+
*/
|
|
68
|
+
historyIntent?: {
|
|
69
|
+
dir: 'up' | 'down';
|
|
70
|
+
nonce: number;
|
|
71
|
+
};
|
|
51
72
|
}
|
|
52
73
|
/** Apply an edit intent as a pure buffer/cursor transform. */
|
|
53
74
|
export declare function applyEdit(buffer: string, cursor: number, intent: EditIntent): {
|
|
@@ -77,5 +98,5 @@ export declare function fileTokenAt(buffer: string, cursor: number): {
|
|
|
77
98
|
end: number;
|
|
78
99
|
query: string;
|
|
79
100
|
} | null;
|
|
80
|
-
export declare function PromptInput({ isActive, running, history, commands, cwd, theme, caps, popupMaxRows, onSubmit, onHelp, bordered, onDraftChange, }: PromptInputProps): React.ReactElement;
|
|
101
|
+
export declare function PromptInput({ isActive, running, history, commands, cwd, theme, caps, popupMaxRows, agentMode, onSubmit, onHelp, bordered, onDraftChange, historyIntent, }: PromptInputProps): React.ReactElement;
|
|
81
102
|
export {};
|
package/dist/ui/PromptInput.js
CHANGED
|
@@ -127,7 +127,7 @@ function isControlSeq(input) {
|
|
|
127
127
|
// ---------------------------------------------------------------------------
|
|
128
128
|
// Component
|
|
129
129
|
// ---------------------------------------------------------------------------
|
|
130
|
-
export function PromptInput({ isActive, running, history, commands, cwd, theme, caps, popupMaxRows, onSubmit, onHelp, bordered, onDraftChange, }) {
|
|
130
|
+
export function PromptInput({ isActive, running, history, commands, cwd, theme, caps, popupMaxRows, agentMode, onSubmit, onHelp, bordered, onDraftChange, historyIntent, }) {
|
|
131
131
|
const [buffer, setBuffer] = useState('');
|
|
132
132
|
const [cursor, setCursor] = useState(0);
|
|
133
133
|
const [historyIndex, setHistoryIndex] = useState(null);
|
|
@@ -261,8 +261,53 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
|
|
|
261
261
|
recallDown();
|
|
262
262
|
}
|
|
263
263
|
};
|
|
264
|
+
// --- Wheel-driven history step (mouse-wheel-region-routing §5.6). ---------
|
|
265
|
+
// The applier closes over the buffer, the cursor and the popup state, so it
|
|
266
|
+
// is re-created on every render and reached through a ref: depending on it
|
|
267
|
+
// directly would re-fire the effect on every keystroke.
|
|
268
|
+
const applyHistoryIntent = useRef(() => { });
|
|
269
|
+
applyHistoryIntent.current = (dir) => {
|
|
270
|
+
// Mirrors the keyboard branch exactly — with the completion popup open,
|
|
271
|
+
// Up/Down move the selection rather than recalling history.
|
|
272
|
+
if (popupItems.length > 0) {
|
|
273
|
+
setSel(dir === 'up'
|
|
274
|
+
? Math.max(0, clampedSel - 1)
|
|
275
|
+
: Math.min(popupItems.length - 1, clampedSel + 1));
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
verticalOrHistory(dir);
|
|
279
|
+
};
|
|
280
|
+
const historyIntentRef = useRef(historyIntent);
|
|
281
|
+
historyIntentRef.current = historyIntent;
|
|
282
|
+
// I-9: seeded from the incoming nonce during the FIRST render, so a mount
|
|
283
|
+
// that inherits a nonce cannot replay it. This component really does remount
|
|
284
|
+
// — `App` returns the terminal-too-small placeholder before `AppShell` —
|
|
285
|
+
// so without the guard, shrinking the window mid-wheel and growing it back
|
|
286
|
+
// recalls a history entry with no gesture behind it.
|
|
287
|
+
const lastHistoryNonce = useRef(historyIntent?.nonce);
|
|
288
|
+
const historyNonce = historyIntent?.nonce ?? 0;
|
|
289
|
+
useEffect(() => {
|
|
290
|
+
const current = historyIntentRef.current;
|
|
291
|
+
if (!current || !isActive)
|
|
292
|
+
return;
|
|
293
|
+
if (current.nonce === lastHistoryNonce.current)
|
|
294
|
+
return;
|
|
295
|
+
lastHistoryNonce.current = current.nonce;
|
|
296
|
+
applyHistoryIntent.current(current.dir);
|
|
297
|
+
// Keyed on the nonce alone, same reasoning as `ScrollViewport`'s intent
|
|
298
|
+
// effect: `historyIntent` is a fresh object on every parent render.
|
|
299
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
300
|
+
}, [historyNonce]);
|
|
264
301
|
// --- Input handling. -----------------------------------------------------
|
|
265
302
|
useInput((input, key) => {
|
|
303
|
+
// FIRST STATEMENT, BEFORE THE POPUP BRANCH, AND IT HAS TO STAY THERE.
|
|
304
|
+
// `App` owns the Shift+Tab mode toggle. The popup branch below treats
|
|
305
|
+
// `key.tab` as "accept the highlighted completion", so without this guard
|
|
306
|
+
// pressing Shift+Tab with the `/` palette or an `@` file popup open would
|
|
307
|
+
// BOTH rewrite the user's draft and change the mode — a bug that only
|
|
308
|
+
// appears with a popup open and is therefore easy to ship (AC-P3 / R-P2).
|
|
309
|
+
if (key.tab && key.shift)
|
|
310
|
+
return;
|
|
266
311
|
// Popup navigation owns Up/Down/Tab/→/Enter/Esc while it is open.
|
|
267
312
|
if (popupItems.length > 0) {
|
|
268
313
|
if (key.upArrow) {
|
|
@@ -353,7 +398,9 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
|
|
|
353
398
|
const markerColor = running ? theme.toolRunning : theme.primary;
|
|
354
399
|
const placeholder = running
|
|
355
400
|
? `Type to steer the run, Esc to abort${glyphs.ellipsis}`
|
|
356
|
-
:
|
|
401
|
+
: agentMode === 'plan'
|
|
402
|
+
? `Describe what you want to build; I'll research and plan it first${glyphs.ellipsis}`
|
|
403
|
+
: `Send a message (/ for commands, @ for files)${glyphs.ellipsis}`;
|
|
357
404
|
const lines = renderWithCursor(buffer, cursor, isActive);
|
|
358
405
|
const inputRow = (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: markerColor, bold: true, children: [marker, ' '] }), _jsx(Box, { flexDirection: "column", children: buffer.length === 0 ? (_jsx(Text, { color: theme.muted, children: placeholder })) : (lines.map((line, i) => _jsx(Text, { children: line }, i))) })] }));
|
|
359
406
|
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [popupItems.length > 0 && (_jsx(AutocompletePopup, { items: popupItems, selected: clampedSel, theme: theme, caps: caps, maxRows: popupMaxRows })), bordered ? (_jsx(Box, { flexDirection: "column", flexShrink: 0, borderStyle: glyphs.boxStyle, borderColor: bordered.color, paddingX: 1, children: inputRow })) : (inputRow)] }));
|
package/dist/ui/StatusBar.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ import React from 'react';
|
|
|
11
11
|
import type { Theme } from './theme.js';
|
|
12
12
|
import type { TermCapabilities } from './capabilities.js';
|
|
13
13
|
import type { UsageTotal } from '../agent/reducer.js';
|
|
14
|
+
import { type AgentMode } from '../agent/agent-mode.js';
|
|
14
15
|
interface StatusBarProps {
|
|
15
16
|
model: string;
|
|
16
17
|
provider: string;
|
|
@@ -34,6 +35,20 @@ interface StatusBarProps {
|
|
|
34
35
|
* Ctrl+L redraw carrier — invariant I-5 (spec §4.13). See `redrawChar`.
|
|
35
36
|
*/
|
|
36
37
|
redrawNonce?: number;
|
|
38
|
+
/**
|
|
39
|
+
* The session mode. THIS IS THE GUARANTEED MODE INDICATOR (plan-mode §6.4 /
|
|
40
|
+
* P1-6): the composer chip disappears on a short terminal, under
|
|
41
|
+
* `hints: false`, and in inline render mode, but `StatusBar` renders in both
|
|
42
|
+
* `AppShell` branches and its left cluster has no opt-out and no width gate.
|
|
43
|
+
*
|
|
44
|
+
* Design intent, stated here so it survives a later refactor: the status bar
|
|
45
|
+
* must ALWAYS name a non-default mode; the chip may. Do not give this a
|
|
46
|
+
* `cols >= N` breakpoint, and do not move it into the right cluster — those
|
|
47
|
+
* children are flexible and drop characters under pressure.
|
|
48
|
+
*/
|
|
49
|
+
agentMode?: AgentMode;
|
|
50
|
+
/** Non-null while a deferred `plan -> build` waits for `agent_end`. */
|
|
51
|
+
pendingAgentMode?: AgentMode | null;
|
|
37
52
|
}
|
|
38
53
|
export declare function StatusBar(props: StatusBarProps): React.ReactElement;
|
|
39
54
|
export {};
|
package/dist/ui/StatusBar.js
CHANGED
|
@@ -3,6 +3,7 @@ import { Box, Text, useStdout } from 'ink';
|
|
|
3
3
|
import { pickGlyphs } from './glyphs.js';
|
|
4
4
|
import { formatCost, formatTokens, formatDuration } from '../agent/usage.js';
|
|
5
5
|
import { buildGauge } from './gauge.js';
|
|
6
|
+
import { MODE_LABEL } from '../agent/agent-mode.js';
|
|
6
7
|
/**
|
|
7
8
|
* I-5 — DO NOT "CLEAN THIS UP". Ink short-circuits identical output at TWO
|
|
8
9
|
* gates (`ink.js:132` and `log-update.js:13`), so a fixed frame whose React
|
|
@@ -22,7 +23,7 @@ function redrawChar(nonce) {
|
|
|
22
23
|
return nonce % 2 === 0 ? ' ' : NBSP;
|
|
23
24
|
}
|
|
24
25
|
export function StatusBar(props) {
|
|
25
|
-
const { model, provider, usageTotal, contextTokens, contextWindow, contextWindowKnown, status, elapsedMs, thinkingLevel, tokPerSec, theme, caps, scrolledLines = 0, redrawNonce = 0, } = props;
|
|
26
|
+
const { model, provider, usageTotal, contextTokens, contextWindow, contextWindowKnown, status, elapsedMs, thinkingLevel, tokPerSec, theme, caps, scrolledLines = 0, redrawNonce = 0, agentMode = 'build', pendingAgentMode = null, } = props;
|
|
26
27
|
const { stdout } = useStdout();
|
|
27
28
|
const cols = stdout?.columns ?? 80;
|
|
28
29
|
const running = status === 'running';
|
|
@@ -32,6 +33,13 @@ export function StatusBar(props) {
|
|
|
32
33
|
const pctLabel = `${contextWindowKnown ? '' : '~'}${gauge.pct}%`;
|
|
33
34
|
const tokens = `${formatTokens(usageTotal.inputTokens)}${glyphs.arrowUp} ${formatTokens(usageTotal.outputTokens)}${glyphs.arrowDown}`;
|
|
34
35
|
const statusGlyph = running ? theme.symbols.toolRunning : theme.symbols.toolPending;
|
|
36
|
+
// Nothing at all in build mode with nothing pending: the default posture is
|
|
37
|
+
// not news, and a permanent word here would be furniture.
|
|
38
|
+
const modeWord = agentMode === 'build' && !pendingAgentMode
|
|
39
|
+
? ''
|
|
40
|
+
: pendingAgentMode
|
|
41
|
+
? `${MODE_LABEL[agentMode]} ${glyphs.arrowRight} ${MODE_LABEL[pendingAgentMode]}`
|
|
42
|
+
: MODE_LABEL[agentMode];
|
|
35
43
|
return (
|
|
36
44
|
// `flexShrink={0}` on the left cluster is not cosmetic. When the two clusters
|
|
37
45
|
// over-subscribe the row, yoga shrinks the flexible children and Ink's text
|
|
@@ -39,5 +47,5 @@ export function StatusBar(props) {
|
|
|
39
47
|
// and "? hel" rather than truncating cleanly at one end. Pinning the left
|
|
40
48
|
// cluster keeps the run status and model intact and makes the degradation
|
|
41
49
|
// land in one predictable place.
|
|
42
|
-
_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", flexShrink: 0, children: [_jsxs(Box, { flexDirection: "row", flexShrink: 0, children: [_jsxs(Text, { color: running ? theme.toolRunning : theme.toolDone, children: [redrawChar(redrawNonce), statusGlyph, " ", running ? 'running' : 'idle'] }), cols >= 60 && (_jsxs(Text, { color: theme.muted, children: [' ', provider, ":", model] })), thinkingLevel !== 'off' && cols >= 72 && (_jsxs(Text, { color: theme.accent, children: [" think:", thinkingLevel] }))] }), _jsxs(Box, { flexDirection: "row", children: [scrolledLines > 0 && (_jsxs(Text, { color: theme.noticeWarn, children: [glyphs.arrowUp, scrolledLines, ' '] })), cols >= 60 && (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: "[" }), _jsx(Text, { color: gauge.fillColor, children: gauge.filled }), _jsx(Text, { color: gauge.trackColor, children: gauge.empty }), _jsx(Text, { color: theme.muted, children: "] " })] })), _jsx(Text, { color: theme.muted, children: pctLabel }), cols >= 72 && _jsxs(Text, { color: theme.muted, children: [" ", tokens] }), _jsxs(Text, { color: theme.muted, children: [" ", formatCost(usageTotal.costUsd)] }), running && (_jsxs(Text, { color: theme.muted, children: [tokPerSec > 0 && cols >= 72 ? ` ${tokPerSec} tok/s` : '', " ", formatDuration(elapsedMs)] }))] })] }));
|
|
50
|
+
_jsxs(Box, { flexDirection: "row", justifyContent: "space-between", flexShrink: 0, children: [_jsxs(Box, { flexDirection: "row", flexShrink: 0, children: [_jsxs(Text, { color: running ? theme.toolRunning : theme.toolDone, children: [redrawChar(redrawNonce), statusGlyph, " ", running ? 'running' : 'idle'] }), modeWord && (_jsxs(Text, { color: theme.accent, bold: true, children: [' ', modeWord] })), cols >= 60 && (_jsxs(Text, { color: theme.muted, children: [' ', provider, ":", model] })), thinkingLevel !== 'off' && cols >= 72 && (_jsxs(Text, { color: theme.accent, children: [" think:", thinkingLevel] }))] }), _jsxs(Box, { flexDirection: "row", children: [scrolledLines > 0 && (_jsxs(Text, { color: theme.noticeWarn, children: [glyphs.arrowUp, scrolledLines, ' '] })), cols >= 60 && (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: "[" }), _jsx(Text, { color: gauge.fillColor, children: gauge.filled }), _jsx(Text, { color: gauge.trackColor, children: gauge.empty }), _jsx(Text, { color: theme.muted, children: "] " })] })), _jsx(Text, { color: theme.muted, children: pctLabel }), cols >= 72 && _jsxs(Text, { color: theme.muted, children: [" ", tokens] }), _jsxs(Text, { color: theme.muted, children: [" ", formatCost(usageTotal.costUsd)] }), running && (_jsxs(Text, { color: theme.muted, children: [tokPerSec > 0 && cols >= 72 ? ` ${tokPerSec} tok/s` : '', " ", formatDuration(elapsedMs)] }))] })] }));
|
|
43
51
|
}
|
|
@@ -34,6 +34,8 @@ export declare function parseWriteFile(text: string): {
|
|
|
34
34
|
bytes: number;
|
|
35
35
|
path: string;
|
|
36
36
|
} | null;
|
|
37
|
+
/** `3 questions · Postgres · REST` — or `dismissed`. */
|
|
38
|
+
export declare function summarizeAskUser(lines: string[]): string | null;
|
|
37
39
|
export interface ListRow {
|
|
38
40
|
kind: 'dir' | 'file';
|
|
39
41
|
name: string;
|
|
@@ -44,6 +44,40 @@ export function parseWriteFile(text) {
|
|
|
44
44
|
return null;
|
|
45
45
|
return { bytes: Number.parseInt(m[1], 10), path: m[2] };
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Parse the JSON tail of a tool result whose first line is prose.
|
|
49
|
+
*
|
|
50
|
+
* FAILS SOFT, ALWAYS. This runs inside a render pass, and the stored preview may
|
|
51
|
+
* have been truncated at `STORED_PREVIEW_CHARS` or restored from a session file
|
|
52
|
+
* written by another build. `null` means "fall through to the generic
|
|
53
|
+
* renderer", never "throw while drawing the transcript".
|
|
54
|
+
*/
|
|
55
|
+
function parseJsonTail(lines) {
|
|
56
|
+
const start = lines.findIndex((l) => l.trimStart().startsWith('{'));
|
|
57
|
+
if (start < 0)
|
|
58
|
+
return null;
|
|
59
|
+
try {
|
|
60
|
+
return JSON.parse(lines.slice(start).join('\n'));
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
/** `3 questions · Postgres · REST` — or `dismissed`. */
|
|
67
|
+
export function summarizeAskUser(lines) {
|
|
68
|
+
const parsed = parseJsonTail(lines);
|
|
69
|
+
if (!parsed || !Array.isArray(parsed.answers))
|
|
70
|
+
return null;
|
|
71
|
+
if (parsed.cancelled)
|
|
72
|
+
return 'dismissed';
|
|
73
|
+
const count = parsed.answers.length;
|
|
74
|
+
const picks = parsed.answers
|
|
75
|
+
.map((a) => (typeof a.custom === 'string' && a.custom.length > 0
|
|
76
|
+
? a.custom
|
|
77
|
+
: Array.isArray(a.selected) ? a.selected.join('/') : ''))
|
|
78
|
+
.filter((s) => s.length > 0);
|
|
79
|
+
return [`${count} question${count === 1 ? '' : 's'}`, ...picks].join(' - ');
|
|
80
|
+
}
|
|
47
81
|
/** Parse a real `list_dir` row (`dir <name>/` / `file <name> (<n>b)`) (P2-9). */
|
|
48
82
|
export function parseListRow(line) {
|
|
49
83
|
const dir = /^dir\s+(.*?)\/?\s*$/.exec(line);
|
|
@@ -113,6 +147,30 @@ function renderGlob(lines, theme) {
|
|
|
113
147
|
return (_jsxs(Text, { children: [_jsx(Text, { color: theme.muted, children: line.slice(0, slash + 1) }), line.slice(slash + 1)] }, i));
|
|
114
148
|
});
|
|
115
149
|
}
|
|
150
|
+
function renderAskUser(lines, theme) {
|
|
151
|
+
const summary = summarizeAskUser(lines);
|
|
152
|
+
// An unparseable preview falls through to the generic renderer rather than
|
|
153
|
+
// rendering nothing: a truncated result is still information.
|
|
154
|
+
if (!summary)
|
|
155
|
+
return renderPlain(lines, theme, false);
|
|
156
|
+
return [
|
|
157
|
+
_jsx(Text, { color: theme.muted, children: summary }, "q"),
|
|
158
|
+
];
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* `Plan approved.` / `Plan rejected. ...` — the verdict is the whole content of
|
|
162
|
+
* a `submit_plan` result, and it is the first line, so this needs no parsing.
|
|
163
|
+
*/
|
|
164
|
+
function renderSubmitPlan(lines, theme) {
|
|
165
|
+
const first = lines.find((l) => l.trim().length > 0) ?? '';
|
|
166
|
+
const approved = /^Plan approved/.test(first);
|
|
167
|
+
const rejected = /^Plan rejected/.test(first);
|
|
168
|
+
if (!approved && !rejected)
|
|
169
|
+
return renderPlain(lines, theme, false);
|
|
170
|
+
return [
|
|
171
|
+
_jsx(Text, { color: approved ? theme.toolDone : theme.noticeWarn, children: first }, "v"),
|
|
172
|
+
];
|
|
173
|
+
}
|
|
116
174
|
function renderPlain(lines, theme, error) {
|
|
117
175
|
return lines.map((line, i) => (_jsx(Text, { color: error ? theme.toolError : theme.muted, children: line.length > 0 ? line : ' ' }, i)));
|
|
118
176
|
}
|
|
@@ -137,6 +195,12 @@ export function ToolPreview({ name, lines, theme, isError }) {
|
|
|
137
195
|
case 'glob':
|
|
138
196
|
body = renderGlob(lines, theme);
|
|
139
197
|
break;
|
|
198
|
+
case 'ask_user':
|
|
199
|
+
body = renderAskUser(lines, theme);
|
|
200
|
+
break;
|
|
201
|
+
case 'submit_plan':
|
|
202
|
+
body = renderSubmitPlan(lines, theme);
|
|
203
|
+
break;
|
|
140
204
|
default:
|
|
141
205
|
body = renderPlain(lines, theme, isError);
|
|
142
206
|
}
|
package/dist/ui/glyphs.d.ts
CHANGED
|
@@ -37,6 +37,15 @@ export interface Glyphs {
|
|
|
37
37
|
gaugeEmpty: string;
|
|
38
38
|
keyOn: string;
|
|
39
39
|
keyOff: string;
|
|
40
|
+
/**
|
|
41
|
+
* Checkbox pair for a multi-select question (`allowMultiple`).
|
|
42
|
+
*
|
|
43
|
+
* Distinct from `keyOn` / `keyOff`, which are radios: a user looking at a list
|
|
44
|
+
* has to be able to tell "pick one" from "pick several" without reading the
|
|
45
|
+
* footer, and that distinction is exactly what the two shapes carry.
|
|
46
|
+
*/
|
|
47
|
+
boxChecked: string;
|
|
48
|
+
boxEmpty: string;
|
|
40
49
|
wordmark: string;
|
|
41
50
|
ellipsis: string;
|
|
42
51
|
emDash: string;
|
|
@@ -63,6 +72,18 @@ export interface Glyphs {
|
|
|
63
72
|
railVertical: string;
|
|
64
73
|
railBranch: string;
|
|
65
74
|
railEnd: string;
|
|
75
|
+
/**
|
|
76
|
+
* Scroll-position indicator on the viewport's right edge (mouse-wheel §4.8).
|
|
77
|
+
*
|
|
78
|
+
* These are byte-for-byte identical to `railVertical` / `gaugeFull` in both
|
|
79
|
+
* tiers, and that is DELIBERATE (P2-2). This file's header mandates "add a
|
|
80
|
+
* field here instead" of a new exception, and semantic field names are what
|
|
81
|
+
* make the ASCII tier reviewable at a glance. Reusing `gaugeFull` would
|
|
82
|
+
* couple the scroll thumb to the context-window gauge, so that restyling one
|
|
83
|
+
* silently restyles the other — do not "deduplicate" them.
|
|
84
|
+
*/
|
|
85
|
+
scrollTrack: string;
|
|
86
|
+
scrollThumb: string;
|
|
66
87
|
/** Redaction dot for `maskSecret` in the settings screen (§4.1 tier B). */
|
|
67
88
|
maskDot: string;
|
|
68
89
|
/**
|
package/dist/ui/glyphs.js
CHANGED
|
@@ -35,6 +35,8 @@ const UNICODE_GLYPHS = {
|
|
|
35
35
|
gaugeEmpty: '░',
|
|
36
36
|
keyOn: '●',
|
|
37
37
|
keyOff: '○',
|
|
38
|
+
boxChecked: '☑',
|
|
39
|
+
boxEmpty: '☐',
|
|
38
40
|
wordmark: '◇',
|
|
39
41
|
ellipsis: '…',
|
|
40
42
|
emDash: '—',
|
|
@@ -53,6 +55,8 @@ const UNICODE_GLYPHS = {
|
|
|
53
55
|
railVertical: '│',
|
|
54
56
|
railBranch: '├',
|
|
55
57
|
railEnd: '╰',
|
|
58
|
+
scrollTrack: '│',
|
|
59
|
+
scrollThumb: '█',
|
|
56
60
|
maskDot: '•',
|
|
57
61
|
boxStyle: 'round',
|
|
58
62
|
tool: {
|
|
@@ -63,6 +67,8 @@ const UNICODE_GLYPHS = {
|
|
|
63
67
|
glob: '⌕',
|
|
64
68
|
grep: '⌕',
|
|
65
69
|
list_dir: '▤',
|
|
70
|
+
ask_user: '?',
|
|
71
|
+
submit_plan: '◈',
|
|
66
72
|
},
|
|
67
73
|
toolDefault: '•',
|
|
68
74
|
};
|
|
@@ -83,6 +89,8 @@ const ASCII_GLYPHS = {
|
|
|
83
89
|
gaugeEmpty: '-',
|
|
84
90
|
keyOn: '*',
|
|
85
91
|
keyOff: 'o',
|
|
92
|
+
boxChecked: '[x]',
|
|
93
|
+
boxEmpty: '[ ]',
|
|
86
94
|
wordmark: '<>',
|
|
87
95
|
ellipsis: '...',
|
|
88
96
|
emDash: '--',
|
|
@@ -101,6 +109,8 @@ const ASCII_GLYPHS = {
|
|
|
101
109
|
railVertical: '|',
|
|
102
110
|
railBranch: '|',
|
|
103
111
|
railEnd: '\\',
|
|
112
|
+
scrollTrack: '|',
|
|
113
|
+
scrollThumb: '#',
|
|
104
114
|
maskDot: '*',
|
|
105
115
|
boxStyle: 'classic',
|
|
106
116
|
tool: {
|
|
@@ -111,6 +121,8 @@ const ASCII_GLYPHS = {
|
|
|
111
121
|
glob: '/',
|
|
112
122
|
grep: '/',
|
|
113
123
|
list_dir: '=',
|
|
124
|
+
ask_user: '?',
|
|
125
|
+
submit_plan: '#',
|
|
114
126
|
},
|
|
115
127
|
toolDefault: '*',
|
|
116
128
|
};
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import React from 'react';
|
|
13
13
|
import { type RenderMode } from './frame.js';
|
|
14
|
+
import { type FrameGeometry } from './regions.js';
|
|
14
15
|
export interface AppShellProps {
|
|
15
16
|
mode: RenderMode;
|
|
16
17
|
rows: number;
|
|
@@ -20,5 +21,13 @@ export interface AppShellProps {
|
|
|
20
21
|
toast: React.ReactNode;
|
|
21
22
|
composer: React.ReactNode;
|
|
22
23
|
status: React.ReactNode;
|
|
24
|
+
/**
|
|
25
|
+
* Where the wheel router reads the composer band boundary from (mouse-wheel
|
|
26
|
+
* §4.5). Written in a LAYOUT EFFECT to a MUTABLE REF, never to state: the
|
|
27
|
+
* measurement happens on every commit, and routing it through `setState`
|
|
28
|
+
* would add a re-render to the streaming path for a value nothing renders
|
|
29
|
+
* (R-7).
|
|
30
|
+
*/
|
|
31
|
+
geometryRef?: React.MutableRefObject<FrameGeometry>;
|
|
23
32
|
}
|
|
24
|
-
export declare function AppShell({ mode, rows, cols, header, viewport, toast, composer, status, }: AppShellProps): React.ReactElement;
|
|
33
|
+
export declare function AppShell({ mode, rows, cols, header, viewport, toast, composer, status, geometryRef, }: AppShellProps): React.ReactElement;
|
|
@@ -1,9 +1,44 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* AppShell (spec §4.3) — the frame that makes R2 true.
|
|
4
|
+
*
|
|
5
|
+
* In `fullscreen` the root box has a FIXED height of `frameHeight(rows)` and the
|
|
6
|
+
* middle viewport is the only `flexGrow` child, so the bottom chrome (toast /
|
|
7
|
+
* composer / status bar) is physically the last thing on the screen from the
|
|
8
|
+
* very first frame — empty session, long session, mid-scroll, overlay open.
|
|
9
|
+
*
|
|
10
|
+
* In `inline` the same children are emitted in the same order with no height
|
|
11
|
+
* constraint, i.e. byte-for-byte the v0.2.0 document-flow behavior.
|
|
12
|
+
*/
|
|
13
|
+
import { useLayoutEffect, useRef } from 'react';
|
|
14
|
+
import { Box, measureElement } from 'ink';
|
|
3
15
|
import { frameHeight } from './frame.js';
|
|
4
|
-
|
|
16
|
+
import { UNMEASURED_GEOMETRY } from './regions.js';
|
|
17
|
+
export function AppShell({ mode, rows, cols, header, viewport, toast, composer, status, geometryRef, }) {
|
|
18
|
+
const bottomRef = useRef(null);
|
|
19
|
+
// DECLARED ABOVE THE INLINE EARLY RETURN, and it has to stay there: React
|
|
20
|
+
// keys hooks by call order, and a hook that only runs in one mode changes
|
|
21
|
+
// that order between modes.
|
|
22
|
+
//
|
|
23
|
+
// Ink computes yoga layout in `onRender`, which React runs BEFORE layout
|
|
24
|
+
// effects, so this reflects the commit that just happened — the same
|
|
25
|
+
// mechanism `ScrollViewport` already relies on.
|
|
26
|
+
useLayoutEffect(() => {
|
|
27
|
+
if (!geometryRef)
|
|
28
|
+
return;
|
|
29
|
+
if (mode === 'inline' || !bottomRef.current) {
|
|
30
|
+
geometryRef.current = UNMEASURED_GEOMETRY;
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
geometryRef.current = {
|
|
34
|
+
// 1 in the alternate screen — see the note on `FrameGeometry.top`.
|
|
35
|
+
top: 1,
|
|
36
|
+
height: frameHeight(rows),
|
|
37
|
+
bottom: measureElement(bottomRef.current).height,
|
|
38
|
+
};
|
|
39
|
+
});
|
|
5
40
|
if (mode === 'inline') {
|
|
6
41
|
return (_jsxs(Box, { flexDirection: "column", children: [header, viewport, toast, composer, status] }));
|
|
7
42
|
}
|
|
8
|
-
return (_jsxs(Box, { flexDirection: "column", height: frameHeight(rows), width: cols, overflow: "hidden", children: [_jsx(Box, { flexDirection: "column", flexShrink: 0, children: header }), _jsx(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, overflow: "hidden", children: viewport }), _jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [toast, composer, status] })] }));
|
|
43
|
+
return (_jsxs(Box, { flexDirection: "column", height: frameHeight(rows), width: cols, overflow: "hidden", children: [_jsx(Box, { flexDirection: "column", flexShrink: 0, children: header }), _jsx(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, overflow: "hidden", children: viewport }), _jsxs(Box, { ref: bottomRef, flexDirection: "column", flexShrink: 0, children: [toast, composer, status] })] }));
|
|
9
44
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ScrollIndicator (mouse-wheel-region-routing §4.8) — the one-column scroll
|
|
3
|
+
* position rail on the viewport's right edge.
|
|
4
|
+
*
|
|
5
|
+
* NAMING (P2-1): this is NOT `layout/Gutter.tsx`. That one is the two-column
|
|
6
|
+
* left rail every transcript entry sits in, and it carries an invariant of its
|
|
7
|
+
* own. Two components called a gutter, on opposite edges, for unrelated
|
|
8
|
+
* purposes, is how a codebase stops being able to say what it means.
|
|
9
|
+
*
|
|
10
|
+
* THE COLUMN IS RESERVED UNCONDITIONALLY, track glyph and all. Showing it only
|
|
11
|
+
* while scrolled would change the content width, re-wrap every line, change the
|
|
12
|
+
* content height, and feed that back into the measurement that decides whether
|
|
13
|
+
* to show it — a shimmer at the exact moment the user is reading. Constant
|
|
14
|
+
* width, no feedback loop (D-9); `width={1}` and `flexShrink={0}` hold the
|
|
15
|
+
* column even on the first frame, before anything has been measured.
|
|
16
|
+
*/
|
|
17
|
+
import React from 'react';
|
|
18
|
+
import type { Theme } from '../theme.js';
|
|
19
|
+
import type { TermCapabilities } from '../capabilities.js';
|
|
20
|
+
import type { ThumbRange } from './scroll-indicator.js';
|
|
21
|
+
/** Below this width a column of content is worth more than the affordance. */
|
|
22
|
+
export declare const MIN_INDICATOR_COLS = 50;
|
|
23
|
+
export interface ScrollIndicatorProps {
|
|
24
|
+
/** Viewport height in rows, as measured by `ScrollViewport`. */
|
|
25
|
+
rows: number;
|
|
26
|
+
/** `null` when the content fits — the rail is drawn as bare track. */
|
|
27
|
+
thumb: ThumbRange | null;
|
|
28
|
+
theme: Theme;
|
|
29
|
+
caps: TermCapabilities;
|
|
30
|
+
}
|
|
31
|
+
export declare function ScrollIndicator({ rows, thumb, theme, caps, }: ScrollIndicatorProps): React.ReactElement;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box, Text } from 'ink';
|
|
3
|
+
import { pickGlyphs } from '../glyphs.js';
|
|
4
|
+
/** Below this width a column of content is worth more than the affordance. */
|
|
5
|
+
export const MIN_INDICATOR_COLS = 50;
|
|
6
|
+
export function ScrollIndicator({ rows, thumb, theme, caps, }) {
|
|
7
|
+
const glyphs = pickGlyphs(caps);
|
|
8
|
+
const count = Math.max(0, Math.floor(rows) || 0);
|
|
9
|
+
const cells = [];
|
|
10
|
+
for (let i = 0; i < count; i += 1) {
|
|
11
|
+
const onThumb = !!thumb && i >= thumb.start && i < thumb.start + thumb.size;
|
|
12
|
+
cells.push(_jsx(Text, { color: onThumb ? theme.primary : theme.border, children: onThumb ? glyphs.scrollThumb : glyphs.scrollTrack }, i));
|
|
13
|
+
}
|
|
14
|
+
return (_jsx(Box, { flexDirection: "column", flexShrink: 0, width: 1, children: cells }));
|
|
15
|
+
}
|