tickmarkr 1.80.0 → 1.83.0
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/README.md +4 -4
- package/dist/brand.d.ts +4 -1
- package/dist/brand.js +46 -7
- package/dist/cli/commands/approve.js +53 -8
- package/dist/cli/commands/plan.js +17 -2
- package/dist/cli/commands/report.js +15 -6
- package/dist/cli/commands/ui.d.ts +1 -1
- package/dist/cli/commands/ui.js +47 -4
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/compile/collateral.d.ts +19 -0
- package/dist/compile/collateral.js +90 -0
- package/dist/compile/index.js +18 -4
- package/dist/drivers/herdr.d.ts +1 -0
- package/dist/drivers/herdr.js +19 -0
- package/dist/drivers/types.d.ts +1 -0
- package/dist/gates/acceptance.js +50 -8
- package/dist/gates/llm.js +34 -19
- package/dist/gates/review.d.ts +9 -1
- package/dist/gates/review.js +173 -7
- package/dist/gates/run-gates.d.ts +1 -0
- package/dist/gates/run-gates.js +18 -1
- package/dist/report/bundle.d.ts +6 -1
- package/dist/report/bundle.js +13 -3
- package/dist/run/daemon.d.ts +5 -0
- package/dist/run/daemon.js +188 -27
- package/dist/run/journal.d.ts +5 -0
- package/dist/run/journal.js +71 -1
- package/dist/tui/cockpit/capture.d.ts +107 -1
- package/dist/tui/cockpit/capture.js +287 -10
- package/dist/tui/cockpit/components.d.ts +15 -86
- package/dist/tui/cockpit/components.js +49 -55
- package/dist/tui/cockpit/derive.d.ts +73 -1
- package/dist/tui/cockpit/derive.js +211 -15
- package/dist/tui/cockpit/keys.d.ts +244 -0
- package/dist/tui/cockpit/keys.js +634 -0
- package/dist/tui/cockpit/layout.d.ts +162 -0
- package/dist/tui/cockpit/layout.js +229 -0
- package/dist/tui/cockpit/live.d.ts +80 -0
- package/dist/tui/cockpit/live.js +311 -0
- package/dist/tui/cockpit/run-cockpit.d.ts +65 -2
- package/dist/tui/cockpit/run-cockpit.js +705 -128
- package/dist/tui/cockpit/setup-cockpit.d.ts +149 -1
- package/dist/tui/cockpit/setup-cockpit.js +432 -56
- package/dist/tui/cockpit/views.d.ts +72 -0
- package/dist/tui/cockpit/views.js +56 -0
- package/dist/tui/cockpit/width.d.ts +105 -0
- package/dist/tui/cockpit/width.js +276 -0
- package/fixtures/speckit-sample/tasks.md +2 -2
- package/package.json +3 -1
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
-
import { Box } from "ink";
|
|
3
|
-
import { cloneElement, } from "react";
|
|
2
|
+
import { Box, useApp, useStdout } from "ink";
|
|
3
|
+
import { cloneElement, useInsertionEffect, useMemo, useRef, } from "react";
|
|
4
4
|
import { GLYPHS, PLAIN_COMPACT_LOCKUP, } from "../../brand.js";
|
|
5
|
-
import { allocateBandColumns, BandLines, BodyText, CockpitGrid, composeBandLine, JournalRowPanel,
|
|
6
|
-
import {
|
|
5
|
+
import { allocateBandColumns, BandLines, BodyText, CockpitGrid, composeBandLine, JournalRowPanel, keyRosterLines, Panel, PANEL_CHROME_ROWS, ProgressMeter, Sparkline, StatTile, StatusStrip, } from "./components.js";
|
|
6
|
+
import { fieldReading } from "./derive.js";
|
|
7
|
+
import { initialRunInteractionState, projectRunKeyEntries, reconcileRunInteraction, RUN_SIDE_RAIL_COLUMN_FLOOR, runPanelFocusOrder, runSideRailVisible, selectableRunViewRowIds, } from "./keys.js";
|
|
8
|
+
import { DECISIONS_TAB_TITLE, RUN_VIEWS, runViewById, setupSectionAt, SETUP_SECTIONS, } from "./views.js";
|
|
9
|
+
import { FRAME_HEADER_GAP, FULL_JOURNAL_ROWS, planFrame, SIDEBAR_COLUMN_FLOOR, } from "./layout.js";
|
|
10
|
+
import { cellWidth, fitCells, wrapCells } from "./width.js";
|
|
7
11
|
export { deriveRunCockpitData } from "./derive.js";
|
|
8
12
|
export { PANEL_CHROME_ROWS } from "./components.js";
|
|
9
|
-
|
|
13
|
+
/** The width focus and key routing resolve at. */
|
|
14
|
+
export function runKeyColumns(columns) {
|
|
15
|
+
return columns >= SIDEBAR_COLUMN_FLOOR
|
|
16
|
+
? Math.max(columns, RUN_SIDE_RAIL_COLUMN_FLOOR)
|
|
17
|
+
: columns;
|
|
18
|
+
}
|
|
10
19
|
const NARROW_HEADER_ALLOCATION = {
|
|
11
20
|
chromeColumns: 0,
|
|
12
21
|
minimumReadableColumns: 1,
|
|
@@ -18,52 +27,15 @@ const NARROW_HEADER_ALLOCATION = {
|
|
|
18
27
|
* rows it will really occupy instead of a hand-tuned constant.
|
|
19
28
|
*/
|
|
20
29
|
export function wrappedRows(text, columns) {
|
|
21
|
-
|
|
22
|
-
return 1;
|
|
23
|
-
let rows = 1;
|
|
24
|
-
let used = 0;
|
|
25
|
-
for (const [index, word] of text.split(" ").entries()) {
|
|
26
|
-
// ponytail: code points, not display columns — every glyph the cockpit
|
|
27
|
-
// draws is one column wide. Reach for a width table only if it stops being.
|
|
28
|
-
const length = [...word].length;
|
|
29
|
-
if (index !== 0) {
|
|
30
|
-
if (used >= columns) {
|
|
31
|
-
rows += 1;
|
|
32
|
-
used = 0;
|
|
33
|
-
}
|
|
34
|
-
used += 1;
|
|
35
|
-
}
|
|
36
|
-
if (length > columns) {
|
|
37
|
-
const startsThisLine = 1 + Math.floor((length - (columns - used) - 1) / columns);
|
|
38
|
-
if (Math.floor((length - 1) / columns) < startsThisLine) {
|
|
39
|
-
rows += 1;
|
|
40
|
-
used = 0;
|
|
41
|
-
}
|
|
42
|
-
for (let taken = 0; taken < length; taken += 1) {
|
|
43
|
-
if (used < columns)
|
|
44
|
-
used += 1;
|
|
45
|
-
else {
|
|
46
|
-
rows += 1;
|
|
47
|
-
used = 1;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
continue;
|
|
51
|
-
}
|
|
52
|
-
if (used > 0 && used + length > columns) {
|
|
53
|
-
rows += 1;
|
|
54
|
-
used = 0;
|
|
55
|
-
}
|
|
56
|
-
used += length;
|
|
57
|
-
}
|
|
58
|
-
return rows;
|
|
30
|
+
return wrapCells(text, columns).length;
|
|
59
31
|
}
|
|
60
32
|
/** The rows a bordered panel occupies when its body wraps into `columns`. */
|
|
61
33
|
export function panelRows(lines, columns) {
|
|
62
|
-
return lines.reduce((rows, line) => rows +
|
|
34
|
+
return lines.reduce((rows, line) => rows + wrapCells(line, columns, { continuationPrefix: "↳ " }).length, PANEL_CHROME_ROWS);
|
|
63
35
|
}
|
|
64
36
|
function IdentityHeader({ data, columns, secondary = true, }) {
|
|
65
37
|
const lockup = PLAIN_COMPACT_LOCKUP.split("\n");
|
|
66
|
-
if (columns
|
|
38
|
+
if (!runSideRailVisible(columns)) {
|
|
67
39
|
const primary = [
|
|
68
40
|
{ title: "", lines: [lockup[0] ?? "tickmarkr"] },
|
|
69
41
|
{
|
|
@@ -88,107 +60,712 @@ function NarrowHeaderBand({ columns, content, rightStrong = false, }) {
|
|
|
88
60
|
const widths = allocateBandColumns(columns, content, NARROW_HEADER_ALLOCATION);
|
|
89
61
|
return (_jsxs(CockpitGrid, { columns: columns, columnContents: content, allocation: NARROW_HEADER_ALLOCATION, children: [_jsx(BandLines, { columns: Math.max(1, widths[0]), lines: content[0].lines, emphasis: "strong" }), _jsx(Box, { alignItems: "flex-end", flexDirection: "column", children: _jsx(BandLines, { columns: Math.max(1, widths[1]), lines: content[1].lines, emphasis: rightStrong ? "strong" : "normal" }) })] }));
|
|
90
62
|
}
|
|
91
|
-
|
|
92
|
-
|
|
63
|
+
const RUN_SIDE_RAIL_WIDTH = 16;
|
|
64
|
+
/** Border plus horizontal padding charged by Panel around its body. */
|
|
65
|
+
const RUN_PANEL_HORIZONTAL_CHROME = 4;
|
|
66
|
+
/** The header caption: the run's identity line, planned as the header's refinable sub-region. */
|
|
67
|
+
function frameCaption(data) {
|
|
68
|
+
return `${data.runId} · ${data.branch} · ${data.status} · ${data.elapsed}`;
|
|
93
69
|
}
|
|
94
|
-
|
|
95
|
-
|
|
70
|
+
/** The header's fixed left part: brand, version and the two tab labels. */
|
|
71
|
+
function headerLead(data, plan) {
|
|
72
|
+
const tabs = plan.tab === "watch"
|
|
73
|
+
? `[ WATCH ]${FRAME_HEADER_GAP}${DECISIONS_TAB_TITLE}`
|
|
74
|
+
: `WATCH${FRAME_HEADER_GAP}[ ${DECISIONS_TAB_TITLE} ]`;
|
|
75
|
+
return `tickmarkr ${data.binaryVersion}${FRAME_HEADER_GAP}${tabs}`;
|
|
96
76
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
77
|
+
/** The one plain header line: brand, version and the two tab labels, the active one marked. */
|
|
78
|
+
function PlannedHeader({ data, plan, columns, }) {
|
|
79
|
+
return (_jsx(BodyText, { emphasis: "strong", children: fitCells(headerLead(data, plan), Math.max(1, columns)) }));
|
|
80
|
+
}
|
|
81
|
+
/** The header's caption sub-region: the run identity, right-aligned in it. */
|
|
82
|
+
function PlannedCaption({ data, columns, }) {
|
|
83
|
+
const caption = frameCaption(data);
|
|
84
|
+
const pad = Math.max(0, columns - cellWidth(caption));
|
|
85
|
+
return (_jsx(BodyText, { children: fitCells(`${" ".repeat(pad)}${caption}`, Math.max(1, columns)) }));
|
|
86
|
+
}
|
|
87
|
+
function runViewBodyColumns(columns, navigationRail, keysRail) {
|
|
88
|
+
const railColumns = (navigationRail ? RUN_SIDE_RAIL_WIDTH + 1 : 0)
|
|
89
|
+
+ (keysRail ? RUN_SIDE_RAIL_WIDTH + 1 : 0);
|
|
90
|
+
return Math.max(1, columns - railColumns - RUN_PANEL_HORIZONTAL_CHROME);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* One row of a side rail. A rail row is exactly one row: where the layout
|
|
94
|
+
* squeezes the rail narrower than a label, the label is clipped rather than
|
|
95
|
+
* wrapped onto a second row nothing charges to the height budget — the rail's
|
|
96
|
+
* height is then a constant, and a rail can never be what overruns the terminal.
|
|
97
|
+
*/
|
|
98
|
+
function RailRow({ label, selected, }) {
|
|
99
|
+
return (_jsx(Box, { height: 1, overflow: "hidden", children: _jsxs(BodyText, { emphasis: selected ? "strong" : "normal", children: [selected ? GLYPHS.pointer : " ", " ", label] }) }));
|
|
100
|
+
}
|
|
101
|
+
function NavigationPanel({ focused = false, selection = 0, }) {
|
|
102
|
+
return (_jsx(Panel, { title: "VIEWS", width: RUN_SIDE_RAIL_WIDTH, focused: focused, children: RUN_VIEWS.map((view, index) => (_jsx(RailRow, { label: view.label, selected: index === selection }, view.id))) }));
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The setup tab's rail. It draws in the same column as VIEWS and holds fewer
|
|
106
|
+
* rows than it, so the rail can never be what decides the frame's height — the
|
|
107
|
+
* body's budget stays the one budget, and the labels fit their column unwrapped.
|
|
108
|
+
*/
|
|
109
|
+
/** The lines the selected setup section states about itself. */
|
|
110
|
+
function setupSectionLines(section, data) {
|
|
111
|
+
if (section.readOnly) {
|
|
112
|
+
return [`${section.title} is read-only on this surface`];
|
|
113
|
+
}
|
|
114
|
+
const parked = data.taskRows.filter((row) => row.state === "human");
|
|
115
|
+
if (parked.length === 0)
|
|
116
|
+
return ["No parked decisions in this engagement"];
|
|
117
|
+
return parked.map((row) => `${row.taskId} · parked · attempt ${row.attempts ?? 0}`);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* The setup tab's body, drawn inside the same row budget the watch tab's views
|
|
121
|
+
* are drawn inside — the tab an operator switches to can never be taller than
|
|
122
|
+
* the one they switched away from.
|
|
123
|
+
*/
|
|
124
|
+
function SetupTabPanel({ data, selection, focused, keyEntries, help, bodyRows, bodyColumns, }) {
|
|
125
|
+
const section = setupSectionAt(selection);
|
|
126
|
+
return (_jsx(SizedLinesPanel, { title: help ? "HELP" : section.title, lines: help ? helpLines(keyEntries) : setupSectionLines(section, data), bodyRows: bodyRows, wrapColumns: bodyColumns, focused: focused, flexGrow: 1 }));
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The one keybar, drawn once. While the help overlay is up it is not drawn at
|
|
130
|
+
* all: the overlay is this same projected roster, one entry per line, so
|
|
131
|
+
* drawing both states every key twice — and costs the frame the very rows the
|
|
132
|
+
* shortest supported terminal has none of.
|
|
133
|
+
*/
|
|
134
|
+
function RunKeybar({ entries, width, help, }) {
|
|
135
|
+
if (help)
|
|
136
|
+
return _jsx(_Fragment, {});
|
|
137
|
+
// The plan grants this band one row: the first lossless roster slice is drawn explicitly.
|
|
138
|
+
return (_jsx(Box, { height: 1, flexShrink: 0, overflow: "hidden", children: _jsx(BodyText, { emphasis: "dim", children: keyRosterLines(entries, width)[0] ?? "" }) }));
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* The selectable rows owned by one view. The overview's journal is a passive
|
|
142
|
+
* tail, not its item collection; tasks, gates and fleet remain honestly empty
|
|
143
|
+
* until their dedicated derivations land. Journal rows keep the source-record
|
|
144
|
+
* identities assigned by the production journal derivation.
|
|
145
|
+
*/
|
|
146
|
+
export function deriveRunViewRows(data, viewId, filterQuery = "") {
|
|
147
|
+
const query = filterQuery.trim().toLowerCase();
|
|
148
|
+
const rows = promotedViewRows(data, viewId);
|
|
149
|
+
return query === ""
|
|
150
|
+
? rows
|
|
151
|
+
: rows.filter((row) => row.text.toLowerCase().includes(query));
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* The rows a view owns, in the identities `runViewRowIdentities` assigns them —
|
|
155
|
+
* the two must agree, or the surface's keys would stand on rows the frame never
|
|
156
|
+
* drew. The overview owns no list of its own, and the journal is a tail.
|
|
157
|
+
*/
|
|
158
|
+
function promotedViewRows(data, viewId) {
|
|
159
|
+
if (viewId === "journal")
|
|
160
|
+
return data.journalRows;
|
|
161
|
+
if (viewId === "tasks") {
|
|
162
|
+
return data.taskRows.map((row) => ({
|
|
163
|
+
id: row.id,
|
|
164
|
+
time: fieldReading(row.lastEventTime),
|
|
165
|
+
state: taskRowState(row.state),
|
|
166
|
+
text: `${row.taskId} · ${fieldReading(row.state)} · attempt ${fieldReading(row.attempts)} · ${fieldReading(row.actor)}${row.title === undefined ? "" : ` · ${row.title}`}`,
|
|
167
|
+
}));
|
|
168
|
+
}
|
|
169
|
+
if (viewId === "gates") {
|
|
170
|
+
return data.gateRows.map((row) => ({
|
|
171
|
+
id: row.id,
|
|
172
|
+
time: row.time,
|
|
173
|
+
state: row.state,
|
|
174
|
+
text: `${fieldReading(row.taskId)} · ${fieldReading(row.gate)} · ${row.pass === undefined ? fieldReading(undefined) : row.pass ? "pass" : "fail"}${row.details === undefined ? "" : ` · ${row.details}`}`,
|
|
175
|
+
}));
|
|
176
|
+
}
|
|
177
|
+
if (viewId === "fleet") {
|
|
178
|
+
return data.fleetRows.map((row) => ({
|
|
179
|
+
id: row.id,
|
|
180
|
+
time: fieldReading(row.lastEventTime),
|
|
181
|
+
state: "neutral",
|
|
182
|
+
text: `${row.adapter} · ${row.model} · dispatches ${fieldReading(row.dispatches)}`,
|
|
183
|
+
}));
|
|
184
|
+
}
|
|
185
|
+
return [];
|
|
186
|
+
}
|
|
187
|
+
/** A task's recorded state read as the component vocabulary the rows draw in. */
|
|
188
|
+
function taskRowState(state) {
|
|
189
|
+
if (state === "done")
|
|
190
|
+
return "pass";
|
|
191
|
+
if (state === "failed")
|
|
192
|
+
return "fail";
|
|
193
|
+
if (state === "running" || state === "human")
|
|
194
|
+
return "active";
|
|
195
|
+
return "neutral";
|
|
196
|
+
}
|
|
197
|
+
function windowedRows(rows, bodyRows, selection) {
|
|
198
|
+
const limit = Math.max(0, Math.floor(bodyRows));
|
|
199
|
+
const selectedIndex = selection === null
|
|
200
|
+
? -1
|
|
201
|
+
: rows.findIndex((row) => row.id === selection);
|
|
202
|
+
const offset = selectedIndex < limit
|
|
203
|
+
? 0
|
|
204
|
+
: selectedIndex - limit + 1;
|
|
205
|
+
return {
|
|
206
|
+
rows: rows.slice(offset, offset + limit),
|
|
207
|
+
...(selectedIndex < 0 ? {} : { selection: selectedIndex - offset }),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
function SizedJournalPanel({ rows, bodyRows, title, selection, focused, }) {
|
|
211
|
+
const window = windowedRows(rows, bodyRows, selection);
|
|
212
|
+
const emptyRows = Math.max(0, bodyRows - window.rows.length);
|
|
213
|
+
const journal = JournalRowPanel({
|
|
214
|
+
rows: window.rows,
|
|
215
|
+
title,
|
|
216
|
+
selection: window.selection,
|
|
217
|
+
focused,
|
|
218
|
+
});
|
|
101
219
|
const journalProps = journal.props;
|
|
102
220
|
return cloneElement(journal, {}, journalProps.children, emptyRows > 0 ? _jsx(Box, { height: emptyRows }, "journal-space") : null);
|
|
103
221
|
}
|
|
222
|
+
/** A body panel (help overlay, opened event) padded to the journal's rows. */
|
|
223
|
+
function SizedLinesPanel({ title, lines, bodyRows, wrapColumns, focused, flexGrow, }) {
|
|
224
|
+
const drawnLines = wrapColumns === undefined
|
|
225
|
+
? lines
|
|
226
|
+
: lines.flatMap((line) => composeBandLine(line, wrapColumns));
|
|
227
|
+
const visible = drawnLines.slice(0, bodyRows);
|
|
228
|
+
const emptyRows = Math.max(0, bodyRows - visible.length);
|
|
229
|
+
return (_jsxs(Panel, { title: title, focused: focused, flexGrow: flexGrow, children: [visible.map((line, index) => (_jsx(Box, { height: 1, overflow: "hidden", children: _jsx(BodyText, { children: line }) }, `${index}:${line}`))), emptyRows > 0 && _jsx(Box, { height: emptyRows })] }));
|
|
230
|
+
}
|
|
231
|
+
/** The lines the help overlay draws: every advertised key and its promise. */
|
|
232
|
+
function helpLines(entries) {
|
|
233
|
+
return entries.map(({ key, label }) => `${key} ${label}`);
|
|
234
|
+
}
|
|
235
|
+
/** The lines the opened-event view draws for one journal row. */
|
|
236
|
+
function eventLines(row) {
|
|
237
|
+
return [`${row.time} · ${row.state}`, row.text];
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* A view drawn from the rows its own derivation produces: its detail while the
|
|
241
|
+
* opened row stands, its genuinely empty state when the derivation yields
|
|
242
|
+
* nothing, otherwise a window over the rows marked at the selection. The state
|
|
243
|
+
* arrives already repaired by the frame, which owns both it and the data.
|
|
244
|
+
*/
|
|
245
|
+
function RunViewPanel({ data, viewId, interaction, focused, keyEntries, bodyRows, bodyColumns, }) {
|
|
246
|
+
const view = runViewById(viewId);
|
|
247
|
+
const rows = deriveRunViewRows(data, viewId, interaction.filterQuery);
|
|
248
|
+
if (interaction.help) {
|
|
249
|
+
return (_jsx(Panel, { title: "HELP", focused: focused, flexGrow: 1, children: helpLines(keyEntries).map((line) => _jsx(BodyText, { children: line }, line)) }));
|
|
250
|
+
}
|
|
251
|
+
const opened = interaction.opened === null
|
|
252
|
+
? undefined
|
|
253
|
+
: rows.find((row) => row.id === interaction.opened);
|
|
254
|
+
if (opened !== undefined) {
|
|
255
|
+
return (_jsx(SizedLinesPanel, { title: view.detailTitle, lines: eventLines(opened), bodyRows: bodyRows, wrapColumns: bodyColumns, focused: focused, flexGrow: 1 }));
|
|
256
|
+
}
|
|
257
|
+
if (rows.length === 0) {
|
|
258
|
+
return (_jsx(Panel, { title: view.title, focused: focused, flexGrow: 1, children: _jsxs(BodyText, { emphasis: "dim", children: ["No ", view.id, " in this engagement"] }) }));
|
|
259
|
+
}
|
|
260
|
+
return (_jsx(SizedJournalPanel, { rows: rows, bodyRows: bodyRows, title: interaction.filterPrompt
|
|
261
|
+
? `${view.title} /${interaction.filterQuery}`
|
|
262
|
+
: view.title, selection: interaction.selection, focused: focused }));
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* What the journal's slot of the RUN panel draws under an interaction state:
|
|
266
|
+
* the help overlay while ? is up, otherwise the passive overview tail. Row
|
|
267
|
+
* selection, filtering and detail live only on the Journal view above.
|
|
268
|
+
*/
|
|
269
|
+
function JournalBody({ data, interaction, bodyRows, keyEntries, }) {
|
|
270
|
+
const rows = deriveRunViewRows(data, "journal");
|
|
271
|
+
if (interaction?.help === true) {
|
|
272
|
+
return (_jsx(SizedLinesPanel, { title: "HELP", lines: helpLines(keyEntries), bodyRows: bodyRows }));
|
|
273
|
+
}
|
|
274
|
+
return (_jsx(SizedJournalPanel, { rows: rows, bodyRows: bodyRows, selection: null }));
|
|
275
|
+
}
|
|
104
276
|
function RunStats({ data, mode, }) {
|
|
277
|
+
if (mode === "none")
|
|
278
|
+
return _jsx(_Fragment, {});
|
|
105
279
|
if (mode === "summary") {
|
|
106
280
|
return (_jsx(BodyText, { children: `tasks ${data.tasks.done}/${data.tasks.total} · gates ${data.gates.passed}/${data.gates.total} · pass ${data.passRate.value}%` }));
|
|
107
281
|
}
|
|
108
282
|
return (_jsxs(CockpitGrid, { children: [_jsx(StatTile, { label: "TASKS", value: `${data.tasks.done}/${data.tasks.total}`, samples: data.tasks.samples }), _jsx(StatTile, { label: "GATES", value: `${data.gates.passed}/${data.gates.total}`, samples: data.gates.samples }), _jsx(StatTile, { label: "PASS RATE", value: `${data.passRate.value}%`, samples: data.passRate.samples })] }));
|
|
109
283
|
}
|
|
284
|
+
/** The sparkline's block ramp, one glyph per trailing sample, newest last. */
|
|
285
|
+
const COMPACT_SPARK_BLOCKS = "▁▂▃▄▅▆▇█";
|
|
286
|
+
function compactSparkline(samples, cells) {
|
|
287
|
+
const buckets = samples.slice(-Math.max(0, cells));
|
|
288
|
+
const populated = buckets.filter((sample) => typeof sample === "number" && Number.isFinite(sample));
|
|
289
|
+
const minimum = populated.length > 0 ? Math.min(...populated) : 0;
|
|
290
|
+
const maximum = populated.length > 0 ? Math.max(...populated) : 0;
|
|
291
|
+
let glyphs = "";
|
|
292
|
+
for (const sample of buckets) {
|
|
293
|
+
if (sample === null) {
|
|
294
|
+
glyphs += " ";
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
297
|
+
const level = maximum === minimum
|
|
298
|
+
? COMPACT_SPARK_BLOCKS.length - 1
|
|
299
|
+
: Math.round(((sample - minimum) / (maximum - minimum))
|
|
300
|
+
* (COMPACT_SPARK_BLOCKS.length - 1));
|
|
301
|
+
glyphs += COMPACT_SPARK_BLOCKS[level];
|
|
302
|
+
}
|
|
303
|
+
return glyphs;
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* One vitals element of the sidebar's block, drawn whole: the plan has already
|
|
307
|
+
* decided which elements draw and in which mode, so nothing here measures or
|
|
308
|
+
* surrenders. The full form is a count row over a sparkline row (the meter: its
|
|
309
|
+
* label over its bar); the compact form is one inline row per element.
|
|
310
|
+
*/
|
|
311
|
+
function RailVitals({ data, columns, mode, elements, }) {
|
|
312
|
+
const figures = {
|
|
313
|
+
tasks: {
|
|
314
|
+
value: `${data.tasks.done}/${data.tasks.total}`,
|
|
315
|
+
samples: data.tasks.samples,
|
|
316
|
+
},
|
|
317
|
+
gates: {
|
|
318
|
+
value: `${data.gates.passed}/${data.gates.total}`,
|
|
319
|
+
samples: data.gates.samples,
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
const figure = (element) => {
|
|
323
|
+
const reading = figures[element];
|
|
324
|
+
if (mode === "compact") {
|
|
325
|
+
const lead = `${element} ${reading.value} `;
|
|
326
|
+
return (_jsx(Box, { height: 1, flexShrink: 0, overflow: "hidden", children: _jsx(BodyText, { emphasis: "strong", children: fitCells(`${lead}${compactSparkline(reading.samples, columns - cellWidth(lead))}`, columns) }) }, element));
|
|
327
|
+
}
|
|
328
|
+
return (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsx(Box, { height: 1, flexShrink: 0, overflow: "hidden", children: _jsx(BodyText, { emphasis: "strong", children: fitCells(`${element} ${reading.value}`, columns) }) }), _jsx(Sparkline, { samples: reading.samples })] }, element));
|
|
329
|
+
};
|
|
330
|
+
const meter = mode === "compact"
|
|
331
|
+
? (_jsxs(Box, { height: 1, flexDirection: "row", flexWrap: "nowrap", flexShrink: 0, overflow: "hidden", children: [_jsx(BodyText, { emphasis: "strong", children: "pass " }), _jsx(ProgressMeter, { value: data.passRate.value, width: Math.max(1, columns - 9) })] }, "meter"))
|
|
332
|
+
: (_jsxs(Box, { flexDirection: "column", flexShrink: 0, children: [_jsx(Box, { height: 1, flexShrink: 0, overflow: "hidden", children: _jsx(BodyText, { emphasis: "strong", children: fitCells("pass", columns) }) }), _jsx(Box, { height: 1, flexShrink: 0, overflow: "hidden", children: _jsx(ProgressMeter, { value: data.passRate.value, width: Math.max(1, columns - 6) }) })] }, "meter"));
|
|
333
|
+
return (_jsx(Box, { flexDirection: "column", width: columns, flexShrink: 0, overflow: "hidden", children: elements.map((element) => (element === "meter" ? meter : figure(element))) }));
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* The navigation rail, unboxed, drawn at the composition the plan computed:
|
|
337
|
+
* the menu at the top, the vitals anchored to the bottom, and the planned gap
|
|
338
|
+
* of blank rows between them — the gap is planFrame's, never a spacing hack
|
|
339
|
+
* applied here.
|
|
340
|
+
*/
|
|
341
|
+
function RailBand({ data, focused, selection, columns, rows, sidebar, }) {
|
|
342
|
+
return (_jsxs(Box, { flexDirection: "column", width: columns, height: rows, overflow: "hidden", children: [sidebar.menuRows > RUN_VIEWS.length && (_jsx(Box, { height: 1, flexShrink: 0, overflow: "hidden", children: _jsx(BodyText, { emphasis: "strong", children: fitCells(focused ? `${GLYPHS.pointer} VIEWS` : "VIEWS", columns) }) })), RUN_VIEWS.map((view, index) => (_jsx(Box, { height: 1, flexShrink: 0, overflow: "hidden", children: _jsx(BodyText, { emphasis: index === selection ? "strong" : "normal", children: fitCells(`${index === selection ? GLYPHS.pointer : " "} ${view.label}`, columns) }) }, view.id))), sidebar.gapRows > 0 && _jsx(Box, { height: sidebar.gapRows, flexShrink: 0 }), _jsx(RailVitals, { data: data, columns: columns, mode: sidebar.vitalsMode, elements: sidebar.vitals })] }));
|
|
343
|
+
}
|
|
344
|
+
/** The setup tab's rail, drawn unboxed in the same planned columns. */
|
|
345
|
+
function SectionsBand({ focused, selection, columns, }) {
|
|
346
|
+
return (_jsxs(Box, { flexDirection: "column", width: columns, overflow: "hidden", children: [_jsx(Box, { height: 1, overflow: "hidden", children: _jsx(BodyText, { emphasis: "strong", children: fitCells(focused ? `${GLYPHS.pointer} SECTIONS` : "SECTIONS", columns) }) }), SETUP_SECTIONS.map((section, index) => (_jsx(Box, { height: 1, overflow: "hidden", children: _jsx(BodyText, { emphasis: index === selection ? "strong" : "normal", children: fitCells(`${index === selection ? GLYPHS.pointer : " "} ${section.label}`, columns) }) }, section.id)))] }));
|
|
347
|
+
}
|
|
348
|
+
/** A planned rule band: one row of the region edge glyph, exactly `columns` wide. */
|
|
349
|
+
function RuleBand({ columns }) {
|
|
350
|
+
return (_jsx(Box, { height: 1, overflow: "hidden", children: _jsx(BodyText, { emphasis: "dim", children: "─".repeat(Math.max(0, columns)) }) }));
|
|
351
|
+
}
|
|
352
|
+
/** The strip the plan draws instead of a rail between 40 and 63 columns: the five views on one row. */
|
|
353
|
+
function ViewStrip({ selection, columns, }) {
|
|
354
|
+
return (_jsx(Box, { width: columns, height: 1, flexDirection: "row", flexWrap: "nowrap", overflow: "hidden", children: RUN_VIEWS.map((view, index) => (_jsx(Box, { flexShrink: 0, children: _jsxs(BodyText, { children: [index === selection ? `${GLYPHS.pointer}${view.label}` : ` ${view.label}`, index === RUN_VIEWS.length - 1 ? "" : " "] }) }, view.id))) }));
|
|
355
|
+
}
|
|
356
|
+
/** The journal tail band: it draws exactly the rows the plan left it, surrendered whole by tier. */
|
|
357
|
+
function TailBand({ data, rows, columns, pendingWrites, }) {
|
|
358
|
+
const lines = pendingWrites
|
|
359
|
+
? ["PENDING WRITES · none"]
|
|
360
|
+
: deriveRunViewRows(data, "journal")
|
|
361
|
+
.slice(-rows)
|
|
362
|
+
.map((row) => `${row.time} ${row.text}`);
|
|
363
|
+
// A styled blank cell differs with colour on and off; these rows carry no dim style.
|
|
364
|
+
return (_jsx(Box, { flexDirection: "column", height: rows, overflow: "hidden", children: Array.from({ length: rows }, (_, index) => (_jsx(Box, { height: 1, overflow: "hidden", children: _jsx(BodyText, { children: fitCells(lines[index] ?? "", columns) }) }, index))) }));
|
|
365
|
+
}
|
|
110
366
|
function ApprovedProgress({ data, caption, }) {
|
|
111
367
|
return (_jsxs(Panel, { title: "PROGRESS", children: [_jsx(ProgressMeter, { value: data.progress, width: 28 }), caption && _jsx(BodyText, { emphasis: "dim", children: data.progressCaption })] }));
|
|
112
368
|
}
|
|
113
369
|
function CompactProgress({ data, caption, meterWidth = 28, }) {
|
|
114
370
|
return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { flexDirection: "row", flexWrap: "nowrap", children: [_jsx(BodyText, { emphasis: "strong", children: "PROGRESS " }), _jsx(ProgressMeter, { value: data.progress, width: meterWidth })] }), caption && _jsx(BodyText, { emphasis: "dim", children: data.progressCaption })] }));
|
|
115
371
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
372
|
+
/** The RUN view's interior, allocated out of the planned body span, each element granted whole or not at all. */
|
|
373
|
+
function resolveRunViewContent(bodySpan, bodyColumns, data) {
|
|
374
|
+
const interior = Math.max(0, bodySpan - PANEL_CHROME_ROWS);
|
|
375
|
+
let available = interior;
|
|
376
|
+
const progressBar = available >= 1;
|
|
377
|
+
if (progressBar)
|
|
378
|
+
available -= 1;
|
|
379
|
+
// Every element is granted whole or not at all, and the summary reports the fit decision it computed.
|
|
380
|
+
let journal = 0;
|
|
381
|
+
if (available >= PANEL_CHROME_ROWS + 1) {
|
|
382
|
+
journal = 1;
|
|
383
|
+
available -= PANEL_CHROME_ROWS + 1;
|
|
384
|
+
}
|
|
385
|
+
const summary = available >= 1;
|
|
386
|
+
if (summary)
|
|
387
|
+
available -= 1;
|
|
388
|
+
const tail = journal > 0
|
|
389
|
+
? Math.min(available, Math.max(0, FULL_JOURNAL_ROWS - journal))
|
|
390
|
+
: 0;
|
|
391
|
+
journal += tail;
|
|
392
|
+
available -= tail;
|
|
393
|
+
const captionRows = progressBar
|
|
394
|
+
? wrapCells(data.progressCaption, Math.max(1, bodyColumns)).length
|
|
395
|
+
: 0;
|
|
396
|
+
const progressCaption = captionRows > 0 && available >= captionRows;
|
|
397
|
+
if (progressCaption)
|
|
398
|
+
available -= captionRows;
|
|
399
|
+
// The tiles are retired into the rail's unboxed vitals; the summary line is the only stats form drawn.
|
|
400
|
+
if (journal > 0)
|
|
401
|
+
journal += available;
|
|
402
|
+
// Restored only once every element is full and the journal can pay the chrome rows.
|
|
403
|
+
const approvedProgress = progressBar && progressCaption
|
|
404
|
+
&& journal > PANEL_CHROME_ROWS;
|
|
405
|
+
if (approvedProgress)
|
|
406
|
+
journal -= PANEL_CHROME_ROWS;
|
|
407
|
+
return {
|
|
408
|
+
journal,
|
|
409
|
+
stats: summary ? "summary" : "none",
|
|
410
|
+
progressBar,
|
|
411
|
+
progressCaption,
|
|
412
|
+
approvedProgress,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
/** What fills the regions this paint draws, resolved from the plan's own spans. */
|
|
416
|
+
function resolveRegionContent(plan, data, interaction) {
|
|
417
|
+
const bodyRegion = requiredRegion(plan, "body");
|
|
418
|
+
const bodySpan = bodyRegion.rows;
|
|
419
|
+
const sideRails = plan.band === "sidebar";
|
|
420
|
+
const body = Math.max(1, bodySpan - PANEL_CHROME_ROWS);
|
|
421
|
+
const bodyColumns = Math.max(1, bodyRegion.columns - RUN_PANEL_HORIZONTAL_CHROME);
|
|
422
|
+
const common = { body, bodyColumns, sideRails };
|
|
423
|
+
if (interaction.tab !== "setup" && interaction.activeView === "run") {
|
|
424
|
+
return {
|
|
425
|
+
...common,
|
|
426
|
+
// The help overlay stands in for the journal tail, drawing into its whole interior.
|
|
427
|
+
...(interaction.help
|
|
428
|
+
? {
|
|
429
|
+
journal: Math.max(1, bodySpan - PANEL_CHROME_ROWS * 2),
|
|
430
|
+
stats: "summary",
|
|
431
|
+
progressBar: false,
|
|
432
|
+
progressCaption: false,
|
|
433
|
+
approvedProgress: false,
|
|
434
|
+
}
|
|
435
|
+
: resolveRunViewContent(bodySpan, bodyColumns, data)),
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
return {
|
|
439
|
+
...common,
|
|
440
|
+
journal: 0,
|
|
441
|
+
stats: "summary",
|
|
442
|
+
progressBar: false,
|
|
443
|
+
progressCaption: false,
|
|
444
|
+
approvedProgress: false,
|
|
445
|
+
};
|
|
446
|
+
}
|
|
447
|
+
/** The RUN view drawn at its planned spans: every row budget arrives resolved. */
|
|
448
|
+
function LadderRunPanel({ data, content, compactMeterWidth, interaction, keyEntries, focused = true, }) {
|
|
449
|
+
if (interaction?.help === true) {
|
|
450
|
+
return (_jsx(SizedLinesPanel, { title: "HELP", lines: helpLines(keyEntries), bodyRows: content.body, wrapColumns: content.bodyColumns, focused: focused, flexGrow: 1 }));
|
|
451
|
+
}
|
|
452
|
+
return (_jsxs(Panel, { title: "RUN", focused: focused, flexGrow: 1, children: [_jsx(RunStats, { data: data, mode: content.stats }), content.progressBar && (content.approvedProgress
|
|
453
|
+
? _jsx(ApprovedProgress, { data: data, caption: content.progressCaption })
|
|
454
|
+
: (_jsx(CompactProgress, { data: data, caption: content.progressCaption, meterWidth: compactMeterWidth }))), content.journal > 0 && (_jsx(JournalBody, { data: data, interaction: interaction, bodyRows: content.journal, keyEntries: keyEntries }))] }));
|
|
455
|
+
}
|
|
456
|
+
function LegacyRunPanel({ data, interaction, keyEntries, focused = true, }) {
|
|
457
|
+
const rows = deriveRunViewRows(data, "journal");
|
|
458
|
+
return (_jsxs(Panel, { title: "RUN", focused: focused, flexGrow: 1, children: [_jsx(RunStats, { data: data, mode: "tiles" }), _jsxs(Panel, { title: "PROGRESS", children: [_jsx(ProgressMeter, { value: data.progress, width: 28 }), _jsx(BodyText, { emphasis: "dim", children: data.progressCaption })] }), _jsx(JournalBody, { data: data, interaction: interaction, bodyRows: rows.length, keyEntries: keyEntries })] }));
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* One planned band, drawn at exactly the span the plan gives it. Unsized (the
|
|
462
|
+
* legacy surface) it is whatever it contains.
|
|
463
|
+
*/
|
|
464
|
+
function FrameBand({ rows, row = false, gap, regionRef, children, }) {
|
|
465
|
+
return (_jsx(Box, { ref: regionRef, flexDirection: row ? "row" : "column", ...(gap === undefined ? {} : { gap }), ...(rows === undefined ? {} : { height: rows, flexShrink: 0 }), overflow: "hidden", children: children }));
|
|
466
|
+
}
|
|
467
|
+
/** A sized sub-floor plan draws the plain fallback, clipped to the measured size. */
|
|
468
|
+
function PlainFallbackPaint({ data, size, }) {
|
|
469
|
+
const columns = Math.max(1, size.columns);
|
|
470
|
+
const lines = [
|
|
471
|
+
`tickmarkr ${data.binaryVersion}`,
|
|
472
|
+
`${data.runId} ${data.status} ${data.elapsed}`,
|
|
473
|
+
...data.statusItems.map((item) => item.text),
|
|
130
474
|
];
|
|
131
|
-
|
|
132
|
-
return Math.max(...content.map((column, index) => composeBandLine(column.lines[0], Math.max(1, widths[index])).length));
|
|
475
|
+
return (_jsx(Box, { flexDirection: "column", width: size.columns, height: Math.max(0, size.rows), overflow: "hidden", children: lines.map((line, index) => (_jsx(Box, { height: 1, flexShrink: 0, overflow: "hidden", children: _jsx(BodyText, { children: fitCells(line, columns) }) }, `${index}:${line}`))) }));
|
|
133
476
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
:
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
const
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
477
|
+
/** The paint: every span is read off the plan's regions and every row budget off the resolved content. */
|
|
478
|
+
function RunCockpitPaint({ data, columns, plan, content, interaction, keyProjection = projectRunKeyEntries, onRegion, }) {
|
|
479
|
+
const suppliedInteraction = interaction ?? initialRunInteractionState();
|
|
480
|
+
const suppliedRows = deriveRunViewRows(data, suppliedInteraction.activeView, suppliedInteraction.filterQuery);
|
|
481
|
+
const currentInteraction = reconcileRunInteraction(suppliedInteraction, suppliedRows.map((row) => row.id));
|
|
482
|
+
const focusOrder = runPanelFocusOrder(runKeyColumns(columns));
|
|
483
|
+
const focus = currentInteraction.panel % focusOrder.length;
|
|
484
|
+
const focusedPanel = focusOrder[focus] ?? "CONTENT";
|
|
485
|
+
const contentFocused = focusedPanel === "CONTENT";
|
|
486
|
+
const activeView = currentInteraction.activeView;
|
|
487
|
+
const railSelection = currentInteraction.railSelection;
|
|
488
|
+
const keyEntries = keyProjection({
|
|
489
|
+
interaction: currentInteraction,
|
|
490
|
+
columns: runKeyColumns(columns),
|
|
491
|
+
// What the keybar promises is what a key can actually stand on: the journal
|
|
492
|
+
// tail owns no selectable row, so it advertises no row key.
|
|
493
|
+
rowIds: selectableRunViewRowIds(currentInteraction.activeView, suppliedRows.map((row) => row.id)),
|
|
494
|
+
});
|
|
495
|
+
// Follow mode reports its on state as a status-strip item carrying the
|
|
496
|
+
// brand's reserved active-toggle glyph; off it draws nothing, so the pinned
|
|
497
|
+
// frame is untouched.
|
|
498
|
+
const interactionStatusItems = [
|
|
499
|
+
...(currentInteraction.quit
|
|
500
|
+
? [{ state: "active", text: "Quit requested" }]
|
|
501
|
+
: []),
|
|
502
|
+
...(currentInteraction.follow
|
|
503
|
+
? [{ state: "active", text: "Follow" }]
|
|
504
|
+
: []),
|
|
505
|
+
];
|
|
506
|
+
const reportedStatusItems = [...interactionStatusItems, ...data.statusItems];
|
|
507
|
+
const regionRef = (id) => onRegion?.(id);
|
|
508
|
+
const railFocused = focusedPanel === "VIEWS";
|
|
509
|
+
if (plan === undefined || content === undefined) {
|
|
510
|
+
return (_jsxs(Box, { flexDirection: "column", width: columns, overflow: "hidden", children: [_jsx(IdentityHeader, { data: data, columns: columns }), _jsxs(Box, { flexDirection: "row", gap: 1, children: [_jsx(NavigationPanel, { focused: railFocused, selection: railSelection }), activeView === "run"
|
|
511
|
+
? (_jsx(LegacyRunPanel, { data: data, interaction: currentInteraction, keyEntries: keyEntries, focused: contentFocused }))
|
|
512
|
+
: (_jsx(RunViewPanel, { data: data, viewId: activeView, interaction: currentInteraction, focused: contentFocused, keyEntries: keyEntries, bodyRows: Math.max(1, currentInteraction.help
|
|
513
|
+
? keyEntries.length
|
|
514
|
+
: suppliedRows.length), bodyColumns: runViewBodyColumns(columns, true, false) }))] }), _jsx(StatusStrip, { items: reportedStatusItems, width: columns }), _jsx(RunKeybar, { entries: keyEntries, width: columns, help: currentInteraction.help })] }));
|
|
515
|
+
}
|
|
516
|
+
// Every region the plan places, drawn at the plan's own offset and span.
|
|
517
|
+
const region = (id) => plan.regions.find((candidate) => candidate.id === id);
|
|
518
|
+
const header = requiredRegion(plan, "header");
|
|
519
|
+
const body = requiredRegion(plan, "body");
|
|
520
|
+
const caption = region("caption");
|
|
521
|
+
const strip = region("strip");
|
|
522
|
+
const rail = region("rail");
|
|
523
|
+
const tail = region("tail");
|
|
524
|
+
const setup = currentInteraction.tab === "setup";
|
|
525
|
+
return (_jsxs(Box, { flexDirection: "column", width: plan.size.columns, height: plan.size.rows, overflow: "hidden", children: [_jsxs(FrameBand, { rows: header.rows, row: true, regionRef: regionRef("header"), children: [_jsx(Box, { flexGrow: 1, flexShrink: 1, height: header.rows, overflow: "hidden", children: _jsx(PlannedHeader, { data: data, plan: plan, columns: plan.size.columns - (caption?.columns ?? 0) }) }), caption !== undefined && (_jsx(Box, { ref: regionRef("caption"), width: caption.columns, height: caption.rows, flexShrink: 0, overflow: "hidden", children: _jsx(PlannedCaption, { data: data, columns: caption.columns }) }))] }), _jsx(FrameBand, { rows: requiredRegion(plan, "rule").rows, regionRef: regionRef("rule"), children: _jsx(RuleBand, { columns: plan.size.columns }) }), strip !== undefined && (_jsx(FrameBand, { rows: strip.rows, regionRef: regionRef("strip"), children: _jsx(ViewStrip, { selection: railSelection, columns: strip.columns }) })), _jsxs(FrameBand, { rows: body.rows, row: true, children: [rail !== undefined && (_jsx(Box, { ref: regionRef("rail"), width: rail.columns, height: rail.rows, flexShrink: 0, overflow: "hidden", children: setup
|
|
526
|
+
? (_jsx(SectionsBand, { focused: railFocused, selection: railSelection, columns: rail.columns }))
|
|
527
|
+
: (_jsx(RailBand, { data: data, focused: railFocused, selection: railSelection, columns: rail.columns, rows: rail.rows, sidebar: plan.sidebar })) })), _jsx(Box, { ref: regionRef("body"), width: body.columns, height: body.rows, flexShrink: 0, flexDirection: "column", overflow: "hidden", children: setup
|
|
528
|
+
? (_jsx(SetupTabPanel, { data: data, selection: railSelection, focused: contentFocused, keyEntries: keyEntries, help: currentInteraction.help, bodyRows: content.body, bodyColumns: content.bodyColumns }))
|
|
529
|
+
: activeView === "run"
|
|
530
|
+
? (_jsx(LadderRunPanel, { data: data, content: content, compactMeterWidth: content.sideRails ? undefined : 24, interaction: currentInteraction, keyEntries: keyEntries, focused: contentFocused }))
|
|
531
|
+
: (_jsx(RunViewPanel, { data: data, viewId: activeView, interaction: currentInteraction, focused: contentFocused, keyEntries: keyEntries, bodyRows: content.body, bodyColumns: content.bodyColumns })) })] }), _jsx(FrameBand, { rows: requiredRegion(plan, "rule2").rows, regionRef: regionRef("rule2"), children: _jsx(RuleBand, { columns: plan.size.columns }) }), tail !== undefined && (_jsx(FrameBand, { rows: tail.rows, regionRef: regionRef("tail"), children: _jsx(TailBand, { data: data, rows: tail.rows, columns: tail.columns, pendingWrites: plan.content.tail === "pending-writes" }) })), _jsx(FrameBand, { rows: requiredRegion(plan, "status").rows, regionRef: regionRef("status"), children: _jsx(StatusStrip, { items: reportedStatusItems, width: plan.size.columns }) }), _jsx(FrameBand, { rows: requiredRegion(plan, "keybar").rows, regionRef: regionRef("keybar"), children: _jsx(RunKeybar, { entries: keyEntries, width: plan.size.columns, help: currentInteraction.help }) })] }));
|
|
532
|
+
}
|
|
533
|
+
/** Resolve every draw-time input; the plan is planFrame's own return, consumed unmodified. */
|
|
534
|
+
export function planRunCockpitFrame({ data, columns, rows, interaction, keyProjection = projectRunKeyEntries, }) {
|
|
535
|
+
const suppliedInteraction = interaction ?? initialRunInteractionState();
|
|
536
|
+
const suppliedRows = deriveRunViewRows(data, suppliedInteraction.activeView, suppliedInteraction.filterQuery);
|
|
537
|
+
const rowIds = suppliedRows.map((row) => row.id);
|
|
538
|
+
const repaired = reconcileRunInteraction(suppliedInteraction, rowIds);
|
|
539
|
+
const keyEntries = keyProjection({
|
|
540
|
+
interaction: repaired,
|
|
541
|
+
columns: runKeyColumns(columns),
|
|
542
|
+
rowIds: selectableRunViewRowIds(repaired.activeView, rowIds),
|
|
543
|
+
});
|
|
544
|
+
const size = { columns, rows: rows ?? 1 };
|
|
545
|
+
const view = repaired.activeView;
|
|
546
|
+
const state = {
|
|
547
|
+
tab: repaired.tab,
|
|
548
|
+
detail: repaired.opened !== null,
|
|
549
|
+
captionCells: cellWidth(frameCaption(data)),
|
|
550
|
+
};
|
|
551
|
+
const plan = planFrame(size, view, state);
|
|
552
|
+
const content = rows === undefined || plan.kind !== "frame"
|
|
553
|
+
? undefined
|
|
554
|
+
: resolveRegionContent(plan, data, repaired);
|
|
555
|
+
return {
|
|
556
|
+
plan,
|
|
557
|
+
columns,
|
|
558
|
+
rows,
|
|
559
|
+
content,
|
|
560
|
+
planning: rows === undefined || plan.kind !== "frame"
|
|
561
|
+
? undefined
|
|
562
|
+
: { size, view, state },
|
|
563
|
+
interaction: repaired,
|
|
564
|
+
keyEntries,
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
/** The public surface always enters paint through one measured plan. */
|
|
568
|
+
export function RunCockpitFrame({ data, columns, rows, interaction, keyProjection = projectRunKeyEntries, }) {
|
|
569
|
+
const plannedFrame = useMemo(() => planRunCockpitFrame({
|
|
570
|
+
data,
|
|
571
|
+
columns,
|
|
572
|
+
rows,
|
|
573
|
+
interaction,
|
|
574
|
+
keyProjection,
|
|
575
|
+
}), [data, columns, rows, interaction, keyProjection]);
|
|
576
|
+
return (_jsx(RunCockpitFrameFromPlan, { data: data, plannedFrame: plannedFrame }));
|
|
577
|
+
}
|
|
578
|
+
function requiredRegion(plan, id) {
|
|
579
|
+
const region = plan.regions.find((candidate) => candidate.id === id);
|
|
580
|
+
if (region === undefined)
|
|
581
|
+
throw new Error(`planned region ${id} is missing`);
|
|
582
|
+
return region;
|
|
583
|
+
}
|
|
584
|
+
function optionalRegion(plan, id) {
|
|
585
|
+
return plan.regions.find((candidate) => candidate.id === id);
|
|
586
|
+
}
|
|
587
|
+
function assertPlanSpans(plan) {
|
|
588
|
+
let nextRow = 0;
|
|
589
|
+
for (const [id, span] of Object.entries(plan.rowSpans)) {
|
|
590
|
+
const region = requiredRegion(plan, id);
|
|
591
|
+
const fullWidth = id !== "body";
|
|
592
|
+
if (region.row !== nextRow ||
|
|
593
|
+
region.rows !== span ||
|
|
594
|
+
(fullWidth && (region.column !== 0 || region.columns !== plan.size.columns))) {
|
|
595
|
+
throw new Error(`planned row region ${id} has offset/span ${region.row}/${region.rows}, expected ${nextRow}/${span}`);
|
|
596
|
+
}
|
|
597
|
+
nextRow += span;
|
|
598
|
+
}
|
|
599
|
+
if (nextRow !== plan.size.rows) {
|
|
600
|
+
throw new Error(`planned row spans total ${nextRow}, expected ${plan.size.rows}`);
|
|
601
|
+
}
|
|
602
|
+
let nextColumn = 0;
|
|
603
|
+
for (const [id, span] of Object.entries(plan.columnSpans)) {
|
|
604
|
+
const region = requiredRegion(plan, id);
|
|
605
|
+
if (region.column !== nextColumn || region.columns !== span) {
|
|
606
|
+
throw new Error(`planned column region ${id} has offset/span ${region.column}/${region.columns}, expected ${nextColumn}/${span}`);
|
|
607
|
+
}
|
|
608
|
+
nextColumn += span;
|
|
609
|
+
}
|
|
610
|
+
if (nextColumn !== plan.size.columns) {
|
|
611
|
+
throw new Error(`planned column spans total ${nextColumn}, expected ${plan.size.columns}`);
|
|
612
|
+
}
|
|
613
|
+
const body = requiredRegion(plan, "body");
|
|
614
|
+
const rail = optionalRegion(plan, "rail");
|
|
615
|
+
if (rail !== undefined && (rail.row !== body.row || rail.rows !== body.rows)) {
|
|
616
|
+
throw new Error("planned rail region does not share the body row span");
|
|
617
|
+
}
|
|
618
|
+
const caption = optionalRegion(plan, "caption");
|
|
619
|
+
if (caption !== undefined) {
|
|
620
|
+
const header = requiredRegion(plan, "header");
|
|
621
|
+
if (caption.row !== header.row ||
|
|
622
|
+
caption.rows !== header.rows ||
|
|
623
|
+
caption.column + caption.columns !== header.column + header.columns) {
|
|
624
|
+
throw new Error("planned caption region does not refine the header span");
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
const ANSI = /\x1b\[[0-9;?]*[ -/]*[@-~]/gu;
|
|
629
|
+
/** The rows the renderer drew, Ink's transport framing removed: the shipped path appends a newline of its own. */
|
|
630
|
+
function drawnRows(drawnFrame, plannedRows) {
|
|
631
|
+
const rows = drawnFrame.replace(ANSI, "").split("\n");
|
|
632
|
+
return rows.length === plannedRows + 1 && rows.at(-1) === ""
|
|
633
|
+
? rows.slice(0, -1)
|
|
634
|
+
: rows;
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Assert the plan's tiled extents, the bytes production paints, and — given
|
|
638
|
+
* the offsets read off the painted nodes — that every planned region was
|
|
639
|
+
* drawn where the plan puts it.
|
|
640
|
+
*/
|
|
641
|
+
export function assertFrameConformance(plan, drawnFrame, drawnRegions) {
|
|
642
|
+
assertPlanSpans(plan);
|
|
643
|
+
const lines = drawnRows(drawnFrame, plan.size.rows);
|
|
644
|
+
if (lines.length !== plan.size.rows) {
|
|
645
|
+
throw new Error(`drawn frame has ${lines.length} rows, planned ${plan.size.rows} for ${plan.tab}/${plan.view} at ${plan.size.columns} columns`);
|
|
646
|
+
}
|
|
647
|
+
for (const [index, line] of lines.entries()) {
|
|
648
|
+
if (cellWidth(line) > plan.size.columns) {
|
|
649
|
+
throw new Error(`drawn frame row ${index} has ${cellWidth(line)} cells, exceeding planned span ${plan.size.columns} for ${plan.tab}/${plan.view}: ${JSON.stringify(line)}`);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
if (drawnRegions === undefined)
|
|
653
|
+
return;
|
|
654
|
+
// Every planned region is located in the drawn frame, so a redistribution
|
|
655
|
+
// that keeps the plan tiled at unchanged height still fails here.
|
|
656
|
+
for (const region of plan.regions) {
|
|
657
|
+
const drawn = drawnRegions.find((candidate) => candidate.id === region.id);
|
|
658
|
+
if (drawn === undefined) {
|
|
659
|
+
throw new Error(`drawn frame has no region ${region.id}; planned row ${region.row} span ${region.rows} column ${region.column} span ${region.columns} for ${plan.tab}/${plan.view}`);
|
|
660
|
+
}
|
|
661
|
+
if (drawn.row !== region.row ||
|
|
662
|
+
drawn.rows !== region.rows ||
|
|
663
|
+
drawn.column !== region.column ||
|
|
664
|
+
drawn.columns !== region.columns) {
|
|
665
|
+
throw new Error(`drawn region ${region.id} is at row ${drawn.row} span ${drawn.rows} column ${drawn.column} span ${drawn.columns}, planned row ${region.row} span ${region.rows} column ${region.column} span ${region.columns} for ${plan.tab}/${plan.view} at ${plan.size.columns} columns`);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
/** The offsets and spans the renderer actually drew, read off the painted nodes' computed layout. */
|
|
670
|
+
function measuredDrawnRegions(nodes) {
|
|
671
|
+
return [...nodes.entries()].map(([id, node]) => {
|
|
672
|
+
let row = 0;
|
|
673
|
+
let column = 0;
|
|
674
|
+
for (let current = node; current?.parentNode !== undefined; current = current.parentNode) {
|
|
675
|
+
row += current.yogaNode?.getComputedTop() ?? 0;
|
|
676
|
+
column += current.yogaNode?.getComputedLeft() ?? 0;
|
|
677
|
+
}
|
|
678
|
+
return {
|
|
679
|
+
id,
|
|
680
|
+
row: Math.round(row),
|
|
681
|
+
rows: Math.round(node.yogaNode?.getComputedHeight() ?? 0),
|
|
682
|
+
column: Math.round(column),
|
|
683
|
+
columns: Math.round(node.yogaNode?.getComputedWidth() ?? 0),
|
|
684
|
+
};
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
/** The plan's regions measured off the painted nodes; a planned region with no painted node is a refusal. */
|
|
688
|
+
function drawnPlannedRegions(plan, nodes) {
|
|
689
|
+
const missing = plan.regions
|
|
690
|
+
.filter((region) => !nodes.has(region.id))
|
|
691
|
+
.map((region) => region.id);
|
|
692
|
+
if (missing.length > 0) {
|
|
693
|
+
throw new Error(`drawn frame registered no node for planned region(s) ${String(missing)} for ${plan.tab}/${plan.view} at ${plan.size.columns} columns`);
|
|
694
|
+
}
|
|
695
|
+
return measuredDrawnRegions(nodes);
|
|
696
|
+
}
|
|
697
|
+
/** planFrame's own regions for the inputs the plan was planned from, so a moved span is a refusal. */
|
|
698
|
+
function authoritativeRegions(planning) {
|
|
699
|
+
const authoritative = planFrame(planning.size, planning.view, planning.state);
|
|
700
|
+
return authoritative.kind === "frame" ? authoritative.regions : undefined;
|
|
701
|
+
}
|
|
702
|
+
/** Production paint consumes only resolved plan inputs and fails closed with
|
|
703
|
+
* the same conformance assertion the acceptance suite runs. */
|
|
704
|
+
export function RunCockpitFrameFromPlan({ data, plannedFrame, }) {
|
|
705
|
+
if (plannedFrame.plan.kind === "frame") {
|
|
706
|
+
assertPlanSpans(plannedFrame.plan);
|
|
707
|
+
}
|
|
708
|
+
const { stdout } = useStdout();
|
|
709
|
+
const { exit } = useApp();
|
|
710
|
+
const regionNodes = useRef(new Map());
|
|
711
|
+
const regionRefCallbacks = useRef(new Map());
|
|
712
|
+
useInsertionEffect(() => {
|
|
713
|
+
const plan = plannedFrame.plan;
|
|
714
|
+
if (plan.kind !== "frame" || plannedFrame.content === undefined)
|
|
715
|
+
return;
|
|
716
|
+
const originalWrite = stdout.write;
|
|
717
|
+
const writeFrame = ((chunk, ...args) => {
|
|
718
|
+
const bytes = typeof chunk === "string"
|
|
719
|
+
? chunk
|
|
720
|
+
: Buffer.from(chunk).toString("utf8");
|
|
721
|
+
if (bytes.replace(ANSI, "").trim().length === 0) {
|
|
722
|
+
return Reflect.apply(originalWrite, stdout, [chunk, ...args]);
|
|
723
|
+
}
|
|
724
|
+
// Byte spans refuse before the frame leaves; the region check measures the nodes its commit painted.
|
|
725
|
+
try {
|
|
726
|
+
assertFrameConformance(plan, bytes);
|
|
727
|
+
}
|
|
728
|
+
catch (error) {
|
|
729
|
+
queueMicrotask(() => exit(error));
|
|
730
|
+
return true;
|
|
731
|
+
}
|
|
732
|
+
const planning = plannedFrame.planning;
|
|
733
|
+
queueMicrotask(() => {
|
|
734
|
+
try {
|
|
735
|
+
if (planning !== undefined) {
|
|
736
|
+
const planned = authoritativeRegions(planning);
|
|
737
|
+
if (planned !== undefined) {
|
|
738
|
+
assertFrameConformance(plan, bytes, planned);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
assertFrameConformance(plan, bytes, drawnPlannedRegions(plan, regionNodes.current));
|
|
742
|
+
}
|
|
743
|
+
catch (error) {
|
|
744
|
+
exit(error);
|
|
745
|
+
}
|
|
746
|
+
});
|
|
747
|
+
return Reflect.apply(originalWrite, stdout, [chunk, ...args]);
|
|
748
|
+
});
|
|
749
|
+
stdout.write = writeFrame;
|
|
750
|
+
return () => {
|
|
751
|
+
if (stdout.write === writeFrame)
|
|
752
|
+
stdout.write = originalWrite;
|
|
192
753
|
};
|
|
193
|
-
|
|
754
|
+
}, [plannedFrame, stdout, exit]);
|
|
755
|
+
if (plannedFrame.plan.kind === "plain" && plannedFrame.rows !== undefined) {
|
|
756
|
+
return _jsx(PlainFallbackPaint, { data: data, size: plannedFrame.plan.size });
|
|
757
|
+
}
|
|
758
|
+
return (_jsx(RunCockpitPaint, { data: data, columns: plannedFrame.columns, plan: plannedFrame.plan.kind === "frame" ? plannedFrame.plan : undefined, content: plannedFrame.content, interaction: plannedFrame.interaction, keyProjection: () => plannedFrame.keyEntries, onRegion: (id) => {
|
|
759
|
+
let callback = regionRefCallbacks.current.get(id);
|
|
760
|
+
if (callback === undefined) {
|
|
761
|
+
callback = (node) => {
|
|
762
|
+
if (node === null)
|
|
763
|
+
regionNodes.current.delete(id);
|
|
764
|
+
else
|
|
765
|
+
regionNodes.current.set(id, node);
|
|
766
|
+
};
|
|
767
|
+
regionRefCallbacks.current.set(id, callback);
|
|
768
|
+
}
|
|
769
|
+
return callback;
|
|
770
|
+
} }));
|
|
194
771
|
}
|