@aragon-agent/cli 0.6.32 → 0.6.34
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 +2305 -2223
- package/README.md +3007 -2957
- package/dist/cli.js +4 -0
- package/dist/commands/perf.d.ts +34 -1
- package/dist/commands/perf.js +11 -3
- package/dist/ui/App.js +64 -12
- package/dist/ui/ScrollbackShell.d.ts +48 -13
- package/dist/ui/ScrollbackShell.js +7 -3
- package/dist/ui/scrollback/LiveTail.d.ts +64 -15
- package/dist/ui/scrollback/LiveTail.js +27 -11
- package/dist/ui/scrollback/band.d.ts +33 -24
- package/dist/ui/scrollback/band.js +43 -22
- package/dist/ui/scrollback/budget.d.ts +31 -19
- package/dist/ui/scrollback/budget.js +37 -25
- package/dist/ui/scrollback/limits.d.ts +34 -10
- package/dist/ui/scrollback/limits.js +34 -10
- package/dist/ui/scrollback/live-preview.d.ts +18 -10
- package/dist/ui/scrollback/live-preview.js +14 -11
- package/dist/ui/scrollback/overflow-print.d.ts +108 -0
- package/dist/ui/scrollback/overflow-print.js +120 -0
- package/dist/ui/scrollback/renderer.d.ts +11 -1
- package/dist/ui/scrollback/renderer.js +53 -25
- package/dist/ui/scrollback/tail-anchor.d.ts +68 -0
- package/dist/ui/scrollback/tail-anchor.js +77 -0
- package/dist/ui/scrollback/tail-window.d.ts +61 -0
- package/dist/ui/scrollback/tail-window.js +74 -0
- package/dist/ui/scrollback/use-scrollback-view.d.ts +60 -24
- package/dist/ui/scrollback/use-scrollback-view.js +434 -134
- package/package.json +2 -2
package/dist/cli.js
CHANGED
|
@@ -901,6 +901,10 @@ async function runInteractive(flags, extras = {}) {
|
|
|
901
901
|
const { writer } = scrollbackBridge;
|
|
902
902
|
setScrollbackStatsProvider(() => ({
|
|
903
903
|
...writer.scrollbackStats(), regionRows: writer.regionRows(),
|
|
904
|
+
// The canvas top is already published by `anchorStats()`; `/perf` needs it on the
|
|
905
|
+
// mode line to explain a region that does not start on row 0 (tui-full-height-
|
|
906
|
+
// live-region §5.8). Read per call, never cached: `/perf` shows THIS frame.
|
|
907
|
+
anchorTop: writer.anchorStats().top,
|
|
904
908
|
}));
|
|
905
909
|
}
|
|
906
910
|
// The observer only serves the full-screen scrollbar geometry; native has none.
|
package/dist/commands/perf.d.ts
CHANGED
|
@@ -59,14 +59,41 @@ export interface PerfSnapshot {
|
|
|
59
59
|
*/
|
|
60
60
|
export interface BandPerf {
|
|
61
61
|
reason: 'visible' | 'empty' | 'disabled' | 'overlay' | 'narrow' | 'short';
|
|
62
|
+
/**
|
|
63
|
+
* The band's height. In the full-height region this IS the pool
|
|
64
|
+
* (`tui-full-height-live-region` D5), which is why `/perf` prints one `pool=` number
|
|
65
|
+
* rather than two fields that can only ever be equal.
|
|
66
|
+
*/
|
|
67
|
+
bandRows: number;
|
|
62
68
|
railRows: number;
|
|
63
69
|
railCols: number;
|
|
64
70
|
teamRows: number;
|
|
71
|
+
/** The tail column: the rows between the roster and the composer. */
|
|
65
72
|
previewRows: number;
|
|
66
73
|
contentCols: number;
|
|
67
74
|
previewCols: number;
|
|
68
75
|
heldRailRows: number;
|
|
69
76
|
heldTeamRows: number;
|
|
77
|
+
/**
|
|
78
|
+
* The tail window: first visible entry index and how many entries are in it.
|
|
79
|
+
*
|
|
80
|
+
* The ONLY number that makes "what is on screen" and "what has been printed"
|
|
81
|
+
* comparable. A long answer can push earlier entries out of sight while they are
|
|
82
|
+
* still inside the window, so they are neither visible nor printed yet; the readout
|
|
83
|
+
* turns that disclosed gap (known limitation 5) into a number instead of a guess.
|
|
84
|
+
*/
|
|
85
|
+
windowStart: number;
|
|
86
|
+
windowEntries: number;
|
|
87
|
+
/**
|
|
88
|
+
* Rows the TOP GIVE-WAY has printed for entries that are still inside the window.
|
|
89
|
+
*
|
|
90
|
+
* `window=` says what the region holds; `head=` says how much of it the region could
|
|
91
|
+
* not show and therefore gave back to the terminal scrollback
|
|
92
|
+
* (tui-final-reply-truncated-unscrollable). Zero is the healthy answer for a
|
|
93
|
+
* conversation that always fits; a growing number means the box is smaller than the
|
|
94
|
+
* reply, which is exactly the state that used to be unrecoverable.
|
|
95
|
+
*/
|
|
96
|
+
headRows: number;
|
|
70
97
|
}
|
|
71
98
|
/**
|
|
72
99
|
* Why the right-edge scrollbar is or is not usable, in one record
|
|
@@ -136,8 +163,14 @@ export interface ScrollbackPerf {
|
|
|
136
163
|
anchorCanvasRows: number;
|
|
137
164
|
/** Content rows inside that canvas — how much of the conversation is on screen. */
|
|
138
165
|
anchorHeldRows: number;
|
|
139
|
-
/** Blank rows above the content.
|
|
166
|
+
/** Blank rows above the content. Zero by construction in the full-height region. */
|
|
140
167
|
anchorGapRows: number;
|
|
168
|
+
/**
|
|
169
|
+
* Canvas top row (0-based). 0 means the canvas starts on the terminal's first row and
|
|
170
|
+
* is the direct answer to "why does the app start halfway down the screen" - the one
|
|
171
|
+
* number that used to be missing from this block.
|
|
172
|
+
*/
|
|
173
|
+
anchorTop: number;
|
|
141
174
|
/** Rows this session pushed into the terminal's own scrollback. */
|
|
142
175
|
anchorScrollRows: number;
|
|
143
176
|
/** Times the geometry was dropped on purpose (column change, shrink, geometry lost). */
|
package/dist/commands/perf.js
CHANGED
|
@@ -136,8 +136,10 @@ function formatBandLine(band) {
|
|
|
136
136
|
if (!band)
|
|
137
137
|
return 'band n/a (full-screen layout)';
|
|
138
138
|
return `band rail=${band.railRows}x${band.railCols} team=${band.teamRows} ` +
|
|
139
|
-
`
|
|
140
|
-
`
|
|
139
|
+
`tail=${band.previewRows} pool=${band.bandRows} ` +
|
|
140
|
+
`cols=${band.contentCols}/${band.previewCols} ` +
|
|
141
|
+
`reason=${band.reason} held=${band.heldRailRows}/${band.heldTeamRows} ` +
|
|
142
|
+
`window=${band.windowStart}/${band.windowEntries} head=${band.headRows}`;
|
|
141
143
|
}
|
|
142
144
|
/**
|
|
143
145
|
* Format a snapshot as the seven-line block documented in §5.4 (plus the scrollbar
|
|
@@ -149,6 +151,11 @@ export function formatPerfReport(snap) {
|
|
|
149
151
|
const eco = snap.rung > 0 ? ' - eco' : '';
|
|
150
152
|
const governor = snap.governorEnabled ? '' : ' - governor off';
|
|
151
153
|
const native = snap.mode === 'native';
|
|
154
|
+
// The canvas top lives in the writer's counters, not in the snapshot: it is the direct
|
|
155
|
+
// explanation for "the app starts halfway down", so it belongs on the mode line next
|
|
156
|
+
// to the region height. `n/a` rather than 0 when no writer is attached, because 0 is a
|
|
157
|
+
// real and healthy answer.
|
|
158
|
+
const anchor = native ? scrollbackStats?.() ?? null : null;
|
|
152
159
|
const scrollbar = native
|
|
153
160
|
? ['scrollbar n/a (native scroll)']
|
|
154
161
|
: snap.scrollbar ? [formatScrollbarLine(snap.scrollbar)] : [];
|
|
@@ -163,7 +170,8 @@ export function formatPerfReport(snap) {
|
|
|
163
170
|
`hl ${caches.highlightEntries} (${mb(caches.highlightBytes)}) - ` +
|
|
164
171
|
`lines ${caches.lineEntries}`,
|
|
165
172
|
native
|
|
166
|
-
? `mode native - live region ${snap.viewportRows} rows`
|
|
173
|
+
? `mode native - live region ${snap.viewportRows} rows - ` +
|
|
174
|
+
`top ${anchor ? anchor.anchorTop : 'n/a'}`
|
|
167
175
|
: `mode ${snap.mode} - viewport ${snap.viewportRows} rows - offset ${snap.offset}`,
|
|
168
176
|
formatBandLine(snap.band),
|
|
169
177
|
formatWriterLine(),
|
package/dist/ui/App.js
CHANGED
|
@@ -2102,12 +2102,30 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2102
2102
|
// `ctrl+o log` hint, the keystroke would silently expand AN OLDER TOOL
|
|
2103
2103
|
// CARD, and nothing would error. The terminal one-row record is skipped -
|
|
2104
2104
|
// it has nothing to reveal, and expanding it would consume the press.
|
|
2105
|
-
|
|
2105
|
+
//
|
|
2106
|
+
// TWO TIERS, AND THE ORDER IS THE WHOLE FIX (tui-final-reply-truncated-
|
|
2107
|
+
// unscrollable B1). The closed list above answers "which card is this key for",
|
|
2108
|
+
// and it must keep answering it EXACTLY as before: a settled answer is almost
|
|
2109
|
+
// always the newest entry, so adding `assistant` to THIS filter would take the
|
|
2110
|
+
// press away from the tool card the user just watched finish. Only when no card
|
|
2111
|
+
// is asking for the key does it fall through to the Agent's last answer - which
|
|
2112
|
+
// is the escape hatch `docs/plans/tui-full-height-live-region/spec.md` §13
|
|
2113
|
+
// (known limitation 2) already promised and never wired up.
|
|
2114
|
+
const entries = stateRef.current.entries;
|
|
2115
|
+
const card = [...entries]
|
|
2106
2116
|
.reverse()
|
|
2107
2117
|
.find((e) => e.kind === 'tool' ||
|
|
2108
2118
|
e.kind === 'team' ||
|
|
2109
2119
|
e.kind === 'compaction' ||
|
|
2110
2120
|
(e.kind === 'service' && !e.terminal));
|
|
2121
|
+
// NATIVE MODE ONLY. The fallback exists because native mode cannot scroll inside the
|
|
2122
|
+
// frame - the answer's head is reachable only if it is printed. Full-screen mode has a
|
|
2123
|
+
// viewport that already reaches it, and it has no `expanded` rendering for an answer,
|
|
2124
|
+
// so falling through there would consume the press and show nothing.
|
|
2125
|
+
const answer = card === undefined && native
|
|
2126
|
+
? [...entries].reverse().find((e) => e.kind === 'assistant' && !e.streaming)
|
|
2127
|
+
: undefined;
|
|
2128
|
+
const last = card ?? answer;
|
|
2111
2129
|
// Native mode cannot rewrite what is already printed, so it PRINTS an expanded
|
|
2112
2130
|
// copy as a new block instead of toggling the old one (spec 4.7).
|
|
2113
2131
|
if (last && native) {
|
|
@@ -2293,9 +2311,17 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2293
2311
|
pending: state.pendingSteering, columns: baseRail.contentCols, terminalRows: rows,
|
|
2294
2312
|
availableRows: Math.max(0, viewportBudget - composerBaseRows - 1), paused: !running,
|
|
2295
2313
|
}), [state.pendingSteering, baseRail.contentCols, rows, viewportBudget, composerBaseRows, running]);
|
|
2314
|
+
// THE POOL, NOT THE VIEWPORT MINUS A QUEUE THAT IS NOT DRAWN
|
|
2315
|
+
// (tui-full-height-live-region RV-P1-3). `QueuePanel` only mounts in the full-screen
|
|
2316
|
+
// layout, so native mode used to reserve rows for a card nobody paints. And it MUST be
|
|
2317
|
+
// `viewportBudget` rather than `nativeBudget.band.*`: `viewportRows === remaining` by
|
|
2318
|
+
// construction, and reading the band back here would close a loop with `teamRows`
|
|
2319
|
+
// (AC-34, asserted textually by `live-band.test.ts`).
|
|
2296
2320
|
const teamLayout = buildTeamPanelLayout({
|
|
2297
2321
|
snapshot: state.team, terminalRows: rows,
|
|
2298
|
-
availableRows:
|
|
2322
|
+
availableRows: native
|
|
2323
|
+
? Math.max(0, viewportBudget - 1)
|
|
2324
|
+
: Math.max(0, viewportBudget - composerBaseRows - queueLayout.rows - 1),
|
|
2299
2325
|
});
|
|
2300
2326
|
const railLayout = buildTodoRailLayout({
|
|
2301
2327
|
cols: cols - 1, panelEnabled: cfg.todo.panel, overlayOpen: false,
|
|
@@ -2315,14 +2341,14 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2315
2341
|
// an inert hook is what keeps `scrollMode: 'app'` byte-identical (I-S9).
|
|
2316
2342
|
const viewCols = native ? Math.max(1, cols - 1) : cols;
|
|
2317
2343
|
const header = (_jsx(Header, { columns: viewCols, statusExpanded: statusExpanded, scrolledLines: scrolledLines, overlayOpen: overlay !== null, cwd: controller.getCwd(), provider: cfg.provider, model: cfg.model, hasKey: hasKey, variant: pickHeaderVariant(cols), theme: theme, caps: caps }));
|
|
2318
|
-
// The
|
|
2319
|
-
//
|
|
2320
|
-
//
|
|
2321
|
-
|
|
2344
|
+
// The header and the opener are SLOTS OF THE SHELL, not printed bytes (D3/D4): at
|
|
2345
|
+
// `H = rows - 1` a printed row is pushed off the screen by the very next frame, so
|
|
2346
|
+
// `printBanner()` was a way of making the banner invisible. Neither is passed here.
|
|
2347
|
+
// The opener node is built below, because its height now comes from the native band.
|
|
2322
2348
|
const scrollbackView = useScrollbackView({
|
|
2323
2349
|
enabled: native, bridge: scrollback, entries: state.entries, runActive: running,
|
|
2324
2350
|
theme, caps, density: cfg.density, thinkingVisible: state.thinkingVisible,
|
|
2325
|
-
reducedMotion, rows, cols,
|
|
2351
|
+
reducedMotion, rows, cols,
|
|
2326
2352
|
overlayOpen: overlay !== null, draftRows, popupRows: overlay ? 0 : popupRows,
|
|
2327
2353
|
statusExpanded, forceInactive: native && guardTripped,
|
|
2328
2354
|
// The band's inputs. `teamRowsNatural` is the roster layout computed above, so the
|
|
@@ -2331,6 +2357,10 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2331
2357
|
itemCount: state.todos?.items.length ?? 0,
|
|
2332
2358
|
panelEnabled: cfg.todo.panel,
|
|
2333
2359
|
teamRowsNatural: teamLayout.rowCount,
|
|
2360
|
+
// The top give-way measures a window entry's height, and an expanded tool card is a
|
|
2361
|
+
// different height from a collapsed one - a forgotten value would print rows the region
|
|
2362
|
+
// is not drawing.
|
|
2363
|
+
expandedToolIds: state.expandedToolIds,
|
|
2334
2364
|
});
|
|
2335
2365
|
scrollbackViewRef.current = scrollbackView;
|
|
2336
2366
|
// The two hooks `cli.tsx` calls from OUTSIDE React: the exit summary (every exit
|
|
@@ -2352,6 +2382,14 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2352
2382
|
}, [native, scrollback, controller, glyphs]);
|
|
2353
2383
|
useEffect(() => { setGuardTripped(false); }, [rows, cols]);
|
|
2354
2384
|
const nativeBudget = native ? scrollbackView.budget : null;
|
|
2385
|
+
// THE EMPTY-SESSION CARD, ONE NODE FOR BOTH MODES (D4 / RV-P0-1). The wordmark and the
|
|
2386
|
+
// getting-started card are the first block of content in `app` mode (they scroll away
|
|
2387
|
+
// with the transcript) and the shell's `opener` slot in native mode. Its height used to
|
|
2388
|
+
// be a `rows - 8` guess in native mode; the honest input is the band's own pool, which
|
|
2389
|
+
// is why this node is built after `nativeBudget`.
|
|
2390
|
+
const openerRows = nativeBudget ? nativeBudget.band.bandRows : viewportBudget;
|
|
2391
|
+
const openerCols = nativeBudget ? nativeBudget.previewCols : contentCols;
|
|
2392
|
+
const opener = empty ? (_jsx(SessionOpener, { variant: pickOpenerVariant(openerRows, openerCols), version: version, cwd: controller.getCwd(), hasKey: hasKey, viewportRows: openerRows, theme: theme, caps: caps })) : null;
|
|
2355
2393
|
const overlayMaxRows = nativeBudget ? nativeBudget.overlayRows : viewportBudget;
|
|
2356
2394
|
const overlayNode = overlay === 'queue' ? (_jsx(QueueOverlay, { pending: state.pendingSteering, cols: viewCols, maxRows: overlayMaxRows, scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps })) : overlay === 'help' ? (_jsx(OverlayFrame, { title: "Help", hint: `Esc close ${glyphs.midDot} PgUp/PgDn scroll`, maxRows: overlayMaxRows, cols: viewCols, rows: helpRows(theme, caps, native), scrollOffset: overlayScroll, onScrollClamp: setOverlayScroll, theme: theme, caps: caps })) : overlay === 'model' ? (_jsx(ModelPicker, { isActive: !tooSmall, registry: controller.getModelRegistry(), currentProvider: cfg.provider, currentModel: cfg.model, maxRows: overlayMaxRows, cols: viewCols, theme: theme, caps: caps, onSelect: handleModelSelect })) : overlay === 'settings' ? (_jsx(SettingsScreen, { isActive: !tooSmall, initial: {
|
|
2357
2395
|
provider: cfg.provider,
|
|
@@ -2415,7 +2453,17 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2415
2453
|
// `null` outside native mode, and `/perf` prints `n/a` for it: a band row of
|
|
2416
2454
|
// zeroes in full-screen mode would read like a panel that failed to show.
|
|
2417
2455
|
band: nativeBudget
|
|
2418
|
-
? {
|
|
2456
|
+
? {
|
|
2457
|
+
...nativeBudget.band,
|
|
2458
|
+
contentCols: nativeBudget.contentCols,
|
|
2459
|
+
// The tail window, joined here because it belongs to the VIEW pipeline rather
|
|
2460
|
+
// than to the band: `window=<start>/<n>` is what makes the visible-vs-printed
|
|
2461
|
+
// gap observable (known limitation 5), and `head=` counts the rows the top
|
|
2462
|
+
// give-way printed for it, so the mechanism is never invisible.
|
|
2463
|
+
windowStart: scrollbackView.windowStart,
|
|
2464
|
+
windowEntries: scrollbackView.tailEntries.length,
|
|
2465
|
+
headRows: scrollbackView.headRows,
|
|
2466
|
+
}
|
|
2419
2467
|
: null,
|
|
2420
2468
|
// A getter: the bridge's `frameReady` and geometry change outside React renders.
|
|
2421
2469
|
get scrollbar() {
|
|
@@ -2552,11 +2600,15 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2552
2600
|
}
|
|
2553
2601
|
: {}) }));
|
|
2554
2602
|
if (nativeBudget) {
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2603
|
+
// THE OPENER IS MOUNTED ON THE SHELL'S TOP-ALIGNED SLOT (D4 / RV-P0-1): it is sized
|
|
2604
|
+
// from the band's pool above, and bottom-aligning it would reproduce the reported
|
|
2605
|
+
// "blank upper half" INSIDE the region, where the canvas `gap` assertion cannot see it.
|
|
2606
|
+
return (_jsx(ScrollbackShell, { budget: nativeBudget, header: header, overlay: overlayNode, liveTailLines: scrollbackView.liveTailLines, ...(opener ? { opener } : {}),
|
|
2607
|
+
// Real components for the tail window (I-7); the plain rows above stay as the
|
|
2608
|
+
// fallback for a frame with no preview rows. The WHOLE list is passed: the window
|
|
2609
|
+
// belongs to `tail-window.ts`, and `prev` must come from the same list.
|
|
2558
2610
|
tail: nativeBudget.band.previewRows > 0
|
|
2559
|
-
? (_jsx(LiveTail, {
|
|
2611
|
+
? (_jsx(LiveTail, { entries: state.entries, maxEntries: SCROLLBACK_LIMITS.liveTailMaxEntries, rows: nativeBudget.band.previewRows, cols: nativeBudget.previewCols, expandedToolIds: state.expandedToolIds, thinkingVisible: state.thinkingVisible, reducedMotion: viewReducedMotion, density: cfg.density, nowSec: nowSec, theme: theme, caps: caps, measuredRows: scrollbackView.measuredTailRows }))
|
|
2560
2612
|
: undefined, team: nativeTeam, rail: nativeRail, composer: composer, status: statusNode, details: detailsNode, placeholder: _jsxs(Text, { wrap: "truncate", color: theme.noticeWarn, children: [glyphs.warn, " Terminal too small - needs at least 40 columns and 12 rows."] }), theme: theme }));
|
|
2561
2613
|
}
|
|
2562
2614
|
return (_jsx(AppShell, { inactive: tooSmall, placeholder: _jsxs(Text, { wrap: "truncate", color: theme.noticeWarn, children: [glyphs.warn, " Terminal too small - needs at least 40 columns and 12 rows."] }), rows: rows, cols: cols, header: header, viewport: viewport, viewportRows: frameBudget.viewportRows, composer: composer, composerSlotRows: frameBudget.composerSlotRows, statusRows: frameBudget.statusRows, details: detailsNode, status: statusNode }));
|
|
@@ -1,14 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* ScrollbackShell — the
|
|
3
|
-
* (tui-native-terminal-scroll §4.6).
|
|
2
|
+
* ScrollbackShell — the LIVE REGION of native-scroll mode
|
|
3
|
+
* (tui-native-terminal-scroll §4.6, tui-full-height-live-region D3/D4).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
* `
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* terminal
|
|
5
|
+
* THE REGION IS THE SCREEN. The root box is `budget.liveRows` tall, and that number is
|
|
6
|
+
* `rows - 1` - the whole area above the writer's parked row. It used to be only as tall
|
|
7
|
+
* as the panel band asked for, and every row it did not ask for became the canvas
|
|
8
|
+
* `gap`: blank rows ABOVE the content, which is what a user reports as "the app starts
|
|
9
|
+
* halfway down the terminal" and as "a big blank between the result and the running
|
|
10
|
+
* card". `H = rows - 1` is also the ONLY height that both fills the screen and keeps Ink
|
|
11
|
+
* off its `clearTerminal` branch (that branch fires at `outputHeight >= rows` and
|
|
12
|
+
* contains `ESC[3J`, which erases the user's scrollback).
|
|
10
13
|
*
|
|
11
|
-
* Top to bottom: [overlay | panel band][composer][status][details].
|
|
14
|
+
* Top to bottom: [header][overlay | panel band][composer][status][details].
|
|
12
15
|
*
|
|
13
16
|
* THE PANEL BAND IS THE PART NATIVE MODE WAS MISSING
|
|
14
17
|
* (release-gate-and-native-panels §6.1). A fixed frame can put a rail beside the
|
|
@@ -18,28 +21,60 @@
|
|
|
18
21
|
* +---------------------------------------------+-----------------+
|
|
19
22
|
* | team roster (height = band.teamRows) | TODO rail |
|
|
20
23
|
* +---------------------------------------------+ (height = |
|
|
21
|
-
* |
|
|
24
|
+
* | tail window (height = band.previewRows) | band.railRows) |
|
|
22
25
|
* +---------------------------------------------+-----------------+
|
|
23
26
|
*
|
|
24
27
|
* Every box in it has an explicit `height` and `width` plus `overflow="hidden"`, and
|
|
25
28
|
* that is a correctness requirement rather than tidiness: a box that grows with its
|
|
26
29
|
* content would make the region taller than `budget.liveRows`, and a frame as tall as
|
|
27
|
-
* the terminal makes Ink erase the user's scrollback
|
|
30
|
+
* the terminal makes Ink erase the user's scrollback.
|
|
31
|
+
*
|
|
32
|
+
* THE BAND'S PREVIEW BOX IS TOP-ALIGNED; THE OTHER SLOTS ARE FIXED
|
|
33
|
+
* (D3 / D4 / I-F7; tui-first-reply-bottom-aligned, plan A-prime):
|
|
34
|
+
* - `header` is the fixed 1-row slot at the very top. It is NOT printed into the
|
|
35
|
+
* scrollback any more: at `H = rows - 1` any printed row is pushed off the screen by
|
|
36
|
+
* the next frame, so "print the header" would mean "the header is never visible".
|
|
37
|
+
* - the band's preview box is TOP-aligned unconditionally, which is why the alignment is
|
|
38
|
+
* no longer a function of `opener`. `opener` is a 12-row card in a 24-row column, so
|
|
39
|
+
* bottom-aligning it left 12 blank rows in the upper half of the screen - the original
|
|
40
|
+
* defect, still inside the region, where the `gap` assertion could not see it
|
|
41
|
+
* (RV-P0-1). `tail` had the same shape for a short conversation: four rows pressed
|
|
42
|
+
* against the composer with every unused row ABOVE them, which is the defect plan
|
|
43
|
+
* A-prime fixes. `tail` therefore decides its own alignment one level down
|
|
44
|
+
* (`scrollback/tail-anchor.ts`): top while its content fits, and the old `flex-end`
|
|
45
|
+
* only on the frames where the content overflows and the newest row must stay visible.
|
|
46
|
+
* `opener` and `tail` share this box because two boxes would be one more height to
|
|
47
|
+
* get wrong, and they are mutually exclusive by construction (`App` keys them off
|
|
48
|
+
* `entries.length === 0`).
|
|
28
49
|
*
|
|
29
50
|
* `liveTailLines` STAYS. It is the plain-text fallback for the frames where the real
|
|
30
51
|
* tail is not mounted (and the floor of the measurement path); `tail` wins when both
|
|
31
|
-
* are given, so the two can never be drawn on top of each other.
|
|
52
|
+
* are given, so the two can never be drawn on top of each other. It is sliced to
|
|
53
|
+
* `budget.liveTailRows` below, which is the same number as `band.previewRows`: that slice
|
|
54
|
+
* is what makes the unconditional `flex-start` safe here, because a fallback taller than
|
|
55
|
+
* the box would clip its NEWEST line - the one direction this box must never clip.
|
|
32
56
|
*/
|
|
33
57
|
import React from 'react';
|
|
34
58
|
import type { Theme } from './theme.js';
|
|
35
59
|
import type { ScrollbackBudget } from './scrollback/budget.js';
|
|
36
60
|
export interface ScrollbackShellProps {
|
|
37
61
|
budget: ScrollbackBudget;
|
|
62
|
+
/**
|
|
63
|
+
* REQUIRED: the one-row slot at the top of the region, `budget.headerRows` tall.
|
|
64
|
+
* Required rather than optional for the reason `budget` and `band` are - a frame
|
|
65
|
+
* that forgot it would render a header-less screen with nothing to catch it.
|
|
66
|
+
*/
|
|
67
|
+
header: React.ReactNode;
|
|
38
68
|
/** Shown instead of the band when an overlay is open. */
|
|
39
69
|
overlay: React.ReactNode | null;
|
|
40
70
|
/** Plain preview rows: the fallback when `tail` is absent (and what `/perf` counts). */
|
|
41
71
|
liveTailLines: readonly string[];
|
|
42
|
-
/**
|
|
72
|
+
/**
|
|
73
|
+
* The empty-session card. Mutually exclusive with `tail`, TOP-aligned like the rest of
|
|
74
|
+
* the band's preview box (RV-P0-1).
|
|
75
|
+
*/
|
|
76
|
+
opener?: React.ReactNode;
|
|
77
|
+
/** Real components for the tail window (`LiveTail`). */
|
|
43
78
|
tail?: React.ReactNode;
|
|
44
79
|
/** The subagent roster, drawn at the top of the band's left column. */
|
|
45
80
|
team?: React.ReactNode;
|
|
@@ -51,4 +86,4 @@ export interface ScrollbackShellProps {
|
|
|
51
86
|
placeholder: React.ReactNode;
|
|
52
87
|
theme: Theme;
|
|
53
88
|
}
|
|
54
|
-
export declare function ScrollbackShell({ budget, overlay, liveTailLines, tail, team, rail, composer, status, details, placeholder, theme, }: ScrollbackShellProps): React.ReactElement;
|
|
89
|
+
export declare function ScrollbackShell({ budget, header, overlay, liveTailLines, opener, tail, team, rail, composer, status, details, placeholder, theme, }: ScrollbackShellProps): React.ReactElement;
|
|
@@ -1,15 +1,19 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
-
export function ScrollbackShell({ budget, overlay, liveTailLines, tail, team, rail, composer, status, details, placeholder, theme, }) {
|
|
3
|
+
export function ScrollbackShell({ budget, header, overlay, liveTailLines, opener, tail, team, rail, composer, status, details, placeholder, theme, }) {
|
|
4
4
|
const width = budget.contentCols;
|
|
5
5
|
if (budget.inactive) {
|
|
6
6
|
return (_jsx(Box, { flexDirection: "column", height: budget.liveRows, width: width, overflow: "hidden", flexShrink: 0, children: budget.liveRows > 0 ? placeholder : null }));
|
|
7
7
|
}
|
|
8
8
|
const band = budget.band;
|
|
9
|
+
// `opener ?? tail ?? lines` is ONE slot, not three: they are mutually exclusive, so
|
|
10
|
+
// the two can never both be drawn and fight for the height. This box's alignment is
|
|
11
|
+
// fixed (`flex-start`); `tail` alone may fall back to `flex-end` when it overflows.
|
|
12
|
+
const leftColumn = opener ?? tail;
|
|
9
13
|
const tailLines = liveTailLines.slice(-budget.liveTailRows);
|
|
10
14
|
// The overlay and the band are alternatives, not siblings: the band reports
|
|
11
15
|
// `bandRows = remaining` for an overlay frame, so drawing both would double the
|
|
12
16
|
// region's height and take the frame to the terminal height (the `ESC[3J` case).
|
|
13
|
-
const top = band.reason === 'overlay' ? (_jsx(Box, { flexDirection: "column", height: budget.overlayRows, flexShrink: 0, overflow: "hidden", children: overlay })) : band.bandRows > 0 ? (_jsxs(Box, { flexDirection: "row", height: band.bandRows, flexShrink: 0, overflow: "hidden", children: [_jsxs(Box, { flexDirection: "column", width: budget.previewCols, flexShrink: 0, overflow: "hidden", children: [band.teamRows > 0 && (_jsx(Box, { height: band.teamRows, flexShrink: 0, overflow: "hidden", children: team })), band.previewRows > 0 && (_jsx(Box, { flexDirection: "column", height: band.previewRows, flexShrink: 0, justifyContent: "flex-
|
|
14
|
-
return (_jsxs(Box, { flexDirection: "column", height: budget.liveRows, width: width, overflow: "hidden", flexShrink: 0, children: [top, _jsx(Box, { flexDirection: "column", height: budget.composerSlotRows, justifyContent: "flex-end", flexShrink: 0, overflow: "hidden", children: composer }), _jsx(Box, { height: budget.statusRows > 0 ? 1 : 0, flexShrink: 0, overflow: "hidden", children: status }), budget.statusRows === 2 && (_jsx(Box, { height: 1, flexShrink: 0, overflow: "hidden", children: details }))] }));
|
|
17
|
+
const top = band.reason === 'overlay' ? (_jsx(Box, { flexDirection: "column", height: budget.overlayRows, flexShrink: 0, overflow: "hidden", children: overlay })) : band.bandRows > 0 ? (_jsxs(Box, { flexDirection: "row", height: band.bandRows, flexShrink: 0, overflow: "hidden", children: [_jsxs(Box, { flexDirection: "column", width: budget.previewCols, flexShrink: 0, overflow: "hidden", children: [band.teamRows > 0 && (_jsx(Box, { height: band.teamRows, flexShrink: 0, overflow: "hidden", children: team })), band.previewRows > 0 && (_jsx(Box, { flexDirection: "column", height: band.previewRows, flexShrink: 0, justifyContent: "flex-start", overflow: "hidden", children: leftColumn ?? tailLines.map((line, index) => (_jsx(Text, { wrap: "truncate", color: theme.muted, children: line }, index))) }))] }), band.railRows > 0 && (_jsx(Box, { height: band.railRows, width: band.railCols, flexShrink: 0, overflow: "hidden", children: rail }))] })) : null;
|
|
18
|
+
return (_jsxs(Box, { flexDirection: "column", height: budget.liveRows, width: width, overflow: "hidden", flexShrink: 0, children: [budget.headerRows > 0 && (_jsx(Box, { height: budget.headerRows, flexShrink: 0, overflow: "hidden", children: header })), top, _jsx(Box, { flexDirection: "column", height: budget.composerSlotRows, justifyContent: "flex-end", flexShrink: 0, overflow: "hidden", children: composer }), _jsx(Box, { height: budget.statusRows > 0 ? 1 : 0, flexShrink: 0, overflow: "hidden", children: status }), budget.statusRows === 2 && (_jsx(Box, { height: 1, flexShrink: 0, overflow: "hidden", children: details }))] }));
|
|
15
19
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* LiveTail - the band's
|
|
3
|
-
* (release-gate-and-native-panels §6.5).
|
|
2
|
+
* LiveTail - the band's TAIL WINDOW, rendered with the real entry components
|
|
3
|
+
* (release-gate-and-native-panels §6.5, tui-full-height-live-region D6).
|
|
4
4
|
*
|
|
5
5
|
* WHY THIS FILE EXISTS. Native scroll mode cannot print an entry until it settles, so
|
|
6
6
|
* the live region used to show `buildLivePreview`'s plain-text projection - and for a
|
|
@@ -9,6 +9,15 @@
|
|
|
9
9
|
* it, which is why the region's content type changed from `readonly string[]` to a
|
|
10
10
|
* fixed-height box that can hold components.
|
|
11
11
|
*
|
|
12
|
+
* IT SHOWS THE CONVERSATION, NOT JUST THE UNFINISHED PART (D6/D7). The input used to be
|
|
13
|
+
* `entries.slice(settledCount)` - the entries the reducer had not finished writing -
|
|
14
|
+
* because everything settled was printed into the scrollback immediately. Now the region
|
|
15
|
+
* is the whole screen, `H = rows - 1`, so a printed row is pushed off the top by the next
|
|
16
|
+
* frame; the region therefore holds the last `SCROLLBACK_LIMITS.liveTailMaxEntries`
|
|
17
|
+
* PRINTABLE entries, settled or not, and an entry is printed when it LEAVES that window.
|
|
18
|
+
* The window is `tail-window.ts`'s definition; this component only slides it over the
|
|
19
|
+
* list and lets the fixed-height box clip from the top when it overflows.
|
|
20
|
+
*
|
|
12
21
|
* IT RENDERS `EntryView` AND NOTHING ELSE. The mapping from an entry to its component
|
|
13
22
|
* (`assistant` / `tool` / `team` / `todo` / service cards, notice frames, the `null`
|
|
14
23
|
* cases) lives in `Transcript.tsx` exactly once, together with `EntryFrame`'s gutter,
|
|
@@ -17,10 +26,39 @@
|
|
|
17
26
|
* left rail glyph and the transcript's would keep it, and the two halves of one screen
|
|
18
27
|
* would disagree about the same entry.
|
|
19
28
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
29
|
+
* TOP-ANCHORED WHILE THE CONTENT FITS, BOTTOM-ANCHORED WHEN IT DOES NOT
|
|
30
|
+
* (tui-first-reply-bottom-aligned, plan A-prime). A constant `flex-end` is what a user
|
|
31
|
+
* reports as "the first reply shows up at the bottom": the box is always the whole pool,
|
|
32
|
+
* so a four-row first turn was pushed against the composer and every unused row sat ABOVE
|
|
33
|
+
* the conversation. `tailAnchor` therefore sums the estimated height of exactly the
|
|
34
|
+
* entries this frame renders and asks for `flex-start` while they fit - the conversation
|
|
35
|
+
* is then written downwards from under the header, the way a terminal writes.
|
|
36
|
+
*
|
|
37
|
+
* THE OVERFLOW FRAME STAYS `flex-end`, AND THAT IS THE OTHER HALF OF THE FIX.
|
|
38
|
+
* `flex-start` PLUS `overflow="hidden"` clips the NEWEST rows (Ink's clip rectangle is
|
|
39
|
+
* the box's own `[top, top + height]`), and a streaming entry can never be printed into
|
|
40
|
+
* the scrollback to make up for it (`settled-boundary.ts` holds it unstable, and printed
|
|
41
|
+
* rows are permanent). `flex-end` clips the OLDEST rows instead, which is the promise
|
|
42
|
+
* `scrollback/canvas.ts` makes about the live tail: the newest line always sits against
|
|
43
|
+
* the composer. Those clipped rows are what `overflow-print.ts` prints, which is why the
|
|
44
|
+
* branch has to be `bottom` before anything is printed - and why `measuredRows` exists.
|
|
45
|
+
*
|
|
46
|
+
* THE SWITCH COSTS ONE DOWNWARD MOVE ON A STREAMING FRAME, AND IT IS NOT FREE. The two
|
|
47
|
+
* branches are only pixel-identical where the content EXACTLY fills the box, and the
|
|
48
|
+
* estimate cannot know that: `estimateEntryRows` charges every entry a separation row while
|
|
49
|
+
* `density.ts::separationRows` only draws one above a `user` entry, so the frame that
|
|
50
|
+
* flips is still a few rows short of the box and the block drops by those rows once -
|
|
51
|
+
* three on a 30-row terminal (the margin plus that over-count) - before it starts
|
|
52
|
+
* scrolling up a row per line. Accepted and pinned by
|
|
53
|
+
* `scrollback-first-reply-anchor.test.tsx`: anchoring to the top until the content
|
|
54
|
+
* really fills the box is exactly the frame that clips the newest row, and measuring the
|
|
55
|
+
* box every frame is the cost plan A-prime exists to avoid.
|
|
56
|
+
*
|
|
57
|
+
* A SETTLED WINDOW IS MEASURED ANYWAY, so the move is exact there (`measuredRows`).
|
|
58
|
+
* `use-scrollback-view.ts` has to measure the window to decide what the box hides, and
|
|
59
|
+
* `resolveTailAnchor` lets that measurement decide the anchor: `flex-end` exactly when the
|
|
60
|
+
* content is taller than the box. So the glitch above survives only on frames where no
|
|
61
|
+
* measurement exists yet - every streaming frame, i.e. exactly the case the pin covers.
|
|
24
62
|
*
|
|
25
63
|
* BOUNDED: at most `SCROLLBACK_LIMITS.liveTailMaxEntries` component instances per frame
|
|
26
64
|
* (I-10). That is what keeps the per-frame cost the same order as the full-screen live
|
|
@@ -32,8 +70,12 @@ import type { TermCapabilities } from '../capabilities.js';
|
|
|
32
70
|
import type { DensityMode } from '../density.js';
|
|
33
71
|
import type { Theme } from '../theme.js';
|
|
34
72
|
export interface LiveTailProps {
|
|
35
|
-
/**
|
|
36
|
-
|
|
73
|
+
/**
|
|
74
|
+
* The whole entry list (settled and unfinished alike); the window is taken here.
|
|
75
|
+
* Callers must not pre-slice it: `prev` has to come from the same list the transcript
|
|
76
|
+
* draws, or the top card of the band loses the separator row it has upstairs.
|
|
77
|
+
*/
|
|
78
|
+
entries: readonly Entry[];
|
|
37
79
|
/** Render only the last N entries; the box clips the older ones from the top. */
|
|
38
80
|
maxEntries: number;
|
|
39
81
|
/** Fixed height: the root box's `height`. The content never decides it (I-4). */
|
|
@@ -47,18 +89,25 @@ export interface LiveTailProps {
|
|
|
47
89
|
nowSec?: number;
|
|
48
90
|
theme: Theme;
|
|
49
91
|
caps: TermCapabilities;
|
|
92
|
+
/**
|
|
93
|
+
* Measured height of exactly the entries this frame renders, when the owner of the
|
|
94
|
+
* measurement (`use-scrollback-view.ts`) has it. It decides the anchor INSTEAD of the
|
|
95
|
+
* estimate - see `resolveTailAnchor`. `undefined` on every frame without a full
|
|
96
|
+
* measurement, which is every streaming frame: the estimate then decides, as before.
|
|
97
|
+
*/
|
|
98
|
+
measuredRows?: number;
|
|
50
99
|
}
|
|
51
100
|
export interface TailEntry {
|
|
52
101
|
entry: Entry;
|
|
53
|
-
/** The entry before it in the
|
|
102
|
+
/** The entry before it in the passed list: `EntryView` decides the separator from it. */
|
|
54
103
|
prev: Entry | undefined;
|
|
55
104
|
}
|
|
56
105
|
/**
|
|
57
|
-
* The last `maxEntries` entries, each with its predecessor.
|
|
106
|
+
* The last `maxEntries` printable entries, each with its predecessor.
|
|
58
107
|
*
|
|
59
|
-
* `prev` must come from the COMPLETE
|
|
60
|
-
*
|
|
61
|
-
*
|
|
108
|
+
* `prev` must come from the same COMPLETE list rather than from the truncated slice, or
|
|
109
|
+
* the first rendered entry would look like the head of the conversation and drop the
|
|
110
|
+
* separation row the transcript shows above the same card.
|
|
62
111
|
*/
|
|
63
|
-
export declare function selectTailEntries(
|
|
64
|
-
export declare function LiveTail({
|
|
112
|
+
export declare function selectTailEntries(entries: readonly Entry[], maxEntries: number): readonly TailEntry[];
|
|
113
|
+
export declare function LiveTail({ entries, maxEntries, rows, cols, expandedToolIds, thinkingVisible, reducedMotion, density, nowSec, theme, caps, measuredRows, }: LiveTailProps): React.ReactElement;
|
|
@@ -1,27 +1,43 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Box } from 'ink';
|
|
3
3
|
import { EntryView, nowSecFor } from '../Transcript.js';
|
|
4
|
+
import { estimateEntryRows } from '../layout/virtual-window.js';
|
|
4
5
|
import { SCROLLBACK_LIMITS } from './limits.js';
|
|
6
|
+
import { isExcluded } from './settled-boundary.js';
|
|
7
|
+
import { resolveTailAnchor } from './tail-anchor.js';
|
|
5
8
|
/**
|
|
6
|
-
* The last `maxEntries` entries, each with its predecessor.
|
|
9
|
+
* The last `maxEntries` printable entries, each with its predecessor.
|
|
7
10
|
*
|
|
8
|
-
* `prev` must come from the COMPLETE
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
+
* `prev` must come from the same COMPLETE list rather than from the truncated slice, or
|
|
12
|
+
* the first rendered entry would look like the head of the conversation and drop the
|
|
13
|
+
* separation row the transcript shows above the same card.
|
|
11
14
|
*/
|
|
12
|
-
export function selectTailEntries(
|
|
15
|
+
export function selectTailEntries(entries, maxEntries) {
|
|
16
|
+
const printable = entries.filter((entry) => !isExcluded(entry));
|
|
13
17
|
const limit = Number.isFinite(maxEntries) ? Math.max(0, Math.floor(maxEntries)) : 0;
|
|
14
18
|
if (limit === 0)
|
|
15
19
|
return [];
|
|
16
|
-
const start = Math.max(0,
|
|
20
|
+
const start = Math.max(0, printable.length - limit);
|
|
17
21
|
const selected = [];
|
|
18
|
-
for (let index = start; index <
|
|
19
|
-
selected.push({
|
|
22
|
+
for (let index = start; index < printable.length; index += 1) {
|
|
23
|
+
selected.push({
|
|
24
|
+
entry: printable[index],
|
|
25
|
+
prev: index > 0 ? printable[index - 1] : undefined,
|
|
26
|
+
});
|
|
20
27
|
}
|
|
21
28
|
return selected;
|
|
22
29
|
}
|
|
23
|
-
export function LiveTail({
|
|
30
|
+
export function LiveTail({ entries, maxEntries, rows, cols, expandedToolIds, thinkingVisible, reducedMotion, density, nowSec, theme, caps, measuredRows, }) {
|
|
24
31
|
const limited = Math.min(maxEntries, SCROLLBACK_LIMITS.liveTailMaxEntries);
|
|
25
|
-
const selected = selectTailEntries(
|
|
26
|
-
|
|
32
|
+
const selected = selectTailEntries(entries, limited);
|
|
33
|
+
// `estimateEntryRows` is the estimate the transcript's virtual window already uses, so
|
|
34
|
+
// the anchor can never disagree with the height the rest of the interface plans for.
|
|
35
|
+
const estimateRows = (entry) => estimateEntryRows(entry, cols, density, expandedToolIds[entry.id] === true, thinkingVisible);
|
|
36
|
+
const anchor = resolveTailAnchor({
|
|
37
|
+
entries: selected.map((tailEntry) => tailEntry.entry),
|
|
38
|
+
budgetRows: rows,
|
|
39
|
+
rowOf: estimateRows,
|
|
40
|
+
measuredRows,
|
|
41
|
+
});
|
|
42
|
+
return (_jsx(Box, { flexDirection: "column", height: rows, width: cols, justifyContent: anchor === 'top' ? 'flex-start' : 'flex-end', flexShrink: 0, overflow: "hidden", children: selected.map(({ entry, prev }) => (_jsx(EntryView, { entry: entry, prev: prev, expanded: expandedToolIds[entry.id] === true, thinkingVisible: thinkingVisible, reducedMotion: reducedMotion, density: density, nowSec: nowSecFor(entry, nowSec), theme: theme, caps: caps }, entry.id))) }));
|
|
27
43
|
}
|