@aragon-agent/cli 0.6.8 → 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 (78) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +38 -37
  3. package/dist/agent/controller.d.ts +1 -1
  4. package/dist/agent/reducer.d.ts +7 -113
  5. package/dist/agent/reducer.js +0 -62
  6. package/dist/cli.js +48 -149
  7. package/dist/commands/perf.d.ts +32 -2
  8. package/dist/commands/perf.js +32 -2
  9. package/dist/commands/registry.d.ts +2 -2
  10. package/dist/config/env.js +0 -130
  11. package/dist/config/load.d.ts +0 -2
  12. package/dist/config/load.js +2 -31
  13. package/dist/config/schema.d.ts +1 -14
  14. package/dist/config/schema.js +0 -1
  15. package/dist/config/store.js +2 -0
  16. package/dist/logging/install.d.ts +2 -2
  17. package/dist/logging/install.js +2 -2
  18. package/dist/todo/follow-through.d.ts +0 -4
  19. package/dist/todo/follow-through.js +0 -4
  20. package/dist/todo/limits.d.ts +0 -10
  21. package/dist/todo/limits.js +0 -10
  22. package/dist/todo/prompt.d.ts +0 -15
  23. package/dist/ui/ActivityLine.d.ts +26 -34
  24. package/dist/ui/ActivityLine.js +30 -19
  25. package/dist/ui/App.d.ts +1 -41
  26. package/dist/ui/App.js +66 -128
  27. package/dist/ui/BottomStatusRow.d.ts +1 -3
  28. package/dist/ui/BottomStatusRow.js +4 -8
  29. package/dist/ui/Composer.d.ts +28 -1
  30. package/dist/ui/Composer.js +43 -17
  31. package/dist/ui/Header.d.ts +2 -16
  32. package/dist/ui/Header.js +3 -18
  33. package/dist/ui/Logo.d.ts +1 -17
  34. package/dist/ui/Logo.js +0 -16
  35. package/dist/ui/ModeChip.d.ts +7 -18
  36. package/dist/ui/ModeChip.js +9 -0
  37. package/dist/ui/PromptInput.d.ts +2 -28
  38. package/dist/ui/PromptInput.js +4 -7
  39. package/dist/ui/StatusBar.d.ts +0 -11
  40. package/dist/ui/ToastStack.d.ts +2 -18
  41. package/dist/ui/ToastStack.js +4 -11
  42. package/dist/ui/Transcript.d.ts +5 -71
  43. package/dist/ui/Transcript.js +9 -199
  44. package/dist/ui/entries/AssistantEntry.d.ts +0 -23
  45. package/dist/ui/entries/AssistantEntry.js +3 -4
  46. package/dist/ui/entries/DiffView.d.ts +1 -7
  47. package/dist/ui/entries/DiffView.js +2 -2
  48. package/dist/ui/entries/ToolCard.d.ts +0 -6
  49. package/dist/ui/entries/ToolCard.js +4 -13
  50. package/dist/ui/frame-differ.d.ts +13 -0
  51. package/dist/ui/frame-differ.js +36 -6
  52. package/dist/ui/ink-runtime.d.ts +1 -0
  53. package/dist/ui/ink-runtime.js +21 -0
  54. package/dist/ui/layout/AppShell.d.ts +1 -10
  55. package/dist/ui/layout/AppShell.js +2 -5
  56. package/dist/ui/layout/OverlayFrame.d.ts +1 -25
  57. package/dist/ui/layout/OverlayFrame.js +0 -24
  58. package/dist/ui/layout/ScrollViewport.d.ts +3 -0
  59. package/dist/ui/layout/ScrollViewport.js +17 -5
  60. package/dist/ui/layout/frame.d.ts +0 -22
  61. package/dist/ui/layout/frame.js +0 -36
  62. package/dist/ui/layout/todo-layout.d.ts +1 -3
  63. package/dist/ui/layout/todo-layout.js +7 -8
  64. package/dist/ui/layout/viewport-geometry.d.ts +1 -1
  65. package/dist/ui/layout/viewport-geometry.js +1 -1
  66. package/dist/ui/live-clamp.d.ts +1 -29
  67. package/dist/ui/live-clamp.js +1 -29
  68. package/dist/ui/overlays/HelpOverlay.js +1 -0
  69. package/dist/ui/run-status-row.d.ts +61 -0
  70. package/dist/ui/run-status-row.js +70 -0
  71. package/dist/ui/transcript-text.d.ts +0 -8
  72. package/dist/ui/transcript-text.js +0 -8
  73. package/dist/ui/use-startup-notices.d.ts +1 -57
  74. package/dist/ui/use-startup-notices.js +3 -54
  75. package/dist/ui/use-wheel-routing.d.ts +1 -1
  76. package/package.json +2 -2
  77. package/dist/ui/TodoStrip.d.ts +0 -36
  78. package/dist/ui/TodoStrip.js +0 -32
@@ -1,33 +1,8 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- /**
3
- * Transcript (spec §3.8). Splits the entry list into a settled prefix rendered
4
- * once into Ink's `<Static>` (native terminal scrollback, never re-rendered)
5
- * and a live tail rendered normally. This kills per-token full re-renders and
6
- * flicker on long/fast sessions.
7
- *
8
- * `computeSettledCount` is pure and exported for `transcript-static.test.ts`.
9
- * The settled boundary is held MONOTONIC at render time: a previously-settled
10
- * entry never leaves `<Static>` (which cannot un-print it) even if it later
11
- * becomes expanded, so the live tail can never duplicate a scrolled-off entry.
12
- *
13
- * `EntryView` is shared by both render paths ON PURPOSE (§4.3 / §12). Forking it
14
- * to freeze inline's v0.2.0 output would mean writing every future entry-layer
15
- * change twice and watching the two drift; inline is a degradation path, not a
16
- * parallel product. What inline still guarantees is its GEOMETRY contract — no
17
- * fixed frame, `<Static>` keeps the settled history, no private ANSI — not its
18
- * pixel-for-pixel appearance.
19
- *
20
- * Two things changed with tui-render-performance:
21
- * - `EntryView` is `React.memo`'d with an explicit comparator (L2 / R3), which
22
- * is only worth anything because `mapEntry` already preserves object
23
- * identity for untouched entries;
24
- * - `TranscriptList` (the FULL-SCREEN body) mounts only the entries inside the
25
- * viewport and replaces the rest with two spacer boxes (L3 / R1), and the
26
- * inline `Transcript` clamps its live region so `ink.js:121` is unreachable
27
- * (L5 / R6).
28
- */
29
- import React, { useCallback, useRef } from 'react';
30
- import { Box, Static, Text } from 'ink';
2
+ /** Virtualized full-screen transcript and entry cards. */
3
+ /** Virtualized full-screen transcript and shared entry cards. */
4
+ import React, { useCallback } from 'react';
5
+ import { Box, Text } from 'ink';
31
6
  import Spinner from 'ink-spinner';
