tickmarkr 1.80.0 → 1.83.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/brand.d.ts +4 -1
- package/dist/brand.js +46 -7
- package/dist/cli/commands/approve.js +53 -8
- package/dist/cli/commands/plan.js +17 -2
- package/dist/cli/commands/report.js +15 -6
- package/dist/cli/commands/ui.d.ts +1 -1
- package/dist/cli/commands/ui.js +47 -4
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/compile/collateral.d.ts +19 -0
- package/dist/compile/collateral.js +90 -0
- package/dist/compile/index.js +18 -4
- package/dist/drivers/herdr.d.ts +1 -0
- package/dist/drivers/herdr.js +19 -0
- package/dist/drivers/types.d.ts +1 -0
- package/dist/gates/acceptance.js +50 -8
- package/dist/gates/llm.js +34 -19
- package/dist/gates/review.d.ts +9 -1
- package/dist/gates/review.js +173 -7
- package/dist/gates/run-gates.d.ts +1 -0
- package/dist/gates/run-gates.js +18 -1
- package/dist/report/bundle.d.ts +6 -1
- package/dist/report/bundle.js +13 -3
- package/dist/run/daemon.d.ts +5 -0
- package/dist/run/daemon.js +188 -27
- package/dist/run/journal.d.ts +5 -0
- package/dist/run/journal.js +71 -1
- package/dist/tui/cockpit/capture.d.ts +107 -1
- package/dist/tui/cockpit/capture.js +287 -10
- package/dist/tui/cockpit/components.d.ts +15 -86
- package/dist/tui/cockpit/components.js +49 -55
- package/dist/tui/cockpit/derive.d.ts +73 -1
- package/dist/tui/cockpit/derive.js +211 -15
- package/dist/tui/cockpit/keys.d.ts +244 -0
- package/dist/tui/cockpit/keys.js +634 -0
- package/dist/tui/cockpit/layout.d.ts +162 -0
- package/dist/tui/cockpit/layout.js +229 -0
- package/dist/tui/cockpit/live.d.ts +80 -0
- package/dist/tui/cockpit/live.js +311 -0
- package/dist/tui/cockpit/run-cockpit.d.ts +65 -2
- package/dist/tui/cockpit/run-cockpit.js +705 -128
- package/dist/tui/cockpit/setup-cockpit.d.ts +149 -1
- package/dist/tui/cockpit/setup-cockpit.js +432 -56
- package/dist/tui/cockpit/views.d.ts +72 -0
- package/dist/tui/cockpit/views.js +56 -0
- package/dist/tui/cockpit/width.d.ts +105 -0
- package/dist/tui/cockpit/width.js +276 -0
- package/fixtures/speckit-sample/tasks.md +2 -2
- package/package.json +3 -1
|
@@ -1,14 +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 {
|
|
2
|
+
import { Box, render, useApp, useInput, useStdout } from "ink";
|
|
3
|
+
import { userInfo } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { cloneElement, createElement, useState, } from "react";
|
|
4
6
|
import { parse } from "yaml";
|
|
7
|
+
import { approve } from "../../cli/commands/approve.js";
|
|
8
|
+
import { ATTEMPT_CAP_RELEASE, GATE_SATISFIED_RELEASE, Journal, REVIEW_UPHELD_RELEASE, } from "../../run/journal.js";
|
|
5
9
|
import { BANNER, GLYPHS, PLAIN_BANNER, } from "../../brand.js";
|
|
6
10
|
import { DEFAULT_CONFIG, INTEGRITY_FLOOR_SHAPES, TIER_RANK, TickmarkrConfigSchema, unifiedYamlDiff, } from "../../config/config.js";
|
|
7
|
-
import { SHAPES } from "../../graph/schema.js";
|
|
8
|
-
import {
|
|
11
|
+
import { GATE_NAMES, SHAPES } from "../../graph/schema.js";
|
|
12
|
+
import { stateDirName } from "../../graph/graph.js";
|
|
13
|
+
import { allocateBandColumns, BandLines, BodyText, CockpitGrid, composeBandLine, JournalRowPanel, measureBandRows, Panel, StateGlyph, StatTile, StatusStrip, } from "./components.js";
|
|
9
14
|
import { FULL_JOURNAL_ROWS, resolveCockpitLayout, } from "./layout.js";
|
|
10
15
|
import { deriveRunCockpitData, PANEL_CHROME_ROWS, panelRows, wrappedRows, } from "./run-cockpit.js";
|
|
16
|
+
import { initialSetupInteractionState, keybarEntries, SETUP_PANEL_FOCUS_ORDER, SURFACE_KEY_BINDINGS, } from "./keys.js";
|
|
17
|
+
import { DECISIONS_TAB_HINT, DECISIONS_TAB_TITLE, } from "./views.js";
|
|
18
|
+
// Split across two statements because `setup.test.ts` pins the clipping import
|
|
19
|
+
// by its exact text, proving the rows are drawn through the width module.
|
|
20
|
+
import { fitCells } from "./width.js";
|
|
21
|
+
import { cellWidth } from "./width.js";
|
|
11
22
|
const APPROVED_SIDE_RAIL_COLUMN_FLOOR = 80;
|
|
23
|
+
const setupAdditionalBindings = (bindings) => bindings.filter((binding) => !SURFACE_KEY_BINDINGS.run.some((runBinding) => runBinding.key === binding.key));
|
|
12
24
|
function deepMerge(base, overlay) {
|
|
13
25
|
if (overlay === undefined || overlay === null)
|
|
14
26
|
return base;
|
|
@@ -158,28 +170,79 @@ export function deriveSetupCockpitData(captures, binaryVersion) {
|
|
|
158
170
|
function identityBannerLines() {
|
|
159
171
|
return (process.env.NO_COLOR === undefined ? BANNER : PLAIN_BANNER).trimEnd().split("\n");
|
|
160
172
|
}
|
|
161
|
-
function identityVersionLines(data) {
|
|
162
|
-
return [
|
|
173
|
+
function identityVersionLines(data, interaction) {
|
|
174
|
+
return [
|
|
175
|
+
`v${data.binaryVersion} · binary ${GLYPHS.pass}`,
|
|
176
|
+
`setup · step ${interaction?.step ?? 2}/6`,
|
|
177
|
+
];
|
|
163
178
|
}
|
|
164
|
-
function IdentityHeader({ data, secondary = true, }) {
|
|
179
|
+
function IdentityHeader({ data, secondary = true, interaction, }) {
|
|
165
180
|
const mark = identityBannerLines();
|
|
166
|
-
const [version, step] = identityVersionLines(data);
|
|
181
|
+
const [version, step] = identityVersionLines(data, interaction);
|
|
167
182
|
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 })] })] }));
|
|
168
183
|
}
|
|
169
|
-
function NavigationPanel({ width = 16, compact = false, }) {
|
|
184
|
+
function NavigationPanel({ width = 16, compact = false, step = 2, focused = false, }) {
|
|
185
|
+
const entries = [
|
|
186
|
+
"Detect",
|
|
187
|
+
"Harnesses",
|
|
188
|
+
"Fleet",
|
|
189
|
+
"Gates",
|
|
190
|
+
"Consults",
|
|
191
|
+
"Reviewers",
|
|
192
|
+
"Review",
|
|
193
|
+
];
|
|
170
194
|
if (!compact) {
|
|
171
|
-
return (
|
|
195
|
+
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))) }));
|
|
172
196
|
}
|
|
173
|
-
return (
|
|
197
|
+
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))) }));
|
|
174
198
|
}
|
|
175
|
-
function KeysPanel({ width = 16 }) {
|
|
176
|
-
return (_jsx(Panel, { title: "KEYS", width: width, children:
|
|
199
|
+
function KeysPanel({ bindings, width = 16, focused = false, }) {
|
|
200
|
+
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))) }));
|
|
177
201
|
}
|
|
178
|
-
function CompactKeysPanel({ width = 16 }) {
|
|
202
|
+
function CompactKeysPanel({ bindings, width = 16, focused = false, }) {
|
|
179
203
|
const contentColumns = Math.max(1, width - 4);
|
|
180
|
-
return (_jsx(Panel, { title: "KEYS", width: width, children:
|
|
204
|
+
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 - cellWidth(item.key) - 1))] })] }, item.key))) }));
|
|
205
|
+
}
|
|
206
|
+
function SetupKeybar({ bindings, width, }) {
|
|
207
|
+
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))) }));
|
|
208
|
+
}
|
|
209
|
+
function SetupInteractionPanel({ data, interaction, bindings, }) {
|
|
210
|
+
if (interaction?.savePrompt === true) {
|
|
211
|
+
return (_jsx(LinePanel, { title: "SAVE", lines: ["confirm save · y yes · n cancel", ...overlayDiffLines(data)], columns: 72 }));
|
|
212
|
+
}
|
|
213
|
+
if (interaction?.help === true) {
|
|
214
|
+
return (_jsx(LinePanel, { title: "HELP", lines: keybarEntries(bindings).map((item) => `${item.key} ${item.label}`), columns: 72 }));
|
|
215
|
+
}
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
function setupStatusItems(data, interaction) {
|
|
219
|
+
const items = [
|
|
220
|
+
{ state: "neutral", text: `overlay ${data.overlayTarget}` },
|
|
221
|
+
{ state: "pass", text: "base untouched" },
|
|
222
|
+
interaction?.saved === true
|
|
223
|
+
? { state: "active", text: "changes saved" }
|
|
224
|
+
: { state: "warn", text: `${data.stagedChanges.length} changes unsaved` },
|
|
225
|
+
];
|
|
226
|
+
if (interaction !== undefined) {
|
|
227
|
+
if (interaction.all)
|
|
228
|
+
items.push({ state: "active", text: "all selected" });
|
|
229
|
+
else if (interaction.selected.length > 0) {
|
|
230
|
+
items.push({ state: "active", text: "selection selected" });
|
|
231
|
+
}
|
|
232
|
+
else if (interaction.selection >= 0) {
|
|
233
|
+
items.push({ state: "neutral", text: `selection ${interaction.selection + 1}` });
|
|
234
|
+
}
|
|
235
|
+
if (interaction.probeRevision > 0) {
|
|
236
|
+
items.push({ state: "pass", text: `re-probed ${interaction.probeRevision}` });
|
|
237
|
+
}
|
|
238
|
+
if (interaction.panel !== 1) {
|
|
239
|
+
const panel = SETUP_PANEL_FOCUS_ORDER[interaction.panel] ?? "HARNESSES";
|
|
240
|
+
items.push({ state: "neutral", text: `panel ${panel}` });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
return items;
|
|
181
244
|
}
|
|
182
|
-
function fullKeysFit(rows, layout, data, columns) {
|
|
245
|
+
function fullKeysFit(rows, layout, data, columns, bindings) {
|
|
183
246
|
if (!layout.elements.secondaryHeader)
|
|
184
247
|
return false;
|
|
185
248
|
const wrappedKeyRows = {
|
|
@@ -187,7 +250,7 @@ function fullKeysFit(rows, layout, data, columns) {
|
|
|
187
250
|
"folded-keys": 2,
|
|
188
251
|
stacked: 4,
|
|
189
252
|
}[layout.arrangement];
|
|
190
|
-
const panelRows =
|
|
253
|
+
const panelRows = bindings.length
|
|
191
254
|
+ wrappedKeyRows
|
|
192
255
|
+ 3; // panel borders and title row
|
|
193
256
|
return headerRowsFor(layout, data, columns) + 2 + panelRows <= Math.floor(rows);
|
|
@@ -395,7 +458,7 @@ function setupDetailPlan(rows, data, inner, base, journalPreferred) {
|
|
|
395
458
|
};
|
|
396
459
|
}
|
|
397
460
|
function versionColumnWidth(data) {
|
|
398
|
-
return Math.max(...identityVersionLines(data).map((line) =>
|
|
461
|
+
return Math.max(...identityVersionLines(data).map((line) => cellWidth(line)));
|
|
399
462
|
}
|
|
400
463
|
/**
|
|
401
464
|
* The banner sits beside the version column, so a narrow frame leaves it fewer
|
|
@@ -423,7 +486,7 @@ function baseRows(layout, data, columns, inner) {
|
|
|
423
486
|
+ statRowsFor(layout, data, inner)
|
|
424
487
|
+ PANEL_CHROME_ROWS; // review border and title
|
|
425
488
|
}
|
|
426
|
-
function LadderSetupPanel({ data, layout, columns, rows, }) {
|
|
489
|
+
function LadderSetupPanel({ data, layout, columns, rows, interaction, bindings, focused = true, }) {
|
|
427
490
|
const inner = contentInnerWidth(columns, layout);
|
|
428
491
|
const preferredReviewRows = Math.min(FULL_JOURNAL_ROWS, layout.journalRows);
|
|
429
492
|
const base = baseRows(layout, data, columns, inner);
|
|
@@ -434,34 +497,32 @@ function LadderSetupPanel({ data, layout, columns, rows, }) {
|
|
|
434
497
|
const visibleReviewRows = details.complete
|
|
435
498
|
? spare
|
|
436
499
|
: Math.min(preferredReviewRows, spare);
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
500
|
+
const interactionPanel = SetupInteractionPanel({ data, interaction, bindings });
|
|
501
|
+
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: [
|
|
502
|
+
lineBandContent("FLEET", fleetLines(data)),
|
|
503
|
+
lineBandContent("GATES", gatesLines(data)),
|
|
504
|
+
] })), details.retained.has("consultsAndReviewers") && (_jsx(LineBand, { columns: inner, panels: [
|
|
505
|
+
lineBandContent("CONSULTS", consultsLines(data)),
|
|
506
|
+
lineBandContent("REVIEWERS", reviewersLines(data)),
|
|
507
|
+
] })), 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 })] })) }));
|
|
508
|
+
}
|
|
509
|
+
function SetupPanel({ data, inner, interaction, bindings, focused = true, }) {
|
|
510
|
+
const interactionPanel = SetupInteractionPanel({ data, interaction, bindings });
|
|
511
|
+
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: [
|
|
512
|
+
lineBandContent("FLEET", fleetLines(data)),
|
|
513
|
+
lineBandContent("GATES", gatesLines(data)),
|
|
514
|
+
] }), _jsx(LineBand, { columns: inner, panels: [
|
|
515
|
+
lineBandContent("CONSULTS", consultsLines(data)),
|
|
516
|
+
lineBandContent("REVIEWERS", reviewersLines(data)),
|
|
517
|
+
] }), _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 })] })) }));
|
|
518
|
+
}
|
|
519
|
+
export function SetupCockpitFrame({ data, columns, rows, interaction, bindings = SURFACE_KEY_BINDINGS.setup, }) {
|
|
520
|
+
const currentInteraction = interaction ?? initialSetupInteractionState();
|
|
521
|
+
const focusedPanel = SETUP_PANEL_FOCUS_ORDER[currentInteraction.panel]
|
|
522
|
+
?? "HARNESSES";
|
|
455
523
|
if (rows === undefined) {
|
|
456
524
|
const inner = Math.max(1, Math.floor(columns) - (2 * 16) - 2 - 4);
|
|
457
|
-
return (_jsxs(Box, { flexDirection: "column", width: columns, children: [_jsx(IdentityHeader, { data: data }), _jsxs(Box, { flexDirection: "row", gap: 1, children: [_jsx(NavigationPanel, {}), _jsx(SetupPanel, { data: data, inner: inner }), _jsx(KeysPanel, {})] }), _jsx(StatusStrip, { width: columns, items:
|
|
458
|
-
{ state: "neutral", text: `overlay ${data.overlayTarget}` },
|
|
459
|
-
{ state: "pass", text: "base untouched" },
|
|
460
|
-
{
|
|
461
|
-
state: "warn",
|
|
462
|
-
text: `${data.stagedChanges.length} changes unsaved`,
|
|
463
|
-
},
|
|
464
|
-
] }), _jsx(Keybar, { surface: "setup", width: columns })] }));
|
|
525
|
+
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 })] }));
|
|
465
526
|
}
|
|
466
527
|
const layout = resolveCockpitLayout(columns, rows);
|
|
467
528
|
if (layout.renderer === "plain")
|
|
@@ -473,16 +534,331 @@ export function SetupCockpitFrame({ data, columns, rows, }) {
|
|
|
473
534
|
...layout,
|
|
474
535
|
stats: { ...layout.stats, mode: "summary", rows: 1 },
|
|
475
536
|
};
|
|
476
|
-
const showFullSidePanels = fullKeysFit(rows, contentLayout, data, columns);
|
|
537
|
+
const showFullSidePanels = fullKeysFit(rows, contentLayout, data, columns, bindings);
|
|
477
538
|
const resolvedSidePanelWidth = sidePanelWidth(contentLayout);
|
|
478
|
-
return (_jsxs(Box, { flexDirection: "column", width: columns, children: [_jsx(IdentityHeader, { data: data, secondary: layout.elements.secondaryHeader }), _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 }) })), _jsx(LadderSetupPanel, { data: data, layout: contentLayout, columns: columns, rows: rows }), showSideRails && (showFullSidePanels
|
|
479
|
-
? (_jsx(Box, { width: resolvedSidePanelWidth, flexShrink: 0, children: _jsx(KeysPanel, { width: resolvedSidePanelWidth }) }))
|
|
480
|
-
: (_jsx(Box, { width: resolvedSidePanelWidth, flexShrink: 0, children: _jsx(CompactKeysPanel, { width: resolvedSidePanelWidth }) })))] }), _jsx(StatusStrip, { width: columns, items:
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
539
|
+
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
|
|
540
|
+
? (_jsx(Box, { width: resolvedSidePanelWidth, flexShrink: 0, children: _jsx(KeysPanel, { bindings: bindings, width: resolvedSidePanelWidth, focused: focusedPanel === "KEYS" }) }))
|
|
541
|
+
: (_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 })] }));
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* There is no closed park kind for a declaration-shaped retirement, so the
|
|
545
|
+
* evidence read here is the one the daemon recorded: a pre-dispatch human-gate
|
|
546
|
+
* park whose reason carries the task's own title, and that title is where the
|
|
547
|
+
* spec declares the tombstone. Read narrowly on purpose — every other park kind
|
|
548
|
+
* is actionable no matter what its prose says.
|
|
549
|
+
*/
|
|
550
|
+
function isTombstonePark(kind, reason) {
|
|
551
|
+
return kind === "human-gate" && /\btombstone\b/iu.test(reason ?? "");
|
|
552
|
+
}
|
|
553
|
+
/** The parks a verb can release — the rows the surface draws live verbs on. */
|
|
554
|
+
export function actionableDecisions(decisions) {
|
|
555
|
+
return decisions.filter((decision) => !decision.tombstone);
|
|
556
|
+
}
|
|
557
|
+
/**
|
|
558
|
+
* The decisions section's rows: one per task the journal's own status replay
|
|
559
|
+
* leaves parked on a human, in the order the journal first named them. The
|
|
560
|
+
* replay is the production one (`Journal.replayStatuses`) so this surface and
|
|
561
|
+
* `tickmarkr status` can never disagree about what is parked.
|
|
562
|
+
*/
|
|
563
|
+
export function deriveParkedDecisions(journal) {
|
|
564
|
+
const statuses = journal.replayStatuses();
|
|
565
|
+
const events = journal.read();
|
|
566
|
+
const decisions = [];
|
|
567
|
+
for (const [taskId, status] of statuses) {
|
|
568
|
+
if (status !== "human")
|
|
569
|
+
continue;
|
|
570
|
+
let parkedIndex = -1;
|
|
571
|
+
let attempts = 0;
|
|
572
|
+
for (let index = 0; index < events.length; index += 1) {
|
|
573
|
+
const event = events[index];
|
|
574
|
+
if (event.taskId !== taskId)
|
|
575
|
+
continue;
|
|
576
|
+
if (event.event === "task-dispatch")
|
|
577
|
+
attempts += 1;
|
|
578
|
+
else if (event.event === "task-human")
|
|
579
|
+
parkedIndex = index;
|
|
580
|
+
}
|
|
581
|
+
const parked = parkedIndex >= 0 ? events[parkedIndex] : undefined;
|
|
582
|
+
// The same rule the production approve command applies: the newest failed
|
|
583
|
+
// gate-result before the newest task-human controls what approve records
|
|
584
|
+
// and whether uphold exists at all. Never inferred from the park's prose.
|
|
585
|
+
const failedGate = events.slice(0, Math.max(0, parkedIndex)).reverse().find((event) => event.event === "gate-result" && event.taskId === taskId && event.data.pass === false
|
|
586
|
+
&& typeof event.data.gate === "string"
|
|
587
|
+
&& GATE_NAMES.includes(event.data.gate))?.data.gate;
|
|
588
|
+
const kind = typeof parked?.data.kind === "string" ? parked.data.kind : "human-gate";
|
|
589
|
+
const reason = typeof parked?.data.reason === "string" ? parked.data.reason : undefined;
|
|
590
|
+
decisions.push({
|
|
591
|
+
taskId,
|
|
592
|
+
kind,
|
|
593
|
+
reason,
|
|
594
|
+
parkedAt: typeof parked?.ts === "string" && parked.ts.length >= 19
|
|
595
|
+
? parked.ts.slice(11, 19)
|
|
596
|
+
: "unknown",
|
|
597
|
+
attempts,
|
|
598
|
+
failedGate,
|
|
599
|
+
tombstone: isTombstonePark(kind, reason),
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
return decisions;
|
|
603
|
+
}
|
|
604
|
+
export function initialSetupDecisionsSession() {
|
|
605
|
+
return { selection: 0, confirming: null, writes: [], notice: null };
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Uphold exists only where the production command honours it: the newest
|
|
609
|
+
* failed gate before the park is the review gate. Everywhere else the key is
|
|
610
|
+
* inert and the keybar does not advertise it — the surface never promises a
|
|
611
|
+
* decision the command must refuse.
|
|
612
|
+
*/
|
|
613
|
+
export function upholdAvailable(decision) {
|
|
614
|
+
return decision.failedGate === "review";
|
|
615
|
+
}
|
|
616
|
+
/**
|
|
617
|
+
* The decisions surface's whole key grammar: ↑↓ move, a approve, u uphold,
|
|
618
|
+
* y confirm, n/Esc cancel, q quit. The reducer never writes — it can only
|
|
619
|
+
* *name* a command, and only on the confirm key, so a write the operator did
|
|
620
|
+
* not predict cannot happen. a/u are inert when nothing actionable is parked,
|
|
621
|
+
* and u is inert on a park the command would refuse, matching the keybar, which
|
|
622
|
+
* then does not advertise them. Tombstone parks sit outside the grammar: the
|
|
623
|
+
* pointer never reaches one, so no key can name a decision on it.
|
|
624
|
+
*/
|
|
625
|
+
export function applySetupDecisionsKey(session, event, parked) {
|
|
626
|
+
const decisions = actionableDecisions(parked);
|
|
627
|
+
if (session.confirming !== null) {
|
|
628
|
+
if (event.input === "y") {
|
|
629
|
+
return {
|
|
630
|
+
session: { ...session, confirming: null },
|
|
631
|
+
command: session.confirming,
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
if (event.input === "n" || event.key.escape === true) {
|
|
635
|
+
return { session: { ...session, confirming: null } };
|
|
636
|
+
}
|
|
637
|
+
return { session };
|
|
638
|
+
}
|
|
639
|
+
if (event.input === "q")
|
|
640
|
+
return { session, quit: true };
|
|
641
|
+
const selection = Math.min(session.selection, Math.max(0, decisions.length - 1));
|
|
642
|
+
if (event.key.upArrow === true || event.key.downArrow === true) {
|
|
643
|
+
if (decisions.length === 0)
|
|
644
|
+
return { session };
|
|
645
|
+
const delta = event.key.upArrow === true ? -1 : 1;
|
|
646
|
+
return {
|
|
647
|
+
session: {
|
|
648
|
+
...session,
|
|
649
|
+
selection: Math.max(0, Math.min(decisions.length - 1, selection + delta)),
|
|
650
|
+
},
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
if (event.input === "a" || event.input === "u") {
|
|
654
|
+
const decision = decisions[selection];
|
|
655
|
+
if (decision === undefined)
|
|
656
|
+
return { session };
|
|
657
|
+
if (event.input === "u" && !upholdAvailable(decision))
|
|
658
|
+
return { session };
|
|
659
|
+
return {
|
|
660
|
+
session: {
|
|
661
|
+
...session,
|
|
662
|
+
selection,
|
|
663
|
+
confirming: {
|
|
664
|
+
verb: event.input === "a" ? "approve" : "uphold",
|
|
665
|
+
taskId: decision.taskId,
|
|
666
|
+
},
|
|
667
|
+
},
|
|
668
|
+
};
|
|
669
|
+
}
|
|
670
|
+
return { session };
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* THE write. It calls the production `approve` command — validation, fail-closed
|
|
674
|
+
* refusals and the journal append are inherited, never re-implemented — and then
|
|
675
|
+
* reads the file back so the session records the release the journal actually
|
|
676
|
+
* gained. A refusal is the command's own message, verbatim, and appends nothing.
|
|
677
|
+
*/
|
|
678
|
+
export async function executeSetupDecision(command, { cwd, runId, by }) {
|
|
679
|
+
try {
|
|
680
|
+
const message = await approve([
|
|
681
|
+
runId,
|
|
682
|
+
command.taskId,
|
|
683
|
+
...(command.verb === "uphold" ? ["--uphold"] : []),
|
|
684
|
+
"--by",
|
|
685
|
+
by,
|
|
686
|
+
], cwd);
|
|
687
|
+
const events = Journal.open(cwd, runId).read();
|
|
688
|
+
let release;
|
|
689
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
690
|
+
const event = events[index];
|
|
691
|
+
if (event.event === "task-approved" && event.taskId === command.taskId) {
|
|
692
|
+
release = typeof event.data.release === "string" ? event.data.release : undefined;
|
|
693
|
+
break;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
return {
|
|
697
|
+
ok: true,
|
|
698
|
+
command,
|
|
699
|
+
message,
|
|
700
|
+
write: { taskId: command.taskId, verb: command.verb, release, by },
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
catch (error) {
|
|
704
|
+
return {
|
|
705
|
+
ok: false,
|
|
706
|
+
command,
|
|
707
|
+
refusal: error instanceof Error ? error.message : String(error),
|
|
708
|
+
};
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
/** Fold an executed command's outcome into the session the surface draws. */
|
|
712
|
+
export function recordSetupDecisionOutcome(session, outcome) {
|
|
713
|
+
return outcome.ok
|
|
714
|
+
? { ...session, notice: outcome.message, writes: [...session.writes, outcome.write] }
|
|
715
|
+
: { ...session, notice: outcome.refusal };
|
|
716
|
+
}
|
|
717
|
+
/** The text of one decisions row — measured and clipped only by the width module. */
|
|
718
|
+
function decisionRowText(decision) {
|
|
719
|
+
return `${decision.taskId} · ${decision.kind} · parked ${decision.parkedAt}`
|
|
720
|
+
+ ` · attempt ${decision.attempts}`
|
|
721
|
+
+ (decision.reason === undefined ? "" : ` · ${decision.reason}`);
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* The effect the confirmed write will actually have, derived from the park
|
|
725
|
+
* kind and the failed gate the same way the production command derives its
|
|
726
|
+
* append — the release markers are the command's own constants, so this text
|
|
727
|
+
* cannot drift from the write. An effect the operator cannot read here is an
|
|
728
|
+
* effect this surface does not have.
|
|
729
|
+
*/
|
|
730
|
+
function decisionEffectLines(command, decision) {
|
|
731
|
+
if (command.verb === "uphold") {
|
|
732
|
+
return [
|
|
733
|
+
`effect appends one task-approved event with release ${REVIEW_UPHELD_RELEASE};`
|
|
734
|
+
+ " the next resume funds one fixed attempt carrying the findings",
|
|
735
|
+
"it does NOT mark the task done or pass any gate",
|
|
736
|
+
];
|
|
737
|
+
}
|
|
738
|
+
if (decision?.kind === "gate-fail") {
|
|
739
|
+
if (decision.failedGate === undefined) {
|
|
740
|
+
// The command's own refusal, predicted: no write is promised here.
|
|
741
|
+
return [
|
|
742
|
+
"effect none — the command refuses: parked on gate-fail"
|
|
743
|
+
+ " but no failed gate result is recorded",
|
|
744
|
+
];
|
|
745
|
+
}
|
|
746
|
+
const gate = decision.failedGate;
|
|
747
|
+
return [
|
|
748
|
+
`effect appends one task-approved event with release ${GATE_SATISFIED_RELEASE}`
|
|
749
|
+
+ ` for gate ${gate}; the next resume continues past the approved gate`,
|
|
750
|
+
`it marks gate ${gate} satisfied; it does NOT mark the task done`,
|
|
751
|
+
];
|
|
752
|
+
}
|
|
753
|
+
if (decision?.kind === ATTEMPT_CAP_RELEASE) {
|
|
754
|
+
return [
|
|
755
|
+
`effect appends one task-approved event with release ${ATTEMPT_CAP_RELEASE};`
|
|
756
|
+
+ ` the next resume dispatches ${command.taskId} with a fresh attempt budget`,
|
|
757
|
+
"it does NOT mark the task done or pass any gate",
|
|
758
|
+
];
|
|
759
|
+
}
|
|
760
|
+
return [
|
|
761
|
+
"effect appends one task-approved event with no release;"
|
|
762
|
+
+ ` the next resume dispatches ${command.taskId}`,
|
|
763
|
+
"it does NOT mark the task done or pass any gate",
|
|
764
|
+
];
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* The confirm inset's lines: the task, the actor, the effect and the file,
|
|
768
|
+
* stated before the single key that can write.
|
|
769
|
+
*/
|
|
770
|
+
export function setupDecisionConfirmLines(command, decision, actor, journalFile) {
|
|
771
|
+
return [
|
|
772
|
+
`task ${command.taskId}${decision === undefined ? "" : ` · ${decision.kind} · attempt ${decision.attempts}`}`,
|
|
773
|
+
`actor ${actor}`,
|
|
774
|
+
...decisionEffectLines(command, decision),
|
|
775
|
+
`file ${journalFile} · append only`,
|
|
776
|
+
`y ${command.verb} · n cancel`,
|
|
777
|
+
];
|
|
778
|
+
}
|
|
779
|
+
/** The keybar the surface's state can actually honour — nothing else is drawn. */
|
|
780
|
+
function setupDecisionsKeybar(session, decisions) {
|
|
781
|
+
if (session.confirming !== null)
|
|
782
|
+
return "y Confirm · n Cancel";
|
|
783
|
+
if (decisions.length === 0)
|
|
784
|
+
return "q Quit";
|
|
785
|
+
const selected = Math.min(session.selection, decisions.length - 1);
|
|
786
|
+
const uphold = upholdAvailable(decisions[selected]) ? " · u Uphold" : "";
|
|
787
|
+
return `↑↓ Move · a Approve${uphold} · q Quit`;
|
|
788
|
+
}
|
|
789
|
+
/**
|
|
790
|
+
* What the tab is, drawn on the tab it names. The operator's v1.83 UAT read
|
|
791
|
+
* "Setup" as configuration; this surface configures nothing, so it says what it
|
|
792
|
+
* does and what it may write, from the one constant the tab strip and the
|
|
793
|
+
* keybar hint also draw.
|
|
794
|
+
*/
|
|
795
|
+
const DECISIONS_TAB_LINE = `tab ${DECISIONS_TAB_HINT} · journal-parked human decisions`
|
|
796
|
+
+ " · approve and uphold are its only writes";
|
|
797
|
+
/**
|
|
798
|
+
* What the section says when no park needs the operator. It names its own
|
|
799
|
+
* trigger and both verbs, so an operator who arrives at a quiet surface learns
|
|
800
|
+
* what would put a row here rather than reading the quiet as unfinished work.
|
|
801
|
+
*/
|
|
802
|
+
const DECISIONS_EMPTY_STATE = [
|
|
803
|
+
"nothing needs you now — no task is parked on a human gate",
|
|
804
|
+
"a task that parks on one appears here with its two decisions —"
|
|
805
|
+
+ " approve releases it, uphold sides with the reviewer",
|
|
806
|
+
];
|
|
807
|
+
/**
|
|
808
|
+
* The decisions surface. Every row it draws — decisions, the confirm inset,
|
|
809
|
+
* the pending writes region — is measured and clipped through the width
|
|
810
|
+
* module alone (`fitCells`), so a task name carrying a ZWJ grapheme can
|
|
811
|
+
* neither overflow its row nor be cut mid-cluster. The one unclipped element
|
|
812
|
+
* is `notice`: the production command's own words, verbatim.
|
|
813
|
+
*/
|
|
814
|
+
export function SetupDecisionsSurface({ decisions, session, columns, actor, journalFile, }) {
|
|
815
|
+
const inner = Math.max(1, Math.floor(columns) - 4);
|
|
816
|
+
const actionable = actionableDecisions(decisions);
|
|
817
|
+
const tombstones = decisions.filter((decision) => decision.tombstone);
|
|
818
|
+
const selected = Math.min(session.selection, Math.max(0, actionable.length - 1));
|
|
819
|
+
const confirming = session.confirming;
|
|
820
|
+
return (_jsxs(Box, { flexDirection: "column", width: columns, children: [_jsxs(Panel, { title: `${DECISIONS_TAB_TITLE} · ${decisions.length} parked`, focused: true, children: [_jsx(BodyText, { emphasis: "dim", children: fitCells(DECISIONS_TAB_LINE, inner) }), actionable.map((decision, index) => (_jsx(BodyText, { emphasis: index === selected ? "strong" : "normal", children: fitCells(`${index === selected ? `${GLYPHS.pointer} ` : " "}${decisionRowText(decision)}`, inner) }, decision.taskId))), actionable.length === 0 && DECISIONS_EMPTY_STATE.map((line) => (_jsx(BodyText, { children: fitCells(line, inner) }, line))), tombstones.map((decision) => (_jsx(BodyText, { emphasis: "dim", children: fitCells(` read-only · permanent by design · ${decisionRowText(decision)}`, inner) }, decision.taskId)))] }), confirming !== null && (_jsx(Panel, { title: `CONFIRM ${confirming.verb.toUpperCase()}`, focused: true, children: setupDecisionConfirmLines(confirming, decisions.find((decision) => decision.taskId === confirming.taskId), actor, journalFile).map((line) => (_jsx(BodyText, { children: fitCells(line, inner) }, line))) })), session.notice !== null && _jsx(BodyText, { children: session.notice }), _jsx(Panel, { title: "PENDING WRITES", children: session.writes.length === 0
|
|
821
|
+
? _jsx(BodyText, { children: fitCells("nothing written this session", inner) })
|
|
822
|
+
: session.writes.map((write) => (_jsx(BodyText, { children: fitCells(`${write.verb} ${write.taskId} · release ${write.release ?? "none"} · by ${write.by}`, inner) }, `${write.verb}:${write.taskId}`))) }), _jsx(BodyText, { emphasis: "dim", children: fitCells(setupDecisionsKeybar(session, actionable), Math.max(1, Math.floor(columns))) })] }));
|
|
823
|
+
}
|
|
824
|
+
function SetupDecisionsApp({ cwd, runId, actor, }) {
|
|
825
|
+
const { exit } = useApp();
|
|
826
|
+
const { stdout } = useStdout();
|
|
827
|
+
const readDecisions = () => deriveParkedDecisions(Journal.open(cwd, runId));
|
|
828
|
+
const [decisions, setDecisions] = useState(readDecisions);
|
|
829
|
+
const [session, setSession] = useState(initialSetupDecisionsSession);
|
|
830
|
+
useInput((input, key) => {
|
|
831
|
+
if (key.ctrl && input === "c") {
|
|
832
|
+
exit();
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
const result = applySetupDecisionsKey(session, { input, key }, decisions);
|
|
836
|
+
setSession(result.session);
|
|
837
|
+
if (result.quit === true)
|
|
838
|
+
exit();
|
|
839
|
+
if (result.command !== undefined) {
|
|
840
|
+
const command = result.command;
|
|
841
|
+
void executeSetupDecision(command, { cwd, runId, by: actor }).then((outcome) => {
|
|
842
|
+
setSession((current) => recordSetupDecisionOutcome(current, outcome));
|
|
843
|
+
// Re-derive from the file the write landed in: the released task's row
|
|
844
|
+
// leaves the section because the journal says so, not because we say so.
|
|
845
|
+
setDecisions(readDecisions());
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
});
|
|
849
|
+
return (_jsx(SetupDecisionsSurface, { decisions: decisions, session: session, columns: stdout.columns ?? 80, actor: actor, journalFile: join(stateDirName(cwd), "runs", runId, "journal.jsonl") }));
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* The setup decisions surface on a live engagement. This is the only cockpit
|
|
853
|
+
* surface that writes, and it writes exactly one way: a confirmed approve or
|
|
854
|
+
* uphold through the production command path.
|
|
855
|
+
*/
|
|
856
|
+
export async function runSetupDecisionsCockpit({ input, output, cwd, runId, actor = userInfo().username, }) {
|
|
857
|
+
const app = render(createElement(SetupDecisionsApp, { cwd, runId, actor }), { stdin: input, stdout: output, exitOnCtrlC: false, patchConsole: false });
|
|
858
|
+
try {
|
|
859
|
+
await app.waitUntilExit();
|
|
860
|
+
}
|
|
861
|
+
finally {
|
|
862
|
+
app.unmount();
|
|
863
|
+
}
|
|
488
864
|
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/** The run cockpit's rail order. Number keys are one-based positions here. */
|
|
2
|
+
export declare const RUN_VIEWS: readonly [{
|
|
3
|
+
readonly id: "run";
|
|
4
|
+
readonly label: "Run";
|
|
5
|
+
readonly title: "RUN";
|
|
6
|
+
readonly detailTitle: "RUN DETAIL";
|
|
7
|
+
}, {
|
|
8
|
+
readonly id: "tasks";
|
|
9
|
+
readonly label: "Tasks";
|
|
10
|
+
readonly title: "TASKS";
|
|
11
|
+
readonly detailTitle: "TASK DETAIL";
|
|
12
|
+
}, {
|
|
13
|
+
readonly id: "gates";
|
|
14
|
+
readonly label: "Gates";
|
|
15
|
+
readonly title: "GATES";
|
|
16
|
+
readonly detailTitle: "GATE DETAIL";
|
|
17
|
+
}, {
|
|
18
|
+
readonly id: "journal";
|
|
19
|
+
readonly label: "Journal";
|
|
20
|
+
readonly title: "JOURNAL";
|
|
21
|
+
readonly detailTitle: "EVENT DETAIL";
|
|
22
|
+
}, {
|
|
23
|
+
readonly id: "fleet";
|
|
24
|
+
readonly label: "Fleet";
|
|
25
|
+
readonly title: "FLEET";
|
|
26
|
+
readonly detailTitle: "CHANNEL DETAIL";
|
|
27
|
+
}];
|
|
28
|
+
export type RunView = (typeof RUN_VIEWS)[number];
|
|
29
|
+
export type RunViewId = RunView["id"];
|
|
30
|
+
export declare function runViewAt(index: number): RunView | undefined;
|
|
31
|
+
export declare function runViewById(id: RunViewId): RunView;
|
|
32
|
+
/** Rail movement stops at either end; it never points outside the five views. */
|
|
33
|
+
export declare function moveRunViewSelection(selection: number, delta: number): number;
|
|
34
|
+
/** A one-based number key's view, or undefined for a digit the rail does not own. */
|
|
35
|
+
export declare function runViewForNumber(input: string): RunView | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* The second tab's drawn name. It was SETUP through v1.83; the operator's UAT
|
|
38
|
+
* read that as configuration, and what the tab draws is journal-parked human
|
|
39
|
+
* decisions whose only writes are approve and uphold. Stated once here so the
|
|
40
|
+
* tab strip, the keybar hint and the surface's own header cannot drift apart.
|
|
41
|
+
*/
|
|
42
|
+
export declare const DECISIONS_TAB_TITLE = "DECISIONS";
|
|
43
|
+
/** The same name in the keybar's sentence case — the word drawn beside Tab. */
|
|
44
|
+
export declare const DECISIONS_TAB_HINT = "Decisions";
|
|
45
|
+
/**
|
|
46
|
+
* The decisions tab's rail. Every label fits the rail's own column unwrapped —
|
|
47
|
+
* a wrapped rail row is a row nothing charges to the height budget, and that is
|
|
48
|
+
* what pushed the surface past a 14-row terminal. Read-only sections say so in
|
|
49
|
+
* their body, not in a label long enough to wrap.
|
|
50
|
+
*/
|
|
51
|
+
export declare const SETUP_SECTIONS: readonly [{
|
|
52
|
+
readonly id: "decisions";
|
|
53
|
+
readonly label: "Decisions";
|
|
54
|
+
readonly title: "DECISIONS";
|
|
55
|
+
readonly readOnly: false;
|
|
56
|
+
}, {
|
|
57
|
+
readonly id: "fleet";
|
|
58
|
+
readonly label: "Fleet";
|
|
59
|
+
readonly title: "FLEET";
|
|
60
|
+
readonly readOnly: true;
|
|
61
|
+
}, {
|
|
62
|
+
readonly id: "config";
|
|
63
|
+
readonly label: "Config";
|
|
64
|
+
readonly title: "CONFIG";
|
|
65
|
+
readonly readOnly: true;
|
|
66
|
+
}];
|
|
67
|
+
export type SetupSection = (typeof SETUP_SECTIONS)[number];
|
|
68
|
+
export type SetupSectionId = SetupSection["id"];
|
|
69
|
+
/** The section a rail position names; out-of-range positions clamp to the rail. */
|
|
70
|
+
export declare function setupSectionAt(index: number): SetupSection;
|
|
71
|
+
/** Rail movement stops at either end; it never points outside the sections. */
|
|
72
|
+
export declare function moveSetupSectionSelection(selection: number, delta: number): number;
|