@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
|
@@ -1,27 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Height and column budget of the native-scroll PANEL BAND
|
|
3
|
-
* (release-gate-and-native-panels §6.2).
|
|
3
|
+
* (release-gate-and-native-panels §6.2, tui-full-height-live-region D5).
|
|
4
4
|
*
|
|
5
5
|
* Native scroll mode drops the fixed frame, so it used to drop the panels with it:
|
|
6
6
|
* the TODO rail lived in `AppShell`'s middle band and the subagent roster in the
|
|
7
7
|
* viewport footer, and neither is rendered once `ScrollbackShell` takes over. The
|
|
8
|
-
* band is the replacement - one row pool shared by the roster (`TeamPanel`), the
|
|
9
|
-
*
|
|
8
|
+
* band is the replacement - one row pool shared by the roster (`TeamPanel`), the tail
|
|
9
|
+
* and the rail (`TodoPanel`), sitting directly above the composer.
|
|
10
|
+
*
|
|
11
|
+
* `fill` IS THE FEATURE (D5). In `fill` mode the band takes the WHOLE pool:
|
|
12
|
+
* `bandRows = remaining`, `railRows = bandRows` (so the rail spans every row the user
|
|
13
|
+
* can see) and `previewRows = bandRows - teamRows` (so the tail column ends against
|
|
14
|
+
* the composer). Before `fill`, the band asked for "just enough" rows and every row it
|
|
15
|
+
* did not ask for became the canvas `gap` - the blank band between the result and the
|
|
16
|
+
* running card that this feature removes.
|
|
10
17
|
*
|
|
11
18
|
* PURE: no Ink, no clock, no terminal, no config read. Every invariant below is
|
|
12
19
|
* therefore established BY CONSTRUCTION for all input combinations and can be
|
|
13
|
-
* enumerated by a property test (AC-9..AC-14, AC-
|
|
20
|
+
* enumerated by a property test (AC-9..AC-14, AC-34, AC-F3/F4):
|
|
14
21
|
*
|
|
15
22
|
* - `bandRows <= remaining` and therefore `liveRows <= rows - 1` (I-4). Ink writes
|
|
16
23
|
* `clearTerminal` - which contains `ESC[3J`, erasing the user's scrollback - as
|
|
17
|
-
* soon as a frame is as tall as the terminal
|
|
18
|
-
* between a tidy region and data loss.
|
|
24
|
+
* soon as a frame is as tall as the terminal.
|
|
19
25
|
* - `previewCols + railCols === contentCols`, always (AC-10). The rail is a column
|
|
20
26
|
* INSIDE the region, so the preview loses exactly the columns the rail takes and
|
|
21
27
|
* nothing else (I-5: printing keeps the full `contentCols`).
|
|
22
28
|
* - `teamRows > 0` leaves room for the rail plus one preview row (AC-12).
|
|
23
|
-
* - `
|
|
24
|
-
*
|
|
29
|
+
* - `fill` mode: `bandRows === remaining`, `railRows === bandRows` when the rail is
|
|
30
|
+
* visible, and `teamRows + previewRows === bandRows`.
|
|
25
31
|
*
|
|
26
32
|
* THERE IS EXACTLY ONE CALL SITE: `buildScrollbackBudget` (I-8). This module is
|
|
27
33
|
* exported so it can be tested, not so a second caller can allocate rows of its own
|
|
@@ -42,18 +48,14 @@ export type BandReason =
|
|
|
42
48
|
| 'short';
|
|
43
49
|
export interface LiveBandInput {
|
|
44
50
|
/**
|
|
45
|
-
* `buildFrameBudget().viewportRows
|
|
46
|
-
*
|
|
51
|
+
* `buildFrameBudget().viewportRows`: every row the band may use (the pool). Computed
|
|
52
|
+
* once by `buildScrollbackBudget`; callers must not re-derive it.
|
|
47
53
|
*/
|
|
48
54
|
remaining: number;
|
|
49
55
|
/** `cols - 1`: width of the whole band. `previewCols` is derived from it. */
|
|
50
56
|
contentCols: number;
|
|
51
57
|
/** `todoRailWidth(contentCols)`; `layout/rail.ts` stays the width authority. */
|
|
52
58
|
railCols: number;
|
|
53
|
-
/** The existing two caps combined: `min(maxLiveTailRows, floor(rows * share))`. */
|
|
54
|
-
previewCap: number;
|
|
55
|
-
/** Plain-text rows `buildLivePreview` wants (the measuring authority is unchanged). */
|
|
56
|
-
previewWanted: number;
|
|
57
59
|
/** `state.todos.items.length` (0 = no active plan). */
|
|
58
60
|
itemCount: number;
|
|
59
61
|
/** `cfg.todo.panel`. */
|
|
@@ -65,10 +67,18 @@ export interface LiveBandInput {
|
|
|
65
67
|
heldRailRows: number;
|
|
66
68
|
/** Hysteresis: the roster rows the previous frame already used. */
|
|
67
69
|
heldTeamRows: number;
|
|
70
|
+
/**
|
|
71
|
+
* The band takes the whole pool (native mode: always true, set by the budget).
|
|
72
|
+
*
|
|
73
|
+
* A plain boolean rather than "absence of a cap" because the difference is semantic:
|
|
74
|
+
* with `fill` the allocation is INDEPENDENT of the content (which is what makes the
|
|
75
|
+
* region the screen), while without it the band is sized to what it has to show.
|
|
76
|
+
*/
|
|
77
|
+
fill: boolean;
|
|
68
78
|
}
|
|
69
79
|
export interface LiveBand {
|
|
70
80
|
reason: BandReason;
|
|
71
|
-
/** Band height: `
|
|
81
|
+
/** Band height: `remaining` in `fill` mode, `teamRows + previewRows` otherwise. */
|
|
72
82
|
bandRows: number;
|
|
73
83
|
teamRows: number;
|
|
74
84
|
previewRows: number;
|
|
@@ -85,20 +95,19 @@ export interface LiveBand {
|
|
|
85
95
|
/**
|
|
86
96
|
* Allocate the band. THE ORDER OF THE STEPS IS THE SPECIFICATION - reordering them
|
|
87
97
|
* changes the priority between "what this run is doing" (rail + roster) and "the
|
|
88
|
-
* words arriving right now" (
|
|
98
|
+
* words arriving right now" (tail), which is a product decision, not a detail.
|
|
89
99
|
*
|
|
90
100
|
* 0. Normalise every numeric input (`NaN` / `Infinity` / negatives become 0).
|
|
91
101
|
* 1. An open overlay takes the whole pool (the band would otherwise double-count it).
|
|
92
|
-
* 2. Seat the roster first, but never at the cost of the rail plus one
|
|
102
|
+
* 2. Seat the roster first, but never at the cost of the rail plus one tail row:
|
|
93
103
|
* a dispatch is transient, the plan is the shape of the turn, both must fit.
|
|
94
104
|
* 3. Decide the rail's visibility, in the same reason order as `buildTodoRailLayout`.
|
|
95
|
-
* 4. RESERVE the rail's rows
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
* 6. `bandRows
|
|
100
|
-
*
|
|
101
|
-
* reading a result and still needs the list).
|
|
105
|
+
* 4. RESERVE the rail's rows - only when the band is NOT filling. `fill` derives
|
|
106
|
+
* `railRows` from `bandRows` in step 7, so the reservation has no consumer there.
|
|
107
|
+
* 5. The tail takes what is left: the whole column in `fill` mode, `previewWanted`
|
|
108
|
+
* clamped by the pool otherwise.
|
|
109
|
+
* 6. `bandRows`: the pool in `fill` mode, `max(railReserved, teamRows + previewRows)`
|
|
110
|
+
* otherwise (where a rail must still be there with no tail rows).
|
|
102
111
|
* 7. Columns: the input `contentCols` is the only width authority.
|
|
103
112
|
* 8. Hand the late-arriving values (the hysteresis) back out for the next frame.
|
|
104
113
|
*/
|
|
@@ -1,27 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Height and column budget of the native-scroll PANEL BAND
|
|
3
|
-
* (release-gate-and-native-panels §6.2).
|
|
3
|
+
* (release-gate-and-native-panels §6.2, tui-full-height-live-region D5).
|
|
4
4
|
*
|
|
5
5
|
* Native scroll mode drops the fixed frame, so it used to drop the panels with it:
|
|
6
6
|
* the TODO rail lived in `AppShell`'s middle band and the subagent roster in the
|
|
7
7
|
* viewport footer, and neither is rendered once `ScrollbackShell` takes over. The
|
|
8
|
-
* band is the replacement - one row pool shared by the roster (`TeamPanel`), the
|
|
9
|
-
*
|
|
8
|
+
* band is the replacement - one row pool shared by the roster (`TeamPanel`), the tail
|
|
9
|
+
* and the rail (`TodoPanel`), sitting directly above the composer.
|
|
10
|
+
*
|
|
11
|
+
* `fill` IS THE FEATURE (D5). In `fill` mode the band takes the WHOLE pool:
|
|
12
|
+
* `bandRows = remaining`, `railRows = bandRows` (so the rail spans every row the user
|
|
13
|
+
* can see) and `previewRows = bandRows - teamRows` (so the tail column ends against
|
|
14
|
+
* the composer). Before `fill`, the band asked for "just enough" rows and every row it
|
|
15
|
+
* did not ask for became the canvas `gap` - the blank band between the result and the
|
|
16
|
+
* running card that this feature removes.
|
|
10
17
|
*
|
|
11
18
|
* PURE: no Ink, no clock, no terminal, no config read. Every invariant below is
|
|
12
19
|
* therefore established BY CONSTRUCTION for all input combinations and can be
|
|
13
|
-
* enumerated by a property test (AC-9..AC-14, AC-
|
|
20
|
+
* enumerated by a property test (AC-9..AC-14, AC-34, AC-F3/F4):
|
|
14
21
|
*
|
|
15
22
|
* - `bandRows <= remaining` and therefore `liveRows <= rows - 1` (I-4). Ink writes
|
|
16
23
|
* `clearTerminal` - which contains `ESC[3J`, erasing the user's scrollback - as
|
|
17
|
-
* soon as a frame is as tall as the terminal
|
|
18
|
-
* between a tidy region and data loss.
|
|
24
|
+
* soon as a frame is as tall as the terminal.
|
|
19
25
|
* - `previewCols + railCols === contentCols`, always (AC-10). The rail is a column
|
|
20
26
|
* INSIDE the region, so the preview loses exactly the columns the rail takes and
|
|
21
27
|
* nothing else (I-5: printing keeps the full `contentCols`).
|
|
22
28
|
* - `teamRows > 0` leaves room for the rail plus one preview row (AC-12).
|
|
23
|
-
* - `
|
|
24
|
-
*
|
|
29
|
+
* - `fill` mode: `bandRows === remaining`, `railRows === bandRows` when the rail is
|
|
30
|
+
* visible, and `teamRows + previewRows === bandRows`.
|
|
25
31
|
*
|
|
26
32
|
* THERE IS EXACTLY ONE CALL SITE: `buildScrollbackBudget` (I-8). This module is
|
|
27
33
|
* exported so it can be tested, not so a second caller can allocate rows of its own
|
|
@@ -36,20 +42,19 @@ function nonNegativeInt(value) {
|
|
|
36
42
|
/**
|
|
37
43
|
* Allocate the band. THE ORDER OF THE STEPS IS THE SPECIFICATION - reordering them
|
|
38
44
|
* changes the priority between "what this run is doing" (rail + roster) and "the
|
|
39
|
-
* words arriving right now" (
|
|
45
|
+
* words arriving right now" (tail), which is a product decision, not a detail.
|
|
40
46
|
*
|
|
41
47
|
* 0. Normalise every numeric input (`NaN` / `Infinity` / negatives become 0).
|
|
42
48
|
* 1. An open overlay takes the whole pool (the band would otherwise double-count it).
|
|
43
|
-
* 2. Seat the roster first, but never at the cost of the rail plus one
|
|
49
|
+
* 2. Seat the roster first, but never at the cost of the rail plus one tail row:
|
|
44
50
|
* a dispatch is transient, the plan is the shape of the turn, both must fit.
|
|
45
51
|
* 3. Decide the rail's visibility, in the same reason order as `buildTodoRailLayout`.
|
|
46
|
-
* 4. RESERVE the rail's rows
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
* 6. `bandRows
|
|
51
|
-
*
|
|
52
|
-
* reading a result and still needs the list).
|
|
52
|
+
* 4. RESERVE the rail's rows - only when the band is NOT filling. `fill` derives
|
|
53
|
+
* `railRows` from `bandRows` in step 7, so the reservation has no consumer there.
|
|
54
|
+
* 5. The tail takes what is left: the whole column in `fill` mode, `previewWanted`
|
|
55
|
+
* clamped by the pool otherwise.
|
|
56
|
+
* 6. `bandRows`: the pool in `fill` mode, `max(railReserved, teamRows + previewRows)`
|
|
57
|
+
* otherwise (where a rail must still be there with no tail rows).
|
|
53
58
|
* 7. Columns: the input `contentCols` is the only width authority.
|
|
54
59
|
* 8. Hand the late-arriving values (the hysteresis) back out for the next frame.
|
|
55
60
|
*/
|
|
@@ -57,12 +62,11 @@ export function buildLiveBand(input) {
|
|
|
57
62
|
const remaining = nonNegativeInt(input.remaining);
|
|
58
63
|
const contentCols = nonNegativeInt(input.contentCols);
|
|
59
64
|
const railCols = nonNegativeInt(input.railCols);
|
|
60
|
-
const previewCap = nonNegativeInt(input.previewCap);
|
|
61
|
-
const previewWanted = nonNegativeInt(input.previewWanted);
|
|
62
65
|
const itemCount = nonNegativeInt(input.itemCount);
|
|
63
66
|
const teamRowsNatural = nonNegativeInt(input.teamRowsNatural);
|
|
64
67
|
const heldRailRows = nonNegativeInt(input.heldRailRows);
|
|
65
68
|
const heldTeamRows = nonNegativeInt(input.heldTeamRows);
|
|
69
|
+
const fill = input.fill === true;
|
|
66
70
|
if (input.overlayOpen) {
|
|
67
71
|
return {
|
|
68
72
|
reason: 'overlay', bandRows: remaining, teamRows: 0, previewRows: 0,
|
|
@@ -76,6 +80,14 @@ export function buildLiveBand(input) {
|
|
|
76
80
|
// ever seat the roster - the panel this feature exists to restore would never appear
|
|
77
81
|
// (found while implementing; recorded as IF-1 in the spec). The memory below only
|
|
78
82
|
// ever ADDS height back after a squeeze; it never withholds it.
|
|
83
|
+
//
|
|
84
|
+
// THE FLOOR SURVIVES `fill` ON PURPOSE (spec §5.3.2; RV-P2-5, recorded as not changed
|
|
85
|
+
// back). It holds 4 rows back from the roster even when the rail is not drawn, which
|
|
86
|
+
// looks like waste in `fill` mode - but it is the vetted `AC-12` behaviour, and
|
|
87
|
+
// `TodoPanel`'s 3-5 row compact form needs those rows to name the running item. The
|
|
88
|
+
// only visible cost is 4 blank rows at the TOP of the left column (the tail still
|
|
89
|
+
// ends against the composer), which is not one of the three reported defects; changing
|
|
90
|
+
// it here would reopen a different feature in the same commit.
|
|
79
91
|
const teamRows = teamCeiling;
|
|
80
92
|
const reason = !input.panelEnabled ? 'disabled'
|
|
81
93
|
: itemCount === 0 ? 'empty'
|
|
@@ -83,11 +95,20 @@ export function buildLiveBand(input) {
|
|
|
83
95
|
: remaining - teamRows < SCROLLBACK_LIMITS.railBandMinRows ? 'short'
|
|
84
96
|
: 'visible';
|
|
85
97
|
const railVisible = reason === 'visible';
|
|
86
|
-
|
|
98
|
+
// NO CONSUMER IN `fill` MODE (step 7 derives `railRows` from `bandRows`); kept as the
|
|
99
|
+
// documented historical formula so the non-fill allocation stays readable and a future
|
|
100
|
+
// caller that does not fill cannot silently lose the rail's reservation.
|
|
101
|
+
const railReserved = railVisible && !fill
|
|
87
102
|
? Math.min(Math.max(heldRailRows, TODO_LIMITS.panelFullRows), Math.max(0, remaining - teamRows - 1))
|
|
88
103
|
: 0;
|
|
89
|
-
const previewRows =
|
|
90
|
-
|
|
104
|
+
const previewRows = fill
|
|
105
|
+
? Math.max(0, remaining - teamRows)
|
|
106
|
+
// Non-fill: the tail takes what the rail's reservation leaves. The two row caps that
|
|
107
|
+
// used to clamp this line (`previewCap` / `previewWanted`) are RETIRED (D9) - they
|
|
108
|
+
// were the only inputs that made this branch differ from `fill`, so a non-fill caller
|
|
109
|
+
// now gets the pool too. The branch stays as the named expression of the older policy.
|
|
110
|
+
: Math.max(0, remaining - teamRows - railReserved);
|
|
111
|
+
const bandRows = fill ? remaining : Math.max(railReserved, teamRows + previewRows);
|
|
91
112
|
const railRows = railVisible ? bandRows : 0;
|
|
92
113
|
const railColsOut = railVisible ? railCols : 0;
|
|
93
114
|
return {
|
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Height budget of the native-scroll LIVE REGION
|
|
2
|
+
* Height budget of the native-scroll LIVE REGION
|
|
3
|
+
* (tui-native-terminal-scroll §4.6, tui-full-height-live-region D1/D3/D5).
|
|
3
4
|
*
|
|
4
5
|
* I-S1 · `liveRows <= max(0, rows - 1)` for every input. Ink writes
|
|
5
6
|
* `clearTerminal` (which contains `ESC[3J`, erasing the user's scrollback) as soon
|
|
6
|
-
* as a frame is as tall as the terminal
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* as a frame is as tall as the terminal. This feature takes I-S1's ONE row of margin
|
|
8
|
+
* and nothing else: `liveRows === rows - 1` exactly, which is why the frame-height
|
|
9
|
+
* guard (I-F13 / AC-F17) exists - there is no slack left to absorb a stray row.
|
|
10
|
+
*
|
|
11
|
+
* THE REGION IS THE SCREEN. The region used to be only as tall as the band asked for,
|
|
12
|
+
* and every row it did not ask for became `gap` - blank rows ABOVE the content, which
|
|
13
|
+
* is what the user reported as "the app starts halfway down the screen" and as "a big
|
|
14
|
+
* blank between the result and the running card". The band now takes the whole pool
|
|
15
|
+
* (`fill`), so the region is `[header][band][composer][status]` top to bottom and
|
|
16
|
+
* `gap` cannot exist: `resolveCanvasLayout` is given `frameRows === capacity` and
|
|
17
|
+
* derives `gap = 0` (see the spec's §5.5 proof, which is why the writer needs no
|
|
18
|
+
* change at all).
|
|
9
19
|
*
|
|
10
20
|
* The composer, popup and status rows come from the SAME `buildFrameBudget` the
|
|
11
21
|
* full-screen layout uses, so the two modes cannot disagree about how tall the
|
|
12
|
-
* editor is. What differs is the top:
|
|
13
|
-
*
|
|
14
|
-
* preview.
|
|
22
|
+
* editor is. What differs is the top: the header is a real slot here (D3), and the
|
|
23
|
+
* row it occupies is spent from the region, not from the printed transcript.
|
|
15
24
|
*
|
|
16
25
|
* THIS IS THE ONLY CALL SITE OF `buildLiveBand` (I-8). `remaining` is computed here
|
|
17
26
|
* once and handed to the band; an `App` that computed its own copy would make
|
|
@@ -22,16 +31,15 @@ import { type LiveBand, type LiveBandInput } from './band.js';
|
|
|
22
31
|
import { type FrameBudgetInput } from '../layout/budget.js';
|
|
23
32
|
/**
|
|
24
33
|
* Everything the band needs that this module derives for itself: `remaining`,
|
|
25
|
-
* `contentCols`, `railCols`, `
|
|
26
|
-
*
|
|
27
|
-
* The caller supplies only the facts it
|
|
28
|
-
* hysteresis), never a number this
|
|
34
|
+
* `contentCols`, `railCols`, `overlayOpen` and `fill` are computed below - `fill` is
|
|
35
|
+
* NOT part of the caller's input (I-8: the budget decides it, and "the band fills the
|
|
36
|
+
* pool" is not a preference an `App` may hold). The caller supplies only the facts it
|
|
37
|
+
* alone knows (plan, panel switch, roster size, hysteresis), never a number this
|
|
38
|
+
* module is supposed to own.
|
|
29
39
|
*/
|
|
30
|
-
export type ScrollbackBandInput = Omit<LiveBandInput, 'remaining' | 'contentCols' | 'railCols' | '
|
|
40
|
+
export type ScrollbackBandInput = Omit<LiveBandInput, 'remaining' | 'contentCols' | 'railCols' | 'overlayOpen' | 'fill'>;
|
|
31
41
|
export interface ScrollbackBudgetInput extends FrameBudgetInput {
|
|
32
42
|
overlayOpen: boolean;
|
|
33
|
-
/** Preview rows the content would like; the budget grants what fits. */
|
|
34
|
-
liveTailWanted: number;
|
|
35
43
|
/** The writer tripped its tall-frame guard: degrade to the placeholder until a resize. */
|
|
36
44
|
forceInactive?: boolean;
|
|
37
45
|
/**
|
|
@@ -49,6 +57,12 @@ export interface ScrollbackBudget {
|
|
|
49
57
|
contentCols: number;
|
|
50
58
|
/** Terminal below the 40x12 floor: the region is a one-row placeholder. */
|
|
51
59
|
inactive: boolean;
|
|
60
|
+
/**
|
|
61
|
+
* The header slot, a real row inside the region (D3). 0 on a degraded frame: the
|
|
62
|
+
* placeholder draws no header, and reporting 1 there would break the row identity
|
|
63
|
+
* (I-F4) on exactly the frames `/perf` is read to explain.
|
|
64
|
+
*/
|
|
65
|
+
headerRows: number;
|
|
52
66
|
statusRows: 0 | 1 | 2;
|
|
53
67
|
composerRows: number;
|
|
54
68
|
composerSlotRows: number;
|
|
@@ -56,17 +70,15 @@ export interface ScrollbackBudget {
|
|
|
56
70
|
popupRows: number;
|
|
57
71
|
popupMaxHeight: number;
|
|
58
72
|
overlayRows: number;
|
|
59
|
-
/**
|
|
73
|
+
/** Tail rows: the band's left column below the roster (`= band.previewRows`). */
|
|
60
74
|
liveTailRows: number;
|
|
61
|
-
/** Rows above the composer: `= band.bandRows
|
|
75
|
+
/** Rows above the composer: `= band.bandRows = remaining`. */
|
|
62
76
|
topRows: number;
|
|
63
|
-
/** Total live-region height
|
|
77
|
+
/** Total live-region height: `rows - 1`, the whole screen above the parked row. */
|
|
64
78
|
liveRows: number;
|
|
65
79
|
/** Height and column allocation of the panel band; the single authority is `band.ts`. */
|
|
66
80
|
band: LiveBand;
|
|
67
81
|
/** Preview/tail width: `contentCols - railCols`. Printing keeps `contentCols`. */
|
|
68
82
|
previewCols: number;
|
|
69
|
-
/** Rows the preview wanted before any clipping (feeds hysteresis and `/perf`). */
|
|
70
|
-
previewWanted: number;
|
|
71
83
|
}
|
|
72
84
|
export declare function buildScrollbackBudget(input: ScrollbackBudgetInput): ScrollbackBudget;
|
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Height budget of the native-scroll LIVE REGION
|
|
2
|
+
* Height budget of the native-scroll LIVE REGION
|
|
3
|
+
* (tui-native-terminal-scroll §4.6, tui-full-height-live-region D1/D3/D5).
|
|
3
4
|
*
|
|
4
5
|
* I-S1 · `liveRows <= max(0, rows - 1)` for every input. Ink writes
|
|
5
6
|
* `clearTerminal` (which contains `ESC[3J`, erasing the user's scrollback) as soon
|
|
6
|
-
* as a frame is as tall as the terminal
|
|
7
|
-
*
|
|
8
|
-
*
|
|
7
|
+
* as a frame is as tall as the terminal. This feature takes I-S1's ONE row of margin
|
|
8
|
+
* and nothing else: `liveRows === rows - 1` exactly, which is why the frame-height
|
|
9
|
+
* guard (I-F13 / AC-F17) exists - there is no slack left to absorb a stray row.
|
|
10
|
+
*
|
|
11
|
+
* THE REGION IS THE SCREEN. The region used to be only as tall as the band asked for,
|
|
12
|
+
* and every row it did not ask for became `gap` - blank rows ABOVE the content, which
|
|
13
|
+
* is what the user reported as "the app starts halfway down the screen" and as "a big
|
|
14
|
+
* blank between the result and the running card". The band now takes the whole pool
|
|
15
|
+
* (`fill`), so the region is `[header][band][composer][status]` top to bottom and
|
|
16
|
+
* `gap` cannot exist: `resolveCanvasLayout` is given `frameRows === capacity` and
|
|
17
|
+
* derives `gap = 0` (see the spec's §5.5 proof, which is why the writer needs no
|
|
18
|
+
* change at all).
|
|
9
19
|
*
|
|
10
20
|
* The composer, popup and status rows come from the SAME `buildFrameBudget` the
|
|
11
21
|
* full-screen layout uses, so the two modes cannot disagree about how tall the
|
|
12
|
-
* editor is. What differs is the top:
|
|
13
|
-
*
|
|
14
|
-
* preview.
|
|
22
|
+
* editor is. What differs is the top: the header is a real slot here (D3), and the
|
|
23
|
+
* row it occupies is spent from the region, not from the printed transcript.
|
|
15
24
|
*
|
|
16
25
|
* THIS IS THE ONLY CALL SITE OF `buildLiveBand` (I-8). `remaining` is computed here
|
|
17
26
|
* once and handed to the band; an `App` that computed its own copy would make
|
|
@@ -21,7 +30,6 @@
|
|
|
21
30
|
import { buildLiveBand } from './band.js';
|
|
22
31
|
import { buildFrameBudget } from '../layout/budget.js';
|
|
23
32
|
import { todoRailWidth } from '../layout/rail.js';
|
|
24
|
-
import { SCROLLBACK_LIMITS } from './limits.js';
|
|
25
33
|
/**
|
|
26
34
|
* The band of a frame that shows no content at all (below the 40x12 floor, or after
|
|
27
35
|
* the writer's tall-frame guard tripped). It is a REAL value rather than `undefined`
|
|
@@ -37,30 +45,35 @@ function degradedBand(contentCols) {
|
|
|
37
45
|
export function buildScrollbackBudget(input) {
|
|
38
46
|
const base = buildFrameBudget(input);
|
|
39
47
|
const contentCols = Math.max(1, base.cols - 1);
|
|
40
|
-
// `liveTailWanted` comes from a measurement, but a caller that never measured passes
|
|
41
|
-
// `undefined`/`NaN`; normalising here keeps `NaN` out of `/perf` and out of the band.
|
|
42
|
-
const previewWanted = Number.isFinite(input.liveTailWanted)
|
|
43
|
-
? Math.max(0, Math.floor(input.liveTailWanted))
|
|
44
|
-
: 0;
|
|
45
48
|
if (base.inactive || input.forceInactive === true) {
|
|
46
49
|
return {
|
|
47
|
-
rows: base.rows, cols: base.cols, contentCols, inactive: true,
|
|
48
|
-
composerRows: 0, composerSlotRows: 0, composerCols: base.composerCols,
|
|
49
|
-
popupMaxHeight: 0, overlayRows: 0, liveTailRows: 0, topRows: 0,
|
|
50
|
+
rows: base.rows, cols: base.cols, contentCols, inactive: true, headerRows: 0,
|
|
51
|
+
statusRows: 0, composerRows: 0, composerSlotRows: 0, composerCols: base.composerCols,
|
|
52
|
+
popupRows: 0, popupMaxHeight: 0, overlayRows: 0, liveTailRows: 0, topRows: 0,
|
|
50
53
|
liveRows: Math.max(0, Math.min(1, base.rows - 1)),
|
|
51
|
-
band: degradedBand(contentCols), previewCols: contentCols,
|
|
54
|
+
band: degradedBand(contentCols), previewCols: contentCols,
|
|
52
55
|
};
|
|
53
56
|
}
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
+
// D1 · H = C. `frameRows` is `rows - 1` by construction (`layout/budget.ts`), so the
|
|
58
|
+
// region ends exactly on the row above the writer's parked cursor.
|
|
59
|
+
const headerRows = 1;
|
|
60
|
+
const liveRows = Math.max(1, base.frameRows);
|
|
61
|
+
// The whole pool: everything between the header and the status line. Two independent
|
|
62
|
+
// derivations put its floor at 1, so `max(0, ...)` is defensive only:
|
|
63
|
+
// 1. `composerSlotRows = 2 + clampDraftRows(rows, draftRows)` and
|
|
64
|
+
// `draftMaxRows(rows) = max(1, min(6, floor(rows / 4)))`, so on the narrowest
|
|
65
|
+
// full-screen frame (`rows = 12`) `remaining >= 11 - 1 - 5 - 1 = 4`;
|
|
66
|
+
// 2. with the completion menu at its maximum, `popupMaxHeight` already subtracts
|
|
67
|
+
// this same pair of rows, so `remaining = popupMaxHeight + 1 - popupRows >= 1`.
|
|
68
|
+
const remaining = Math.max(0, liveRows - headerRows - base.composerSlotRows - base.statusRows);
|
|
57
69
|
const band = buildLiveBand({
|
|
58
70
|
...input.band,
|
|
59
71
|
remaining,
|
|
60
72
|
contentCols,
|
|
61
73
|
railCols: todoRailWidth(contentCols),
|
|
62
|
-
|
|
63
|
-
|
|
74
|
+
// The band fills the pool: no row of the region is left blank, which is the whole
|
|
75
|
+
// point of the feature (D5). Taken from HERE, never from `input.band`.
|
|
76
|
+
fill: true,
|
|
64
77
|
// Taken from THIS input, never from `input.band`: an overlay that the frame knows
|
|
65
78
|
// about but the band did not would draw no overlay at all, and the shell keys the
|
|
66
79
|
// overlay's rows off the band.
|
|
@@ -69,13 +82,12 @@ export function buildScrollbackBudget(input) {
|
|
|
69
82
|
const overlayRows = band.reason === 'overlay' ? band.bandRows : 0;
|
|
70
83
|
const liveTailRows = band.previewRows;
|
|
71
84
|
const topRows = band.bandRows;
|
|
72
|
-
const liveRows = topRows + base.composerSlotRows + base.statusRows;
|
|
73
85
|
return {
|
|
74
|
-
rows: base.rows, cols: base.cols, contentCols, inactive: false,
|
|
86
|
+
rows: base.rows, cols: base.cols, contentCols, inactive: false, headerRows,
|
|
75
87
|
statusRows: base.statusRows, composerRows: base.composerRows,
|
|
76
88
|
composerSlotRows: base.composerSlotRows, composerCols: base.composerCols,
|
|
77
89
|
popupRows: base.popupRows, popupMaxHeight: base.popupMaxHeight, overlayRows,
|
|
78
90
|
liveTailRows, topRows, liveRows,
|
|
79
|
-
band, previewCols: band.previewCols,
|
|
91
|
+
band, previewCols: band.previewCols,
|
|
80
92
|
};
|
|
81
93
|
}
|
|
@@ -18,19 +18,43 @@ export declare const SCROLLBACK_LIMITS: {
|
|
|
18
18
|
readonly guardTripCount: 3;
|
|
19
19
|
/** Session summary rows printed on exit. */
|
|
20
20
|
readonly exitSummaryMaxRows: 4;
|
|
21
|
-
/** Streaming preview rows, absolute cap. */
|
|
22
|
-
readonly maxLiveTailRows: 12;
|
|
23
|
-
/** ... and as a share of the terminal height. */
|
|
24
|
-
readonly liveTailRowShare: 0.4;
|
|
25
21
|
/**
|
|
26
|
-
*
|
|
22
|
+
* Entries the region may hold on screen at once
|
|
23
|
+
* (tui-full-height-live-region D6; the WINDOW is defined by I-F8 and computed in
|
|
24
|
+
* `tail-window.ts`).
|
|
27
25
|
*
|
|
28
|
-
*
|
|
29
|
-
* rows
|
|
30
|
-
*
|
|
31
|
-
*
|
|
26
|
+
* 10 is DERIVED, not a preference. The band's left column is the whole pool
|
|
27
|
+
* (`pool = rows - 1 - header - composer - status`, i.e. 24 rows on a 30-row
|
|
28
|
+
* terminal) and one entry occupies about 2.4 rows, so a window of 4 would leave
|
|
29
|
+
* the column half empty - the blank band this feature removes. It stays in the
|
|
30
|
+
* per-frame COST class the old value of 4 was measured in: `EntryView` is
|
|
31
|
+
* memoised and only the streaming entry re-renders, so this bounds component
|
|
32
|
+
* instances, not per-frame work.
|
|
33
|
+
*
|
|
34
|
+
* There is deliberately NO companion row cap any more (retired: the old
|
|
35
|
+
* `maxLiveTailRows` / `liveTailRowShare` pair). The rows a tail may take are
|
|
36
|
+
* `band.previewRows` - the whole left column - and "the region can never be
|
|
37
|
+
* taller than the screen" is `liveRows === rows - 1`'s job (I-F5), not a second
|
|
38
|
+
* number's. Those two caps disagreed with the pool, and that disagreement WAS the
|
|
39
|
+
* blank band between the result and the running card.
|
|
40
|
+
*/
|
|
41
|
+
readonly liveTailMaxEntries: 10;
|
|
42
|
+
/**
|
|
43
|
+
* Safety margin the live tail asks for before it anchors its content to the TOP of
|
|
44
|
+
* the region (tui-first-reply-bottom-aligned, plan A-prime; the decision itself is
|
|
45
|
+
* `tail-anchor.ts`).
|
|
46
|
+
*
|
|
47
|
+
* 2 is DERIVED, not a preference. The anchor is decided from
|
|
48
|
+
* `layout/virtual-window.ts::estimateEntryRows`, which charges one separation row per
|
|
49
|
+
* entry and counts RAW markdown lines - so it UNDER-estimates a `#` heading (which
|
|
50
|
+
* also draws an `hrule`) and a code block (whose rail narrows the text by two columns,
|
|
51
|
+
* making long lines wrap). Inside a virtual window that error self-corrects when the
|
|
52
|
+
* entry is measured; the live region has no measurement pass, so the same error would
|
|
53
|
+
* decide which rows get CLIPPED - and `flex-start` clips the NEWEST rows.
|
|
54
|
+
* The margin must therefore always stay on the `bottom` side: a needless fallback
|
|
55
|
+
* costs one stacked frame, an early anchor hides the text the user is waiting for.
|
|
32
56
|
*/
|
|
33
|
-
readonly
|
|
57
|
+
readonly tailAnchorSlackRows: 2;
|
|
34
58
|
/**
|
|
35
59
|
* Rows the rail needs before the band will show it: `TODO_LIMITS.panelMinRows + 1`.
|
|
36
60
|
*
|
|
@@ -18,19 +18,43 @@ export const SCROLLBACK_LIMITS = {
|
|
|
18
18
|
guardTripCount: 3,
|
|
19
19
|
/** Session summary rows printed on exit. */
|
|
20
20
|
exitSummaryMaxRows: 4,
|
|
21
|
-
/** Streaming preview rows, absolute cap. */
|
|
22
|
-
maxLiveTailRows: 12,
|
|
23
|
-
/** ... and as a share of the terminal height. */
|
|
24
|
-
liveTailRowShare: 0.4,
|
|
25
21
|
/**
|
|
26
|
-
*
|
|
22
|
+
* Entries the region may hold on screen at once
|
|
23
|
+
* (tui-full-height-live-region D6; the WINDOW is defined by I-F8 and computed in
|
|
24
|
+
* `tail-window.ts`).
|
|
27
25
|
*
|
|
28
|
-
*
|
|
29
|
-
* rows
|
|
30
|
-
*
|
|
31
|
-
*
|
|
26
|
+
* 10 is DERIVED, not a preference. The band's left column is the whole pool
|
|
27
|
+
* (`pool = rows - 1 - header - composer - status`, i.e. 24 rows on a 30-row
|
|
28
|
+
* terminal) and one entry occupies about 2.4 rows, so a window of 4 would leave
|
|
29
|
+
* the column half empty - the blank band this feature removes. It stays in the
|
|
30
|
+
* per-frame COST class the old value of 4 was measured in: `EntryView` is
|
|
31
|
+
* memoised and only the streaming entry re-renders, so this bounds component
|
|
32
|
+
* instances, not per-frame work.
|
|
33
|
+
*
|
|
34
|
+
* There is deliberately NO companion row cap any more (retired: the old
|
|
35
|
+
* `maxLiveTailRows` / `liveTailRowShare` pair). The rows a tail may take are
|
|
36
|
+
* `band.previewRows` - the whole left column - and "the region can never be
|
|
37
|
+
* taller than the screen" is `liveRows === rows - 1`'s job (I-F5), not a second
|
|
38
|
+
* number's. Those two caps disagreed with the pool, and that disagreement WAS the
|
|
39
|
+
* blank band between the result and the running card.
|
|
40
|
+
*/
|
|
41
|
+
liveTailMaxEntries: 10,
|
|
42
|
+
/**
|
|
43
|
+
* Safety margin the live tail asks for before it anchors its content to the TOP of
|
|
44
|
+
* the region (tui-first-reply-bottom-aligned, plan A-prime; the decision itself is
|
|
45
|
+
* `tail-anchor.ts`).
|
|
46
|
+
*
|
|
47
|
+
* 2 is DERIVED, not a preference. The anchor is decided from
|
|
48
|
+
* `layout/virtual-window.ts::estimateEntryRows`, which charges one separation row per
|
|
49
|
+
* entry and counts RAW markdown lines - so it UNDER-estimates a `#` heading (which
|
|
50
|
+
* also draws an `hrule`) and a code block (whose rail narrows the text by two columns,
|
|
51
|
+
* making long lines wrap). Inside a virtual window that error self-corrects when the
|
|
52
|
+
* entry is measured; the live region has no measurement pass, so the same error would
|
|
53
|
+
* decide which rows get CLIPPED - and `flex-start` clips the NEWEST rows.
|
|
54
|
+
* The margin must therefore always stay on the `bottom` side: a needless fallback
|
|
55
|
+
* costs one stacked frame, an early anchor hides the text the user is waiting for.
|
|
32
56
|
*/
|
|
33
|
-
|
|
57
|
+
tailAnchorSlackRows: 2,
|
|
34
58
|
/**
|
|
35
59
|
* Rows the rail needs before the band will show it: `TODO_LIMITS.panelMinRows + 1`.
|
|
36
60
|
*
|
|
@@ -1,18 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* (tui-native-terminal-scroll §4.6).
|
|
2
|
+
* Plain-text fallback rows for the native-scroll live region
|
|
3
|
+
* (tui-native-terminal-scroll §4.6, tui-full-height-live-region RV-P2-7).
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* THE REAL TAIL IS `LiveTail`, NOT THIS. Native mode draws the region's tail with the
|
|
6
|
+
* actual entry components, so these rows are only the fallback for a frame that mounts
|
|
7
|
+
* no component there (and the floor of the measurement path). They are kept rather than
|
|
8
|
+
* deleted because the render tests use them to assert exactly that frame, and because
|
|
9
|
+
* `<= 24 wrapToRows` calls per frame is far below the cost of the components it stands
|
|
10
|
+
* in for.
|
|
11
|
+
*
|
|
12
|
+
* The input is the TAIL WINDOW (`selectTailWindow`), settled entries included: at
|
|
13
|
+
* `H = rows - 1` the region shows the last K entries of the conversation, so a
|
|
14
|
+
* projection built only from the unfinished ones would disagree with the screen.
|
|
15
|
+
* The field keeps its historical name `unsettled` because it is still "the entries the
|
|
16
|
+
* region is showing", and renaming it would touch the preview tests for no behaviour.
|
|
10
17
|
*
|
|
11
18
|
* PURE: no Ink, no theme, no I/O. Every returned row fits `cols` display columns.
|
|
12
19
|
*/
|
|
13
20
|
import type { Entry } from '../../agent/reducer.js';
|
|
14
21
|
export interface LivePreviewInput {
|
|
15
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* The entries to project, oldest first - the tail window in native mode. May include
|
|
24
|
+
* excluded entries; they are skipped.
|
|
25
|
+
*/
|
|
16
26
|
unsettled: readonly Entry[];
|
|
17
27
|
/** Content width (`cols - 1`). */
|
|
18
28
|
cols: number;
|
|
@@ -22,5 +32,3 @@ export interface LivePreviewInput {
|
|
|
22
32
|
}
|
|
23
33
|
/** The last `rows` preview rows, oldest first. `[]` when `rows <= 0`. */
|
|
24
34
|
export declare function buildLivePreview(input: LivePreviewInput): string[];
|
|
25
|
-
/** How many preview rows the unsettled entries would like, before budget clamping. */
|
|
26
|
-
export declare function wantedPreviewRows(input: Omit<LivePreviewInput, 'rows'>, cap: number): number;
|