tickmarkr 1.81.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.
Files changed (49) hide show
  1. package/README.md +4 -4
  2. package/dist/brand.d.ts +4 -1
  3. package/dist/brand.js +46 -7
  4. package/dist/cli/commands/approve.js +53 -8
  5. package/dist/cli/commands/plan.js +17 -2
  6. package/dist/cli/commands/report.js +15 -6
  7. package/dist/cli/commands/ui.js +13 -1
  8. package/dist/cli/index.d.ts +1 -1
  9. package/dist/cli/index.js +1 -1
  10. package/dist/compile/collateral.d.ts +19 -0
  11. package/dist/compile/collateral.js +90 -0
  12. package/dist/compile/index.js +18 -4
  13. package/dist/drivers/herdr.d.ts +1 -0
  14. package/dist/drivers/herdr.js +19 -0
  15. package/dist/drivers/types.d.ts +1 -0
  16. package/dist/gates/acceptance.js +50 -8
  17. package/dist/gates/llm.js +34 -19
  18. package/dist/gates/review.d.ts +9 -1
  19. package/dist/gates/review.js +173 -7
  20. package/dist/gates/run-gates.d.ts +1 -0
  21. package/dist/gates/run-gates.js +18 -1
  22. package/dist/report/bundle.d.ts +6 -1
  23. package/dist/report/bundle.js +13 -3
  24. package/dist/run/daemon.d.ts +5 -0
  25. package/dist/run/daemon.js +188 -27
  26. package/dist/run/journal.d.ts +5 -0
  27. package/dist/run/journal.js +71 -1
  28. package/dist/tui/cockpit/capture.d.ts +107 -1
  29. package/dist/tui/cockpit/capture.js +287 -10
  30. package/dist/tui/cockpit/components.d.ts +12 -46
  31. package/dist/tui/cockpit/components.js +48 -48
  32. package/dist/tui/cockpit/derive.d.ts +73 -1
  33. package/dist/tui/cockpit/derive.js +211 -15
  34. package/dist/tui/cockpit/keys.d.ts +114 -19
  35. package/dist/tui/cockpit/keys.js +413 -44
  36. package/dist/tui/cockpit/layout.d.ts +162 -0
  37. package/dist/tui/cockpit/layout.js +229 -0
  38. package/dist/tui/cockpit/live.d.ts +52 -3
  39. package/dist/tui/cockpit/live.js +216 -40
  40. package/dist/tui/cockpit/run-cockpit.d.ts +64 -8
  41. package/dist/tui/cockpit/run-cockpit.js +686 -163
  42. package/dist/tui/cockpit/setup-cockpit.d.ts +146 -1
  43. package/dist/tui/cockpit/setup-cockpit.js +337 -5
  44. package/dist/tui/cockpit/views.d.ts +72 -0
  45. package/dist/tui/cockpit/views.js +56 -0
  46. package/dist/tui/cockpit/width.d.ts +105 -0
  47. package/dist/tui/cockpit/width.js +276 -0
  48. package/fixtures/speckit-sample/tasks.md +2 -2
  49. package/package.json +3 -1
@@ -1,13 +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, KEYBAR_KEYS, Keybar, Panel, PANEL_CHROME_ROWS, ProgressMeter, StatTile, StatusStrip, } from "./components.js";
6
- import { RUN_PANEL_FOCUS_ORDER, RUN_KEY_BINDINGS, } from "./keys.js";
7
- import { resolveCockpitLayout, } from "./layout.js";
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";
8
11
  export { deriveRunCockpitData } from "./derive.js";
9
12
  export { PANEL_CHROME_ROWS } from "./components.js";