32
7
  import { pickGlyphs, toolGlyph } from './glyphs.js';
33
8
  import { separationRows } from './density.js';
@@ -45,98 +20,6 @@ import { MeasuredEntry } from './layout/MeasuredEntry.js';
45
20
  import { useViewportGeometry } from './layout/viewport-geometry.js';
46
21
  import { estimateEntryRows, heightKey, selectWindow, VIRTUAL_LIMITS, } from './layout/virtual-window.js';
47
22
  import { advanceTailRows } from './layout/follow-state.js';
48
- import { useTerminalSize } from './layout/useTerminalSize.js';
49
- /** Keep the last K entries in the live region so a mutating tail stays hot. */
50
- const LIVE_TAIL = 1;
51
- /**
52
- * Rows the inline live region leaves for the composer, the prompt echo and the
53
- * one-row margin Ink needs before `outputHeight >= stdout.rows` (I-L5-1).
54
- */
55
- const INLINE_LIVE_MARGIN = 4;
56
- /** Floor on the clamp, so a very short terminal still shows something useful. */
57
- const INLINE_LIVE_MIN_ROWS = 4;
58
- /**
59
- * Largest prefix length P such that `entries[0..P)` are all terminal, none is
60
- * within the last `LIVE_TAIL` entries, and none is currently expanded. Pure and
61
- * non-decreasing as entries terminalize (expanding a card can lower it — the
62
- * component clamps with a monotonic high-water mark to avoid Static duplication).
63
- */
64
- export function computeSettledCount(entries, expandedToolIds) {
65
- const limit = entries.length - LIVE_TAIL;
66
- let count = 0;
67
- for (let i = 0; i < entries.length; i += 1) {
68
- if (i >= limit)
69
- break;
70
- const e = entries[i];
71
- if (expandedToolIds[e.id])
72
- break;
73
- if (e.kind === 'assistant' && e.streaming)
74
- break;
75
- if (e.kind === 'tool' && e.status !== 'done' && e.status !== 'error')
76
- break;
77
- // A live dispatch is not settled: its `runs` are rewritten on every
78
- // `teamUpdate`, and `<Static>` cannot un-print what it has already drawn.
79
- // The boundary is held MONOTONIC above, which is also why a `team` entry
80
- // that never settles would be re-rendered on every frame for the rest of the
81
- // session — the failure `session/persist.ts` normalizes away on load (P1-5).
82
- if (e.kind === 'team' && e.active)
83
- break;
84
- // A live todo card is not settled either: it is rewritten on every
85
- // `todoUpdate` for the whole turn, and `<Static>` cannot un-print what it has
86
- // already drawn (C-5 / I-6). `session/persist.ts` forces `live: false` on
87
- // load, which is the other half — a card that never settles is re-rendered
88
- // on every frame for the rest of the session.
89
- if (e.kind === 'todo' && e.live)
90
- break;
91
- // Nor is a retry card that is still counting down or still in flight: it is
92
- // rewritten on every `retryScheduled` / `retryAttempt` and once a second by
93
- // `retryTick`, and `<Static>` cannot un-print what it has already drawn.
94
- // `session/persist.ts` forces a loaded card to `interrupted`, which is the
95
- // other half — a card that never settles is re-rendered on every frame for
96
- // the rest of the session (R-10).
97
- if (e.kind === 'retry' && (e.phase === 'waiting' || e.phase === 'retrying'))
98
- break;
99
- // A live review card is not settled either: it is rewritten once, when the
100
- // call comes back, and `<Static>` cannot un-print what it has already drawn
101
- // (C-5). `session/persist.ts` forces `live: false` on load, which is the
102
- // other half.
103
- if (e.kind === 'fast' && e.live)
104
- break;
105
- // Nor is a compaction card whose call is still open: it is rewritten once,
106
- // when the engine reports its verdict, and `<Static>` cannot un-print what it
107
- // has already drawn (C-8). `session/persist.ts` forces `live: false` on load,
108
- // which is the other half.
109
- if (e.kind === 'compaction' && e.live)
110
- break;
111
- /**
112
- * A SERVICE BLOCKS ONLY WHILE IT IS `starting` (D-11 / P0-4).
113
- *
114
- * Every clause above is bounded by an OPERATION — a dispatch, a turn, one
115
- * LLM call, one countdown — which is why each of them can safely pin this
116
- * prefix scan: the wait is short and it ends. A service is bounded by the
117
- * USER'S INTENT. A dev server left up for an hour is the normal case, not
118
- * the pathological one, so treating it like the other five would pin the
119
- * boundary at the card and re-render the whole transcript from there on
120
- * EVERY FRAME, FOREVER, on exactly the long sessions where it is least
121
- * likely to be noticed.
122
- *
123
- * `starting` is different, and it is different for the reason that makes
124
- * every other clause here legitimate: it is bounded by `readyTimeoutMs`,
125
- * after which the supervisor calls the service `running` whatever happened.
126
- *
127
- * The cost of the rest is that a printed card stops mirroring live state —
128
- * which is what a transcript IS. A terminal transition appends a NEW one-row
129
- * entry rather than rewriting a card `<Static>` has already drawn, and live
130
- * state lives in the status chip, `/bg` and `bash_output`. Weakening this to
131
- * "any non-terminal service blocks" is the change D-11 exists to prevent;
132
- * see condition 5 of the design's review verdict before making it.
133
- */
134
- if (e.kind === 'service' && e.status === 'starting')
135
- break;
136
- count = i + 1;
137
- }
138
- return count;
139
- }
140
23
  /**
141
24
  * The value `EntryView` should receive for `nowSec` on THIS entry.
142
25
  *
@@ -168,7 +51,7 @@ function noticeSymbol(level, theme) {
168
51
  return theme.symbols.warn;
169
52
  return theme.symbols.info;
170
53
  }
171
- function EntryViewImpl({ entry, prev, expanded, thinkingVisible, reducedMotion, density, mode, theme, caps, liveClampRows, nowSec, }) {
54
+ function EntryViewImpl({ entry, prev, expanded, thinkingVisible, reducedMotion, density, theme, caps, nowSec, }) {
172
55
  const glyphs = pickGlyphs(caps);
173
56
  const separation = separationRows(prev, entry, density);
174
57
  const frame = (glyph, color, children) => (_jsx(EntryFrame, { glyph: glyph, color: color, continuation: glyphs.railVertical, separation: separation, spacerColor: theme.border, children: children }));
@@ -176,32 +59,14 @@ function EntryViewImpl({ entry, prev, expanded, thinkingVisible, reducedMotion,
176
59
  case 'user':
177
60
  return frame(glyphs.user, theme.user, _jsx(UserEntry, { text: entry.text, theme: theme, caps: caps }));
178
61
  case 'assistant': {
179
- // While text streams, the spinner IS the role marker. Braille dots are
180
- // both an animation and a Unicode-only glyph, so reduced motion and an
181
- // ASCII terminal fall back to the same static marker (P2-11 / A-10).
182
- //
183
- // `reducedMotion` ARRIVES PRE-WIDENED FROM `App`: it is now
184
- // `cfg.reducedMotion || activityVisible`, so this marker is static for the
185
- // whole of every run and the activity row above the composer carries the
186
- // one animation (single-spinner-while-running D-2). Nothing here changes —
187
- // the expression already meant "do not animate here".
188
- //
189
- // THAT IS SAFE FOR `<Static>` BY CONSTRUCTION, not by luck: a settled
190
- // prefix is printed once and can never be re-printed, so a boolean that
191
- // changed how an already-printed entry renders would tear the history.
192
- // `computeSettledCount` below breaks on EVERY condition that makes an
193
- // entry animate (`:84` streaming, `:85` running tool, `:91` active team,
194
- // `:104` retry, `:109` live fast), so every animating entry is in the live
195
- // region and every entry in `<Static>` already has `animate === false` on
196
- // both sides of the flip. Weakening a break clause breaks that proof.
197
62
  const animate = entry.streaming && !reducedMotion && caps.unicode;
198
63
  return frame(animate ? _jsx(Spinner, { type: "dots" }) : glyphs.assistant, theme.primary, _jsx(AssistantEntry, { text: entry.text, thinking: entry.thinking, thinkingOpen: entry.thinkingOpen, thinkingVisible: thinkingVisible, streaming: entry.streaming, aborted: entry.aborted,
199
64
  // `EntryViewImpl` hands each renderer EXPLICIT SCALAR PROPS, so
200
65
  // nothing on the entry reaches a component by itself (P1-6b).
201
- thinkingMs: entry.thinkingMs, revealable: mode === 'fullscreen', liveClampRows: liveClampRows, theme: theme, caps: caps }));
66
+ thinkingMs: entry.thinkingMs, theme: theme, caps: caps }));
202
67
  }
203
68
  case 'tool':
204
- return frame(toolEntryGlyph(entry.name, caps), statusColor(entry.status, theme), _jsx(ToolCard, { name: entry.name, label: entry.label, argsRaw: entry.argsRaw, args: entry.args, status: entry.status, preview: entry.preview, patch: entry.patch, durationMs: entry.durationMs, isError: entry.isError, expanded: expanded, reducedMotion: reducedMotion, liveClampRows: liveClampRows,
69
+ return frame(toolEntryGlyph(entry.name, caps), statusColor(entry.status, theme), _jsx(ToolCard, { name: entry.name, label: entry.label, argsRaw: entry.argsRaw, args: entry.args, status: entry.status, preview: entry.preview, patch: entry.patch, durationMs: entry.durationMs, isError: entry.isError, expanded: expanded, reducedMotion: reducedMotion,
205
70
  // `entry.live` AND NOT `entry.live ?? []` (P2-4): `ToolCard`'s default
206
71
  // comparator sees a fresh `[]` as a changed prop, which is why that
207
72
  // file already keeps a shared `EMPTY_LINES`.
@@ -263,10 +128,8 @@ export const EntryView = React.memo(EntryViewImpl, (a, b) => {
263
128
  a.thinkingVisible === b.thinkingVisible &&
264
129
  a.reducedMotion === b.reducedMotion &&
265
130
  a.density === b.density &&
266
- a.mode === b.mode &&
267
131
  a.theme === b.theme &&
268
132
  a.caps === b.caps &&
269
- a.liveClampRows === b.liveClampRows &&
270
133
  /*
271
134
  * ELEVENTH TERM, AND OMITTING IT IS THE TRAP I-L2-1 DESCRIBES ONE COMMENT UP
272
135
  * (P1-3). This comparator is a CLOSED LIST: a prop absent from it changes
@@ -279,27 +142,7 @@ export const EntryView = React.memo(EntryViewImpl, (a, b) => {
279
142
  */
