@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.
Files changed (117) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +107 -77
  3. package/dist/agent/controller.d.ts +11 -7
  4. package/dist/agent/controller.js +35 -48
  5. package/dist/agent/headless.d.ts +2 -1
  6. package/dist/agent/prompt-options.d.ts +15 -0
  7. package/dist/agent/prompt-options.js +1 -0
  8. package/dist/agent/reducer.d.ts +7 -113
  9. package/dist/agent/reducer.js +0 -62
  10. package/dist/cli.js +111 -182
  11. package/dist/commands/builtins.js +3 -1
  12. package/dist/commands/perf.d.ts +32 -2
  13. package/dist/commands/perf.js +32 -2
  14. package/dist/commands/registry.d.ts +4 -5
  15. package/dist/config/env.js +0 -130
  16. package/dist/config/load.d.ts +0 -2
  17. package/dist/config/load.js +2 -31
  18. package/dist/config/schema.d.ts +1 -14
  19. package/dist/config/schema.js +0 -1
  20. package/dist/config/store.js +2 -0
  21. package/dist/input/interrupt-gesture.d.ts +18 -0
  22. package/dist/input/interrupt-gesture.js +19 -0
  23. package/dist/input/pointer-router.d.ts +11 -0
  24. package/dist/input/pointer-router.js +14 -0
  25. package/dist/logging/install.d.ts +2 -2
  26. package/dist/logging/install.js +2 -2
  27. package/dist/todo/follow-through.d.ts +0 -4
  28. package/dist/todo/follow-through.js +0 -4
  29. package/dist/todo/limits.d.ts +0 -10
  30. package/dist/todo/limits.js +0 -10
  31. package/dist/todo/prompt.d.ts +0 -15
  32. package/dist/todo/store.d.ts +8 -13
  33. package/dist/todo/store.js +13 -14
  34. package/dist/todo/types.d.ts +2 -1
  35. package/dist/ui/ActivityLine.d.ts +26 -34
  36. package/dist/ui/ActivityLine.js +30 -19
  37. package/dist/ui/App.d.ts +3 -41
  38. package/dist/ui/App.js +223 -247
  39. package/dist/ui/BottomStatusRow.d.ts +1 -3
  40. package/dist/ui/BottomStatusRow.js +4 -8
  41. package/dist/ui/Composer.d.ts +38 -5
  42. package/dist/ui/Composer.js +45 -23
  43. package/dist/ui/Header.d.ts +2 -16
  44. package/dist/ui/Header.js +3 -18
  45. package/dist/ui/Logo.d.ts +1 -17
  46. package/dist/ui/Logo.js +0 -16
  47. package/dist/ui/ModeChip.d.ts +7 -18
  48. package/dist/ui/ModeChip.js +9 -0
  49. package/dist/ui/PromptCaret.d.ts +11 -0
  50. package/dist/ui/PromptCaret.js +31 -0
  51. package/dist/ui/PromptInput.d.ts +10 -33
  52. package/dist/ui/PromptInput.js +37 -20
  53. package/dist/ui/StatusBar.d.ts +1 -11
  54. package/dist/ui/StatusBar.js +14 -8
  55. package/dist/ui/ToastStack.d.ts +2 -18
  56. package/dist/ui/ToastStack.js +4 -11
  57. package/dist/ui/Transcript.d.ts +5 -71
  58. package/dist/ui/Transcript.js +10 -199
  59. package/dist/ui/composer-rows.js +50 -9
  60. package/dist/ui/entries/AssistantEntry.d.ts +0 -23
  61. package/dist/ui/entries/AssistantEntry.js +3 -4
  62. package/dist/ui/entries/DiffView.d.ts +1 -7
  63. package/dist/ui/entries/DiffView.js +2 -2
  64. package/dist/ui/entries/ToolCard.d.ts +0 -6
  65. package/dist/ui/entries/ToolCard.js +4 -13
  66. package/dist/ui/frame-differ.d.ts +13 -0
  67. package/dist/ui/frame-differ.js +37 -24
  68. package/dist/ui/frame-observer.d.ts +17 -0
  69. package/dist/ui/frame-observer.js +81 -0
  70. package/dist/ui/frame-parser.d.ts +7 -0
  71. package/dist/ui/frame-parser.js +18 -0
  72. package/dist/ui/ink-runtime.d.ts +1 -0
  73. package/dist/ui/ink-runtime.js +21 -0
  74. package/dist/ui/layout/AppShell.d.ts +4 -58
  75. package/dist/ui/layout/AppShell.js +3 -6
  76. package/dist/ui/layout/OverlayFrame.d.ts +1 -25
  77. package/dist/ui/layout/OverlayFrame.js +0 -24
  78. package/dist/ui/layout/ScrollIndicator.d.ts +4 -3
  79. package/dist/ui/layout/ScrollIndicator.js +4 -4
  80. package/dist/ui/layout/ScrollViewport.d.ts +16 -66
  81. package/dist/ui/layout/ScrollViewport.js +195 -185
  82. package/dist/ui/layout/budget.d.ts +3 -51
  83. package/dist/ui/layout/budget.js +4 -53
  84. package/dist/ui/layout/document-layout.d.ts +13 -0
  85. package/dist/ui/layout/document-layout.js +11 -0
  86. package/dist/ui/layout/follow-state.d.ts +1 -0
  87. package/dist/ui/layout/follow-state.js +1 -3
  88. package/dist/ui/layout/frame.d.ts +0 -22
  89. package/dist/ui/layout/frame.js +0 -36
  90. package/dist/ui/layout/scroll-indicator.d.ts +1 -1
  91. package/dist/ui/layout/scroll-indicator.js +3 -2
  92. package/dist/ui/layout/scrollbar-geometry.d.ts +17 -0
  93. package/dist/ui/layout/scrollbar-geometry.js +13 -0
  94. package/dist/ui/layout/todo-layout.d.ts +2 -3
  95. package/dist/ui/layout/todo-layout.js +9 -9
  96. package/dist/ui/layout/viewport-geometry.d.ts +2 -1
  97. package/dist/ui/layout/viewport-geometry.js +2 -1
  98. package/dist/ui/layout/virtual-window.d.ts +1 -0
  99. package/dist/ui/layout/virtual-window.js +1 -1
  100. package/dist/ui/live-clamp.d.ts +1 -29
  101. package/dist/ui/live-clamp.js +1 -29
  102. package/dist/ui/overlays/HelpOverlay.js +6 -2
  103. package/dist/ui/run-status-row.d.ts +61 -0
  104. package/dist/ui/run-status-row.js +70 -0
  105. package/dist/ui/screen.d.ts +1 -1
  106. package/dist/ui/screen.js +4 -6
  107. package/dist/ui/scrollbar-controller.d.ts +30 -0
  108. package/dist/ui/scrollbar-controller.js +124 -0
  109. package/dist/ui/transcript-text.d.ts +0 -8
  110. package/dist/ui/transcript-text.js +0 -8
  111. package/dist/ui/use-startup-notices.d.ts +1 -57
  112. package/dist/ui/use-startup-notices.js +3 -54
  113. package/dist/ui/use-wheel-routing.d.ts +3 -1
  114. package/dist/ui/use-wheel-routing.js +6 -0
  115. package/package.json +2 -2
  116. package/dist/ui/TodoStrip.d.ts +0 -36
  117. package/dist/ui/TodoStrip.js +0 -32
