@aragon-agent/cli 0.5.6 → 0.5.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 +356 -0
- package/README.md +1429 -1105
- package/dist/agent/coalesce.d.ts +13 -0
- package/dist/agent/coalesce.js +50 -11
- package/dist/agent/controller.d.ts +141 -2
- package/dist/agent/controller.js +396 -4
- package/dist/agent/entry-limits.d.ts +65 -0
- package/dist/agent/entry-limits.js +133 -0
- package/dist/agent/headless.d.ts +8 -0
- package/dist/agent/headless.js +40 -0
- package/dist/agent/reducer.d.ts +253 -3
- package/dist/agent/reducer.js +553 -28
- package/dist/agent/retry-view.d.ts +68 -0
- package/dist/agent/retry-view.js +95 -0
- package/dist/agent/system-prompt.d.ts +11 -0
- package/dist/agent/system-prompt.js +2 -0
- package/dist/cli.js +122 -4
- package/dist/commands/builtins.d.ts +1 -1
- package/dist/commands/builtins.js +313 -2
- package/dist/commands/perf.d.ts +45 -0
- package/dist/commands/perf.js +78 -0
- package/dist/config/cli-commands.d.ts +53 -2
- package/dist/config/cli-commands.js +136 -3
- package/dist/config/env.d.ts +6 -1
- package/dist/config/env.js +131 -1
- package/dist/config/load.d.ts +63 -0
- package/dist/config/load.js +125 -2
- package/dist/config/schema.d.ts +273 -10
- package/dist/config/schema.js +250 -7
- package/dist/config/store.js +20 -1
- package/dist/fast/critique.d.ts +43 -0
- package/dist/fast/critique.js +62 -0
- package/dist/fast/digest.d.ts +42 -0
- package/dist/fast/digest.js +85 -0
- package/dist/fast/frames.d.ts +99 -0
- package/dist/fast/frames.js +180 -0
- package/dist/fast/limits.d.ts +98 -0
- package/dist/fast/limits.js +98 -0
- package/dist/fast/prompt.d.ts +72 -0
- package/dist/fast/prompt.js +94 -0
- package/dist/fast/resolve.d.ts +43 -0
- package/dist/fast/resolve.js +105 -0
- package/dist/fast/review-call.d.ts +99 -0
- package/dist/fast/review-call.js +130 -0
- package/dist/fast/reviewer.d.ts +248 -0
- package/dist/fast/reviewer.js +664 -0
- package/dist/fast/types.d.ts +128 -0
- package/dist/fast/types.js +14 -0
- package/dist/fast/wiring.d.ts +170 -0
- package/dist/fast/wiring.js +316 -0
- package/dist/logging/install.d.ts +15 -1
- package/dist/logging/install.js +37 -4
- package/dist/logging/logger.d.ts +1 -1
- package/dist/session/persist.js +57 -1
- package/dist/team/normalize.d.ts +26 -1
- package/dist/team/normalize.js +23 -3
- package/dist/team/prompt.d.ts +11 -0
- package/dist/team/prompt.js +10 -0
- package/dist/team/report.d.ts +18 -0
- package/dist/team/report.js +27 -1
- package/dist/team/runtime.d.ts +13 -2
- package/dist/team/runtime.js +33 -6
- package/dist/team/subagent.d.ts +19 -1
- package/dist/team/subagent.js +72 -10
- package/dist/team/task-tool.d.ts +26 -0
- package/dist/team/task-tool.js +48 -4
- package/dist/team/types.d.ts +80 -1
- package/dist/tools/bash-tool.js +19 -3
- package/dist/tools/diff.d.ts +25 -5
- package/dist/tools/diff.js +72 -41
- package/dist/tools/file-change-store.d.ts +34 -0
- package/dist/tools/file-change-store.js +57 -0
- package/dist/tools/fs-tools.d.ts +26 -0
- package/dist/tools/fs-tools.js +62 -5
- package/dist/tools/index.d.ts +31 -0
- package/dist/tools/index.js +7 -1
- package/dist/tools/patch.d.ts +115 -0
- package/dist/tools/patch.js +453 -0
- package/dist/tools/terminal-output.d.ts +107 -0
- package/dist/tools/terminal-output.js +198 -0
- package/dist/tools/tool-output-store.d.ts +68 -0
- package/dist/tools/tool-output-store.js +102 -0
- package/dist/ui/ActivityLine.d.ts +48 -0
- package/dist/ui/ActivityLine.js +22 -0
- package/dist/ui/App.d.ts +1 -4
- package/dist/ui/App.js +357 -44
- package/dist/ui/BottomStatusRow.d.ts +43 -0
- package/dist/ui/BottomStatusRow.js +14 -0
- package/dist/ui/Composer.d.ts +1 -6
- package/dist/ui/Composer.js +2 -2
- package/dist/ui/Markdown.d.ts +7 -1
- package/dist/ui/Markdown.js +15 -48
- package/dist/ui/PromptInput.d.ts +1 -15
- package/dist/ui/PromptInput.js +1 -38
- package/dist/ui/StatusBar.d.ts +50 -0
- package/dist/ui/StatusBar.js +7 -3
- package/dist/ui/TeamPanel.d.ts +5 -4
- package/dist/ui/TeamPanel.js +1 -1
- package/dist/ui/TodoPanel.d.ts +5 -4
- package/dist/ui/Transcript.d.ts +80 -5
- package/dist/ui/Transcript.js +179 -11
- package/dist/ui/activity-phrases.d.ts +33 -0
- package/dist/ui/activity-phrases.js +50 -0
- package/dist/ui/entries/AssistantEntry.d.ts +38 -1
- package/dist/ui/entries/AssistantEntry.js +34 -3
- package/dist/ui/entries/DiffView.d.ts +67 -0
- package/dist/ui/entries/DiffView.js +103 -0
- package/dist/ui/entries/FastCard.d.ts +53 -0
- package/dist/ui/entries/FastCard.js +87 -0
- package/dist/ui/entries/RetryCard.d.ts +56 -0
- package/dist/ui/entries/RetryCard.js +60 -0
- package/dist/ui/entries/TeamCard.d.ts +9 -1
- package/dist/ui/entries/TeamCard.js +28 -2
- package/dist/ui/entries/TodoCard.d.ts +9 -1
- package/dist/ui/entries/TodoCard.js +23 -1
- package/dist/ui/entries/ToolCard.d.ts +50 -1
- package/dist/ui/entries/ToolCard.js +104 -7
- package/dist/ui/entries/ToolPreview.d.ts +2 -2
- package/dist/ui/entries/UserEntry.d.ts +9 -1
- package/dist/ui/entries/UserEntry.js +17 -1
- package/dist/ui/exit-snapshot.d.ts +7 -0
- package/dist/ui/glyphs.d.ts +9 -0
- package/dist/ui/glyphs.js +2 -0
- package/dist/ui/layout/AppShell.d.ts +13 -22
- package/dist/ui/layout/AppShell.js +3 -38
- package/dist/ui/layout/MeasuredEntry.d.ts +21 -0
- package/dist/ui/layout/MeasuredEntry.js +30 -0
- package/dist/ui/layout/ScrollViewport.js +8 -2
- package/dist/ui/layout/viewport-geometry.d.ts +28 -0
- package/dist/ui/layout/viewport-geometry.js +27 -0
- package/dist/ui/layout/virtual-window.d.ts +100 -0
- package/dist/ui/layout/virtual-window.js +308 -0
- package/dist/ui/live-clamp.d.ts +42 -0
- package/dist/ui/live-clamp.js +74 -0
- package/dist/ui/markdown-blocks.d.ts +25 -1
- package/dist/ui/markdown-blocks.js +43 -0
- package/dist/ui/overlays/HelpOverlay.js +16 -2
- package/dist/ui/overlays/SettingsScreen.d.ts +82 -1
- package/dist/ui/overlays/SettingsScreen.js +147 -1
- package/dist/ui/render-cache.d.ts +81 -0
- package/dist/ui/render-cache.js +195 -0
- package/dist/ui/render-governor.d.ts +54 -0
- package/dist/ui/render-governor.js +78 -0
- package/dist/ui/transcript-text.d.ts +11 -0
- package/dist/ui/transcript-text.js +32 -0
- package/dist/ui/use-height-store.d.ts +31 -0
- package/dist/ui/use-height-store.js +76 -0
- package/dist/ui/use-render-governor.d.ts +39 -0
- package/dist/ui/use-render-governor.js +57 -0
- package/dist/ui/use-startup-notices.d.ts +9 -1
- package/dist/ui/use-startup-notices.js +35 -1
- package/dist/ui/use-wheel-routing.d.ts +16 -32
- package/dist/ui/use-wheel-routing.js +21 -38
- package/package.json +2 -2
- package/dist/ui/layout/regions.d.ts +0 -55
- package/dist/ui/layout/regions.js +0 -52
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Render a user message entry.
|
|
4
|
+
*
|
|
5
|
+
* The role glyph, the gutter rail and the inter-entry spacing all belong to
|
|
6
|
+
* `EntryFrame` now (§4.3), so this component renders content and nothing else.
|
|
7
|
+
* That is also why it takes no `caps`: it has no glyph of its own left to
|
|
8
|
+
* degrade.
|
|
9
|
+
*/
|
|
10
|
+
import React from 'react';
|
|
2
11
|
import { Box, Text } from 'ink';
|
|
3
|
-
|
|
12
|
+
function UserEntryImpl({ text, theme }) {
|
|
4
13
|
return (_jsx(Box, { flexDirection: "column", children: text.split('\n').map((line, i) => (_jsx(Text, { color: theme.user, bold: i === 0, children: line }, i))) }));
|
|
5
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* `React.memo` with the DEFAULT comparator (tui-render-performance L2 / R3).
|
|
17
|
+
* `mapEntry` already preserves object identity for untouched entries, so the
|
|
18
|
+
* array props below are stable references on a settled card, and `theme` /
|
|
19
|
+
* `caps` are `useMemo`d in `App.tsx` (I-L2-1).
|
|
20
|
+
*/
|
|
21
|
+
export const UserEntry = React.memo(UserEntryImpl);
|
|
@@ -18,6 +18,13 @@ export interface ExitSnapshot {
|
|
|
18
18
|
provider: string;
|
|
19
19
|
model: string;
|
|
20
20
|
startedAt: number;
|
|
21
|
+
/**
|
|
22
|
+
* Entries the retain ring removed (tui-render-performance L1).
|
|
23
|
+
*
|
|
24
|
+
* OPTIONAL so every existing caller and test is unchanged; absent means the
|
|
25
|
+
* replay says nothing, which is correct for a session that dropped nothing.
|
|
26
|
+
*/
|
|
27
|
+
droppedEntries?: number;
|
|
21
28
|
}
|
|
22
29
|
/** Called by `App` after every view-state change. Synchronous, side-effect free. */
|
|
23
30
|
export declare function publishExitSnapshot(next: ExitSnapshot): void;
|
package/dist/ui/glyphs.d.ts
CHANGED
|
@@ -108,6 +108,15 @@ export interface Glyphs {
|
|
|
108
108
|
todoPending: string;
|
|
109
109
|
todoActive: string;
|
|
110
110
|
todoDone: string;
|
|
111
|
+
/**
|
|
112
|
+
* API retry (llm-api-retry-backoff §6.5). The rail marker on the retry card.
|
|
113
|
+
*
|
|
114
|
+
* HERE RATHER THAN IN `RetryCard`, for the reason this file's header gives: a
|
|
115
|
+
* literal in a component bypasses `pickGlyphs` entirely and shows mojibake on a
|
|
116
|
+
* legacy console whatever the capability probe said. `glyphs.test.ts`'s static
|
|
117
|
+
* scan fails the build if either spelling appears anywhere else.
|
|
118
|
+
*/
|
|
119
|
+
retry: string;
|
|
111
120
|
/**
|
|
112
121
|
* Ink `borderStyle` name for every boxed region (composer, completion popup,
|
|
113
122
|
* overlay frame).
|
package/dist/ui/glyphs.js
CHANGED
|
@@ -64,6 +64,7 @@ const UNICODE_GLYPHS = {
|
|
|
64
64
|
todoPending: '○',
|
|
65
65
|
todoActive: '▸',
|
|
66
66
|
todoDone: '✔',
|
|
67
|
+
retry: '↻',
|
|
67
68
|
tool: {
|
|
68
69
|
read_file: '▤',
|
|
69
70
|
write_file: '✎',
|
|
@@ -125,6 +126,7 @@ const ASCII_GLYPHS = {
|
|
|
125
126
|
todoPending: '[ ]',
|
|
126
127
|
todoActive: '>',
|
|
127
128
|
todoDone: '[x]',
|
|
129
|
+
retry: '[r]',
|
|
128
130
|
tool: {
|
|
129
131
|
read_file: '=',
|
|
130
132
|
write_file: '+',
|
|
@@ -11,7 +11,6 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import React from 'react';
|
|
13
13
|
import { type RenderMode } from './frame.js';
|
|
14
|
-
import { type FrameGeometry } from './regions.js';
|
|
15
14
|
export interface AppShellProps {
|
|
16
15
|
mode: RenderMode;
|
|
17
16
|
rows: number;
|
|
@@ -21,11 +20,10 @@ export interface AppShellProps {
|
|
|
21
20
|
/**
|
|
22
21
|
* The live subagent roster (team-subagents §6.1), or nothing.
|
|
23
22
|
*
|
|
24
|
-
* IT RENDERS
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* composer band boundary stale the instant a dispatch started.
|
|
23
|
+
* IT RENDERS IN THE BOTTOM CHROME OF THE FULL-SCREEN BRANCH, above `toast` and
|
|
24
|
+
* below the viewport: the roster is transient status about the run in progress,
|
|
25
|
+
* so it belongs with the other transient rows rather than inside the scrolling
|
|
26
|
+
* transcript, where it would be interleaved with history and scroll away.
|
|
29
27
|
*/
|
|
30
28
|
team?: React.ReactNode;
|
|
31
29
|
/**
|
|
@@ -36,10 +34,11 @@ export interface AppShellProps {
|
|
|
36
34
|
* primitive and has no business knowing what a todo is. `team` above already
|
|
37
35
|
* leaks content vocabulary in here; there is no reason to repeat it.
|
|
38
36
|
*
|
|
39
|
-
* IT MUST
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
37
|
+
* IT MUST BE RENDERED IN THE MIDDLE BAND, BESIDE THE VIEWPORT (C-6 / I-4) —
|
|
38
|
+
* that is what makes it a rail rather than another row of bottom chrome. Moved
|
|
39
|
+
* down into the bottom box it would stop being a column beside the transcript
|
|
40
|
+
* and start eating viewport height, growing and shrinking the transcript every
|
|
41
|
+
* time the list changed length.
|
|
43
42
|
*
|
|
44
43
|
* The INLINE branch does not include it at all — a structural guarantee rather
|
|
45
44
|
* than a promise that the caller passes `null` (non-goal 4).
|
|
@@ -56,21 +55,13 @@ export interface AppShellProps {
|
|
|
56
55
|
* both on screen showing the same list (I-6). Fullscreen has the rail; that is
|
|
57
56
|
* the whole design.
|
|
58
57
|
*
|
|
59
|
-
* It sits between `viewport` and `team`,
|
|
60
|
-
* other branch
|
|
61
|
-
*
|
|
58
|
+
* It sits between `viewport` and `team`, mirroring where the rail sits in the
|
|
59
|
+
* other branch: last thing above the bottom chrome, so the strip and the
|
|
60
|
+
* roster read top-to-bottom in the same order in both modes.
|
|
62
61
|
*/
|
|
63
62
|
strip?: React.ReactNode;
|
|
64
63
|
toast: React.ReactNode;
|
|
65
64
|
composer: React.ReactNode;
|
|
66
65
|
status: React.ReactNode;
|
|
67
|
-
/**
|
|
68
|
-
* Where the wheel router reads the composer band boundary from (mouse-wheel
|
|
69
|
-
* §4.5). Written in a LAYOUT EFFECT to a MUTABLE REF, never to state: the
|
|
70
|
-
* measurement happens on every commit, and routing it through `setState`
|
|
71
|
-
* would add a re-render to the streaming path for a value nothing renders
|
|
72
|
-
* (R-7).
|
|
73
|
-
*/
|
|
74
|
-
geometryRef?: React.MutableRefObject<FrameGeometry>;
|
|
75
66
|
}
|
|
76
|
-
export declare function AppShell({ mode, rows, cols, header, viewport, team, rail, strip, toast, composer, status,
|
|
67
|
+
export declare function AppShell({ mode, rows, cols, header, viewport, team, rail, strip, toast, composer, status, }: AppShellProps): React.ReactElement;
|
|
@@ -1,44 +1,9 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
|
|
3
|
-
* AppShell (spec §4.3) — the frame that makes R2 true.
|
|
4
|
-
*
|
|
5
|
-
* In `fullscreen` the root box has a FIXED height of `frameHeight(rows)` and the
|
|
6
|
-
* middle viewport is the only `flexGrow` child, so the bottom chrome (toast /
|
|
7
|
-
* composer / status bar) is physically the last thing on the screen from the
|
|
8
|
-
* very first frame — empty session, long session, mid-scroll, overlay open.
|
|
9
|
-
*
|
|
10
|
-
* In `inline` the same children are emitted in the same order with no height
|
|
11
|
-
* constraint, i.e. byte-for-byte the v0.2.0 document-flow behavior.
|
|
12
|
-
*/
|
|
13
|
-
import { useLayoutEffect, useRef } from 'react';
|
|
14
|
-
import { Box, measureElement } from 'ink';
|
|
2
|
+
import { Box } from 'ink';
|
|
15
3
|
import { frameHeight } from './frame.js';
|
|
16
|
-
|
|
17
|
-
export function AppShell({ mode, rows, cols, header, viewport, team, rail, strip, toast, composer, status, geometryRef, }) {
|
|
18
|
-
const bottomRef = useRef(null);
|
|
19
|
-
// DECLARED ABOVE THE INLINE EARLY RETURN, and it has to stay there: React
|
|
20
|
-
// keys hooks by call order, and a hook that only runs in one mode changes
|
|
21
|
-
// that order between modes.
|
|
22
|
-
//
|
|
23
|
-
// Ink computes yoga layout in `onRender`, which React runs BEFORE layout
|
|
24
|
-
// effects, so this reflects the commit that just happened — the same
|
|
25
|
-
// mechanism `ScrollViewport` already relies on.
|
|
26
|
-
useLayoutEffect(() => {
|
|
27
|
-
if (!geometryRef)
|
|
28
|
-
return;
|
|
29
|
-
if (mode === 'inline' || !bottomRef.current) {
|
|
30
|
-
geometryRef.current = UNMEASURED_GEOMETRY;
|
|
31
|
-
return;
|
|
32
|
-
}
|
|
33
|
-
geometryRef.current = {
|
|
34
|
-
// 1 in the alternate screen — see the note on `FrameGeometry.top`.
|
|
35
|
-
top: 1,
|
|
36
|
-
height: frameHeight(rows),
|
|
37
|
-
bottom: measureElement(bottomRef.current).height,
|
|
38
|
-
};
|
|
39
|
-
});
|
|
4
|
+
export function AppShell({ mode, rows, cols, header, viewport, team, rail, strip, toast, composer, status, }) {
|
|
40
5
|
if (mode === 'inline') {
|
|
41
6
|
return (_jsxs(Box, { flexDirection: "column", children: [header, viewport, strip, team, toast, composer, status] }));
|
|
42
7
|
}
|
|
43
|
-
return (_jsxs(Box, { flexDirection: "column", height: frameHeight(rows), width: cols, overflow: "hidden", children: [_jsx(Box, { flexDirection: "column", flexShrink: 0, children: header }), _jsxs(Box, { flexDirection: "row", flexGrow: 1, flexShrink: 1, overflow: "hidden", children: [_jsx(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, overflow: "hidden", children: viewport }), rail] }), _jsxs(Box, {
|
|
8
|
+
return (_jsxs(Box, { flexDirection: "column", height: frameHeight(rows), width: cols, overflow: "hidden", children: [_jsx(Box, { flexDirection: "column", flexShrink: 0, children: header }), _jsxs(Box, { flexDirection: "row", flexGrow: 1, flexShrink: 1, overflow: "hidden", children: [_jsx(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, overflow: "hidden", children: viewport }), rail] }), _jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [team, toast, composer, status] })] }));
|
|
44
9
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One transcript entry, wrapped so its rendered height reaches the height store
|
|
3
|
+
* (tui-render-performance L3).
|
|
4
|
+
*
|
|
5
|
+
* IT MUST NOT CONSTRAIN HEIGHT. The natural height IS the value being measured;
|
|
6
|
+
* a `height` prop here would make every entry report back whatever it was told,
|
|
7
|
+
* and the spacers would encode a fiction.
|
|
8
|
+
*
|
|
9
|
+
* Ink computes yoga layout in `onRender`, which React runs BEFORE layout
|
|
10
|
+
* effects, so the measurement below reflects the commit that just happened —
|
|
11
|
+
* the same mechanism `ScrollViewport.tsx:87-88` and `AppShell.tsx:100-102`
|
|
12
|
+
* already document and rely on.
|
|
13
|
+
*/
|
|
14
|
+
import React from 'react';
|
|
15
|
+
import type { HeightKey } from './virtual-window.js';
|
|
16
|
+
export interface MeasuredEntryProps {
|
|
17
|
+
heightKey: HeightKey;
|
|
18
|
+
onMeasure: (key: HeightKey, rows: number) => void;
|
|
19
|
+
children: React.ReactNode;
|
|
20
|
+
}
|
|
21
|
+
export declare function MeasuredEntry({ heightKey, onMeasure, children, }: MeasuredEntryProps): React.ReactElement;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* One transcript entry, wrapped so its rendered height reaches the height store
|
|
4
|
+
* (tui-render-performance L3).
|
|
5
|
+
*
|
|
6
|
+
* IT MUST NOT CONSTRAIN HEIGHT. The natural height IS the value being measured;
|
|
7
|
+
* a `height` prop here would make every entry report back whatever it was told,
|
|
8
|
+
* and the spacers would encode a fiction.
|
|
9
|
+
*
|
|
10
|
+
* Ink computes yoga layout in `onRender`, which React runs BEFORE layout
|
|
11
|
+
* effects, so the measurement below reflects the commit that just happened —
|
|
12
|
+
* the same mechanism `ScrollViewport.tsx:87-88` and `AppShell.tsx:100-102`
|
|
13
|
+
* already document and rely on.
|
|
14
|
+
*/
|
|
15
|
+
import { useLayoutEffect, useRef } from 'react';
|
|
16
|
+
import { Box, measureElement } from 'ink';
|
|
17
|
+
export function MeasuredEntry({ heightKey, onMeasure, children, }) {
|
|
18
|
+
const ref = useRef(null);
|
|
19
|
+
// No dependency array on purpose: the entry's content can change without the
|
|
20
|
+
// key changing identity in a way this component can see, and `report` is a
|
|
21
|
+
// no-op when the measurement is unchanged — so an unconditional effect costs
|
|
22
|
+
// one `getComputedHeight()` read and cannot miss a resize.
|
|
23
|
+
useLayoutEffect(() => {
|
|
24
|
+
const node = ref.current;
|
|
25
|
+
if (!node)
|
|
26
|
+
return;
|
|
27
|
+
onMeasure(heightKey, measureElement(node).height);
|
|
28
|
+
});
|
|
29
|
+
return (_jsx(Box, { ref: ref, flexDirection: "column", flexShrink: 0, children: children }));
|
|
30
|
+
}
|
|
@@ -13,9 +13,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
13
13
|
* measure → setState → re-render → measure loop whose convergence is provable
|
|
14
14
|
* but whose frame count is not — expensive inside a 33 ms streaming window.
|
|
15
15
|
*/
|
|
16
|
-
import { useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
16
|
+
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
17
17
|
import { Box, Text, measureElement, useStdout } from 'ink';
|
|
18
18
|
import { pickGlyphs } from '../glyphs.js';
|
|
19
|
+
import { ViewportGeometryContext } from './viewport-geometry.js';
|
|
19
20
|
import { applyScrollTimes, clampScroll, } from './scroll.js';
|
|
20
21
|
import { thumbRange } from './scroll-indicator.js';
|
|
21
22
|
import { MIN_INDICATOR_COLS, ScrollIndicator } from './ScrollIndicator.js';
|
|
@@ -81,7 +82,12 @@ export function ScrollViewport({ intent, pinToBottomNonce = 0, onScrolledLinesCh
|
|
|
81
82
|
// more than the affordance. The routing never reads this flag, which is what
|
|
82
83
|
// keeps the indicator independently revertible (§13-Q3).
|
|
83
84
|
const indicatorVisible = showScrollIndicator && cols >= MIN_INDICATOR_COLS;
|
|
84
|
-
|
|
85
|
+
// PUBLISHED, NOT LIFTED (tui-render-performance L3 / §3.3.2). The three
|
|
86
|
+
// numbers virtualisation needs are already owned here; a `useMemo`d context
|
|
87
|
+
// value hands them down without adding a second owner or a second measure
|
|
88
|
+
// pass, which is exactly what this component's header (P1-7) forbids.
|
|
89
|
+
const geometry = useMemo(() => ({ viewportRows: metrics.viewport, offset, contentRows: metrics.content }), [metrics.viewport, metrics.content, offset]);
|
|
90
|
+
return (_jsxs(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, overflow: "hidden", children: [_jsxs(Box, { flexDirection: "row", flexGrow: 1, flexShrink: 1, overflow: "hidden", children: [_jsx(Box, { ref: clipRef, flexDirection: "column", flexGrow: 1, flexShrink: 1, overflow: "hidden", children: _jsx(Box, { ref: innerRef, flexDirection: "column", flexShrink: 0, marginTop: -shiftUp, children: _jsx(ViewportGeometryContext.Provider, { value: geometry, children: children }) }) }), indicatorVisible && (_jsx(ScrollIndicator, { rows: metrics.viewport, thumb: thumbRange(metrics.viewport, metrics.content, offset), theme: theme, caps: caps }))] }), showHint && (
|
|
85
91
|
// Not an overlay — Ink has no absolute positioning or z-index. This takes
|
|
86
92
|
// the viewport's last row, trading one line of content for a hint that
|
|
87
93
|
// cannot drift. Cheaper than stacking with negative margins.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Viewport geometry, published downward (tui-render-performance L3).
|
|
3
|
+
*
|
|
4
|
+
* `ScrollViewport`'s header records (P1-7) why it — and not its parent — owns
|
|
5
|
+
* the metrics and the scroll offset: holding the offset in two places creates a
|
|
6
|
+
* measure -> setState -> re-render -> measure loop whose convergence is provable
|
|
7
|
+
* but whose frame count is not.
|
|
8
|
+
*
|
|
9
|
+
* Virtualisation needs those same three numbers one level DOWN, in
|
|
10
|
+
* `TranscriptList`. So the viewport PUBLISHES rather than lifting: a `useMemo`d
|
|
11
|
+
* context value, no new state, no second owner, no extra layout pass.
|
|
12
|
+
*/
|
|
13
|
+
export interface ViewportGeometry {
|
|
14
|
+
/** Visible height of the clipping box, in rows. `0` before the first measure. */
|
|
15
|
+
viewportRows: number;
|
|
16
|
+
/** Rows hidden BELOW the viewport (`layout/scroll.ts` semantics; V-4). */
|
|
17
|
+
offset: number;
|
|
18
|
+
/** Natural (unclipped) height of the content, in rows. */
|
|
19
|
+
contentRows: number;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The default is the UNMEASURED geometry, and consumers must treat it as such:
|
|
23
|
+
* a `TranscriptList` rendered outside a `ScrollViewport` (inline mode, and every
|
|
24
|
+
* test that mounts it bare) sees `viewportRows: 0`, which `selectWindow` reads
|
|
25
|
+
* as "first frame" and answers by pinning the live tail.
|
|
26
|
+
*/
|
|
27
|
+
export declare const ViewportGeometryContext: import("react").Context<ViewportGeometry>;
|
|
28
|
+
export declare function useViewportGeometry(): ViewportGeometry;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Viewport geometry, published downward (tui-render-performance L3).
|
|
3
|
+
*
|
|
4
|
+
* `ScrollViewport`'s header records (P1-7) why it — and not its parent — owns
|
|
5
|
+
* the metrics and the scroll offset: holding the offset in two places creates a
|
|
6
|
+
* measure -> setState -> re-render -> measure loop whose convergence is provable
|
|
7
|
+
* but whose frame count is not.
|
|
8
|
+
*
|
|
9
|
+
* Virtualisation needs those same three numbers one level DOWN, in
|
|
10
|
+
* `TranscriptList`. So the viewport PUBLISHES rather than lifting: a `useMemo`d
|
|
11
|
+
* context value, no new state, no second owner, no extra layout pass.
|
|
12
|
+
*/
|
|
13
|
+
import { createContext, useContext } from 'react';
|
|
14
|
+
/**
|
|
15
|
+
* The default is the UNMEASURED geometry, and consumers must treat it as such:
|
|
16
|
+
* a `TranscriptList` rendered outside a `ScrollViewport` (inline mode, and every
|
|
17
|
+
* test that mounts it bare) sees `viewportRows: 0`, which `selectWindow` reads
|
|
18
|
+
* as "first frame" and answers by pinning the live tail.
|
|
19
|
+
*/
|
|
20
|
+
export const ViewportGeometryContext = createContext({
|
|
21
|
+
viewportRows: 0,
|
|
22
|
+
offset: 0,
|
|
23
|
+
contentRows: 0,
|
|
24
|
+
});
|
|
25
|
+
export function useViewportGeometry() {
|
|
26
|
+
return useContext(ViewportGeometryContext);
|
|
27
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Viewport virtualisation maths (tui-render-performance L3 / R1). Pure,
|
|
3
|
+
* React-free, unit-tested.
|
|
4
|
+
*
|
|
5
|
+
* THE STRUCTURAL FIX. Ink's `renderNodeToOutput` has no early-out for clipped
|
|
6
|
+
* subtrees: for every `ink-text` node it runs `squashTextNodes`, `widestLine`
|
|
7
|
+
* and possibly `wrapText` — all O(chars) — and `Output.get()` then evaluates
|
|
8
|
+
* `widestLine` a SECOND time before it is allowed to skip an out-of-clip
|
|
9
|
+
* operation. A clipped 50 000-character entry therefore costs two full string
|
|
10
|
+
* scans per frame and buys nothing. Bounding the ENTRY COUNT (`transcriptWindow`)
|
|
11
|
+
* never bounded that, because one entry can be fifty thousand lines.
|
|
12
|
+
*
|
|
13
|
+
* So entries outside the visible band are not mounted at all. They are replaced
|
|
14
|
+
* by two spacer boxes whose heights come from a height cache, which makes the
|
|
15
|
+
* per-frame cost O(visible rows) instead of O(characters ever produced).
|
|
16
|
+
*
|
|
17
|
+
* V-4 — `offset` counts rows FROM THE BOTTOM (`layout/scroll.ts:1-13`), and that
|
|
18
|
+
* is what makes virtualisation safe here for free: a height correction on an
|
|
19
|
+
* entry ABOVE the viewport changes the content height and therefore
|
|
20
|
+
* `overflowLines`, while `offset` — and hence what the user is looking at — is
|
|
21
|
+
* unchanged. No scroll-anchoring compensation pass is required. Do NOT
|
|
22
|
+
* "simplify" `offset` to count from the top.
|
|
23
|
+
*/
|
|
24
|
+
import type { Entry } from '../../agent/reducer.js';
|
|
25
|
+
import type { DensityMode } from '../density.js';
|
|
26
|
+
export declare const VIRTUAL_LIMITS: {
|
|
27
|
+
/**
|
|
28
|
+
* Entries kept mounted on each side of the visible band.
|
|
29
|
+
*
|
|
30
|
+
* V-2 — must stay `>= 2`, so a one-row scroll can never expose an entry whose
|
|
31
|
+
* height has never been measured at the very edge of the viewport.
|
|
32
|
+
*/
|
|
33
|
+
readonly overscan: 2;
|
|
34
|
+
/** Height-cache ceiling (LRU). Four thousand numbers, not four thousand strings. */
|
|
35
|
+
readonly heightEntries: 4096;
|
|
36
|
+
};
|
|
37
|
+
export type HeightKey = string;
|
|
38
|
+
/**
|
|
39
|
+
* A cheap content version: it changes exactly when the rendered content does.
|
|
40
|
+
*
|
|
41
|
+
* DERIVED, NEVER HASHED. Hashing a 256 KiB string once per entry per frame would
|
|
42
|
+
* recreate the very cost this module exists to remove, so every field below is
|
|
43
|
+
* an O(1) read of a length or a scalar.
|
|
44
|
+
*
|
|
45
|
+
* I-L3-1 — this must change whenever the rendered output changes. The only lossy
|
|
46
|
+
* case is a same-LENGTH text replacement, which the reducer cannot produce: all
|
|
47
|
+
* text fields are append-only (`appendBounded` only ever elides the middle,
|
|
48
|
+
* which changes the length). A future NON-APPEND mutation MUST extend the
|
|
49
|
+
* revision, or the entry freezes at a stale height AND a stale rendered subtree
|
|
50
|
+
* with nothing anywhere reporting it. `virtual-window.test.ts` enumerates every
|
|
51
|
+
* kind against every mutating action.
|
|
52
|
+
*/
|
|
53
|
+
export declare function entryRevision(entry: Entry): string;
|
|
54
|
+
/**
|
|
55
|
+
* The height-cache key.
|
|
56
|
+
*
|
|
57
|
+
* V-5 — `cols` is part of the key, so a resize invalidates every entry. The
|
|
58
|
+
* frame after a resize renders from estimates and re-measures lazily: one
|
|
59
|
+
* reflow, no crash, no stale layout.
|
|
60
|
+
*
|
|
61
|
+
* `flags` carries the render-affecting switches that are neither per-entry nor
|
|
62
|
+
* part of `density` — today only `thinkingVisible` (Ctrl+T). It is a parameter
|
|
63
|
+
* rather than a fifth positional scalar so a future switch costs one character
|
|
64
|
+
* at the call site instead of a signature change at four.
|
|
65
|
+
*/
|
|
66
|
+
export declare function heightKey(entry: Entry, cols: number, expanded: boolean, density: DensityMode, flags?: string): HeightKey;
|
|
67
|
+
/**
|
|
68
|
+
* Pure UPPER-BOUND estimate of an entry's height, used until a real measurement
|
|
69
|
+
* exists.
|
|
70
|
+
*
|
|
71
|
+
* Upper bound rather than best guess, and deliberately so: an over-estimate
|
|
72
|
+
* makes the scroll position slightly conservative and self-corrects the frame
|
|
73
|
+
* after the entry is measured, while an under-estimate can put the newest output
|
|
74
|
+
* off the bottom of a viewport the user believes is pinned.
|
|
75
|
+
*/
|
|
76
|
+
export declare function estimateEntryRows(entry: Entry, cols: number, density: DensityMode, expanded: boolean, thinkingVisible?: boolean): number;
|
|
77
|
+
export interface WindowSelection {
|
|
78
|
+
/** First mounted entry, inclusive. */
|
|
79
|
+
startIndex: number;
|
|
80
|
+
/** One past the last mounted entry. */
|
|
81
|
+
endIndex: number;
|
|
82
|
+
/** Spacer rows above the mounted slice. */
|
|
83
|
+
leadingRows: number;
|
|
84
|
+
/** Spacer rows below the mounted slice. */
|
|
85
|
+
trailingRows: number;
|
|
86
|
+
/** Sum of every entry height, measured or estimated. */
|
|
87
|
+
totalRows: number;
|
|
88
|
+
}
|
|
89
|
+
export interface SelectWindowInput {
|
|
90
|
+
entries: readonly Entry[];
|
|
91
|
+
/** Measured, else estimated; never 0 for a real entry. */
|
|
92
|
+
heightOf: (index: number) => number;
|
|
93
|
+
viewportRows: number;
|
|
94
|
+
/** Rows hidden BELOW the viewport — the existing `scroll.ts` semantic (V-4). */
|
|
95
|
+
offset: number;
|
|
96
|
+
/** `>= 2` (V-2). Lower values are raised rather than honoured. */
|
|
97
|
+
overscan: number;
|
|
98
|
+
}
|
|
99
|
+
/** Choose which entries to mount, and how tall the two spacers must be. */
|
|
100
|
+
export declare function selectWindow(input: SelectWindowInput): WindowSelection;
|