tickmarkr 1.79.0 → 1.81.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.
@@ -0,0 +1,248 @@
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";
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";
8
+ export { deriveRunCockpitData } from "./derive.js";
9
+ export { PANEL_CHROME_ROWS } from "./components.js";
10
+ const APPROVED_SIDE_RAIL_COLUMN_FLOOR = 80;
11
+ const NARROW_HEADER_ALLOCATION = {
12
+ chromeColumns: 0,
13
+ minimumReadableColumns: 1,
14
+ };
15
+ /**
16
+ * How many rows one line of body text occupies once the renderer wraps it into
17
+ * `columns`. This mirrors the renderer's own word wrap — greedy, and breaking a
18
+ * word that cannot fit on a line of its own — so a frame can charge a panel the
19
+ * rows it will really occupy instead of a hand-tuned constant.
20
+ */
21
+ 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;
60
+ }
61
+ /** The rows a bordered panel occupies when its body wraps into `columns`. */
62
+ export function panelRows(lines, columns) {
63
+ return lines.reduce((rows, line) => rows + composeBandLine(line, columns).length, PANEL_CHROME_ROWS);
64
+ }
65
+ function IdentityHeader({ data, columns, secondary = true, }) {
66
+ const lockup = PLAIN_COMPACT_LOCKUP.split("\n");
67
+ if (columns < APPROVED_SIDE_RAIL_COLUMN_FLOOR) {
68
+ const primary = [
69
+ { title: "", lines: [lockup[0] ?? "tickmarkr"] },
70
+ {
71
+ title: "",
72
+ lines: [`v${data.binaryVersion} · binary ${GLYPHS.pass}`],
73
+ },
74
+ ];
75
+ const secondaryBand = [
76
+ { title: "", lines: [lockup[1] ?? "tickmarkr"] },
77
+ {
78
+ title: "",
79
+ lines: [
80
+ `${data.runId} · ${data.branch} ${data.status} · ${data.elapsed}`,
81
+ ],
82
+ },
83
+ ];
84
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsx(NarrowHeaderBand, { columns: columns, content: primary, rightStrong: true }), secondary && (_jsx(NarrowHeaderBand, { columns: columns, content: secondaryBand }))] }));
85
+ }
86
+ return (_jsxs(Box, { flexDirection: "column", children: [_jsxs(Box, { justifyContent: "space-between", children: [_jsx(BodyText, { emphasis: "strong", children: lockup[0] ?? "tickmarkr" }), _jsxs(BodyText, { emphasis: "strong", children: ["v", data.binaryVersion, " \u00B7 binary ", GLYPHS.pass] })] }), secondary && (_jsxs(Box, { justifyContent: "space-between", children: [_jsx(BodyText, { emphasis: "strong", children: lockup[1] ?? "tickmarkr" }), _jsxs(BodyText, { children: [data.runId, " \u00B7 ", data.branch, " ", data.status, " \u00B7 ", data.elapsed] })] }))] }));
87
+ }
88
+ function NarrowHeaderBand({ columns, content, rightStrong = false, }) {
89
+ const widths = allocateBandColumns(columns, content, NARROW_HEADER_ALLOCATION);
90
+ 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
+ }
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" })] }));
94
+ }
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))) }));
97
+ }
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));
104
+ }
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 });
109
+ const journalProps = journal.props;
110
+ return cloneElement(journal, {}, journalProps.children, emptyRows > 0 ? _jsx(Box, { height: emptyRows }, "journal-space") : null);
111
+ }
112
+ /** 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);
115
+ 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 })] }));
117
+ }
118
+ /** 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}`);
121
+ }
122
+ /** The lines the opened-event view draws for one journal row. */
123
+ function eventLines(row) {
124
+ return [`${row.time} · ${row.state}`, row.text];
125
+ }
126
+ /**
127
+ * 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.
131
+ */
132
+ function JournalBody({ data, interaction, bodyRows, }) {
133
+ const rows = visibleJournalRows(data.journalRows, interaction);
134
+ 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 }));
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 }));
146
+ }
147
+ function RunStats({ data, mode, }) {
148
+ if (mode === "summary") {
149
+ return (_jsx(BodyText, { children: `tasks ${data.tasks.done}/${data.tasks.total} · gates ${data.gates.passed}/${data.gates.total} · pass ${data.passRate.value}%` }));
150
+ }
151
+ 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
+ }
153
+ function ApprovedProgress({ data, caption, }) {
154
+ return (_jsxs(Panel, { title: "PROGRESS", children: [_jsx(ProgressMeter, { value: data.progress, width: 28 }), caption && _jsx(BodyText, { emphasis: "dim", children: data.progressCaption })] }));
155
+ }
156
+ function CompactProgress({ data, caption, meterWidth = 28, }) {
157
+ 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
+ }
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] },
173
+ ];
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));
176
+ }
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";
228
+ // Follow mode reports its on state as a status-strip item carrying the
229
+ // brand's reserved active-toggle glyph; off it draws nothing, so the pinned
230
+ // 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 },
246
+ };
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 })] }));
248
+ }
@@ -0,0 +1,44 @@
1
+ import { type ReactElement } from "react";
2
+ import { type TickmarkrConfig } from "../../config/config.js";
3
+ import { type ComponentState, type JournalRow } from "./components.js";
4
+ import type { DemoCaptures } from "./demo.js";
5
+ import { type SetupInteractionState, type SetupKeyBinding } from "./keys.js";
6
+ export type SetupCockpitData = {
7
+ readonly binaryVersion: string;
8
+ readonly harnesses: readonly {
9
+ readonly id: string;
10
+ readonly version: string;
11
+ readonly modelCount: number;
12
+ readonly state: ComponentState;
13
+ readonly stateWord: string;
14
+ }[];
15
+ readonly deniedChannels: readonly {
16
+ readonly channel: string;
17
+ readonly reason: string;
18
+ }[];
19
+ readonly counts: {
20
+ readonly found: number;
21
+ readonly authenticated: number;
22
+ readonly routable: number;
23
+ };
24
+ readonly config: TickmarkrConfig;
25
+ readonly overlayDiff: string;
26
+ readonly stagedChanges: readonly string[];
27
+ readonly reviewRows: readonly JournalRow[];
28
+ readonly overlayTarget: string;
29
+ };
30
+ export declare function deriveSetupCockpitData(captures: DemoCaptures, binaryVersion: string): SetupCockpitData;
31
+ /**
32
+ * The contracted ladder (§5.4) governs the elements every surface shares. The
33
+ * detail panels below it are the setup body, so they surrender first, from the
34
+ * bottom of this list upward: what a step is *about* outlives what merely
35
+ * describes the fleet.
36
+ */
37
+ export declare const SETUP_DETAIL_ORDER: readonly ["detected", "consultsAndReviewers", "overlayDiff", "fleetAndGates"];
38
+ export declare function SetupCockpitFrame({ data, columns, rows, interaction, bindings, }: {
39
+ data: SetupCockpitData;
40
+ columns: number;
41
+ rows?: number;
42
+ interaction?: SetupInteractionState;
43
+ bindings?: readonly SetupKeyBinding[];
44
+ }): ReactElement;