@aragon-agent/cli 0.6.32 → 0.6.33
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 +19 -0
- package/README.md +44 -31
- package/dist/cli.js +4 -0
- package/dist/commands/perf.d.ts +24 -1
- package/dist/commands/perf.js +11 -3
- package/dist/ui/App.js +39 -11
- package/dist/ui/ScrollbackShell.d.ts +37 -12
- package/dist/ui/ScrollbackShell.js +6 -3
- package/dist/ui/scrollback/LiveTail.d.ts +27 -12
- package/dist/ui/scrollback/LiveTail.js +15 -10
- 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 +18 -10
- package/dist/ui/scrollback/limits.js +18 -10
- package/dist/ui/scrollback/live-preview.d.ts +18 -10
- package/dist/ui/scrollback/live-preview.js +14 -11
- package/dist/ui/scrollback/tail-window.d.ts +60 -0
- package/dist/ui/scrollback/tail-window.js +73 -0
- package/dist/ui/scrollback/use-scrollback-view.d.ts +26 -18
- package/dist/ui/scrollback/use-scrollback-view.js +85 -65
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -121,6 +121,25 @@ were written.
|
|
|
121
121
|
|
|
122
122
|
### Fixed
|
|
123
123
|
|
|
124
|
+
- **The native-scroll screen is filled edge to edge, with no blank band anywhere.**
|
|
125
|
+
Three complaints, one cause: the live region was only as tall as the panel band
|
|
126
|
+
asked for, and every row it did not ask for became the canvas `gap` - blank rows
|
|
127
|
+
drawn *above* the content. So the app appeared to start halfway down the terminal,
|
|
128
|
+
a long blank band sat between a finished result and the card that was still running,
|
|
129
|
+
and the `TODO` rail stopped six rows or so above the input box. The region is now
|
|
130
|
+
the whole screen (`rows - 1`, the same height the full-screen layout uses): the
|
|
131
|
+
header is its first row, the input box and status line are its last two, the band
|
|
132
|
+
takes every row in between (`bandRows = pool`), so the rail spans the full height
|
|
133
|
+
above the composer and the tail column ends against it. The `gap` is 0 by
|
|
134
|
+
construction, not by tuning, and the scrollback writer is unchanged - `[gap][held]
|
|
135
|
+
[region]` collapses to `[region]` at that height. Two consequences worth knowing:
|
|
136
|
+
the region now shows the last ten entries of the conversation instead of only the
|
|
137
|
+
unfinished ones, and an entry is printed into your scrollback when it leaves that
|
|
138
|
+
window (so nothing is ever in both places at once); `/clear`, `--resume` and exit
|
|
139
|
+
flush the window first, which is why nothing visible is lost. The empty-session card
|
|
140
|
+
is drawn against the header rather than the input box, and `/perf` grew the three
|
|
141
|
+
numbers that make this self-checking: `top <n>` on the `mode native` line, plus
|
|
142
|
+
`pool=` and `window=<start>/<n>` on the `band:` line.
|
|
124
143
|
- **The TODO rail and the subagent roster are back in the default (native) scroll
|
|
125
144
|
mode.** Printing the conversation into the terminal's own scrollback took the two
|
|
126
145
|
panels with it: the rail lived in the full-screen layout's middle band and the
|
package/README.md
CHANGED
|
@@ -782,11 +782,12 @@ near-identical added ones.
|
|
|
782
782
|
### Native scroll mode (default)
|
|
783
783
|
|
|
784
784
|
The conversation lives in **your terminal's own scrollback**, exactly like the output
|
|
785
|
-
of any other command.
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
iTerm2, Terminal.app,
|
|
785
|
+
of any other command. The *live region* fills the screen - header on the first row,
|
|
786
|
+
input box and status line on the last two, the tail of the conversation in between -
|
|
787
|
+
and an entry is printed into the scrollback once it leaves that region. Scrolling,
|
|
788
|
+
selecting and searching are your terminal's job, on every platform: PowerShell,
|
|
789
|
+
`cmd`, Windows Terminal, VS Code, `bash`, `zsh`, `tmux`, iTerm2, Terminal.app,
|
|
790
|
+
gnome-terminal and friends.
|
|
790
791
|
|
|
791
792
|
- **Wheel, scrollbar, `Shift+PgUp` / `Shift+PgDn`, touchpad inertia** — the terminal's.
|
|
792
793
|
`aragon` does not enter the alternate screen and does not turn on mouse reporting,
|
|
@@ -801,11 +802,15 @@ iTerm2, Terminal.app, gnome-terminal and friends.
|
|
|
801
802
|
time, usage, and the `aragon --resume <id>` line) instead of replaying the
|
|
802
803
|
transcript, because the transcript is already in your scrollback — `exitTranscript`
|
|
803
804
|
is ignored in this mode.
|
|
804
|
-
- **While a reply streams**,
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
805
|
+
- **While a reply streams**, it is drawn in the live region with the real cards (a
|
|
806
|
+
running `tool` or `team` card looks exactly like the full-screen one). The region
|
|
807
|
+
holds the last ten entries of the conversation, settled or not, and an entry is
|
|
808
|
+
printed into your scrollback when it LEAVES that window - so what is on the screen
|
|
809
|
+
and what you scroll up to are two disjoint halves of one conversation, never the
|
|
810
|
+
same paragraph twice. Scrolling up to read earlier output while a reply streams is
|
|
811
|
+
safe; how the viewport behaves when new output arrives is your terminal's own
|
|
812
|
+
"scroll on output" setting. A message you have just sent is therefore on the screen
|
|
813
|
+
only: it reaches the scrollback when ten newer entries push it out, or when you exit.
|
|
809
814
|
- **Printed rows are final once they leave the screen.** A row that has scrolled into
|
|
810
815
|
your scrollback is never rewritten, and it is wrapped to the width at which it was
|
|
811
816
|
printed, so a later window resize does not re-wrap it. The rows still *on* the screen
|
|
@@ -814,15 +819,16 @@ iTerm2, Terminal.app, gnome-terminal and friends.
|
|
|
814
819
|
(thinking) and `/theme` affect only *new* messages, and `Ctrl+O` prints an
|
|
815
820
|
expanded copy of a tool card instead of expanding the old one. `/clear` prints a
|
|
816
821
|
`---- conversation cleared ----` separator; it cannot erase scrollback.
|
|
817
|
-
- **The
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
822
|
+
- **The interface fills the terminal.** The header is the first row of the screen, the
|
|
823
|
+
input box and status line are the last two, and everything between them belongs to
|
|
824
|
+
the conversation: the newest line always touches the input box, so the blank space of
|
|
825
|
+
a conversation that has not filled the screen sits at the *top of the region* - never
|
|
826
|
+
between the reply and the composer, and never as a band above the app itself. Three
|
|
827
|
+
things are still true. A terminal that cannot be measured (`/perf` shows `anchor
|
|
828
|
+
bottom via fill`, or `none` when the probe is unavailable) is filled to the bottom
|
|
829
|
+
once at startup, which scrolls your screen up by a page — that one-off flip is the
|
|
830
|
+
price of pinning without a cursor query. Making the window narrower or shorter drops
|
|
831
|
+
the geometry until the next measurement (a couple of hundred milliseconds) instead of
|
|
826
832
|
guessing: new output parks under the last printed line until the composer pins itself
|
|
827
833
|
again, and old content is neither re-wrapped nor redrawn. `Ctrl+G`, `/help` and the
|
|
828
834
|
model picker open as overlays inside the live region, and they take the whole top of
|
|
@@ -831,21 +837,28 @@ iTerm2, Terminal.app, gnome-terminal and friends.
|
|
|
831
837
|
puts a `TODO d/t` column on the right of the band - markers, index, the running
|
|
832
838
|
item's `activeForm`, an overflow marker and the gauge - and a running dispatch puts
|
|
833
839
|
the `team` roster above the reply, one row per subagent with its activity and
|
|
834
|
-
elapsed time. The
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
+
elapsed time. The tail underneath them is drawn with the same cards the full-screen
|
|
841
|
+
mode uses, so a running tool or team card looks identical in both modes. The band
|
|
842
|
+
takes **every row between the header and the composer**: the rail spans the full
|
|
843
|
+
height above the input box instead of stopping partway down, the tail column ends
|
|
844
|
+
against the composer, and the printed conversation above always keeps the full width.
|
|
845
|
+
Below 76 columns there is no room for the rail and the status line's `todo` chip
|
|
846
|
+
takes over; `/perf`'s `band:` line says which of the six reasons applied.
|
|
840
847
|
- **If something looks wrong**, `/perf` has a `scrollback` line (`guardHits`,
|
|
841
848
|
`clippedRows`, `fallbacks`, and the geometry: `region`/`canvas`/`content`/`gap` rows,
|
|
842
849
|
`anchor bottom via probe|fill|none`, `scrolled` rows, `anchor resets`, `reprobes`).
|
|
843
|
-
|
|
844
|
-
`
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
850
|
+
The `mode native` line carries `top <n>`, the canvas's first screen row. A `band:`
|
|
851
|
+
line reports the panel band - `rail=<rows>x<cols> team=<rows> tail=<rows>
|
|
852
|
+
pool=<rows> cols=<contentCols>/<previewCols> reason=<visible|empty|disabled|`
|
|
853
|
+
`narrow|short|overlay> held=<rail>/<team> window=<start>/<n>` - which is the only
|
|
854
|
+
place that says why a panel is not on screen.
|
|
855
|
+
`gap` is the blank space above the content and is **always 0** now (the region is the
|
|
856
|
+
whole screen), `canvas` is the canvas height including the live region, `pool` is the
|
|
857
|
+
band's height (the rows between the header and the composer) and `window=<start>/<n>`
|
|
858
|
+
is the tail window: the first entry on screen and how many entries it holds. That
|
|
859
|
+
number is the honest answer to "why is a message I can see not in my scrollback yet"
|
|
860
|
+
- the region shows ten entries, and an entry is printed when it leaves them.
|
|
861
|
+
`scrolled` counts the rows that have moved into your scrollback.
|
|
849
862
|
The one-step escape hatch is `aragon config set scrollMode app`, or `--scroll-mode
|
|
850
863
|
app` for a single run.
|
|
851
864
|
|
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,31 @@ 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;
|
|
70
87
|
}
|
|
71
88
|
/**
|
|
72
89
|
* Why the right-edge scrollbar is or is not usable, in one record
|
|
@@ -136,8 +153,14 @@ export interface ScrollbackPerf {
|
|
|
136
153
|
anchorCanvasRows: number;
|
|
137
154
|
/** Content rows inside that canvas — how much of the conversation is on screen. */
|
|
138
155
|
anchorHeldRows: number;
|
|
139
|
-
/** Blank rows above the content.
|
|
156
|
+
/** Blank rows above the content. Zero by construction in the full-height region. */
|
|
140
157
|
anchorGapRows: number;
|
|
158
|
+
/**
|
|
159
|
+
* Canvas top row (0-based). 0 means the canvas starts on the terminal's first row and
|
|
160
|
+
* is the direct answer to "why does the app start halfway down the screen" - the one
|
|
161
|
+
* number that used to be missing from this block.
|
|
162
|
+
*/
|
|
163
|
+
anchorTop: number;
|
|
141
164
|
/** Rows this session pushed into the terminal's own scrollback. */
|
|
142
165
|
anchorScrollRows: number;
|
|
143
166
|
/** 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}`;
|
|
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
|
@@ -2293,9 +2293,17 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2293
2293
|
pending: state.pendingSteering, columns: baseRail.contentCols, terminalRows: rows,
|
|
2294
2294
|
availableRows: Math.max(0, viewportBudget - composerBaseRows - 1), paused: !running,
|
|
2295
2295
|
}), [state.pendingSteering, baseRail.contentCols, rows, viewportBudget, composerBaseRows, running]);
|
|
2296
|
+
// THE POOL, NOT THE VIEWPORT MINUS A QUEUE THAT IS NOT DRAWN
|
|
2297
|
+
// (tui-full-height-live-region RV-P1-3). `QueuePanel` only mounts in the full-screen
|
|
2298
|
+
// layout, so native mode used to reserve rows for a card nobody paints. And it MUST be
|
|
2299
|
+
// `viewportBudget` rather than `nativeBudget.band.*`: `viewportRows === remaining` by
|
|
2300
|
+
// construction, and reading the band back here would close a loop with `teamRows`
|
|
2301
|
+
// (AC-34, asserted textually by `live-band.test.ts`).
|
|
2296
2302
|
const teamLayout = buildTeamPanelLayout({
|
|
2297
2303
|
snapshot: state.team, terminalRows: rows,
|
|
2298
|
-
availableRows:
|
|
2304
|
+
availableRows: native
|
|
2305
|
+
? Math.max(0, viewportBudget - 1)
|
|
2306
|
+
: Math.max(0, viewportBudget - composerBaseRows - queueLayout.rows - 1),
|
|
2299
2307
|
});
|
|
2300
2308
|
const railLayout = buildTodoRailLayout({
|
|
2301
2309
|
cols: cols - 1, panelEnabled: cfg.todo.panel, overlayOpen: false,
|
|
@@ -2315,14 +2323,14 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2315
2323
|
// an inert hook is what keeps `scrollMode: 'app'` byte-identical (I-S9).
|
|
2316
2324
|
const viewCols = native ? Math.max(1, cols - 1) : cols;
|
|
2317
2325
|
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
|
-
|
|
2326
|
+
// The header and the opener are SLOTS OF THE SHELL, not printed bytes (D3/D4): at
|
|
2327
|
+
// `H = rows - 1` a printed row is pushed off the screen by the very next frame, so
|
|
2328
|
+
// `printBanner()` was a way of making the banner invisible. Neither is passed here.
|
|
2329
|
+
// The opener node is built below, because its height now comes from the native band.
|
|
2322
2330
|
const scrollbackView = useScrollbackView({
|
|
2323
2331
|
enabled: native, bridge: scrollback, entries: state.entries, runActive: running,
|
|
2324
2332
|
theme, caps, density: cfg.density, thinkingVisible: state.thinkingVisible,
|
|
2325
|
-
reducedMotion, rows, cols,
|
|
2333
|
+
reducedMotion, rows, cols,
|
|
2326
2334
|
overlayOpen: overlay !== null, draftRows, popupRows: overlay ? 0 : popupRows,
|
|
2327
2335
|
statusExpanded, forceInactive: native && guardTripped,
|
|
2328
2336
|
// The band's inputs. `teamRowsNatural` is the roster layout computed above, so the
|
|
@@ -2352,6 +2360,14 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2352
2360
|
}, [native, scrollback, controller, glyphs]);
|
|
2353
2361
|
useEffect(() => { setGuardTripped(false); }, [rows, cols]);
|
|
2354
2362
|
const nativeBudget = native ? scrollbackView.budget : null;
|
|
2363
|
+
// THE EMPTY-SESSION CARD, ONE NODE FOR BOTH MODES (D4 / RV-P0-1). The wordmark and the
|
|
2364
|
+
// getting-started card are the first block of content in `app` mode (they scroll away
|
|
2365
|
+
// with the transcript) and the shell's `opener` slot in native mode. Its height used to
|
|
2366
|
+
// be a `rows - 8` guess in native mode; the honest input is the band's own pool, which
|
|
2367
|
+
// is why this node is built after `nativeBudget`.
|
|
2368
|
+
const openerRows = nativeBudget ? nativeBudget.band.bandRows : viewportBudget;
|
|
2369
|
+
const openerCols = nativeBudget ? nativeBudget.previewCols : contentCols;
|
|
2370
|
+
const opener = empty ? (_jsx(SessionOpener, { variant: pickOpenerVariant(openerRows, openerCols), version: version, cwd: controller.getCwd(), hasKey: hasKey, viewportRows: openerRows, theme: theme, caps: caps })) : null;
|
|
2355
2371
|
const overlayMaxRows = nativeBudget ? nativeBudget.overlayRows : viewportBudget;
|
|
2356
2372
|
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
2373
|
provider: cfg.provider,
|
|
@@ -2415,7 +2431,15 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2415
2431
|
// `null` outside native mode, and `/perf` prints `n/a` for it: a band row of
|
|
2416
2432
|
// zeroes in full-screen mode would read like a panel that failed to show.
|
|
2417
2433
|
band: nativeBudget
|
|
2418
|
-
? {
|
|
2434
|
+
? {
|
|
2435
|
+
...nativeBudget.band,
|
|
2436
|
+
contentCols: nativeBudget.contentCols,
|
|
2437
|
+
// The tail window, joined here because it belongs to the VIEW pipeline rather
|
|
2438
|
+
// than to the band: `window=<start>/<n>` is what makes the visible-vs-printed
|
|
2439
|
+
// gap observable (known limitation 5).
|
|
2440
|
+
windowStart: scrollbackView.windowStart,
|
|
2441
|
+
windowEntries: scrollbackView.tailEntries.length,
|
|
2442
|
+
}
|
|
2419
2443
|
: null,
|
|
2420
2444
|
// A getter: the bridge's `frameReady` and geometry change outside React renders.
|
|
2421
2445
|
get scrollbar() {
|
|
@@ -2552,11 +2576,15 @@ export function App({ controller, version, initialOverlay, initialPrompt, confir
|
|
|
2552
2576
|
}
|
|
2553
2577
|
: {}) }));
|
|
2554
2578
|
if (nativeBudget) {
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2579
|
+
// THE OPENER IS MOUNTED ON THE SHELL'S TOP-ALIGNED SLOT (D4 / RV-P0-1): it is sized
|
|
2580
|
+
// from the band's pool above, and bottom-aligning it would reproduce the reported
|
|
2581
|
+
// "blank upper half" INSIDE the region, where the canvas `gap` assertion cannot see it.
|
|
2582
|
+
return (_jsx(ScrollbackShell, { budget: nativeBudget, header: header, overlay: overlayNode, liveTailLines: scrollbackView.liveTailLines, ...(opener ? { opener } : {}),
|
|
2583
|
+
// Real components for the tail window (I-7); the plain rows above stay as the
|
|
2584
|
+
// fallback for a frame with no preview rows. The WHOLE list is passed: the window
|
|
2585
|
+
// belongs to `tail-window.ts`, and `prev` must come from the same list.
|
|
2558
2586
|
tail: nativeBudget.band.previewRows > 0
|
|
2559
|
-
? (_jsx(LiveTail, {
|
|
2587
|
+
? (_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 }))
|
|
2560
2588
|
: 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
2589
|
}
|
|
2562
2590
|
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,13 +21,24 @@
|
|
|
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
|
+
* TWO SLOTS ARE NOT BOTTOM-ALIGNED, AND ONLY TWO (D3 / D4 / I-F7):
|
|
33
|
+
* - `header` is the fixed 1-row slot at the very top. It is NOT printed into the
|
|
34
|
+
* scrollback any more: at `H = rows - 1` any printed row is pushed off the screen by
|
|
35
|
+
* the next frame, so "print the header" would mean "the header is never visible".
|
|
36
|
+
* - `opener` is the empty-session card, and it is the one thing inside the band that is
|
|
37
|
+
* TOP-aligned: a 12-row card bottom-aligned in a 24-row column would leave 12 blank
|
|
38
|
+
* rows in the upper half of the screen - the original defect, still inside the
|
|
39
|
+
* region, where the `gap` assertion could not see it. `opener` and `tail` are
|
|
40
|
+
* mutually exclusive by construction (`App` keys them off `entries.length === 0`),
|
|
41
|
+
* and they share one box because two boxes would be one more height to get wrong.
|
|
28
42
|
*
|
|
29
43
|
* `liveTailLines` STAYS. It is the plain-text fallback for the frames where the real
|
|
30
44
|
* tail is not mounted (and the floor of the measurement path); `tail` wins when both
|
|
@@ -35,11 +49,22 @@ import type { Theme } from './theme.js';
|
|
|
35
49
|
import type { ScrollbackBudget } from './scrollback/budget.js';
|
|
36
50
|
export interface ScrollbackShellProps {
|
|
37
51
|
budget: ScrollbackBudget;
|
|
52
|
+
/**
|
|
53
|
+
* REQUIRED: the one-row slot at the top of the region, `budget.headerRows` tall.
|
|
54
|
+
* Required rather than optional for the reason `budget` and `band` are - a frame
|
|
55
|
+
* that forgot it would render a header-less screen with nothing to catch it.
|
|
56
|
+
*/
|
|
57
|
+
header: React.ReactNode;
|
|
38
58
|
/** Shown instead of the band when an overlay is open. */
|
|
39
59
|
overlay: React.ReactNode | null;
|
|
40
60
|
/** Plain preview rows: the fallback when `tail` is absent (and what `/perf` counts). */
|
|
41
61
|
liveTailLines: readonly string[];
|
|
42
|
-
/**
|
|
62
|
+
/**
|
|
63
|
+
* The empty-session card. Mutually exclusive with `tail`, TOP-aligned, and the only
|
|
64
|
+
* thing in this shell that is not bottom-aligned or a fixed slot (RV-P0-1).
|
|
65
|
+
*/
|
|
66
|
+
opener?: React.ReactNode;
|
|
67
|
+
/** Real components for the tail window (`LiveTail`). */
|
|
43
68
|
tail?: React.ReactNode;
|
|
44
69
|
/** The subagent roster, drawn at the top of the band's left column. */
|
|
45
70
|
team?: React.ReactNode;
|
|
@@ -51,4 +76,4 @@ export interface ScrollbackShellProps {
|
|
|
51
76
|
placeholder: React.ReactNode;
|
|
52
77
|
theme: Theme;
|
|
53
78
|
}
|
|
54
|
-
export declare function ScrollbackShell({ budget, overlay, liveTailLines, tail, team, rail, composer, status, details, placeholder, theme, }: ScrollbackShellProps): React.ReactElement;
|
|
79
|
+
export declare function ScrollbackShell({ budget, header, overlay, liveTailLines, opener, tail, team, rail, composer, status, details, placeholder, theme, }: ScrollbackShellProps): React.ReactElement;
|
|
@@ -1,15 +1,18 @@
|
|
|
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: whichever is mounted decides the
|
|
10
|
+
// alignment of the box, so the two can never both be drawn and fight for the height.
|
|
11
|
+
const leftColumn = opener ?? tail;
|
|
9
12
|
const tailLines = liveTailLines.slice(-budget.liveTailRows);
|
|
10
13
|
// The overlay and the band are alternatives, not siblings: the band reports
|
|
11
14
|
// `bandRows = remaining` for an overlay frame, so drawing both would double the
|
|
12
15
|
// 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:
|
|
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 }))] }));
|
|
16
|
+
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: opener ? 'flex-start' : 'flex-end', 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;
|
|
17
|
+
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
18
|
}
|
|
@@ -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.
|
|
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,
|
|
@@ -20,7 +29,9 @@
|
|
|
20
29
|
* `justifyContent="flex-end"` PLUS `overflow="hidden"` IS CORRECTNESS, NOT STYLE: when
|
|
21
30
|
* the content is taller than the budget the rows that get clipped are the OLDEST ones,
|
|
22
31
|
* so the newest line always sits against the composer - the same promise
|
|
23
|
-
* `scrollback/canvas.ts` makes about the live tail.
|
|
32
|
+
* `scrollback/canvas.ts` makes about the live tail. (`ScrollbackShell` flips this to
|
|
33
|
+
* `flex-start` for the empty-session opener, which is the one frame where the content
|
|
34
|
+
* must hug the HEADER instead; it never renders this component.)
|
|
24
35
|
*
|
|
25
36
|
* BOUNDED: at most `SCROLLBACK_LIMITS.liveTailMaxEntries` component instances per frame
|
|
26
37
|
* (I-10). That is what keeps the per-frame cost the same order as the full-screen live
|
|
@@ -32,8 +43,12 @@ import type { TermCapabilities } from '../capabilities.js';
|
|
|
32
43
|
import type { DensityMode } from '../density.js';
|
|
33
44
|
import type { Theme } from '../theme.js';
|
|
34
45
|
export interface LiveTailProps {
|
|
35
|
-
/**
|
|
36
|
-
|
|
46
|
+
/**
|
|
47
|
+
* The whole entry list (settled and unfinished alike); the window is taken here.
|
|
48
|
+
* Callers must not pre-slice it: `prev` has to come from the same list the transcript
|
|
49
|
+
* draws, or the top card of the band loses the separator row it has upstairs.
|
|
50
|
+
*/
|
|
51
|
+
entries: readonly Entry[];
|
|
37
52
|
/** Render only the last N entries; the box clips the older ones from the top. */
|
|
38
53
|
maxEntries: number;
|
|
39
54
|
/** Fixed height: the root box's `height`. The content never decides it (I-4). */
|
|
@@ -50,15 +65,15 @@ export interface LiveTailProps {
|
|
|
50
65
|
}
|
|
51
66
|
export interface TailEntry {
|
|
52
67
|
entry: Entry;
|
|
53
|
-
/** The entry before it in the
|
|
68
|
+
/** The entry before it in the passed list: `EntryView` decides the separator from it. */
|
|
54
69
|
prev: Entry | undefined;
|
|
55
70
|
}
|
|
56
71
|
/**
|
|
57
|
-
* The last `maxEntries` entries, each with its predecessor.
|
|
72
|
+
* The last `maxEntries` printable entries, each with its predecessor.
|
|
58
73
|
*
|
|
59
|
-
* `prev` must come from the COMPLETE
|
|
60
|
-
*
|
|
61
|
-
*
|
|
74
|
+
* `prev` must come from the same COMPLETE list rather than from the truncated slice, or
|
|
75
|
+
* the first rendered entry would look like the head of the conversation and drop the
|
|
76
|
+
* separation row the transcript shows above the same card.
|
|
62
77
|
*/
|
|
63
|
-
export declare function selectTailEntries(
|
|
64
|
-
export declare function LiveTail({
|
|
78
|
+
export declare function selectTailEntries(entries: readonly Entry[], maxEntries: number): readonly TailEntry[];
|
|
79
|
+
export declare function LiveTail({ entries, maxEntries, rows, cols, expandedToolIds, thinkingVisible, reducedMotion, density, nowSec, theme, caps, }: LiveTailProps): React.ReactElement;
|
|
@@ -2,26 +2,31 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import { Box } from 'ink';
|
|
3
3
|
import { EntryView, nowSecFor } from '../Transcript.js';
|
|
4
4
|
import { SCROLLBACK_LIMITS } from './limits.js';
|
|
5
|
+
import { isExcluded } from './settled-boundary.js';
|
|
5
6
|
/**
|
|
6
|
-
* The last `maxEntries` entries, each with its predecessor.
|
|
7
|
+
* The last `maxEntries` printable entries, each with its predecessor.
|
|
7
8
|
*
|
|
8
|
-
* `prev` must come from the COMPLETE
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* `prev` must come from the same COMPLETE list rather than from the truncated slice, or
|
|
10
|
+
* the first rendered entry would look like the head of the conversation and drop the
|
|
11
|
+
* separation row the transcript shows above the same card.
|
|
11
12
|
*/
|
|
12
|
-
export function selectTailEntries(
|
|
13
|
+
export function selectTailEntries(entries, maxEntries) {
|
|
14
|
+
const printable = entries.filter((entry) => !isExcluded(entry));
|
|
13
15
|
const limit = Number.isFinite(maxEntries) ? Math.max(0, Math.floor(maxEntries)) : 0;
|
|
14
16
|
if (limit === 0)
|
|
15
17
|
return [];
|
|
16
|
-
const start = Math.max(0,
|
|
18
|
+
const start = Math.max(0, printable.length - limit);
|
|
17
19
|
const selected = [];
|
|
18
|
-
for (let index = start; index <
|
|
19
|
-
selected.push({
|
|
20
|
+
for (let index = start; index < printable.length; index += 1) {
|
|
21
|
+
selected.push({
|
|
22
|
+
entry: printable[index],
|
|
23
|
+
prev: index > 0 ? printable[index - 1] : undefined,
|
|
24
|
+
});
|
|
20
25
|
}
|
|
21
26
|
return selected;
|
|
22
27
|
}
|
|
23
|
-
export function LiveTail({
|
|
28
|
+
export function LiveTail({ entries, maxEntries, rows, cols, expandedToolIds, thinkingVisible, reducedMotion, density, nowSec, theme, caps, }) {
|
|
24
29
|
const limited = Math.min(maxEntries, SCROLLBACK_LIMITS.liveTailMaxEntries);
|
|
25
|
-
const selected = selectTailEntries(
|
|
30
|
+
const selected = selectTailEntries(entries, limited);
|
|
26
31
|
return (_jsx(Box, { flexDirection: "column", height: rows, width: cols, justifyContent: "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
32
|
}
|