@aragon-agent/cli 0.6.29 → 0.6.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2223 -2103
- package/README.md +2957 -2759
- package/dist/agent/controller.d.ts +46 -0
- package/dist/agent/controller.js +175 -7
- package/dist/agent/job-notices.d.ts +143 -0
- package/dist/agent/job-notices.js +182 -0
- package/dist/agent/project-guidance-prompt.js +20 -20
- package/dist/agent/reducer.d.ts +21 -1
- package/dist/agent/reducer.js +7 -0
- package/dist/agent/system-prompt.d.ts +12 -0
- package/dist/agent/system-prompt.js +2 -0
- package/dist/cli.js +399 -62
- package/dist/commands/builtins.js +27 -8
- package/dist/commands/perf.d.ts +79 -2
- package/dist/commands/perf.js +51 -3
- package/dist/commands/registry.d.ts +6 -0
- package/dist/config/cli-commands.d.ts +27 -0
- package/dist/config/cli-commands.js +54 -1
- package/dist/config/env.js +42 -0
- package/dist/config/load.d.ts +3 -1
- package/dist/config/load.js +61 -1
- package/dist/config/schema.d.ts +100 -0
- package/dist/config/schema.js +43 -0
- package/dist/config/store.js +14 -1
- package/dist/config/ui-state.d.ts +9 -0
- package/dist/config/ui-state.js +9 -0
- package/dist/exec/events.d.ts +29 -3
- package/dist/exec/events.js +3 -12
- package/dist/exec/runner.d.ts +24 -0
- package/dist/exec/runner.js +79 -0
- package/dist/input/cursor-report.d.ts +61 -0
- package/dist/input/cursor-report.js +97 -0
- package/dist/input/stdin-filter.d.ts +22 -0
- package/dist/input/stdin-filter.js +48 -3
- package/dist/proc/limits.d.ts +76 -2
- package/dist/proc/limits.js +83 -2
- package/dist/proc/log-mirror.d.ts +59 -0
- package/dist/proc/log-mirror.js +203 -0
- package/dist/proc/log-ring.d.ts +22 -0
- package/dist/proc/log-ring.js +20 -1
- package/dist/proc/notices.d.ts +57 -0
- package/dist/proc/notices.js +101 -0
- package/dist/proc/progress.d.ts +40 -0
- package/dist/proc/progress.js +135 -0
- package/dist/proc/prompt.d.ts +24 -2
- package/dist/proc/prompt.js +51 -2
- package/dist/proc/supervisor.d.ts +35 -2
- package/dist/proc/supervisor.js +142 -43
- package/dist/proc/types.d.ts +89 -1
- package/dist/proc/wait.d.ts +57 -0
- package/dist/proc/wait.js +133 -0
- package/dist/session/persist.js +3 -1
- package/dist/session/validate-session.js +17 -0
- package/dist/tools/bash-tool.d.ts +12 -1
- package/dist/tools/bash-tool.js +100 -13
- package/dist/tools/index.d.ts +26 -2
- package/dist/tools/index.js +43 -2
- package/dist/tools/job-tools.d.ts +54 -0
- package/dist/tools/job-tools.js +329 -0
- package/dist/tools/proc-tools.d.ts +1 -1
- package/dist/tools/proc-tools.js +49 -17
- package/dist/ui/ActivityLine.js +7 -0
- package/dist/ui/App.d.ts +11 -1
- package/dist/ui/App.js +181 -63
- package/dist/ui/ScrollbackShell.d.ts +54 -0
- package/dist/ui/ScrollbackShell.js +15 -0
- package/dist/ui/StatusBar.d.ts +5 -0
- package/dist/ui/StatusBar.js +1 -0
- package/dist/ui/Transcript.d.ts +14 -0
- package/dist/ui/Transcript.js +14 -2
- package/dist/ui/entries/ServiceCard.d.ts +17 -1
- package/dist/ui/entries/ServiceCard.js +39 -4
- package/dist/ui/frame-differ.d.ts +12 -0
- package/dist/ui/layout/status-layout.d.ts +13 -1
- package/dist/ui/layout/status-layout.js +10 -0
- package/dist/ui/layout/virtual-window.js +14 -1
- package/dist/ui/overlays/HelpOverlay.d.ts +1 -1
- package/dist/ui/overlays/HelpOverlay.js +21 -3
- package/dist/ui/run-status-row.d.ts +12 -0
- package/dist/ui/screen.d.ts +29 -0
- package/dist/ui/screen.js +40 -0
- package/dist/ui/scrollback/LiveTail.d.ts +64 -0
- package/dist/ui/scrollback/LiveTail.js +27 -0
- package/dist/ui/scrollback/band.d.ts +105 -0
- package/dist/ui/scrollback/band.js +109 -0
- package/dist/ui/scrollback/budget.d.ts +72 -0
- package/dist/ui/scrollback/budget.js +81 -0
- package/dist/ui/scrollback/canvas.d.ts +99 -0
- package/dist/ui/scrollback/canvas.js +113 -0
- package/dist/ui/scrollback/cursor-probe.d.ts +71 -0
- package/dist/ui/scrollback/cursor-probe.js +133 -0
- package/dist/ui/scrollback/drain.d.ts +32 -0
- package/dist/ui/scrollback/drain.js +64 -0
- package/dist/ui/scrollback/exit-summary.d.ts +23 -0
- package/dist/ui/scrollback/exit-summary.js +25 -0
- package/dist/ui/scrollback/limits.d.ts +74 -0
- package/dist/ui/scrollback/limits.js +74 -0
- package/dist/ui/scrollback/live-preview.d.ts +26 -0
- package/dist/ui/scrollback/live-preview.js +106 -0
- package/dist/ui/scrollback/renderer.d.ts +38 -0
- package/dist/ui/scrollback/renderer.js +158 -0
- package/dist/ui/scrollback/scrollback-writer.d.ts +162 -0
- package/dist/ui/scrollback/scrollback-writer.js +549 -0
- package/dist/ui/scrollback/settled-boundary.d.ts +42 -0
- package/dist/ui/scrollback/settled-boundary.js +106 -0
- package/dist/ui/scrollback/use-scrollback-view.d.ts +103 -0
- package/dist/ui/scrollback/use-scrollback-view.js +294 -0
- package/dist/ui/scrollback/writer-shapes.d.ts +127 -0
- package/dist/ui/scrollback/writer-shapes.js +159 -0
- package/dist/ui/stdout-frame-writer.js +12 -2
- package/dist/ui/terminal-plan.d.ts +52 -0
- package/dist/ui/terminal-plan.js +49 -0
- package/dist/ui/transcript-text.d.ts +11 -0
- package/dist/ui/transcript-text.js +21 -7
- package/dist/ui/use-startup-notices.d.ts +12 -1
- package/dist/ui/use-startup-notices.js +28 -2
- package/package.json +3 -3
- package/skills/project-indexer/SKILL.md +163 -163
- package/skills/project-indexer/templates/cleancode-overrides.md +46 -46
- package/skills/project-indexer/templates/cleancode-template.md +94 -94
- package/skills/project-indexer/templates/config-template.md +69 -69
- package/skills/project-indexer/templates/index-template.md +129 -129
- package/skills/skill-creator/SKILL.md +96 -96
|
@@ -0,0 +1,549 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ScrollbackWriter — the byte contract of native-scroll mode
|
|
3
|
+
* (tui-native-terminal-scroll §4.3, extended by tui-bottom-anchored-live-region
|
|
4
|
+
* and re-based on a CANVAS by tui-canvas-bottom-anchor).
|
|
5
|
+
*
|
|
6
|
+
* It implements `FrameDiffer`, so `stdout-frame-writer.ts::wrapStdoutForFrames`
|
|
7
|
+
* binds it to a real stream exactly as it binds the full-screen differ. What it
|
|
8
|
+
* does differently: the screen is NOT the app's. Settled conversation rows are
|
|
9
|
+
* printed into the host terminal's own scrollback, and Ink's frame is only a small
|
|
10
|
+
* LIVE REGION parked under the last printed row.
|
|
11
|
+
*
|
|
12
|
+
* GEOMETRY IS RELATIVE (D2/D4). A batch is `CUU(canvasRows)` to the top of the
|
|
13
|
+
* previous CANVAS, erase down, skip the gap, print the content rows, print the
|
|
14
|
+
* region, park on the last row. Nothing needs the terminal's absolute row count, so
|
|
15
|
+
* a terminal that scrolls underneath cannot misplace anything.
|
|
16
|
+
*
|
|
17
|
+
* THE CANVAS (tui-canvas-bottom-anchor D1/D3). The `rows - 1` rows above the parked
|
|
18
|
+
* row belong to the writer and are always laid out top-down as
|
|
19
|
+
* `[gap][content][region]`. The previous feature put its blank rows between the
|
|
20
|
+
* content and the region (`[content][pad][region]`), which is why a short
|
|
21
|
+
* conversation left a reply floating far above the composer. The blank space now
|
|
22
|
+
* lives above every visible content row, so the newest printed row always touches
|
|
23
|
+
* the top row of the live region, while the composer stays on the last two rows.
|
|
24
|
+
* The gap is SKIPPED with CUD and never written, so a blank row can never be pushed
|
|
25
|
+
* into the scrollback (I-B3).
|
|
26
|
+
*
|
|
27
|
+
* THE PRICE, and it is the only one: the content rows still on the canvas can be
|
|
28
|
+
* re-drawn when the layout changes (a region collapse, a longer draft), because the
|
|
29
|
+
* rows that the region gives back have to move onto the canvas top. That is why the
|
|
30
|
+
* writer keeps a bounded cache of them (`held`, at most `capacity - H` rows). A row
|
|
31
|
+
* that leaves the canvas — pushed into the scrollback or erased at exit — is never
|
|
32
|
+
* written again, so the history the user scrolls through is exactly what was
|
|
33
|
+
* printed, once (I-B5). `anchor: unknown` (nothing measured: no TTY, no filter, no
|
|
34
|
+
* CPR answer) keeps the pre-feature bytes exactly, which is what keeps the old byte
|
|
35
|
+
* tests meaningful (I-B7).
|
|
36
|
+
*
|
|
37
|
+
* THE INVARIANTS THAT FAIL SILENTLY IF DROPPED:
|
|
38
|
+
* - I-S4 · every batch parks the cursor one row below the region, column 0, the
|
|
39
|
+
* same place Ink's `previousLineCount = H + 1` expects.
|
|
40
|
+
* - I-S8 · geometry in doubt (foreign write, unparseable chunk) => `prev = null`
|
|
41
|
+
* => next batch is a SEED. Never keep erasing on a stale height.
|
|
42
|
+
* - I-S14 · after `finalize` EVERY Ink write is swallowed.
|
|
43
|
+
* - I-S13 · no emitted row is wider than `cols - 1` (clipped, counted).
|
|
44
|
+
* - I-B1 · a batch ends parked on `rows - 1`, which is what the next `CUU` assumes.
|
|
45
|
+
* - I-B3 · blank rows only ever exist as SKIPPED rows, and a batch that scrolls has
|
|
46
|
+
* `gap = 0`, so the scrollback never receives an empty row from this file.
|
|
47
|
+
* - I-B5 · `held` is a contiguous suffix of the printed content, and `dropped` is
|
|
48
|
+
* exactly the number of rows this batch pushed off the top. The BYTES carry the
|
|
49
|
+
* untrimmed content: the terminal can only scroll away rows it was given.
|
|
50
|
+
* - I-B14 · a re-measurement re-derives `capacity` from `spaceBelow + canvasRows`,
|
|
51
|
+
* never from `spaceBelow` alone: the canvas itself still owns the rows above the
|
|
52
|
+
* cursor, and forgetting them makes the next batch read its own screen as an
|
|
53
|
+
* overflow and print every visible row a second time.
|
|
54
|
+
* - I-B15 · every batch starts by reconciling `top + capacity` with the live
|
|
55
|
+
* `rows - 1` (a resize event can arrive one frame late), and a column change
|
|
56
|
+
* resets before it writes.
|
|
57
|
+
* - I-A9 · the writer never writes a probe byte; it only asks the host, which owns
|
|
58
|
+
* deferral, coalescing and the death switch.
|
|
59
|
+
*/
|
|
60
|
+
import { parseInkFrame } from '../frame-parser.js';
|
|
61
|
+
import { clampSpaceBelow, resolveCanvasLayout, screenCapacity, UNKNOWN_CANVAS, applyResizeGrowth, } from './canvas.js';
|
|
62
|
+
import { SCROLLBACK_LIMITS } from './limits.js';
|
|
63
|
+
import { canvasBytes, canvasSeedBytes, clipLine, diffBytes, finalizeBytes, fullBytes, seedBytes, } from './writer-shapes.js';
|
|
64
|
+
/** `ansiEscapes.clearTerminal` — Ink's tall-frame write. It contains `ESC[3J`. */
|
|
65
|
+
const CLEAR_TERMINAL = '\x1b[2J\x1b[3J\x1b[H';
|
|
66
|
+
export function createScrollbackWriter(o) {
|
|
67
|
+
const now = o.now ?? Date.now;
|
|
68
|
+
const maxBatch = SCROLLBACK_LIMITS.maxRowsPerBatch;
|
|
69
|
+
/** Where the bottom of the screen is, as far as anything has measured it. */
|
|
70
|
+
let anchor = UNKNOWN_CANVAS;
|
|
71
|
+
/** Content rows still on the canvas, oldest first, already clipped (I-B9). */
|
|
72
|
+
let held = [];
|
|
73
|
+
/** Gap rows of the previous batch. */
|
|
74
|
+
let gapRows = 0;
|
|
75
|
+
/** Frame rows of the previous batch (`regionRows()`). */
|
|
76
|
+
let frameRows = 0;
|
|
77
|
+
/** Canvas height of the previous batch = the erase distance. */
|
|
78
|
+
let canvasRows = 0;
|
|
79
|
+
/** The canvas as it stands on screen (`''` = a skipped gap row), or null if lost. */
|
|
80
|
+
let prev = null;
|
|
81
|
+
/** The last frame BEFORE clipping, so unchanged rows are not re-measured. */
|
|
82
|
+
let prevRawFrame = null;
|
|
83
|
+
/** The last frame AFTER clipping, aligned with `prevRawFrame` (P2-3). */
|
|
84
|
+
let prevClippedFrame = null;
|
|
85
|
+
let everWritten = false;
|
|
86
|
+
let seenAnyChunk = false;
|
|
87
|
+
let finalized = false;
|
|
88
|
+
let guardTripped = false;
|
|
89
|
+
let consecutiveGuard = 0;
|
|
90
|
+
let lastResizeAt = Number.NEGATIVE_INFINITY;
|
|
91
|
+
/** Width the previous canvas was written at, for the column-change reset. */
|
|
92
|
+
let lastWriteCols;
|
|
93
|
+
/** Terminal height at the previous batch: the basis of a resize delta. */
|
|
94
|
+
let lastRows;
|
|
95
|
+
let repaintQueued = false;
|
|
96
|
+
let anchorSource = 'none';
|
|
97
|
+
/** A fresh measurement is waiting to be consumed by the next write. */
|
|
98
|
+
let anchorDirty = false;
|
|
99
|
+
let anchorReprobes = 0;
|
|
100
|
+
let scrolledRows = 0;
|
|
101
|
+
/**
|
|
102
|
+
* Rows between the cursor and the canvas top for the NEXT seed, when the canvas is
|
|
103
|
+
* shorter than the measured space below the cursor (a terminal taller than
|
|
104
|
+
* `maxCanvasRows`). Zero in every other state, because a canvas that exists has its
|
|
105
|
+
* top above the parked cursor by construction.
|
|
106
|
+
*/
|
|
107
|
+
let seedDownToTop = 0;
|
|
108
|
+
const stats = {
|
|
109
|
+
framesSeed: 0, framesDiff: 0, framesFull: 0, staticRowsWritten: 0, bytesWritten: 0,
|
|
110
|
+
fallbacks: 0, guardHits: 0, guardHitsOutsideResize: 0, clippedRows: 0, elidedEntries: 0,
|
|
111
|
+
resizes: 0, repaints: 0, anchorSource: 'none', anchorCanvasRows: 0, anchorHeldRows: 0,
|
|
112
|
+
anchorGapRows: 0, anchorScrollRows: 0, anchorResets: 0, anchorReprobes: 0,
|
|
113
|
+
};
|
|
114
|
+
const liveCols = () => {
|
|
115
|
+
const cols = o.cols();
|
|
116
|
+
return cols !== undefined && Number.isFinite(cols) && cols >= 1 ? Math.floor(cols) : undefined;
|
|
117
|
+
};
|
|
118
|
+
const liveRows = () => {
|
|
119
|
+
const rows = o.rows();
|
|
120
|
+
return rows !== undefined && Number.isFinite(rows) ? Math.floor(rows) : undefined;
|
|
121
|
+
};
|
|
122
|
+
const scheduleRepaint = () => {
|
|
123
|
+
if (!o.requestRepaint || repaintQueued || finalized)
|
|
124
|
+
return;
|
|
125
|
+
repaintQueued = true;
|
|
126
|
+
// Deferred by the host (setImmediate): this can fire from inside Ink's write.
|
|
127
|
+
o.requestRepaint();
|
|
128
|
+
};
|
|
129
|
+
/**
|
|
130
|
+
* Ask for a measurement. Never writes anything itself, and never after `finalize`
|
|
131
|
+
* (I-A9): the host owns the deferral, the coalescing and the death switch.
|
|
132
|
+
*/
|
|
133
|
+
const requestProbe = () => {
|
|
134
|
+
if (finalized || anchorSource === 'none')
|
|
135
|
+
return;
|
|
136
|
+
o.requestAnchorProbe?.();
|
|
137
|
+
};
|
|
138
|
+
/** Report what the canvas looks like right now. Produces no bytes. */
|
|
139
|
+
const publishAnchorStats = () => {
|
|
140
|
+
const pinned = anchor.kind === 'pinned';
|
|
141
|
+
stats.anchorSource = anchorSource;
|
|
142
|
+
stats.anchorCanvasRows = pinned ? canvasRows : 0;
|
|
143
|
+
stats.anchorHeldRows = pinned ? held.length : 0;
|
|
144
|
+
stats.anchorGapRows = pinned ? gapRows : 0;
|
|
145
|
+
stats.anchorScrollRows = scrolledRows;
|
|
146
|
+
stats.anchorReprobes = anchorReprobes;
|
|
147
|
+
};
|
|
148
|
+
/**
|
|
149
|
+
* Drop the canvas and go back to the append path (D5, I-B8, I-A3).
|
|
150
|
+
*
|
|
151
|
+
* Shared by a column change, a shrink, `invalidate()` and a lost geometry: all
|
|
152
|
+
* four have the same safe answer, "re-measure and print nothing until then".
|
|
153
|
+
* The rows already on screen are NOT redrawn — they are not where the cache says
|
|
154
|
+
* they are, and a wrong erase both duplicates and destroys user-visible content.
|
|
155
|
+
*/
|
|
156
|
+
const reset = () => {
|
|
157
|
+
prev = null;
|
|
158
|
+
prevRawFrame = null;
|
|
159
|
+
prevClippedFrame = null;
|
|
160
|
+
held = [];
|
|
161
|
+
gapRows = 0;
|
|
162
|
+
canvasRows = 0;
|
|
163
|
+
seedDownToTop = 0;
|
|
164
|
+
anchor = UNKNOWN_CANVAS;
|
|
165
|
+
anchorDirty = false;
|
|
166
|
+
stats.anchorResets += 1;
|
|
167
|
+
publishAnchorStats();
|
|
168
|
+
requestProbe();
|
|
169
|
+
};
|
|
170
|
+
/** Clip a frame row to `cols - 1`, reusing the previous result for unchanged rows. */
|
|
171
|
+
const clipFrame = (raw, cols) => {
|
|
172
|
+
if (cols === undefined)
|
|
173
|
+
return [...raw];
|
|
174
|
+
const limit = cols - 1;
|
|
175
|
+
return raw.map((line, i) => {
|
|
176
|
+
const cached = prevClippedFrame?.[i];
|
|
177
|
+
if (cached !== undefined && prevRawFrame?.[i] === line)
|
|
178
|
+
return cached;
|
|
179
|
+
const result = clipLine(line, limit);
|
|
180
|
+
if (result.clipped)
|
|
181
|
+
stats.clippedRows += 1;
|
|
182
|
+
return result.text;
|
|
183
|
+
});
|
|
184
|
+
};
|
|
185
|
+
const clipStatics = (rows, cols) => {
|
|
186
|
+
if (cols === undefined || rows.length === 0)
|
|
187
|
+
return rows;
|
|
188
|
+
return rows.map((line) => {
|
|
189
|
+
const result = clipLine(line, cols - 1);
|
|
190
|
+
if (result.clipped)
|
|
191
|
+
stats.clippedRows += 1;
|
|
192
|
+
return result.text;
|
|
193
|
+
});
|
|
194
|
+
};
|
|
195
|
+
const record = (bytes, shape, statics) => {
|
|
196
|
+
if (shape === 'seed')
|
|
197
|
+
stats.framesSeed += 1;
|
|
198
|
+
else if (shape === 'diff')
|
|
199
|
+
stats.framesDiff += 1;
|
|
200
|
+
else if (shape === 'full')
|
|
201
|
+
stats.framesFull += 1;
|
|
202
|
+
else
|
|
203
|
+
stats.repaints += 1;
|
|
204
|
+
stats.staticRowsWritten += statics;
|
|
205
|
+
stats.bytesWritten += bytes.length;
|
|
206
|
+
return bytes;
|
|
207
|
+
};
|
|
208
|
+
/**
|
|
209
|
+
* Bring `top + capacity` back in line with the live `rows - 1` BEFORE the batch
|
|
210
|
+
* reads either of them (§6.11, [P0-2]).
|
|
211
|
+
*
|
|
212
|
+
* The `resize` event is dispatched a libuv turn after `stdout.rows` changed, so a
|
|
213
|
+
* frame can arrive on the new grid while the anchor still describes the old one.
|
|
214
|
+
* The pre-canvas writer healed that one batch later, by deriving `gap` from the
|
|
215
|
+
* current height; a canvas cannot, because `top` and `capacity` only ever change
|
|
216
|
+
* where they are measured — and the batch that ran on the stale grid would record
|
|
217
|
+
* the new height, leaving the late `onResize` with a delta of zero. On a terminal
|
|
218
|
+
* that never answers CPR that is a composer stuck mid-screen for the session.
|
|
219
|
+
*/
|
|
220
|
+
const reconcile = (rows, cols) => {
|
|
221
|
+
if (anchor.kind !== 'pinned')
|
|
222
|
+
return;
|
|
223
|
+
if (cols !== undefined && lastWriteCols !== undefined && cols !== lastWriteCols) {
|
|
224
|
+
reset();
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const capacity = screenCapacity(rows);
|
|
228
|
+
const parked = anchor.top + anchor.capacity;
|
|
229
|
+
if (capacity === parked)
|
|
230
|
+
return;
|
|
231
|
+
if (capacity > parked) {
|
|
232
|
+
// The terminal grew below the cursor: the cursor did not move, so the canvas
|
|
233
|
+
// owns exactly the extra rows.
|
|
234
|
+
anchor = { kind: 'pinned', top: anchor.top, capacity: anchor.capacity + (capacity - parked) };
|
|
235
|
+
lastRows = rows;
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
reset();
|
|
239
|
+
};
|
|
240
|
+
const afterCanvasBatch = (layout, heldNext, frame, raw, cols, rows) => {
|
|
241
|
+
prev = [...new Array(layout.gap).fill(''), ...heldNext, ...frame];
|
|
242
|
+
prevRawFrame = [...raw];
|
|
243
|
+
prevClippedFrame = [...frame];
|
|
244
|
+
canvasRows = prev.length;
|
|
245
|
+
held = [...heldNext];
|
|
246
|
+
gapRows = layout.gap;
|
|
247
|
+
frameRows = frame.length;
|
|
248
|
+
anchor = { kind: 'pinned', top: layout.top, capacity: layout.canvasRows };
|
|
249
|
+
everWritten = true;
|
|
250
|
+
lastWriteCols = cols;
|
|
251
|
+
lastRows = rows;
|
|
252
|
+
anchorDirty = false;
|
|
253
|
+
seedDownToTop = 0;
|
|
254
|
+
scrolledRows += layout.scroll;
|
|
255
|
+
publishAnchorStats();
|
|
256
|
+
// A feed larger than one batch continues on the next tick, region still parked.
|
|
257
|
+
if (o.drain.pending() > 0) {
|
|
258
|
+
repaintQueued = false;
|
|
259
|
+
scheduleRepaint();
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
const afterLegacyBatch = (raw, frame, cols, rows) => {
|
|
263
|
+
prev = [...frame];
|
|
264
|
+
prevRawFrame = [...raw];
|
|
265
|
+
prevClippedFrame = [...frame];
|
|
266
|
+
// The region IS the erasable block on this path, which is also the number a
|
|
267
|
+
// later measurement needs in order to know how far above the cursor its top is.
|
|
268
|
+
canvasRows = frame.length;
|
|
269
|
+
gapRows = 0;
|
|
270
|
+
frameRows = frame.length;
|
|
271
|
+
held = [];
|
|
272
|
+
everWritten = true;
|
|
273
|
+
lastWriteCols = cols;
|
|
274
|
+
lastRows = rows;
|
|
275
|
+
anchorDirty = false;
|
|
276
|
+
publishAnchorStats();
|
|
277
|
+
if (o.drain.pending() > 0) {
|
|
278
|
+
repaintQueued = false;
|
|
279
|
+
scheduleRepaint();
|
|
280
|
+
}
|
|
281
|
+
};
|
|
282
|
+
/**
|
|
283
|
+
* The pre-canvas append path: static rows above a region that is never redrawn.
|
|
284
|
+
* Byte-identical to the writer before either anchor existed (I-B7): `seedBytes` /
|
|
285
|
+
* `fullBytes` / `diffBytes`, `eraseUp = canvasRows` (= the frame height).
|
|
286
|
+
*/
|
|
287
|
+
const assembleLegacy = (raw, statics, frame, rows, cols, mode) => {
|
|
288
|
+
let out;
|
|
289
|
+
if (prev === null) {
|
|
290
|
+
out = record(seedBytes({ lines: frame, statics, sync: o.sync, reseed: everWritten }), 'seed', statics.length);
|
|
291
|
+
}
|
|
292
|
+
else if (mode === 'repaint') {
|
|
293
|
+
out = record(fullBytes({ lines: frame, statics, eraseUp: canvasRows, sync: o.sync }), 'repaint', statics.length);
|
|
294
|
+
}
|
|
295
|
+
else if (statics.length > 0 || frame.length !== canvasRows || o.fullEveryFrame) {
|
|
296
|
+
out = record(fullBytes({ lines: frame, statics, eraseUp: canvasRows, sync: o.sync }), 'full', statics.length);
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
out = record(diffBytes({ lines: frame, prev, sync: o.sync }), 'diff', 0);
|
|
300
|
+
}
|
|
301
|
+
afterLegacyBatch(raw, frame, cols, rows);
|
|
302
|
+
return out;
|
|
303
|
+
};
|
|
304
|
+
/**
|
|
305
|
+
* A canvas batch repaints EVERYTHING when the layout moved, and only the rows that
|
|
306
|
+
* differ when it did not. `layout.scroll > 0` forces the full shape for a reason
|
|
307
|
+
* the other four conditions do not cover: when the canvas is already the whole
|
|
308
|
+
* screen its height cannot change, so a batch that pushes rows out would otherwise
|
|
309
|
+
* be diffed against rows it has just lost.
|
|
310
|
+
*/
|
|
311
|
+
const needsCanvasShape = (layout, frame, statics, mode) => {
|
|
312
|
+
if (mode === 'repaint')
|
|
313
|
+
return true;
|
|
314
|
+
return statics > 0 || layout.scroll > 0 || layout.gap !== gapRows ||
|
|
315
|
+
layout.canvasRows !== canvasRows || frame.length !== frameRows || o.fullEveryFrame;
|
|
316
|
+
};
|
|
317
|
+
const emitCanvas = (layout, content, kept, frame, statics, mode) => {
|
|
318
|
+
if (prev === null) {
|
|
319
|
+
return record(canvasSeedBytes({
|
|
320
|
+
downToTop: seedDownToTop,
|
|
321
|
+
gap: layout.gap, held: content, lines: frame, sync: o.sync, reseed: everWritten,
|
|
322
|
+
}), 'seed', statics);
|
|
323
|
+
}
|
|
324
|
+
if (needsCanvasShape(layout, frame, statics, mode)) {
|
|
325
|
+
return record(canvasBytes({
|
|
326
|
+
eraseUp: canvasRows, gap: layout.gap, held: content, lines: frame, sync: o.sync,
|
|
327
|
+
}), mode === 'repaint' ? 'repaint' : 'full', statics);
|
|
328
|
+
}
|
|
329
|
+
const shown = [...new Array(layout.gap).fill(''), ...kept, ...frame];
|
|
330
|
+
return record(diffBytes({ lines: shown, prev, sync: o.sync }), 'diff', 0);
|
|
331
|
+
};
|
|
332
|
+
/**
|
|
333
|
+
* THE ONE PLACE THE GEOMETRY IS COMPUTED (§6.2), shared by `transform` and
|
|
334
|
+
* `repaint` so the two can never disagree about where the region goes.
|
|
335
|
+
*
|
|
336
|
+
* Order is load-bearing: reconcile, then static rows, then the layout, then the
|
|
337
|
+
* bytes. `ED0`, the gap, the content, the region — in that order, always.
|
|
338
|
+
*/
|
|
339
|
+
const assemble = (raw, rows, cols, mode) => {
|
|
340
|
+
reconcile(rows, cols);
|
|
341
|
+
const statics = clipStatics(o.drain.takeUpTo(maxBatch), cols);
|
|
342
|
+
const frame = clipFrame(raw, cols);
|
|
343
|
+
if (anchor.kind === 'unknown') {
|
|
344
|
+
return assembleLegacy(raw, statics, frame, rows, cols, mode);
|
|
345
|
+
}
|
|
346
|
+
const layout = resolveCanvasLayout({
|
|
347
|
+
capacity: anchor.capacity,
|
|
348
|
+
top: anchor.top,
|
|
349
|
+
heldRows: held.length,
|
|
350
|
+
statRows: statics.length,
|
|
351
|
+
frameRows: frame.length,
|
|
352
|
+
lead: prev === null && everWritten ? 1 : 0,
|
|
353
|
+
});
|
|
354
|
+
// The bytes carry every content row; the ones that do not fit leave through the
|
|
355
|
+
// bottom, and the terminal can only scroll away rows it was given. `held` keeps
|
|
356
|
+
// the suffix that is still on screen (I-B5).
|
|
357
|
+
const content = held.concat(statics);
|
|
358
|
+
const kept = layout.dropped > 0 ? content.slice(layout.dropped) : content;
|
|
359
|
+
const out = emitCanvas(layout, content, kept, frame, statics.length, mode);
|
|
360
|
+
afterCanvasBatch(layout, kept, frame, raw, cols, rows);
|
|
361
|
+
return out;
|
|
362
|
+
};
|
|
363
|
+
const swallowTallFrame = () => {
|
|
364
|
+
stats.guardHits += 1;
|
|
365
|
+
if (now() - lastResizeAt >= SCROLLBACK_LIMITS.guardResizeGraceMs) {
|
|
366
|
+
stats.guardHitsOutsideResize += 1;
|
|
367
|
+
consecutiveGuard += 1;
|
|
368
|
+
if (consecutiveGuard >= SCROLLBACK_LIMITS.guardTripCount && !guardTripped) {
|
|
369
|
+
guardTripped = true;
|
|
370
|
+
o.onGuardTrip?.();
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
// Swallow the whole chunk: it would clear the visible screen AND scrollback.
|
|
374
|
+
return '';
|
|
375
|
+
};
|
|
376
|
+
const countFallback = (counted) => {
|
|
377
|
+
reset();
|
|
378
|
+
if (counted) {
|
|
379
|
+
stats.fallbacks += 1;
|
|
380
|
+
if (stats.fallbacks === 1)
|
|
381
|
+
o.onFirstFallback?.();
|
|
382
|
+
}
|
|
383
|
+
return null;
|
|
384
|
+
};
|
|
385
|
+
/**
|
|
386
|
+
* Ink wrote a frame we cannot address (a one-row terminal). Record only what the
|
|
387
|
+
* FRAME tells us (P2-6): a frame is not a canvas, so on the append path it is the
|
|
388
|
+
* block (`canvasRows = H`), and on the canvas path nothing survives but the fact
|
|
389
|
+
* that the geometry is lost.
|
|
390
|
+
*/
|
|
391
|
+
const passThrough = (lines) => {
|
|
392
|
+
if (lines === null)
|
|
393
|
+
return;
|
|
394
|
+
prevRawFrame = lines;
|
|
395
|
+
prevClippedFrame = lines;
|
|
396
|
+
frameRows = lines.length;
|
|
397
|
+
if (anchor.kind === 'pinned') {
|
|
398
|
+
reset();
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
prev = lines;
|
|
402
|
+
canvasRows = lines.length;
|
|
403
|
+
gapRows = 0;
|
|
404
|
+
};
|
|
405
|
+
const transform = (chunk) => {
|
|
406
|
+
if (finalized)
|
|
407
|
+
return '';
|
|
408
|
+
if (chunk.startsWith(CLEAR_TERMINAL))
|
|
409
|
+
return swallowTallFrame();
|
|
410
|
+
consecutiveGuard = 0;
|
|
411
|
+
guardTripped = false;
|
|
412
|
+
const firstChunk = !seenAnyChunk;
|
|
413
|
+
seenAnyChunk = true;
|
|
414
|
+
const rows = liveRows();
|
|
415
|
+
const lines = parseInkFrame(chunk);
|
|
416
|
+
if (rows === undefined || rows < 2) {
|
|
417
|
+
passThrough(lines);
|
|
418
|
+
return null;
|
|
419
|
+
}
|
|
420
|
+
if (lines === null)
|
|
421
|
+
return countFallback(!firstChunk);
|
|
422
|
+
if (lines.length + 1 > rows) {
|
|
423
|
+
// Unreachable by construction (I-S1); never erase on a guess.
|
|
424
|
+
reset();
|
|
425
|
+
return '';
|
|
426
|
+
}
|
|
427
|
+
return assemble(lines, rows, liveCols(), 'frame');
|
|
428
|
+
};
|
|
429
|
+
const repaint = () => {
|
|
430
|
+
repaintQueued = false;
|
|
431
|
+
if (finalized || prevClippedFrame === null || prevRawFrame === null)
|
|
432
|
+
return '';
|
|
433
|
+
// `anchorDirty` is why a probe result arriving while the feed is idle lands at
|
|
434
|
+
// all: without it, "re-pin after a resize" would write zero bytes (P0-2).
|
|
435
|
+
if (o.drain.pending() === 0 && !anchorDirty)
|
|
436
|
+
return '';
|
|
437
|
+
const rows = liveRows();
|
|
438
|
+
if (rows === undefined || rows < 2)
|
|
439
|
+
return '';
|
|
440
|
+
return assemble(prevRawFrame, rows, liveCols(), 'repaint');
|
|
441
|
+
};
|
|
442
|
+
const finalize = (summary) => {
|
|
443
|
+
if (finalized)
|
|
444
|
+
return '';
|
|
445
|
+
finalized = true;
|
|
446
|
+
const pending = clipStatics(o.drain.takeAll(), liveCols());
|
|
447
|
+
// Unknown geometry after a foreign write: no safe erase, start on a fresh line.
|
|
448
|
+
// Otherwise erase the whole CANVAS — the gap included, or its blank rows stay
|
|
449
|
+
// behind as a dead band above the summary (I-B11).
|
|
450
|
+
const eraseUp = prev === null && everWritten ? -1 : canvasRows;
|
|
451
|
+
// ... and reprint what the erase removes. The content rows live INSIDE the canvas,
|
|
452
|
+
// so erasing it without them would delete the user's last screenful of conversation
|
|
453
|
+
// at exactly the moment they want to read it (the pre-canvas writer never had to:
|
|
454
|
+
// its static rows were already outside the erasable block).
|
|
455
|
+
const bytes = finalizeBytes({ eraseUp, statics: [...held, ...pending], summary });
|
|
456
|
+
stats.staticRowsWritten += pending.length;
|
|
457
|
+
stats.bytesWritten += bytes.length;
|
|
458
|
+
return bytes;
|
|
459
|
+
};
|
|
460
|
+
o.drain.onEnqueue = scheduleRepaint;
|
|
461
|
+
return {
|
|
462
|
+
transform,
|
|
463
|
+
repaint,
|
|
464
|
+
finalize,
|
|
465
|
+
invalidate: reset,
|
|
466
|
+
onResize() {
|
|
467
|
+
stats.resizes += 1;
|
|
468
|
+
lastResizeAt = now();
|
|
469
|
+
consecutiveGuard = 0;
|
|
470
|
+
guardTripped = false;
|
|
471
|
+
const rowsNow = liveRows();
|
|
472
|
+
const colsNow = liveCols();
|
|
473
|
+
// A column change re-flows what is already on screen: the cached rows no longer
|
|
474
|
+
// describe it, and erasing by their height would cut a line in half (D5).
|
|
475
|
+
if (colsNow !== undefined && lastWriteCols !== undefined && colsNow !== lastWriteCols) {
|
|
476
|
+
reset();
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
if (rowsNow !== undefined && lastRows !== undefined) {
|
|
480
|
+
if (rowsNow < lastRows) {
|
|
481
|
+
// The terminal deleted rows; they are gone and must not be redrawn.
|
|
482
|
+
reset();
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (anchor.kind === 'pinned') {
|
|
486
|
+
const capacity = screenCapacity(rowsNow);
|
|
487
|
+
const grown = Math.min(applyResizeGrowth(anchor.capacity, lastRows, rowsNow), capacity);
|
|
488
|
+
anchor = { kind: 'pinned', top: Math.max(0, capacity - grown), capacity: grown };
|
|
489
|
+
}
|
|
490
|
+
lastRows = rowsNow;
|
|
491
|
+
}
|
|
492
|
+
// Coalesced and deferred by the host (P0-3); `anchorReprobes` is NOT bumped
|
|
493
|
+
// here — a drag fires dozens of resize events for one probe (P1-5).
|
|
494
|
+
requestProbe();
|
|
495
|
+
},
|
|
496
|
+
cursorNeutralWrite() {
|
|
497
|
+
// OSC 52 and friends move no cell and no cursor (I-S11).
|
|
498
|
+
},
|
|
499
|
+
regionRows: () => Math.max(0, frameRows),
|
|
500
|
+
scrollbackStats: () => ({ ...stats, elidedEntries: o.drain.elided() }),
|
|
501
|
+
setAnchorSpaceBelow(spaceBelow, source) {
|
|
502
|
+
if (finalized)
|
|
503
|
+
return;
|
|
504
|
+
const rows = liveRows();
|
|
505
|
+
const capacity = screenCapacity(rows);
|
|
506
|
+
// I-B14: the report is "rows BELOW the cursor", and the canvas also still owns
|
|
507
|
+
// `canvasRows` rows ABOVE it, so the reachable height is their sum. Keeping only
|
|
508
|
+
// the first term collapses the canvas onto the parked row, and the next batch
|
|
509
|
+
// then reads its own visible content as an overflow and scrolls it away twice.
|
|
510
|
+
const reach = clampSpaceBelow(spaceBelow, rows) + canvasRows;
|
|
511
|
+
// The cap bounds only a STARTING canvas: clipping a live one would make the
|
|
512
|
+
// next erase reach above its own top (see SCROLLBACK_LIMITS.maxCanvasRows).
|
|
513
|
+
const ceiling = canvasRows === 0
|
|
514
|
+
? Math.min(capacity, SCROLLBACK_LIMITS.maxCanvasRows)
|
|
515
|
+
: capacity;
|
|
516
|
+
const usable = Math.max(0, Math.min(reach, ceiling));
|
|
517
|
+
anchor = { kind: 'pinned', top: Math.max(0, capacity - usable), capacity: usable };
|
|
518
|
+
// A capped starting canvas begins BELOW the cursor row, so the batch that seeds
|
|
519
|
+
// it has to descend first: `spaceBelow - capacity` rows. Zero whenever the cap
|
|
520
|
+
// does not bind, and zero for every canvas that already exists.
|
|
521
|
+
seedDownToTop = Math.max(0, clampSpaceBelow(spaceBelow, rows) - usable);
|
|
522
|
+
// Only measurements AFTER the first are re-probes (P1-5): `/perf` promises a
|
|
523
|
+
// re-probe count, not a measurement count, and the startup measurement is
|
|
524
|
+
// reported through `anchorSource` instead.
|
|
525
|
+
if (anchorSource !== 'none')
|
|
526
|
+
anchorReprobes += 1;
|
|
527
|
+
anchorSource = source;
|
|
528
|
+
anchorDirty = true;
|
|
529
|
+
publishAnchorStats();
|
|
530
|
+
},
|
|
531
|
+
anchorStats: () => ({
|
|
532
|
+
state: anchor.kind === 'pinned' ? 'pinned' : 'unknown',
|
|
533
|
+
source: anchorSource,
|
|
534
|
+
canvasRows: anchor.kind === 'pinned' ? canvasRows : 0,
|
|
535
|
+
heldRows: anchor.kind === 'pinned' ? held.length : 0,
|
|
536
|
+
gapRows: anchor.kind === 'pinned' ? gapRows : 0,
|
|
537
|
+
top: anchor.kind === 'pinned' ? anchor.top : 0,
|
|
538
|
+
}),
|
|
539
|
+
stats: () => ({
|
|
540
|
+
framesTotal: stats.framesSeed + stats.framesFull + stats.framesDiff,
|
|
541
|
+
framesDiffed: stats.framesDiff,
|
|
542
|
+
framesFull: stats.framesSeed + stats.framesFull,
|
|
543
|
+
linesWritten: stats.staticRowsWritten,
|
|
544
|
+
bytesWritten: stats.bytesWritten,
|
|
545
|
+
fallbacks: stats.fallbacks,
|
|
546
|
+
repaints: stats.repaints,
|
|
547
|
+
}),
|
|
548
|
+
};
|
|
549
|
+
}
|
|
@@ -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[];
|