10
- const APPROVED_SIDE_RAIL_COLUMN_FLOOR = 80;
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
+ }
11
19
  const NARROW_HEADER_ALLOCATION = {
12
20
  chromeColumns: 0,
13
21
  minimumReadableColumns: 1,
@@ -19,52 +27,15 @@ const NARROW_HEADER_ALLOCATION = {
19
27
  * rows it will really occupy instead of a hand-tuned constant.
20
28
  */
21
29
  export function wrappedRows(text, columns) {
22
- if (columns < 1)
23
- return 1;
24
- let rows = 1;
25
- let used = 0;
26
- for (const [index, word] of text.split(" ").entries()) {
27
- // ponytail: code points, not display columns — every glyph the cockpit
28
- // draws is one column wide. Reach for a width table only if it stops being.
29
- const length = [...word].length;
30
- if (index !== 0) {
31
- if (used >= columns) {
32
- rows += 1;
33
- used = 0;
34
- }
35
- used += 1;
36
- }
37
- if (length > columns) {
38
- const startsThisLine = 1 + Math.floor((length - (columns - used) - 1) / columns);
39
- if (Math.floor((length - 1) / columns) < startsThisLine) {
40
- rows += 1;
41
- used = 0;
42
- }
43
- for (let taken = 0; taken < length; taken += 1) {
44
- if (used < columns)
45
- used += 1;
46
- else {
47
- rows += 1;
48
- used = 1;
49
- }
50
- }
51
- continue;
52
- }
53
- if (used > 0 && used + length > columns) {
54
- rows += 1;
55
- used = 0;
56
- }
57
- used += length;
58
- }
59
- return rows;
30
+ return wrapCells(text, columns).length;
60
31
  }
61
32
  /** The rows a bordered panel occupies when its body wraps into `columns`. */
62
33
  export function panelRows(lines, columns) {
63
- return lines.reduce((rows, line) => rows + composeBandLine(line, columns).length, PANEL_CHROME_ROWS);
34
+ return lines.reduce((rows, line) => rows + wrapCells(line, columns, { continuationPrefix: "↳ " }).length, PANEL_CHROME_ROWS);
64
35
  }
65
36
  function IdentityHeader({ data, columns, secondary = true, }) {
66
37
  const lockup = PLAIN_COMPACT_LOCKUP.split("\n");
67
- if (columns < APPROVED_SIDE_RAIL_COLUMN_FLOOR) {
38
+ if (!runSideRailVisible(columns)) {
68
39
  const primary = [
69
40
  { title: "", lines: [lockup[0] ?? "tickmarkr"] },
70
41
  {
@@ -89,160 +60,712 @@ function NarrowHeaderBand({ columns, content, rightStrong = false, }) {
89
60
  const widths = allocateBandColumns(columns, content, NARROW_HEADER_ALLOCATION);
90
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" }) })] }));
91
62
  }
92
- function NavigationPanel({ focused = false }) {
93
- return (_jsxs(Panel, { title: "VIEWS", width: 16, focused: focused, children: [_jsxs(BodyText, { emphasis: "strong", children: [GLYPHS.pointer, " Run"] }), _jsx(BodyText, { children: " Tasks" }), _jsx(BodyText, { children: " Gates" }), _jsx(BodyText, { children: " Journal" }), _jsx(BodyText, { children: " Fleet" })] }));
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}`;
94
69
  }
95
- function KeysPanel({ focused = false }) {
96
- return (_jsx(Panel, { title: "KEYS", width: 16, focused: focused, children: KEYBAR_KEYS.run.map((item) => (_jsxs(Box, { children: [_jsx(BodyText, { emphasis: "strong", children: item.key }), _jsxs(BodyText, { children: [" ", item.label] })] }, item.key))) }));
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}`;
97
76
  }
98
- /** The journal rows an interaction state leaves visible. */
99
- function visibleJournalRows(rows, interaction) {
100
- const query = interaction?.filterQuery.trim().toLowerCase() ?? "";
101
- if (query === "")
102
- return rows;
103
- return rows.filter((row) => row.text.toLowerCase().includes(query));
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)) }));
104
80
  }
105
- function SizedJournalPanel({ rows, bodyRows, title, selection, }) {
106
- const visible = rows.slice(0, bodyRows);
107
- const emptyRows = Math.max(0, bodyRows - visible.length);
108
- const journal = JournalRowPanel({ rows: visible, title, selection });
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
+ });
109
219
  const journalProps = journal.props;
110
220
  return cloneElement(journal, {}, journalProps.children, emptyRows > 0 ? _jsx(Box, { height: emptyRows }, "journal-space") : null);
111
221
  }
112
222
  /** A body panel (help overlay, opened event) padded to the journal's rows. */
113
- function SizedLinesPanel({ title, lines, bodyRows, }) {
114
- const visible = lines.slice(0, bodyRows);
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);
115
228
  const emptyRows = Math.max(0, bodyRows - visible.length);
116
- return (_jsxs(Panel, { title: title, children: [visible.map((line, index) => (_jsx(BodyText, { children: line }, `${index}:${line}`))), emptyRows > 0 && _jsx(Box, { height: emptyRows })] }));
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 })] }));
117
230
  }
118
231
  /** The lines the help overlay draws: every advertised key and its promise. */