280
143
  a.nowSec === b.nowSec);
281
144
  });
282
- /**
283
- * Full-screen transcript body — the same entry renderers with NO `<Static>`.
284
- *
285
- * `<Static>` prints above the live frame; once the frame is `rows - 1` tall
286
- * there is exactly one visible line up there, so the mechanism stops being a
287
- * history view and starts being a leak. Full-screen therefore owns its history
288
- * inside `ScrollViewport` instead.
289
- *
290
- * `computeSettledCount` is deliberately NOT called here: its only job is to keep
291
- * an entry that already reached `<Static>` from flowing back into the live
292
- * region, and with no Static there is nothing to protect. The settled high-water
293
- * mark feeds nothing else — `/save` serializes `entries` in full — so leaving it
294
- * un-advanced has no side effect.
295
- *
296
- * VIRTUALISED (L3 / R1). `windowSize` is now the SCROLL HORIZON — how far back
297
- * the user can scroll — and no longer the rendering budget, because entries
298
- * outside the viewport are not mounted at all. What bounds the frame is
299
- * `selectWindow`, and the two spacers are what keep the scroll maths honest
300
- * while they are absent.
301
- */
302
- export function TranscriptList({ entries, expandedToolIds, thinkingVisible, reducedMotion, density, mode, nowSec, theme, caps, windowSize, cols, heights, mountedSink, tailSink, }) {
145
+ export function TranscriptList({ entries, expandedToolIds, thinkingVisible, reducedMotion, density, nowSec, theme, caps, windowSize, cols, heights, mountedSink, tailSink, }) {
303
146
  const size = Math.max(1, Math.floor(windowSize));
304
147
  const visible = entries.length > size ? entries.slice(-size) : entries;
305
148
  const collapsed = entries.length - visible.length;
@@ -352,39 +195,6 @@ export function TranscriptList({ entries, expandedToolIds, thinkingVisible, redu
352
195
  }
353
196
  return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [collapsed > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.hintFg ?? theme.muted, children: [glyphs.ellipsis, " ", collapsed, " earlier ", collapsed === 1 ? 'entry' : 'entries', " collapsed (/save exports the full session)"] })), selection.leadingRows > 0 && (_jsx(Box, { flexShrink: 0, height: selection.leadingRows })), slice.map((entry, i) => {
354
197
  const index = selection.startIndex + i;
355
- return (_jsx(MeasuredEntry, { heightKey: keyOf(entry), onMeasure: report, children: _jsx(EntryView, { entry: entry, prev: index > 0 ? visible[index - 1] : undefined, expanded: !!expandedToolIds[entry.id], thinkingVisible: thinkingVisible, reducedMotion: reducedMotion, density: density, mode: mode, nowSec: nowSecFor(entry, nowSec), theme: theme, caps: caps }) }, entry.id));
198
+ return (_jsx(MeasuredEntry, { heightKey: keyOf(entry), onMeasure: report, children: _jsx(EntryView, { entry: entry, prev: index > 0 ? visible[index - 1] : undefined, expanded: !!expandedToolIds[entry.id], thinkingVisible: thinkingVisible, reducedMotion: reducedMotion, density: density, nowSec: nowSecFor(entry, nowSec), theme: theme, caps: caps }) }, entry.id));
356
199
  }), selection.trailingRows > 0 && (_jsx(Box, { flexShrink: 0, height: selection.trailingRows }))] }));
