@aragon-agent/cli 0.6.29 → 0.6.31

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 (58) hide show
  1. package/CHANGELOG.md +2123 -2103
  2. package/README.md +81 -14
  3. package/dist/agent/project-guidance-prompt.js +20 -20
  4. package/dist/cli.js +169 -56
  5. package/dist/commands/builtins.js +4 -0
  6. package/dist/commands/perf.d.ts +25 -1
  7. package/dist/commands/perf.js +26 -2
  8. package/dist/commands/registry.d.ts +6 -0
  9. package/dist/config/env.js +4 -0
  10. package/dist/config/load.d.ts +3 -1
  11. package/dist/config/load.js +12 -1
  12. package/dist/config/schema.d.ts +27 -0
  13. package/dist/config/schema.js +9 -0
  14. package/dist/config/ui-state.d.ts +9 -0
  15. package/dist/config/ui-state.js +9 -0
  16. package/dist/ui/App.d.ts +11 -1
  17. package/dist/ui/App.js +122 -62
  18. package/dist/ui/ScrollbackShell.d.ts +28 -0
  19. package/dist/ui/ScrollbackShell.js +10 -0
  20. package/dist/ui/frame-differ.d.ts +12 -0
  21. package/dist/ui/overlays/HelpOverlay.d.ts +1 -1
  22. package/dist/ui/overlays/HelpOverlay.js +21 -3
  23. package/dist/ui/screen.d.ts +16 -0
  24. package/dist/ui/screen.js +32 -0
  25. package/dist/ui/scrollback/budget.d.ts +43 -0
  26. package/dist/ui/scrollback/budget.js +44 -0
  27. package/dist/ui/scrollback/drain.d.ts +32 -0
  28. package/dist/ui/scrollback/drain.js +64 -0
  29. package/dist/ui/scrollback/exit-summary.d.ts +23 -0
  30. package/dist/ui/scrollback/exit-summary.js +25 -0
  31. package/dist/ui/scrollback/limits.d.ts +25 -0
  32. package/dist/ui/scrollback/limits.js +25 -0
  33. package/dist/ui/scrollback/live-preview.d.ts +26 -0
  34. package/dist/ui/scrollback/live-preview.js +106 -0
  35. package/dist/ui/scrollback/renderer.d.ts +38 -0
  36. package/dist/ui/scrollback/renderer.js +158 -0
  37. package/dist/ui/scrollback/scrollback-writer.d.ts +74 -0
  38. package/dist/ui/scrollback/scrollback-writer.js +263 -0
  39. package/dist/ui/scrollback/settled-boundary.d.ts +42 -0
  40. package/dist/ui/scrollback/settled-boundary.js +106 -0
  41. package/dist/ui/scrollback/use-scrollback-view.d.ts +92 -0
  42. package/dist/ui/scrollback/use-scrollback-view.js +263 -0
  43. package/dist/ui/scrollback/writer-shapes.d.ts +61 -0
  44. package/dist/ui/scrollback/writer-shapes.js +114 -0
  45. package/dist/ui/stdout-frame-writer.js +12 -2
  46. package/dist/ui/terminal-plan.d.ts +52 -0
  47. package/dist/ui/terminal-plan.js +49 -0
  48. package/dist/ui/transcript-text.d.ts +11 -0
  49. package/dist/ui/transcript-text.js +21 -7
  50. package/dist/ui/use-startup-notices.d.ts +12 -1
  51. package/dist/ui/use-startup-notices.js +28 -2
  52. package/package.json +2 -2
  53. package/skills/project-indexer/SKILL.md +163 -163
  54. package/skills/project-indexer/templates/cleancode-overrides.md +46 -46
  55. package/skills/project-indexer/templates/cleancode-template.md +94 -94
  56. package/skills/project-indexer/templates/config-template.md +69 -69
  57. package/skills/project-indexer/templates/index-template.md +129 -129
  58. package/skills/skill-creator/SKILL.md +96 -96
