@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
|
@@ -80,7 +80,7 @@ export function toolEntryGlyph(name, caps) {
|
|
|
80
80
|
return toolGlyph(pickGlyphs(caps), name);
|
|
81
81
|
}
|
|
82
82
|
function ToolCardImpl(props) {
|
|
83
|
-
const { name, argsRaw, args, status, preview, patch, durationMs, isError, expanded, reducedMotion,
|
|
83
|
+
const { name, argsRaw, args, status, preview, patch, durationMs, isError, expanded, reducedMotion, live, lastOutputAt, nowSec, theme, caps, } = props;
|
|
84
84
|
const glyphs = pickGlyphs(caps);
|
|
85
85
|
const color = statusColor(status, theme);
|
|
86
86
|
const previewText = preview ?? '';
|
|
@@ -88,12 +88,7 @@ function ToolCardImpl(props) {
|
|
|
88
88
|
// is immutable once written, and this used to reallocate up to 200 strings per
|
|
89
89
|
// card per frame (R3).
|
|
90
90
|
const previewLines = previewText.length > 0 ? splitLinesCached(previewText) : EMPTY_LINES;
|
|
91
|
-
|
|
92
|
-
// The inline clamp only lowers the ceiling — it never changes which end is
|
|
93
|
-
// kept, because `Ctrl+O` on a 5 000-line bash result is precisely how an
|
|
94
|
-
// inline session reaches `ink.js:121` (I-L5-1) and the fix must not also
|
|
95
|
-
// reshuffle what a full-screen user has been reading for four releases.
|
|
96
|
-
const ceiling = Math.min(expanded ? previewLines.length : COLLAPSED_LINES, liveClampRows ?? Number.POSITIVE_INFINITY);
|
|
91
|
+
const ceiling = expanded ? previewLines.length : COLLAPSED_LINES;
|
|
97
92
|
const visibleLines = ceiling >= previewLines.length ? previewLines : previewLines.slice(0, ceiling);
|
|
98
93
|
const hidden = previewLines.length - visibleLines.length;
|
|
99
94
|
const settled = status === 'done' || status === 'error';
|
|
@@ -110,11 +105,7 @@ function ToolCardImpl(props) {
|
|
|
110
105
|
// NOW" (D-25). Head-first while live would freeze on the first eight lines a
|
|
111
106
|
// build ever printed and then never change again.
|
|
112
107
|
const liveRows = live ?? EMPTY_LINES;
|
|
113
|
-
const liveCeiling =
|
|
114
|
-
// Inline only: `liveClampRows` already subtracts one row per live entry to
|
|
115
|
-
// pay for a marker (`Transcript.tsx:534-538`), which is what this card's
|
|
116
|
-
// unconditional footer spends (AC-36 / P2-7).
|
|
117
|
-
liveClampRows ?? Number.POSITIVE_INFINITY);
|
|
108
|
+
const liveCeiling = COLLAPSED_LINES;
|
|
118
109
|
const visibleLive = liveRows.length > liveCeiling ? liveRows.slice(-Math.max(1, liveCeiling)) : liveRows;
|
|
119
110
|
const showLive = !settled && visibleLive.length > 0;
|
|
120
111
|
// THE OUTER GUARD WIDENS WITH THE BRANCH (P2-2). It was `previewLines.length >
|
|
@@ -153,7 +144,7 @@ function ToolCardImpl(props) {
|
|
|
153
144
|
// "constant row count" the exactness its arithmetic claims. A
|
|
154
145
|
// space is also what the terminal itself shows for a blank line.
|
|
155
146
|
// eslint-disable-next-line react/no-array-index-key
|
|
156
|
-
_jsx(Text, { wrap: "truncate", color: theme.muted, children: row.length > 0 ? row : ' ' }, i))), _jsx(Text, { color: theme.muted, children: liveFooter })] })) : showPatch ? (_jsx(DiffView, { patch: patch, expanded: expanded,
|
|
147
|
+
_jsx(Text, { wrap: "truncate", color: theme.muted, children: row.length > 0 ? row : ' ' }, i))), _jsx(Text, { color: theme.muted, children: liveFooter })] })) : showPatch ? (_jsx(DiffView, { patch: patch, expanded: expanded, theme: theme })) : (_jsxs(_Fragment, { children: [_jsx(ToolPreview, { name: name, lines: visibleLines, theme: theme, isError: isError }), !expanded && hidden > 0 && (_jsxs(Text, { color: theme.muted, children: ["+", hidden, " ", hidden === 1 ? 'line' : 'lines', " (Ctrl+O)"] })), expanded && hidden > 0 && (_jsxs(Text, { color: theme.muted, children: ["+", hidden, " ", hidden === 1 ? 'line' : 'lines', " ", glyphs.midDot, " shown in full when this entry finishes"] })), expanded && previewLines.length > COLLAPSED_LINES && (_jsx(Text, { color: theme.muted, children: "(Ctrl+O to collapse)" }))] })) }))] }));
|
|
157
148
|
}
|
|
158
149
|
/**
|
|
159
150
|
* `React.memo` with the DEFAULT comparator (tui-render-performance L2 / R3).
|
|
@@ -25,6 +25,12 @@
|
|
|
25
25
|
* trailing `CSI K` removes only the residue a longer previous line left, and
|
|
26
26
|
* the trailing SGR reset before it stops a BCE terminal painting that residue
|
|
27
27
|
* in whatever background colour the line ended inside.
|
|
28
|
+
* `CSI K` IS EMITTED ONLY WHEN THE LINE IS NARROWER THAN THE TERMINAL. A row
|
|
29
|
+
* that already fills every column (the unified scrollbar paints the last one)
|
|
30
|
+
* has no residue to clear, and on terminals that keep the cursor ON the last
|
|
31
|
+
* column after writing it (a pending-wrap flag rather than a virtual column
|
|
32
|
+
* N+1) the EL would erase the cell that was just written. `cols` unknown keeps
|
|
33
|
+
* the old always-EL behaviour (`paintLine`).
|
|
28
34
|
* - I-5 · Every emitted batch ends parked on row `H+1`. Ink's own
|
|
29
35
|
* `previousLineCount` is computed from the string IT produced and is
|
|
30
36
|
* unaffected by what we actually wrote, so a later pass-through's
|
|
@@ -56,6 +62,13 @@ export interface FrameDifferOptions {
|
|
|
56
62
|
* on resize. `undefined` ⇒ stand down and pass through.
|
|
57
63
|
*/
|
|
58
64
|
rows: () => number | undefined;
|
|
65
|
+
/**
|
|
66
|
+
* Live terminal width, read on every paint and never cached (like `rows`).
|
|
67
|
+
* Absent or non-finite => every changed row ends in `CSI K` (the original
|
|
68
|
+
* behaviour); finite => rows whose display width is >= `cols` skip the EL so
|
|
69
|
+
* the last column (the scrollbar) is never erased (I-4).
|
|
70
|
+
*/
|
|
71
|
+
cols?: () => number | undefined;
|
|
59
72
|
/** Raised once, on the 0→1 edge of `fallbacks` (§5.6). */
|
|
60
73
|
onFirstFallback?: () => void;
|
|
61
74
|
/**
|
package/dist/ui/frame-differ.js
CHANGED
|
@@ -25,6 +25,12 @@
|
|
|
25
25
|
* trailing `CSI K` removes only the residue a longer previous line left, and
|
|
26
26
|
* the trailing SGR reset before it stops a BCE terminal painting that residue
|
|
27
27
|
* in whatever background colour the line ended inside.
|
|
28
|
+
* `CSI K` IS EMITTED ONLY WHEN THE LINE IS NARROWER THAN THE TERMINAL. A row
|
|
29
|
+
* that already fills every column (the unified scrollbar paints the last one)
|
|
30
|
+
* has no residue to clear, and on terminals that keep the cursor ON the last
|
|
31
|
+
* column after writing it (a pending-wrap flag rather than a virtual column
|
|
32
|
+
* N+1) the EL would erase the cell that was just written. `cols` unknown keeps
|
|
33
|
+
* the old always-EL behaviour (`paintLine`).
|
|
28
34
|
* - I-5 · Every emitted batch ends parked on row `H+1`. Ink's own
|
|
29
35
|
* `previousLineCount` is computed from the string IT produced and is
|
|
30
36
|
* unaffected by what we actually wrote, so a later pass-through's
|
|
@@ -37,6 +43,8 @@
|
|
|
37
43
|
* the guard being absent is a permanently misaligned screen while the failure
|
|
38
44
|
* mode of it firing spuriously is one extra full repaint.
|
|
39
45
|
*/
|
|
46
|
+
import stringWidth from 'string-width';
|
|
47
|
+
import stripAnsi from 'strip-ansi';
|
|
40
48
|
import { MIN_FULLSCREEN_ROWS } from './layout/frame.js';
|
|
41
49
|
import { matchInkErasePrefix } from './frame-parser.js';
|
|
42
50
|
// ---------------------------------------------------------------------------
|
|
@@ -59,6 +67,23 @@ const SYNC_END = `${CSI}?2026l`;
|
|
|
59
67
|
function cursorTo(row) {
|
|
60
68
|
return `${CSI}${row};1H`;
|
|
61
69
|
}
|
|
70
|
+
/**
|
|
71
|
+
* One addressed row: `CUP · SGR0 · line · SGR0 [· CSI K]` (I-4).
|
|
72
|
+
*
|
|
73
|
+
* The trailing `CSI K` clears residue a longer previous line left behind, so it is
|
|
74
|
+
* only meaningful when the line is narrower than the terminal. A line that already
|
|
75
|
+
* fills `cols` columns ends with the cursor in the pending-wrap state, where some
|
|
76
|
+
* terminals run EL from the LAST column and erase the cell just written; those
|
|
77
|
+
* rows get no EL. Width is measured exactly as Ink measures it (`string-width` over
|
|
78
|
+
* the ANSI-stripped text) and only for rows that changed, so it costs at most one
|
|
79
|
+
* measurement per changed viewport row. `cols` unknown => the original always-EL.
|
|
80
|
+
*/
|
|
81
|
+
function paintLine(row, line, cols) {
|
|
82
|
+
const head = cursorTo(row) + SGR_RESET + line + SGR_RESET;
|
|
83
|
+
if (cols !== undefined && stringWidth(stripAnsi(line)) >= cols)
|
|
84
|
+
return head;
|
|
85
|
+
return head + ERASE_TO_EOL;
|
|
86
|
+
}
|
|
62
87
|
/**
|
|
63
88
|
* Rebuild `ansiEscapes.eraseLines(count)` byte for byte.
|
|
64
89
|
*
|
|
@@ -211,13 +236,18 @@ export function createFrameDiffer(options) {
|
|
|
211
236
|
cursorTo(lines.length + 1);
|
|
212
237
|
return emit(wrapSync(payload), lines, raw, lines.length, true);
|
|
213
238
|
};
|
|
239
|
+
/** Terminal width for this paint, or `undefined` when unknown (I-4: always EL). */
|
|
240
|
+
const liveCols = () => {
|
|
241
|
+
const cols = options.cols?.();
|
|
242
|
+
return cols !== undefined && Number.isFinite(cols) ? cols : undefined;
|
|
243
|
+
};
|
|
214
244
|
const diffRepaint = (lines, raw, previous) => {
|
|
215
245
|
const out = [];
|
|
246
|
+
const cols = liveCols();
|
|
216
247
|
for (let i = 0; i < lines.length; i += 1) {
|
|
217
248
|
// I-4: paint, THEN clear the tail. Never `ERASE_LINE` first.
|
|
218
|
-
if (lines[i] !== previous[i])
|
|
219
|
-
out.push(
|
|
220
|
-
}
|
|
249
|
+
if (lines[i] !== previous[i])
|
|
250
|
+
out.push(paintLine(i + 1, lines[i], cols));
|
|
221
251
|
}
|
|
222
252
|
// DEFENSIVE ONLY, not a live path (P2-4). `log-update.js:13-15` returns early
|
|
223
253
|
// on an identical frame, so a chunk that reaches here differs by at least one
|
|
@@ -251,10 +281,10 @@ export function createFrameDiffer(options) {
|
|
|
251
281
|
return '';
|
|
252
282
|
const painted = applyDecorate(rawPrev);
|
|
253
283
|
const out = [];
|
|
284
|
+
const cols = liveCols();
|
|
254
285
|
for (let i = 0; i < painted.length; i += 1) {
|
|
255
|
-
if (painted[i] !== prev[i])
|
|
256
|
-
out.push(
|
|
257
|
-
}
|
|
286
|
+
if (painted[i] !== prev[i])
|
|
287
|
+
out.push(paintLine(i + 1, painted[i], cols));
|
|
258
288
|
}
|
|
259
289
|
prev = painted;
|
|
260
290
|
if (out.length === 0)
|
package/dist/ui/glyphs.d.ts
CHANGED
|
@@ -117,6 +117,15 @@ export interface Glyphs {
|
|
|
117
117
|
* scan fails the build if either spelling appears anywhere else.
|
|
118
118
|
*/
|
|
119
119
|
retry: string;
|
|
120
|
+
/**
|
|
121
|
+
* A message queued behind a running turn (tui-shift-enter-copy-queue 5.4).
|
|
122
|
+
* The rail marker on the `Queue: ...` row.
|
|
123
|
+
*
|
|
124
|
+
* HERE rather than in `QueuedEntry`, for the reason this file's header
|
|
125
|
+
* gives: a literal in a component bypasses `pickGlyphs` entirely and
|
|
126
|
+
* shows mojibake on a legacy console whatever the capability probe said.
|
|
127
|
+
*/
|
|
128
|
+
queued: string;
|
|
120
129
|
/**
|
|
121
130
|
* Context compaction (context-auto-compaction §6.3). The rail marker on the
|
|
122
131
|
* compaction card.
|
package/dist/ui/glyphs.js
CHANGED
|
@@ -66,6 +66,7 @@ const UNICODE_GLYPHS = {
|
|
|
66
66
|
todoDone: '✔',
|
|
67
67
|
retry: '↻',
|
|
68
68
|
compaction: '⤓',
|
|
69
|
+
queued: '◷',
|
|
69
70
|
tool: {
|
|
70
71
|
read_file: '▤',
|
|
71
72
|
write_file: '✎',
|
|
@@ -129,6 +130,7 @@ const ASCII_GLYPHS = {
|
|
|
129
130
|
todoDone: '[x]',
|
|
130
131
|
retry: '[r]',
|
|
131
132
|
compaction: '[c]',
|
|
133
|
+
queued: '*',
|
|
132
134
|
tool: {
|
|
133
135
|
read_file: '=',
|
|
134
136
|
write_file: '+',
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const render: (node: import("react").ReactNode, options?: NodeJS.WriteStream | import("ink").RenderOptions) => import("ink").Instance;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
/** Ink 5 caches CI detection at import time and otherwise paints only on exit. */
|
|
3
|
+
async function loadInk() {
|
|
4
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY)
|
|
5
|
+
return import('ink');
|
|
6
|
+
const originalCi = process.env.CI;
|
|
7
|
+
try {
|
|
8
|
+
// A real interactive terminal always needs live frames and resize events.
|
|
9
|
+
// `0` also overrides CI_* / CONTINUOUS_INTEGRATION in Ink's detector.
|
|
10
|
+
process.env.CI = '0';
|
|
11
|
+
return await import('ink');
|
|
12
|
+
}
|
|
13
|
+
finally {
|
|
14
|
+
// Commands, subprocesses and the updater must see the original environment.
|
|
15
|
+
if (originalCi === undefined)
|
|
16
|
+
delete process.env.CI;
|
|
17
|
+
else
|
|
18
|
+
process.env.CI = originalCi;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export const { render } = await loadInk();
|
|
@@ -1,22 +1,13 @@
|
|
|
1
1
|
/** Stable fullscreen slots preserve the editor during overlays and small resizes. */
|
|
2
2
|
import React from 'react';
|
|
3
|
-
import { type RenderMode } from './frame.js';
|
|
4
3
|
export interface AppShellProps {
|
|
5
|
-
mode: RenderMode;
|
|
6
4
|
rows: number;
|
|
7
5
|
cols: number;
|
|
8
6
|
header: React.ReactNode;
|
|
9
7
|
viewport: React.ReactNode;
|
|
10
|
-
/** Legacy inline footer; fullscreen assembles these slots inside its viewport. */
|
|
11
|
-
team?: React.ReactNode;
|
|
12
|
-
/** Kept source-compatible; fullscreen rail is owned by ScrollViewport. */
|
|
13
|
-
rail?: React.ReactNode;
|
|
14
|
-
/** Inline-only plan strip. */
|
|
15
|
-
strip?: React.ReactNode;
|
|
16
8
|
toast: React.ReactNode;
|
|
17
|
-
composer: React.ReactNode;
|
|
18
9
|
status: React.ReactNode;
|
|
19
10
|
inactive?: boolean;
|
|
20
11
|
placeholder?: React.ReactNode;
|
|
21
12
|
}
|
|
22
|
-
export declare function AppShell({
|
|
13
|
+
export declare function AppShell({ rows, cols, header, viewport, toast, status, inactive, placeholder, }: AppShellProps): React.ReactElement;
|
|
@@ -1,9 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box } from 'ink';
|
|
3
3
|
import { frameHeight } from './frame.js';
|
|
4
|
-
export function AppShell({
|
|
5
|
-
if (mode === 'inline') {
|
|
6
|
-
return (_jsxs(Box, { flexDirection: "column", children: [header, viewport, strip, team, toast, composer, status] }));
|
|
7
|
-
}
|
|
4
|
+
export function AppShell({ rows, cols, header, viewport, toast, status, inactive = false, placeholder, }) {
|
|
8
5
|
return (_jsxs(Box, { flexDirection: "column", height: frameHeight(rows), width: cols, overflow: "hidden", children: [_jsxs(Box, { display: inactive ? 'none' : 'flex', flexDirection: "column", height: frameHeight(rows), width: cols, overflow: "hidden", children: [_jsx(Box, { height: 1, flexShrink: 0, overflow: "hidden", children: header }), _jsx(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, overflow: "hidden", children: viewport }), _jsx(Box, { height: 1, flexShrink: 0, overflow: "hidden", children: toast }), _jsx(Box, { height: 1, flexShrink: 0, overflow: "hidden", children: status })] }), _jsx(Box, { display: inactive ? 'flex' : 'none', height: frameHeight(rows), overflow: "hidden", children: placeholder })] }));
|
|
9
6
|
}
|
|
@@ -1,27 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OverlayFrame (spec §4.4) — the shared shell for all four overlays.
|
|
3
|
-
*
|
|
4
|
-
* TWO CONTENT MODES, and the distinction is load-bearing:
|
|
5
|
-
*
|
|
6
|
-
* - mode A (`rows`): the caller hands over an array of elements that are each
|
|
7
|
-
* exactly one row and `wrap="truncate"`. The frame slices them and OWNS the
|
|
8
|
-
* scroll keys. Used by Help and Settings.
|
|
9
|
-
*
|
|
10
|
-
* - mode B (`children`): the content manages itself. The frame draws the title,
|
|
11
|
-
* the border and the footer, and touches NOTHING else — no slicing, no
|
|
12
|
-
* arrow-key handling, no position indicator.
|
|
13
|
-
*
|
|
14
|
-
* Mode B exists because of `ink-select-input`: it already windows itself via
|
|
15
|
-
* `limit` and registers its own `useInput` with `isFocused` defaulting to true.
|
|
16
|
-
* Ink dispatches a key to EVERY mounted input handler, so a frame that also
|
|
17
|
-
* claimed the arrow keys would move the model picker's selection twice per press
|
|
18
|
-
* — breaking a feature that already works. Slicing it by element would be worse
|
|
19
|
-
* still, since it is a single component rather than a list of rows.
|
|
20
|
-
*
|
|
21
|
-
* `maxRows` may be `Infinity` (the inline render path, which has no fixed frame
|
|
22
|
-
* and therefore no height to fit): the frame then renders everything and shows
|
|
23
|
-
* no position indicator.
|
|
24
|
-
*/
|
|
25
1
|
import React from 'react';
|
|
26
2
|
import type { Theme } from '../theme.js';
|
|
27
3
|
import type { TermCapabilities } from '../capabilities.js';
|
|
@@ -29,7 +5,7 @@ interface OverlayFrameBase {
|
|
|
29
5
|
title: string;
|
|
30
6
|
/** Footer text. Written by the CALLER so each overlay names its own keys. */
|
|
31
7
|
hint: string;
|
|
32
|
-
/** Viewport rows the overlay may occupy
|
|
8
|
+
/** Viewport rows the overlay may occupy. */
|
|
33
9
|
maxRows: number;
|
|
34
10
|
cols?: number;
|
|
35
11
|
theme: Theme;
|
|
@@ -1,28 +1,4 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
/**
|
|
3
|
-
* OverlayFrame (spec §4.4) — the shared shell for all four overlays.
|
|
4
|
-
*
|
|
5
|
-
* TWO CONTENT MODES, and the distinction is load-bearing:
|
|
6
|
-
*
|
|
7
|
-
* - mode A (`rows`): the caller hands over an array of elements that are each
|
|
8
|
-
* exactly one row and `wrap="truncate"`. The frame slices them and OWNS the
|
|
9
|
-
* scroll keys. Used by Help and Settings.
|
|
10
|
-
*
|
|
11
|
-
* - mode B (`children`): the content manages itself. The frame draws the title,
|
|
12
|
-
* the border and the footer, and touches NOTHING else — no slicing, no
|
|
13
|
-
* arrow-key handling, no position indicator.
|
|
14
|
-
*
|
|
15
|
-
* Mode B exists because of `ink-select-input`: it already windows itself via
|
|
16
|
-
* `limit` and registers its own `useInput` with `isFocused` defaulting to true.
|
|
17
|
-
* Ink dispatches a key to EVERY mounted input handler, so a frame that also
|
|
18
|
-
* claimed the arrow keys would move the model picker's selection twice per press
|
|
19
|
-
* — breaking a feature that already works. Slicing it by element would be worse
|
|
20
|
-
* still, since it is a single component rather than a list of rows.
|
|
21
|
-
*
|
|
22
|
-
* `maxRows` may be `Infinity` (the inline render path, which has no fixed frame
|
|
23
|
-
* and therefore no height to fit): the frame then renders everything and shows
|
|
24
|
-
* no position indicator.
|
|
25
|
-
*/
|
|
26
2
|
import { useEffect } from 'react';
|
|
27
3
|
import { Box, Text } from 'ink';
|
|
28
4
|
import { pickGlyphs } from '../glyphs.js';
|
|
@@ -27,6 +27,9 @@ export interface ScrollViewportProps {
|
|
|
27
27
|
scrollbar?: ScrollbarBridge;
|
|
28
28
|
composerRef?: React.RefObject<DOMElement>;
|
|
29
29
|
onComposerVisibilityChange?: (visible: boolean) => void;
|
|
30
|
+
/** The run status row above the input box (tui-scrollbar-edge-and-run-row). */
|
|
31
|
+
activityRef?: React.RefObject<DOMElement>;
|
|
32
|
+
onActivityVisibilityChange?: (visible: boolean) => void;
|
|
30
33
|
theme: Theme;
|
|
31
34
|
caps: TermCapabilities;
|
|
32
35
|
children: React.ReactNode;
|
|
@@ -8,17 +8,23 @@ import { reduceFollow, shouldArmResume } from './follow-state.js';
|
|
|
8
8
|
import { thumbRange } from './scroll-indicator.js';
|
|
9
9
|
import { ScrollIndicator } from './ScrollIndicator.js';
|
|
10
10
|
import { buildDocumentLayout } from './document-layout.js';
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Whether `element` lies fully inside `clip`. A missing element (not mounted) or
|
|
13
|
+
* clip reports `true`, so a caller that consumes the answer must first know the
|
|
14
|
+
* element is actually mounted. Rows are one line tall, so "partially visible"
|
|
15
|
+
* cannot occur for the run status row.
|
|
16
|
+
*/
|
|
17
|
+
function elementVisible(element, clip) {
|
|
18
|
+
if (!element || !clip)
|
|
13
19
|
return true;
|
|
14
20
|
let top = 0;
|
|
15
|
-
let current =
|
|
21
|
+
let current = element;
|
|
16
22
|
while (current && current !== clip) {
|
|
17
23
|
top += current.yogaNode?.getComputedTop() ?? 0;
|
|
18
24
|
current = current.parentNode;
|
|
19
25
|
}
|
|
20
26
|
return current === clip && top >= 0 &&
|
|
21
|
-
top + measureElement(
|
|
27
|
+
top + measureElement(element).height <= measureElement(clip).height;
|
|
22
28
|
}
|
|
23
29
|
/** One offset for messages, padding and editor; hidden trees keep their local state. */
|
|
24
30
|
export function ScrollViewport(props) {
|
|
@@ -45,6 +51,7 @@ export function ScrollViewport(props) {
|
|
|
45
51
|
const lastPin = useRef(props.pinToBottomNonce);
|
|
46
52
|
const lastReset = useRef(props.resetKey);
|
|
47
53
|
const lastVisible = useRef();
|
|
54
|
+
const lastActivityVisible = useRef();
|
|
48
55
|
const previousActive = useRef(active);
|
|
49
56
|
const geometryRevision = useRef(0);
|
|
50
57
|
const geometryBounds = useRef('');
|
|
@@ -186,11 +193,16 @@ export function ScrollViewport(props) {
|
|
|
186
193
|
thumb: thumbRange(viewport, content, next, true), revision: geometryRevision.current };
|
|
187
194
|
scrollbar.onGeometry?.();
|
|
188
195
|
}
|
|
189
|
-
const visible =
|
|
196
|
+
const visible = elementVisible(props.composerRef?.current ?? null, clipRef.current);
|
|
190
197
|
if (lastVisible.current !== visible) {
|
|
191
198
|
lastVisible.current = visible;
|
|
192
199
|
props.onComposerVisibilityChange?.(visible);
|
|
193
200
|
}
|
|
201
|
+
const activityVisible = elementVisible(props.activityRef?.current ?? null, clipRef.current);
|
|
202
|
+
if (lastActivityVisible.current !== activityVisible) {
|
|
203
|
+
lastActivityVisible.current = activityVisible;
|
|
204
|
+
props.onActivityVisibilityChange?.(activityVisible);
|
|
205
|
+
}
|
|
194
206
|
});
|
|
195
207
|
const shiftUp = Math.max(0, metrics.content - metrics.viewport - offset);
|
|
196
208
|
useEffect(() => { props.onScrolledLinesChange?.(offset); }, [offset, props.onScrolledLinesChange]);
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
* Two pure functions with zero React dependency so the load-bearing invariant
|
|
5
5
|
* I-1 (`frameHeight(r) < r`) can be pinned by a unit test rather than by hope.
|
|
6
6
|
*/
|
|
7
|
-
export type RenderMode = 'fullscreen' | 'inline';
|
|
8
7
|
/** Below this many rows a full-screen frame has no usable viewport left. */
|
|
9
8
|
export declare const MIN_FULLSCREEN_ROWS = 12;
|
|
10
9
|
/** Below this many columns the chrome (composer + status bar) cannot line-wrap sanely. */
|
|
@@ -35,24 +34,3 @@ export declare const FALLBACK_COLS = 80;
|
|
|
35
34
|
* terminal is strictly better than a per-frame full-screen repaint.
|
|
36
35
|
*/
|
|
37
36
|
export declare function frameHeight(rows: number | undefined): number;
|
|
38
|
-
interface StdoutLike {
|
|
39
|
-
isTTY?: boolean;
|
|
40
|
-
rows?: number;
|
|
41
|
-
columns?: number;
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* Decide the render mode once, at startup (never re-decided at run time —
|
|
45
|
-
* entering/leaving the alt-screen mid-session is a state machine we do not want).
|
|
46
|
-
*
|
|
47
|
-
* Order is semantics, not style:
|
|
48
|
-
* 1. an explicit opt-out always wins (users can always get v0.2.0 back);
|
|
49
|
-
* 2. `isTTY` is the one gate `--fullscreen` cannot override — writing ANSI into
|
|
50
|
-
* a pipe or a redirected file is a contract violation, whereas a heuristic
|
|
51
|
-
* misfire only costs polish;
|
|
52
|
-
* 3. the explicit force comes next so it can beat the heuristics below;
|
|
53
|
-
* 4. heuristics (TERM/CI/size), all overridable by `--fullscreen`.
|
|
54
|
-
*/
|
|
55
|
-
export declare function decideRenderMode(flags: {
|
|
56
|
-
fullscreen?: boolean;
|
|
57
|
-
}, env: Record<string, string | undefined>, stdout: StdoutLike | undefined): RenderMode;
|
|
58
|
-
export {};
|
package/dist/ui/layout/frame.js
CHANGED
|
@@ -37,39 +37,3 @@ export function frameHeight(rows) {
|
|
|
37
37
|
const r = rows && rows > 0 ? rows : FALLBACK_ROWS;
|
|
38
38
|
return Math.max(0, r - 1);
|
|
39
39
|
}
|
|
40
|
-
/** `CI=` / `CI=0` / `CI=false` are not CI; anything else non-empty is. */
|
|
41
|
-
function isTruthyEnv(value) {
|
|
42
|
-
if (value === undefined)
|
|
43
|
-
return false;
|
|
44
|
-
const v = value.trim().toLowerCase();
|
|
45
|
-
return v.length > 0 && v !== '0' && v !== 'false';
|
|
46
|
-
}
|
|
47
|
-
/**
|
|
48
|
-
* Decide the render mode once, at startup (never re-decided at run time —
|
|
49
|
-
* entering/leaving the alt-screen mid-session is a state machine we do not want).
|
|
50
|
-
*
|
|
51
|
-
* Order is semantics, not style:
|
|
52
|
-
* 1. an explicit opt-out always wins (users can always get v0.2.0 back);
|
|
53
|
-
* 2. `isTTY` is the one gate `--fullscreen` cannot override — writing ANSI into
|
|
54
|
-
* a pipe or a redirected file is a contract violation, whereas a heuristic
|
|
55
|
-
* misfire only costs polish;
|
|
56
|
-
* 3. the explicit force comes next so it can beat the heuristics below;
|
|
57
|
-
* 4. heuristics (TERM/CI/size), all overridable by `--fullscreen`.
|
|
58
|
-
*/
|
|
59
|
-
export function decideRenderMode(flags, env, stdout) {
|
|
60
|
-
if (flags.fullscreen === false)
|
|
61
|
-
return 'inline';
|
|
62
|
-
if (!stdout?.isTTY)
|
|
63
|
-
return 'inline';
|
|
64
|
-
if (flags.fullscreen === true)
|
|
65
|
-
return 'fullscreen';
|
|
66
|
-
if (env.TERM === 'dumb')
|
|
67
|
-
return 'inline';
|
|
68
|
-
if (isTruthyEnv(env.CI))
|
|
69
|
-
return 'inline';
|
|
70
|
-
if ((stdout.rows ?? 0) < MIN_FULLSCREEN_ROWS)
|
|
71
|
-
return 'inline';
|
|
72
|
-
if ((stdout.columns ?? 0) < MIN_FULLSCREEN_COLS)
|
|
73
|
-
return 'inline';
|
|
74
|
-
return 'fullscreen';
|
|
75
|
-
}
|
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import type { RenderMode } from './frame.js';
|
|
2
1
|
export interface TodoRailLayoutInput {
|
|
3
|
-
mode: RenderMode;
|
|
4
2
|
cols: number;
|
|
5
3
|
panelEnabled: boolean;
|
|
6
4
|
overlayOpen: boolean;
|
|
@@ -12,7 +10,7 @@ export interface TodoRailLayoutInput {
|
|
|
12
10
|
}
|
|
13
11
|
export interface TodoRailLayout {
|
|
14
12
|
visible: boolean;
|
|
15
|
-
reason: 'visible' | '
|
|
13
|
+
reason: 'visible' | 'disabled' | 'overlay' | 'empty' | 'narrow' | 'short';
|
|
16
14
|
width: number;
|
|
17
15
|
rows: number;
|
|
18
16
|
contentCols: number;
|
|
@@ -11,15 +11,14 @@ export function buildTodoRailLayout(input) {
|
|
|
11
11
|
const popupMaxHeight = Math.max(0, budget - teamRows - TODO_LIMITS.panelMinRows
|
|
12
12
|
- nonNegativeInt(input.composerBaseRows ?? 0));
|
|
13
13
|
const popupRows = popupMaxHeight < 3 ? 0 : Math.min(nonNegativeInt(input.popupRows), popupMaxHeight);
|
|
14
|
-
const rows =
|
|
14
|
+
const rows = budget;
|
|
15
15
|
const width = todoRailWidth(cols);
|
|
16
|
-
const reason = input.
|
|
17
|
-
:
|
|
18
|
-
: input.
|
|
19
|
-
:
|
|
20
|
-
:
|
|
21
|
-
:
|
|
22
|
-
: 'visible';
|
|
16
|
+
const reason = !input.panelEnabled ? 'disabled'
|
|
17
|
+
: input.overlayOpen ? 'overlay'
|
|
18
|
+
: nonNegativeInt(input.itemCount) === 0 ? 'empty'
|
|
19
|
+
: width === 0 ? 'narrow'
|
|
20
|
+
: rows < TODO_LIMITS.panelMinRows ? 'short'
|
|
21
|
+
: 'visible';
|
|
23
22
|
const visible = reason === 'visible';
|
|
24
23
|
return { visible, reason, width, rows, contentCols: cols - (visible ? width : 0), popupMaxHeight };
|
|
25
24
|
}
|
|
@@ -21,7 +21,7 @@ export interface ViewportGeometry {
|
|
|
21
21
|
}
|
|
22
22
|
/**
|
|
23
23
|
* The default is the UNMEASURED geometry, and consumers must treat it as such:
|
|
24
|
-
* a `TranscriptList` rendered outside a `ScrollViewport` (
|
|
24
|
+
* a `TranscriptList` rendered outside a `ScrollViewport` (including
|
|
25
25
|
* test that mounts it bare) sees `viewportRows: 0`, which `selectWindow` reads
|
|
26
26
|
* as "first frame" and answers by pinning the live tail.
|
|
27
27
|
*/
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
import { createContext, useContext } from 'react';
|
|
14
14
|
/**
|
|
15
15
|
* The default is the UNMEASURED geometry, and consumers must treat it as such:
|
|
16
|
-
* a `TranscriptList` rendered outside a `ScrollViewport` (
|
|
16
|
+
* a `TranscriptList` rendered outside a `ScrollViewport` (including
|
|
17
17
|
* test that mounts it bare) sees `viewportRows: 0`, which `selectWindow` reads
|
|
18
18
|
* as "first frame" and answers by pinning the live tail.
|
|
19
19
|
*/
|
package/dist/ui/live-clamp.d.ts
CHANGED
|
@@ -1,32 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Live-region clamp for INLINE render mode (tui-render-performance L5 / R6).
|
|
3
|
-
*
|
|
4
|
-
* Inline mode has no fixed frame, so nothing kept its live (non-`<Static>`)
|
|
5
|
-
* region shorter than the terminal. When one live entry grew taller than
|
|
6
|
-
* `stdout.rows`, Ink took the branch at `ink.js:118-123`:
|
|
7
|
-
*
|
|
8
|
-
* if (outputHeight >= this.options.stdout.rows) {
|
|
9
|
-
* stdout.write(clearTerminal + this.fullStaticOutput + output);
|
|
10
|
-
* }
|
|
11
|
-
*
|
|
12
|
-
* `fullStaticOutput` accumulates EVERY `<Static>` frame of the whole session and
|
|
13
|
-
* is never trimmed, so once that branch is taken it is taken on every subsequent
|
|
14
|
-
* frame — bypassing both the `output !== lastOutput` dedupe and `throttledLog` —
|
|
15
|
-
* and the terminal receives the entire session history thirty times a second.
|
|
16
|
-
* That is a genuine hard freeze, and it is reachable today by any inline user
|
|
17
|
-
* (`--no-fullscreen`, `TERM=dumb`, CI, a short or narrow terminal) whose model
|
|
18
|
-
* writes a long answer.
|
|
19
|
-
*
|
|
20
|
-
* I-L5-1 — keeping the live region strictly below `stdout.rows` makes that
|
|
21
|
-
* branch UNREACHABLE, which is the inline analogue of `frame.ts`'s
|
|
22
|
-
* `frameHeight(r) < r` and deserves the same standing. We cannot bound Ink's
|
|
23
|
-
* `fullStaticOutput` growth (it is private and append-only); preventing the
|
|
24
|
-
* branch that WRITES it is the entire mitigation, and nothing about the memory
|
|
25
|
-
* it holds is fixed by this module.
|
|
26
|
-
*
|
|
27
|
-
* Nothing is lost, only deferred: when the entry settles it moves into
|
|
28
|
-
* `<Static>` and is printed in full.
|
|
29
|
-
*/
|
|
1
|
+
/** Keep the newest thinking lines visible while an assistant entry streams. */
|
|
30
2
|
export interface LiveClamp {
|
|
31
3
|
text: string;
|
|
32
4
|
/** Rows dropped off the FRONT. `0` means the text was returned untouched. */
|
package/dist/ui/live-clamp.js
CHANGED
|
@@ -1,32 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Live-region clamp for INLINE render mode (tui-render-performance L5 / R6).
|
|
3
|
-
*
|
|
4
|
-
* Inline mode has no fixed frame, so nothing kept its live (non-`<Static>`)
|
|
5
|
-
* region shorter than the terminal. When one live entry grew taller than
|
|
6
|
-
* `stdout.rows`, Ink took the branch at `ink.js:118-123`:
|
|
7
|
-
*
|
|
8
|
-
* if (outputHeight >= this.options.stdout.rows) {
|
|
9
|
-
* stdout.write(clearTerminal + this.fullStaticOutput + output);
|
|
10
|
-
* }
|
|
11
|
-
*
|
|
12
|
-
* `fullStaticOutput` accumulates EVERY `<Static>` frame of the whole session and
|
|
13
|
-
* is never trimmed, so once that branch is taken it is taken on every subsequent
|
|
14
|
-
* frame — bypassing both the `output !== lastOutput` dedupe and `throttledLog` —
|
|
15
|
-
* and the terminal receives the entire session history thirty times a second.
|
|
16
|
-
* That is a genuine hard freeze, and it is reachable today by any inline user
|
|
17
|
-
* (`--no-fullscreen`, `TERM=dumb`, CI, a short or narrow terminal) whose model
|
|
18
|
-
* writes a long answer.
|
|
19
|
-
*
|
|
20
|
-
* I-L5-1 — keeping the live region strictly below `stdout.rows` makes that
|
|
21
|
-
* branch UNREACHABLE, which is the inline analogue of `frame.ts`'s
|
|
22
|
-
* `frameHeight(r) < r` and deserves the same standing. We cannot bound Ink's
|
|
23
|
-
* `fullStaticOutput` growth (it is private and append-only); preventing the
|
|
24
|
-
* branch that WRITES it is the entire mitigation, and nothing about the memory
|
|
25
|
-
* it holds is fixed by this module.
|
|
26
|
-
*
|
|
27
|
-
* Nothing is lost, only deferred: when the entry settles it moves into
|
|
28
|
-
* `<Static>` and is printed in full.
|
|
29
|
-
*/
|
|
1
|
+
/** Keep the newest thinking lines visible while an assistant entry streams. */
|
|
30
2
|
/**
|
|
31
3
|
* Keep the LAST `maxRows` rows of `text`; report how many were dropped.
|
|
32
4
|
*
|
|
@@ -27,7 +27,9 @@ function keyRows(times) {
|
|
|
27
27
|
// a key, so it has to be named somewhere they can look it up. The one-shot
|
|
28
28
|
// startup notice is the other place, and it scrolls away.
|
|
29
29
|
['Drag right edge', 'Scroll messages and input together; type to return'],
|
|
30
|
-
['
|
|
30
|
+
['Run status row', 'Spinner and queue / interrupt keys, above the input during a run'],
|
|
31
|
+
['Drag text (left button)', 'Select text on screen; the highlight waits'],
|
|
32
|
+
['Ctrl+C (with a selection)', 'Copy the pending selection - does not arm exit'],
|
|
31
33
|
['Up / Down', 'Prompt history (empty input)'],
|
|
32
34
|
// Pasting is the one affordance here with no key of its own, so the only
|
|
33
35
|
// place a user can learn what happened to their 218 lines is this row.
|
|
@@ -28,6 +28,7 @@ import { pickGlyphs } from '../glyphs.js';
|
|
|
28
28
|
import { OverlayFrame } from '../layout/OverlayFrame.js';
|
|
29
29
|
import { wrapToRows } from '../layout/wrap-rows.js';
|
|
30
30
|
import { stripPasteFrames } from '../paste-frames.js';
|
|
31
|
+
import { stripEnterFrames } from '../enter-frames.js';
|
|
31
32
|
/** The frame's border plus its one-column padding on each side. */
|
|
32
33
|
const FRAME_CHROME_COLS = 4;
|
|
33
34
|
export function PlanReviewOverlay({ plan, maxRows, cols, scrollOffset, onScrollClamp, theme, caps, onVerdict, }) {
|
|
@@ -63,7 +64,10 @@ export function PlanReviewOverlay({ plan, maxRows, cols, scrollOffset, onScrollC
|
|
|
63
64
|
// pastes — a failing test, a stack trace — and a framed paste appended
|
|
64
65
|
// verbatim would put NULs into text the model then reads.
|
|
65
66
|
if (input && !key.ctrl && !key.meta && !key.tab) {
|
|
66
|
-
|
|
67
|
+
// `stripEnterFrames` rides inside it for the newline frame a
|
|
68
|
+
// Shift+Enter broadcasts (tui-shift-enter-copy-queue 3.5): same hazard,
|
|
69
|
+
// same fix, and this order is the one that leaves no NUL in the draft.
|
|
70
|
+
setFeedback(feedback + stripPasteFrames(stripEnterFrames(input)));
|
|
67
71
|
}
|
|
68
72
|
return;
|
|
69
73
|
}
|