@@ -25,6 +25,12 @@
25
25
  * trailing `CSI K` removes only the residue a longer previous line left, and
26
26
  * the trailing SGR reset before it stops a BCE terminal painting that residue
27
27
  * in whatever background colour the line ended inside.
28
+ * `CSI K` IS EMITTED ONLY WHEN THE LINE IS NARROWER THAN THE TERMINAL. A row
29
+ * that already fills every column (the unified scrollbar paints the last one)
30
+ * has no residue to clear, and on terminals that keep the cursor ON the last
31
+ * column after writing it (a pending-wrap flag rather than a virtual column
32
+ * N+1) the EL would erase the cell that was just written. `cols` unknown keeps
33
+ * the old always-EL behaviour (`paintLine`).
28
34
  * - I-5 · Every emitted batch ends parked on row `H+1`. Ink's own
29
35
  * `previousLineCount` is computed from the string IT produced and is
30
36
  * unaffected by what we actually wrote, so a later pass-through's
@@ -37,7 +43,10 @@
37
43
  * the guard being absent is a permanently misaligned screen while the failure
38
44
  * mode of it firing spuriously is one extra full repaint.
39
45
  */
46
+ import stringWidth from 'string-width';
47
+ import stripAnsi from 'strip-ansi';
40
48
  import { MIN_FULLSCREEN_ROWS } from './layout/frame.js';
49
+ import { matchInkErasePrefix } from './frame-parser.js';
41
50
  // ---------------------------------------------------------------------------
42
51
  // Constants — exact, so no call site ever re-derives an escape sequence.
43
52
  // ---------------------------------------------------------------------------
@@ -55,20 +64,25 @@ const ERASE_DOWN = `${CSI}J`;
55
64
  /** DEC private mode 2026 — begin/end synchronized update (F2, §3.3). */
56
65
  const SYNC_BEGIN = `${CSI}?2026h`;
57
66
  const SYNC_END = `${CSI}?2026l`;
