@aragon-agent/cli 0.6.7 → 0.6.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -43
- package/dist/agent/controller.d.ts +10 -6
- package/dist/agent/controller.js +35 -48
- package/dist/agent/headless.d.ts +2 -1
- package/dist/agent/prompt-options.d.ts +15 -0
- package/dist/agent/prompt-options.js +1 -0
- package/dist/cli.js +83 -53
- package/dist/commands/builtins.js +3 -1
- package/dist/commands/registry.d.ts +2 -3
- package/dist/input/interrupt-gesture.d.ts +18 -0
- package/dist/input/interrupt-gesture.js +19 -0
- package/dist/input/pointer-router.d.ts +11 -0
- package/dist/input/pointer-router.js +14 -0
- package/dist/todo/store.d.ts +8 -13
- package/dist/todo/store.js +13 -14
- package/dist/todo/types.d.ts +2 -1
- package/dist/ui/App.d.ts +2 -0
- package/dist/ui/App.js +166 -128
- package/dist/ui/Composer.d.ts +11 -5
- package/dist/ui/Composer.js +7 -11
- package/dist/ui/PromptCaret.d.ts +11 -0
- package/dist/ui/PromptCaret.js +31 -0
- package/dist/ui/PromptInput.d.ts +9 -6
- package/dist/ui/PromptInput.js +34 -14
- package/dist/ui/StatusBar.d.ts +1 -0
- package/dist/ui/StatusBar.js +14 -8
- package/dist/ui/Transcript.js +1 -0
- package/dist/ui/composer-rows.js +50 -9
- package/dist/ui/frame-differ.js +2 -19
- package/dist/ui/frame-observer.d.ts +17 -0
- package/dist/ui/frame-observer.js +81 -0
- package/dist/ui/frame-parser.d.ts +7 -0
- package/dist/ui/frame-parser.js +18 -0
- package/dist/ui/layout/AppShell.d.ts +7 -52
- package/dist/ui/layout/AppShell.js +2 -2
- package/dist/ui/layout/ScrollIndicator.d.ts +4 -3
- package/dist/ui/layout/ScrollIndicator.js +4 -4
- package/dist/ui/layout/ScrollViewport.d.ts +13 -66
- package/dist/ui/layout/ScrollViewport.js +183 -185
- package/dist/ui/layout/budget.d.ts +3 -51
- package/dist/ui/layout/budget.js +4 -53
- package/dist/ui/layout/document-layout.d.ts +13 -0
- package/dist/ui/layout/document-layout.js +11 -0
- package/dist/ui/layout/follow-state.d.ts +1 -0
- package/dist/ui/layout/follow-state.js +1 -3
- package/dist/ui/layout/scroll-indicator.d.ts +1 -1
- package/dist/ui/layout/scroll-indicator.js +3 -2
- package/dist/ui/layout/scrollbar-geometry.d.ts +17 -0
- package/dist/ui/layout/scrollbar-geometry.js +13 -0
- package/dist/ui/layout/todo-layout.d.ts +1 -0
- package/dist/ui/layout/todo-layout.js +3 -2
- package/dist/ui/layout/viewport-geometry.d.ts +1 -0
- package/dist/ui/layout/viewport-geometry.js +1 -0
- package/dist/ui/layout/virtual-window.d.ts +1 -0
- package/dist/ui/layout/virtual-window.js +1 -1
- package/dist/ui/overlays/HelpOverlay.js +5 -2
- package/dist/ui/screen.d.ts +1 -1
- package/dist/ui/screen.js +4 -6
- package/dist/ui/scrollbar-controller.d.ts +30 -0
- package/dist/ui/scrollbar-controller.js +124 -0
- package/dist/ui/use-wheel-routing.d.ts +2 -0
- package/dist/ui/use-wheel-routing.js +6 -0
- package/package.json +2 -2
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useLayoutEffect, useState } from 'react';
|
|
3
|
+
import { Text } from 'ink';
|
|
4
|
+
import { displayWidth } from './composer-rows.js';
|
|
5
|
+
function monochromeMarker(text) {
|
|
6
|
+
// Ink's output grid advances for combining marks, even though they have no
|
|
7
|
+
// terminal width. Retain them in both phases so its cell count stays stable.
|
|
8
|
+
const replace = (marker) => Array.from(text, (point) => {
|
|
9
|
+
const width = displayWidth(point);
|
|
10
|
+
return width > 0 ? marker.repeat(width) : point;
|
|
11
|
+
}).join('');
|
|
12
|
+
const underline = replace('_');
|
|
13
|
+
return underline === text ? replace('^') : underline;
|
|
14
|
+
}
|
|
15
|
+
/** The only blinking state belongs to this leaf, never to the editor. */
|
|
16
|
+
export function PromptCaret({ text, active, reducedMotion, colorLevel, color, resetKey, }) {
|
|
17
|
+
const [visible, setVisible] = useState(true);
|
|
18
|
+
useLayoutEffect(() => {
|
|
19
|
+
setVisible(true);
|
|
20
|
+
if (!active || reducedMotion)
|
|
21
|
+
return;
|
|
22
|
+
const timer = setInterval(() => setVisible((previous) => !previous), 500);
|
|
23
|
+
return () => clearInterval(timer);
|
|
24
|
+
}, [resetKey, active, reducedMotion]);
|
|
25
|
+
const bright = active && (reducedMotion || visible);
|
|
26
|
+
let content = text;
|
|
27
|
+
if (bright && colorLevel === 0) {
|
|
28
|
+
content = monochromeMarker(text);
|
|
29
|
+
}
|
|
30
|
+
return _jsx(Text, { color: color, inverse: bright && colorLevel > 0, children: content });
|
|
31
|
+
}
|
package/dist/ui/PromptInput.d.ts
CHANGED
|
@@ -31,6 +31,9 @@ export interface CommandOption {
|
|
|
31
31
|
description: string;
|
|
32
32
|
}
|
|
33
33
|
interface PromptInputProps {
|
|
34
|
+
cols?: number;
|
|
35
|
+
cursorVisible?: boolean;
|
|
36
|
+
onInteraction?: () => void;
|
|
34
37
|
isActive: boolean;
|
|
35
38
|
running: boolean;
|
|
36
39
|
history: string[];
|
|
@@ -41,11 +44,8 @@ interface PromptInputProps {
|
|
|
41
44
|
/**
|
|
42
45
|
* Row budget for the completion popup (§5.2 / R-14).
|
|
43
46
|
*
|
|
44
|
-
* The popup
|
|
45
|
-
*
|
|
46
|
-
* status bar past the frame height, where the root box's `overflow: hidden`
|
|
47
|
-
* clips it away — the one piece of chrome R2 promises is always visible.
|
|
48
|
-
* `App` derives this from `budget.ts` so the popup shrinks instead.
|
|
47
|
+
* The popup shares the scrolling footer with the editor. Its height budget
|
|
48
|
+
* reserves visible message rows without changing the viewport or TODO rail.
|
|
49
49
|
*/
|
|
50
50
|
popupMaxRows?: number;
|
|
51
51
|
/** Total menu rows, including borders and the overflow hint. */
|
|
@@ -53,6 +53,9 @@ interface PromptInputProps {
|
|
|
53
53
|
onPopupRowsChange?: (rows: number) => void;
|
|
54
54
|
onSubmit: (text: string) => void;
|
|
55
55
|
onHelp?: () => void;
|
|
56
|
+
onEscape?: () => void;
|
|
57
|
+
onEscapeDismiss?: () => void;
|
|
58
|
+
reducedMotion?: boolean;
|
|
56
59
|
/**
|
|
57
60
|
* Mirror of the session mode. Read for the placeholder only — the toggle
|
|
58
61
|
* itself belongs to `App`, and this component's job is to get out of its way
|
|
@@ -176,5 +179,5 @@ export declare function fileTokenAt(buffer: string, cursor: number): {
|
|
|
176
179
|
end: number;
|
|
177
180
|
query: string;
|
|
178
181
|
} | null;
|
|
179
|
-
export declare function PromptInput({ isActive, running, history, commands, cwd, theme, caps, popupMaxRows, popupMaxHeight, onPopupRowsChange, agentMode, onSubmit, onHelp, bordered, onDraftChange, onNotice, scrolledLines, }: PromptInputProps): React.ReactElement;
|
|
182
|
+
export declare function PromptInput({ cols: colsProp, cursorVisible, onInteraction, isActive, running, history, commands, cwd, theme, caps, popupMaxRows, popupMaxHeight, onPopupRowsChange, agentMode, onSubmit, onHelp, onEscape, onEscapeDismiss, reducedMotion, bordered, onDraftChange, onNotice, scrolledLines, }: PromptInputProps): React.ReactElement;
|
|
180
183
|
export {};
|
package/dist/ui/PromptInput.js
CHANGED
|
@@ -19,10 +19,11 @@ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-run
|
|
|
19
19
|
* stays a plain `useState` because it is written from the debounced glob effect
|
|
20
20
|
* rather than from a key.
|
|
21
21
|
*/
|
|
22
|
-
import { useEffect, useLayoutEffect, useReducer, useRef, useState } from 'react';
|
|
22
|
+
import { useEffect, useLayoutEffect, useMemo, useReducer, useRef, useState } from 'react';
|
|
23
23
|
import { Box, Text, useInput, useStdout } from 'ink';
|
|
24
24
|
import { glob as tinyGlob } from 'tinyglobby';
|
|
25
25
|
import { pickGlyphs } from './glyphs.js';
|
|
26
|
+
import { PromptCaret } from './PromptCaret.js';
|
|
26
27
|
import { recognize } from '../input/keymap.js';
|
|
27
28
|
import { AutocompletePopup } from './AutocompletePopup.js';
|
|
28
29
|
import { buildAutocompleteLayout } from './layout/autocomplete.js';
|
|
@@ -43,8 +44,8 @@ const CHIP_FULL_MIN_COLS = 64;
|
|
|
43
44
|
* reads `999+`.
|
|
44
45
|
*/
|
|
45
46
|
const CHIP_MAX_N = 999;
|
|
46
|
-
/** `↓ 999+
|
|
47
|
-
const CHIP_FULL_CELLS =
|
|
47
|
+
/** `↓ 999+ lines below · PgDn` — the widest form the full branch can produce. */
|
|
48
|
+
const CHIP_FULL_CELLS = 27;
|
|
48
49
|
/** `↓999+` — the widest form the terse branch can produce. */
|
|
49
50
|
const CHIP_TERSE_CELLS = 6;
|
|
50
51
|
/**
|
|
@@ -64,7 +65,7 @@ export function scrollChip(scrolledLines, cols, glyphs) {
|
|
|
64
65
|
}
|
|
65
66
|
const noun = n === 1 ? 'line' : 'lines';
|
|
66
67
|
return {
|
|
67
|
-
text: `${glyphs.arrowDown} ${shown}
|
|
68
|
+
text: `${glyphs.arrowDown} ${shown} ${noun} below ${glyphs.midDot} PgDn`,
|
|
68
69
|
cells: CHIP_FULL_CELLS,
|
|
69
70
|
};
|
|
70
71
|
}
|
|
@@ -247,7 +248,7 @@ function isControlSeq(input) {
|
|
|
247
248
|
// ---------------------------------------------------------------------------
|
|
248
249
|
// Component
|
|
249
250
|
// ---------------------------------------------------------------------------
|
|
250
|
-
export function PromptInput({ isActive, running, history, commands, cwd, theme, caps, popupMaxRows, popupMaxHeight, onPopupRowsChange, agentMode, onSubmit, onHelp, bordered, onDraftChange, onNotice, scrolledLines = 0, }) {
|
|
251
|
+
export function PromptInput({ cols: colsProp, cursorVisible = true, onInteraction, isActive, running, history, commands, cwd, theme, caps, popupMaxRows, popupMaxHeight, onPopupRowsChange, agentMode, onSubmit, onHelp, onEscape, onEscapeDismiss, reducedMotion = false, bordered, onDraftChange, onNotice, scrolledLines = 0, }) {
|
|
251
252
|
const { stdout } = useStdout();
|
|
252
253
|
const [editor, dispatch] = useReducer(editorReducer, INITIAL_EDITOR_STATE);
|
|
253
254
|
const { buffer, cursor, historyIndex, dismissed, sel } = editor;
|
|
@@ -314,7 +315,7 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
|
|
|
314
315
|
// Deriving it twice — once for the frame and once for the budget — is exactly
|
|
315
316
|
// the trap `BottomStatusRow.tsx:5-27` documents.
|
|
316
317
|
const glyphs = pickGlyphs(caps);
|
|
317
|
-
const cols = stdout?.columns ?? 80;
|
|
318
|
+
const cols = colsProp ?? stdout?.columns ?? 80;
|
|
318
319
|
const terminalRows = stdout?.rows ?? 24;
|
|
319
320
|
// Full-screen only: inline mode has no self-drawn viewport, so `scrolledLines`
|
|
320
321
|
// is always 0 there and the branch is unreachable rather than merely unused.
|
|
@@ -330,7 +331,7 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
|
|
|
330
331
|
cursor,
|
|
331
332
|
cols: wrapCols,
|
|
332
333
|
maxRows: draftMaxRows(terminalRows),
|
|
333
|
-
active: isActive,
|
|
334
|
+
active: isActive && cursorVisible,
|
|
334
335
|
});
|
|
335
336
|
// TWO PASSES, AND THE SECOND ONE IS LOAD-BEARING (I-8). The overflow indicator
|
|
336
337
|
// rides the last rendered row INSIDE the box the draft wraps in, so its cells
|
|
@@ -348,6 +349,7 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
|
|
|
348
349
|
const usableCols = overflows ? Math.max(8, baseCols - OVERFLOW_CELLS - 1) : baseCols;
|
|
349
350
|
const layout = overflows ? measure(usableCols) : wide;
|
|
350
351
|
const draftRowCount = Math.max(1, layout.rows.length);
|
|
352
|
+
const caretResetKey = useMemo(() => ({}), [buffer, cursor, isActive, usableCols]);
|
|
351
353
|
// --- Draft presence and height, reported only on a transition. -----------
|
|
352
354
|
// Notifying on every keystroke would re-render the whole App tree per key. The
|
|
353
355
|
// row count changes on a line break or a wrap boundary, not per character
|
|
@@ -368,6 +370,7 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
|
|
|
368
370
|
// by line. Two dispatches from one key would put the legacy root back to two
|
|
369
371
|
// commits and undo the whole of F3 for that branch.
|
|
370
372
|
const insert = (text) => {
|
|
373
|
+
onInteraction?.();
|
|
371
374
|
dispatch({ type: 'insert', text });
|
|
372
375
|
};
|
|
373
376
|
/**
|
|
@@ -379,6 +382,7 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
|
|
|
379
382
|
* needs. `clear` releases every payload in the same dispatch.
|
|
380
383
|
*/
|
|
381
384
|
const submit = (text) => {
|
|
385
|
+
onInteraction?.();
|
|
382
386
|
onSubmit(expandPastes(text, editor.pastes));
|
|
383
387
|
dispatch({ type: 'clear' });
|
|
384
388
|
};
|
|
@@ -404,6 +408,7 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
|
|
|
404
408
|
dispatch({ type: 'recall', buffer: value, cursor: value.length, historyIndex: idx });
|
|
405
409
|
};
|
|
406
410
|
const completeSelection = () => {
|
|
411
|
+
onInteraction?.();
|
|
407
412
|
const chosen = popupItems[clampedSel];
|
|
408
413
|
if (!chosen)
|
|
409
414
|
return;
|
|
@@ -417,6 +422,7 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
|
|
|
417
422
|
}
|
|
418
423
|
};
|
|
419
424
|
const verticalOrHistory = (dir) => {
|
|
425
|
+
onInteraction?.();
|
|
420
426
|
const moved = moveVertical(buffer, cursor, dir);
|
|
421
427
|
if (moved) {
|
|
422
428
|
dispatch({ type: 'moveCursor', cursor: moved.cursor });
|
|
@@ -441,13 +447,17 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
|
|
|
441
447
|
// appears with a popup open and is therefore easy to ship (AC-P3 / R-P2).
|
|
442
448
|
if (key.tab && key.shift)
|
|
443
449
|
return;
|
|
450
|
+
if (key.pageUp || key.pageDown || (key.shift && (key.upArrow || key.downArrow)))
|
|
451
|
+
return;
|
|
444
452
|
// Popup navigation owns Up/Down/Tab/→/Enter/Esc while it is open.
|
|
445
453
|
if (popupVisible) {
|
|
446
454
|
if (key.upArrow) {
|
|
455
|
+
onInteraction?.();
|
|
447
456
|
dispatch({ type: 'select', sel: Math.max(0, clampedSel - 1) });
|
|
448
457
|
return;
|
|
449
458
|
}
|
|
450
459
|
if (key.downArrow) {
|
|
460
|
+
onInteraction?.();
|
|
451
461
|
dispatch({ type: 'select', sel: Math.min(popupItems.length - 1, clampedSel + 1) });
|
|
452
462
|
return;
|
|
453
463
|
}
|
|
@@ -463,15 +473,19 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
|
|
|
463
473
|
}
|
|
464
474
|
if (key.escape) {
|
|
465
475
|
dispatch({ type: 'dismiss' });
|
|
476
|
+
onEscapeDismiss?.();
|
|
466
477
|
return;
|
|
467
478
|
}
|
|
468
479
|
// Any other key falls through so typing keeps filtering the popup.
|
|
469
480
|
}
|
|
470
|
-
if (key.escape)
|
|
471
|
-
|
|
481
|
+
if (key.escape) {
|
|
482
|
+
onEscape?.();
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
472
485
|
// Line-editing intents (Home/End/word/kill) recognized from raw keys.
|
|
473
486
|
const intent = recognize(input, key);
|
|
474
487
|
if (intent) {
|
|
488
|
+
onInteraction?.();
|
|
475
489
|
const next = applyEdit(buffer, cursor, intent);
|
|
476
490
|
dispatch({ type: 'replace', buffer: next.buffer, cursor: next.cursor });
|
|
477
491
|
return;
|
|
@@ -489,6 +503,7 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
|
|
|
489
503
|
return;
|
|
490
504
|
}
|
|
491
505
|
if (key.backspace || key.delete) {
|
|
506
|
+
onInteraction?.();
|
|
492
507
|
// The `cursor > 0` guard lives in the reducer now, so this branch is one
|
|
493
508
|
// unconditional dispatch and a no-op at the left edge returns the same
|
|
494
509
|
// state object — which React bails out on, exactly as the old
|
|
@@ -497,11 +512,15 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
|
|
|
497
512
|
return;
|
|
498
513
|
}
|
|
499
514
|
if (key.leftArrow) {
|
|
500
|
-
|
|
515
|
+
onInteraction?.();
|
|
516
|
+
const step = cursor >= 2 && (buffer.codePointAt(cursor - 2) ?? 0) > 0xffff ? 2 : 1;
|
|
517
|
+
dispatch({ type: 'moveCursor', cursor: Math.max(0, cursor - step) });
|
|
501
518
|
return;
|
|
502
519
|
}
|
|
503
520
|
if (key.rightArrow) {
|
|
504
|
-
|
|
521
|
+
onInteraction?.();
|
|
522
|
+
const step = (buffer.codePointAt(cursor) ?? 0) > 0xffff ? 2 : 1;
|
|
523
|
+
dispatch({ type: 'moveCursor', cursor: Math.min(buffer.length, cursor + step) });
|
|
505
524
|
return;
|
|
506
525
|
}
|
|
507
526
|
// `!key.shift` is load-bearing: Shift+↑/↓ is the viewport's line-scroll
|
|
@@ -539,6 +558,7 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
|
|
|
539
558
|
onNotice?.('warn', refusal);
|
|
540
559
|
return;
|
|
541
560
|
}
|
|
561
|
+
onInteraction?.();
|
|
542
562
|
dispatch({ type: 'input', segments });
|
|
543
563
|
return;
|
|
544
564
|
}
|
|
@@ -549,7 +569,7 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
|
|
|
549
569
|
const marker = running ? glyphs.steer : glyphs.caret;
|
|
550
570
|
const markerColor = running ? theme.toolRunning : theme.primary;
|
|
551
571
|
const placeholder = running
|
|
552
|
-
? `Type to steer the run, Esc to
|
|
572
|
+
? `Type to steer the run, Esc twice to interrupt${glyphs.ellipsis}`
|
|
553
573
|
: agentMode === 'plan'
|
|
554
574
|
? `Describe what you want to build; I'll research and plan it first${glyphs.ellipsis}`
|
|
555
575
|
: `Send a message (/ for commands, @ for files)${glyphs.ellipsis}`;
|
|
@@ -562,9 +582,9 @@ export function PromptInput({ isActive, running, history, commands, cwd, theme,
|
|
|
562
582
|
// The caret is drawn as one inverse cell, split at the column
|
|
563
583
|
// `layoutComposer` reported — the same width table it wrapped with (I-9).
|
|
564
584
|
const split = splitRowAtColumn(row, layout.cursorCol);
|
|
565
|
-
return (_jsxs(_Fragment, { children: [renderSegments(split.before, `b${i}.`), _jsx(
|
|
585
|
+
return (_jsxs(_Fragment, { children: [renderSegments(split.before, `b${i}.`), _jsx(PromptCaret, { text: split.at ? split.at.text : ' ', active: isActive && cursorVisible, reducedMotion: reducedMotion, colorLevel: caps.colorLevel, resetKey: caretResetKey, color: split.at?.kind === 'token' ? tokenColor : undefined }), renderSegments(split.after, `a${i}.`)] }));
|
|
566
586
|
});
|
|
567
|
-
const inputRow = (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: markerColor, bold: true, children: [marker, ' '] }), _jsx(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, children: buffer.length === 0 ? (
|
|
587
|
+
const inputRow = (_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: markerColor, bold: true, children: [marker, ' '] }), _jsx(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, children: buffer.length === 0 ? (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [_jsx(PromptCaret, { text: String.fromCodePoint(placeholder.codePointAt(0)), active: isActive && cursorVisible, reducedMotion: reducedMotion, colorLevel: caps.colorLevel, color: theme.muted, resetKey: caretResetKey }), placeholder.slice(String.fromCodePoint(placeholder.codePointAt(0)).length)] })) : (rowNodes.map((node, i) => overflow && i === rowNodes.length - 1 ? (
|
|
568
588
|
// The overflow indicator rides the LAST rendered row rather than
|
|
569
589
|
// taking one of its own: an extra row would put the composer's
|
|
570
590
|
// real height one above the `draftRows` it just reported (I-8).
|
package/dist/ui/StatusBar.d.ts
CHANGED
|
@@ -21,6 +21,7 @@ import type { UsageTotal } from '../agent/reducer.js';
|
|
|
21
21
|
import type { ContextUsageSnapshot } from '../compaction/types.js';
|
|
22
22
|
import { type AgentMode } from '../agent/agent-mode.js';
|
|
23
23
|
interface StatusBarProps {
|
|
24
|
+
interruptHint?: string;
|
|
24
25
|
model: string;
|
|
25
26
|
provider: string;
|
|
26
27
|
usageTotal: UsageTotal;
|
package/dist/ui/StatusBar.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import stringWidth from 'string-width';
|
|
2
3
|
import { Box, Text, useStdout } from 'ink';
|
|
3
4
|
import { pickGlyphs } from './glyphs.js';
|
|
4
5
|
import { formatCost, formatTokens, formatDuration, promptTokensOf } from '../agent/usage.js';
|
|
@@ -95,6 +96,11 @@ export function StatusBar(props) {
|
|
|
95
96
|
: pendingAgentMode
|
|
96
97
|
? `${MODE_LABEL[agentMode]} ${glyphs.arrowRight} ${MODE_LABEL[pendingAgentMode]}`
|
|
97
98
|
: MODE_LABEL[agentMode];
|
|
99
|
+
const actionHints = [running ? (props.interruptHint ?? 'Esc abort') : '',
|
|
100
|
+
servicesActive ? `Ctrl+C stop ${servicesActive.live}` : '',
|
|
101
|
+
scrolledLines > 0 ? 'PgDn down' : ''].filter(Boolean).join(' | ');
|
|
102
|
+
const hintCols = actionHints ? Math.min(Math.max(0, cols - 1), stringWidth(actionHints) + 2) : 0;
|
|
103
|
+
const detailCols = Math.max(0, cols - 1 - hintCols);
|
|
98
104
|
return (
|
|
99
105
|
// `flexShrink={0}` on the left cluster is not cosmetic. When the two clusters
|
|
100
106
|
// over-subscribe the row, yoga shrinks the flexible children and Ink's text
|
|
@@ -102,11 +108,11 @@ export function StatusBar(props) {
|
|
|
102
108
|
// and "? hel" rather than truncating cleanly at one end. Pinning the left
|
|
103
109
|
// cluster keeps the run status and model intact and makes the degradation
|
|
104
110
|
// land in one predictable place.
|
|
105
|
-
_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: [
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
111
|
+
_jsxs(Box, { width: cols, flexDirection: "row", justifyContent: "space-between", flexShrink: 0, height: 1, overflow: "hidden", children: [_jsx(Box, { width: 1, flexShrink: 0, children: _jsx(Text, { children: redrawChar(redrawNonce) }) }), hintCols > 0 && (_jsx(Box, { width: hintCols, flexShrink: 0, overflow: "hidden", children: _jsxs(Text, { wrap: "truncate", color: theme.noticeWarn, children: [actionHints, ' '] }) })), _jsxs(Box, { display: detailCols > 0 ? 'flex' : 'none', width: detailCols, flexShrink: 0, flexDirection: "row", justifyContent: "space-between", overflow: "hidden", children: [_jsxs(Box, { flexDirection: "row", flexShrink: 0, overflow: "hidden", children: [_jsxs(Text, { color: running ? theme.toolRunning : theme.toolDone, children: [statusGlyph, " ", running ? 'running' : 'idle'] }), modeWord && (_jsxs(Text, { color: theme.accent, bold: true, children: [' ', modeWord] })), teamActive && (_jsxs(Text, { color: theme.toolRunning, bold: true, children: [' ', cols >= TEAM_LIMITS.statusCompactCols
|
|
112
|
+
? `agents ${teamActive.running}/${teamActive.total}`
|
|
113
|
+
: `[${teamActive.running}]`] })), retryActive && (_jsxs(Text, { color: theme.noticeWarn, bold: true, children: [' ', formatRetryChip(retryActive, cols)] })), todoActive && (_jsxs(Text, { color: theme.accent, children: [' ', cols >= TODO_LIMITS.statusCompactCols
|
|
114
|
+
? `todo ${todoActive.done}/${todoActive.total}`
|
|
115
|
+
: `[${todoActive.done}/${todoActive.total}]`] })), servicesActive && (_jsxs(Text, { color: theme.accent, children: [' ', cols >= PROC_LIMITS.statusCompactCols
|
|
116
|
+
? `svc ${servicesActive.live}`
|
|
117
|
+
: `[${servicesActive.live}]`] })), cols >= 60 && (cols >= 110 || (!running && !servicesActive && scrolledLines === 0)) && (_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: [compactionActive && cols >= COMPACTION_LIMITS.statusCompactCols && (_jsxs(Text, { color: compactionActive.inFlight ? theme.accent : theme.muted, children: [compactionActive.inFlight ? 'compacting' : 'compact', ' '] })), fastActive && cols >= FAST_LIMITS.statusCompactCols && (_jsxs(Text, { color: theme.muted, children: ["fast", fastActive.inFlight ? '*' : '', ' '] })), ecoRung > 0 && cols >= ECO_MIN_COLS && (_jsxs(Text, { color: theme.muted, children: ["eco", ' '] })), 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 >= ABSOLUTE_PAIR_MIN_COLS && (_jsxs(Text, { color: theme.muted, children: [" ", absolutePair] })), cols >= SESSION_TOTAL_MIN_COLS && _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)] }))] })] })] }));
|
|
112
118
|
}
|
package/dist/ui/Transcript.js
CHANGED
|
@@ -336,6 +336,7 @@ export function TranscriptList({ entries, expandedToolIds, thinkingVisible, redu
|
|
|
336
336
|
entries: visible,
|
|
337
337
|
heightOf,
|
|
338
338
|
viewportRows: geometry.viewportRows,
|
|
339
|
+
trailingContentRows: geometry.trailingContentRows,
|
|
339
340
|
offset: geometry.offset,
|
|
340
341
|
overscan: VIRTUAL_LIMITS.overscan,
|
|
341
342
|
});
|
package/dist/ui/composer-rows.js
CHANGED
|
@@ -52,6 +52,32 @@ export function displayWidth(text) {
|
|
|
52
52
|
w += charWidth(ch);
|
|
53
53
|
return w;
|
|
54
54
|
}
|
|
55
|
+
/** Normalize only the visual projection; the editor keeps its UTF-16 index. */
|
|
56
|
+
function visualCursor(buffer, cursor) {
|
|
57
|
+
let index = Math.max(0, Math.min(buffer.length, cursor));
|
|
58
|
+
const code = buffer.charCodeAt(index);
|
|
59
|
+
const previous = buffer.charCodeAt(index - 1);
|
|
60
|
+
if (code >= 0xdc00 && code <= 0xdfff && previous >= 0xd800 && previous <= 0xdbff)
|
|
61
|
+
index -= 1;
|
|
62
|
+
if (index === buffer.length || buffer[index] === '\n')
|
|
63
|
+
return index;
|
|
64
|
+
const ch = String.fromCodePoint(buffer.codePointAt(index));
|
|
65
|
+
if (ch === '\t' || charWidth(ch) > 0)
|
|
66
|
+
return index;
|
|
67
|
+
const start = buffer.lastIndexOf('\n', index - 1) + 1;
|
|
68
|
+
let nearest = -1;
|
|
69
|
+
let i = start;
|
|
70
|
+
while (i < buffer.length && buffer[i] !== '\n') {
|
|
71
|
+
const point = String.fromCodePoint(buffer.codePointAt(i));
|
|
72
|
+
if (point === '\t' || charWidth(point) > 0) {
|
|
73
|
+
if (i > index)
|
|
74
|
+
return nearest >= 0 ? nearest : i;
|
|
75
|
+
nearest = i;
|
|
76
|
+
}
|
|
77
|
+
i += point.length;
|
|
78
|
+
}
|
|
79
|
+
return nearest >= 0 ? nearest : i;
|
|
80
|
+
}
|
|
55
81
|
/**
|
|
56
82
|
* Wrap `buffer` into rows, locate the caret, and select a window of at most
|
|
57
83
|
* `maxRows` rows that contains it.
|
|
@@ -62,7 +88,8 @@ export function displayWidth(text) {
|
|
|
62
88
|
* to live in the reducer and would then need its own invalidation rules.
|
|
63
89
|
*/
|
|
64
90
|
export function layoutComposer(input) {
|
|
65
|
-
const { buffer,
|
|
91
|
+
const { buffer, active } = input;
|
|
92
|
+
const cursor = visualCursor(buffer, input.cursor);
|
|
66
93
|
const cols = Math.max(1, Math.floor(input.cols));
|
|
67
94
|
const maxRows = input.maxRows >= 1 ? Math.floor(input.maxRows) : 1;
|
|
68
95
|
const spans = tokenSpans(buffer);
|
|
@@ -111,6 +138,7 @@ export function layoutComposer(input) {
|
|
|
111
138
|
const ch = String.fromCodePoint(buffer.codePointAt(i));
|
|
112
139
|
if (ch === '\n') {
|
|
113
140
|
if (atCursor) {
|
|
141
|
+
ensureRoom(1);
|
|
114
142
|
cursorRowAbs = rows.length;
|
|
115
143
|
cursorCol = col;
|
|
116
144
|
}
|
|
@@ -182,20 +210,33 @@ export function splitRowAtColumn(row, column) {
|
|
|
182
210
|
else
|
|
183
211
|
list.push({ text, kind });
|
|
184
212
|
};
|
|
213
|
+
const cells = [];
|
|
214
|
+
let leading = '';
|
|
185
215
|
for (const segment of row.segments) {
|
|
186
|
-
if (at !== null) {
|
|
187
|
-
push(after, segment.text, segment.kind);
|
|
188
|
-
continue;
|
|
189
|
-
}
|
|
190
216
|
for (const ch of segment.text) {
|
|
191
|
-
if (
|
|
192
|
-
|
|
217
|
+
if (charWidth(ch) === 0) {
|
|
218
|
+
const last = cells[cells.length - 1];
|
|
219
|
+
if (last)
|
|
220
|
+
cells[cells.length - 1] = { ...last, text: last.text + ch };
|
|
221
|
+
else
|
|
222
|
+
leading += ch;
|
|
193
223
|
}
|
|
194
224
|
else {
|
|
195
|
-
push(
|
|
225
|
+
cells.push({ text: leading + ch, kind: segment.kind });
|
|
226
|
+
leading = '';
|
|
196
227
|
}
|
|
197
|
-
x += charWidth(ch);
|
|
198
228
|
}
|
|
199
229
|
}
|
|
230
|
+
// A zero-width-only row keeps its text before the explicitly budgeted space.
|
|
231
|
+
if (leading)
|
|
232
|
+
push(before, leading, row.segments[0]?.kind ?? 'text');
|
|
233
|
+
for (const cell of cells) {
|
|
234
|
+
const width = displayWidth(cell.text);
|
|
235
|
+
if (at === null && column >= x && column < x + width)
|
|
236
|
+
at = cell;
|
|
237
|
+
else
|
|
238
|
+
push(at === null ? before : after, cell.text, cell.kind);
|
|
239
|
+
x += width;
|
|
240
|
+
}
|
|
200
241
|
return { before, at, after };
|
|
201
242
|
}
|
package/dist/ui/frame-differ.js
CHANGED
|
@@ -38,6 +38,7 @@
|
|
|
38
38
|
* mode of it firing spuriously is one extra full repaint.
|
|
39
39
|
*/
|
|
40
40
|
import { MIN_FULLSCREEN_ROWS } from './layout/frame.js';
|
|
41
|
+
import { matchInkErasePrefix } from './frame-parser.js';
|
|
41
42
|
// ---------------------------------------------------------------------------
|
|
42
43
|
// Constants — exact, so no call site ever re-derives an escape sequence.
|
|
43
44
|
// ---------------------------------------------------------------------------
|
|
@@ -55,18 +56,6 @@ const ERASE_DOWN = `${CSI}J`;
|
|
|
55
56
|
/** DEC private mode 2026 — begin/end synchronized update (F2, §3.3). */
|
|
56
57
|
const SYNC_BEGIN = `${CSI}?2026h`;
|
|
57
58
|
const SYNC_END = `${CSI}?2026l`;
|
|
58
|
-
/**
|
|
59
|
-
* `eraseLines(n)` for any n ≥ 1, anchored at the start of the chunk.
|
|
60
|
-
*
|
|
61
|
-
* FALLBACK ONLY (P2-3). Whenever `prev !== null` the differ already knows the
|
|
62
|
-
* exact prefix it expects — `eraseLinesPrefix(prev.length + 1)` is a
|
|
63
|
-
* deterministic string — so the hot path is `chunk.startsWith(expected)`:
|
|
64
|
-
* linear, unambiguous, and it collapses step 7's `erased !== prev.length + 1`
|
|
65
|
-
* test into the same comparison. This regex is reached only on the first
|
|
66
|
-
* diffable chunk after a session start or an `invalidate()`, where no
|
|
67
|
-
* expectation exists yet.
|
|
68
|
-
*/
|
|
69
|
-
const ERASE_PREFIX_RE = /^(?:\x1b\[2K(?:\x1b\[1A)?)+\x1b\[G/;
|
|
70
59
|
function cursorTo(row) {
|
|
71
60
|
return `${CSI}${row};1H`;
|
|
72
61
|
}
|
|
@@ -88,9 +77,6 @@ export function eraseLinesPrefix(count) {
|
|
|
88
77
|
}
|
|
89
78
|
return out + CURSOR_LEFT;
|
|
90
79
|
}
|
|
91
|
-
function countErases(prefix) {
|
|
92
|
-
return prefix.split(ERASE_LINE).length - 1;
|
|
93
|
-
}
|
|
94
80
|
export function createFrameDiffer(options) {
|
|
95
81
|
/**
|
|
96
82
|
* Last frame AS PAINTED — what is actually on the screen.
|
|
@@ -200,10 +186,7 @@ export function createFrameDiffer(options) {
|
|
|
200
186
|
if (chunk.startsWith(expected))
|
|
201
187
|
return { prefix: expected, erased: prev.length + 1 };
|
|
202
188
|
}
|
|
203
|
-
|
|
204
|
-
if (m === null)
|
|
205
|
-
return null;
|
|
206
|
-
return { prefix: m[0], erased: countErases(m[0]) };
|
|
189
|
+
return matchInkErasePrefix(chunk);
|
|
207
190
|
};
|
|
208
191
|
/**
|
|
209
192
|
* Full repaint — the first diffable frame of a session, every invalidation
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ScrollbarBridge } from './scrollbar-controller.js';
|
|
2
|
+
export interface FrameSummary {
|
|
3
|
+
columns: number;
|
|
4
|
+
rows: number;
|
|
5
|
+
trackTop: number;
|
|
6
|
+
trackRows: number;
|
|
7
|
+
}
|
|
8
|
+
/** Observe before differ transformation, but acknowledge only after downstream write returns. */
|
|
9
|
+
export declare function createFrameObserver(options: {
|
|
10
|
+
stdout: NodeJS.WriteStream;
|
|
11
|
+
terminal: NodeJS.WriteStream;
|
|
12
|
+
scrollbar: ScrollbarBridge;
|
|
13
|
+
}): {
|
|
14
|
+
stdout: NodeJS.WriteStream;
|
|
15
|
+
dispose(): void;
|
|
16
|
+
invalidate(): void;
|
|
17
|
+
};
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import stripAnsi from 'strip-ansi';
|
|
2
|
+
import stringWidth from 'string-width';
|
|
3
|
+
import { parseInkFrame } from './frame-parser.js';
|
|
4
|
+
import { pickGlyphs } from './glyphs.js';
|
|
5
|
+
function summarizeFrame(chunk, rows, cols) {
|
|
6
|
+
const lines = typeof chunk === 'string' ? parseInkFrame(chunk) : null;
|
|
7
|
+
if (!lines || rows < 12 || cols < 40 || lines.length !== rows - 1)
|
|
8
|
+
return null;
|
|
9
|
+
const unicode = pickGlyphs({ unicode: true, colorLevel: 0 });
|
|
10
|
+
const cells = new Set(['|', '#', unicode.scrollTrack, unicode.scrollThumb]);
|
|
11
|
+
const plain = lines.map((line) => stripAnsi(line));
|
|
12
|
+
const track = plain.slice(1, rows - 3);
|
|
13
|
+
if (plain.some((line) => stringWidth(line) > cols) ||
|
|
14
|
+
!track.every((line) => stringWidth(line) === cols && cells.has(line.slice(-1))))
|
|
15
|
+
return null;
|
|
16
|
+
return { columns: cols, rows, trackTop: 2, trackRows: rows - 4 };
|
|
17
|
+
}
|
|
18
|
+
/** Observe before differ transformation, but acknowledge only after downstream write returns. */
|
|
19
|
+
export function createFrameObserver(options) {
|
|
20
|
+
const { stdout, terminal, scrollbar } = options;
|
|
21
|
+
let summary = null;
|
|
22
|
+
let disposed = false;
|
|
23
|
+
let resizeTimer = null;
|
|
24
|
+
const invalidate = () => { summary = null; scrollbar.invalidate(); };
|
|
25
|
+
const onResize = () => {
|
|
26
|
+
invalidate();
|
|
27
|
+
if (resizeTimer)
|
|
28
|
+
clearTimeout(resizeTimer);
|
|
29
|
+
// Ink suppresses identical frames when a resize burst returns to its starting size.
|
|
30
|
+
// Wait past the 50 ms React size debounce, then request a normal redraw if needed.
|
|
31
|
+
resizeTimer = setTimeout(() => {
|
|
32
|
+
resizeTimer = null;
|
|
33
|
+
if (!disposed && !scrollbar.frameReady)
|
|
34
|
+
scrollbar.requestRedraw?.();
|
|
35
|
+
}, 60);
|
|
36
|
+
resizeTimer.unref?.();
|
|
37
|
+
};
|
|
38
|
+
const match = () => {
|
|
39
|
+
const g = scrollbar.geometry;
|
|
40
|
+
scrollbar.frameReady = !!summary && !!g && summary.columns === terminal.columns &&
|
|
41
|
+
summary.rows === terminal.rows && g.trackCol === summary.columns &&
|
|
42
|
+
g.trackTop === summary.trackTop && g.trackRows === summary.trackRows;
|
|
43
|
+
};
|
|
44
|
+
const observe = (chunk) => {
|
|
45
|
+
summary = summarizeFrame(chunk, terminal.rows, terminal.columns);
|
|
46
|
+
if (!summary) {
|
|
47
|
+
invalidate();
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
match();
|
|
51
|
+
};
|
|
52
|
+
scrollbar.onGeometry = match;
|
|
53
|
+
terminal.on('resize', onResize);
|
|
54
|
+
const bound = new Map();
|
|
55
|
+
const proxy = new Proxy(stdout, {
|
|
56
|
+
get(target, prop) {
|
|
57
|
+
if (prop === 'write')
|
|
58
|
+
return (...args) => {
|
|
59
|
+
const result = Reflect.apply(target.write, target, args);
|
|
60
|
+
if (!disposed)
|
|
61
|
+
observe(args[0]);
|
|
62
|
+
return result;
|
|
63
|
+
};
|
|
64
|
+
const value = Reflect.get(target, prop, target);
|
|
65
|
+
if (typeof value !== 'function')
|
|
66
|
+
return value;
|
|
67
|
+
if (!bound.has(prop))
|
|
68
|
+
bound.set(prop, value.bind(target));
|
|
69
|
+
return bound.get(prop);
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
return { stdout: proxy, invalidate, dispose: () => {
|
|
73
|
+
disposed = true;
|
|
74
|
+
if (resizeTimer)
|
|
75
|
+
clearTimeout(resizeTimer);
|
|
76
|
+
resizeTimer = null;
|
|
77
|
+
terminal.off('resize', onResize);
|
|
78
|
+
scrollbar.onGeometry = undefined;
|
|
79
|
+
invalidate();
|
|
80
|
+
} };
|
|
81
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/** Recognize Ink's erase-lines prefix without accepting arbitrary control writes. */
|
|
2
|
+
export declare function matchInkErasePrefix(chunk: string): {
|
|
3
|
+
prefix: string;
|
|
4
|
+
erased: number;
|
|
5
|
+
} | null;
|
|
6
|
+
/** Parse complete frames, including Ink's prefixless initial frame. */
|
|
7
|
+
export declare function parseInkFrame(chunk: string): string[] | null;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Recognize Ink's erase-lines prefix without accepting arbitrary control writes. */
|
|
2
|
+
export function matchInkErasePrefix(chunk) {
|
|
3
|
+
const match = /^(?:\x1b\[2K(?:\x1b\[1A)?)+\x1b\[G/.exec(chunk);
|
|
4
|
+
return match ? { prefix: match[0], erased: match[0].split('\x1b[2K').length - 1 } : null;
|
|
5
|
+
}
|
|
6
|
+
/** Parse complete frames, including Ink's prefixless initial frame. */
|
|
7
|
+
export function parseInkFrame(chunk) {
|
|
8
|
+
const prefix = matchInkErasePrefix(chunk);
|
|
9
|
+
const body = chunk.slice(prefix?.prefix.length ?? 0);
|
|
10
|
+
if (!body.endsWith('\n'))
|
|
11
|
+
return null;
|
|
12
|
+
// SGR and OSC-8 may decorate text; cursor movement is never frame content.
|
|
13
|
+
const controls = body.replace(/\x1b\[[0-9;:]*m/g, '')
|
|
14
|
+
.replace(/\x1b\]8;[^\x07\x1b]*(?:\x07|\x1b\\)/g, '');
|
|
15
|
+
if (/[\x00-\x08\x0b-\x1f\x7f]/.test(controls))
|
|
16
|
+
return null;
|
|
17
|
+
return body.slice(0, -1).split('\n');
|
|
18
|
+
}
|