@aragon-agent/cli 0.6.7 → 0.6.9
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 +107 -77
- package/dist/agent/controller.d.ts +11 -7
- 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/agent/reducer.d.ts +7 -113
- package/dist/agent/reducer.js +0 -62
- package/dist/cli.js +111 -182
- package/dist/commands/builtins.js +3 -1
- package/dist/commands/perf.d.ts +32 -2
- package/dist/commands/perf.js +32 -2
- package/dist/commands/registry.d.ts +4 -5
- 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/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/logging/install.d.ts +2 -2
- package/dist/logging/install.js +2 -2
- 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/todo/store.d.ts +8 -13
- package/dist/todo/store.js +13 -14
- package/dist/todo/types.d.ts +2 -1
- package/dist/ui/ActivityLine.d.ts +26 -34
- package/dist/ui/ActivityLine.js +30 -19
- package/dist/ui/App.d.ts +3 -41
- package/dist/ui/App.js +223 -247
- package/dist/ui/BottomStatusRow.d.ts +1 -3
- package/dist/ui/BottomStatusRow.js +4 -8
- package/dist/ui/Composer.d.ts +38 -5
- package/dist/ui/Composer.js +45 -23
- 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/PromptCaret.d.ts +11 -0
- package/dist/ui/PromptCaret.js +31 -0
- package/dist/ui/PromptInput.d.ts +10 -33
- package/dist/ui/PromptInput.js +37 -20
- package/dist/ui/StatusBar.d.ts +1 -11
- package/dist/ui/StatusBar.js +14 -8
- 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 +10 -199
- package/dist/ui/composer-rows.js +50 -9
- 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/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 +37 -24
- 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/ink-runtime.d.ts +1 -0
- package/dist/ui/ink-runtime.js +21 -0
- package/dist/ui/layout/AppShell.d.ts +4 -58
- package/dist/ui/layout/AppShell.js +3 -6
- package/dist/ui/layout/OverlayFrame.d.ts +1 -25
- package/dist/ui/layout/OverlayFrame.js +0 -24
- package/dist/ui/layout/ScrollIndicator.d.ts +4 -3
- package/dist/ui/layout/ScrollIndicator.js +4 -4
- package/dist/ui/layout/ScrollViewport.d.ts +16 -66
- package/dist/ui/layout/ScrollViewport.js +195 -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/frame.d.ts +0 -22
- package/dist/ui/layout/frame.js +0 -36
- 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 +2 -3
- package/dist/ui/layout/todo-layout.js +9 -9
- package/dist/ui/layout/viewport-geometry.d.ts +2 -1
- package/dist/ui/layout/viewport-geometry.js +2 -1
- package/dist/ui/layout/virtual-window.d.ts +1 -0
- package/dist/ui/layout/virtual-window.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 +6 -2
- package/dist/ui/run-status-row.d.ts +61 -0
- package/dist/ui/run-status-row.js +70 -0
- 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/transcript-text.d.ts +0 -8
- package/dist/ui/transcript-text.js +0 -8
- package/dist/ui/use-startup-notices.d.ts +1 -57
- package/dist/ui/use-startup-notices.js +3 -54
- package/dist/ui/use-wheel-routing.d.ts +3 -1
- package/dist/ui/use-wheel-routing.js +6 -0
- package/package.json +2 -2
- package/dist/ui/TodoStrip.d.ts +0 -36
- package/dist/ui/TodoStrip.js +0 -32
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
|
-
}
|
|
@@ -24,4 +24,4 @@ export interface ThumbRange {
|
|
|
24
24
|
* gets a one-row thumb, because an indicator that vanishes at exactly the
|
|
25
25
|
* moment the transcript is longest is worse than no indicator at all.
|
|
26
26
|
*/
|
|
27
|
-
export declare function thumbRange(viewportRows: number, contentRows: number, offsetFromBottom: number): ThumbRange | null;
|
|
27
|
+
export declare function thumbRange(viewportRows: number, contentRows: number, offsetFromBottom: number, reserveTravel?: boolean): ThumbRange | null;
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* gets a one-row thumb, because an indicator that vanishes at exactly the
|
|
19
19
|
* moment the transcript is longest is worse than no indicator at all.
|
|
20
20
|
*/
|
|
21
|
-
export function thumbRange(viewportRows, contentRows, offsetFromBottom) {
|
|
21
|
+
export function thumbRange(viewportRows, contentRows, offsetFromBottom, reserveTravel = false) {
|
|
22
22
|
const viewport = Math.floor(viewportRows);
|
|
23
23
|
const content = Math.floor(contentRows);
|
|
24
24
|
if (!Number.isFinite(viewport) || !Number.isFinite(content))
|
|
@@ -27,7 +27,8 @@ export function thumbRange(viewportRows, contentRows, offsetFromBottom) {
|
|
|
27
27
|
return null;
|
|
28
28
|
const overflow = content - viewport;
|
|
29
29
|
const offset = Math.min(Math.max(Math.floor(offsetFromBottom) || 0, 0), overflow);
|
|
30
|
-
const
|
|
30
|
+
const maximum = reserveTravel && viewport >= 2 ? viewport - 1 : viewport;
|
|
31
|
+
const size = Math.min(maximum, Math.max(1, Math.round((viewport * viewport) / content)));
|
|
31
32
|
const travel = viewport - size;
|
|
32
33
|
const hiddenAbove = overflow - offset;
|
|
33
34
|
const start = travel <= 0 ? 0 : Math.round((hiddenAbove / overflow) * travel);
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ThumbRange } from './scroll-indicator.js';
|
|
2
|
+
export interface ScrollbarGeometry {
|
|
3
|
+
trackTop: number;
|
|
4
|
+
trackCol: number;
|
|
5
|
+
trackRows: number;
|
|
6
|
+
contentRows: number;
|
|
7
|
+
offset: number;
|
|
8
|
+
thumb: ThumbRange | null;
|
|
9
|
+
revision: number;
|
|
10
|
+
}
|
|
11
|
+
export interface ThumbOffsetInput {
|
|
12
|
+
geometry: ScrollbarGeometry;
|
|
13
|
+
pointerY: number;
|
|
14
|
+
grabRow: number;
|
|
15
|
+
}
|
|
16
|
+
/** Convert a captured pointer to a bottom-relative offset, with exact endpoints. */
|
|
17
|
+
export declare function offsetForThumb({ geometry: g, pointerY, grabRow }: ThumbOffsetInput): number;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/** Convert a captured pointer to a bottom-relative offset, with exact endpoints. */
|
|
2
|
+
export function offsetForThumb({ geometry: g, pointerY, grabRow }) {
|
|
3
|
+
const values = [g.trackTop, g.trackRows, g.contentRows, pointerY, grabRow];
|
|
4
|
+
if (!values.every(Number.isFinite) || !g.thumb)
|
|
5
|
+
return g.offset;
|
|
6
|
+
const overflow = Math.max(0, g.contentRows - g.trackRows);
|
|
7
|
+
const travel = g.trackRows - g.thumb.size;
|
|
8
|
+
if (travel <= 0 || overflow === 0)
|
|
9
|
+
return g.offset;
|
|
10
|
+
const grab = Math.min(Math.max(0, Math.floor(grabRow)), g.thumb.size - 1);
|
|
11
|
+
const top = Math.min(travel, Math.max(0, Math.floor(pointerY) - g.trackTop - grab));
|
|
12
|
+
return overflow - Math.round(top / travel * overflow);
|
|
13
|
+
}
|
|
@@ -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;
|
|
@@ -8,10 +6,11 @@ export interface TodoRailLayoutInput {
|
|
|
8
6
|
viewportBudget: number;
|
|
9
7
|
teamRows: number;
|
|
10
8
|
popupRows: number;
|
|
9
|
+
composerBaseRows?: number;
|
|
11
10
|
}
|
|
12
11
|
export interface TodoRailLayout {
|
|
13
12
|
visible: boolean;
|
|
14
|
-
reason: 'visible' | '
|
|
13
|
+
reason: 'visible' | 'disabled' | 'overlay' | 'empty' | 'narrow' | 'short';
|
|
15
14
|
width: number;
|
|
16
15
|
rows: number;
|
|
17
16
|
contentCols: number;
|
|
@@ -8,17 +8,17 @@ export function buildTodoRailLayout(input) {
|
|
|
8
8
|
const cols = nonNegativeInt(input.cols);
|
|
9
9
|
const budget = nonNegativeInt(input.viewportBudget);
|
|
10
10
|
const teamRows = Math.min(budget, nonNegativeInt(input.teamRows));
|
|
11
|
-
const popupMaxHeight = Math.max(0, budget - teamRows - TODO_LIMITS.panelMinRows
|
|
11
|
+
const popupMaxHeight = Math.max(0, budget - teamRows - TODO_LIMITS.panelMinRows
|
|
12
|
+
- nonNegativeInt(input.composerBaseRows ?? 0));
|
|
12
13
|
const popupRows = popupMaxHeight < 3 ? 0 : Math.min(nonNegativeInt(input.popupRows), popupMaxHeight);
|
|
13
|
-
const rows = budget
|
|
14
|
+
const rows = budget;
|
|
14
15
|
const width = todoRailWidth(cols);
|
|
15
|
-
const reason = input.
|
|
16
|
-
:
|
|
17
|
-
: input.
|
|
18
|
-
:
|
|
19
|
-
:
|
|
20
|
-
:
|
|
21
|
-
: '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';
|
|
22
22
|
const visible = reason === 'visible';
|
|
23
23
|
return { visible, reason, width, rows, contentCols: cols - (visible ? width : 0), popupMaxHeight };
|
|
24
24
|
}
|
|
@@ -17,10 +17,11 @@ export interface ViewportGeometry {
|
|
|
17
17
|
offset: number;
|
|
18
18
|
/** Natural (unclipped) height of the content, in rows. */
|
|
19
19
|
contentRows: number;
|
|
20
|
+
trailingContentRows?: number;
|
|
20
21
|
}
|
|
21
22
|
/**
|
|
22
23
|
* The default is the UNMEASURED geometry, and consumers must treat it as such:
|
|
23
|
-
* a `TranscriptList` rendered outside a `ScrollViewport` (
|
|
24
|
+
* a `TranscriptList` rendered outside a `ScrollViewport` (including
|
|
24
25
|
* test that mounts it bare) sees `viewportRows: 0`, which `selectWindow` reads
|
|
25
26
|
* as "first frame" and answers by pinning the live tail.
|
|
26
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
|
*/
|
|
@@ -21,6 +21,7 @@ export const ViewportGeometryContext = createContext({
|
|
|
21
21
|
viewportRows: 0,
|
|
22
22
|
offset: 0,
|
|
23
23
|
contentRows: 0,
|
|
24
|
+
trailingContentRows: 0,
|
|
24
25
|
});
|
|
25
26
|
export function useViewportGeometry() {
|
|
26
27
|
return useContext(ViewportGeometryContext);
|
|
@@ -98,6 +98,7 @@ export interface WindowSelection {
|
|
|
98
98
|
totalRows: number;
|
|
99
99
|
}
|
|
100
100
|
export interface SelectWindowInput {
|
|
101
|
+
trailingContentRows?: number;
|
|
101
102
|
entries: readonly Entry[];
|
|
102
103
|
/** Measured, else estimated; never 0 for a real entry. */
|
|
103
104
|
heightOf: (index: number) => number;
|
|
@@ -352,7 +352,7 @@ export function selectWindow(input) {
|
|
|
352
352
|
const viewport = Math.max(1, Math.floor(input.viewportRows) || 0);
|
|
353
353
|
const offset = Math.max(0, Math.floor(input.offset) || 0);
|
|
354
354
|
const overscan = Math.max(VIRTUAL_LIMITS.overscan, Math.floor(input.overscan) || 0);
|
|
355
|
-
const bottom = totalRows - offset;
|
|
355
|
+
const bottom = totalRows + (input.trailingContentRows ?? 0) - offset;
|
|
356
356
|
const top = bottom - viewport;
|
|
357
357
|
let start = n - 1;
|
|
358
358
|
let acc = 0;
|
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
|
*
|
|
@@ -6,7 +6,9 @@ function keyRows(times) {
|
|
|
6
6
|
return [
|
|
7
7
|
['Enter', 'Submit (idle) / queue steering (running); re-pins to the newest output'],
|
|
8
8
|
['Alt+Enter / Shift+Enter', 'Insert newline'],
|
|
9
|
-
['Esc', '
|
|
9
|
+
['Esc', 'Close menu / overlay'],
|
|
10
|
+
['Esc twice', 'Interrupt run (within 1.5s)'],
|
|
11
|
+
['Esc again', 'Force-stop after requesting interruption'],
|
|
10
12
|
// BOTH KEYS ARE LISTED HERE, and this row is why (shift-tab-mode-toggle-
|
|
11
13
|
// still-dead-on-windows, review R-7). The hint row above the composer is the
|
|
12
14
|
// other place the fallback is named, and it has two off-switches - a short
|
|
@@ -24,7 +26,9 @@ function keyRows(times) {
|
|
|
24
26
|
// Drag-select is the one affordance here a user cannot discover by pressing
|
|
25
27
|
// a key, so it has to be named somewhere they can look it up. The one-shot
|
|
26
28
|
// startup notice is the other place, and it scrolls away.
|
|
27
|
-
['Drag
|
|
29
|
+
['Drag right edge', 'Scroll messages and input together; type to return'],
|
|
30
|
+
['Run status row', 'Spinner and steer / interrupt keys, above the input during a run'],
|
|
31
|
+
['Drag text (left button)', 'Select text on screen; releasing copies it'],
|
|
28
32
|
['Up / Down', 'Prompt history (empty input)'],
|
|
29
33
|
// Pasting is the one affordance here with no key of its own, so the only
|
|
30
34
|
// place a user can learn what happened to their 218 lines is this row.
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The run status row's width plan (tui-scrollbar-edge-and-run-row §3.3.3).
|
|
3
|
+
*
|
|
4
|
+
* While a run is in flight, ONE row above the input box carries the spinner, the
|
|
5
|
+
* phrase and the `steer / interrupt / exit` hint clauses, and `planRunRow` decides
|
|
6
|
+
* what survives when the line is too narrow. It is a pure function with no React
|
|
7
|
+
* and no I/O so the whole ladder can be tested as arithmetic.
|
|
8
|
+
*
|
|
9
|
+
* THE ALLOCATION IS PER CLAUSE, NOT PER STRING, and that is the whole point.
|
|
10
|
+
* The idle hint row this one replaces owned the full line width and ended in
|
|
11
|
+
* `wrap="truncate"`; handing the label half the line and truncating the joined
|
|
12
|
+
* hint would cut `interrupt` to `inter...` at 38 columns - a regression of the
|
|
13
|
+
* "running hint is NEVER abbreviated" rule `Composer.hintText` documents. So:
|
|
14
|
+
*
|
|
15
|
+
* - clauses are dropped WHOLE, from the tail (`exit`, then `stop N`), and
|
|
16
|
+
* - `steer` and `interrupt` (REQUIRED_CLAUSES) are never dropped or cut mid-word
|
|
17
|
+
* while the row is enabled, and
|
|
18
|
+
* - the only thing allowed to end in an ellipsis is the label.
|
|
19
|
+
*/
|
|
20
|
+
/** Everything the label needs, kept in one shape so there is one place that names it. */
|
|
21
|
+
export interface RunActivity {
|
|
22
|
+
/** Epoch ms the current run began - the phrase sequence's seed. */
|
|
23
|
+
startedAt: number;
|
|
24
|
+
/** `Date.now() - startedAt`, already computed by App's 200 ms ticker. */
|
|
25
|
+
elapsedMs: number;
|
|
26
|
+
/** The RAW config flag, never the widened one (single-spinner D-5). */
|
|
27
|
+
reducedMotion: boolean;
|
|
28
|
+
/** The tool currently in flight, if any. */
|
|
29
|
+
runningTool?: string;
|
|
30
|
+
/** A context compaction is in flight; outranks the tool name and the phrase. */
|
|
31
|
+
compacting?: boolean;
|
|
32
|
+
}
|
|
33
|
+
export interface RunRowPlanInput {
|
|
34
|
+
/** Width of the row's container (the composer's content columns). */
|
|
35
|
+
cols: number;
|
|
36
|
+
/** Spinner + space + label, in display columns (the label's natural width). */
|
|
37
|
+
labelCols: number;
|
|
38
|
+
/** Display width of each hint clause, most urgent first. */
|
|
39
|
+
hintClauseCols: readonly number[];
|
|
40
|
+
/** Display width of the separator between clauses (` · ` or ` - `). */
|
|
41
|
+
separatorCols: number;
|
|
42
|
+
/** Display width of the right-aligned mode chip; 0 = no chip. */
|
|
43
|
+
chipCols: number;
|
|
44
|
+
}
|
|
45
|
+
export interface RunRowPlan {
|
|
46
|
+
/** Columns the label may occupy; below its natural width it is ellipsized. */
|
|
47
|
+
labelCols: number;
|
|
48
|
+
/** How many leading clauses to show; 0 = none (defensive tier only). */
|
|
49
|
+
hintClauses: number;
|
|
50
|
+
/** Whether the chip fits after everything else. */
|
|
51
|
+
chip: boolean;
|
|
52
|
+
}
|
|
53
|
+
/** One leading space, matching the idle hint row's `' '` indent. */
|
|
54
|
+
export declare const RUN_ROW_LEAD = 1;
|
|
55
|
+
/** Narrowest the label may be squeezed before a clause is dropped instead. */
|
|
56
|
+
export declare const RUN_ROW_MIN_LABEL_COLS = 10;
|
|
57
|
+
/** `steer` and `interrupt`: always whole while the row is enabled. */
|
|
58
|
+
export declare const RUN_ROW_REQUIRED_CLAUSES = 2;
|
|
59
|
+
/** Space kept between the last clause and the chip. */
|
|
60
|
+
export declare const RUN_ROW_CHIP_GAP = 2;
|
|
61
|
+
export declare function planRunRow(input: RunRowPlanInput): RunRowPlan;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The run status row's width plan (tui-scrollbar-edge-and-run-row §3.3.3).
|
|
3
|
+
*
|
|
4
|
+
* While a run is in flight, ONE row above the input box carries the spinner, the
|
|
5
|
+
* phrase and the `steer / interrupt / exit` hint clauses, and `planRunRow` decides
|
|
6
|
+
* what survives when the line is too narrow. It is a pure function with no React
|
|
7
|
+
* and no I/O so the whole ladder can be tested as arithmetic.
|
|
8
|
+
*
|
|
9
|
+
* THE ALLOCATION IS PER CLAUSE, NOT PER STRING, and that is the whole point.
|
|
10
|
+
* The idle hint row this one replaces owned the full line width and ended in
|
|
11
|
+
* `wrap="truncate"`; handing the label half the line and truncating the joined
|
|
12
|
+
* hint would cut `interrupt` to `inter...` at 38 columns - a regression of the
|
|
13
|
+
* "running hint is NEVER abbreviated" rule `Composer.hintText` documents. So:
|
|
14
|
+
*
|
|
15
|
+
* - clauses are dropped WHOLE, from the tail (`exit`, then `stop N`), and
|
|
16
|
+
* - `steer` and `interrupt` (REQUIRED_CLAUSES) are never dropped or cut mid-word
|
|
17
|
+
* while the row is enabled, and
|
|
18
|
+
* - the only thing allowed to end in an ellipsis is the label.
|
|
19
|
+
*/
|
|
20
|
+
/** One leading space, matching the idle hint row's `' '` indent. */
|
|
21
|
+
export const RUN_ROW_LEAD = 1;
|
|
22
|
+
/** Narrowest the label may be squeezed before a clause is dropped instead. */
|
|
23
|
+
export const RUN_ROW_MIN_LABEL_COLS = 10;
|
|
24
|
+
/** `steer` and `interrupt`: always whole while the row is enabled. */
|
|
25
|
+
export const RUN_ROW_REQUIRED_CLAUSES = 2;
|
|
26
|
+
/** Space kept between the last clause and the chip. */
|
|
27
|
+
export const RUN_ROW_CHIP_GAP = 2;
|
|
28
|
+
/** Non-finite or negative widths collapse to 0 so no input can make the plan throw. */
|
|
29
|
+
function width(value) {
|
|
30
|
+
return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
|
|
31
|
+
}
|
|
32
|
+
/** Width of the first `count` clauses joined by `separator`. */
|
|
33
|
+
function clausesWidth(clauses, count, separator) {
|
|
34
|
+
if (count <= 0)
|
|
35
|
+
return 0;
|
|
36
|
+
let total = separator * (count - 1);
|
|
37
|
+
for (let i = 0; i < count; i += 1)
|
|
38
|
+
total += clauses[i] ?? 0;
|
|
39
|
+
return total;
|
|
40
|
+
}
|
|
41
|
+
export function planRunRow(input) {
|
|
42
|
+
const inner = Math.max(0, width(input.cols) - RUN_ROW_LEAD);
|
|
43
|
+
const natural = width(input.labelCols);
|
|
44
|
+
const separator = width(input.separatorCols);
|
|
45
|
+
const chipCols = width(input.chipCols);
|
|
46
|
+
const clauses = input.hintClauseCols.map(width);
|
|
47
|
+
const total = clauses.length;
|
|
48
|
+
const labelOnly = {
|
|
49
|
+
labelCols: Math.min(natural, inner), hintClauses: 0, chip: false,
|
|
50
|
+
};
|
|
51
|
+
if (total < RUN_ROW_REQUIRED_CLAUSES)
|
|
52
|
+
return labelOnly;
|
|
53
|
+
// Tier 1: everything fits; the chip is the first thing to go.
|
|
54
|
+
const everything = natural + separator + clausesWidth(clauses, total, separator);
|
|
55
|
+
if (everything <= inner) {
|
|
56
|
+
const chip = chipCols > 0 && everything + RUN_ROW_CHIP_GAP + chipCols <= inner;
|
|
57
|
+
return { labelCols: natural, hintClauses: total, chip };
|
|
58
|
+
}
|
|
59
|
+
// Tier 2: drop clauses from the tail, but never below the required two; only the
|
|
60
|
+
// label may be squeezed (down to its minimum) to make the remaining clauses fit.
|
|
61
|
+
for (let kept = total - 1; kept >= RUN_ROW_REQUIRED_CLAUSES; kept -= 1) {
|
|
62
|
+
const available = inner - separator - clausesWidth(clauses, kept, separator);
|
|
63
|
+
if (available >= RUN_ROW_MIN_LABEL_COLS) {
|
|
64
|
+
return { labelCols: Math.min(natural, available), hintClauses: kept, chip: false };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// Tier 3 (defensive, below ~40 columns): the label alone. The status bar's own
|
|
68
|
+
// `Esc x2 interrupt` carries the urgent hint at this width.
|
|
69
|
+
return labelOnly;
|
|
70
|
+
}
|
package/dist/ui/screen.d.ts
CHANGED
|
@@ -43,7 +43,7 @@ export interface AltScreenOptions {
|
|
|
43
43
|
*
|
|
44
44
|
* SAME "ALREADY DECIDED ELSEWHERE" DISCIPLINE AS `mouse`: this module must
|
|
45
45
|
* never derive it from config. `cli.tsx` passes
|
|
46
|
-
* `
|
|
46
|
+
* `mouseOn`, after the filter and VT capability have been resolved.
|
|
47
47
|
*
|
|
48
48
|
* Optional, defaulting to `false`, so every pre-existing caller and every
|
|
49
49
|
* pre-existing test emits exactly the bytes it emitted before (AC-8 / T-34).
|
package/dist/ui/screen.js
CHANGED
|
@@ -23,18 +23,16 @@ const SHOW_CURSOR = '\x1b[?25h';
|
|
|
23
23
|
* text rather than raw bytes above 0x7F that `setEncoding('utf8')` would mangle.
|
|
24
24
|
*
|
|
25
25
|
* `?1002` is BUTTON-EVENT TRACKING: motion is reported ONLY WHILE A BUTTON IS
|
|
26
|
-
* HELD, which
|
|
26
|
+
* HELD, which both scrollbar dragging and text selection need and costs exactly zero reports while the
|
|
27
27
|
* pointer is idle. This comment used to lump it together with `?1003` and
|
|
28
28
|
* dismiss both as "floods stdin on every pointer move" — true of `?1003`
|
|
29
29
|
* (any-event), false of `?1002`, and the sentence had to go in the same commit
|
|
30
30
|
* that started asking for it or it would stand as a documented reason not to
|
|
31
31
|
* (AC-11 / D-10).
|
|
32
32
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
* they replaced (AC-8 / D-15). `disableMouse` is the exact reverse of
|
|
37
|
-
* `enableMouse` for both values (I-1 / AC-7).
|
|
33
|
+
* Motion is enabled with mouse reporting, independently of text selection.
|
|
34
|
+
* Disabling selection must still allow scrollbar gestures. The enable/disable
|
|
35
|
+
* sequences remain paired for both values of this resolved capability.
|
|
38
36
|
*/
|
|
39
37
|
const enableMouse = (motion) => `\x1b[?1000h${motion ? '\x1b[?1002h' : ''}\x1b[?1006h`;
|
|
40
38
|
const disableMouse = (motion) => `\x1b[?1006l${motion ? '\x1b[?1002l' : ''}\x1b[?1000l`;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { MouseEvent } from '../input/mouse-events.js';
|
|
2
|
+
import { type ScrollbarGeometry } from './layout/scrollbar-geometry.js';
|
|
3
|
+
export interface ScrollbarController {
|
|
4
|
+
handle(event: MouseEvent): boolean;
|
|
5
|
+
cancel(): void;
|
|
6
|
+
dispose(): void;
|
|
7
|
+
isCaptured(): boolean;
|
|
8
|
+
}
|
|
9
|
+
export interface ScrollbarOptions {
|
|
10
|
+
getGeometry(): ScrollbarGeometry | null;
|
|
11
|
+
setOffset(offset: number): void;
|
|
12
|
+
page(direction: 'pageUp' | 'pageDown'): void;
|
|
13
|
+
onHold(held: boolean): void;
|
|
14
|
+
onStart(): void;
|
|
15
|
+
isEnabled(): boolean;
|
|
16
|
+
}
|
|
17
|
+
export interface ScrollbarBridge {
|
|
18
|
+
controller: ScrollbarController | null;
|
|
19
|
+
geometry: ScrollbarGeometry | null;
|
|
20
|
+
frameReady: boolean;
|
|
21
|
+
isEnabled(): boolean;
|
|
22
|
+
onStart?: () => void;
|
|
23
|
+
onGeometry?: () => void;
|
|
24
|
+
requestRedraw?: () => void;
|
|
25
|
+
invalidate(): void;
|
|
26
|
+
}
|
|
27
|
+
/** Build the late-bound terminal bridge; no scroll position is owned here. */
|
|
28
|
+
export declare function createScrollbarBridge(isEnabled: () => boolean): ScrollbarBridge;
|
|
29
|
+
/** Own one pointer gesture and bounded timers; positioning remains in the viewport. */
|
|
30
|
+
export declare function createScrollbarController(options: ScrollbarOptions): ScrollbarController;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { offsetForThumb } from './layout/scrollbar-geometry.js';
|
|
2
|
+
/** Build the late-bound terminal bridge; no scroll position is owned here. */
|
|
3
|
+
export function createScrollbarBridge(isEnabled) {
|
|
4
|
+
const bridge = {
|
|
5
|
+
controller: null, geometry: null, frameReady: false, isEnabled,
|
|
6
|
+
invalidate: () => { bridge.frameReady = false; bridge.controller?.cancel(); },
|
|
7
|
+
};
|
|
8
|
+
return bridge;
|
|
9
|
+
}
|
|
10
|
+
/** Own one pointer gesture and bounded timers; positioning remains in the viewport. */
|
|
11
|
+
export function createScrollbarController(options) {
|
|
12
|
+
return new ScrollbarGesture(options);
|
|
13
|
+
}
|
|
14
|
+
class ScrollbarGesture {
|
|
15
|
+
options;
|
|
16
|
+
capture = null;
|
|
17
|
+
timer = null;
|
|
18
|
+
watchdog = null;
|
|
19
|
+
swallowLate = false;
|
|
20
|
+
disposed = false;
|
|
21
|
+
constructor(options) {
|
|
22
|
+
this.options = options;
|
|
23
|
+
}
|
|
24
|
+
isCaptured() { return this.capture !== null; }
|
|
25
|
+
cancel() {
|
|
26
|
+
if (this.timer)
|
|
27
|
+
clearTimeout(this.timer);
|
|
28
|
+
if (this.watchdog)
|
|
29
|
+
clearTimeout(this.watchdog);
|
|
30
|
+
this.timer = this.watchdog = null;
|
|
31
|
+
const held = this.capture !== null;
|
|
32
|
+
this.capture = null;
|
|
33
|
+
if (held) {
|
|
34
|
+
this.swallowLate = true;
|
|
35
|
+
this.options.onHold(false);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
dispose() { this.cancel(); this.disposed = true; }
|
|
39
|
+
armWatchdog() {
|
|
40
|
+
if (this.watchdog)
|
|
41
|
+
clearTimeout(this.watchdog);
|
|
42
|
+
this.watchdog = setTimeout(() => this.cancel(), 30000);
|
|
43
|
+
this.watchdog.unref?.();
|
|
44
|
+
}
|
|
45
|
+
flush() {
|
|
46
|
+
if (this.timer)
|
|
47
|
+
clearTimeout(this.timer);
|
|
48
|
+
this.timer = null;
|
|
49
|
+
const g = this.options.getGeometry();
|
|
50
|
+
if (!this.capture || !g || !this.options.isEnabled() ||
|
|
51
|
+
g.revision !== this.capture.revision) {
|
|
52
|
+
this.cancel();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
const y = this.capture.pendingY;
|
|
56
|
+
this.capture.pendingY = null;
|
|
57
|
+
if (y !== null)
|
|
58
|
+
this.options.setOffset(offsetForThumb({ geometry: g,
|
|
59
|
+
pointerY: y, grabRow: this.capture.grabRow }));
|
|
60
|
+
}
|
|
61
|
+
move(y) {
|
|
62
|
+
if (!this.capture || this.capture.kind !== 'thumb' || y === this.capture.lastPointerY)
|
|
63
|
+
return;
|
|
64
|
+
this.capture.lastPointerY = y;
|
|
65
|
+
this.capture.pendingY = y;
|
|
66
|
+
if (!this.timer) {
|
|
67
|
+
this.timer = setTimeout(() => this.flush(), 16);
|
|
68
|
+
this.timer.unref?.();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
press(event) {
|
|
72
|
+
this.cancel();
|
|
73
|
+
this.swallowLate = false;
|
|
74
|
+
const g = this.options.getGeometry();
|
|
75
|
+
if (!g || !this.canPress(event, g))
|
|
76
|
+
return false;
|
|
77
|
+
const row = event.y - g.trackTop;
|
|
78
|
+
const thumb = g.thumb;
|
|
79
|
+
const inside = thumb && row >= thumb.start && row < thumb.start + thumb.size;
|
|
80
|
+
this.capture = { kind: inside && g.trackRows > thumb.size ? 'thumb' : 'track',
|
|
81
|
+
grabRow: inside ? row - thumb.start : 0, revision: g.revision,
|
|
82
|
+
lastPointerY: event.y, pendingY: null };
|
|
83
|
+
this.options.onStart();
|
|
84
|
+
this.options.onHold(true);
|
|
85
|
+
this.armWatchdog();
|
|
86
|
+
if (thumb && !inside)
|
|
87
|
+
this.options.page(row < thumb.start ? 'pageUp' : 'pageDown');
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
canPress(event, g) {
|
|
91
|
+
const plainLeft = event.button === 0 && !event.shift && !event.alt && !event.ctrl;
|
|
92
|
+
const inTrack = event.x === g.trackCol && event.y >= g.trackTop &&
|
|
93
|
+
event.y < g.trackTop + g.trackRows;
|
|
94
|
+
return this.options.isEnabled() && plainLeft && inTrack;
|
|
95
|
+
}
|
|
96
|
+
handle(event) {
|
|
97
|
+
if (this.disposed)
|
|
98
|
+
return false;
|
|
99
|
+
if (event.kind === 'wheel')
|
|
100
|
+
return this.capture !== null;
|
|
101
|
+
if (![event.x, event.y].every(Number.isFinite))
|
|
102
|
+
return this.capture !== null;
|
|
103
|
+
if (event.kind === 'press')
|
|
104
|
+
return this.press(event);
|
|
105
|
+
if (!this.capture)
|
|
106
|
+
return this.swallowLate;
|
|
107
|
+
if (event.button !== 0) {
|
|
108
|
+
this.cancel();
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
if (!this.options.isEnabled()) {
|
|
112
|
+
this.cancel();
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
this.move(event.y);
|
|
116
|
+
if (event.kind === 'release') {
|
|
117
|
+
this.flush();
|
|
118
|
+
this.cancel();
|
|
119
|
+
}
|
|
120
|
+
else
|
|
121
|
+
this.armWatchdog();
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Plain-text session rendering (spec §4.4). Pure, colorless, ANSI-free, so the
|
|
3
|
-
* exit replay can be piped, diffed, or pasted into an issue.
|
|
4
|
-
*
|
|
5
|
-
* Used only by the full-screen exit replay: inline mode already left the real
|
|
6
|
-
* transcript in the terminal's native scrollback, and replaying there would
|
|
7
|
-
* simply print everything twice.
|
|
8
|
-
*/
|
|
9
1
|
import type { Entry, UsageTotal } from '../agent/reducer.js';
|
|
10
2
|
import type { Glyphs } from './glyphs.js';
|
|
11
3
|
export interface TranscriptTextOptions {
|
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Plain-text session rendering (spec §4.4). Pure, colorless, ANSI-free, so the
|
|
3
|
-
* exit replay can be piped, diffed, or pasted into an issue.
|
|
4
|
-
*
|
|
5
|
-
* Used only by the full-screen exit replay: inline mode already left the real
|
|
6
|
-
* transcript in the terminal's native scrollback, and replaying there would
|
|
7
|
-
* simply print everything twice.
|
|
8
|
-
*/
|
|
9
1
|
import { formatCost, formatDuration, formatTokens, promptTokensOf } from '../agent/usage.js';
|
|
10
2
|
import { formatRetryLine } from '../agent/retry-view.js';
|
|
11
3
|
const DEFAULT_MAX_ENTRIES = 200;
|