357
200
  }
358
- export function Transcript({ entries, expandedToolIds, thinkingVisible, reducedMotion, density, mode, nowSec, theme, caps, }) {
359
- const highWater = useRef(0);
360
- const prevLen = useRef(0);
361
- const { rows } = useTerminalSize();
362
- const raw = computeSettledCount(entries, expandedToolIds);
363
- // The transcript shrank (clear / reset / restore) — drop the high-water mark.
364
- if (entries.length < prevLen.current)
365
- highWater.current = 0;
366
- prevLen.current = entries.length;
367
- const settled = Math.min(entries.length, Math.max(highWater.current, raw));
368
- highWater.current = settled;
369
- const settledEntries = entries.slice(0, settled);
370
- const liveEntries = entries.slice(settled);
371
- // I-L5-1 — the live region must stay strictly below `stdout.rows`, or Ink
372
- // takes the `ink.js:118-123` branch and writes `clearTerminal +
373
- // fullStaticOutput + output` on EVERY subsequent frame for the rest of the
374
- // session.
375
- //
376
- // `LIVE_TAIL` is 1, so the common case is the design's `rows - 4` less the one
377
- // row the clamp marker costs. THE DIVISION IS NOT DECORATION: the settled
378
- // boundary is held back by an expanded card, a running tool, a live dispatch
379
- // or a live todo list, and in every one of those cases the region holds
380
- // SEVERAL entries at once — a flat per-entry ceiling of `rows - 4` would then
381
- // overrun by a multiple of itself. `- 1` pays for each entry's marker row.
382
- //
383
- // The residual: an entry contributes at least its header row whatever the
384
- // clamp says, so the bound holds while the live region holds fewer than about
385
- // `rows / 2` entries. Bounding it unconditionally would mean refusing to draw
386
- // a live entry at all, which is worse than a tall frame.
387
- const liveBudget = Math.max(INLINE_LIVE_MIN_ROWS, rows - INLINE_LIVE_MARGIN);
388
- const liveClampRows = Math.max(1, Math.floor(liveBudget / Math.max(1, liveEntries.length)) - 1);
389
- return (_jsxs(Box, { flexDirection: "column", children: [_jsx(Static, { items: settledEntries, children: (entry, index) => (_jsx(EntryView, { entry: entry, prev: index > 0 ? settledEntries[index - 1] : undefined, expanded: !!expandedToolIds[entry.id], thinkingVisible: thinkingVisible, reducedMotion: reducedMotion, density: density, mode: mode, theme: theme, caps: caps }, entry.id)) }), _jsx(Box, { flexDirection: "column", children: liveEntries.map((entry, i) => (_jsx(EntryView, { entry: entry, prev: i > 0 ? liveEntries[i - 1] : entries[settled - 1], expanded: !!expandedToolIds[entry.id], thinkingVisible: thinkingVisible, reducedMotion: reducedMotion, density: density, mode: mode, theme: theme, caps: caps, liveClampRows: liveClampRows, nowSec: nowSecFor(entry, nowSec) }, entry.id))) })] }));
390
- }
@@ -29,29 +29,6 @@ interface AssistantEntryProps {
29
29
  * `thought for 0s` is a lie where a bare `thought` is merely terse.
30
30
  */
31
31
  thinkingMs?: number;
32
- /**
33
- * Whether pressing `Ctrl+T` would actually reveal THIS entry's body — i.e.
34
- * `mode === 'fullscreen'` (D-16 / P0-2).
35
- *
36
- * In inline mode a settled entry has already been printed into Ink's
37
- * `<Static>`, which cannot un-print or re-print (`Transcript.tsx:9-10`), and
38
- * the settled boundary is held monotonic precisely so an entry never flows back
39
- * out. With thinking hidden by default the body is never printed there AT ALL,
40
- * so a row reading `ctrl+t to show` would be an instruction that provably does
41
- * nothing. The row degrades to `* thought for 12s` instead — still honest that
42
- * reasoning happened and how long it took, and silent about a key that cannot
43
- * deliver.
44
- */
45
- revealable?: boolean;
46
- /**
47
- * Inline mode only (L5): keep at most this many rows of a LIVE entry, so the
48
- * non-`<Static>` region cannot reach `stdout.rows`. `undefined` means no
49
- * clamp on the ANSWER BODY, which is what the full-screen branch passes — so
50
- * the rendered answer there is byte-identical to a pre-feature build. (The
51
- * thinking block has its own streaming clamp below and is not covered by that
52
- * statement.)
53
- */
54
- liveClampRows?: number;
55
32
  theme: Theme;
56
33
  caps: TermCapabilities;
57
34
  }
