@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.
Files changed (156) hide show
  1. package/CHANGELOG.md +356 -0
  2. package/README.md +1429 -1105
  3. package/dist/agent/coalesce.d.ts +13 -0
  4. package/dist/agent/coalesce.js +50 -11
  5. package/dist/agent/controller.d.ts +141 -2
  6. package/dist/agent/controller.js +396 -4
  7. package/dist/agent/entry-limits.d.ts +65 -0
  8. package/dist/agent/entry-limits.js +133 -0
  9. package/dist/agent/headless.d.ts +8 -0
  10. package/dist/agent/headless.js +40 -0
  11. package/dist/agent/reducer.d.ts +253 -3
  12. package/dist/agent/reducer.js +553 -28
  13. package/dist/agent/retry-view.d.ts +68 -0
  14. package/dist/agent/retry-view.js +95 -0
  15. package/dist/agent/system-prompt.d.ts +11 -0
  16. package/dist/agent/system-prompt.js +2 -0
  17. package/dist/cli.js +122 -4
  18. package/dist/commands/builtins.d.ts +1 -1
  19. package/dist/commands/builtins.js +313 -2
  20. package/dist/commands/perf.d.ts +45 -0
  21. package/dist/commands/perf.js +78 -0
  22. package/dist/config/cli-commands.d.ts +53 -2
  23. package/dist/config/cli-commands.js +136 -3
  24. package/dist/config/env.d.ts +6 -1
  25. package/dist/config/env.js +131 -1
  26. package/dist/config/load.d.ts +63 -0
  27. package/dist/config/load.js +125 -2
  28. package/dist/config/schema.d.ts +273 -10
  29. package/dist/config/schema.js +250 -7
  30. package/dist/config/store.js +20 -1
  31. package/dist/fast/critique.d.ts +43 -0
  32. package/dist/fast/critique.js +62 -0
  33. package/dist/fast/digest.d.ts +42 -0
  34. package/dist/fast/digest.js +85 -0
  35. package/dist/fast/frames.d.ts +99 -0
  36. package/dist/fast/frames.js +180 -0
  37. package/dist/fast/limits.d.ts +98 -0
  38. package/dist/fast/limits.js +98 -0
  39. package/dist/fast/prompt.d.ts +72 -0
  40. package/dist/fast/prompt.js +94 -0
  41. package/dist/fast/resolve.d.ts +43 -0
  42. package/dist/fast/resolve.js +105 -0
  43. package/dist/fast/review-call.d.ts +99 -0
  44. package/dist/fast/review-call.js +130 -0
  45. package/dist/fast/reviewer.d.ts +248 -0
  46. package/dist/fast/reviewer.js +664 -0
  47. package/dist/fast/types.d.ts +128 -0
  48. package/dist/fast/types.js +14 -0
  49. package/dist/fast/wiring.d.ts +170 -0
  50. package/dist/fast/wiring.js +316 -0
  51. package/dist/logging/install.d.ts +15 -1
  52. package/dist/logging/install.js +37 -4
  53. package/dist/logging/logger.d.ts +1 -1
  54. package/dist/session/persist.js +57 -1
  55. package/dist/team/normalize.d.ts +26 -1
  56. package/dist/team/normalize.js +23 -3
  57. package/dist/team/prompt.d.ts +11 -0
  58. package/dist/team/prompt.js +10 -0
  59. package/dist/team/report.d.ts +18 -0
  60. package/dist/team/report.js +27 -1
  61. package/dist/team/runtime.d.ts +13 -2
  62. package/dist/team/runtime.js +33 -6
  63. package/dist/team/subagent.d.ts +19 -1
  64. package/dist/team/subagent.js +72 -10
  65. package/dist/team/task-tool.d.ts +26 -0
  66. package/dist/team/task-tool.js +48 -4
  67. package/dist/team/types.d.ts +80 -1
  68. package/dist/tools/bash-tool.js +19 -3
  69. package/dist/tools/diff.d.ts +25 -5
  70. package/dist/tools/diff.js +72 -41
  71. package/dist/tools/file-change-store.d.ts +34 -0
  72. package/dist/tools/file-change-store.js +57 -0
  73. package/dist/tools/fs-tools.d.ts +26 -0
  74. package/dist/tools/fs-tools.js +62 -5
  75. package/dist/tools/index.d.ts +31 -0
  76. package/dist/tools/index.js +7 -1
  77. package/dist/tools/patch.d.ts +115 -0
  78. package/dist/tools/patch.js +453 -0
  79. package/dist/tools/terminal-output.d.ts +107 -0
  80. package/dist/tools/terminal-output.js +198 -0
  81. package/dist/tools/tool-output-store.d.ts +68 -0
  82. package/dist/tools/tool-output-store.js +102 -0
  83. package/dist/ui/ActivityLine.d.ts +48 -0
  84. package/dist/ui/ActivityLine.js +22 -0
  85. package/dist/ui/App.d.ts +1 -4
  86. package/dist/ui/App.js +357 -44
  87. package/dist/ui/BottomStatusRow.d.ts +43 -0
  88. package/dist/ui/BottomStatusRow.js +14 -0
  89. package/dist/ui/Composer.d.ts +1 -6
  90. package/dist/ui/Composer.js +2 -2
  91. package/dist/ui/Markdown.d.ts +7 -1
  92. package/dist/ui/Markdown.js +15 -48
  93. package/dist/ui/PromptInput.d.ts +1 -15
  94. package/dist/ui/PromptInput.js +1 -38
  95. package/dist/ui/StatusBar.d.ts +50 -0
  96. package/dist/ui/StatusBar.js +7 -3
  97. package/dist/ui/TeamPanel.d.ts +5 -4
  98. package/dist/ui/TeamPanel.js +1 -1
  99. package/dist/ui/TodoPanel.d.ts +5 -4
  100. package/dist/ui/Transcript.d.ts +80 -5
  101. package/dist/ui/Transcript.js +179 -11
  102. package/dist/ui/activity-phrases.d.ts +33 -0
  103. package/dist/ui/activity-phrases.js +50 -0
  104. package/dist/ui/entries/AssistantEntry.d.ts +38 -1
  105. package/dist/ui/entries/AssistantEntry.js +34 -3
  106. package/dist/ui/entries/DiffView.d.ts +67 -0
  107. package/dist/ui/entries/DiffView.js +103 -0
  108. package/dist/ui/entries/FastCard.d.ts +53 -0
  109. package/dist/ui/entries/FastCard.js +87 -0
  110. package/dist/ui/entries/RetryCard.d.ts +56 -0
  111. package/dist/ui/entries/RetryCard.js +60 -0
  112. package/dist/ui/entries/TeamCard.d.ts +9 -1
  113. package/dist/ui/entries/TeamCard.js +28 -2
  114. package/dist/ui/entries/TodoCard.d.ts +9 -1
  115. package/dist/ui/entries/TodoCard.js +23 -1
  116. package/dist/ui/entries/ToolCard.d.ts +50 -1
  117. package/dist/ui/entries/ToolCard.js +104 -7
  118. package/dist/ui/entries/ToolPreview.d.ts +2 -2
  119. package/dist/ui/entries/UserEntry.d.ts +9 -1
  120. package/dist/ui/entries/UserEntry.js +17 -1
  121. package/dist/ui/exit-snapshot.d.ts +7 -0
  122. package/dist/ui/glyphs.d.ts +9 -0
  123. package/dist/ui/glyphs.js +2 -0
  124. package/dist/ui/layout/AppShell.d.ts +13 -22
  125. package/dist/ui/layout/AppShell.js +3 -38
  126. package/dist/ui/layout/MeasuredEntry.d.ts +21 -0
  127. package/dist/ui/layout/MeasuredEntry.js +30 -0
  128. package/dist/ui/layout/ScrollViewport.js +8 -2
  129. package/dist/ui/layout/viewport-geometry.d.ts +28 -0
  130. package/dist/ui/layout/viewport-geometry.js +27 -0
  131. package/dist/ui/layout/virtual-window.d.ts +100 -0
  132. package/dist/ui/layout/virtual-window.js +308 -0
  133. package/dist/ui/live-clamp.d.ts +42 -0
  134. package/dist/ui/live-clamp.js +74 -0
  135. package/dist/ui/markdown-blocks.d.ts +25 -1
  136. package/dist/ui/markdown-blocks.js +43 -0
  137. package/dist/ui/overlays/HelpOverlay.js +16 -2
  138. package/dist/ui/overlays/SettingsScreen.d.ts +82 -1
  139. package/dist/ui/overlays/SettingsScreen.js +147 -1
  140. package/dist/ui/render-cache.d.ts +81 -0
  141. package/dist/ui/render-cache.js +195 -0
  142. package/dist/ui/render-governor.d.ts +54 -0
  143. package/dist/ui/render-governor.js +78 -0
  144. package/dist/ui/transcript-text.d.ts +11 -0
  145. package/dist/ui/transcript-text.js +32 -0
  146. package/dist/ui/use-height-store.d.ts +31 -0
  147. package/dist/ui/use-height-store.js +76 -0
  148. package/dist/ui/use-render-governor.d.ts +39 -0
  149. package/dist/ui/use-render-governor.js +57 -0
  150. package/dist/ui/use-startup-notices.d.ts +9 -1
  151. package/dist/ui/use-startup-notices.js +35 -1
  152. package/dist/ui/use-wheel-routing.d.ts +16 -32
  153. package/dist/ui/use-wheel-routing.js +21 -38
  154. package/package.json +2 -2
  155. package/dist/ui/layout/regions.d.ts +0 -55
  156. 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