@@ -0,0 +1,263 @@
1
+ /**
2
+ * ScrollbackWriter — the byte contract of native-scroll mode
3
+ * (tui-native-terminal-scroll §4.3).
4
+ *
5
+ * It implements `FrameDiffer`, so `stdout-frame-writer.ts::wrapStdoutForFrames`
6
+ * binds it to a real stream exactly as it binds the full-screen differ. What it
7
+ * does differently: the screen is NOT the app's. Settled conversation rows are
8
+ * printed into the host terminal's own scrollback, and Ink's frame is only a small
9
+ * LIVE REGION parked directly under the last printed row.
10
+ *
11
+ * GEOMETRY IS RELATIVE (D2). A batch is `CUU(H0)` to the top of the previous
12
+ * region, erase down, print static rows, print the new region, park one row below
13
+ * it. Nothing in that sequence depends on the terminal's absolute row count, so a
14
+ * terminal that scrolls underneath it cannot misplace anything.
15
+ *
16
+ * THE INVARIANTS THAT FAIL SILENTLY IF DROPPED:
17
+ * - I-S4 · every batch parks the cursor at (regionBottom + 1, column 0), the same
18
+ * place Ink's `previousLineCount = H + 1` expects, so falling back to a raw Ink
19
+ * write is still correct.
20
+ * - I-S8 · geometry in doubt (foreign write, unparseable chunk) => `prev = null`
21
+ * => next batch is a SEED. Never keep erasing on a stale `H0`.
22
+ * - I-S14 · after `finalize` EVERY Ink write is swallowed. Ink's unmount renders
23
+ * once more (and a 32 ms trailing throttle call can follow); passed through,
24
+ * its `eraseLines(H + 1)` would erase the summary and the end of the
25
+ * conversation. With an unknown or sub-2-row terminal the frame is still
26
+ * parsed so `prev`/`H0` track Ink's own `previousLineCount`.
27
+ * - I-S13 · no emitted row is wider than `cols - 1` (clipped, counted).
28
+ */
29
+ import { parseInkFrame } from '../frame-parser.js';
30
+ import { SCROLLBACK_LIMITS } from './limits.js';
31
+ import { clipLine, diffBytes, estimateReflowRows, finalizeBytes, fullBytes, seedBytes, } from './writer-shapes.js';
32
+ /** `ansiEscapes.clearTerminal` — Ink's tall-frame write. It contains `ESC[3J`. */
33
+ const CLEAR_TERMINAL = '\x1b[2J\x1b[3J\x1b[H';
34
+ export function createScrollbackWriter(o) {
35
+ const now = o.now ?? Date.now;
36
+ const maxBatch = SCROLLBACK_LIMITS.maxRowsPerBatch;
37
+ /** Region lines as written (clipped), or null when the geometry is in doubt. */
38
+ let prev = null;
39
+ /** The same lines before clipping, so unchanged rows are not re-measured. */
40
+ let prevRaw = null;
41
+ /** Rows between region top and the parked cursor. Tracks Ink's own count. */
42
+ let regionHeight = 0;
43
+ /** Whether anything has ever been written (a re-seed then starts on a new line). */
44
+ let everWritten = false;
45
+ let seenAnyChunk = false;
46
+ let finalized = false;
47
+ let guardTripped = false;
48
+ let consecutiveGuard = 0;
49
+ let lastResizeAt = Number.NEGATIVE_INFINITY;
50
+ let resizePending = false;
51
+ /** Width the previous region was written at, for the reflow estimate. */
52
+ let lastWriteCols;
53
+ let repaintQueued = false;
54
+ const stats = {
55
+ framesSeed: 0, framesDiff: 0, framesFull: 0, staticRowsWritten: 0, bytesWritten: 0,
56
+ fallbacks: 0, guardHits: 0, guardHitsOutsideResize: 0, clippedRows: 0, elidedEntries: 0,
57
+ resizes: 0, repaints: 0,
58
+ };
59
+ const liveCols = () => {
60
+ const cols = o.cols();
61
+ return cols !== undefined && Number.isFinite(cols) && cols >= 1 ? Math.floor(cols) : undefined;
62
+ };
63
+ const liveRows = () => {
64
+ const rows = o.rows();
65
+ return rows !== undefined && Number.isFinite(rows) ? Math.floor(rows) : undefined;
66
+ };
67
+ const scheduleRepaint = () => {
68
+ if (!o.requestRepaint || repaintQueued || finalized)
69
+ return;
70
+ repaintQueued = true;
71
+ // Deferred by the host (setImmediate): this can fire from inside Ink's write.
72
+ o.requestRepaint();
73
+ };
74
+ const invalidate = () => {
75
+ prev = null;
76
+ prevRaw = null;
77
+ resizePending = false;
78
+ };
79
+ /** Clip every row to `cols - 1`, reusing the previous result for unchanged rows (I-S13). */
80
+ const clipRegion = (lines, cols) => {
81
+ if (cols === undefined)
82
+ return [...lines];
83
+ const limit = cols - 1;
84
+ return lines.map((line, i) => {
85
+ if (prevRaw && prev && prevRaw[i] === line && prev[i] !== undefined)
86
+ return prev[i];
87
+ const result = clipLine(line, limit);
88
+ if (result.clipped)
89
+ stats.clippedRows += 1;
90
+ return result.text;
91
+ });
92
+ };
93
+ const clipStatics = (rows, cols) => {
94
+ if (cols === undefined || rows.length === 0)
95
+ return rows;
96
+ return rows.map((line) => {
97
+ const result = clipLine(line, cols - 1);
98
+ if (result.clipped)
99
+ stats.clippedRows += 1;
100
+ return result.text;
101
+ });
102
+ };
103
+ /** Rows to move up to reach the region top: the plain height, or a post-resize estimate. */
104
+ const eraseDistance = (rows, cols) => {
105
+ if (!resizePending || prev === null || cols === undefined)
106
+ return regionHeight;
107
+ const reflowed = estimateReflowRows(prev, cols);
108
+ return Math.min(Math.max(0, rows - 1), reflowed);
109
+ };
110
+ const record = (bytes, shape, statics) => {
111
+ if (shape === 'seed')
112
+ stats.framesSeed += 1;
113
+ else if (shape === 'diff')
114
+ stats.framesDiff += 1;
115
+ else if (shape === 'full')
116
+ stats.framesFull += 1;
117
+ else
118
+ stats.repaints += 1;
119
+ stats.staticRowsWritten += statics;
120
+ stats.bytesWritten += bytes.length;
121
+ return bytes;
122
+ };
123
+ const afterBatch = (lines, raw, cols) => {
124
+ prev = lines;
125
+ prevRaw = raw;
126
+ regionHeight = lines.length;
127
+ everWritten = true;
128
+ resizePending = false;
129
+ lastWriteCols = cols;
130
+ // A feed larger than one batch continues on the next tick, region still parked.
131
+ if (o.drain.pending() > 0) {
132
+ repaintQueued = false;
133
+ scheduleRepaint();
134
+ }
135
+ };
136
+ const assemble = (lines, raw, rows, cols) => {
137
+ const statics = clipStatics(o.drain.takeUpTo(maxBatch), cols);
138
+ let out;
139
+ if (prev === null) {
140
+ out = record(seedBytes({ lines, statics, sync: o.sync, reseed: everWritten }), 'seed', statics.length);
141
+ }
142
+ else if (statics.length > 0 || lines.length !== prev.length || o.fullEveryFrame || resizePending) {
143
+ out = record(fullBytes({ lines, statics, eraseUp: eraseDistance(rows, cols), sync: o.sync }), 'full', statics.length);
144
+ }
145
+ else {
146
+ out = record(diffBytes({ lines, prev, sync: o.sync }), 'diff', 0);
147
+ }
148
+ afterBatch(lines, raw, cols);
149
+ return out;
150
+ };
151
+ const swallowTallFrame = () => {
152
+ stats.guardHits += 1;
153
+ if (now() - lastResizeAt >= SCROLLBACK_LIMITS.guardResizeGraceMs) {
154
+ stats.guardHitsOutsideResize += 1;
155
+ consecutiveGuard += 1;
156
+ if (consecutiveGuard >= SCROLLBACK_LIMITS.guardTripCount && !guardTripped) {
157
+ guardTripped = true;
158
+ o.onGuardTrip?.();
159
+ }
160
+ }
161
+ // Swallow the whole chunk: it would clear the visible screen AND scrollback.
162
+ return '';
163
+ };
164
+ const countFallback = (counted) => {
165
+ invalidate();
166
+ if (counted) {
167
+ stats.fallbacks += 1;
168
+ if (stats.fallbacks === 1)
169
+ o.onFirstFallback?.();
170
+ }
171
+ return null;
172
+ };
173
+ const transform = (chunk) => {
174
+ if (finalized)
175
+ return '';
176
+ if (chunk.startsWith(CLEAR_TERMINAL))
177
+ return swallowTallFrame();
178
+ consecutiveGuard = 0;
179
+ guardTripped = false;
180
+ const firstChunk = !seenAnyChunk;
181
+ seenAnyChunk = true;
182
+ const rows = liveRows();
183
+ const lines = parseInkFrame(chunk);
184
+ if (rows === undefined || rows < 2) {
185
+ // Pass through, but keep `prev`/`H0` in step with Ink's own count (RV-6).
186
+ if (lines) {
187
+ prev = lines;
188
+ prevRaw = lines;
189
+ regionHeight = lines.length;
190
+ }
191
+ return null;
192
+ }
193
+ if (lines === null)
194
+ return countFallback(!firstChunk);
195
+ if (lines.length + 1 > rows) {
196
+ // Unreachable by construction (I-S1); never erase on a guess.
197
+ invalidate();
198
+ return '';
199
+ }
200
+ const cols = liveCols();
201
+ return assemble(clipRegion(lines, cols), lines, rows, cols);
202
+ };
203
+ const repaint = () => {
204
+ repaintQueued = false;
205
+ if (finalized || prev === null || prevRaw === null)
206
+ return '';
207
+ if (o.drain.pending() === 0 && !resizePending)
208
+ return '';
209
+ const rows = liveRows();
210
+ if (rows === undefined || rows < 2)
211
+ return '';
212
+ const cols = liveCols();
213
+ const lines = prev;
214
+ const statics = clipStatics(o.drain.takeUpTo(maxBatch), cols);
215
+ const bytes = fullBytes({ lines, statics, eraseUp: eraseDistance(rows, cols), sync: o.sync });
216
+ // A repaint is its own counter: `framesFull` counts frames INK produced.
217
+ record(bytes, 'repaint', statics.length);
218
+ afterBatch(lines, prevRaw, cols);
219
+ return bytes;
220
+ };
221
+ const finalize = (summary) => {
222
+ if (finalized)
223
+ return '';
224
+ finalized = true;
225
+ const statics = clipStatics(o.drain.takeAll(), liveCols());
226
+ // Unknown geometry after a foreign write: no safe erase, start on a fresh line.
227
+ const eraseUp = prev === null && everWritten ? -1 : eraseDistance(liveRows() ?? 24, liveCols());
228
+ const bytes = finalizeBytes({ eraseUp, statics, summary });
229
+ stats.staticRowsWritten += statics.length;
230
+ stats.bytesWritten += bytes.length;
231
+ return bytes;
232
+ };
233
+ o.drain.onEnqueue = scheduleRepaint;
234
+ return {
235
+ transform,
236
+ repaint,
237
+ finalize,
238
+ invalidate,
239
+ onResize() {
240
+ stats.resizes += 1;
241
+ lastResizeAt = now();
242
+ consecutiveGuard = 0;
243
+ guardTripped = false;
244
+ // Keep `prev`: the reflow estimate needs the lines the terminal re-flowed.
245
+ if (prev !== null && !finalized)
246
+ resizePending = true;
247
+ },
248
+ cursorNeutralWrite() {
249
+ // OSC 52 and friends move no cell and no cursor (I-S11).
250
+ },
251
+ regionRows: () => regionHeight,
252
+ scrollbackStats: () => ({ ...stats, elidedEntries: o.drain.elided() }),
253
+ stats: () => ({
254
+ framesTotal: stats.framesSeed + stats.framesFull + stats.framesDiff,
255
+ framesDiffed: stats.framesDiff,
256
+ framesFull: stats.framesSeed + stats.framesFull,
257
+ linesWritten: stats.staticRowsWritten,
258
+ bytesWritten: stats.bytesWritten,
259
+ fallbacks: stats.fallbacks,
260
+ repaints: stats.repaints,
261
+ }),
262
+ };
263
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Settled boundary — which transcript entries may be PRINTED
3
+ * (tui-native-terminal-scroll §4.4).
4
+ *
5
+ * Printed rows live in the host terminal's scrollback and can never be rewritten
6
+ * ("printed is forever"). So the only entries that may be printed are the ones the
7
+ * reducer will NEVER write to again. This module derives that from the reducer's
8
+ * real write set, one exhaustive `switch` per entry kind. Adding an `Entry` kind is
9
+ * a compile error here (`never`), which is the whole point; widening the write set
10
+ * of an EXISTING kind is not caught by the compiler, so `agent/reducer.ts` carries
11
+ * a pointer comment beside the `Entry` union and the unit tests pin each row.
12
+ *
13
+ * Three rules that are easy to "simplify" wrongly:
14
+ * - A tool is stable only when `done` or `error`. `pending` is NOT stable: the
15
+ * card is created at `pending` and `toolCallDelta` keeps rewriting `argsRaw`
16
+ * while the model streams its arguments (RV-1).
17
+ * - A trailing `Context length exceeded` error notice is NOT stable while a run is
18
+ * active: `compactionStart` rewrites it in place into an info notice (RV-4).
19
+ * - A non-terminal `service` card is neither printed nor a wall (RV-3). It is
20
+ * rewritten for the whole session; the one-row `terminal: true` record the
21
+ * reducer appends at the end is an ordinary stable entry and carries the news.
22
+ */
23
+ import type { Entry } from '../../agent/reducer.js';
24
+ /** Never printed and never a wall: rewritten for the whole session (RV-3). */
25
+ export declare function isExcluded(entry: Entry): boolean;
26
+ /** True when the reducer will never write to `entry` again. */
27
+ export declare function isEntryStable(entry: Entry, runActive: boolean, isTail: boolean): boolean;
28
+ /**
29
+ * Length of the printable PREFIX of `entries`.
30
+ *
31
+ * The first unstable (non-excluded) entry is a wall: everything before it may
32
+ * print, nothing after it may, which keeps scrollback order equal to conversation
33
+ * order (I-S3). Excluded entries are skipped, so a long-lived service card never
34
+ * blocks the conversation behind it.
35
+ */
36
+ export declare function settledCount(entries: readonly Entry[], runActive: boolean): number;
37
+ /**
38
+ * Everything printable at shutdown: all entries except those that never settle
39
+ * (`queued`, non-terminal services). The last answer must not be lost to a
40
+ * boundary that was waiting for a frame that will never come.
41
+ */
42
+ export declare function settleAllEntries(entries: readonly Entry[]): Entry[];
@@ -0,0 +1,106 @@
1
+ /**
2
+ * Settled boundary — which transcript entries may be PRINTED
3
+ * (tui-native-terminal-scroll §4.4).
4
+ *
5
+ * Printed rows live in the host terminal's scrollback and can never be rewritten
6
+ * ("printed is forever"). So the only entries that may be printed are the ones the
7
+ * reducer will NEVER write to again. This module derives that from the reducer's
8
+ * real write set, one exhaustive `switch` per entry kind. Adding an `Entry` kind is
9
+ * a compile error here (`never`), which is the whole point; widening the write set
10
+ * of an EXISTING kind is not caught by the compiler, so `agent/reducer.ts` carries
11
+ * a pointer comment beside the `Entry` union and the unit tests pin each row.
12
+ *
13
+ * Three rules that are easy to "simplify" wrongly:
14
+ * - A tool is stable only when `done` or `error`. `pending` is NOT stable: the
15
+ * card is created at `pending` and `toolCallDelta` keeps rewriting `argsRaw`
16
+ * while the model streams its arguments (RV-1).
17
+ * - A trailing `Context length exceeded` error notice is NOT stable while a run is
18
+ * active: `compactionStart` rewrites it in place into an info notice (RV-4).
19
+ * - A non-terminal `service` card is neither printed nor a wall (RV-3). It is
20
+ * rewritten for the whole session; the one-row `terminal: true` record the
21
+ * reducer appends at the end is an ordinary stable entry and carries the news.
22
+ */
23
+ import { CONTEXT_OVERFLOW_NOTICE_PREFIX } from '../../agent/reducer.js';
24
+ /** Kinds the reducer may still rewrite while a run is active, so a tail one is held. */
25
+ const MUTABLE_TAIL_KINDS = new Set([
26
+ 'assistant', 'tool', 'compaction', 'todo', 'team', 'fast', 'retry', 'queued',
27
+ ]);
28
+ /** Never printed and never a wall: rewritten for the whole session (RV-3). */
29
+ export function isExcluded(entry) {
30
+ return entry.kind === 'service' && entry.terminal !== true;
31
+ }
32
+ /** True when the reducer will never write to `entry` again. */
33
+ export function isEntryStable(entry, runActive, isTail) {
34
+ switch (entry.kind) {
35
+ case 'user':
36
+ return true;
37
+ case 'notice':
38
+ return !(runActive &&
39
+ isTail &&
40
+ entry.level === 'error' &&
41
+ entry.text.startsWith(CONTEXT_OVERFLOW_NOTICE_PREFIX));
42
+ case 'assistant':
43
+ return !entry.streaming;
44
+ case 'tool':
45
+ return entry.status === 'done' || entry.status === 'error';
46
+ case 'compaction':
47
+ return entry.live !== true;
48
+ case 'todo':
49
+ return entry.live === false;
50
+ case 'team':
51
+ return entry.active === false;
52
+ case 'fast':
53
+ return entry.live === false;
54
+ case 'retry':
55
+ return (entry.phase === 'recovered' ||
56
+ entry.phase === 'exhausted' ||
57
+ entry.phase === 'interrupted');
58
+ case 'queued':
59
+ // Rewritten in place into a `user` entry by the matching receipt.
60
+ return false;
61
+ case 'service':
62
+ // Only the terminal record is ever a candidate; `isExcluded` filters the rest.
63
+ return entry.terminal === true;
64
+ default: {
65
+ const exhaustive = entry;
66
+ return exhaustive;
67
+ }
68
+ }
69
+ }
70
+ /**
71
+ * Length of the printable PREFIX of `entries`.
72
+ *
73
+ * The first unstable (non-excluded) entry is a wall: everything before it may
74
+ * print, nothing after it may, which keeps scrollback order equal to conversation
75
+ * order (I-S3). Excluded entries are skipped, so a long-lived service card never
76
+ * blocks the conversation behind it.
77
+ */
78
+ export function settledCount(entries, runActive) {
79
+ const last = entries.length - 1;
80
+ let count = entries.length;
81
+ for (let i = 0; i < entries.length; i += 1) {
82
+ const entry = entries[i];
83
+ if (isExcluded(entry))
84
+ continue;
85
+ if (!isEntryStable(entry, runActive, i === last)) {
86
+ count = i;
87
+ break;
88
+ }
89
+ }
90
+ // Hold a mutable TAIL while a run is active, but only a mutable one: a `user` or
91
+ // `notice` at the tail prints immediately (a submitted message must appear at once).
92
+ if (runActive && count === entries.length && count > 0) {
93
+ const tail = entries[last];
94
+ if (!isExcluded(tail) && MUTABLE_TAIL_KINDS.has(tail.kind))
95
+ return last;
96
+ }
97
+ return count;
98
+ }
99
+ /**
100
+ * Everything printable at shutdown: all entries except those that never settle
101
+ * (`queued`, non-terminal services). The last answer must not be lost to a
102
+ * boundary that was waiting for a frame that will never come.
103
+ */
104
+ export function settleAllEntries(entries) {
105
+ return entries.filter((entry) => entry.kind !== 'queued' && !isExcluded(entry));
106
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * `useScrollbackView` — the promotion pipeline of native-scroll mode
3
+ * (tui-native-terminal-scroll §4.4 / §6.2).
4
+ *
5
+ * `App` calls it ALWAYS (hooks cannot be conditional); `enabled: false` makes every
6
+ * effect a no-op and keeps `scrollMode: 'app'` byte-identical to the pre-feature
7
+ * build (I-S9).
8
+ *
9
+ * What it does when enabled:
10
+ * 1. Prints the session header (and the opener for an empty session) once.
11
+ * 2. On every entries change, promotes each newly SETTLED entry: renders it once
12
+ * through the offscreen renderer and enqueues the rows for the writer.
13
+ * 3. Computes the live-region budget and the streaming preview rows for the shell.
14
+ *
15
+ * THE PROMOTION RUNS IN A MICROTASK, NOT IN THE EFFECT. The offscreen renderer is a
16
+ * second Ink instance, and both instances share one React reconciler. A `rerender`
17
+ * issued from inside the main tree's commit (a layout effect) is deferred until that
18
+ * commit finishes, so its frame would not exist when the effect reads it. A
19
+ * microtask runs after the commit has unwound, where the offscreen render is
20
+ * synchronous again. Everything the pass needs is read from refs for that reason.
21
+ *
22
+ * `printedIds` is the ONLY authority on "already printed" (RV-7). The cursor is just
23
+ * a hint for where to resume scanning: entries leave the list by retain-ring
24
+ * trimming AND by reducer deletion (empty settled assistants, abandoned tool
25
+ * cards), so an id-keyed cursor can dangle, and then the only correct answer is to
26
+ * rescan and let `printedIds` skip.
27
+ */
28
+ import React from 'react';
29
+ import type { Entry } from '../../agent/reducer.js';
30
+ import type { TermCapabilities } from '../capabilities.js';
31
+ import type { DensityMode } from '../density.js';
32
+ import type { Theme } from '../theme.js';
33
+ import { type ScrollbackBudget } from './budget.js';
34
+ import type { ScrollbackDrain } from './drain.js';
35
+ import type { ScrollbackWriter } from './scrollback-writer.js';
36
+ /** The channel between `cli.tsx` (which owns the stream) and the React tree. */
37
+ export interface ScrollbackBridge {
38
+ writer: ScrollbackWriter;
39
+ drain: ScrollbackDrain;
40
+ /** Installed by `cli.tsx`: schedule `writer.repaint()` soon. */
41
+ requestFlush: () => void;
42
+ /**
43
+ * Installed by the hook on mount (reads the latest entries through a ref).
44
+ * `restore()` calls it on every exit path; it is idempotent through `printedIds`
45
+ * and it releases the offscreen renderer when done.
46
+ */
47
+ settleAllNow: () => void;
48
+ /** Installed by `App`; at most `exitSummaryMaxRows` rows. Wrapped in try/catch by the caller. */
49
+ getSummaryRows: () => readonly string[];
50
+ /** Installed by `App`: the writer swallowed repeated tall frames (degrade to the placeholder). */
51
+ onGuardTrip: (() => void) | null;
52
+ }
53
+ export declare function createScrollbackBridge(parts: Pick<ScrollbackBridge, 'writer' | 'drain'> & {
54
+ requestFlush?: () => void;
55
+ }): ScrollbackBridge;
56
+ export type ScrollbackResetKind = 'clear' | 'restore';
57
+ export interface UseScrollbackViewArgs {
58
+ /** `scrollMode === 'native'`. */
59
+ enabled: boolean;
60
+ bridge: ScrollbackBridge | undefined;
61
+ entries: readonly Entry[];
62
+ runActive: boolean;
63
+ theme: Theme;
64
+ caps: TermCapabilities;
65
+ density: DensityMode;
66
+ thinkingVisible: boolean;
67
+ reducedMotion: boolean;
68
+ /** Terminal size. */
69
+ rows: number;
70
+ cols: number;
71
+ /** Printed first, once. */
72
+ headerNode: React.ReactNode;
73
+ /** Printed after the header for an empty session. */
74
+ openerNode: React.ReactNode | null;
75
+ overlayOpen: boolean;
76
+ draftRows: number;
77
+ popupRows: number;
78
+ statusExpanded: boolean;
79
+ /** Degrade the region to the placeholder (the writer's tall-frame guard tripped). */
80
+ forceInactive?: boolean;
81
+ }
82
+ export interface ScrollbackView {
83
+ budget: ScrollbackBudget | null;
84
+ liveTailLines: string[];
85
+ /** Print a fresh EXPANDED copy of an entry; false when it cannot be found / rendered. */
86
+ reprintExpanded(entryId: string): boolean;
87
+ /** Promote everything printable now (shutdown). */
88
+ settleAll(): void;
89
+ /** Called by `App`'s dispatch wrapper BEFORE a reset action reaches the reducer. */
90
+ notifyReset(kind: ScrollbackResetKind): void;
91
+ }
92
+ export declare function useScrollbackView(args: UseScrollbackViewArgs): ScrollbackView;