67
+ function cursorTo(row) {
68
+ return `${CSI}${row};1H`;
69
+ }
58
70
  /**
59
- * `eraseLines(n)` for any n ≥ 1, anchored at the start of the chunk.
71
+ * One addressed row: `CUP · SGR0 · line · SGR0 [· CSI K]` (I-4).
60
72
  *
61
- * FALLBACK ONLY (P2-3). Whenever `prev !== null` the differ already knows the
62
- * exact prefix it expects — `eraseLinesPrefix(prev.length + 1)` is a
63
- * deterministic string — so the hot path is `chunk.startsWith(expected)`:
64
- * linear, unambiguous, and it collapses step 7's `erased !== prev.length + 1`
65
- * test into the same comparison. This regex is reached only on the first
66
- * diffable chunk after a session start or an `invalidate()`, where no
67
- * expectation exists yet.
73
+ * The trailing `CSI K` clears residue a longer previous line left behind, so it is
74
+ * only meaningful when the line is narrower than the terminal. A line that already
75
+ * fills `cols` columns ends with the cursor in the pending-wrap state, where some
76
+ * terminals run EL from the LAST column and erase the cell just written; those
77
+ * rows get no EL. Width is measured exactly as Ink measures it (`string-width` over
78
+ * the ANSI-stripped text) and only for rows that changed, so it costs at most one
79
+ * measurement per changed viewport row. `cols` unknown => the original always-EL.
68
80
  */
