@aragon-agent/cli 0.6.31 → 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 +119 -0
- package/README.md +2970 -2826
- package/dist/agent/controller.d.ts +46 -0
- package/dist/agent/controller.js +175 -7
- package/dist/agent/job-notices.d.ts +143 -0
- package/dist/agent/job-notices.js +182 -0
- package/dist/agent/reducer.d.ts +21 -1
- package/dist/agent/reducer.js +7 -0
- package/dist/agent/system-prompt.d.ts +12 -0
- package/dist/agent/system-prompt.js +2 -0
- package/dist/cli.js +236 -8
- package/dist/commands/builtins.js +23 -8
- package/dist/commands/perf.d.ts +77 -1
- package/dist/commands/perf.js +37 -5
- package/dist/config/cli-commands.d.ts +27 -0
- package/dist/config/cli-commands.js +54 -1
- package/dist/config/env.js +38 -0
- package/dist/config/load.js +50 -1
- package/dist/config/schema.d.ts +73 -0
- package/dist/config/schema.js +34 -0
- package/dist/config/store.js +14 -1
- package/dist/exec/events.d.ts +29 -3
- package/dist/exec/events.js +3 -12
- package/dist/exec/runner.d.ts +24 -0
- package/dist/exec/runner.js +79 -0
- package/dist/input/cursor-report.d.ts +61 -0
- package/dist/input/cursor-report.js +97 -0
- package/dist/input/stdin-filter.d.ts +22 -0
- package/dist/input/stdin-filter.js +48 -3
- package/dist/proc/limits.d.ts +76 -2
- package/dist/proc/limits.js +83 -2
- package/dist/proc/log-mirror.d.ts +59 -0
- package/dist/proc/log-mirror.js +203 -0
- package/dist/proc/log-ring.d.ts +22 -0
- package/dist/proc/log-ring.js +20 -1
- package/dist/proc/notices.d.ts +57 -0
- package/dist/proc/notices.js +101 -0
- package/dist/proc/progress.d.ts +40 -0
- package/dist/proc/progress.js +135 -0
- package/dist/proc/prompt.d.ts +24 -2
- package/dist/proc/prompt.js +51 -2
- package/dist/proc/supervisor.d.ts +35 -2
- package/dist/proc/supervisor.js +142 -43
- package/dist/proc/types.d.ts +89 -1
- package/dist/proc/wait.d.ts +57 -0
- package/dist/proc/wait.js +133 -0
- package/dist/session/persist.js +3 -1
- package/dist/session/validate-session.js +17 -0
- package/dist/tools/bash-tool.d.ts +12 -1
- package/dist/tools/bash-tool.js +100 -13
- package/dist/tools/index.d.ts +26 -2
- package/dist/tools/index.js +43 -2
- package/dist/tools/job-tools.d.ts +54 -0
- package/dist/tools/job-tools.js +329 -0
- package/dist/tools/proc-tools.d.ts +1 -1
- package/dist/tools/proc-tools.js +49 -17
- package/dist/ui/ActivityLine.js +7 -0
- package/dist/ui/App.js +98 -12
- package/dist/ui/ScrollbackShell.d.ts +62 -11
- package/dist/ui/ScrollbackShell.js +11 -3
- package/dist/ui/StatusBar.d.ts +5 -0
- package/dist/ui/StatusBar.js +1 -0
- package/dist/ui/Transcript.d.ts +14 -0
- package/dist/ui/Transcript.js +14 -2
- package/dist/ui/entries/ServiceCard.d.ts +17 -1
- package/dist/ui/entries/ServiceCard.js +39 -4
- package/dist/ui/layout/status-layout.d.ts +13 -1
- package/dist/ui/layout/status-layout.js +10 -0
- package/dist/ui/layout/virtual-window.js +14 -1
- package/dist/ui/run-status-row.d.ts +12 -0
- package/dist/ui/screen.d.ts +17 -4
- package/dist/ui/screen.js +14 -6
- package/dist/ui/scrollback/LiveTail.d.ts +79 -0
- package/dist/ui/scrollback/LiveTail.js +32 -0
- package/dist/ui/scrollback/band.d.ts +114 -0
- package/dist/ui/scrollback/band.js +130 -0
- package/dist/ui/scrollback/budget.d.ts +51 -10
- package/dist/ui/scrollback/budget.js +69 -20
- package/dist/ui/scrollback/canvas.d.ts +99 -0
- package/dist/ui/scrollback/canvas.js +113 -0
- package/dist/ui/scrollback/cursor-probe.d.ts +71 -0
- package/dist/ui/scrollback/cursor-probe.js +133 -0
- package/dist/ui/scrollback/limits.d.ts +61 -4
- package/dist/ui/scrollback/limits.js +61 -4
- package/dist/ui/scrollback/live-preview.d.ts +18 -10
- package/dist/ui/scrollback/live-preview.js +14 -11
- package/dist/ui/scrollback/scrollback-writer.d.ts +104 -16
- package/dist/ui/scrollback/scrollback-writer.js +368 -82
- 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 +34 -15
- package/dist/ui/scrollback/use-scrollback-view.js +110 -59
- package/dist/ui/scrollback/writer-shapes.d.ts +68 -2
- package/dist/ui/scrollback/writer-shapes.js +53 -8
- package/package.json +3 -3
package/dist/ui/screen.d.ts
CHANGED
|
@@ -74,6 +74,15 @@ export declare function enterAltScreen(stdout: NodeJS.WriteStream | undefined, o
|
|
|
74
74
|
export interface NativeScreenOptions {
|
|
75
75
|
/** Same discipline as `AltScreenOptions.bracketedPaste`: a filter that consumes it exists. */
|
|
76
76
|
readonly bracketedPaste?: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Line feeds to write BEFORE anything else, so the first batch starts from the
|
|
79
|
+
* bottom row of the screen (tui-bottom-anchored-live-region D4 rung 2).
|
|
80
|
+
*
|
|
81
|
+
* `cli.tsx` passes `rows - 1` when the cursor probe could not measure the
|
|
82
|
+
* position, and `0` when it did. Filling on a measured terminal would undo the
|
|
83
|
+
* measurement — and cost the page-flip this feature exists to avoid.
|
|
84
|
+
*/
|
|
85
|
+
readonly anchorFillRows?: number;
|
|
77
86
|
}
|
|
78
87
|
/**
|
|
79
88
|
* Native-scroll mode's counterpart of `enterAltScreen` (tui-native-terminal-scroll
|
|
@@ -81,10 +90,14 @@ export interface NativeScreenOptions {
|
|
|
81
90
|
* the conversation lands in the terminal's own scrollback; there is no alternate
|
|
82
91
|
* screen, no mouse reporting and no DEC 1007 save/restore.
|
|
83
92
|
*
|
|
84
|
-
* The only
|
|
85
|
-
* is encoded and has nothing to do with scrolling
|
|
86
|
-
*
|
|
87
|
-
*
|
|
93
|
+
* The only modes it touches are DEC 2004 (bracketed paste), which changes how input
|
|
94
|
+
* is encoded and has nothing to do with scrolling, and — when the bottom anchor could
|
|
95
|
+
* not be measured — the one-off fill that puts the cursor on the last row. Returns the
|
|
96
|
+
* SAME `ScreenHandle` shape, so the four idempotent restore paths in `cli.tsx` need not
|
|
97
|
+
* know which mode is running. A non-TTY stdout is a hard no-op, like `enterAltScreen`.
|
|
98
|
+
*
|
|
99
|
+
* ORDER IS LOAD-BEARING: the fill goes FIRST. Written after `?2004h` it would be the
|
|
100
|
+
* same bytes, but written after any content it would scroll that content away.
|
|
88
101
|
*/
|
|
89
102
|
export declare function enterNativeScreen(stdout: NodeJS.WriteStream | undefined, options?: NativeScreenOptions): ScreenHandle;
|
|
90
103
|
/**
|
package/dist/ui/screen.js
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* Hoisting it into `buildProgram()` / `parseAsync` would push `\x1b[?1049h`
|
|
12
12
|
* into `aragon -p "…" > out.txt`, `aragon config set`, and `aragon --version`.
|
|
13
13
|
*/
|
|
14
|
+
import { fillBytes } from './scrollback/writer-shapes.js';
|
|
14
15
|
const ENTER_ALT = '\x1b[?1049h';
|
|
15
16
|
const LEAVE_ALT = '\x1b[?1049l';
|
|
16
17
|
const CURSOR_HOME = '\x1b[H';
|
|
@@ -187,17 +188,24 @@ export function enterAltScreen(stdout, options = { mouse: false }) {
|
|
|
187
188
|
* the conversation lands in the terminal's own scrollback; there is no alternate
|
|
188
189
|
* screen, no mouse reporting and no DEC 1007 save/restore.
|
|
189
190
|
*
|
|
190
|
-
* The only
|
|
191
|
-
* is encoded and has nothing to do with scrolling
|
|
192
|
-
*
|
|
193
|
-
*
|
|
191
|
+
* The only modes it touches are DEC 2004 (bracketed paste), which changes how input
|
|
192
|
+
* is encoded and has nothing to do with scrolling, and — when the bottom anchor could
|
|
193
|
+
* not be measured — the one-off fill that puts the cursor on the last row. Returns the
|
|
194
|
+
* SAME `ScreenHandle` shape, so the four idempotent restore paths in `cli.tsx` need not
|
|
195
|
+
* know which mode is running. A non-TTY stdout is a hard no-op, like `enterAltScreen`.
|
|
196
|
+
*
|
|
197
|
+
* ORDER IS LOAD-BEARING: the fill goes FIRST. Written after `?2004h` it would be the
|
|
198
|
+
* same bytes, but written after any content it would scroll that content away.
|
|
194
199
|
*/
|
|
195
200
|
export function enterNativeScreen(stdout, options = {}) {
|
|
196
201
|
if (!stdout || !stdout.isTTY || typeof stdout.write !== 'function')
|
|
197
202
|
return NOOP_HANDLE;
|
|
198
203
|
const bracketedPaste = options.bracketedPaste === true;
|
|
199
|
-
|
|
200
|
-
|
|
204
|
+
const fillRows = Math.max(0, Math.floor(options.anchorFillRows ?? 0));
|
|
205
|
+
const head = (fillRows > 0 ? fillBytes(fillRows) : '') +
|
|
206
|
+
(bracketedPaste ? ENABLE_BRACKETED_PASTE : '');
|
|
207
|
+
if (head.length > 0)
|
|
208
|
+
safeWrite(stdout, head);
|
|
201
209
|
let restored = false;
|
|
202
210
|
return {
|
|
203
211
|
restore() {
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
/**
|
|
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
|
+
*
|
|
5
|
+
* WHY THIS FILE EXISTS. Native scroll mode cannot print an entry until it settles, so
|
|
6
|
+
* the live region used to show `buildLivePreview`'s plain-text projection - and for a
|
|
7
|
+
* running dispatch that projection is four grey rows saying `Team: 2/4 running`. That
|
|
8
|
+
* is the "simple look" the feature was opened for. A text-only region can never fix
|
|
9
|
+
* it, which is why the region's content type changed from `readonly string[]` to a
|
|
10
|
+
* fixed-height box that can hold components.
|
|
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
|
+
*
|
|
21
|
+
* IT RENDERS `EntryView` AND NOTHING ELSE. The mapping from an entry to its component
|
|
22
|
+
* (`assistant` / `tool` / `team` / `todo` / service cards, notice frames, the `null`
|
|
23
|
+
* cases) lives in `Transcript.tsx` exactly once, together with `EntryFrame`'s gutter,
|
|
24
|
+
* the separation rows, the per-entry `nowSecFor` clock and the memo comparator. A
|
|
25
|
+
* second enumeration here would be a copy that drifts: the band's cards would lose the
|
|
26
|
+
* left rail glyph and the transcript's would keep it, and the two halves of one screen
|
|
27
|
+
* would disagree about the same entry.
|
|
28
|
+
*
|
|
29
|
+
* `justifyContent="flex-end"` PLUS `overflow="hidden"` IS CORRECTNESS, NOT STYLE: when
|
|
30
|
+
* the content is taller than the budget the rows that get clipped are the OLDEST ones,
|
|
31
|
+
* so the newest line always sits against the composer - the same promise
|
|
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.)
|
|
35
|
+
*
|
|
36
|
+
* BOUNDED: at most `SCROLLBACK_LIMITS.liveTailMaxEntries` component instances per frame
|
|
37
|
+
* (I-10). That is what keeps the per-frame cost the same order as the full-screen live
|
|
38
|
+
* tail; growing it makes the cost grow linearly because this box has no windowing.
|
|
39
|
+
*/
|
|
40
|
+
import React from 'react';
|
|
41
|
+
import type { Entry } from '../../agent/reducer.js';
|
|
42
|
+
import type { TermCapabilities } from '../capabilities.js';
|
|
43
|
+
import type { DensityMode } from '../density.js';
|
|
44
|
+
import type { Theme } from '../theme.js';
|
|
45
|
+
export interface LiveTailProps {
|
|
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[];
|
|
52
|
+
/** Render only the last N entries; the box clips the older ones from the top. */
|
|
53
|
+
maxEntries: number;
|
|
54
|
+
/** Fixed height: the root box's `height`. The content never decides it (I-4). */
|
|
55
|
+
rows: number;
|
|
56
|
+
/** `budget.previewCols` - the width left after the rail took its column (I-5). */
|
|
57
|
+
cols: number;
|
|
58
|
+
expandedToolIds: Record<string, true>;
|
|
59
|
+
thinkingVisible: boolean;
|
|
60
|
+
reducedMotion: boolean;
|
|
61
|
+
density: DensityMode;
|
|
62
|
+
nowSec?: number;
|
|
63
|
+
theme: Theme;
|
|
64
|
+
caps: TermCapabilities;
|
|
65
|
+
}
|
|
66
|
+
export interface TailEntry {
|
|
67
|
+
entry: Entry;
|
|
68
|
+
/** The entry before it in the passed list: `EntryView` decides the separator from it. */
|
|
69
|
+
prev: Entry | undefined;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The last `maxEntries` printable entries, each with its predecessor.
|
|
73
|
+
*
|
|
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.
|
|
77
|
+
*/
|
|
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;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Box } from 'ink';
|
|
3
|
+
import { EntryView, nowSecFor } from '../Transcript.js';
|
|
4
|
+
import { SCROLLBACK_LIMITS } from './limits.js';
|
|
5
|
+
import { isExcluded } from './settled-boundary.js';
|
|
6
|
+
/**
|
|
7
|
+
* The last `maxEntries` printable entries, each with its predecessor.
|
|
8
|
+
*
|
|
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.
|
|
12
|
+
*/
|
|
13
|
+
export function selectTailEntries(entries, maxEntries) {
|
|
14
|
+
const printable = entries.filter((entry) => !isExcluded(entry));
|
|
15
|
+
const limit = Number.isFinite(maxEntries) ? Math.max(0, Math.floor(maxEntries)) : 0;
|
|
16
|
+
if (limit === 0)
|
|
17
|
+
return [];
|
|
18
|
+
const start = Math.max(0, printable.length - limit);
|
|
19
|
+
const selected = [];
|
|
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
|
+
});
|
|
25
|
+
}
|
|
26
|
+
return selected;
|
|
27
|
+
}
|
|
28
|
+
export function LiveTail({ entries, maxEntries, rows, cols, expandedToolIds, thinkingVisible, reducedMotion, density, nowSec, theme, caps, }) {
|
|
29
|
+
const limited = Math.min(maxEntries, SCROLLBACK_LIMITS.liveTailMaxEntries);
|
|
30
|
+
const selected = selectTailEntries(entries, limited);
|
|
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))) }));
|
|
32
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Height and column budget of the native-scroll PANEL BAND
|
|
3
|
+
* (release-gate-and-native-panels §6.2, tui-full-height-live-region D5).
|
|
4
|
+
*
|
|
5
|
+
* Native scroll mode drops the fixed frame, so it used to drop the panels with it:
|
|
6
|
+
* the TODO rail lived in `AppShell`'s middle band and the subagent roster in the
|
|
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 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.
|
|
17
|
+
*
|
|
18
|
+
* PURE: no Ink, no clock, no terminal, no config read. Every invariant below is
|
|
19
|
+
* therefore established BY CONSTRUCTION for all input combinations and can be
|
|
20
|
+
* enumerated by a property test (AC-9..AC-14, AC-34, AC-F3/F4):
|
|
21
|
+
*
|
|
22
|
+
* - `bandRows <= remaining` and therefore `liveRows <= rows - 1` (I-4). Ink writes
|
|
23
|
+
* `clearTerminal` - which contains `ESC[3J`, erasing the user's scrollback - as
|
|
24
|
+
* soon as a frame is as tall as the terminal.
|
|
25
|
+
* - `previewCols + railCols === contentCols`, always (AC-10). The rail is a column
|
|
26
|
+
* INSIDE the region, so the preview loses exactly the columns the rail takes and
|
|
27
|
+
* nothing else (I-5: printing keeps the full `contentCols`).
|
|
28
|
+
* - `teamRows > 0` leaves room for the rail plus one preview row (AC-12).
|
|
29
|
+
* - `fill` mode: `bandRows === remaining`, `railRows === bandRows` when the rail is
|
|
30
|
+
* visible, and `teamRows + previewRows === bandRows`.
|
|
31
|
+
*
|
|
32
|
+
* THERE IS EXACTLY ONE CALL SITE: `buildScrollbackBudget` (I-8). This module is
|
|
33
|
+
* exported so it can be tested, not so a second caller can allocate rows of its own
|
|
34
|
+
* - `remaining` is computed there, and a second copy of it is a second authority.
|
|
35
|
+
*/
|
|
36
|
+
export type BandReason =
|
|
37
|
+
/** Both panels are on screen. */
|
|
38
|
+
'visible'
|
|
39
|
+
/** No active plan: no rail. Same name and meaning as `TodoRailLayout`'s `'empty'`. */
|
|
40
|
+
| 'empty'
|
|
41
|
+
/** `cfg.todo.panel === false`. */
|
|
42
|
+
| 'disabled'
|
|
43
|
+
/** An overlay is open: the band yields to it. */
|
|
44
|
+
| 'overlay'
|
|
45
|
+
/** `todoRailWidth(contentCols) === 0` (terminal narrower than 76 columns). */
|
|
46
|
+
| 'narrow'
|
|
47
|
+
/** The remaining rows cannot hold `panelMinRows` for the rail. */
|
|
48
|
+
| 'short';
|
|
49
|
+
export interface LiveBandInput {
|
|
50
|
+
/**
|
|
51
|
+
* `buildFrameBudget().viewportRows`: every row the band may use (the pool). Computed
|
|
52
|
+
* once by `buildScrollbackBudget`; callers must not re-derive it.
|
|
53
|
+
*/
|
|
54
|
+
remaining: number;
|
|
55
|
+
/** `cols - 1`: width of the whole band. `previewCols` is derived from it. */
|
|
56
|
+
contentCols: number;
|
|
57
|
+
/** `todoRailWidth(contentCols)`; `layout/rail.ts` stays the width authority. */
|
|
58
|
+
railCols: number;
|
|
59
|
+
/** `state.todos.items.length` (0 = no active plan). */
|
|
60
|
+
itemCount: number;
|
|
61
|
+
/** `cfg.todo.panel`. */
|
|
62
|
+
panelEnabled: boolean;
|
|
63
|
+
overlayOpen: boolean;
|
|
64
|
+
/** `buildTeamPanelLayout().rowCount`; 0 when there is nothing to show. */
|
|
65
|
+
teamRowsNatural: number;
|
|
66
|
+
/** Hysteresis: the rail rows the previous frame already used. */
|
|
67
|
+
heldRailRows: number;
|
|
68
|
+
/** Hysteresis: the roster rows the previous frame already used. */
|
|
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;
|
|
78
|
+
}
|
|
79
|
+
export interface LiveBand {
|
|
80
|
+
reason: BandReason;
|
|
81
|
+
/** Band height: `remaining` in `fill` mode, `teamRows + previewRows` otherwise. */
|
|
82
|
+
bandRows: number;
|
|
83
|
+
teamRows: number;
|
|
84
|
+
previewRows: number;
|
|
85
|
+
/** Row budget of the rail: the whole band (the rail spans it). 0 when hidden. */
|
|
86
|
+
railRows: number;
|
|
87
|
+
/** The input `railCols` when visible, 0 when hidden (`/perf` reads `rail=0x0`). */
|
|
88
|
+
railCols: number;
|
|
89
|
+
/** `max(0, contentCols - railCols)`; always sums with `railCols` to `contentCols`. */
|
|
90
|
+
previewCols: number;
|
|
91
|
+
/** Hysteresis output, fed back on the next frame. */
|
|
92
|
+
heldRailRows: number;
|
|
93
|
+
heldTeamRows: number;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Allocate the band. THE ORDER OF THE STEPS IS THE SPECIFICATION - reordering them
|
|
97
|
+
* changes the priority between "what this run is doing" (rail + roster) and "the
|
|
98
|
+
* words arriving right now" (tail), which is a product decision, not a detail.
|
|
99
|
+
*
|
|
100
|
+
* 0. Normalise every numeric input (`NaN` / `Infinity` / negatives become 0).
|
|
101
|
+
* 1. An open overlay takes the whole pool (the band would otherwise double-count it).
|
|
102
|
+
* 2. Seat the roster first, but never at the cost of the rail plus one tail row:
|
|
103
|
+
* a dispatch is transient, the plan is the shape of the turn, both must fit.
|
|
104
|
+
* 3. Decide the rail's visibility, in the same reason order as `buildTodoRailLayout`.
|
|
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).
|
|
111
|
+
* 7. Columns: the input `contentCols` is the only width authority.
|
|
112
|
+
* 8. Hand the late-arriving values (the hysteresis) back out for the next frame.
|
|
113
|
+
*/
|
|
114
|
+
export declare function buildLiveBand(input: LiveBandInput): LiveBand;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Height and column budget of the native-scroll PANEL BAND
|
|
3
|
+
* (release-gate-and-native-panels §6.2, tui-full-height-live-region D5).
|
|
4
|
+
*
|
|
5
|
+
* Native scroll mode drops the fixed frame, so it used to drop the panels with it:
|
|
6
|
+
* the TODO rail lived in `AppShell`'s middle band and the subagent roster in the
|
|
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 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.
|
|
17
|
+
*
|
|
18
|
+
* PURE: no Ink, no clock, no terminal, no config read. Every invariant below is
|
|
19
|
+
* therefore established BY CONSTRUCTION for all input combinations and can be
|
|
20
|
+
* enumerated by a property test (AC-9..AC-14, AC-34, AC-F3/F4):
|
|
21
|
+
*
|
|
22
|
+
* - `bandRows <= remaining` and therefore `liveRows <= rows - 1` (I-4). Ink writes
|
|
23
|
+
* `clearTerminal` - which contains `ESC[3J`, erasing the user's scrollback - as
|
|
24
|
+
* soon as a frame is as tall as the terminal.
|
|
25
|
+
* - `previewCols + railCols === contentCols`, always (AC-10). The rail is a column
|
|
26
|
+
* INSIDE the region, so the preview loses exactly the columns the rail takes and
|
|
27
|
+
* nothing else (I-5: printing keeps the full `contentCols`).
|
|
28
|
+
* - `teamRows > 0` leaves room for the rail plus one preview row (AC-12).
|
|
29
|
+
* - `fill` mode: `bandRows === remaining`, `railRows === bandRows` when the rail is
|
|
30
|
+
* visible, and `teamRows + previewRows === bandRows`.
|
|
31
|
+
*
|
|
32
|
+
* THERE IS EXACTLY ONE CALL SITE: `buildScrollbackBudget` (I-8). This module is
|
|
33
|
+
* exported so it can be tested, not so a second caller can allocate rows of its own
|
|
34
|
+
* - `remaining` is computed there, and a second copy of it is a second authority.
|
|
35
|
+
*/
|
|
36
|
+
import { TODO_LIMITS } from '../../todo/limits.js';
|
|
37
|
+
import { SCROLLBACK_LIMITS } from './limits.js';
|
|
38
|
+
/** Invalid geometry becomes zero, exactly like `layout/team-panel.ts::nonNegativeInt`. */
|
|
39
|
+
function nonNegativeInt(value) {
|
|
40
|
+
return Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Allocate the band. THE ORDER OF THE STEPS IS THE SPECIFICATION - reordering them
|
|
44
|
+
* changes the priority between "what this run is doing" (rail + roster) and "the
|
|
45
|
+
* words arriving right now" (tail), which is a product decision, not a detail.
|
|
46
|
+
*
|
|
47
|
+
* 0. Normalise every numeric input (`NaN` / `Infinity` / negatives become 0).
|
|
48
|
+
* 1. An open overlay takes the whole pool (the band would otherwise double-count it).
|
|
49
|
+
* 2. Seat the roster first, but never at the cost of the rail plus one tail row:
|
|
50
|
+
* a dispatch is transient, the plan is the shape of the turn, both must fit.
|
|
51
|
+
* 3. Decide the rail's visibility, in the same reason order as `buildTodoRailLayout`.
|
|
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).
|
|
58
|
+
* 7. Columns: the input `contentCols` is the only width authority.
|
|
59
|
+
* 8. Hand the late-arriving values (the hysteresis) back out for the next frame.
|
|
60
|
+
*/
|
|
61
|
+
export function buildLiveBand(input) {
|
|
62
|
+
const remaining = nonNegativeInt(input.remaining);
|
|
63
|
+
const contentCols = nonNegativeInt(input.contentCols);
|
|
64
|
+
const railCols = nonNegativeInt(input.railCols);
|
|
65
|
+
const itemCount = nonNegativeInt(input.itemCount);
|
|
66
|
+
const teamRowsNatural = nonNegativeInt(input.teamRowsNatural);
|
|
67
|
+
const heldRailRows = nonNegativeInt(input.heldRailRows);
|
|
68
|
+
const heldTeamRows = nonNegativeInt(input.heldTeamRows);
|
|
69
|
+
const fill = input.fill === true;
|
|
70
|
+
if (input.overlayOpen) {
|
|
71
|
+
return {
|
|
72
|
+
reason: 'overlay', bandRows: remaining, teamRows: 0, previewRows: 0,
|
|
73
|
+
railRows: 0, railCols: 0, previewCols: contentCols, heldRailRows: 0, heldTeamRows: 0,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const teamCeiling = Math.min(teamRowsNatural, Math.max(0, remaining - TODO_LIMITS.panelMinRows - 1));
|
|
77
|
+
// THE ROSTER IS SEATED AT ITS NATURAL (CLAMPED) HEIGHT, NOT AT THE REMEMBERED ONE.
|
|
78
|
+
// Seeding from `heldTeamRows` deadlocks the panel: a fresh dispatch starts with no
|
|
79
|
+
// memory, so frame one would seat zero rows, remember zero, and no later frame could
|
|
80
|
+
// ever seat the roster - the panel this feature exists to restore would never appear
|
|
81
|
+
// (found while implementing; recorded as IF-1 in the spec). The memory below only
|
|
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.
|
|
91
|
+
const teamRows = teamCeiling;
|
|
92
|
+
const reason = !input.panelEnabled ? 'disabled'
|
|
93
|
+
: itemCount === 0 ? 'empty'
|
|
94
|
+
: railCols === 0 ? 'narrow'
|
|
95
|
+
: remaining - teamRows < SCROLLBACK_LIMITS.railBandMinRows ? 'short'
|
|
96
|
+
: 'visible';
|
|
97
|
+
const railVisible = reason === 'visible';
|
|
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
|
|
102
|
+
? Math.min(Math.max(heldRailRows, TODO_LIMITS.panelFullRows), Math.max(0, remaining - teamRows - 1))
|
|
103
|
+
: 0;
|
|
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);
|
|
112
|
+
const railRows = railVisible ? bandRows : 0;
|
|
113
|
+
const railColsOut = railVisible ? railCols : 0;
|
|
114
|
+
return {
|
|
115
|
+
reason,
|
|
116
|
+
bandRows,
|
|
117
|
+
teamRows,
|
|
118
|
+
previewRows,
|
|
119
|
+
railRows,
|
|
120
|
+
railCols: railColsOut,
|
|
121
|
+
previewCols: Math.max(0, contentCols - railColsOut),
|
|
122
|
+
// MONOTONE MEMORY, NOT A SHRINKING ONE (I-6). A rail that collapsed to zero rows
|
|
123
|
+
// because the terminal shrank for two frames would otherwise come back four rows
|
|
124
|
+
// shorter and the band would visibly jump on every resize wobble. The RESET points
|
|
125
|
+
// (an emptied plan, a finished dispatch, a resize) live in the one place that owns
|
|
126
|
+
// the lifecycle, `use-scrollback-view.ts`; this function only remembers.
|
|
127
|
+
heldRailRows: Math.max(heldRailRows, railRows),
|
|
128
|
+
heldTeamRows: Math.max(heldTeamRows, teamRows),
|
|
129
|
+
};
|
|
130
|
+
}
|
|
@@ -1,24 +1,54 @@
|
|
|
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
|
-
*
|
|
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.
|
|
24
|
+
*
|
|
25
|
+
* THIS IS THE ONLY CALL SITE OF `buildLiveBand` (I-8). `remaining` is computed here
|
|
26
|
+
* once and handed to the band; an `App` that computed its own copy would make
|
|
27
|
+
* `liveRows <= rows - 1` depend on two numbers happening to agree instead of on the
|
|
28
|
+
* construction that proves it.
|
|
14
29
|
*/
|
|
30
|
+
import { type LiveBand, type LiveBandInput } from './band.js';
|
|
15
31
|
import { type FrameBudgetInput } from '../layout/budget.js';
|
|
32
|
+
/**
|
|
33
|
+
* Everything the band needs that this module derives for itself: `remaining`,
|
|
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.
|
|
39
|
+
*/
|
|
40
|
+
export type ScrollbackBandInput = Omit<LiveBandInput, 'remaining' | 'contentCols' | 'railCols' | 'overlayOpen' | 'fill'>;
|
|
16
41
|
export interface ScrollbackBudgetInput extends FrameBudgetInput {
|
|
17
42
|
overlayOpen: boolean;
|
|
18
|
-
/** Preview rows the content would like; the budget grants what fits. */
|
|
19
|
-
liveTailWanted: number;
|
|
20
43
|
/** The writer tripped its tall-frame guard: degrade to the placeholder until a resize. */
|
|
21
44
|
forceInactive?: boolean;
|
|
45
|
+
/**
|
|
46
|
+
* REQUIRED, not optional (the same rule `TeamPanel`'s header records as D-13). An
|
|
47
|
+
* optional band would mean "forgot to pass it" renders silently without panels -
|
|
48
|
+
* exactly the defect this feature exists to remove - while a required field is a
|
|
49
|
+
* compile error at the one place that forgets.
|
|
50
|
+
*/
|
|
51
|
+
band: ScrollbackBandInput;
|
|
22
52
|
}
|
|
23
53
|
export interface ScrollbackBudget {
|
|
24
54
|
rows: number;
|
|
@@ -27,6 +57,12 @@ export interface ScrollbackBudget {
|
|
|
27
57
|
contentCols: number;
|
|
28
58
|
/** Terminal below the 40x12 floor: the region is a one-row placeholder. */
|
|
29
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;
|
|
30
66
|
statusRows: 0 | 1 | 2;
|
|
31
67
|
composerRows: number;
|
|
32
68
|
composerSlotRows: number;
|
|
@@ -34,10 +70,15 @@ export interface ScrollbackBudget {
|
|
|
34
70
|
popupRows: number;
|
|
35
71
|
popupMaxHeight: number;
|
|
36
72
|
overlayRows: number;
|
|
73
|
+
/** Tail rows: the band's left column below the roster (`= band.previewRows`). */
|
|
37
74
|
liveTailRows: number;
|
|
38
|
-
/** Rows above the composer:
|
|
75
|
+
/** Rows above the composer: `= band.bandRows = remaining`. */
|
|
39
76
|
topRows: number;
|
|
40
|
-
/** Total live-region height
|
|
77
|
+
/** Total live-region height: `rows - 1`, the whole screen above the parked row. */
|
|
41
78
|
liveRows: number;
|
|
79
|
+
/** Height and column allocation of the panel band; the single authority is `band.ts`. */
|
|
80
|
+
band: LiveBand;
|
|
81
|
+
/** Preview/tail width: `contentCols - railCols`. Printing keeps `contentCols`. */
|
|
82
|
+
previewCols: number;
|
|
42
83
|
}
|
|
43
84
|
export declare function buildScrollbackBudget(input: ScrollbackBudgetInput): ScrollbackBudget;
|
|
@@ -1,44 +1,93 @@
|
|
|
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
|
-
*
|
|
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.
|
|
24
|
+
*
|
|
25
|
+
* THIS IS THE ONLY CALL SITE OF `buildLiveBand` (I-8). `remaining` is computed here
|
|
26
|
+
* once and handed to the band; an `App` that computed its own copy would make
|
|
27
|
+
* `liveRows <= rows - 1` depend on two numbers happening to agree instead of on the
|
|
28
|
+
* construction that proves it.
|
|
14
29
|
*/
|
|
30
|
+
import { buildLiveBand } from './band.js';
|
|
15
31
|
import { buildFrameBudget } from '../layout/budget.js';
|
|
16
|
-
import {
|
|
32
|
+
import { todoRailWidth } from '../layout/rail.js';
|
|
33
|
+
/**
|
|
34
|
+
* The band of a frame that shows no content at all (below the 40x12 floor, or after
|
|
35
|
+
* the writer's tall-frame guard tripped). It is a REAL value rather than `undefined`
|
|
36
|
+
* because `/perf` and the render tests must be able to read the reason on exactly the
|
|
37
|
+
* frames where something went wrong (I-14 / AC-23).
|
|
38
|
+
*/
|
|
39
|
+
function degradedBand(contentCols) {
|
|
40
|
+
return {
|
|
41
|
+
reason: 'short', bandRows: 0, teamRows: 0, previewRows: 0,
|
|
42
|
+
railRows: 0, railCols: 0, previewCols: contentCols, heldRailRows: 0, heldTeamRows: 0,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
17
45
|
export function buildScrollbackBudget(input) {
|
|
18
46
|
const base = buildFrameBudget(input);
|
|
19
47
|
const contentCols = Math.max(1, base.cols - 1);
|
|
20
48
|
if (base.inactive || input.forceInactive === true) {
|
|
21
49
|
return {
|
|
22
|
-
rows: base.rows, cols: base.cols, contentCols, inactive: true,
|
|
23
|
-
composerRows: 0, composerSlotRows: 0, composerCols: base.composerCols,
|
|
24
|
-
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,
|
|
25
53
|
liveRows: Math.max(0, Math.min(1, base.rows - 1)),
|
|
54
|
+
band: degradedBand(contentCols), previewCols: contentCols,
|
|
26
55
|
};
|
|
27
56
|
}
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
const
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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);
|
|
69
|
+
const band = buildLiveBand({
|
|
70
|
+
...input.band,
|
|
71
|
+
remaining,
|
|
72
|
+
contentCols,
|
|
73
|
+
railCols: todoRailWidth(contentCols),
|
|
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,
|
|
77
|
+
// Taken from THIS input, never from `input.band`: an overlay that the frame knows
|
|
78
|
+
// about but the band did not would draw no overlay at all, and the shell keys the
|
|
79
|
+
// overlay's rows off the band.
|
|
80
|
+
overlayOpen: input.overlayOpen,
|
|
81
|
+
});
|
|
82
|
+
const overlayRows = band.reason === 'overlay' ? band.bandRows : 0;
|
|
83
|
+
const liveTailRows = band.previewRows;
|
|
84
|
+
const topRows = band.bandRows;
|
|
37
85
|
return {
|
|
38
|
-
rows: base.rows, cols: base.cols, contentCols, inactive: false,
|
|
86
|
+
rows: base.rows, cols: base.cols, contentCols, inactive: false, headerRows,
|
|
39
87
|
statusRows: base.statusRows, composerRows: base.composerRows,
|
|
40
88
|
composerSlotRows: base.composerSlotRows, composerCols: base.composerCols,
|
|
41
89
|
popupRows: base.popupRows, popupMaxHeight: base.popupMaxHeight, overlayRows,
|
|
42
90
|
liveTailRows, topRows, liveRows,
|
|
91
|
+
band, previewCols: band.previewCols,
|
|
43
92
|
};
|
|
44
93
|
}
|