@@ -22,7 +22,7 @@ import { splitLinesCached } from '../render-cache.js';
22
22
  import { clampLiveText } from '../live-clamp.js';
23
23
  import { formatDuration } from '../../agent/usage.js';
24
24
  function AssistantEntryImpl(props) {
25
- const { text, thinking, thinkingOpen, thinkingVisible, streaming, aborted, thinkingMs, revealable, liveClampRows, theme, caps, } = props;
25
+ const { text, thinking, thinkingOpen, thinkingVisible, streaming, aborted, thinkingMs, theme, caps, } = props;
26
26
  const glyphs = pickGlyphs(caps);
27
27
  const hasThinking = !!thinking && thinking.trim().length > 0;
28
28
  const hasText = text.trim().length > 0;
@@ -32,12 +32,11 @@ function AssistantEntryImpl(props) {
32
32
  // full-screen mode: it is scratch reasoning the user reads the newest end of,
33
33
  // and an unbounded one is the single easiest way to push a live entry past the
34
34
  // whole viewport.
35
- const thinkingRows = liveClampRows ?? (streaming ? STREAMING_THINKING_ROWS : undefined);
35
+ const thinkingRows = streaming ? STREAMING_THINKING_ROWS : undefined;
36
36
  const clampedThinking = hasThinking && thinkingRows !== undefined
37
37
  ? clampLiveText(thinking, thinkingRows)
38
38
  : { text: thinking ?? '', hiddenRows: 0 };
39
- const body = liveClampRows === undefined ? { text, hiddenRows: 0 } : clampLiveText(text, liveClampRows);
40
- return (_jsxs(Box, { flexDirection: "column", children: [hasThinking && thinkingVisible && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { wrap: "truncate", color: theme.thinking, children: [glyphs.thinking, " thinking", thinkingOpen ? glyphs.ellipsis : ''] }), _jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [clampedThinking.hiddenRows > 0 && (_jsxs(Text, { color: theme.muted, children: [glyphs.ellipsis, " ", clampedThinking.hiddenRows, " earlier", ' ', clampedThinking.hiddenRows === 1 ? 'line' : 'lines'] })), splitLinesCached(clampedThinking.text).map((line, i) => (_jsx(Text, { color: theme.thinking, dimColor: true, children: line }, i)))] })] })), hasThinking && !thinkingVisible && !streaming && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [glyphs.thinking, " thought", thinkingMs !== undefined ? ` for ${formatDuration(thinkingMs)}` : '', revealable ? ` ${glyphs.midDot} ctrl+t to show` : ''] })), (hasText || streaming) && (_jsxs(Box, { flexDirection: "column", children: [body.hiddenRows > 0 && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [glyphs.ellipsis, " ", body.hiddenRows, " earlier", ' ', body.hiddenRows === 1 ? 'line' : 'lines', " ", glyphs.midDot, " shown in full when this entry finishes"] })), hasText ? (_jsx(Markdown, { text: body.text, theme: theme, caps: caps })) : (_jsx(Text, { color: theme.muted, children: glyphs.ellipsis })), aborted && _jsx(Text, { color: theme.noticeWarn, children: "[aborted]" })] }))] }));
39
+ return (_jsxs(Box, { flexDirection: "column", children: [hasThinking && thinkingVisible && (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Text, { wrap: "truncate", color: theme.thinking, children: [glyphs.thinking, " thinking", thinkingOpen ? glyphs.ellipsis : ''] }), _jsxs(Box, { flexDirection: "column", marginLeft: 2, children: [clampedThinking.hiddenRows > 0 && (_jsxs(Text, { color: theme.muted, children: [glyphs.ellipsis, " ", clampedThinking.hiddenRows, " earlier", ' ', clampedThinking.hiddenRows === 1 ? 'line' : 'lines'] })), splitLinesCached(clampedThinking.text).map((line, i) => (_jsx(Text, { color: theme.thinking, dimColor: true, children: line }, i)))] })] })), hasThinking && !thinkingVisible && !streaming && (_jsxs(Text, { wrap: "truncate", color: theme.muted, children: [glyphs.thinking, " thought", thinkingMs !== undefined ? ` for ${formatDuration(thinkingMs)}` : '', ` ${glyphs.midDot} ctrl+t to show`] })), (hasText || streaming) && (_jsxs(Box, { flexDirection: "column", children: [hasText ? (_jsx(Markdown, { text: text, theme: theme, caps: caps })) : (_jsx(Text, { color: theme.muted, children: glyphs.ellipsis })), aborted && _jsx(Text, { color: theme.noticeWarn, children: "[aborted]" })] }))] }));
41
40
  }
42
41
  /** Rows of live thinking kept on screen while an answer streams. */
43
42
  const STREAMING_THINKING_ROWS = 24;
@@ -32,12 +32,6 @@ export declare const DIFF_COLLAPSED_LINES = 12;
32
32
  export interface DiffViewProps {
33
33
  patch: FilePatch;
34
34
  expanded?: boolean;
35
- /**
36
- * Inline mode only (L5): ceiling on the rows a LIVE card contributes to the
37
- * non-`<Static>` region. `undefined` — what the full-screen branch passes —
38
- * means no clamp.
39
- */
40
- liveClampRows?: number;
41
35
  theme: Theme;
42
36
  }
43
37
  /** One rendered row: a patch line, or the `@@` separator between two hunks. */
