tickmarkr 1.80.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.
- package/dist/cli/commands/ui.d.ts +1 -1
- package/dist/cli/commands/ui.js +35 -4
- package/dist/tui/cockpit/components.d.ts +7 -44
- package/dist/tui/cockpit/components.js +6 -12
- package/dist/tui/cockpit/keys.d.ts +149 -0
- package/dist/tui/cockpit/keys.js +265 -0
- package/dist/tui/cockpit/live.d.ts +31 -0
- package/dist/tui/cockpit/live.js +135 -0
- package/dist/tui/cockpit/run-cockpit.d.ts +8 -1
- package/dist/tui/cockpit/run-cockpit.js +70 -16
- package/dist/tui/cockpit/setup-cockpit.d.ts +4 -1
- package/dist/tui/cockpit/setup-cockpit.js +96 -52
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@ type StudioIO = {
|
|
|
2
2
|
input: NodeJS.ReadStream;
|
|
3
3
|
output: NodeJS.WriteStream;
|
|
4
4
|
};
|
|
5
|
-
export declare function ui(argv: string[], io?: Partial<StudioIO
|
|
5
|
+
export declare function ui(argv: string[], io?: Partial<StudioIO>, cwd?: string): Promise<string | {
|
|
6
6
|
out: string;
|
|
7
7
|
code: number;
|
|
8
8
|
}>;
|
package/dist/cli/commands/ui.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
const NON_TTY_MSG = "tickmarkr ui:
|
|
2
|
-
export async function ui(argv, io = {}) {
|
|
1
|
+
const NON_TTY_MSG = "tickmarkr ui: the cockpit requires a TTY — use `tickmarkr fleet --print` or `tickmarkr status --watch` for line-mode output";
|
|
2
|
+
export async function ui(argv, io = {}, cwd = process.cwd()) {
|
|
3
3
|
const input = io.input ?? process.stdin;
|
|
4
4
|
const output = io.output ?? process.stdout;
|
|
5
5
|
if (input.isTTY !== true || output.isTTY !== true) {
|
|
@@ -15,7 +15,38 @@ export async function ui(argv, io = {}) {
|
|
|
15
15
|
});
|
|
16
16
|
return "ui: closed";
|
|
17
17
|
}
|
|
18
|
-
const
|
|
19
|
-
|
|
18
|
+
const unknownFlag = argv.find((arg) => arg.startsWith("-"));
|
|
19
|
+
if (unknownFlag) {
|
|
20
|
+
return { out: `tickmarkr ui: unknown flag ${unknownFlag}`, code: 1 };
|
|
21
|
+
}
|
|
22
|
+
const live = await import("../../tui/cockpit/live.js");
|
|
23
|
+
const { version } = await import("./version.js");
|
|
24
|
+
const binaryVersion = await version();
|
|
25
|
+
const explicit = argv.find((arg) => !arg.startsWith("-"));
|
|
26
|
+
let runId;
|
|
27
|
+
try {
|
|
28
|
+
runId = live.selectEngagementRunId(cwd, explicit);
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
return { out: `tickmarkr ui: ${error.message}`, code: 1 };
|
|
32
|
+
}
|
|
33
|
+
if (runId === null) {
|
|
34
|
+
return {
|
|
35
|
+
out: "tickmarkr ui: no engagement found — start one with `tickmarkr run` first",
|
|
36
|
+
code: 1,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
// Refuse before drawing anything: unreadable journal bytes must never
|
|
40
|
+
// become a plausible surface derived from nothing.
|
|
41
|
+
try {
|
|
42
|
+
live.deriveLiveRunCockpitData(live.loadEngagementSource(cwd, runId), binaryVersion);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
return {
|
|
46
|
+
out: `tickmarkr ui: cannot read engagement ${runId}: ${error.message}`,
|
|
47
|
+
code: 1,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
await live.runLiveCockpit({ input, output, cwd, runId, binaryVersion });
|
|
20
51
|
return "ui: closed";
|
|
21
52
|
}
|
|
@@ -19,50 +19,11 @@ export type KeybarItem = {
|
|
|
19
19
|
readonly label: string;
|
|
20
20
|
};
|
|
21
21
|
export declare const KEYBAR_KEYS: {
|
|
22
|
-
readonly run: readonly [
|
|
23
|
-
|
|
24
|
-
readonly
|
|
25
|
-
}, {
|
|
26
|
-
readonly key: "⏎";
|
|
27
|
-
readonly label: "Open";
|
|
28
|
-
}, {
|
|
29
|
-
readonly key: "←";
|
|
30
|
-
readonly label: "Back";
|
|
31
|
-
}, {
|
|
32
|
-
readonly key: "Tab";
|
|
33
|
-
readonly label: "Panel";
|
|
34
|
-
}, {
|
|
35
|
-
readonly key: "?";
|
|
36
|
-
readonly label: "Help";
|
|
37
|
-
}, {
|
|
38
|
-
readonly key: "q";
|
|
39
|
-
readonly label: "Quit";
|
|
40
|
-
}, {
|
|
41
|
-
readonly key: "f";
|
|
42
|
-
readonly label: "Follow";
|
|
43
|
-
}, {
|
|
44
|
-
readonly key: "/";
|
|
45
|
-
readonly label: "Filter";
|
|
46
|
-
}];
|
|
47
|
-
readonly setup: readonly [{
|
|
48
|
-
readonly key: "↑↓";
|
|
49
|
-
readonly label: "Move";
|
|
50
|
-
}, {
|
|
51
|
-
readonly key: "⏎";
|
|
22
|
+
readonly run: readonly import("./keys.js").KeybarEntry[];
|
|
23
|
+
readonly setup: readonly [import("./keys.js").KeybarEntry, {
|
|
24
|
+
readonly key: string;
|
|
52
25
|
readonly label: "Next";
|
|
53
|
-
}, {
|
|
54
|
-
readonly key: "←";
|
|
55
|
-
readonly label: "Back";
|
|
56
|
-
}, {
|
|
57
|
-
readonly key: "Tab";
|
|
58
|
-
readonly label: "Panel";
|
|
59
|
-
}, {
|
|
60
|
-
readonly key: "?";
|
|
61
|
-
readonly label: "Help";
|
|
62
|
-
}, {
|
|
63
|
-
readonly key: "q";
|
|
64
|
-
readonly label: "Quit";
|
|
65
|
-
}, {
|
|
26
|
+
}, import("./keys.js").KeybarEntry, import("./keys.js").KeybarEntry, import("./keys.js").KeybarEntry, import("./keys.js").KeybarEntry, {
|
|
66
27
|
readonly key: "␣";
|
|
67
28
|
readonly label: "Toggle";
|
|
68
29
|
}, {
|
|
@@ -171,9 +132,11 @@ export declare function StatusStrip({ items, width, }: {
|
|
|
171
132
|
items: readonly StatusStripItem[];
|
|
172
133
|
width: number;
|
|
173
134
|
}): ReactElement;
|
|
174
|
-
export declare function JournalRowPanel({ rows, width, focused, title, }: {
|
|
135
|
+
export declare function JournalRowPanel({ rows, width, focused, title, selection, }: {
|
|
175
136
|
rows: readonly JournalRow[];
|
|
176
137
|
width?: number | string;
|
|
177
138
|
focused?: boolean;
|
|
178
139
|
title?: string;
|
|
140
|
+
/** Index of the row the pointer marks; undefined draws no pointer. */
|
|
141
|
+
selection?: number;
|
|
179
142
|
}): ReactElement;
|
|
@@ -6,22 +6,16 @@ import { GLYPHS } from "../../brand.js";
|
|
|
6
6
|
import { COCKPIT_DATA_RAMP, COCKPIT_INKS, TEXT_EMPHASIS_TOKENS, } from "./theme.js";
|
|
7
7
|
import { SPARKLINE_BUCKET_WINDOW } from "./derive.js";
|
|
8
8
|
export { SPARKLINE_BUCKET_WINDOW } from "./derive.js";
|
|
9
|
+
import { keybarEntries, RUN_KEY_BINDINGS } from "./keys.js";
|
|
9
10
|
const SPARKLINE_GLYPHS = ["▁", "▂", "▃", "▄", "▅", "▆", "▇", "█"];
|
|
10
11
|
const BAND_GAP_COLUMNS = 1;
|
|
11
12
|
const PANEL_HORIZONTAL_CHROME = 4;
|
|
12
13
|
const READABLE_PANEL_COLUMNS = 20;
|
|
13
14
|
export const PANEL_CHROME_ROWS = 3;
|
|
14
15
|
export const BAND_CONTINUATION_PREFIX = "↳ ";
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
{ key: "←", label: "Back" },
|
|
19
|
-
{ key: "Tab", label: "Panel" },
|
|
20
|
-
{ key: "?", label: "Help" },
|
|
21
|
-
{ key: "q", label: "Quit" },
|
|
22
|
-
{ key: "f", label: "Follow" },
|
|
23
|
-
{ key: "/", label: "Filter" },
|
|
24
|
-
];
|
|
16
|
+
// The run keybar is generated from the registered handlers (keys.ts), so a
|
|
17
|
+
// key with no handler cannot be advertised here.
|
|
18
|
+
const RUN_KEYS = keybarEntries(RUN_KEY_BINDINGS);
|
|
25
19
|
const SETUP_ADDITIONAL_KEYS = [
|
|
26
20
|
{ key: "␣", label: "Toggle" },
|
|
27
21
|
{ key: "a", label: "All" },
|
|
@@ -275,6 +269,6 @@ export function Keybar({ surface, width, }) {
|
|
|
275
269
|
export function StatusStrip({ items, width, }) {
|
|
276
270
|
return (_jsx(OneLineItems, { width: width, label: "cockpit status", children: items.map((item, index) => (_jsxs(Box, { flexShrink: 0, children: [index > 0 && _jsx(BodyText, { emphasis: "dim", children: " \u00B7 " }), _jsx(StateGlyph, { state: item.state }), _jsxs(BodyText, { children: [" ", item.text] })] }, `${item.state}:${item.text}`))) }));
|
|
277
271
|
}
|
|
278
|
-
export function JournalRowPanel({ rows, width, focused, title = "JOURNAL", }) {
|
|
279
|
-
return (_jsx(Panel, { title: title, focused: focused, width: width, children: rows.map((row, index) => (_jsxs(Box, { flexDirection: "row", flexWrap: "nowrap", height: 1, overflow: "hidden", children: [_jsx(BodyText, { emphasis: "dim", children: row.time }), _jsx(BodyText, { children: " " }), _jsx(StateGlyph, { state: row.state }), _jsxs(BodyText, { children: [" ", row.text
|
|
272
|
+
export function JournalRowPanel({ rows, width, focused, title = "JOURNAL", selection, }) {
|
|
273
|
+
return (_jsx(Panel, { title: title, focused: focused, width: width, children: rows.map((row, index) => (_jsxs(Box, { flexDirection: "row", flexWrap: "nowrap", height: 1, overflow: "hidden", children: [_jsx(BodyText, { emphasis: "dim", children: row.time }), _jsx(BodyText, { children: " " }), _jsx(StateGlyph, { state: row.state }), _jsx(BodyText, { children: " " }), index === selection && (_jsxs(BodyText, { emphasis: "strong", children: [GLYPHS.pointer, " "] })), _jsx(BodyText, { children: row.text })] }, row.id ?? `${row.time}:${row.state}:${index}`))) }));
|
|
280
274
|
}
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE key contract for the run cockpit. This registry is the single source of
|
|
3
|
+
* truth for both halves of a key: what the keybar advertises and what the key
|
|
4
|
+
* does. The keybar's entries are derived from these bindings (`keybarEntries`),
|
|
5
|
+
* so a key with no registered handler cannot be drawn — an advertised key that
|
|
6
|
+
* does nothing is not expressible.
|
|
7
|
+
*/
|
|
8
|
+
/** The subset of an ink key event the run bindings discriminate on. */
|
|
9
|
+
export type RunKeyEvent = {
|
|
10
|
+
readonly input: string;
|
|
11
|
+
readonly key: {
|
|
12
|
+
readonly upArrow?: boolean;
|
|
13
|
+
readonly downArrow?: boolean;
|
|
14
|
+
readonly leftArrow?: boolean;
|
|
15
|
+
readonly return?: boolean;
|
|
16
|
+
readonly tab?: boolean;
|
|
17
|
+
readonly escape?: boolean;
|
|
18
|
+
readonly backspace?: boolean;
|
|
19
|
+
readonly delete?: boolean;
|
|
20
|
+
readonly ctrl?: boolean;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* The observable interaction state of the run surface. RunCockpitFrame renders
|
|
25
|
+
* every field of it — selection pointer, panel focus, opened event, follow
|
|
26
|
+
* indicator, filter prompt, help overlay — so the initial state below renders
|
|
27
|
+
* byte-identical to a frame that carries no interaction state at all, and every
|
|
28
|
+
* transition away from it is a change an operator can see.
|
|
29
|
+
*/
|
|
30
|
+
export type RunInteractionState = {
|
|
31
|
+
/** The journal row the pointer marks — moved by ↑↓, -1 while nothing is selected. */
|
|
32
|
+
readonly selection: number;
|
|
33
|
+
/** The focused panel, index into RUN_PANEL_FOCUS_ORDER — cycled by Tab. */
|
|
34
|
+
readonly panel: number;
|
|
35
|
+
/** The selection opened by ⏎, or null once ← returns from it. */
|
|
36
|
+
readonly opened: number | null;
|
|
37
|
+
/** Follow mode — toggled by f; drawn as a status-strip indicator while on. */
|
|
38
|
+
readonly follow: boolean;
|
|
39
|
+
/** The / prompt is open and taking filter text. */
|
|
40
|
+
readonly filterPrompt: boolean;
|
|
41
|
+
/** The text the journal is narrowed to — empty shows every row. */
|
|
42
|
+
readonly filterQuery: string;
|
|
43
|
+
/** Help overlay — toggled by ?. */
|
|
44
|
+
readonly help: boolean;
|
|
45
|
+
/** Quit requested by q. */
|
|
46
|
+
readonly quit: boolean;
|
|
47
|
+
};
|
|
48
|
+
export declare function initialRunInteractionState(): RunInteractionState;
|
|
49
|
+
/**
|
|
50
|
+
* The panels Tab cycles focus through, in cycle order; index 0 is the focus
|
|
51
|
+
* the surface opens with, so the initial frame is the one the anchors pin.
|
|
52
|
+
*/
|
|
53
|
+
export declare const RUN_PANEL_FOCUS_ORDER: readonly ["RUN", "VIEWS", "KEYS"];
|
|
54
|
+
export type RunKeyBinding = {
|
|
55
|
+
/** The glyph the keybar draws, e.g. "↑↓". */
|
|
56
|
+
readonly key: string;
|
|
57
|
+
/** The promise the keybar draws next to the glyph, e.g. "Move". */
|
|
58
|
+
readonly label: string;
|
|
59
|
+
/** Whether a key event activates this binding. */
|
|
60
|
+
readonly matches: (event: RunKeyEvent) => boolean;
|
|
61
|
+
/** The state transition the label promises. */
|
|
62
|
+
readonly apply: (state: RunInteractionState, event: RunKeyEvent) => RunInteractionState;
|
|
63
|
+
/**
|
|
64
|
+
* How a binding that carries on/off state reports it — the brand's reserved
|
|
65
|
+
* toggle glyphs, never bracket toggles. Present exactly on the bindings
|
|
66
|
+
* whose state is on/off.
|
|
67
|
+
*/
|
|
68
|
+
readonly report?: (state: RunInteractionState) => string;
|
|
69
|
+
};
|
|
70
|
+
/** A toggle's on/off report: the brand's reserved glyphs, never brackets. */
|
|
71
|
+
export declare function toggleReport(label: string, on: boolean): string;
|
|
72
|
+
/**
|
|
73
|
+
* Every key the run surface advertises, in keybar order. Each entry carries
|
|
74
|
+
* its handler, so advertisement and behaviour cannot drift apart.
|
|
75
|
+
*/
|
|
76
|
+
export declare const RUN_KEY_BINDINGS: readonly RunKeyBinding[];
|
|
77
|
+
/** The panels the setup surface cycles through, in their painted order. */
|
|
78
|
+
export declare const SETUP_PANEL_FOCUS_ORDER: readonly ["SETUP", "HARNESSES", "KEYS"];
|
|
79
|
+
/**
|
|
80
|
+
* The observable state owned by setup's bindings. Its initial value is the
|
|
81
|
+
* already-pinned setup frame: step two, the harness body focused, nothing
|
|
82
|
+
* selected, no modal action open. Every handler moves away from that state in
|
|
83
|
+
* a way SetupCockpitFrame paints.
|
|
84
|
+
*/
|
|
85
|
+
export type SetupInteractionState = {
|
|
86
|
+
readonly selection: number;
|
|
87
|
+
readonly panel: number;
|
|
88
|
+
readonly step: number;
|
|
89
|
+
readonly selected: readonly number[];
|
|
90
|
+
readonly all: boolean;
|
|
91
|
+
readonly probeRevision: number;
|
|
92
|
+
readonly savePrompt: boolean;
|
|
93
|
+
readonly saved: boolean;
|
|
94
|
+
readonly help: boolean;
|
|
95
|
+
readonly quit: boolean;
|
|
96
|
+
};
|
|
97
|
+
export declare function initialSetupInteractionState(): SetupInteractionState;
|
|
98
|
+
export type SetupKeyBinding = {
|
|
99
|
+
readonly key: string;
|
|
100
|
+
readonly label: string;
|
|
101
|
+
readonly matches: (event: RunKeyEvent) => boolean;
|
|
102
|
+
readonly apply: (state: SetupInteractionState, event: RunKeyEvent) => SetupInteractionState;
|
|
103
|
+
readonly report?: (state: SetupInteractionState) => string;
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* Every setup advertisement and handler, in the exact order the surface has
|
|
107
|
+
* always drawn. The global six are registered here with setup semantics; the
|
|
108
|
+
* six setup-only bindings follow them on the same surface registry.
|
|
109
|
+
*/
|
|
110
|
+
export declare const SETUP_KEY_BINDINGS: readonly SetupKeyBinding[];
|
|
111
|
+
/** One surface-aware registry serves both cockpit renderers and dispatchers. */
|
|
112
|
+
export declare const SURFACE_KEY_BINDINGS: {
|
|
113
|
+
readonly run: readonly RunKeyBinding[];
|
|
114
|
+
readonly setup: readonly SetupKeyBinding[];
|
|
115
|
+
};
|
|
116
|
+
/** Save confirmation owns input until it is accepted or dismissed. */
|
|
117
|
+
export declare function applySetupPromptInput(event: RunKeyEvent, state: SetupInteractionState): SetupInteractionState | undefined;
|
|
118
|
+
/**
|
|
119
|
+
* The transitions of the open / prompt. The prompt is not an advertised key,
|
|
120
|
+
* so it has no binding — but its transitions live here beside the registry
|
|
121
|
+
* because they rewrite the same interaction state: characters grow the query,
|
|
122
|
+
* backspace shrinks it, ⏎ applies it (the journal stays narrowed), escape
|
|
123
|
+
* cancels it. Anything else the prompt swallows unchanged.
|
|
124
|
+
*/
|
|
125
|
+
export declare function applyFilterInput(event: RunKeyEvent, state: RunInteractionState): RunInteractionState | undefined;
|
|
126
|
+
/** What an advertisement looks like once the handler is stripped away. */
|
|
127
|
+
export type KeybarEntry = {
|
|
128
|
+
readonly key: string;
|
|
129
|
+
readonly label: string;
|
|
130
|
+
};
|
|
131
|
+
/**
|
|
132
|
+
* The keybar's entries, derived from a binding registry. This is the only way
|
|
133
|
+
* a keybar list is produced: remove a handler and its entry disappears.
|
|
134
|
+
*/
|
|
135
|
+
export declare function keybarEntries(bindings: readonly {
|
|
136
|
+
readonly key: string;
|
|
137
|
+
readonly label: string;
|
|
138
|
+
}[]): readonly KeybarEntry[];
|
|
139
|
+
/** The binding a key event activates, or undefined when no handler owns it. */
|
|
140
|
+
export declare function resolveRunKeyBinding(bindings: readonly RunKeyBinding[], event: RunKeyEvent): RunKeyBinding | undefined;
|
|
141
|
+
/**
|
|
142
|
+
* Send a key event to the registry: the matching binding's handler produces
|
|
143
|
+
* the next interaction state. An event no handler owns changes nothing.
|
|
144
|
+
*/
|
|
145
|
+
export declare function dispatchRunKey(event: RunKeyEvent, state: RunInteractionState, bindings?: readonly RunKeyBinding[]): RunInteractionState | undefined;
|
|
146
|
+
/** The setup binding a key event activates, or undefined when none owns it. */
|
|
147
|
+
export declare function resolveSetupKeyBinding(bindings: readonly SetupKeyBinding[], event: RunKeyEvent): SetupKeyBinding | undefined;
|
|
148
|
+
/** Send a key event through the setup half of the shared surface registry. */
|
|
149
|
+
export declare function dispatchSetupKey(event: RunKeyEvent, state: SetupInteractionState, bindings?: readonly SetupKeyBinding[]): SetupInteractionState | undefined;
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import { GLYPHS } from "../../brand.js";
|
|
2
|
+
export function initialRunInteractionState() {
|
|
3
|
+
return {
|
|
4
|
+
selection: -1,
|
|
5
|
+
panel: 0,
|
|
6
|
+
opened: null,
|
|
7
|
+
follow: false,
|
|
8
|
+
filterPrompt: false,
|
|
9
|
+
filterQuery: "",
|
|
10
|
+
help: false,
|
|
11
|
+
quit: false,
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The panels Tab cycles focus through, in cycle order; index 0 is the focus
|
|
16
|
+
* the surface opens with, so the initial frame is the one the anchors pin.
|
|
17
|
+
*/
|
|
18
|
+
export const RUN_PANEL_FOCUS_ORDER = ["RUN", "VIEWS", "KEYS"];
|
|
19
|
+
/** A toggle's on/off report: the brand's reserved glyphs, never brackets. */
|
|
20
|
+
export function toggleReport(label, on) {
|
|
21
|
+
return `${label} ${on ? GLYPHS.toggleActive : GLYPHS.toggleInactive}`;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Every key the run surface advertises, in keybar order. Each entry carries
|
|
25
|
+
* its handler, so advertisement and behaviour cannot drift apart.
|
|
26
|
+
*/
|
|
27
|
+
export const RUN_KEY_BINDINGS = [
|
|
28
|
+
{
|
|
29
|
+
key: "↑↓",
|
|
30
|
+
label: "Move",
|
|
31
|
+
matches: ({ key }) => key.upArrow === true || key.downArrow === true,
|
|
32
|
+
apply: (state, { key }) => ({
|
|
33
|
+
...state,
|
|
34
|
+
selection: Math.max(-1, state.selection + (key.upArrow === true ? -1 : 1)),
|
|
35
|
+
}),
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
key: "⏎",
|
|
39
|
+
label: "Open",
|
|
40
|
+
matches: ({ key }) => key.return === true,
|
|
41
|
+
apply: (state) => state.selection >= 0 ? { ...state, opened: state.selection } : state,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
key: "←",
|
|
45
|
+
label: "Back",
|
|
46
|
+
matches: ({ key }) => key.leftArrow === true,
|
|
47
|
+
apply: (state) => ({ ...state, opened: null }),
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
key: "Tab",
|
|
51
|
+
label: "Panel",
|
|
52
|
+
matches: ({ key }) => key.tab === true,
|
|
53
|
+
apply: (state) => ({
|
|
54
|
+
...state,
|
|
55
|
+
panel: (state.panel + 1) % RUN_PANEL_FOCUS_ORDER.length,
|
|
56
|
+
}),
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
key: "?",
|
|
60
|
+
label: "Help",
|
|
61
|
+
matches: ({ input }) => input === "?",
|
|
62
|
+
apply: (state) => ({ ...state, help: !state.help }),
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
key: "q",
|
|
66
|
+
label: "Quit",
|
|
67
|
+
matches: ({ input }) => input === "q",
|
|
68
|
+
apply: (state) => ({ ...state, quit: true }),
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
key: "f",
|
|
72
|
+
label: "Follow",
|
|
73
|
+
matches: ({ input }) => input === "f",
|
|
74
|
+
apply: (state) => ({ ...state, follow: !state.follow }),
|
|
75
|
+
report: (state) => toggleReport("Follow", state.follow),
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
key: "/",
|
|
79
|
+
label: "Filter",
|
|
80
|
+
matches: ({ input }) => input === "/",
|
|
81
|
+
apply: (state) => ({ ...state, filterPrompt: true, filterQuery: "" }),
|
|
82
|
+
},
|
|
83
|
+
];
|
|
84
|
+
/** The panels the setup surface cycles through, in their painted order. */
|
|
85
|
+
export const SETUP_PANEL_FOCUS_ORDER = ["SETUP", "HARNESSES", "KEYS"];
|
|
86
|
+
export function initialSetupInteractionState() {
|
|
87
|
+
return {
|
|
88
|
+
selection: -1,
|
|
89
|
+
panel: 1,
|
|
90
|
+
step: 2,
|
|
91
|
+
selected: [],
|
|
92
|
+
all: false,
|
|
93
|
+
probeRevision: 0,
|
|
94
|
+
savePrompt: false,
|
|
95
|
+
saved: false,
|
|
96
|
+
help: false,
|
|
97
|
+
quit: false,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function setupStep(state, delta) {
|
|
101
|
+
return { ...state, step: Math.min(6, Math.max(1, state.step + delta)) };
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Every setup advertisement and handler, in the exact order the surface has
|
|
105
|
+
* always drawn. The global six are registered here with setup semantics; the
|
|
106
|
+
* six setup-only bindings follow them on the same surface registry.
|
|
107
|
+
*/
|
|
108
|
+
export const SETUP_KEY_BINDINGS = [
|
|
109
|
+
{
|
|
110
|
+
key: "↑↓",
|
|
111
|
+
label: "Move",
|
|
112
|
+
matches: ({ key }) => key.upArrow === true || key.downArrow === true,
|
|
113
|
+
apply: (state, { key }) => ({
|
|
114
|
+
...state,
|
|
115
|
+
selection: key.upArrow === true
|
|
116
|
+
? Math.max(0, state.selection - 1)
|
|
117
|
+
: state.selection + 1,
|
|
118
|
+
}),
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
key: "⏎",
|
|
122
|
+
label: "Next",
|
|
123
|
+
matches: ({ key }) => key.return === true,
|
|
124
|
+
apply: (state) => setupStep(state, 1),
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
key: "←",
|
|
128
|
+
label: "Back",
|
|
129
|
+
matches: ({ key }) => key.leftArrow === true,
|
|
130
|
+
apply: (state) => setupStep(state, -1),
|
|
131
|
+
},
|
|
132
|
+
{
|
|
133
|
+
key: "Tab",
|
|
134
|
+
label: "Panel",
|
|
135
|
+
matches: ({ key }) => key.tab === true,
|
|
136
|
+
apply: (state) => ({
|
|
137
|
+
...state,
|
|
138
|
+
panel: (state.panel + 1) % SETUP_PANEL_FOCUS_ORDER.length,
|
|
139
|
+
}),
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
key: "?",
|
|
143
|
+
label: "Help",
|
|
144
|
+
matches: ({ input }) => input === "?",
|
|
145
|
+
apply: (state) => ({ ...state, help: !state.help }),
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
key: "q",
|
|
149
|
+
label: "Quit",
|
|
150
|
+
matches: ({ input }) => input === "q",
|
|
151
|
+
apply: (state) => ({ ...state, quit: true }),
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
key: "␣",
|
|
155
|
+
label: "Toggle",
|
|
156
|
+
matches: ({ input }) => input === " ",
|
|
157
|
+
apply: (state) => {
|
|
158
|
+
const selection = Math.max(0, state.selection);
|
|
159
|
+
const selected = state.selected.includes(selection)
|
|
160
|
+
? state.selected.filter((index) => index !== selection)
|
|
161
|
+
: [...state.selected, selection];
|
|
162
|
+
return { ...state, selection, selected, all: false };
|
|
163
|
+
},
|
|
164
|
+
report: (state) => toggleReport("Selection", state.all || state.selected.length > 0),
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
key: "a",
|
|
168
|
+
label: "All",
|
|
169
|
+
matches: ({ input }) => input === "a",
|
|
170
|
+
apply: (state) => ({ ...state, all: true, selected: [] }),
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
key: "r",
|
|
174
|
+
label: "Re-probe",
|
|
175
|
+
matches: ({ input }) => input === "r",
|
|
176
|
+
apply: (state) => ({ ...state, probeRevision: state.probeRevision + 1 }),
|
|
177
|
+
},
|
|
178
|
+
{
|
|
179
|
+
key: "s",
|
|
180
|
+
label: "Save",
|
|
181
|
+
matches: ({ input }) => input === "s",
|
|
182
|
+
apply: (state) => ({ ...state, savePrompt: true }),
|
|
183
|
+
report: (state) => toggleReport("Saved", state.saved),
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
key: "n",
|
|
187
|
+
label: "Next",
|
|
188
|
+
matches: ({ input }) => input === "n",
|
|
189
|
+
apply: (state) => setupStep(state, 1),
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
key: "p",
|
|
193
|
+
label: "Prev",
|
|
194
|
+
matches: ({ input }) => input === "p",
|
|
195
|
+
apply: (state) => setupStep(state, -1),
|
|
196
|
+
},
|
|
197
|
+
];
|
|
198
|
+
/** One surface-aware registry serves both cockpit renderers and dispatchers. */
|
|
199
|
+
export const SURFACE_KEY_BINDINGS = {
|
|
200
|
+
run: RUN_KEY_BINDINGS,
|
|
201
|
+
setup: SETUP_KEY_BINDINGS,
|
|
202
|
+
};
|
|
203
|
+
/** Save confirmation owns input until it is accepted or dismissed. */
|
|
204
|
+
export function applySetupPromptInput(event, state) {
|
|
205
|
+
if (!state.savePrompt)
|
|
206
|
+
return undefined;
|
|
207
|
+
if (event.input === "y" || event.key.return === true) {
|
|
208
|
+
return { ...state, savePrompt: false, saved: true };
|
|
209
|
+
}
|
|
210
|
+
if (event.input === "n" || event.key.escape === true) {
|
|
211
|
+
return { ...state, savePrompt: false };
|
|
212
|
+
}
|
|
213
|
+
return state;
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* The transitions of the open / prompt. The prompt is not an advertised key,
|
|
217
|
+
* so it has no binding — but its transitions live here beside the registry
|
|
218
|
+
* because they rewrite the same interaction state: characters grow the query,
|
|
219
|
+
* backspace shrinks it, ⏎ applies it (the journal stays narrowed), escape
|
|
220
|
+
* cancels it. Anything else the prompt swallows unchanged.
|
|
221
|
+
*/
|
|
222
|
+
export function applyFilterInput(event, state) {
|
|
223
|
+
if (!state.filterPrompt)
|
|
224
|
+
return undefined;
|
|
225
|
+
const { input, key } = event;
|
|
226
|
+
if (key.return === true)
|
|
227
|
+
return { ...state, filterPrompt: false };
|
|
228
|
+
if (key.escape === true) {
|
|
229
|
+
return { ...state, filterPrompt: false, filterQuery: "" };
|
|
230
|
+
}
|
|
231
|
+
if (key.backspace === true || key.delete === true) {
|
|
232
|
+
return { ...state, filterQuery: [...state.filterQuery].slice(0, -1).join("") };
|
|
233
|
+
}
|
|
234
|
+
if (input.length === 1 && key.ctrl !== true) {
|
|
235
|
+
return { ...state, filterQuery: state.filterQuery + input };
|
|
236
|
+
}
|
|
237
|
+
return state;
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* The keybar's entries, derived from a binding registry. This is the only way
|
|
241
|
+
* a keybar list is produced: remove a handler and its entry disappears.
|
|
242
|
+
*/
|
|
243
|
+
export function keybarEntries(bindings) {
|
|
244
|
+
return bindings.map(({ key, label }) => ({ key, label }));
|
|
245
|
+
}
|
|
246
|
+
/** The binding a key event activates, or undefined when no handler owns it. */
|
|
247
|
+
export function resolveRunKeyBinding(bindings, event) {
|
|
248
|
+
return bindings.find((binding) => binding.matches(event));
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Send a key event to the registry: the matching binding's handler produces
|
|
252
|
+
* the next interaction state. An event no handler owns changes nothing.
|
|
253
|
+
*/
|
|
254
|
+
export function dispatchRunKey(event, state, bindings = RUN_KEY_BINDINGS) {
|
|
255
|
+
const binding = resolveRunKeyBinding(bindings, event);
|
|
256
|
+
return binding?.apply(state, event);
|
|
257
|
+
}
|
|
258
|
+
/** The setup binding a key event activates, or undefined when none owns it. */
|
|
259
|
+
export function resolveSetupKeyBinding(bindings, event) {
|
|
260
|
+
return bindings.find((binding) => binding.matches(event));
|
|
261
|
+
}
|
|
262
|
+
/** Send a key event through the setup half of the shared surface registry. */
|
|
263
|
+
export function dispatchSetupKey(event, state, bindings = SETUP_KEY_BINDINGS) {
|
|
264
|
+
return resolveSetupKeyBinding(bindings, event)?.apply(state, event);
|
|
265
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { RunCockpitData, RunCockpitSource } from "./derive.js";
|
|
2
|
+
/**
|
|
3
|
+
* THE engagement selection rule — stated once, here, so it can change in one
|
|
4
|
+
* place without touching the renderer or the command: an explicit engagement
|
|
5
|
+
* reference from the command line wins; the bare command opens the most
|
|
6
|
+
* recently started engagement that has a readable journal.
|
|
7
|
+
*/
|
|
8
|
+
export declare function selectEngagementRunId(cwd: string, explicit?: string): string | null;
|
|
9
|
+
/**
|
|
10
|
+
* The journal bytes of a real engagement — never a committed capture. A read
|
|
11
|
+
* failure throws: the caller refuses rather than drawing from nothing.
|
|
12
|
+
*/
|
|
13
|
+
export declare function loadEngagementSource(cwd: string, runId: string): RunCockpitSource;
|
|
14
|
+
export declare function formatEventAge(ageMs: number): string;
|
|
15
|
+
/**
|
|
16
|
+
* Live derivation over a real engagement's journal bytes. The header's
|
|
17
|
+
* elapsed field additionally states how long ago the engagement's last event
|
|
18
|
+
* arrived, so an engagement that has stopped moving reads as stale rather
|
|
19
|
+
* than as current. Throws on unreadable bytes (empty capture, no run-start) —
|
|
20
|
+
* the caller refuses instead of rendering a plausible surface from nothing.
|
|
21
|
+
*/
|
|
22
|
+
export declare function deriveLiveRunCockpitData(source: RunCockpitSource, binaryVersion: string, now?: () => number): RunCockpitData;
|
|
23
|
+
export declare function runLiveCockpit({ input, output, cwd, runId, binaryVersion, refreshMs, now, }: {
|
|
24
|
+
input: NodeJS.ReadStream;
|
|
25
|
+
output: NodeJS.WriteStream;
|
|
26
|
+
cwd: string;
|
|
27
|
+
runId: string;
|
|
28
|
+
binaryVersion: string;
|
|
29
|
+
refreshMs?: number;
|
|
30
|
+
now?: () => number;
|
|
31
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { render, useApp, useInput } from "ink";
|
|
4
|
+
import { createElement, useEffect, useState } from "react";
|
|
5
|
+
import { stateDirName } from "../../graph/graph.js";
|
|
6
|
+
import { Journal, parseRunId } from "../../run/journal.js";
|
|
7
|
+
import { deriveRunCockpitData, RunCockpitFrame } from "./run-cockpit.js";
|
|
8
|
+
import { applyFilterInput, dispatchRunKey, initialRunInteractionState, SURFACE_KEY_BINDINGS, } from "./keys.js";
|
|
9
|
+
// ponytail: fixed 2s re-derive cadence, matching `status --watch`; promote to a
|
|
10
|
+
// config knob only if an operator asks.
|
|
11
|
+
const REFRESH_MS = 2_000;
|
|
12
|
+
/**
|
|
13
|
+
* THE engagement selection rule — stated once, here, so it can change in one
|
|
14
|
+
* place without touching the renderer or the command: an explicit engagement
|
|
15
|
+
* reference from the command line wins; the bare command opens the most
|
|
16
|
+
* recently started engagement that has a readable journal.
|
|
17
|
+
*/
|
|
18
|
+
export function selectEngagementRunId(cwd, explicit) {
|
|
19
|
+
if (explicit !== undefined)
|
|
20
|
+
return parseRunId(explicit);
|
|
21
|
+
return Journal.latestRunId(cwd, { withJournal: true });
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The journal bytes of a real engagement — never a committed capture. A read
|
|
25
|
+
* failure throws: the caller refuses rather than drawing from nothing.
|
|
26
|
+
*/
|
|
27
|
+
export function loadEngagementSource(cwd, runId) {
|
|
28
|
+
const id = parseRunId(runId);
|
|
29
|
+
const path = join(cwd, stateDirName(cwd), "runs", id, "journal.jsonl");
|
|
30
|
+
const raw = readFileSync(path, "utf8");
|
|
31
|
+
return { fileName: `${id}.journal.jsonl`, raw };
|
|
32
|
+
}
|
|
33
|
+
// The same s/m/h age vocabulary `status` speaks (status.ts fmtAge).
|
|
34
|
+
export function formatEventAge(ageMs) {
|
|
35
|
+
const safe = Math.max(0, ageMs);
|
|
36
|
+
if (safe < 90_000)
|
|
37
|
+
return `${Math.floor(safe / 1_000)}s`;
|
|
38
|
+
if (safe < 5_400_000)
|
|
39
|
+
return `${Math.floor(safe / 60_000)}m`;
|
|
40
|
+
return `${Math.floor(safe / 3_600_000)}h`;
|
|
41
|
+
}
|
|
42
|
+
function lastEventMs(raw) {
|
|
43
|
+
const lines = raw.split("\n");
|
|
44
|
+
for (let index = lines.length - 1; index >= 0; index -= 1) {
|
|
45
|
+
const line = lines[index].trim();
|
|
46
|
+
if (!line)
|
|
47
|
+
continue;
|
|
48
|
+
try {
|
|
49
|
+
const parsed = JSON.parse(line);
|
|
50
|
+
const ms = typeof parsed.ts === "string" ? Date.parse(parsed.ts) : NaN;
|
|
51
|
+
if (Number.isFinite(ms))
|
|
52
|
+
return ms;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
// a torn trailing line does not hide the event written before it
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Live derivation over a real engagement's journal bytes. The header's
|
|
62
|
+
* elapsed field additionally states how long ago the engagement's last event
|
|
63
|
+
* arrived, so an engagement that has stopped moving reads as stale rather
|
|
64
|
+
* than as current. Throws on unreadable bytes (empty capture, no run-start) —
|
|
65
|
+
* the caller refuses instead of rendering a plausible surface from nothing.
|
|
66
|
+
*/
|
|
67
|
+
export function deriveLiveRunCockpitData(source, binaryVersion, now = Date.now) {
|
|
68
|
+
const data = deriveRunCockpitData(source, binaryVersion);
|
|
69
|
+
const lastMs = lastEventMs(source.raw);
|
|
70
|
+
const staleness = lastMs === undefined
|
|
71
|
+
? "last event unknown"
|
|
72
|
+
: `last event ${formatEventAge(now() - lastMs)} ago`;
|
|
73
|
+
return { ...data, elapsed: `${data.elapsed} · ${staleness}` };
|
|
74
|
+
}
|
|
75
|
+
function LiveApp({ cwd, runId, binaryVersion, columns, rows, refreshMs, now, }) {
|
|
76
|
+
const { exit } = useApp();
|
|
77
|
+
const [data, setData] = useState(() => deriveLiveRunCockpitData(loadEngagementSource(cwd, runId), binaryVersion, now));
|
|
78
|
+
const [interaction, setInteraction] = useState(initialRunInteractionState);
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
const timer = setInterval(() => {
|
|
81
|
+
try {
|
|
82
|
+
setData(deriveLiveRunCockpitData(loadEngagementSource(cwd, runId), binaryVersion, now));
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
// a torn read mid-append keeps the last good frame rather than blanking the surface
|
|
86
|
+
}
|
|
87
|
+
}, refreshMs);
|
|
88
|
+
return () => clearInterval(timer);
|
|
89
|
+
}, [cwd, runId, binaryVersion, refreshMs, now]);
|
|
90
|
+
useInput((input, key) => {
|
|
91
|
+
if (key.ctrl && input === "c") {
|
|
92
|
+
exit();
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
// While the / prompt is open it owns the keyboard: characters grow the
|
|
96
|
+
// query and no advertised binding fires.
|
|
97
|
+
if (interaction.filterPrompt) {
|
|
98
|
+
const prompted = applyFilterInput({ input, key }, interaction);
|
|
99
|
+
if (prompted !== undefined)
|
|
100
|
+
setInteraction(prompted);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
const next = dispatchRunKey({ input, key }, interaction, SURFACE_KEY_BINDINGS.run);
|
|
104
|
+
if (next === undefined)
|
|
105
|
+
return;
|
|
106
|
+
setInteraction(next);
|
|
107
|
+
if (next.quit)
|
|
108
|
+
exit();
|
|
109
|
+
});
|
|
110
|
+
return createElement(RunCockpitFrame, { data, columns, rows, interaction });
|
|
111
|
+
}
|
|
112
|
+
export async function runLiveCockpit({ input, output, cwd, runId, binaryVersion, refreshMs = REFRESH_MS, now = Date.now, }) {
|
|
113
|
+
const columns = output.columns ?? 80;
|
|
114
|
+
const rows = output.rows ?? 24;
|
|
115
|
+
const app = render(createElement(LiveApp, {
|
|
116
|
+
cwd,
|
|
117
|
+
runId,
|
|
118
|
+
binaryVersion,
|
|
119
|
+
columns,
|
|
120
|
+
rows,
|
|
121
|
+
refreshMs,
|
|
122
|
+
now,
|
|
123
|
+
}), {
|
|
124
|
+
stdin: input,
|
|
125
|
+
stdout: output,
|
|
126
|
+
exitOnCtrlC: false,
|
|
127
|
+
patchConsole: false,
|
|
128
|
+
});
|
|
129
|
+
try {
|
|
130
|
+
await app.waitUntilExit();
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
app.unmount();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ReactElement } from "react";
|
|
2
2
|
import type { RunCockpitData } from "./derive.js";
|
|
3
|
+
import { type RunInteractionState } from "./keys.js";
|
|
3
4
|
export { deriveRunCockpitData } from "./derive.js";
|
|
4
5
|
export type { RunCockpitData } from "./derive.js";
|
|
5
6
|
export { PANEL_CHROME_ROWS } from "./components.js";
|
|
@@ -12,8 +13,14 @@ export { PANEL_CHROME_ROWS } from "./components.js";
|
|
|
12
13
|
export declare function wrappedRows(text: string, columns: number): number;
|
|
13
14
|
/** The rows a bordered panel occupies when its body wraps into `columns`. */
|
|
14
15
|
export declare function panelRows(lines: readonly string[], columns: number): number;
|
|
15
|
-
export declare function RunCockpitFrame({ data, columns, rows, }: {
|
|
16
|
+
export declare function RunCockpitFrame({ data, columns, rows, interaction, }: {
|
|
16
17
|
data: RunCockpitData;
|
|
17
18
|
columns: number;
|
|
18
19
|
rows?: number;
|
|
20
|
+
/**
|
|
21
|
+
* The run surface's interaction state. Omitted — or at its initial value —
|
|
22
|
+
* the frame is byte-identical to the pinned appearance; every transition a
|
|
23
|
+
* binding makes is drawn from it.
|
|
24
|
+
*/
|
|
25
|
+
interaction?: RunInteractionState;
|
|
19
26
|
}): ReactElement;
|
|
@@ -3,6 +3,7 @@ import { Box } from "ink";
|
|
|
3
3
|
import { cloneElement, } from "react";
|
|
4
4
|
import { GLYPHS, PLAIN_COMPACT_LOCKUP, } from "../../brand.js";
|
|
5
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";
|
|
6
7
|
import { resolveCockpitLayout, } from "./layout.js";
|
|
7
8
|
export { deriveRunCockpitData } from "./derive.js";
|
|
8
9
|
export { PANEL_CHROME_ROWS } from "./components.js";
|
|
@@ -88,19 +89,61 @@ function NarrowHeaderBand({ columns, content, rightStrong = false, }) {
|
|
|
88
89
|
const widths = allocateBandColumns(columns, content, NARROW_HEADER_ALLOCATION);
|
|
89
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" }) })] }));
|
|
90
91
|
}
|
|
91
|
-
function NavigationPanel() {
|
|
92
|
-
return (_jsxs(Panel, { title: "VIEWS", width: 16, 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" })] }));
|
|
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" })] }));
|
|
93
94
|
}
|
|
94
|
-
function KeysPanel() {
|
|
95
|
-
return (_jsx(Panel, { title: "KEYS", width: 16, children: KEYBAR_KEYS.run.map((item) => (_jsxs(Box, { children: [_jsx(BodyText, { emphasis: "strong", children: item.key }), _jsxs(BodyText, { children: [" ", item.label] })] }, item.key))) }));
|
|
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))) }));
|
|
96
97
|
}
|
|
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, }) {
|
|
98
106
|
const visible = rows.slice(0, bodyRows);
|
|
99
107
|
const emptyRows = Math.max(0, bodyRows - visible.length);
|
|
100
|
-
const journal = JournalRowPanel({ rows: visible });
|
|
108
|
+
const journal = JournalRowPanel({ rows: visible, title, selection });
|
|
101
109
|
const journalProps = journal.props;
|
|
102
110
|
return cloneElement(journal, {}, journalProps.children, emptyRows > 0 ? _jsx(Box, { height: emptyRows }, "journal-space") : null);
|
|
103
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
|
+
}
|
|
104
147
|
function RunStats({ data, mode, }) {
|
|
105
148
|
if (mode === "summary") {
|
|
106
149
|
return (_jsx(BodyText, { children: `tasks ${data.tasks.done}/${data.tasks.total} · gates ${data.gates.passed}/${data.gates.total} · pass ${data.passRate.value}%` }));
|
|
@@ -154,7 +197,7 @@ function compactJournalRows(rows, layout, data, columns) {
|
|
|
154
197
|
+ 3; // journal border and title
|
|
155
198
|
return Math.max(1, Math.min(layout.journalRows, Math.floor(rows) - fixedRows));
|
|
156
199
|
}
|
|
157
|
-
function LadderRunPanel({ data, layout, columns, rows, compactMeterWidth, }) {
|
|
200
|
+
function LadderRunPanel({ data, layout, columns, rows, compactMeterWidth, interaction, focused = true, }) {
|
|
158
201
|
const compactBodyRows = compactJournalRows(rows, layout, data, columns);
|
|
159
202
|
// The approved bordered progress panel has the same meter/caption as the
|
|
160
203
|
// compact form plus its border, title row and closing border. It is restored
|
|
@@ -169,16 +212,27 @@ function LadderRunPanel({ data, layout, columns, rows, compactMeterWidth, }) {
|
|
|
169
212
|
const journalBodyRows = approvedArrangement
|
|
170
213
|
? compactBodyRows - approvedProgressChromeRows
|
|
171
214
|
: compactBodyRows;
|
|
172
|
-
return (_jsxs(Panel, { title: "RUN", focused:
|
|
215
|
+
return (_jsxs(Panel, { title: "RUN", focused: focused, flexGrow: 1, children: [_jsx(RunStats, { data: data, mode: layout.stats.mode }), layout.elements.progressBar && (approvedArrangement
|
|
173
216
|
? (_jsx(ApprovedProgress, { data: data, caption: layout.elements.progressCaption }))
|
|
174
|
-
: (_jsx(CompactProgress, { data: data, caption: layout.elements.progressCaption, meterWidth: compactMeterWidth }))), _jsx(
|
|
175
|
-
}
|
|
176
|
-
function LegacyRunPanel({ data }) {
|
|
177
|
-
|
|
178
|
-
}
|
|
179
|
-
|
|
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;
|
|
180
234
|
if (rows === undefined) {
|
|
181
|
-
return (_jsxs(Box, { flexDirection: "column", width: columns, children: [_jsx(IdentityHeader, { data: data, columns: columns }), _jsxs(Box, { flexDirection: "row", gap: 1, children: [_jsx(NavigationPanel, {}), _jsx(LegacyRunPanel, { data: data }), _jsx(KeysPanel, {})] }), _jsx(StatusStrip, { items:
|
|
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 })] }));
|
|
182
236
|
}
|
|
183
237
|
const layout = resolveCockpitLayout(columns, rows);
|
|
184
238
|
if (layout.renderer === "plain")
|
|
@@ -190,5 +244,5 @@ export function RunCockpitFrame({ data, columns, rows, }) {
|
|
|
190
244
|
...layout,
|
|
191
245
|
stats: { ...layout.stats, mode: "summary", rows: 1 },
|
|
192
246
|
};
|
|
193
|
-
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, {}), _jsx(LadderRunPanel, { data: data, layout: contentLayout, columns: columns, rows: rows, compactMeterWidth: showSideRails ? undefined : 24 }), showSideRails && _jsx(KeysPanel, {})] }), _jsx(StatusStrip, { items:
|
|
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 })] }));
|
|
194
248
|
}
|
|
@@ -2,6 +2,7 @@ import { type ReactElement } from "react";
|
|
|
2
2
|
import { type TickmarkrConfig } from "../../config/config.js";
|
|
3
3
|
import { type ComponentState, type JournalRow } from "./components.js";
|
|
4
4
|
import type { DemoCaptures } from "./demo.js";
|
|
5
|
+
import { type SetupInteractionState, type SetupKeyBinding } from "./keys.js";
|
|
5
6
|
export type SetupCockpitData = {
|
|
6
7
|
readonly binaryVersion: string;
|
|
7
8
|
readonly harnesses: readonly {
|
|
@@ -34,8 +35,10 @@ export declare function deriveSetupCockpitData(captures: DemoCaptures, binaryVer
|
|
|
34
35
|
* describes the fleet.
|
|
35
36
|
*/
|
|
36
37
|
export declare const SETUP_DETAIL_ORDER: readonly ["detected", "consultsAndReviewers", "overlayDiff", "fleetAndGates"];
|
|
37
|
-
export declare function SetupCockpitFrame({ data, columns, rows, }: {
|
|
38
|
+
export declare function SetupCockpitFrame({ data, columns, rows, interaction, bindings, }: {
|
|
38
39
|
data: SetupCockpitData;
|
|
39
40
|
columns: number;
|
|
40
41
|
rows?: number;
|
|
42
|
+
interaction?: SetupInteractionState;
|
|
43
|
+
bindings?: readonly SetupKeyBinding[];
|
|
41
44
|
}): ReactElement;
|
|
@@ -5,10 +5,12 @@ import { parse } from "yaml";
|
|
|
5
5
|
import { BANNER, GLYPHS, PLAIN_BANNER, } from "../../brand.js";
|
|
6
6
|
import { DEFAULT_CONFIG, INTEGRITY_FLOOR_SHAPES, TIER_RANK, TickmarkrConfigSchema, unifiedYamlDiff, } from "../../config/config.js";
|
|
7
7
|
import { SHAPES } from "../../graph/schema.js";
|
|
8
|
-
import { allocateBandColumns, BandLines, BodyText, CockpitGrid, composeBandLine, JournalRowPanel,
|
|
8
|
+
import { allocateBandColumns, BandLines, BodyText, CockpitGrid, composeBandLine, JournalRowPanel, measureBandRows, Panel, StateGlyph, StatTile, StatusStrip, } from "./components.js";
|
|
9
9
|
import { FULL_JOURNAL_ROWS, resolveCockpitLayout, } from "./layout.js";
|
|
10
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";
|
|
11
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));
|
|
12
14
|
function deepMerge(base, overlay) {
|
|
13
15
|
if (overlay === undefined || overlay === null)
|
|
14
16
|
return base;
|
|
@@ -158,28 +160,79 @@ export function deriveSetupCockpitData(captures, binaryVersion) {
|
|
|
158
160
|
function identityBannerLines() {
|
|
159
161
|
return (process.env.NO_COLOR === undefined ? BANNER : PLAIN_BANNER).trimEnd().split("\n");
|
|
160
162
|
}
|
|
161
|
-
function identityVersionLines(data) {
|
|
162
|
-
return [
|
|
163
|
+
function identityVersionLines(data, interaction) {
|
|
164
|
+
return [
|
|
165
|
+
`v${data.binaryVersion} · binary ${GLYPHS.pass}`,
|
|
166
|
+
`setup · step ${interaction?.step ?? 2}/6`,
|
|
167
|
+
];
|
|
163
168
|
}
|
|
164
|
-
function IdentityHeader({ data, secondary = true, }) {
|
|
169
|
+
function IdentityHeader({ data, secondary = true, interaction, }) {
|
|
165
170
|
const mark = identityBannerLines();
|
|
166
|
-
const [version, step] = identityVersionLines(data);
|
|
171
|
+
const [version, step] = identityVersionLines(data, interaction);
|
|
167
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 })] })] }));
|
|
168
173
|
}
|
|
169
|
-
function NavigationPanel({ width = 16, compact = false, }) {
|
|
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
|
+
];
|
|
170
184
|
if (!compact) {
|
|
171
|
-
return (
|
|
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))) }));
|
|
172
186
|
}
|
|
173
|
-
return (
|
|
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))) }));
|
|
174
188
|
}
|
|
175
|
-
function KeysPanel({ width = 16 }) {
|
|
176
|
-
return (_jsx(Panel, { title: "KEYS", width: width, children:
|
|
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))) }));
|
|
177
191
|
}
|
|
178
|
-
function CompactKeysPanel({ width = 16 }) {
|
|
192
|
+
function CompactKeysPanel({ bindings, width = 16, focused = false, }) {
|
|
179
193
|
const contentColumns = Math.max(1, width - 4);
|
|
180
|
-
return (_jsx(Panel, { title: "KEYS", width: width, children:
|
|
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;
|
|
181
234
|
}
|
|
182
|
-
function fullKeysFit(rows, layout, data, columns) {
|
|
235
|
+
function fullKeysFit(rows, layout, data, columns, bindings) {
|
|
183
236
|
if (!layout.elements.secondaryHeader)
|
|
184
237
|
return false;
|
|
185
238
|
const wrappedKeyRows = {
|
|
@@ -187,7 +240,7 @@ function fullKeysFit(rows, layout, data, columns) {
|
|
|
187
240
|
"folded-keys": 2,
|
|
188
241
|
stacked: 4,
|
|
189
242
|
}[layout.arrangement];
|
|
190
|
-
const panelRows =
|
|
243
|
+
const panelRows = bindings.length
|
|
191
244
|
+ wrappedKeyRows
|
|
192
245
|
+ 3; // panel borders and title row
|
|
193
246
|
return headerRowsFor(layout, data, columns) + 2 + panelRows <= Math.floor(rows);
|
|
@@ -423,7 +476,7 @@ function baseRows(layout, data, columns, inner) {
|
|
|
423
476
|
+ statRowsFor(layout, data, inner)
|
|
424
477
|
+ PANEL_CHROME_ROWS; // review border and title
|
|
425
478
|
}
|
|
426
|
-
function LadderSetupPanel({ data, layout, columns, rows, }) {
|
|
479
|
+
function LadderSetupPanel({ data, layout, columns, rows, interaction, bindings, focused = true, }) {
|
|
427
480
|
const inner = contentInnerWidth(columns, layout);
|
|
428
481
|
const preferredReviewRows = Math.min(FULL_JOURNAL_ROWS, layout.journalRows);
|
|
429
482
|
const base = baseRows(layout, data, columns, inner);
|
|
@@ -434,34 +487,32 @@ function LadderSetupPanel({ data, layout, columns, rows, }) {
|
|
|
434
487
|
const visibleReviewRows = details.complete
|
|
435
488
|
? spare
|
|
436
489
|
: Math.min(preferredReviewRows, spare);
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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";
|
|
455
513
|
if (rows === undefined) {
|
|
456
514
|
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 })] }));
|
|
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 })] }));
|
|
465
516
|
}
|
|
466
517
|
const layout = resolveCockpitLayout(columns, rows);
|
|
467
518
|
if (layout.renderer === "plain")
|
|
@@ -473,16 +524,9 @@ export function SetupCockpitFrame({ data, columns, rows, }) {
|
|
|
473
524
|
...layout,
|
|
474
525
|
stats: { ...layout.stats, mode: "summary", rows: 1 },
|
|
475
526
|
};
|
|
476
|
-
const showFullSidePanels = fullKeysFit(rows, contentLayout, data, columns);
|
|
527
|
+
const showFullSidePanels = fullKeysFit(rows, contentLayout, data, columns, bindings);
|
|
477
528
|
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
|
-
{ state: "neutral", text: `overlay ${data.overlayTarget}` },
|
|
482
|
-
{ state: "pass", text: "base untouched" },
|
|
483
|
-
{
|
|
484
|
-
state: "warn",
|
|
485
|
-
text: `${data.stagedChanges.length} changes unsaved`,
|
|
486
|
-
},
|
|
487
|
-
] }), _jsx(Keybar, { surface: "setup", width: columns })] }));
|
|
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 })] }));
|
|
488
532
|
}
|