119
- function helpLines() {
120
- return RUN_KEY_BINDINGS.map((binding) => `${binding.key} ${binding.label}`);
232
+ function helpLines(entries) {
233
+ return entries.map(({ key, label }) => `${key} ${label}`);
121
234
  }
122
235
  /** The lines the opened-event view draws for one journal row. */
123
236
  function eventLines(row) {
124
237
  return [`${row.time} · ${row.state}`, row.text];
125
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
+ }
126
264
  /**
127
265
  * What the journal's slot of the RUN panel draws under an interaction state:
128
- * the help overlay while ? is up, the opened event while ⏎'s choice stands,
129
- * otherwise the journal itself — narrowed by the filter, marked at the
130
- * selection, titled with the open / prompt.
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.
131
268
  */
132
- function JournalBody({ data, interaction, bodyRows, }) {
133
- const rows = visibleJournalRows(data.journalRows, interaction);
269
+ function JournalBody({ data, interaction, bodyRows, keyEntries, }) {
270
+ const rows = deriveRunViewRows(data, "journal");
134
271
  if (interaction?.help === true) {
135
- return _jsx(SizedLinesPanel, { title: "HELP", lines: helpLines(), bodyRows: bodyRows });
272
+ return (_jsx(SizedLinesPanel, { title: "HELP", lines: helpLines(keyEntries), bodyRows: bodyRows }));
136
273
  }
137
- const opened = interaction?.opened ?? null;
138
- if (opened !== null && opened < rows.length) {
139
- return (_jsx(SizedLinesPanel, { title: "EVENT", lines: eventLines(rows[opened]), bodyRows: bodyRows }));
140
- }
141
- return (_jsx(SizedJournalPanel, { rows: rows, bodyRows: bodyRows, title: interaction?.filterPrompt === true
142
- ? `JOURNAL /${interaction.filterQuery}`
143
- : undefined, selection: interaction === undefined || interaction.selection < 0
144
- ? undefined
145
- : interaction.selection }));
274
+ return (_jsx(SizedJournalPanel, { rows: rows, bodyRows: bodyRows, selection: null }));
146
275
  }
147
276
  function RunStats({ data, mode, }) {
277
+ if (mode === "none")
278
+ return _jsx(_Fragment, {});
148
279
  if (mode === "summary") {
149
280
  return (_jsx(BodyText, { children: `tasks ${data.tasks.done}/${data.tasks.total} · gates ${data.gates.passed}/${data.gates.total} · pass ${data.passRate.value}%` }));
150
281
  }
151
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 })] }));
152
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
+ }
153
366
  function ApprovedProgress({ data, caption, }) {
154
367
  return (_jsxs(Panel, { title: "PROGRESS", children: [_jsx(ProgressMeter, { value: data.progress, width: 28 }), caption && _jsx(BodyText, { emphasis: "dim", children: data.progressCaption })] }));
155
368
  }
156
369
  function CompactProgress({ data, caption, meterWidth = 28, }) {
157
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 })] }));
158
371
  }
159
- function pairedTextRows(left, right, columns) {
160
- const leftWidth = [...left].length;
161
- const rightWidth = [...right].length;
162
- const required = leftWidth + rightWidth;
163
- if (required <= columns)
164
- return 1;
165
- const leftColumns = Math.max(1, Math.floor((Math.max(1, columns) * leftWidth) / required));
166
- const rightColumns = Math.max(1, Math.max(1, columns) - leftColumns);
167
- return Math.max(wrappedRows(left, leftColumns), wrappedRows(right, rightColumns));
168
- }
169
- function narrowHeaderRows(left, right, columns) {
170
- const content = [
171
- { title: "", lines: [left] },
172
- { title: "", lines: [right] },
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),
173
474
  ];
