@aragon-agent/cli 0.6.29 → 0.6.32
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +2223 -2103
- package/README.md +2957 -2759
- package/dist/agent/controller.d.ts +46 -0
- package/dist/agent/controller.js +175 -7
- package/dist/agent/job-notices.d.ts +143 -0
- package/dist/agent/job-notices.js +182 -0
- package/dist/agent/project-guidance-prompt.js +20 -20
- package/dist/agent/reducer.d.ts +21 -1
- package/dist/agent/reducer.js +7 -0
- package/dist/agent/system-prompt.d.ts +12 -0
- package/dist/agent/system-prompt.js +2 -0
- package/dist/cli.js +399 -62
- package/dist/commands/builtins.js +27 -8
- package/dist/commands/perf.d.ts +79 -2
- package/dist/commands/perf.js +51 -3
- package/dist/commands/registry.d.ts +6 -0
- package/dist/config/cli-commands.d.ts +27 -0
- package/dist/config/cli-commands.js +54 -1
- package/dist/config/env.js +42 -0
- package/dist/config/load.d.ts +3 -1
- package/dist/config/load.js +61 -1
- package/dist/config/schema.d.ts +100 -0
- package/dist/config/schema.js +43 -0
- package/dist/config/store.js +14 -1
- package/dist/config/ui-state.d.ts +9 -0
- package/dist/config/ui-state.js +9 -0
- package/dist/exec/events.d.ts +29 -3
- package/dist/exec/events.js +3 -12
- package/dist/exec/runner.d.ts +24 -0
- package/dist/exec/runner.js +79 -0
- package/dist/input/cursor-report.d.ts +61 -0
- package/dist/input/cursor-report.js +97 -0
- package/dist/input/stdin-filter.d.ts +22 -0
- package/dist/input/stdin-filter.js +48 -3
- package/dist/proc/limits.d.ts +76 -2
- package/dist/proc/limits.js +83 -2
- package/dist/proc/log-mirror.d.ts +59 -0
- package/dist/proc/log-mirror.js +203 -0
- package/dist/proc/log-ring.d.ts +22 -0
- package/dist/proc/log-ring.js +20 -1
- package/dist/proc/notices.d.ts +57 -0
- package/dist/proc/notices.js +101 -0
- package/dist/proc/progress.d.ts +40 -0
- package/dist/proc/progress.js +135 -0
- package/dist/proc/prompt.d.ts +24 -2
- package/dist/proc/prompt.js +51 -2
- package/dist/proc/supervisor.d.ts +35 -2
- package/dist/proc/supervisor.js +142 -43
- package/dist/proc/types.d.ts +89 -1
- package/dist/proc/wait.d.ts +57 -0
- package/dist/proc/wait.js +133 -0
- package/dist/session/persist.js +3 -1
- package/dist/session/validate-session.js +17 -0
- package/dist/tools/bash-tool.d.ts +12 -1
- package/dist/tools/bash-tool.js +100 -13
- package/dist/tools/index.d.ts +26 -2
- package/dist/tools/index.js +43 -2
- package/dist/tools/job-tools.d.ts +54 -0
- package/dist/tools/job-tools.js +329 -0
- package/dist/tools/proc-tools.d.ts +1 -1
- package/dist/tools/proc-tools.js +49 -17
- package/dist/ui/ActivityLine.js +7 -0
- package/dist/ui/App.d.ts +11 -1
- package/dist/ui/App.js +181 -63
- package/dist/ui/ScrollbackShell.d.ts +54 -0
- package/dist/ui/ScrollbackShell.js +15 -0
- package/dist/ui/StatusBar.d.ts +5 -0
- package/dist/ui/StatusBar.js +1 -0
- package/dist/ui/Transcript.d.ts +14 -0
- package/dist/ui/Transcript.js +14 -2
- package/dist/ui/entries/ServiceCard.d.ts +17 -1
- package/dist/ui/entries/ServiceCard.js +39 -4
- package/dist/ui/frame-differ.d.ts +12 -0
- package/dist/ui/layout/status-layout.d.ts +13 -1
- package/dist/ui/layout/status-layout.js +10 -0
- package/dist/ui/layout/virtual-window.js +14 -1
- package/dist/ui/overlays/HelpOverlay.d.ts +1 -1
- package/dist/ui/overlays/HelpOverlay.js +21 -3
- package/dist/ui/run-status-row.d.ts +12 -0
- package/dist/ui/screen.d.ts +29 -0
- package/dist/ui/screen.js +40 -0
- package/dist/ui/scrollback/LiveTail.d.ts +64 -0
- package/dist/ui/scrollback/LiveTail.js +27 -0
- package/dist/ui/scrollback/band.d.ts +105 -0
- package/dist/ui/scrollback/band.js +109 -0
- package/dist/ui/scrollback/budget.d.ts +72 -0
- package/dist/ui/scrollback/budget.js +81 -0
- package/dist/ui/scrollback/canvas.d.ts +99 -0
- package/dist/ui/scrollback/canvas.js +113 -0
- package/dist/ui/scrollback/cursor-probe.d.ts +71 -0
- package/dist/ui/scrollback/cursor-probe.js +133 -0
- package/dist/ui/scrollback/drain.d.ts +32 -0
- package/dist/ui/scrollback/drain.js +64 -0
- package/dist/ui/scrollback/exit-summary.d.ts +23 -0
- package/dist/ui/scrollback/exit-summary.js +25 -0
- package/dist/ui/scrollback/limits.d.ts +74 -0
- package/dist/ui/scrollback/limits.js +74 -0
- package/dist/ui/scrollback/live-preview.d.ts +26 -0
- package/dist/ui/scrollback/live-preview.js +106 -0
- package/dist/ui/scrollback/renderer.d.ts +38 -0
- package/dist/ui/scrollback/renderer.js +158 -0
- package/dist/ui/scrollback/scrollback-writer.d.ts +162 -0
- package/dist/ui/scrollback/scrollback-writer.js +549 -0
- package/dist/ui/scrollback/settled-boundary.d.ts +42 -0
- package/dist/ui/scrollback/settled-boundary.js +106 -0
- package/dist/ui/scrollback/use-scrollback-view.d.ts +103 -0
- package/dist/ui/scrollback/use-scrollback-view.js +294 -0
- package/dist/ui/scrollback/writer-shapes.d.ts +127 -0
- package/dist/ui/scrollback/writer-shapes.js +159 -0
- package/dist/ui/stdout-frame-writer.js +12 -2
- package/dist/ui/terminal-plan.d.ts +52 -0
- package/dist/ui/terminal-plan.js +49 -0
- package/dist/ui/transcript-text.d.ts +11 -0
- package/dist/ui/transcript-text.js +21 -7
- package/dist/ui/use-startup-notices.d.ts +12 -1
- package/dist/ui/use-startup-notices.js +28 -2
- package/package.json +3 -3
- package/skills/project-indexer/SKILL.md +163 -163
- package/skills/project-indexer/templates/cleancode-overrides.md +46 -46
- package/skills/project-indexer/templates/cleancode-template.md +94 -94
- package/skills/project-indexer/templates/config-template.md +69 -69
- package/skills/project-indexer/templates/index-template.md +129 -129
- package/skills/skill-creator/SKILL.md +96 -96
|
@@ -81,8 +81,19 @@ function statusClause(props, dot) {
|
|
|
81
81
|
const took = readyAt === undefined ? '' : `${dot}${formatDuration(readyAt - startedAt)}`;
|
|
82
82
|
return `ready${took}`;
|
|
83
83
|
}
|
|
84
|
-
|
|
85
|
-
|
|
84
|
+
// A LIVE JOB SHOWS ITS ELAPSED TIME (D-8), because a task's whole story is
|
|
85
|
+
// "how long has this been going". The number comes from the transcript's 1 Hz
|
|
86
|
+
// `nowSec`; without it the card falls back to the spawn time and reads `0ms`,
|
|
87
|
+
// which is honest about never having been ticked rather than wrong about it.
|
|
88
|
+
if (status === 'running') {
|
|
89
|
+
// NO CLOCK, NO FIGURE. Falling back to `startedAt` would print `0ms` on every
|
|
90
|
+
// settled frame, which reads as "it just started" - a wrong number rather than
|
|
91
|
+
// an absent one.
|
|
92
|
+
if (props.nowSec === undefined)
|
|
93
|
+
return 'running';
|
|
94
|
+
const elapsed = Math.max(0, props.nowSec * 1000 - startedAt);
|
|
95
|
+
return `running${dot}${formatDuration(elapsed)}`;
|
|
96
|
+
}
|
|
86
97
|
if (status === 'starting')
|
|
87
98
|
return 'starting';
|
|
88
99
|
const lived = endedAt === undefined ? '' : `${dot}${formatDuration(endedAt - startedAt)}`;
|
|
@@ -92,20 +103,44 @@ function statusClause(props, dot) {
|
|
|
92
103
|
return `failed${lived}`;
|
|
93
104
|
return `exited${exitCode === null ? '' : ` code ${exitCode}`}${lived}`;
|
|
94
105
|
}
|
|
106
|
+
/**
|
|
107
|
+
* The progress row, or `undefined` when there is nothing to draw.
|
|
108
|
+
*
|
|
109
|
+
* STALLED OUTRANKS PROGRESS: "no output for 6m" is the more urgent of the two
|
|
110
|
+
* facts, and drawing both would cost a second row the height estimator does not
|
|
111
|
+
* know about. The `no output` figure needs `nowSec`; without it the badge is
|
|
112
|
+
* omitted rather than printed with a wrong number.
|
|
113
|
+
*/
|
|
114
|
+
function progressRow(props) {
|
|
115
|
+
if (props.stalled === true) {
|
|
116
|
+
if (props.nowSec === undefined || props.lastOutputAt === undefined) {
|
|
117
|
+
return { text: 'no output', stalled: true };
|
|
118
|
+
}
|
|
119
|
+
const quiet = Math.max(0, props.nowSec * 1000 - props.lastOutputAt);
|
|
120
|
+
return { text: `no output ${formatDuration(quiet)}`, stalled: true };
|
|
121
|
+
}
|
|
122
|
+
if (props.progress)
|
|
123
|
+
return { text: props.progress, stalled: false };
|
|
124
|
+
return undefined;
|
|
125
|
+
}
|
|
95
126
|
function ServiceCardImpl(props) {
|
|
96
127
|
const { serviceId, command, url, rows, expanded, terminal, theme, caps } = props;
|
|
97
128
|
const glyphs = pickGlyphs(caps);
|
|
98
129
|
const color = serviceCardColor(props, theme);
|
|
99
130
|
const dot = ` ${glyphs.midDot} `;
|
|
100
131
|
const clause = statusClause(props, dot);
|
|
132
|
+
// THE NOUN, NOT THE KIND: `task t1` and `service s1` are the same component
|
|
133
|
+
// drawing two kinds of job, and every id keeps its existing prefix (I-2).
|
|
134
|
+
const noun = props.jobKind === 'task' ? 'task' : 'service';
|
|
101
135
|
// THE TERMINAL RECORD IS ONE ROW, ALWAYS. It is a SECOND event at a SECOND
|
|
102
136
|
// time — the only thing `<Static>` permits once the live card is printed — so
|
|
103
137
|
// it names what happened and nothing else (D-11).
|
|
104
138
|
if (terminal) {
|
|
105
|
-
return (_jsx(Box, { flexDirection: "row", children: _jsxs(Text, { color: color, wrap: "truncate", children: ["
|
|
139
|
+
return (_jsx(Box, { flexDirection: "row", children: _jsxs(Text, { color: color, wrap: "truncate", children: [noun, " ", serviceId, " ", clause, props.killIncomplete ? ' (may have left a detached child)' : ''] }) }));
|
|
106
140
|
}
|
|
107
141
|
const shown = rows.slice(-(expanded ? EXPANDED_TAIL_ROWS : COLLAPSED_TAIL_ROWS));
|
|
108
|
-
|
|
142
|
+
const progress = progressRow(props);
|
|
143
|
+
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", children: [_jsxs(Text, { color: theme.accent, bold: true, children: [noun, " ", serviceId] }), _jsxs(Text, { color: theme.muted, wrap: "truncate", children: [' ', command] }), _jsxs(Text, { color: color, wrap: "truncate", children: [dot, clause] })] }), progress !== undefined && (_jsx(Box, { flexDirection: "row", flexShrink: 0, paddingLeft: 1, children: _jsx(Text, { color: progress.stalled ? theme.noticeWarn : theme.muted, wrap: "truncate", children: progress.stalled ? progress.text : `[progress] ${progress.text}` }) })), url !== undefined && (_jsxs(Box, { flexDirection: "row", flexShrink: 0, paddingLeft: 1, children: [_jsx(Text, { color: theme.primary, children: url }), _jsxs(Text, { color: theme.muted, children: [dot, "ctrl+c stop", dot, "ctrl+o log"] })] })), shown.length > 0 && (_jsx(Box, { flexDirection: "column", flexShrink: 0, paddingLeft: 1, ...railBorderProps(glyphs.railVertical, theme.border), children: shown.map((line, index) => (_jsx(Text, { color: theme.muted, wrap: "truncate", children: line }, index))) }))] }));
|
|
109
144
|
}
|
|
110
145
|
/**
|
|
111
146
|
* `React.memo` with the DEFAULT comparator (tui-render-performance L2 / R3).
|
|
@@ -150,5 +150,17 @@ export interface FrameDiffer {
|
|
|
150
150
|
* appears one frame later instead of never.
|
|
151
151
|
*/
|
|
152
152
|
repaint(): string;
|
|
153
|
+
/**
|
|
154
|
+
* The stream was resized. Optional (tui-native-terminal-scroll §5.1): the
|
|
155
|
+
* full-screen differ does not implement it and is invalidated instead, which is
|
|
156
|
+
* its existing behaviour; the scrollback writer keeps its cache for a reflow
|
|
157
|
+
* estimate.
|
|
158
|
+
*/
|
|
159
|
+
onResize?(): void;
|
|
160
|
+
/**
|
|
161
|
+
* An own write that moves neither a cell nor the cursor (OSC 52 copy). Optional
|
|
162
|
+
* for the same reason: absent, the write invalidates the cache as before.
|
|
163
|
+
*/
|
|
164
|
+
cursorNeutralWrite?(): void;
|
|
153
165
|
}
|
|
154
166
|
export declare function createFrameDiffer(options: FrameDifferOptions): FrameDiffer;
|
|
@@ -15,6 +15,18 @@ export interface StatusLayoutInput {
|
|
|
15
15
|
name: string;
|
|
16
16
|
toolCallId: string;
|
|
17
17
|
} | null;
|
|
18
|
+
/**
|
|
19
|
+
* A bounded wait in flight (long-task-jobs §4.9).
|
|
20
|
+
*
|
|
21
|
+
* PRESENT ONLY WHILE `job_wait` IS THE ACTIVE TOOL, and it REPLACES the
|
|
22
|
+
* `Running job_wait` label rather than joining it: "Waiting on t3 (up to 30s)"
|
|
23
|
+
* is the state the user asked about (what, and how long at most), while the
|
|
24
|
+
* tool name is an implementation detail of how the runtime is waiting.
|
|
25
|
+
*/
|
|
26
|
+
wait?: {
|
|
27
|
+
jobId: string;
|
|
28
|
+
upToSeconds: number;
|
|
29
|
+
};
|
|
18
30
|
runOutcome?: 'none' | 'ended' | 'interrupted' | 'failed';
|
|
19
31
|
spinner?: string;
|
|
20
32
|
waitingForConfirmation?: boolean;
|
|
@@ -77,7 +89,7 @@ export interface StatusLinePlan {
|
|
|
77
89
|
export declare function cleanStatusText(text: string): string;
|
|
78
90
|
export declare function statusField(id: string, text: string, tone?: StatusField['tone']): StatusField;
|
|
79
91
|
export declare function statusLinePlan(fields: readonly StatusField[], separator?: string): StatusLinePlan;
|
|
80
|
-
export declare function phaseLabels(input: Pick<StatusLayoutInput, 'phase' | 'activeTool' | 'runOutcome' | 'waitingForConfirmation' | 'stopping' | 'compacting' | 'retryActive'>): readonly [string, string];
|
|
92
|
+
export declare function phaseLabels(input: Pick<StatusLayoutInput, 'phase' | 'activeTool' | 'runOutcome' | 'waitingForConfirmation' | 'stopping' | 'compacting' | 'retryActive' | 'wait'>): readonly [string, string];
|
|
81
93
|
export declare function validContext(context: ContextUsageSnapshot): boolean;
|
|
82
94
|
export declare function shortDuration(ms: number): string;
|
|
83
95
|
/** Every field survives each width rung; only the redraw carrier is outside this plan. */
|
|
@@ -25,6 +25,16 @@ export function phaseLabels(input) {
|
|
|
25
25
|
return copy.compacting;
|
|
26
26
|
if (input.retryActive)
|
|
27
27
|
return copy.retrying;
|
|
28
|
+
// A WAIT OUTRANKS THE TOOL NAME (long-task-jobs AC-28): `Running job_wait`
|
|
29
|
+
// answers "which tool", `Waiting on t3 (up to 30s)` answers "what am I waiting
|
|
30
|
+
// for and how long may it take", which is the question a user staring at a
|
|
31
|
+
// quiet terminal actually has. Both forms carry it - the short rung is
|
|
32
|
+
// truncated by width, not reworded.
|
|
33
|
+
if (input.wait) {
|
|
34
|
+
const label = `Waiting on ${cleanStatusText(input.wait.jobId)} ` +
|
|
35
|
+
`(up to ${Math.max(0, Math.round(input.wait.upToSeconds))}s)`;
|
|
36
|
+
return [label, label];
|
|
37
|
+
}
|
|
28
38
|
if (input.activeTool)
|
|
29
39
|
return [copy.tool[0], copy.tool[1] + ' ' + cleanStatusText(input.activeTool.name)];
|
|
30
40
|
if (input.phase === 'idle' && input.runOutcome && input.runOutcome !== 'none')
|
|
@@ -218,6 +218,13 @@ const TOOL_LIVE_TAIL_ROWS = 8;
|
|
|
218
218
|
const SERVICE_COLLAPSED_TAIL_ROWS = 2;
|
|
219
219
|
/** Tail rows an EXPANDED `ServiceCard` draws; the snapshot cap (8). */
|
|
220
220
|
const SERVICE_EXPANDED_TAIL_ROWS = 8;
|
|
221
|
+
/**
|
|
222
|
+
* The progress row the live `ServiceCard` draws when `progress` or `stalled` is
|
|
223
|
+
* set (long-task-jobs §4.9), duplicated as the number `1` for the same reason as
|
|
224
|
+
* the two constants above: `virtual-window.ts` counts lines, `ServiceCard.tsx`
|
|
225
|
+
* draws them, and the pairing is kept by a test (AC-27) rather than by an import.
|
|
226
|
+
*/
|
|
227
|
+
const SERVICE_PROGRESS_ROWS = 1;
|
|
221
228
|
/**
|
|
222
229
|
* Pure UPPER-BOUND estimate of an entry's height, used until a real measurement
|
|
223
230
|
* exists.
|
|
@@ -358,7 +365,13 @@ export function estimateEntryRows(entry, cols, density, expanded, thinkingVisibl
|
|
|
358
365
|
const tail = expanded
|
|
359
366
|
? Math.min(entry.rows.length, SERVICE_EXPANDED_TAIL_ROWS)
|
|
360
367
|
: Math.min(entry.rows.length, SERVICE_COLLAPSED_TAIL_ROWS);
|
|
361
|
-
|
|
368
|
+
// THE PROGRESS ROW IS ONE MORE LINE (long-task-jobs AC-27), and it is only
|
|
369
|
+
// ever one because `ServiceCard` draws it with `wrap="truncate"` and the
|
|
370
|
+
// supervisor clips it to `PROC_LIMITS.waitProgressChars`. A `stalled` badge
|
|
371
|
+
// REPLACES it rather than adding to it - the two can never both be drawn,
|
|
372
|
+
// so this stays a single term.
|
|
373
|
+
const progress = entry.progress || entry.stalled ? SERVICE_PROGRESS_ROWS : 0;
|
|
374
|
+
return separation + 1 + progress + url + tail;
|
|
362
375
|
}
|
|
363
376
|
default:
|
|
364
377
|
return separation + 1;
|
|
@@ -14,4 +14,4 @@ import React from 'react';
|
|
|
14
14
|
import type { Theme } from '../theme.js';
|
|
15
15
|
import type { TermCapabilities } from '../capabilities.js';
|
|
16
16
|
/** Build the overlay's rows. Exported so `app.test.tsx` can count them. */
|
|
17
|
-
export declare function helpRows(theme: Theme, caps: TermCapabilities): React.ReactElement[];
|
|
17
|
+
export declare function helpRows(theme: Theme, caps: TermCapabilities, native?: boolean): React.ReactElement[];
|
|
@@ -2,7 +2,25 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { Text } from 'ink';
|
|
3
3
|
import { pickGlyphs } from '../glyphs.js';
|
|
4
4
|
const KEY_PAD = 24;
|
|
5
|
-
|
|
5
|
+
/**
|
|
6
|
+
* Rows that only make sense when the APPLICATION scrolls and selects. In native
|
|
7
|
+
* scroll mode (tui-native-terminal-scroll §4.7) the terminal does both, so they are
|
|
8
|
+
* dropped and one honest row replaces them.
|
|
9
|
+
*/
|
|
10
|
+
const APP_SCROLL_ROWS = new Set([
|
|
11
|
+
'Shift+Up / Shift+Down', 'Wheel', 'Shift+Wheel', 'Drag right edge', '^N',
|
|
12
|
+
'Drag text (left button)', 'Ctrl+C (with a selection)',
|
|
13
|
+
]);
|
|
14
|
+
function keyRows(times, native = false) {
|
|
15
|
+
const all = allKeyRows(times);
|
|
16
|
+
if (!native)
|
|
17
|
+
return all;
|
|
18
|
+
return all
|
|
19
|
+
.filter(([key]) => !APP_SCROLL_ROWS.has(key))
|
|
20
|
+
.map(([key, description]) => key === 'PgUp / PgDn' ? [key, 'Page through an open overlay'] : [key, description])
|
|
21
|
+
.concat([['Scroll', 'Use your terminal (wheel, Shift+PgUp, scrollbar, search)']]);
|
|
22
|
+
}
|
|
23
|
+
function allKeyRows(times) {
|
|
6
24
|
return [
|
|
7
25
|
['Enter', 'Submit (idle) / queue steering (running); re-pins to the newest output'],
|
|
8
26
|
['Alt+Enter / Shift+Enter', 'Newline when supported; the host may intercept either binding'],
|
|
@@ -133,13 +151,13 @@ const FAST_ROWS = [
|
|
|
133
151
|
['<fast_review>', 'An automated second opinion from the fast model - advice, not the user'],
|
|
134
152
|
];
|
|
135
153
|
/** Build the overlay's rows. Exported so `app.test.tsx` can count them. */
|
|
136
|
-
export function helpRows(theme, caps) {
|
|
154
|
+
export function helpRows(theme, caps, native = false) {
|
|
137
155
|
const glyphs = pickGlyphs(caps);
|
|
138
156
|
const rows = [];
|
|
139
157
|
const section = (title) => (_jsx(Text, { wrap: "truncate", color: theme.accent, bold: true, children: title }, `s-${title}`));
|
|
140
158
|
const pair = (k, d) => (_jsxs(Text, { wrap: "truncate", children: [_jsx(Text, { color: theme.primary, children: k.padEnd(KEY_PAD) }), _jsx(Text, { color: theme.muted, children: d })] }, `r-${k}`));
|
|
141
159
|
rows.push(section('Keybindings'));
|
|
142
|
-
for (const [k, d] of keyRows(glyphs.times))
|
|
160
|
+
for (const [k, d] of keyRows(glyphs.times, native))
|
|
143
161
|
rows.push(pair(k, d));
|
|
144
162
|
rows.push(section('Slash commands'));
|
|
145
163
|
for (const [c, d] of COMMANDS)
|
|
@@ -17,6 +17,18 @@ export interface RunActivity {
|
|
|
17
17
|
runningTool?: string;
|
|
18
18
|
/** A context compaction is in flight; outranks the tool name and the phrase. */
|
|
19
19
|
compacting?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* A BOUNDED WAIT IS IN FLIGHT (long-task-jobs §4.9).
|
|
22
|
+
*
|
|
23
|
+
* `Running job_wait` is a TOOL NAME; `Waiting on t3 (up to 30s)` is a STATE, and
|
|
24
|
+
* the state is what answers the user's two questions - what is it waiting for,
|
|
25
|
+
* and how long may that take. No countdown: a ticking number would need its own
|
|
26
|
+
* re-render, and the sentence is already complete.
|
|
27
|
+
*/
|
|
28
|
+
wait?: {
|
|
29
|
+
jobId: string;
|
|
30
|
+
upToSeconds: number;
|
|
31
|
+
};
|
|
20
32
|
}
|
|
21
33
|
export interface RunRowPlanInput {
|
|
22
34
|
/** Width of the row's container (the composer's content columns). */
|
package/dist/ui/screen.d.ts
CHANGED
|
@@ -71,6 +71,35 @@ export interface AltScreenOptions {
|
|
|
71
71
|
* CI) — writing screen-control sequences into a redirected file corrupts it.
|
|
72
72
|
*/
|
|
73
73
|
export declare function enterAltScreen(stdout: NodeJS.WriteStream | undefined, options?: AltScreenOptions): ScreenHandle;
|
|
74
|
+
export interface NativeScreenOptions {
|
|
75
|
+
/** Same discipline as `AltScreenOptions.bracketedPaste`: a filter that consumes it exists. */
|
|
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;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Native-scroll mode's counterpart of `enterAltScreen` (tui-native-terminal-scroll
|
|
89
|
+
* §4.8 / D6): it deliberately does almost nothing. The main buffer stays in use so
|
|
90
|
+
* the conversation lands in the terminal's own scrollback; there is no alternate
|
|
91
|
+
* screen, no mouse reporting and no DEC 1007 save/restore.
|
|
92
|
+
*
|
|
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.
|
|
101
|
+
*/
|
|
102
|
+
export declare function enterNativeScreen(stdout: NodeJS.WriteStream | undefined, options?: NativeScreenOptions): ScreenHandle;
|
|
74
103
|
/**
|
|
75
104
|
* Replay the session summary into the normal buffer after `restore()`, so the
|
|
76
105
|
* conversation does not evaporate along with the alternate screen.
|
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';
|
|
@@ -181,6 +182,45 @@ export function enterAltScreen(stdout, options = { mouse: false }) {
|
|
|
181
182
|
setMouseCapture,
|
|
182
183
|
};
|
|
183
184
|
}
|
|
185
|
+
/**
|
|
186
|
+
* Native-scroll mode's counterpart of `enterAltScreen` (tui-native-terminal-scroll
|
|
187
|
+
* §4.8 / D6): it deliberately does almost nothing. The main buffer stays in use so
|
|
188
|
+
* the conversation lands in the terminal's own scrollback; there is no alternate
|
|
189
|
+
* screen, no mouse reporting and no DEC 1007 save/restore.
|
|
190
|
+
*
|
|
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.
|
|
199
|
+
*/
|
|
200
|
+
export function enterNativeScreen(stdout, options = {}) {
|
|
201
|
+
if (!stdout || !stdout.isTTY || typeof stdout.write !== 'function')
|
|
202
|
+
return NOOP_HANDLE;
|
|
203
|
+
const bracketedPaste = options.bracketedPaste === true;
|
|
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);
|
|
209
|
+
let restored = false;
|
|
210
|
+
return {
|
|
211
|
+
restore() {
|
|
212
|
+
if (restored)
|
|
213
|
+
return;
|
|
214
|
+
restored = true;
|
|
215
|
+
// 2004 is FORCED off rather than saved/restored, for the reason recorded at
|
|
216
|
+
// `DISABLE_BRACKETED_PASTE`. The cursor is shown again because Ink's
|
|
217
|
+
// log-update hides it and a signal kill never gives it back.
|
|
218
|
+
safeWrite(stdout, (bracketedPaste ? DISABLE_BRACKETED_PASTE : '') + SHOW_CURSOR);
|
|
219
|
+
},
|
|
220
|
+
// No mouse reporting in this mode: nothing to hand back or take again.
|
|
221
|
+
setMouseCapture() { },
|
|
222
|
+
};
|
|
223
|
+
}
|
|
184
224
|
/**
|
|
185
225
|
* Replay the session summary into the normal buffer after `restore()`, so the
|
|
186
226
|
* conversation does not evaporate along with the alternate screen.
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* LiveTail - the band's UNFINISHED tail, rendered with the real entry components
|
|
3
|
+
* (release-gate-and-native-panels §6.5).
|
|
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 RENDERS `EntryView` AND NOTHING ELSE. The mapping from an entry to its component
|
|
13
|
+
* (`assistant` / `tool` / `team` / `todo` / service cards, notice frames, the `null`
|
|
14
|
+
* cases) lives in `Transcript.tsx` exactly once, together with `EntryFrame`'s gutter,
|
|
15
|
+
* the separation rows, the per-entry `nowSecFor` clock and the memo comparator. A
|
|
16
|
+
* second enumeration here would be a copy that drifts: the band's cards would lose the
|
|
17
|
+
* left rail glyph and the transcript's would keep it, and the two halves of one screen
|
|
18
|
+
* would disagree about the same entry.
|
|
19
|
+
*
|
|
20
|
+
* `justifyContent="flex-end"` PLUS `overflow="hidden"` IS CORRECTNESS, NOT STYLE: when
|
|
21
|
+
* the content is taller than the budget the rows that get clipped are the OLDEST ones,
|
|
22
|
+
* so the newest line always sits against the composer - the same promise
|
|
23
|
+
* `scrollback/canvas.ts` makes about the live tail.
|
|
24
|
+
*
|
|
25
|
+
* BOUNDED: at most `SCROLLBACK_LIMITS.liveTailMaxEntries` component instances per frame
|
|
26
|
+
* (I-10). That is what keeps the per-frame cost the same order as the full-screen live
|
|
27
|
+
* tail; growing it makes the cost grow linearly because this box has no windowing.
|
|
28
|
+
*/
|
|
29
|
+
import React from 'react';
|
|
30
|
+
import type { Entry } from '../../agent/reducer.js';
|
|
31
|
+
import type { TermCapabilities } from '../capabilities.js';
|
|
32
|
+
import type { DensityMode } from '../density.js';
|
|
33
|
+
import type { Theme } from '../theme.js';
|
|
34
|
+
export interface LiveTailProps {
|
|
35
|
+
/** `entries.slice(settledCount).filter(isExcluded === false)`: the same input as `buildLivePreview`. */
|
|
36
|
+
unsettled: readonly Entry[];
|
|
37
|
+
/** Render only the last N entries; the box clips the older ones from the top. */
|
|
38
|
+
maxEntries: number;
|
|
39
|
+
/** Fixed height: the root box's `height`. The content never decides it (I-4). */
|
|
40
|
+
rows: number;
|
|
41
|
+
/** `budget.previewCols` - the width left after the rail took its column (I-5). */
|
|
42
|
+
cols: number;
|
|
43
|
+
expandedToolIds: Record<string, true>;
|
|
44
|
+
thinkingVisible: boolean;
|
|
45
|
+
reducedMotion: boolean;
|
|
46
|
+
density: DensityMode;
|
|
47
|
+
nowSec?: number;
|
|
48
|
+
theme: Theme;
|
|
49
|
+
caps: TermCapabilities;
|
|
50
|
+
}
|
|
51
|
+
export interface TailEntry {
|
|
52
|
+
entry: Entry;
|
|
53
|
+
/** The entry before it in the FULL list: `EntryView` uses it to decide the separator. */
|
|
54
|
+
prev: Entry | undefined;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* The last `maxEntries` entries, each with its predecessor.
|
|
58
|
+
*
|
|
59
|
+
* `prev` must come from the COMPLETE unsettled list rather than from the truncated
|
|
60
|
+
* slice, or the first rendered entry would look like the head of the conversation and
|
|
61
|
+
* drop the separation row the transcript shows above the same card.
|
|
62
|
+
*/
|
|
63
|
+
export declare function selectTailEntries(unsettled: readonly Entry[], maxEntries: number): readonly TailEntry[];
|
|
64
|
+
export declare function LiveTail({ unsettled, maxEntries, rows, cols, expandedToolIds, thinkingVisible, reducedMotion, density, nowSec, theme, caps, }: LiveTailProps): React.ReactElement;
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
/**
|
|
6
|
+
* The last `maxEntries` entries, each with its predecessor.
|
|
7
|
+
*
|
|
8
|
+
* `prev` must come from the COMPLETE unsettled list rather than from the truncated
|
|
9
|
+
* slice, or the first rendered entry would look like the head of the conversation and
|
|
10
|
+
* drop the separation row the transcript shows above the same card.
|
|
11
|
+
*/
|
|
12
|
+
export function selectTailEntries(unsettled, maxEntries) {
|
|
13
|
+
const limit = Number.isFinite(maxEntries) ? Math.max(0, Math.floor(maxEntries)) : 0;
|
|
14
|
+
if (limit === 0)
|
|
15
|
+
return [];
|
|
16
|
+
const start = Math.max(0, unsettled.length - limit);
|
|
17
|
+
const selected = [];
|
|
18
|
+
for (let index = start; index < unsettled.length; index += 1) {
|
|
19
|
+
selected.push({ entry: unsettled[index], prev: index > 0 ? unsettled[index - 1] : undefined });
|
|
20
|
+
}
|
|
21
|
+
return selected;
|
|
22
|
+
}
|
|
23
|
+
export function LiveTail({ unsettled, maxEntries, rows, cols, expandedToolIds, thinkingVisible, reducedMotion, density, nowSec, theme, caps, }) {
|
|
24
|
+
const limited = Math.min(maxEntries, SCROLLBACK_LIMITS.liveTailMaxEntries);
|
|
25
|
+
const selected = selectTailEntries(unsettled, limited);
|
|
26
|
+
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
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Height and column budget of the native-scroll PANEL BAND
|
|
3
|
+
* (release-gate-and-native-panels §6.2).
|
|
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
|
|
9
|
+
* streaming tail and the rail (`TodoPanel`), sitting directly above the composer.
|
|
10
|
+
*
|
|
11
|
+
* PURE: no Ink, no clock, no terminal, no config read. Every invariant below is
|
|
12
|
+
* therefore established BY CONSTRUCTION for all input combinations and can be
|
|
13
|
+
* enumerated by a property test (AC-9..AC-14, AC-31):
|
|
14
|
+
*
|
|
15
|
+
* - `bandRows <= remaining` and therefore `liveRows <= rows - 1` (I-4). Ink writes
|
|
16
|
+
* `clearTerminal` - which contains `ESC[3J`, erasing the user's scrollback - as
|
|
17
|
+
* soon as a frame is as tall as the terminal; that one row is the difference
|
|
18
|
+
* between a tidy region and data loss.
|
|
19
|
+
* - `previewCols + railCols === contentCols`, always (AC-10). The rail is a column
|
|
20
|
+
* INSIDE the region, so the preview loses exactly the columns the rail takes and
|
|
21
|
+
* nothing else (I-5: printing keeps the full `contentCols`).
|
|
22
|
+
* - `teamRows > 0` leaves room for the rail plus one preview row (AC-12).
|
|
23
|
+
* - `previewRows <= previewCap`, so the structural caps
|
|
24
|
+
* `maxLiveTailRows` / `liveTailRowShare` keep binding (I-10).
|
|
25
|
+
*
|
|
26
|
+
* THERE IS EXACTLY ONE CALL SITE: `buildScrollbackBudget` (I-8). This module is
|
|
27
|
+
* exported so it can be tested, not so a second caller can allocate rows of its own
|
|
28
|
+
* - `remaining` is computed there, and a second copy of it is a second authority.
|
|
29
|
+
*/
|
|
30
|
+
export type BandReason =
|
|
31
|
+
/** Both panels are on screen. */
|
|
32
|
+
'visible'
|
|
33
|
+
/** No active plan: no rail. Same name and meaning as `TodoRailLayout`'s `'empty'`. */
|
|
34
|
+
| 'empty'
|
|
35
|
+
/** `cfg.todo.panel === false`. */
|
|
36
|
+
| 'disabled'
|
|
37
|
+
/** An overlay is open: the band yields to it. */
|
|
38
|
+
| 'overlay'
|
|
39
|
+
/** `todoRailWidth(contentCols) === 0` (terminal narrower than 76 columns). */
|
|
40
|
+
| 'narrow'
|
|
41
|
+
/** The remaining rows cannot hold `panelMinRows` for the rail. */
|
|
42
|
+
| 'short';
|
|
43
|
+
export interface LiveBandInput {
|
|
44
|
+
/**
|
|
45
|
+
* `buildFrameBudget().viewportRows + headerRows`: every row the band may use.
|
|
46
|
+
* Computed once by `buildScrollbackBudget`; callers must not re-derive it.
|
|
47
|
+
*/
|
|
48
|
+
remaining: number;
|
|
49
|
+
/** `cols - 1`: width of the whole band. `previewCols` is derived from it. */
|
|
50
|
+
contentCols: number;
|
|
51
|
+
/** `todoRailWidth(contentCols)`; `layout/rail.ts` stays the width authority. */
|
|
52
|
+
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
|
+
/** `state.todos.items.length` (0 = no active plan). */
|
|
58
|
+
itemCount: number;
|
|
59
|
+
/** `cfg.todo.panel`. */
|
|
60
|
+
panelEnabled: boolean;
|
|
61
|
+
overlayOpen: boolean;
|
|
62
|
+
/** `buildTeamPanelLayout().rowCount`; 0 when there is nothing to show. */
|
|
63
|
+
teamRowsNatural: number;
|
|
64
|
+
/** Hysteresis: the rail rows the previous frame already used. */
|
|
65
|
+
heldRailRows: number;
|
|
66
|
+
/** Hysteresis: the roster rows the previous frame already used. */
|
|
67
|
+
heldTeamRows: number;
|
|
68
|
+
}
|
|
69
|
+
export interface LiveBand {
|
|
70
|
+
reason: BandReason;
|
|
71
|
+
/** Band height: `teamRows + previewRows`, or `remaining` for an overlay. */
|
|
72
|
+
bandRows: number;
|
|
73
|
+
teamRows: number;
|
|
74
|
+
previewRows: number;
|
|
75
|
+
/** Row budget of the rail: the whole band (the rail spans it). 0 when hidden. */
|
|
76
|
+
railRows: number;
|
|
77
|
+
/** The input `railCols` when visible, 0 when hidden (`/perf` reads `rail=0x0`). */
|
|
78
|
+
railCols: number;
|
|
79
|
+
/** `max(0, contentCols - railCols)`; always sums with `railCols` to `contentCols`. */
|
|
80
|
+
previewCols: number;
|
|
81
|
+
/** Hysteresis output, fed back on the next frame. */
|
|
82
|
+
heldRailRows: number;
|
|
83
|
+
heldTeamRows: number;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Allocate the band. THE ORDER OF THE STEPS IS THE SPECIFICATION - reordering them
|
|
87
|
+
* changes the priority between "what this run is doing" (rail + roster) and "the
|
|
88
|
+
* words arriving right now" (preview), which is a product decision, not a detail.
|
|
89
|
+
*
|
|
90
|
+
* 0. Normalise every numeric input (`NaN` / `Infinity` / negatives become 0).
|
|
91
|
+
* 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 preview row:
|
|
93
|
+
* a dispatch is transient, the plan is the shape of the turn, both must fit.
|
|
94
|
+
* 3. Decide the rail's visibility, in the same reason order as `buildTodoRailLayout`.
|
|
95
|
+
* 4. RESERVE the rail's rows (`panelFullRows` floor - the 3-5 row compact form cannot
|
|
96
|
+
* show which item is running, and the band reaches 6 rows far more often than the
|
|
97
|
+
* full-screen rail does). A reservation, not the final value: see step 6.
|
|
98
|
+
* 5. The preview takes what is left, clamped by `previewCap`.
|
|
99
|
+
* 6. `bandRows = max(railReserved, teamRows + previewRows)`: with no unsettled
|
|
100
|
+
* content `previewRows` is 0, yet the rail must still be there (the user is
|
|
101
|
+
* reading a result and still needs the list).
|
|
102
|
+
* 7. Columns: the input `contentCols` is the only width authority.
|
|
103
|
+
* 8. Hand the late-arriving values (the hysteresis) back out for the next frame.
|
|
104
|
+
*/
|
|
105
|
+
export declare function buildLiveBand(input: LiveBandInput): LiveBand;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Height and column budget of the native-scroll PANEL BAND
|
|
3
|
+
* (release-gate-and-native-panels §6.2).
|
|
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
|
|
9
|
+
* streaming tail and the rail (`TodoPanel`), sitting directly above the composer.
|
|
10
|
+
*
|
|
11
|
+
* PURE: no Ink, no clock, no terminal, no config read. Every invariant below is
|
|
12
|
+
* therefore established BY CONSTRUCTION for all input combinations and can be
|
|
13
|
+
* enumerated by a property test (AC-9..AC-14, AC-31):
|
|
14
|
+
*
|
|
15
|
+
* - `bandRows <= remaining` and therefore `liveRows <= rows - 1` (I-4). Ink writes
|
|
16
|
+
* `clearTerminal` - which contains `ESC[3J`, erasing the user's scrollback - as
|
|
17
|
+
* soon as a frame is as tall as the terminal; that one row is the difference
|
|
18
|
+
* between a tidy region and data loss.
|
|
19
|
+
* - `previewCols + railCols === contentCols`, always (AC-10). The rail is a column
|
|
20
|
+
* INSIDE the region, so the preview loses exactly the columns the rail takes and
|
|
21
|
+
* nothing else (I-5: printing keeps the full `contentCols`).
|
|
22
|
+
* - `teamRows > 0` leaves room for the rail plus one preview row (AC-12).
|
|
23
|
+
* - `previewRows <= previewCap`, so the structural caps
|
|
24
|
+
* `maxLiveTailRows` / `liveTailRowShare` keep binding (I-10).
|
|
25
|
+
*
|
|
26
|
+
* THERE IS EXACTLY ONE CALL SITE: `buildScrollbackBudget` (I-8). This module is
|
|
27
|
+
* exported so it can be tested, not so a second caller can allocate rows of its own
|
|
28
|
+
* - `remaining` is computed there, and a second copy of it is a second authority.
|
|
29
|
+
*/
|
|
30
|
+
import { TODO_LIMITS } from '../../todo/limits.js';
|
|
31
|
+
import { SCROLLBACK_LIMITS } from './limits.js';
|
|
32
|
+
/** Invalid geometry becomes zero, exactly like `layout/team-panel.ts::nonNegativeInt`. */
|
|
33
|
+
function nonNegativeInt(value) {
|
|
34
|
+
return Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Allocate the band. THE ORDER OF THE STEPS IS THE SPECIFICATION - reordering them
|
|
38
|
+
* changes the priority between "what this run is doing" (rail + roster) and "the
|
|
39
|
+
* words arriving right now" (preview), which is a product decision, not a detail.
|
|
40
|
+
*
|
|
41
|
+
* 0. Normalise every numeric input (`NaN` / `Infinity` / negatives become 0).
|
|
42
|
+
* 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 preview row:
|
|
44
|
+
* a dispatch is transient, the plan is the shape of the turn, both must fit.
|
|
45
|
+
* 3. Decide the rail's visibility, in the same reason order as `buildTodoRailLayout`.
|
|
46
|
+
* 4. RESERVE the rail's rows (`panelFullRows` floor - the 3-5 row compact form cannot
|
|
47
|
+
* show which item is running, and the band reaches 6 rows far more often than the
|
|
48
|
+
* full-screen rail does). A reservation, not the final value: see step 6.
|
|
49
|
+
* 5. The preview takes what is left, clamped by `previewCap`.
|
|
50
|
+
* 6. `bandRows = max(railReserved, teamRows + previewRows)`: with no unsettled
|
|
51
|
+
* content `previewRows` is 0, yet the rail must still be there (the user is
|
|
52
|
+
* reading a result and still needs the list).
|
|
53
|
+
* 7. Columns: the input `contentCols` is the only width authority.
|
|
54
|
+
* 8. Hand the late-arriving values (the hysteresis) back out for the next frame.
|
|
55
|
+
*/
|
|
56
|
+
export function buildLiveBand(input) {
|
|
57
|
+
const remaining = nonNegativeInt(input.remaining);
|
|
58
|
+
const contentCols = nonNegativeInt(input.contentCols);
|
|
59
|
+
const railCols = nonNegativeInt(input.railCols);
|
|
60
|
+
const previewCap = nonNegativeInt(input.previewCap);
|
|
61
|
+
const previewWanted = nonNegativeInt(input.previewWanted);
|
|
62
|
+
const itemCount = nonNegativeInt(input.itemCount);
|
|
63
|
+
const teamRowsNatural = nonNegativeInt(input.teamRowsNatural);
|
|
64
|
+
const heldRailRows = nonNegativeInt(input.heldRailRows);
|
|
65
|
+
const heldTeamRows = nonNegativeInt(input.heldTeamRows);
|
|
66
|
+
if (input.overlayOpen) {
|
|
67
|
+
return {
|
|
68
|
+
reason: 'overlay', bandRows: remaining, teamRows: 0, previewRows: 0,
|
|
69
|
+
railRows: 0, railCols: 0, previewCols: contentCols, heldRailRows: 0, heldTeamRows: 0,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const teamCeiling = Math.min(teamRowsNatural, Math.max(0, remaining - TODO_LIMITS.panelMinRows - 1));
|
|
73
|
+
// THE ROSTER IS SEATED AT ITS NATURAL (CLAMPED) HEIGHT, NOT AT THE REMEMBERED ONE.
|
|
74
|
+
// Seeding from `heldTeamRows` deadlocks the panel: a fresh dispatch starts with no
|
|
75
|
+
// memory, so frame one would seat zero rows, remember zero, and no later frame could
|
|
76
|
+
// ever seat the roster - the panel this feature exists to restore would never appear
|
|
77
|
+
// (found while implementing; recorded as IF-1 in the spec). The memory below only
|
|
78
|
+
// ever ADDS height back after a squeeze; it never withholds it.
|
|
79
|
+
const teamRows = teamCeiling;
|
|
80
|
+
const reason = !input.panelEnabled ? 'disabled'
|
|
81
|
+
: itemCount === 0 ? 'empty'
|
|
82
|
+
: railCols === 0 ? 'narrow'
|
|
83
|
+
: remaining - teamRows < SCROLLBACK_LIMITS.railBandMinRows ? 'short'
|
|
84
|
+
: 'visible';
|
|
85
|
+
const railVisible = reason === 'visible';
|
|
86
|
+
const railReserved = railVisible
|
|
87
|
+
? Math.min(Math.max(heldRailRows, TODO_LIMITS.panelFullRows), Math.max(0, remaining - teamRows - 1))
|
|
88
|
+
: 0;
|
|
89
|
+
const previewRows = Math.min(previewWanted, previewCap, Math.max(0, remaining - teamRows - railReserved));
|
|
90
|
+
const bandRows = Math.max(railReserved, teamRows + previewRows);
|
|
91
|
+
const railRows = railVisible ? bandRows : 0;
|
|
92
|
+
const railColsOut = railVisible ? railCols : 0;
|
|
93
|
+
return {
|
|
94
|
+
reason,
|
|
95
|
+
bandRows,
|
|
96
|
+
teamRows,
|
|
97
|
+
previewRows,
|
|
98
|
+
railRows,
|
|
99
|
+
railCols: railColsOut,
|
|
100
|
+
previewCols: Math.max(0, contentCols - railColsOut),
|
|
101
|
+
// MONOTONE MEMORY, NOT A SHRINKING ONE (I-6). A rail that collapsed to zero rows
|
|
102
|
+
// because the terminal shrank for two frames would otherwise come back four rows
|
|
103
|
+
// shorter and the band would visibly jump on every resize wobble. The RESET points
|
|
104
|
+
// (an emptied plan, a finished dispatch, a resize) live in the one place that owns
|
|
105
|
+
// the lifecycle, `use-scrollback-view.ts`; this function only remembers.
|
|
106
|
+
heldRailRows: Math.max(heldRailRows, railRows),
|
|
107
|
+
heldTeamRows: Math.max(heldTeamRows, teamRows),
|
|
108
|
+
};
|
|
109
|
+
}
|