- export function UserEntry({ text, theme }) {
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;
@@ -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 INSIDE `bottomRef` IN THE FULL-SCREEN BRANCH, and it has to
25
- * (I-10): `geometryRef.current.bottom` is MEASURED from that box, and the
26
- * wheel router uses it to decide whether a notch belongs to the transcript or
27
- * to prompt history. A panel rendered above the measured box would make the
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 NOT BE RENDERED INSIDE `bottomRef` (C-6 / I-4). That box is MEASURED
40
- * and the wheel router reads the composer band boundary from it; a column
41
- * added there would inflate the measurement and start routing transcript
42
- * scrolls into prompt history.
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`, i.e. OUTSIDE the measured box in the
60
- * other branch — but inline has no measured box at all (`geometryRef` is set
61
- * to `UNMEASURED_GEOMETRY` there), so there is nothing here to invalidate.
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, geometryRef, }: AppShellProps): React.ReactElement;
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
- import { UNMEASURED_GEOMETRY } from './regions.js';
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, { ref: bottomRef, flexDirection: "column", flexShrink: 0, children: [team, toast, composer, status] })] }));
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
- 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: children }) }), indicatorVisible && (_jsx(ScrollIndicator, { rows: metrics.viewport, thumb: thumbRange(metrics.viewport, metrics.content, offset), theme: theme, caps: caps }))] }), showHint && (
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;