69
- const ERASE_PREFIX_RE = /^(?:\x1b\[2K(?:\x1b\[1A)?)+\x1b\[G/;
70
- function cursorTo(row) {
71
- return `${CSI}${row};1H`;
81
+ function paintLine(row, line, cols) {
82
+ const head = cursorTo(row) + SGR_RESET + line + SGR_RESET;
83
+ if (cols !== undefined && stringWidth(stripAnsi(line)) >= cols)
84
+ return head;
85
+ return head + ERASE_TO_EOL;
72
86
  }
73
87
  /**
74
88
  * Rebuild `ansiEscapes.eraseLines(count)` byte for byte.
@@ -88,9 +102,6 @@ export function eraseLinesPrefix(count) {
88
102
  }
89
103
  return out + CURSOR_LEFT;
90
104
  }
91
- function countErases(prefix) {
92
- return prefix.split(ERASE_LINE).length - 1;
93
- }
94
105
  export function createFrameDiffer(options) {
95
106
  /**
96
107
  * Last frame AS PAINTED — what is actually on the screen.
@@ -200,10 +211,7 @@ export function createFrameDiffer(options) {
200
211
  if (chunk.startsWith(expected))
201
212
  return { prefix: expected, erased: prev.length + 1 };
202
213
  }
203
- const m = ERASE_PREFIX_RE.exec(chunk);
204
- if (m === null)
205
- return null;
206
- return { prefix: m[0], erased: countErases(m[0]) };
214
+ return matchInkErasePrefix(chunk);
207
215
  };
208
216
  /**
209
217
  * Full repaint — the first diffable frame of a session, every invalidation
@@ -228,13 +236,18 @@ export function createFrameDiffer(options) {
228
236
  cursorTo(lines.length + 1);
229
237
  return emit(wrapSync(payload), lines, raw, lines.length, true);
230
238
  };
239
+ /** Terminal width for this paint, or `undefined` when unknown (I-4: always EL). */
240
+ const liveCols = () => {
241
+ const cols = options.cols?.();
242
+ return cols !== undefined && Number.isFinite(cols) ? cols : undefined;
243
+ };
231
244
  const diffRepaint = (lines, raw, previous) => {
232
245
  const out = [];
246
+ const cols = liveCols();
233
247
  for (let i = 0; i < lines.length; i += 1) {
234
248
  // I-4: paint, THEN clear the tail. Never `ERASE_LINE` first.
235
- if (lines[i] !== previous[i]) {
236
- out.push(cursorTo(i + 1) + SGR_RESET + lines[i] + SGR_RESET + ERASE_TO_EOL);
237
- }
249
+ if (lines[i] !== previous[i])
250
+ out.push(paintLine(i + 1, lines[i], cols));
238
251
  }
239
252
  // DEFENSIVE ONLY, not a live path (P2-4). `log-update.js:13-15` returns early
240
253
  // on an identical frame, so a chunk that reaches here differs by at least one
@@ -268,10 +281,10 @@ export function createFrameDiffer(options) {
268
281
  return '';
269
282
  const painted = applyDecorate(rawPrev);
270
283
  const out = [];
284
+ const cols = liveCols();
271
285
  for (let i = 0; i < painted.length; i += 1) {
272
- if (painted[i] !== prev[i]) {
273
- out.push(cursorTo(i + 1) + SGR_RESET + painted[i] + SGR_RESET + ERASE_TO_EOL);
274
- }
286
+ if (painted[i] !== prev[i])
287
+ out.push(paintLine(i + 1, painted[i], cols));
275
288
  }
276
289
  prev = painted;
277
290
  if (out.length === 0)
@@ -0,0 +1,17 @@
1
+ import type { ScrollbarBridge } from './scrollbar-controller.js';
2
+ export interface FrameSummary {
3
+ columns: number;
4
+ rows: number;
5
+ trackTop: number;
6
+ trackRows: number;
7
+ }
8
+ /** Observe before differ transformation, but acknowledge only after downstream write returns. */
9
+ export declare function createFrameObserver(options: {
10
+ stdout: NodeJS.WriteStream;
11
+ terminal: NodeJS.WriteStream;
12
+ scrollbar: ScrollbarBridge;
13
+ }): {
14
+ stdout: NodeJS.WriteStream;
15
+ dispose(): void;
16
+ invalidate(): void;
17
+ };
@@ -0,0 +1,81 @@
1
+ import stripAnsi from 'strip-ansi';
2
+ import stringWidth from 'string-width';
3
+ import { parseInkFrame } from './frame-parser.js';
4
+ import { pickGlyphs } from './glyphs.js';
5
+ function summarizeFrame(chunk, rows, cols) {
6
+ const lines = typeof chunk === 'string' ? parseInkFrame(chunk) : null;
7
+ if (!lines || rows < 12 || cols < 40 || lines.length !== rows - 1)
8
+ return null;
9
+ const unicode = pickGlyphs({ unicode: true, colorLevel: 0 });
10
+ const cells = new Set(['|', '#', unicode.scrollTrack, unicode.scrollThumb]);
11
+ const plain = lines.map((line) => stripAnsi(line));
12
+ const track = plain.slice(1, rows - 3);
13
+ if (plain.some((line) => stringWidth(line) > cols) ||
14
+ !track.every((line) => stringWidth(line) === cols && cells.has(line.slice(-1))))
15
+ return null;
16
+ return { columns: cols, rows, trackTop: 2, trackRows: rows - 4 };
17
+ }
18
+ /** Observe before differ transformation, but acknowledge only after downstream write returns. */
19
+ export function createFrameObserver(options) {
20
+ const { stdout, terminal, scrollbar } = options;
21
+ let summary = null;
22
+ let disposed = false;
23
+ let resizeTimer = null;
24
+ const invalidate = () => { summary = null; scrollbar.invalidate(); };
25
+ const onResize = () => {
26
+ invalidate();
27
+ if (resizeTimer)
28
+ clearTimeout(resizeTimer);
29
+ // Ink suppresses identical frames when a resize burst returns to its starting size.
30
+ // Wait past the 50 ms React size debounce, then request a normal redraw if needed.
31
+ resizeTimer = setTimeout(() => {
32
+ resizeTimer = null;
33
+ if (!disposed && !scrollbar.frameReady)
34
+ scrollbar.requestRedraw?.();
35
+ }, 60);
36
+ resizeTimer.unref?.();
37
+ };
38
+ const match = () => {
39
+ const g = scrollbar.geometry;
40
+ scrollbar.frameReady = !!summary && !!g && summary.columns === terminal.columns &&
41
+ summary.rows === terminal.rows && g.trackCol === summary.columns &&
42
+ g.trackTop === summary.trackTop && g.trackRows === summary.trackRows;
43
+ };
44
+ const observe = (chunk) => {
45
+ summary = summarizeFrame(chunk, terminal.rows, terminal.columns);
46
+ if (!summary) {
47
+ invalidate();
48
+ return;
49
+ }
50
+ match();
51
+ };
52
+ scrollbar.onGeometry = match;
53
+ terminal.on('resize', onResize);
54
+ const bound = new Map();
55
+ const proxy = new Proxy(stdout, {
56
+ get(target, prop) {
57
+ if (prop === 'write')
58
+ return (...args) => {
59
+ const result = Reflect.apply(target.write, target, args);
60
+ if (!disposed)
61
+ observe(args[0]);
62
+ return result;
63
+ };
64
+ const value = Reflect.get(target, prop, target);
65
+ if (typeof value !== 'function')
66
+ return value;
67
+ if (!bound.has(prop))
68
+ bound.set(prop, value.bind(target));
69
+ return bound.get(prop);
70
+ },
71
+ });
72
+ return { stdout: proxy, invalidate, dispose: () => {
73
+ disposed = true;
74
+ if (resizeTimer)
75
+ clearTimeout(resizeTimer);
76
+ resizeTimer = null;
77
+ terminal.off('resize', onResize);
78
+ scrollbar.onGeometry = undefined;
79
+ invalidate();
80
+ } };
81
+ }
@@ -0,0 +1,7 @@
1
+ /** Recognize Ink's erase-lines prefix without accepting arbitrary control writes. */
2
+ export declare function matchInkErasePrefix(chunk: string): {
3
+ prefix: string;
4
+ erased: number;
5
+ } | null;
6
+ /** Parse complete frames, including Ink's prefixless initial frame. */
7
+ export declare function parseInkFrame(chunk: string): string[] | null;
@@ -0,0 +1,18 @@
1
+ /** Recognize Ink's erase-lines prefix without accepting arbitrary control writes. */
2
+ export function matchInkErasePrefix(chunk) {
3
+ const match = /^(?:\x1b\[2K(?:\x1b\[1A)?)+\x1b\[G/.exec(chunk);
4
+ return match ? { prefix: match[0], erased: match[0].split('\x1b[2K').length - 1 } : null;
5
+ }
6
+ /** Parse complete frames, including Ink's prefixless initial frame. */
7
+ export function parseInkFrame(chunk) {
8
+ const prefix = matchInkErasePrefix(chunk);
9
+ const body = chunk.slice(prefix?.prefix.length ?? 0);
10
+ if (!body.endsWith('\n'))
11
+ return null;
12
+ // SGR and OSC-8 may decorate text; cursor movement is never frame content.
13
+ const controls = body.replace(/\x1b\[[0-9;:]*m/g, '')
14
+ .replace(/\x1b\]8;[^\x07\x1b]*(?:\x07|\x1b\\)/g, '');
15
+ if (/[\x00-\x08\x0b-\x1f\x7f]/.test(controls))
16
+ return null;
17
+ return body.slice(0, -1).split('\n');
18
+ }
@@ -0,0 +1 @@
1
+ export declare const render: (node: import("react").ReactNode, options?: NodeJS.WriteStream | import("ink").RenderOptions) => import("ink").Instance;
@@ -0,0 +1,21 @@
1
+ import process from 'node:process';
2
+ /** Ink 5 caches CI detection at import time and otherwise paints only on exit. */
3
+ async function loadInk() {
4
+ if (!process.stdin.isTTY || !process.stdout.isTTY)
5
+ return import('ink');
6
+ const originalCi = process.env.CI;
7
+ try {
8
+ // A real interactive terminal always needs live frames and resize events.
9
+ // `0` also overrides CI_* / CONTINUOUS_INTEGRATION in Ink's detector.
10
+ process.env.CI = '0';
11
+ return await import('ink');
12
+ }
13
+ finally {
14
+ // Commands, subprocesses and the updater must see the original environment.
15
+ if (originalCi === undefined)
16
+ delete process.env.CI;
17
+ else
18
+ process.env.CI = originalCi;
19
+ }
20
+ }
21
+ export const { render } = await loadInk();
@@ -1,67 +1,13 @@
1
- /**
2
- * AppShell (spec §4.3) — the frame that makes R2 true.
3
- *
4
- * In `fullscreen` the root box has a FIXED height of `frameHeight(rows)` and the
5
- * middle viewport is the only `flexGrow` child, so the bottom chrome (toast /
6
- * composer / status bar) is physically the last thing on the screen from the
7
- * very first frame — empty session, long session, mid-scroll, overlay open.
8
- *
9
- * In `inline` the same children are emitted in the same order with no height
10
- * constraint, i.e. byte-for-byte the v0.2.0 document-flow behavior.
11
- */
1
+ /** Stable fullscreen slots preserve the editor during overlays and small resizes. */
12
2
  import React from 'react';
13
- import { type RenderMode } from './frame.js';
14
3
  export interface AppShellProps {
15
- mode: RenderMode;
16
4
  rows: number;
17
5
  cols: number;
18
6
  header: React.ReactNode;
19
7
  viewport: React.ReactNode;
20
- /**
21
- * The live subagent roster (team-subagents §6.1), or nothing.
22
- *
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.
27
- */
28
- team?: React.ReactNode;
29
- /**
30
- * The right-hand region of the MIDDLE band (todo-plan-execution §3.9), or
31
- * nothing.
32
- *
33
- * NAMED FOR ITS POSITION, NOT ITS CONTENT (D-18): this file is a layout
34
- * primitive and has no business knowing what a todo is. `team` above already
35
- * leaks content vocabulary in here; there is no reason to repeat it.
36
- *
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.
42
- *
43
- * The INLINE branch does not include it at all — a structural guarantee rather
44
- * than a promise that the caller passes `null` (non-goal 4).
45
- */
46
- rail?: React.ReactNode;
47
- /**
48
- * The one-row plan strip for INLINE mode (todo-plan-followthrough §3.7), or
49
- * nothing.
50
- *
51
- * THE STRUCTURAL MIRROR OF `rail` ABOVE, in the opposite direction: the
52
- * FULL-SCREEN branch does not reference it at all. The reason is the same one
53
- * `rail`'s inline omission records — a prop that is merely SUPPOSED to be null
54
- * in one mode becomes non-null eventually, and then the rail and the strip are
55
- * both on screen showing the same list (I-6). Fullscreen has the rail; that is
56
- * the whole design.
57
- *
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.
61
- */
62
- strip?: React.ReactNode;
63
8
  toast: React.ReactNode;
64
- composer: React.ReactNode;
65
9
  status: React.ReactNode;
10
+ inactive?: boolean;
11
+ placeholder?: React.ReactNode;
66
12
  }
67
- export declare function AppShell({ mode, rows, cols, header, viewport, team, rail, strip, toast, composer, status, }: AppShellProps): React.ReactElement;
13
+ export declare function AppShell({ rows, cols, header, viewport, toast, status, inactive, placeholder, }: AppShellProps): React.ReactElement;
@@ -1,9 +1,6 @@
1
- import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { Box } from 'ink';
3
3
  import { frameHeight } from './frame.js';
4
- export function AppShell({ mode, rows, cols, header, viewport, team, rail, strip, toast, composer, status, }) {
5
- if (mode === 'inline') {
6
- return (_jsxs(Box, { flexDirection: "column", children: [header, viewport, strip, team, toast, composer, status] }));
7
- }
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] })] }));
4
+ export function AppShell({ rows, cols, header, viewport, toast, status, inactive = false, placeholder, }) {
5
+ return (_jsxs(Box, { flexDirection: "column", height: frameHeight(rows), width: cols, overflow: "hidden", children: [_jsxs(Box, { display: inactive ? 'none' : 'flex', flexDirection: "column", height: frameHeight(rows), width: cols, overflow: "hidden", children: [_jsx(Box, { height: 1, flexShrink: 0, overflow: "hidden", children: header }), _jsx(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, overflow: "hidden", children: viewport }), _jsx(Box, { height: 1, flexShrink: 0, overflow: "hidden", children: toast }), _jsx(Box, { height: 1, flexShrink: 0, overflow: "hidden", children: status })] }), _jsx(Box, { display: inactive ? 'flex' : 'none', height: frameHeight(rows), overflow: "hidden", children: placeholder })] }));
9
6
  }
@@ -1,27 +1,3 @@
1
- /**
2
- * OverlayFrame (spec §4.4) — the shared shell for all four overlays.
3
- *
4
- * TWO CONTENT MODES, and the distinction is load-bearing:
5
- *
6
- * - mode A (`rows`): the caller hands over an array of elements that are each
7
- * exactly one row and `wrap="truncate"`. The frame slices them and OWNS the
8
- * scroll keys. Used by Help and Settings.
9
- *
10
- * - mode B (`children`): the content manages itself. The frame draws the title,
11
- * the border and the footer, and touches NOTHING else — no slicing, no
12
- * arrow-key handling, no position indicator.
13
- *
14
- * Mode B exists because of `ink-select-input`: it already windows itself via
15
- * `limit` and registers its own `useInput` with `isFocused` defaulting to true.
16
- * Ink dispatches a key to EVERY mounted input handler, so a frame that also
17
- * claimed the arrow keys would move the model picker's selection twice per press
18
- * — breaking a feature that already works. Slicing it by element would be worse
19
- * still, since it is a single component rather than a list of rows.
20
- *
21
- * `maxRows` may be `Infinity` (the inline render path, which has no fixed frame
22
- * and therefore no height to fit): the frame then renders everything and shows
23
- * no position indicator.
24
- */
25
1
  import React from 'react';
26
2
  import type { Theme } from '../theme.js';
27
3
  import type { TermCapabilities } from '../capabilities.js';
@@ -29,7 +5,7 @@ interface OverlayFrameBase {
29
5
  title: string;
30
6
  /** Footer text. Written by the CALLER so each overlay names its own keys. */
31
7
  hint: string;
32
- /** Viewport rows the overlay may occupy; `Infinity` in inline mode. */
8
+ /** Viewport rows the overlay may occupy. */
33
9
  maxRows: number;
34
10
  cols?: number;
35
11
  theme: Theme;
@@ -1,28 +1,4 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
- /**
3
- * OverlayFrame (spec §4.4) — the shared shell for all four overlays.
4
- *
5
- * TWO CONTENT MODES, and the distinction is load-bearing:
6
- *
7
- * - mode A (`rows`): the caller hands over an array of elements that are each
8
- * exactly one row and `wrap="truncate"`. The frame slices them and OWNS the
9
- * scroll keys. Used by Help and Settings.
10
- *
11
- * - mode B (`children`): the content manages itself. The frame draws the title,
12
- * the border and the footer, and touches NOTHING else — no slicing, no
13
- * arrow-key handling, no position indicator.
14
- *
15
- * Mode B exists because of `ink-select-input`: it already windows itself via
16
- * `limit` and registers its own `useInput` with `isFocused` defaulting to true.
17
- * Ink dispatches a key to EVERY mounted input handler, so a frame that also
18
- * claimed the arrow keys would move the model picker's selection twice per press
19
- * — breaking a feature that already works. Slicing it by element would be worse
20
- * still, since it is a single component rather than a list of rows.
21
- *
22
- * `maxRows` may be `Infinity` (the inline render path, which has no fixed frame
23
- * and therefore no height to fit): the frame then renders everything and shows
24
- * no position indicator.
25
- */
26
2
  import { useEffect } from 'react';
27
3
  import { Box, Text } from 'ink';
28
4
  import { pickGlyphs } from '../glyphs.js';
@@ -18,14 +18,15 @@ import React from 'react';
18
18
  import type { Theme } from '../theme.js';
19
19
  import type { TermCapabilities } from '../capabilities.js';
20
20
  import type { ThumbRange } from './scroll-indicator.js';
21
- /** Below this width a column of content is worth more than the affordance. */
22
- export declare const MIN_INDICATOR_COLS = 50;
21
+ /** Smallest supported fullscreen width; every usable chat frame reserves the column. */
22
+ export declare const MIN_INDICATOR_COLS = 40;
23
23
  export interface ScrollIndicatorProps {
24
24
  /** Viewport height in rows, as measured by `ScrollViewport`. */
25
25
  rows: number;
26
26
  /** `null` when the content fits — the rail is drawn as bare track. */
27
27
  thumb: ThumbRange | null;
28
+ dragging?: boolean;
28
29
  theme: Theme;
29
30
  caps: TermCapabilities;
30
31
  }
31
- export declare function ScrollIndicator({ rows, thumb, theme, caps, }: ScrollIndicatorProps): React.ReactElement;
32
+ export declare function ScrollIndicator({ rows, thumb, dragging, theme, caps, }: ScrollIndicatorProps): React.ReactElement;
@@ -1,15 +1,15 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { Box, Text } from 'ink';
3
3
  import { pickGlyphs } from '../glyphs.js';
4
- /** Below this width a column of content is worth more than the affordance. */
5
- export const MIN_INDICATOR_COLS = 50;
6
- export function ScrollIndicator({ rows, thumb, theme, caps, }) {
4
+ /** Smallest supported fullscreen width; every usable chat frame reserves the column. */
5
+ export const MIN_INDICATOR_COLS = 40;
6
+ export function ScrollIndicator({ rows, thumb, dragging = false, theme, caps, }) {
7
7
  const glyphs = pickGlyphs(caps);
8
8
  const count = Math.max(0, Math.floor(rows) || 0);
9
9
  const cells = [];
10
10
  for (let i = 0; i < count; i += 1) {
11
11
  const onThumb = !!thumb && i >= thumb.start && i < thumb.start + thumb.size;
12
- cells.push(_jsx(Text, { color: onThumb ? theme.primary : theme.border, children: onThumb ? glyphs.scrollThumb : glyphs.scrollTrack }, i));
12
+ cells.push(_jsx(Text, { color: onThumb ? theme.primary : theme.muted, bold: onThumb && dragging, children: onThumb ? glyphs.scrollThumb : glyphs.scrollTrack }, i));
13
13
  }
14
14
  return (_jsx(Box, { flexDirection: "column", flexShrink: 0, width: 1, children: cells }));
15
15
  }
@@ -1,88 +1,38 @@
1
- /**
2
- * ScrollViewport (spec §4.5) — a self-drawn scrolling window built from Ink's
3
- * negative `marginTop` + `overflow: hidden` + `measureElement`.
4
- *
5
- * Once the frame occupies the whole screen, `<Static>` history is pushed above
6
- * the frame where only one line is visible, so native scrollback stops being a
7
- * usable mechanism and the app has to own scrolling. That is why this exists.
8
- *
9
- * SINGLE SOURCE OF TRUTH (review P1-7): both the metrics and the scroll offset
10
- * live here. The parent only pushes an *intent* down and receives one derived
11
- * *display* number back. Holding the offset in the parent as well would create a
12
- * measure → setState → re-render → measure loop whose convergence is provable
13
- * but whose frame count is not — expensive inside a 33 ms streaming window.
14
- *
15
- * AS OF tui-selection-and-scroll-follow THIS IS AN ADAPTER, NOT A POLICY OWNER.
16
- * The two follow rules are pure and live in `follow-state.ts`; what stays here is
17
- * measure → compute a tail delta → apply → arm a timer. `scroll.ts` is
18
- * unchanged, so every existing scroll test keeps passing (R-10).
19
- *
20
- * The "N new lines · PgDn" hint used to take this viewport's LAST ROW. It now
21
- * renders as a chip inside the composer's bordered input row (§4.2), which costs
22
- * zero rows and removes the feedback loop where the hint's own presence shrank
23
- * the viewport, which changed `overflowLines`, which was the number the hint
24
- * displayed. The data still leaves through `onScrolledLinesChange`.
25
- */
26
1
  import React from 'react';
2
+ import { type DOMElement } from 'ink';
27
3
  import type { Theme } from '../theme.js';
28
4
  import type { TermCapabilities } from '../capabilities.js';
5
+ import { type ScrollbarBridge } from '../scrollbar-controller.js';
29
6
  import { type ScrollIntent } from './scroll.js';
30
7
  import { type TailSink } from './follow-state.js';
31
8
  export interface ScrollViewportProps {
32
- /**
33
- * Scroll intent. `nonce` is monotonic so pressing the same key twice re-fires.
34
- * `repeat` folds a coalesced wheel burst into one state update (defaults 1).
35
- */
36
9
  intent?: {
37
10
  kind: ScrollIntent;
38
11
  nonce: number;
39
12
  repeat?: number;
40
13
  };
41
- /** Bumped by the parent to force a pin to the newest output (e.g. on submit). */
42
14
  pinToBottomNonce?: number;
43
- /** Derived display value for the status bar's `↑N`. Must be referentially stable. */
15
+ resetKey?: unknown;
44
16
  onScrolledLinesChange?: (lines: number) => void;
45
- /**
46
- * Rows hidden ABOVE the window, published on every change (§4.3.3).
47
- *
48
- * PUBLISHED, NOT LIFTED — the same shape as `onScrolledLinesChange`, so there
49
- * is still exactly one owner of the offset. `shiftUp` changes exactly when the
50
- * rows on screen move, which is the property the selection's clear-list was
51
- * approximating by enumeration (I-9 / P1-4). Must be referentially stable.
52
- */
53
- onViewportShiftChange?: (shiftUp: number) => void;
54
- /**
55
- * The transcript's tail row counter (§4.3.1a), written during ITS render and
56
- * therefore already current when this component's layout effect runs.
57
- *
58
- * ABSENT ⇒ NO ANCHORING AT ALL, deliberately: every existing caller and every
59
- * existing test that does not supply it behaves exactly as it did before this
60
- * feature, because `tailDelta` is then permanently 0.
61
- */
17
+ onViewportShiftChange?: (shift: number) => void;
62
18
  tailRowsRef?: TailSink;
63
- /**
64
- * True while a selection drag is in progress (§4.4). It anchors the content
65
- * even from a pinned viewport, and it suspends the resume timer.
66
- */
67
19
  hold?: boolean;
68
- /** Idle delay before returning to the newest line, in ms; `0` disables (AC-9). */
69
20
  resumeMs?: number;
70
- /** Reserve the right-edge scroll-position rail (mouse-wheel §4.8). */
71
21
  showScrollIndicator?: boolean;
72
- /**
73
- * The viewport's own width, when it is narrower than the terminal
74
- * (todo-plan-execution §3.9 / C-7).
75
- *
76
- * This component is the ONLY one inside the viewport subtree that reads
77
- * `stdout.columns` directly, and the place it uses the number — the
78
- * `MIN_INDICATOR_COLS` gate — is about how much room THIS BOX has, not how
79
- * wide the terminal is. Once the todo rail takes its responsive share of the frame those two
80
- * stop being the same number. Optional so every existing caller (and every
81
- * test) is unchanged.
82
- */
83
22
  cols?: number;
23
+ footer?: React.ReactNode;
24
+ rail?: React.ReactNode;
25
+ overlay?: React.ReactNode;
26
+ active?: boolean;
27
+ scrollbar?: ScrollbarBridge;
28
+ composerRef?: React.RefObject<DOMElement>;
29
+ onComposerVisibilityChange?: (visible: boolean) => void;
30
+ /** The run status row above the input box (tui-scrollbar-edge-and-run-row). */
31
+ activityRef?: React.RefObject<DOMElement>;
32
+ onActivityVisibilityChange?: (visible: boolean) => void;
84
33
  theme: Theme;
85
34
  caps: TermCapabilities;
86
35
  children: React.ReactNode;
87
36
  }
88
- export declare function ScrollViewport({ intent, pinToBottomNonce, onScrolledLinesChange, onViewportShiftChange, tailRowsRef, hold, resumeMs, showScrollIndicator, cols: colsProp, theme, caps, children, }: ScrollViewportProps): React.ReactElement;
37
+ /** One offset for messages, padding and editor; hidden trees keep their local state. */
38
+ export declare function ScrollViewport(props: ScrollViewportProps): React.ReactElement;