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,532 @@
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 { parse } from "yaml";
5
+ import { BANNER, GLYPHS, PLAIN_BANNER, } from "../../brand.js";
6
+ import { DEFAULT_CONFIG, INTEGRITY_FLOOR_SHAPES, TIER_RANK, TickmarkrConfigSchema, unifiedYamlDiff, } from "../../config/config.js";
7
+ import { SHAPES } from "../../graph/schema.js";
8
+ import { allocateBandColumns, BandLines, BodyText, CockpitGrid, composeBandLine, JournalRowPanel, measureBandRows, Panel, StateGlyph, StatTile, StatusStrip, } from "./components.js";
9
+ import { FULL_JOURNAL_ROWS, resolveCockpitLayout, } from "./layout.js";
10
+ import { deriveRunCockpitData, PANEL_CHROME_ROWS, panelRows, wrappedRows, } from "./run-cockpit.js";
11
+ import { initialSetupInteractionState, keybarEntries, SETUP_PANEL_FOCUS_ORDER, SURFACE_KEY_BINDINGS, } from "./keys.js";
12
+ const APPROVED_SIDE_RAIL_COLUMN_FLOOR = 80;
13
+ const setupAdditionalBindings = (bindings) => bindings.filter((binding) => !SURFACE_KEY_BINDINGS.run.some((runBinding) => runBinding.key === binding.key));
14
+ function deepMerge(base, overlay) {
15
+ if (overlay === undefined || overlay === null)
16
+ return base;
17
+ if (Array.isArray(base)
18
+ || Array.isArray(overlay)
19
+ || typeof base !== "object"
20
+ || typeof overlay !== "object"
21
+ || base === null) {
22
+ return overlay;
23
+ }
24
+ const merged = {
25
+ ...base,
26
+ };
27
+ for (const [key, value] of Object.entries(overlay)) {
28
+ if (value === null) {
29
+ delete merged[key];
30
+ continue;
31
+ }
32
+ merged[key] = key in merged
33
+ ? deepMerge(merged[key], value)
34
+ : deepMerge({}, value);
35
+ }
36
+ return merged;
37
+ }
38
+ function floorEdits(layers) {
39
+ const explicit = new Set();
40
+ const tombstoned = new Set();
41
+ for (const layer of layers) {
42
+ const floors = layer?.routing?.floors;
43
+ if (!floors || Array.isArray(floors))
44
+ continue;
45
+ for (const [shape, value] of Object.entries(floors)) {
46
+ if (value === null) {
47
+ explicit.delete(shape);
48
+ tombstoned.add(shape);
49
+ }
50
+ else {
51
+ tombstoned.delete(shape);
52
+ explicit.add(shape);
53
+ }
54
+ }
55
+ }
56
+ return { explicit, tombstoned };
57
+ }
58
+ function lowerTier(tier) {
59
+ return tier === "frontier" ? "mid" : "cheap";
60
+ }
61
+ function integrityMinimum(shape) {
62
+ return INTEGRITY_FLOOR_SHAPES.includes(shape)
63
+ ? "frontier"
64
+ : "cheap";
65
+ }
66
+ function presetFloor(mode, shape, base) {
67
+ if (mode === "partner-led")
68
+ return "frontier";
69
+ if (mode === "risk-based")
70
+ return base;
71
+ const lowered = lowerTier(base);
72
+ const minimum = integrityMinimum(shape);
73
+ return TIER_RANK[lowered] >= TIER_RANK[minimum] ? lowered : minimum;
74
+ }
75
+ function resolveCapturedConfig(captures) {
76
+ const globalLayer = parse(captures.config.global.raw);
77
+ const repoLayer = parse(captures.config.repo.raw);
78
+ const parsed = TickmarkrConfigSchema.parse(deepMerge(deepMerge(structuredClone(DEFAULT_CONFIG), globalLayer), repoLayer));
79
+ const mode = parsed.routing.mode ?? "risk-based";
80
+ const edits = floorEdits([globalLayer, repoLayer]);
81
+ for (const shape of SHAPES) {
82
+ if (edits.tombstoned.has(shape) || edits.explicit.has(shape))
83
+ continue;
84
+ parsed.routing.floors[shape] = presetFloor(mode, shape, DEFAULT_CONFIG.routing.floors[shape]);
85
+ }
86
+ if (mode === "partner-led") {
87
+ parsed.routing.explore = { ...parsed.routing.explore, mode: "off" };
88
+ }
89
+ return parsed;
90
+ }
91
+ function stagedPaths(value, path = []) {
92
+ if (value === null
93
+ || Array.isArray(value)
94
+ || typeof value !== "object") {
95
+ return path.length > 0 ? [path.join(".")] : [];
96
+ }
97
+ return Object.entries(value).flatMap(([key, child]) => stagedPaths(child, [...path, key]));
98
+ }
99
+ function deniedReason(reason) {
100
+ if (!reason)
101
+ return "channel denied access";
102
+ return reason.match(/Invalid params: "[^"]+"/)?.[0]
103
+ ?? reason.split(/\.\s+Run\b/, 1)[0]
104
+ ?? reason;
105
+ }
106
+ export function deriveSetupCockpitData(captures, binaryVersion) {
107
+ const harnessHealth = Object.entries(captures.doctor.data)
108
+ .filter(([id]) => id !== "autoPrefer")
109
+ .map(([id, raw]) => [id, raw]);
110
+ const harnesses = harnessHealth.map(([id, health]) => {
111
+ const models = new Set([
112
+ ...(health.models ?? []),
113
+ ...Object.keys(health.modelAuth ?? {}),
114
+ ]);
115
+ const state = health.installed !== true
116
+ ? "inactive"
117
+ : health.authed === true
118
+ ? "pass"
119
+ : "fail";
120
+ return {
121
+ id,
122
+ version: health.version ?? "not installed",
123
+ modelCount: models.size,
124
+ state,
125
+ stateWord: state === "pass" ? "authed"
126
+ : state === "inactive" ? "missing"
127
+ : "unauthed",
128
+ };
129
+ });
130
+ const deniedChannels = harnessHealth.flatMap(([id, health]) => Object.entries(health.modelAuth ?? {}).flatMap(([model, verdict]) => verdict.authed === false
131
+ ? [{
132
+ channel: `${id}:${model}`,
133
+ reason: deniedReason(verdict.reason),
134
+ }]
135
+ : []));
136
+ const authenticated = harnessHealth.filter(([, health]) => health.installed === true && health.authed === true).length;
137
+ const routable = harnessHealth.filter(([, health]) => health.installed === true
138
+ && health.authed === true
139
+ && Object.values(health.modelAuth ?? {}).some((model) => model.authed === true)).length;
140
+ const repoLayer = parse(captures.config.repo.raw);
141
+ const reviewSource = captures.journals.find((capture) => capture.fileName === "run-20260724-231138.journal.jsonl");
142
+ if (!reviewSource)
143
+ throw new Error("healthy cockpit capture is missing");
144
+ return {
145
+ binaryVersion,
146
+ harnesses,
147
+ deniedChannels,
148
+ counts: {
149
+ found: harnesses.length,
150
+ authenticated,
151
+ routable,
152
+ },
153
+ config: resolveCapturedConfig(captures),
154
+ overlayDiff: unifiedYamlDiff(captures.config.global.raw, captures.config.repo.raw, captures.config.repo.fileName),
155
+ stagedChanges: stagedPaths(repoLayer),
156
+ reviewRows: deriveRunCockpitData(reviewSource, binaryVersion).journalRows,
157
+ overlayTarget: captures.config.repo.fileName,
158
+ };
159
+ }
160
+ function identityBannerLines() {
161
+ return (process.env.NO_COLOR === undefined ? BANNER : PLAIN_BANNER).trimEnd().split("\n");
162
+ }
163
+ function identityVersionLines(data, interaction) {
164
+ return [
165
+ `v${data.binaryVersion} · binary ${GLYPHS.pass}`,
166
+ `setup · step ${interaction?.step ?? 2}/6`,
167
+ ];
168
+ }
169
+ function IdentityHeader({ data, secondary = true, interaction, }) {
170
+ const mark = identityBannerLines();
171
+ const [version, step] = identityVersionLines(data, interaction);
172
+ return (_jsxs(Box, { justifyContent: "space-between", children: [_jsx(Box, { flexDirection: "column", children: (secondary ? mark : mark.slice(0, 1)).map((line, index) => (_jsx(BodyText, { emphasis: "strong", children: line }, `${index}:${line}`))) }), _jsxs(Box, { flexDirection: "column", alignItems: "flex-end", children: [_jsx(BodyText, { emphasis: "strong", children: version }), secondary && _jsx(BodyText, { children: step })] })] }));
173
+ }
174
+ function NavigationPanel({ width = 16, compact = false, step = 2, focused = false, }) {
175
+ const entries = [
176
+ "Detect",
177
+ "Harnesses",
178
+ "Fleet",
179
+ "Gates",
180
+ "Consults",
181
+ "Reviewers",
182
+ "Review",
183
+ ];
184
+ if (!compact) {
185
+ return (_jsx(Panel, { title: "SETUP", width: width, focused: focused, children: entries.map((entry, index) => (_jsxs(BodyText, { emphasis: index + 1 === step ? "strong" : "normal", children: [index + 1 === step ? `${GLYPHS.pointer} ` : " ", entry] }, entry))) }));
186
+ }
187
+ return (_jsx(Panel, { title: "SETUP", width: width, focused: focused, children: entries.map((entry, index) => (_jsx(Box, { height: 1, overflow: "hidden", children: _jsxs(BodyText, { emphasis: index + 1 === step ? "strong" : "normal", children: [index + 1 === step ? `${GLYPHS.pointer} ` : " ", entry] }) }, entry))) }));
188
+ }
189
+ function KeysPanel({ bindings, width = 16, focused = false, }) {
190
+ return (_jsx(Panel, { title: "KEYS", width: width, focused: focused, children: keybarEntries(bindings).map((item) => (_jsxs(Box, { children: [_jsx(BodyText, { emphasis: "strong", children: item.key }), _jsxs(BodyText, { children: [" ", item.label] })] }, item.key))) }));
191
+ }
192
+ function CompactKeysPanel({ bindings, width = 16, focused = false, }) {
193
+ const contentColumns = Math.max(1, width - 4);
194
+ return (_jsx(Panel, { title: "KEYS", width: width, focused: focused, children: keybarEntries(setupAdditionalBindings(bindings)).map((item) => (_jsxs(Box, { flexDirection: "row", flexWrap: "nowrap", height: 1, overflow: "hidden", children: [_jsx(BodyText, { emphasis: "strong", children: item.key }), _jsxs(BodyText, { children: [" ", item.label.slice(0, Math.max(0, contentColumns - [...item.key].length - 1))] })] }, item.key))) }));
195
+ }
196
+ function SetupKeybar({ bindings, width, }) {
197
+ return (_jsx(Box, { flexDirection: "row", flexWrap: "nowrap", width: width, height: 1, overflow: "hidden", "aria-label": "setup cockpit keys", children: keybarEntries(bindings).map((item, index) => (_jsxs(Box, { flexShrink: 0, children: [index > 0 && _jsx(BodyText, { emphasis: "dim", children: " \u00B7 " }), _jsx(BodyText, { emphasis: "strong", children: item.key }), _jsxs(BodyText, { emphasis: "dim", children: [" ", item.label] })] }, item.key))) }));
198
+ }
199
+ function SetupInteractionPanel({ data, interaction, bindings, }) {
200
+ if (interaction?.savePrompt === true) {
201
+ return (_jsx(LinePanel, { title: "SAVE", lines: ["confirm save · y yes · n cancel", ...overlayDiffLines(data)], columns: 72 }));
202
+ }
203
+ if (interaction?.help === true) {
204
+ return (_jsx(LinePanel, { title: "HELP", lines: keybarEntries(bindings).map((item) => `${item.key} ${item.label}`), columns: 72 }));
205
+ }
206
+ return null;
207
+ }
208
+ function setupStatusItems(data, interaction) {
209
+ const items = [
210
+ { state: "neutral", text: `overlay ${data.overlayTarget}` },
211
+ { state: "pass", text: "base untouched" },
212
+ interaction?.saved === true
213
+ ? { state: "active", text: "changes saved" }
214
+ : { state: "warn", text: `${data.stagedChanges.length} changes unsaved` },
215
+ ];
216
+ if (interaction !== undefined) {
217
+ if (interaction.all)
218
+ items.push({ state: "active", text: "all selected" });
219
+ else if (interaction.selected.length > 0) {
220
+ items.push({ state: "active", text: "selection selected" });
221
+ }
222
+ else if (interaction.selection >= 0) {
223
+ items.push({ state: "neutral", text: `selection ${interaction.selection + 1}` });
224
+ }
225
+ if (interaction.probeRevision > 0) {
226
+ items.push({ state: "pass", text: `re-probed ${interaction.probeRevision}` });
227
+ }
228
+ if (interaction.panel !== 1) {
229
+ const panel = SETUP_PANEL_FOCUS_ORDER[interaction.panel] ?? "HARNESSES";
230
+ items.push({ state: "neutral", text: `panel ${panel}` });
231
+ }
232
+ }
233
+ return items;
234
+ }
235
+ function fullKeysFit(rows, layout, data, columns, bindings) {
236
+ if (!layout.elements.secondaryHeader)
237
+ return false;
238
+ const wrappedKeyRows = {
239
+ "three-column": 0,
240
+ "folded-keys": 2,
241
+ stacked: 4,
242
+ }[layout.arrangement];
243
+ const panelRows = bindings.length
244
+ + wrappedKeyRows
245
+ + 3; // panel borders and title row
246
+ return headerRowsFor(layout, data, columns) + 2 + panelRows <= Math.floor(rows);
247
+ }
248
+ function sidePanelWidth(layout) {
249
+ if (layout.arrangement === "three-column")
250
+ return 16;
251
+ if (layout.arrangement === "folded-keys")
252
+ return 12;
253
+ return 11;
254
+ }
255
+ /**
256
+ * Every detail panel publishes the body lines it draws, so the height ladder
257
+ * can charge it the rows those very lines occupy rather than a constant that
258
+ * goes stale the moment the panel changes.
259
+ */
260
+ function detectedRows(data) {
261
+ return [
262
+ ...data.harnesses.map((harness) => ({
263
+ key: harness.id,
264
+ state: harness.state,
265
+ text: ` ${harness.stateWord} · ${harness.id} · ${harness.version}`
266
+ + ` · ${harness.modelCount} models`,
267
+ })),
268
+ ...data.deniedChannels.map((denied) => ({
269
+ key: denied.channel,
270
+ state: "fail",
271
+ text: ` denied · ${denied.channel} — ${denied.reason}`,
272
+ })),
273
+ ];
274
+ }
275
+ function fleetLines(data) {
276
+ const routing = data.config.routing;
277
+ return [
278
+ `mode ${routing.mode ?? "risk-based"}`,
279
+ `floors ${Object.entries(routing.floors)
280
+ .map(([shape, tier]) => `${shape}:${tier}`)
281
+ .join(" · ")}`,
282
+ `deny ${(routing.deny?.models ?? []).join(", ") || "none"}`,
283
+ ];
284
+ }
285
+ function gatesLines(data) {
286
+ return [
287
+ `test ${data.config.gates.test ?? "auto-detected"}`,
288
+ `diff cap ${data.config.gates.diffCap}`,
289
+ ];
290
+ }
291
+ function consultsLines(data) {
292
+ const consult = data.config.consult;
293
+ return [
294
+ `seat ${consult.adapter}:${consult.model}`,
295
+ `stall ${consult.stallMinutes} minutes`,
296
+ ...(consult.prefer ?? []).map((seat) => `prefer ${seat}`),
297
+ ];
298
+ }
299
+ function reviewersLines(data) {
300
+ const review = data.config.review;
301
+ return [
302
+ `required ${String(review.required)}`,
303
+ `complexity threshold ${review.complexityThreshold}`,
304
+ ...(review.prefer ?? []).map((reviewer) => `prefer ${reviewer}`),
305
+ ];
306
+ }
307
+ function overlayDiffLines(data) {
308
+ const lines = data.overlayDiff.split("\n");
309
+ return [
310
+ ...lines.slice(0, 2),
311
+ ...lines.filter((line) => line.startsWith("-") && !line.startsWith("---")).slice(0, 2),
312
+ ...lines.filter((line) => line.startsWith("+") && !line.startsWith("+++")).slice(0, 4),
313
+ ];
314
+ }
315
+ function HarnessRows({ rows, columns, }) {
316
+ return (_jsx(Panel, { title: "DETECTED", children: rows.flatMap((row) => {
317
+ const [first = "", ...continuations] = composeBandLine(row.text.trimStart(), Math.max(1, columns));
318
+ return [
319
+ _jsxs(Box, { children: [_jsx(StateGlyph, { state: row.state }), _jsxs(BodyText, { children: [" ", first] })] }, row.key),
320
+ ...continuations.map((line, index) => (_jsx(BodyText, { children: line }, `${row.key}:continuation:${index}`))),
321
+ ];
322
+ }) }));
323
+ }
324
+ function LinePanel({ title, lines, columns, emphasis, }) {
325
+ return (_jsx(Panel, { title: title, children: _jsx(BandLines, { lines: lines, columns: columns, emphasis: emphasis }) }));
326
+ }
327
+ function lineBandContent(title, lines) {
328
+ return { title, lines };
329
+ }
330
+ function LineBand({ columns, panels, }) {
331
+ const widths = allocateBandColumns(columns, panels);
332
+ return (_jsx(CockpitGrid, { columns: columns, columnContents: panels, children: panels.map((panel, index) => (_jsx(LinePanel, { title: panel.title, lines: panel.lines, columns: Math.max(1, widths[index] - 4) }, panel.title))) }));
333
+ }
334
+ function statsSummaryLine(data) {
335
+ return `found ${data.counts.found} · authenticated ${data.counts.authenticated}`
336
+ + ` · routable ${data.counts.routable}`;
337
+ }
338
+ function SetupStats({ data, mode, }) {
339
+ if (mode === "summary") {
340
+ return _jsx(BodyText, { children: statsSummaryLine(data) });
341
+ }
342
+ return (_jsxs(CockpitGrid, { children: [_jsx(StatTile, { label: "FOUND", value: data.counts.found, samples: [data.counts.found] }), _jsx(StatTile, { label: "AUTHENTICATED", value: data.counts.authenticated, samples: [data.counts.authenticated] }), _jsx(StatTile, { label: "ROUTABLE", value: data.counts.routable, samples: [data.counts.routable] })] }));
343
+ }
344
+ function SizedReviewPanel({ rows, bodyRows, }) {
345
+ const visible = rows.slice(0, bodyRows);
346
+ const emptyRows = Math.max(0, bodyRows - visible.length);
347
+ const review = JournalRowPanel({ rows: visible, title: "REVIEW" });
348
+ const reviewProps = review.props;
349
+ return cloneElement(review, {}, reviewProps.children, emptyRows > 0 ? _jsx(Box, { height: emptyRows }, "review-space") : null);
350
+ }
351
+ /**
352
+ * The contracted ladder (§5.4) governs the elements every surface shares. The
353
+ * detail panels below it are the setup body, so they surrender first, from the
354
+ * bottom of this list upward: what a step is *about* outlives what merely
355
+ * describes the fleet.
356
+ */
357
+ export const SETUP_DETAIL_ORDER = [
358
+ "detected",
359
+ "consultsAndReviewers",
360
+ "overlayDiff",
361
+ "fleetAndGates",
362
+ ];
363
+ /** Columns the HARNESSES panel gives its body, after side rails and chrome. */
364
+ function contentInnerWidth(columns, layout) {
365
+ const rails = Math.floor(columns) >= APPROVED_SIDE_RAIL_COLUMN_FLOOR
366
+ ? 2 * sidePanelWidth(layout) + 2 // both rails and the gaps beside them
367
+ : 0;
368
+ return Math.max(1, Math.floor(columns) - rails - 4);
369
+ }
370
+ function gridRows(panels, inner) {
371
+ return measureBandRows(inner, panels);
372
+ }
373
+ function detailRows(detail, data, inner) {
374
+ switch (detail) {
375
+ case "consultsAndReviewers":
376
+ return gridRows([
377
+ lineBandContent("CONSULTS", consultsLines(data)),
378
+ lineBandContent("REVIEWERS", reviewersLines(data)),
379
+ ], inner);
380
+ case "overlayDiff":
381
+ return panelRows(overlayDiffLines(data), inner - 4);
382
+ case "fleetAndGates":
383
+ return gridRows([
384
+ lineBandContent("FLEET", fleetLines(data)),
385
+ lineBandContent("GATES", gatesLines(data)),
386
+ ], inner);
387
+ }
388
+ }
389
+ /** Every detected row spends its first two columns on the state glyph and gap. */
390
+ function detectedPanelRows(rows, inner) {
391
+ const columns = Math.max(1, inner - 6);
392
+ return PANEL_CHROME_ROWS + rows.reduce((total, row) => total + composeBandLine(row.text.trimStart(), columns).length, 0);
393
+ }
394
+ /**
395
+ * DETECTED is the body of the step the surface is showing, so — like the
396
+ * journal body it stands in for (§5.4 rank 5) — it shrinks rather than being
397
+ * replaced: it gives up rows from the bottom, never below one row per state it
398
+ * still has to report, and only disappears when even that will not fit.
399
+ */
400
+ function detectedPlan(data, inner, budget) {
401
+ const all = detectedRows(data);
402
+ if (detectedPanelRows(all, inner) <= budget)
403
+ return all;
404
+ const states = new Set();
405
+ const smallest = all.filter((row) => {
406
+ if (states.has(row.state))
407
+ return false;
408
+ states.add(row.state);
409
+ return true;
410
+ });
411
+ if (detectedPanelRows(smallest, inner) > budget)
412
+ return [];
413
+ // Grow back toward the full panel, keeping the panel's own row order.
414
+ const kept = new Set(smallest);
415
+ for (const row of all) {
416
+ if (kept.has(row))
417
+ continue;
418
+ const grown = all.filter((candidate) => kept.has(candidate) || candidate === row);
419
+ if (detectedPanelRows(grown, inner) > budget)
420
+ break;
421
+ kept.add(row);
422
+ }
423
+ return all.filter((row) => kept.has(row));
424
+ }
425
+ function setupDetailPlan(rows, data, inner, base, journalPreferred) {
426
+ // DETECTED may borrow from the review body down to its one-row minimum; the
427
+ // configuration panels below it may not.
428
+ const detected = detectedPlan(data, inner, Math.max(0, Math.floor(rows) - base - 1));
429
+ const retained = new Set(detected.length > 0 ? ["detected"] : []);
430
+ let retainedRows = detected.length > 0
431
+ ? detectedPanelRows(detected, inner)
432
+ : 0;
433
+ let available = Math.max(0, Math.floor(rows) - base - journalPreferred - retainedRows);
434
+ for (const detail of SETUP_DETAIL_ORDER.slice(1)) {
435
+ const cost = detailRows(detail, data, inner);
436
+ if (available < cost)
437
+ break;
438
+ retained.add(detail);
439
+ retainedRows += cost;
440
+ available -= cost;
441
+ }
442
+ return {
443
+ detected,
444
+ retained,
445
+ retainedRows,
446
+ complete: retained.size === SETUP_DETAIL_ORDER.length
447
+ && detected.length === detectedRows(data).length,
448
+ };
449
+ }
450
+ function versionColumnWidth(data) {
451
+ return Math.max(...identityVersionLines(data).map((line) => [...line].length));
452
+ }
453
+ /**
454
+ * The banner sits beside the version column, so a narrow frame leaves it fewer
455
+ * columns and it wraps. Charged from the banner the header actually draws.
456
+ */
457
+ function headerRowsFor(layout, data, columns) {
458
+ if (!layout.elements.secondaryHeader)
459
+ return 1;
460
+ const banner = Math.max(1, Math.floor(columns) - versionColumnWidth(data));
461
+ return identityBannerLines().reduce((rows, line) => rows + wrappedRows(line, banner), 0);
462
+ }
463
+ function statRowsFor(layout, data, inner) {
464
+ // A tile is its value, its sparkline and its chrome; the stacked band spends
465
+ // one more row because the widest label wraps.
466
+ if (layout.stats.mode === "tiles") {
467
+ return PANEL_CHROME_ROWS + 2 + (layout.arrangement === "stacked" ? 1 : 0);
468
+ }
469
+ return wrappedRows(statsSummaryLine(data), inner);
470
+ }
471
+ /** Every row the frame spends before the detail panels and the review body. */
472
+ function baseRows(layout, data, columns, inner) {
473
+ return headerRowsFor(layout, data, columns)
474
+ + 2 // permanent status strip and keybar
475
+ + 3 // outer HARNESSES panel border and title
476
+ + statRowsFor(layout, data, inner)
477
+ + PANEL_CHROME_ROWS; // review border and title
478
+ }
479
+ function LadderSetupPanel({ data, layout, columns, rows, interaction, bindings, focused = true, }) {
480
+ const inner = contentInnerWidth(columns, layout);
481
+ const preferredReviewRows = Math.min(FULL_JOURNAL_ROWS, layout.journalRows);
482
+ const base = baseRows(layout, data, columns, inner);
483
+ const details = setupDetailPlan(rows, data, inner, base, preferredReviewRows);
484
+ const spare = Math.max(1, Math.floor(rows) - base - details.retainedRows);
485
+ // Once the whole arrangement is drawn the review body is the only sink left,
486
+ // so it absorbs the surplus; below that it holds its contracted height.
487
+ const visibleReviewRows = details.complete
488
+ ? spare
489
+ : Math.min(preferredReviewRows, spare);
490
+ const interactionPanel = SetupInteractionPanel({ data, interaction, bindings });
491
+ return (_jsx(Panel, { title: "HARNESSES", focused: focused, flexGrow: 1, children: interactionPanel ?? (_jsxs(_Fragment, { children: [_jsx(SetupStats, { data: data, mode: layout.stats.mode }), details.retained.has("detected") && (_jsx(HarnessRows, { rows: details.detected, columns: Math.max(1, inner - 6) })), details.retained.has("fleetAndGates") && (_jsx(LineBand, { columns: inner, panels: [
492
+ lineBandContent("FLEET", fleetLines(data)),
493
+ lineBandContent("GATES", gatesLines(data)),
494
+ ] })), details.retained.has("consultsAndReviewers") && (_jsx(LineBand, { columns: inner, panels: [
495
+ lineBandContent("CONSULTS", consultsLines(data)),
496
+ lineBandContent("REVIEWERS", reviewersLines(data)),
497
+ ] })), details.retained.has("overlayDiff") && (_jsx(LinePanel, { title: "OVERLAY DIFF", lines: overlayDiffLines(data), columns: Math.max(1, inner - 4), emphasis: "dim" })), _jsx(SizedReviewPanel, { rows: data.reviewRows, bodyRows: visibleReviewRows })] })) }));
498
+ }
499
+ function SetupPanel({ data, inner, interaction, bindings, focused = true, }) {
500
+ const interactionPanel = SetupInteractionPanel({ data, interaction, bindings });
501
+ return (_jsx(Panel, { title: "HARNESSES", focused: focused, flexGrow: 1, children: interactionPanel ?? (_jsxs(_Fragment, { children: [_jsxs(CockpitGrid, { children: [_jsx(StatTile, { label: "FOUND", value: data.counts.found, samples: [data.counts.found] }), _jsx(StatTile, { label: "AUTHENTICATED", value: data.counts.authenticated, samples: [data.counts.authenticated] }), _jsx(StatTile, { label: "ROUTABLE", value: data.counts.routable, samples: [data.counts.routable] })] }), _jsx(HarnessRows, { rows: detectedRows(data), columns: Math.max(1, inner - 6) }), _jsx(LineBand, { columns: inner, panels: [
502
+ lineBandContent("FLEET", fleetLines(data)),
503
+ lineBandContent("GATES", gatesLines(data)),
504
+ ] }), _jsx(LineBand, { columns: inner, panels: [
505
+ lineBandContent("CONSULTS", consultsLines(data)),
506
+ lineBandContent("REVIEWERS", reviewersLines(data)),
507
+ ] }), _jsx(LinePanel, { title: "OVERLAY DIFF", lines: overlayDiffLines(data), columns: Math.max(1, inner - 4), emphasis: "dim" }), _jsx(SizedReviewPanel, { rows: data.reviewRows, bodyRows: data.reviewRows.length })] })) }));
508
+ }
509
+ export function SetupCockpitFrame({ data, columns, rows, interaction, bindings = SURFACE_KEY_BINDINGS.setup, }) {
510
+ const currentInteraction = interaction ?? initialSetupInteractionState();
511
+ const focusedPanel = SETUP_PANEL_FOCUS_ORDER[currentInteraction.panel]
512
+ ?? "HARNESSES";
513
+ if (rows === undefined) {
514
+ const inner = Math.max(1, Math.floor(columns) - (2 * 16) - 2 - 4);
515
+ return (_jsxs(Box, { flexDirection: "column", width: columns, children: [_jsx(IdentityHeader, { data: data, interaction: currentInteraction }), _jsxs(Box, { flexDirection: "row", gap: 1, children: [_jsx(NavigationPanel, { step: currentInteraction.step, focused: focusedPanel === "SETUP" }), _jsx(SetupPanel, { data: data, inner: inner, interaction: currentInteraction, bindings: bindings, focused: focusedPanel === "HARNESSES" }), _jsx(KeysPanel, { bindings: bindings, focused: focusedPanel === "KEYS" })] }), _jsx(StatusStrip, { width: columns, items: setupStatusItems(data, currentInteraction) }), _jsx(SetupKeybar, { bindings: bindings, width: columns })] }));
516
+ }
517
+ const layout = resolveCockpitLayout(columns, rows);
518
+ if (layout.renderer === "plain")
519
+ return _jsx(_Fragment, {});
520
+ const showSideRails = columns >= APPROVED_SIDE_RAIL_COLUMN_FLOOR;
521
+ const contentLayout = showSideRails
522
+ ? layout
523
+ : {
524
+ ...layout,
525
+ stats: { ...layout.stats, mode: "summary", rows: 1 },
526
+ };
527
+ const showFullSidePanels = fullKeysFit(rows, contentLayout, data, columns, bindings);
528
+ const resolvedSidePanelWidth = sidePanelWidth(contentLayout);
529
+ return (_jsxs(Box, { flexDirection: "column", width: columns, children: [_jsx(IdentityHeader, { data: data, secondary: layout.elements.secondaryHeader, interaction: currentInteraction }), _jsxs(Box, { flexDirection: "row", gap: showSideRails ? 1 : 0, children: [showSideRails && (_jsx(Box, { width: resolvedSidePanelWidth, flexShrink: 0, children: _jsx(NavigationPanel, { width: resolvedSidePanelWidth, compact: !contentLayout.elements.secondaryHeader, step: currentInteraction.step, focused: focusedPanel === "SETUP" }) })), _jsx(LadderSetupPanel, { data: data, layout: contentLayout, columns: columns, rows: rows, interaction: currentInteraction, bindings: bindings, focused: focusedPanel === "HARNESSES" }), showSideRails && (showFullSidePanels
530
+ ? (_jsx(Box, { width: resolvedSidePanelWidth, flexShrink: 0, children: _jsx(KeysPanel, { bindings: bindings, width: resolvedSidePanelWidth, focused: focusedPanel === "KEYS" }) }))
531
+ : (_jsx(Box, { width: resolvedSidePanelWidth, flexShrink: 0, children: _jsx(CompactKeysPanel, { bindings: bindings, width: resolvedSidePanelWidth, focused: focusedPanel === "KEYS" }) })))] }), _jsx(StatusStrip, { width: columns, items: setupStatusItems(data, currentInteraction) }), _jsx(SetupKeybar, { bindings: bindings, width: columns })] }));
532
+ }