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