174
- const widths = allocateBandColumns(columns, content, NARROW_HEADER_ALLOCATION);
175
- 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}`))) }));
176
476
  }
177
- function headerRowsFor(layout, data, columns) {
178
- const lockup = PLAIN_COMPACT_LOCKUP.split("\n");
179
- const measure = columns < APPROVED_SIDE_RAIL_COLUMN_FLOOR
180
- ? narrowHeaderRows
181
- : pairedTextRows;
182
- const primaryRows = measure(lockup[0] ?? "tickmarkr", `v${data.binaryVersion} · binary ${GLYPHS.pass}`, columns);
183
- if (!layout.elements.secondaryHeader)
184
- return primaryRows;
185
- return primaryRows + measure(lockup[1] ?? "tickmarkr", `${data.runId} · ${data.branch} ${data.status} · ${data.elapsed}`, columns);
186
- }
187
- function compactJournalRows(rows, layout, data, columns) {
188
- const statRows = layout.stats.mode === "tiles" ? 5 : 1;
189
- const progressRows = layout.elements.progressBar
190
- ? 1 + (layout.elements.progressCaption ? 1 : 0)
191
- : 0;
192
- const fixedRows = headerRowsFor(layout, data, columns)
193
- + 2 // permanent status strip and keybar
194
- + 3 // outer RUN panel border and title
195
- + statRows
196
- + progressRows
197
- + 3; // journal border and title
198
- return Math.max(1, Math.min(layout.journalRows, Math.floor(rows) - fixedRows));
199
- }
200
- function LadderRunPanel({ data, layout, columns, rows, compactMeterWidth, interaction, focused = true, }) {
201
- const compactBodyRows = compactJournalRows(rows, layout, data, columns);
202
- // The approved bordered progress panel has the same meter/caption as the
203
- // compact form plus its border, title row and closing border. It is restored
204
- // only after the resolved ladder has retained every full-arrangement element
205
- // and the journal can pay those three chrome rows without disappearing.
206
- const approvedProgressChromeRows = 3;
207
- const approvedArrangement = layout.elements.secondaryHeader
208
- && layout.elements.progressBar
209
- && layout.elements.progressCaption
210
- && layout.stats.mode === "tiles"
211
- && compactBodyRows > approvedProgressChromeRows;
212
- const journalBodyRows = approvedArrangement
213
- ? compactBodyRows - approvedProgressChromeRows
214
- : compactBodyRows;
215
- return (_jsxs(Panel, { title: "RUN", focused: focused, flexGrow: 1, children: [_jsx(RunStats, { data: data, mode: layout.stats.mode }), layout.elements.progressBar && (approvedArrangement
216
- ? (_jsx(ApprovedProgress, { data: data, caption: layout.elements.progressCaption }))
217
- : (_jsx(CompactProgress, { data: data, caption: layout.elements.progressCaption, meterWidth: compactMeterWidth }))), _jsx(JournalBody, { data: data, interaction: interaction, bodyRows: journalBodyRows })] }));
218
- }
219
- function LegacyRunPanel({ data, interaction, focused = true, }) {
220
- const rows = visibleJournalRows(data.journalRows, interaction);
221
- 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 })] }));
222
- }
223
- export function RunCockpitFrame({ data, columns, rows, interaction, }) {
224
- const focus = interaction === undefined
225
- ? 0
226
- : interaction.panel % RUN_PANEL_FOCUS_ORDER.length;
227
- const focusedPanel = RUN_PANEL_FOCUS_ORDER[focus] ?? "RUN";
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
+ });
228
495
  // Follow mode reports its on state as a status-strip item carrying the
229
496
  // brand's reserved active-toggle glyph; off it draws nothing, so the pinned
230
497
  // frame is untouched.
231
- const statusItems = interaction?.follow === true
232
- ? [...data.statusItems, { state: "active", text: "Follow" }]
233
- : data.statusItems;
234
- if (rows === undefined) {
235
- return (_jsxs(Box, { flexDirection: "column", width: columns, children: [_jsx(IdentityHeader, { data: data, columns: columns }), _jsxs(Box, { flexDirection: "row", gap: 1, children: [_jsx(NavigationPanel, { focused: focusedPanel === "VIEWS" }), _jsx(LegacyRunPanel, { data: data, interaction: interaction, focused: focusedPanel === "RUN" }), _jsx(KeysPanel, { focused: focusedPanel === "KEYS" })] }), _jsx(StatusStrip, { items: statusItems, width: columns }), _jsx(Keybar, { surface: "run", width: columns })] }));
236
- }
237
- const layout = resolveCockpitLayout(columns, rows);
238
- if (layout.renderer === "plain")
239
- return _jsx(_Fragment, {});
240
- const showSideRails = columns >= APPROVED_SIDE_RAIL_COLUMN_FLOOR;
241
- const contentLayout = showSideRails
242
- ? layout
243
- : {
244
- ...layout,
245
- stats: { ...layout.stats, mode: "summary", rows: 1 },
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),
246
684
  };
247
- return (_jsxs(Box, { flexDirection: "column", width: columns, children: [_jsx(IdentityHeader, { data: data, columns: columns, secondary: layout.elements.secondaryHeader }), _jsxs(Box, { flexDirection: "row", gap: showSideRails ? 1 : 0, children: [showSideRails && _jsx(NavigationPanel, { focused: focusedPanel === "VIEWS" }), _jsx(LadderRunPanel, { data: data, layout: contentLayout, columns: columns, rows: rows, compactMeterWidth: showSideRails ? undefined : 24, interaction: interaction, focused: focusedPanel === "RUN" }), showSideRails && _jsx(KeysPanel, { focused: focusedPanel === "KEYS" })] }), _jsx(StatusStrip, { items: statusItems, width: columns }), _jsx(Keybar, { surface: "run", width: columns })] }));
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;
753
+ };
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
+ } }));
248
771
  }