@@ -56,7 +50,7 @@ interface DiffRow {
56
50
  * disagree.
57
51
  */
58
52
  export declare function diffRows(patch: FilePatch): DiffRow[];
59
- declare function DiffViewImpl({ patch, expanded, liveClampRows, theme, }: DiffViewProps): React.ReactElement;
53
+ declare function DiffViewImpl({ patch, expanded, theme, }: DiffViewProps): React.ReactElement;
60
54
  /**
61
55
  * `React.memo` with the DEFAULT comparator, matching `ToolCard`'s boundary.
62
56
  * `patch` qualifies as a prop only because the reducer writes it ONCE at
@@ -86,9 +86,9 @@ function gutterWidth(rows) {
86
86
  max = row.num;
87
87
  return Math.min(GUTTER_MAX, Math.max(GUTTER_MIN, String(max).length));
88
88
  }
89
- function DiffViewImpl({ patch, expanded, liveClampRows, theme, }) {
89
+ function DiffViewImpl({ patch, expanded, theme, }) {
90
90
  const rows = diffRows(patch);
91
- const ceiling = Math.min(expanded ? rows.length : DIFF_COLLAPSED_LINES, liveClampRows ?? Number.POSITIVE_INFINITY);
91
+ const ceiling = expanded ? rows.length : DIFF_COLLAPSED_LINES;
92
92
  const visible = ceiling >= rows.length ? rows : rows.slice(0, Math.max(0, ceiling));
93
93
  const hidden = rows.length - visible.length;
94
94
  const width = gutterWidth(rows);
@@ -37,12 +37,6 @@ interface ToolCardProps {
37
37
  isError?: boolean;
38
38
  expanded?: boolean;
39
39
  reducedMotion?: boolean;
40
- /**
41
- * Inline mode only (L5): ceiling on the rows a LIVE card contributes to the
42
- * non-`<Static>` region. `undefined` — what the full-screen branch passes —
43
- * means no clamp and byte-identical output.
44
- */
45
- liveClampRows?: number;
46
40
  /**
47
41
  * The sanitised live tail while the tool runs
48
42
  * (agent-activity-presentation-live §3.3.4).
@@ -80,7 +80,7 @@ export function toolEntryGlyph(name, caps) {
80
80
  return toolGlyph(pickGlyphs(caps), name);
81
81
  }
82
82
  function ToolCardImpl(props) {
83
- const { name, argsRaw, args, status, preview, patch, durationMs, isError, expanded, reducedMotion, liveClampRows, live, lastOutputAt, nowSec, theme, caps, } = props;
83
+ const { name, argsRaw, args, status, preview, patch, durationMs, isError, expanded, reducedMotion, live, lastOutputAt, nowSec, theme, caps, } = props;
84
84
  const glyphs = pickGlyphs(caps);
85
85
  const color = statusColor(status, theme);
86
86
  const previewText = preview ?? '';
@@ -88,12 +88,7 @@ function ToolCardImpl(props) {
88
88
  // is immutable once written, and this used to reallocate up to 200 strings per
89
89
  // card per frame (R3).
90
90
  const previewLines = previewText.length > 0 ? splitLinesCached(previewText) : EMPTY_LINES;
91
- // Head-first, exactly as before: the collapsed card shows the FIRST 8 lines.
92
- // The inline clamp only lowers the ceiling — it never changes which end is
93
- // kept, because `Ctrl+O` on a 5 000-line bash result is precisely how an
94
- // inline session reaches `ink.js:121` (I-L5-1) and the fix must not also
95
- // reshuffle what a full-screen user has been reading for four releases.
96
- const ceiling = Math.min(expanded ? previewLines.length : COLLAPSED_LINES, liveClampRows ?? Number.POSITIVE_INFINITY);
91
+ const ceiling = expanded ? previewLines.length : COLLAPSED_LINES;
97
92
  const visibleLines = ceiling >= previewLines.length ? previewLines : previewLines.slice(0, ceiling);
98
93
  const hidden = previewLines.length - visibleLines.length;
99
94
  const settled = status === 'done' || status === 'error';
@@ -110,11 +105,7 @@ function ToolCardImpl(props) {
110
105
  // NOW" (D-25). Head-first while live would freeze on the first eight lines a
111
106
  // build ever printed and then never change again.
112
107
  const liveRows = live ?? EMPTY_LINES;
113
- const liveCeiling = Math.min(COLLAPSED_LINES,
114
- // Inline only: `liveClampRows` already subtracts one row per live entry to
115
- // pay for a marker (`Transcript.tsx:534-538`), which is what this card's
116
- // unconditional footer spends (AC-36 / P2-7).
117
- liveClampRows ?? Number.POSITIVE_INFINITY);
108
+ const liveCeiling = COLLAPSED_LINES;
118
109
  const visibleLive = liveRows.length > liveCeiling ? liveRows.slice(-Math.max(1, liveCeiling)) : liveRows;
119
110
  const showLive = !settled && visibleLive.length > 0;
120
111
  // THE OUTER GUARD WIDENS WITH THE BRANCH (P2-2). It was `previewLines.length >
@@ -153,7 +144,7 @@ function ToolCardImpl(props) {
153
144
  // "constant row count" the exactness its arithmetic claims. A
154
145
  // space is also what the terminal itself shows for a blank line.
155
146
  // eslint-disable-next-line react/no-array-index-key
156
- _jsx(Text, { wrap: "truncate", color: theme.muted, children: row.length > 0 ? row : ' ' }, i))), _jsx(Text, { color: theme.muted, children: liveFooter })] })) : showPatch ? (_jsx(DiffView, { patch: patch, expanded: expanded, liveClampRows: liveClampRows, theme: theme })) : (_jsxs(_Fragment, { children: [_jsx(ToolPreview, { name: name, lines: visibleLines, theme: theme, isError: isError }), !expanded && hidden > 0 && (_jsxs(Text, { color: theme.muted, children: ["+", hidden, " ", hidden === 1 ? 'line' : 'lines', " (Ctrl+O)"] })), expanded && hidden > 0 && (_jsxs(Text, { color: theme.muted, children: ["+", hidden, " ", hidden === 1 ? 'line' : 'lines', " ", glyphs.midDot, " shown in full when this entry finishes"] })), expanded && previewLines.length > COLLAPSED_LINES && (_jsx(Text, { color: theme.muted, children: "(Ctrl+O to collapse)" }))] })) }))] }));
147
+ _jsx(Text, { wrap: "truncate", color: theme.muted, children: row.length > 0 ? row : ' ' }, i))), _jsx(Text, { color: theme.muted, children: liveFooter })] })) : showPatch ? (_jsx(DiffView, { patch: patch, expanded: expanded, theme: theme })) : (_jsxs(_Fragment, { children: [_jsx(ToolPreview, { name: name, lines: visibleLines, theme: theme, isError: isError }), !expanded && hidden > 0 && (_jsxs(Text, { color: theme.muted, children: ["+", hidden, " ", hidden === 1 ? 'line' : 'lines', " (Ctrl+O)"] })), expanded && hidden > 0 && (_jsxs(Text, { color: theme.muted, children: ["+", hidden, " ", hidden === 1 ? 'line' : 'lines', " ", glyphs.midDot, " shown in full when this entry finishes"] })), expanded && previewLines.length > COLLAPSED_LINES && (_jsx(Text, { color: theme.muted, children: "(Ctrl+O to collapse)" }))] })) }))] }));
157
148
  }
158
149
  /**
159
150
  * `React.memo` with the DEFAULT comparator (tui-render-performance L2 / R3).
@@ -25,6 +25,12 @@
25
25
  * trailing `CSI K` removes only the residue a longer previous line left, and
26
26
  * the trailing SGR reset before it stops a BCE terminal painting that residue
27
27
  * in whatever background colour the line ended inside.
28
+ * `CSI K` IS EMITTED ONLY WHEN THE LINE IS NARROWER THAN THE TERMINAL. A row
29
+ * that already fills every column (the unified scrollbar paints the last one)
30
+ * has no residue to clear, and on terminals that keep the cursor ON the last
31
+ * column after writing it (a pending-wrap flag rather than a virtual column
32
+ * N+1) the EL would erase the cell that was just written. `cols` unknown keeps
33
+ * the old always-EL behaviour (`paintLine`).
28
34
  * - I-5 · Every emitted batch ends parked on row `H+1`. Ink's own
29
35
  * `previousLineCount` is computed from the string IT produced and is
30
36
  * unaffected by what we actually wrote, so a later pass-through's
@@ -56,6 +62,13 @@ export interface FrameDifferOptions {
56
62
  * on resize. `undefined` ⇒ stand down and pass through.
57
63
  */
58
64
  rows: () => number | undefined;
65
+ /**
66
+ * Live terminal width, read on every paint and never cached (like `rows`).
67
+ * Absent or non-finite => every changed row ends in `CSI K` (the original
68
+ * behaviour); finite => rows whose display width is >= `cols` skip the EL so
69
+ * the last column (the scrollbar) is never erased (I-4).
70
+ */
71
+ cols?: () => number | undefined;
59
72
  /** Raised once, on the 0→1 edge of `fallbacks` (§5.6). */
60
73
  onFirstFallback?: () => void;
61
74
  /**
@@ -25,6 +25,12 @@
25
25
  * trailing `CSI K` removes only the residue a longer previous line left, and
26
26
  * the trailing SGR reset before it stops a BCE terminal painting that residue
27
27
  * in whatever background colour the line ended inside.
28
+ * `CSI K` IS EMITTED ONLY WHEN THE LINE IS NARROWER THAN THE TERMINAL. A row
29
+ * that already fills every column (the unified scrollbar paints the last one)
30
+ * has no residue to clear, and on terminals that keep the cursor ON the last
31
+ * column after writing it (a pending-wrap flag rather than a virtual column
32
+ * N+1) the EL would erase the cell that was just written. `cols` unknown keeps
33
+ * the old always-EL behaviour (`paintLine`).
28
34
  * - I-5 · Every emitted batch ends parked on row `H+1`. Ink's own
29
35
  * `previousLineCount` is computed from the string IT produced and is
30
36
  * unaffected by what we actually wrote, so a later pass-through's
@@ -37,6 +43,8 @@
37
43
  * the guard being absent is a permanently misaligned screen while the failure
38
44
  * mode of it firing spuriously is one extra full repaint.
39
45
  */
46
+ import stringWidth from 'string-width';
47
+ import stripAnsi from 'strip-ansi';
40
48
  import { MIN_FULLSCREEN_ROWS } from './layout/frame.js';
41
49
  import { matchInkErasePrefix } from './frame-parser.js';
42
50
  // ---------------------------------------------------------------------------
@@ -59,6 +67,23 @@ const SYNC_END = `${CSI}?2026l`;
59
67
  function cursorTo(row) {
60
68
  return `${CSI}${row};1H`;
61
69
  }
70
+ /**
71
+ * One addressed row: `CUP · SGR0 · line · SGR0 [· CSI K]` (I-4).
72
+ *
73
+ * The trailing `CSI K` clears residue a longer previous line left behind, so it is
74
+ * only meaningful when the line is narrower than the terminal. A line that already
75
+ * fills `cols` columns ends with the cursor in the pending-wrap state, where some
76
+ * terminals run EL from the LAST column and erase the cell just written; those
77
+ * rows get no EL. Width is measured exactly as Ink measures it (`string-width` over
78
+ * the ANSI-stripped text) and only for rows that changed, so it costs at most one
79
+ * measurement per changed viewport row. `cols` unknown => the original always-EL.
80
+ */
81
+ function paintLine(row, line, cols) {
82
+ const head = cursorTo(row) + SGR_RESET + line + SGR_RESET;
83
+ if (cols !== undefined && stringWidth(stripAnsi(line)) >= cols)
84
+ return head;
85
+ return head + ERASE_TO_EOL;
86
+ }
62
87
  /**
63
88
  * Rebuild `ansiEscapes.eraseLines(count)` byte for byte.
64
89
  *
@@ -211,13 +236,18 @@ export function createFrameDiffer(options) {
211
236
  cursorTo(lines.length + 1);
212
237
  return emit(wrapSync(payload), lines, raw, lines.length, true);
213
238
  };
239
+ /** Terminal width for this paint, or `undefined` when unknown (I-4: always EL). */
240
+ const liveCols = () => {
241
+ const cols = options.cols?.();
242
+ return cols !== undefined && Number.isFinite(cols) ? cols : undefined;
243
+ };
214
244
  const diffRepaint = (lines, raw, previous) => {
215
245
  const out = [];
246
+ const cols = liveCols();
216
247
  for (let i = 0; i < lines.length; i += 1) {
217
248
  // I-4: paint, THEN clear the tail. Never `ERASE_LINE` first.
218
- if (lines[i] !== previous[i]) {
219
- out.push(cursorTo(i + 1) + SGR_RESET + lines[i] + SGR_RESET + ERASE_TO_EOL);
220
- }
249
+ if (lines[i] !== previous[i])
250
+ out.push(paintLine(i + 1, lines[i], cols));
221
251
  }
222
252
  // DEFENSIVE ONLY, not a live path (P2-4). `log-update.js:13-15` returns early
223
253
  // on an identical frame, so a chunk that reaches here differs by at least one
@@ -251,10 +281,10 @@ export function createFrameDiffer(options) {
251
281
  return '';
252
282
  const painted = applyDecorate(rawPrev);
253
283
  const out = [];
284
+ const cols = liveCols();
254
285
  for (let i = 0; i < painted.length; i += 1) {
255
- if (painted[i] !== prev[i]) {
256
- out.push(cursorTo(i + 1) + SGR_RESET + painted[i] + SGR_RESET + ERASE_TO_EOL);
257
- }
286
+ if (painted[i] !== prev[i])
287
+ out.push(paintLine(i + 1, painted[i], cols));
258
288
  }
259
289
  prev = painted;
260
290
  if (out.length === 0)
@@ -0,0 +1 @@
1
+ export declare const render: (node: import("react").ReactNode, options?: NodeJS.WriteStream | import("ink").RenderOptions) => import("ink").Instance;
@@ -0,0 +1,21 @@
1
+ import process from 'node:process';
2
+ /** Ink 5 caches CI detection at import time and otherwise paints only on exit. */
3
+ async function loadInk() {
4
+ if (!process.stdin.isTTY || !process.stdout.isTTY)
5
+ return import('ink');
6
+ const originalCi = process.env.CI;
7
+ try {
8
+ // A real interactive terminal always needs live frames and resize events.
9
+ // `0` also overrides CI_* / CONTINUOUS_INTEGRATION in Ink's detector.
10
+ process.env.CI = '0';
11
+ return await import('ink');
12
+ }
13
+ finally {
14
+ // Commands, subprocesses and the updater must see the original environment.
15
+ if (originalCi === undefined)
16
+ delete process.env.CI;
17
+ else
18
+ process.env.CI = originalCi;
19
+ }
20
+ }
21
+ export const { render } = await loadInk();
@@ -1,22 +1,13 @@
1
1
  /** Stable fullscreen slots preserve the editor during overlays and small resizes. */
2
2
  import React from 'react';
3
- import { type RenderMode } from './frame.js';
4
3
  export interface AppShellProps {
5
- mode: RenderMode;
6
4
  rows: number;
7
5
  cols: number;
8
6
  header: React.ReactNode;
9
7
  viewport: React.ReactNode;
10
- /** Legacy inline footer; fullscreen assembles these slots inside its viewport. */
11
- team?: React.ReactNode;
12
- /** Kept source-compatible; fullscreen rail is owned by ScrollViewport. */
13
- rail?: React.ReactNode;
14
- /** Inline-only plan strip. */
15
- strip?: React.ReactNode;
16
8
  toast: React.ReactNode;
17
- composer: React.ReactNode;
18
9
  status: React.ReactNode;
19
10
  inactive?: boolean;
20
11
  placeholder?: React.ReactNode;
21
12
  }
22
- export declare function AppShell({ mode, rows, cols, header, viewport, team, strip, toast, composer, status, inactive, placeholder, }: 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, strip, toast, composer, status, inactive = false, placeholder, }) {
5
- if (mode === 'inline') {
6
- return (_jsxs(Box, { flexDirection: "column", children: [header, viewport, strip, team, toast, composer, status] }));
7
- }
4
+ export function AppShell({ rows, cols, header, viewport, toast, status, inactive = false, placeholder, }) {
8
5
  return (_jsxs(Box, { flexDirection: "column", height: frameHeight(rows), width: cols, overflow: "hidden", children: [_jsxs(Box, { display: inactive ? 'none' : 'flex', flexDirection: "column", height: frameHeight(rows), width: cols, overflow: "hidden", children: [_jsx(Box, { height: 1, flexShrink: 0, overflow: "hidden", children: header }), _jsx(Box, { flexDirection: "column", flexGrow: 1, flexShrink: 1, overflow: "hidden", children: viewport }), _jsx(Box, { height: 1, flexShrink: 0, overflow: "hidden", children: toast }), _jsx(Box, { height: 1, flexShrink: 0, overflow: "hidden", children: status })] }), _jsx(Box, { display: inactive ? 'flex' : 'none', height: frameHeight(rows), overflow: "hidden", children: placeholder })] }));
9
6
  }
@@ -1,27 +1,3 @@
1
- /**
2
- * OverlayFrame (spec §4.4) — the shared shell for all four overlays.
3
- *
4
- * TWO CONTENT MODES, and the distinction is load-bearing:
5
- *
6
- * - mode A (`rows`): the caller hands over an array of elements that are each
7
- * exactly one row and `wrap="truncate"`. The frame slices them and OWNS the
8
- * scroll keys. Used by Help and Settings.
9
- *
10
- * - mode B (`children`): the content manages itself. The frame draws the title,
11
- * the border and the footer, and touches NOTHING else — no slicing, no
12
- * arrow-key handling, no position indicator.
13
- *
14
- * Mode B exists because of `ink-select-input`: it already windows itself via
15
- * `limit` and registers its own `useInput` with `isFocused` defaulting to true.
16
- * Ink dispatches a key to EVERY mounted input handler, so a frame that also
17
- * claimed the arrow keys would move the model picker's selection twice per press
18
- * — breaking a feature that already works. Slicing it by element would be worse
19
- * still, since it is a single component rather than a list of rows.
20
- *
21
- * `maxRows` may be `Infinity` (the inline render path, which has no fixed frame
22
- * and therefore no height to fit): the frame then renders everything and shows
23
- * no position indicator.
24
- */
25
1
  import React from 'react';
26
2
  import type { Theme } from '../theme.js';
27
3
  import type { TermCapabilities } from '../capabilities.js';
@@ -29,7 +5,7 @@ interface OverlayFrameBase {
29
5
  title: string;
30
6
  /** Footer text. Written by the CALLER so each overlay names its own keys. */
31
7
  hint: string;
32
- /** Viewport rows the overlay may occupy; `Infinity` in inline mode. */
8
+ /** Viewport rows the overlay may occupy. */
33
9
  maxRows: number;
34
10
  cols?: number;
35
11
  theme: Theme;