tickmarkr 1.78.0 → 1.80.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/adapters/claude-code.d.ts +11 -0
  2. package/dist/adapters/claude-code.js +105 -16
  3. package/dist/adapters/kimi.d.ts +6 -0
  4. package/dist/adapters/kimi.js +88 -26
  5. package/dist/adapters/registry.js +6 -1
  6. package/dist/brand.d.ts +4 -0
  7. package/dist/brand.js +4 -0
  8. package/dist/cli/commands/doctor.d.ts +2 -0
  9. package/dist/cli/commands/doctor.js +23 -0
  10. package/dist/cli/commands/status.d.ts +4 -0
  11. package/dist/cli/commands/status.js +237 -18
  12. package/dist/cli/commands/ui.d.ts +1 -1
  13. package/dist/cli/commands/ui.js +11 -1
  14. package/dist/drivers/herdr.js +36 -7
  15. package/dist/drivers/types.d.ts +22 -1
  16. package/dist/gates/acceptance.d.ts +5 -0
  17. package/dist/gates/acceptance.js +5 -3
  18. package/dist/gates/llm.d.ts +9 -0
  19. package/dist/gates/llm.js +74 -0
  20. package/dist/gates/review.d.ts +5 -0
  21. package/dist/gates/review.js +5 -3
  22. package/dist/run/journal.d.ts +1 -1
  23. package/dist/tui/cockpit/capture.d.ts +117 -0
  24. package/dist/tui/cockpit/capture.js +329 -0
  25. package/dist/tui/cockpit/components.d.ts +179 -0
  26. package/dist/tui/cockpit/components.js +280 -0
  27. package/dist/tui/cockpit/demo.d.ts +41 -0
  28. package/dist/tui/cockpit/demo.js +144 -0
  29. package/dist/tui/cockpit/derive.d.ts +39 -0
  30. package/dist/tui/cockpit/derive.js +390 -0
  31. package/dist/tui/cockpit/layout.d.ts +76 -0
  32. package/dist/tui/cockpit/layout.js +84 -0
  33. package/dist/tui/cockpit/run-cockpit.d.ts +19 -0
  34. package/dist/tui/cockpit/run-cockpit.js +194 -0
  35. package/dist/tui/cockpit/setup-cockpit.d.ts +41 -0
  36. package/dist/tui/cockpit/setup-cockpit.js +488 -0
  37. package/dist/tui/cockpit/theme.d.ts +161 -0
  38. package/dist/tui/cockpit/theme.js +170 -0
  39. package/package.json +2 -1
@@ -4,7 +4,7 @@ import { renderAcceptanceItem } from "../graph/schema.js";
4
4
  import { getAdapter } from "../adapters/registry.js";
5
5
  import { shOk } from "../run/git.js";
6
6
  import { marginalCostRank } from "../route/router.js";
7
- import { COMPLETION_FAKING_CHECKLIST, extractVerdictJson, generateVerdictNonce, runLlm, verdictNonceLine } from "./llm.js";
7
+ import { appendAnchoredReview, COMPLETION_FAKING_CHECKLIST, extractVerdictJson, generateVerdictNonce, runLlm, verdictNonceLine } from "./llm.js";
8
8
  // legacy flat `issues` shape — every issue blocks; the approve flag must agree with the list.
9
9
  function classifyReviewIssues(approve, issues) {
10
10
  const inconsistencies = [];
@@ -158,8 +158,9 @@ block approval. For a minor concern you have decided not to block on, set "defer
158
158
  one-line "rationale" — it is recorded in the review, never dropped.
159
159
 
160
160
  Respond with ONLY this JSON:
161
- {"nonce": "${nonce}", "approve": true|false, "findings": [{"note": "...", "severity": "material"|"minor", "defer": false, "rationale": ""}]}
161
+ {"nonce": "${nonce}", "approve": true|false, "findings": [{"note": "...", "severity": "material"|"minor", "defer": false, "rationale": ""}], "comments": [{"path": "path/to/file", "line": 42, "body": "actionable feedback"}]}
162
162
  Approve iff no material finding remains; an empty findings list is a clean approval.
163
+ The top-level comments array is optional. Use it only for actionable line-anchored feedback.
163
164
  `;
164
165
  const raw = await runLlm(getAdapter(reviewer.adapter, adapters), reviewer.model, prompt, worktree, via ? { driver: via.driver, keep: via.keep, onSlot: via.onSlot, name: via.nameFor("review", reviewer.adapter), label: via.labelFor("review") } : undefined,
165
166
  // frontier reviewers routinely need >5min on a configured-cap-sized diff, and `claude -p` buffers all
@@ -182,10 +183,11 @@ Approve iff no material finding remains; an empty findings list is a clean appro
182
183
  const decided = findings !== null
183
184
  ? classifyReviewFindings(findings)
184
185
  : classifyReviewIssues(v.approve, v.issues);
186
+ const prose = `reviewer ${reviewer.adapter}:${reviewer.model} (${reviewer.vendor}): ${decided.headline}${decided.lines.length ? "\n" + decided.lines.join("\n") : ""}`;
185
187
  return {
186
188
  gate: "review",
187
189
  pass: decided.pass,
188
- details: `reviewer ${reviewer.adapter}:${reviewer.model} (${reviewer.vendor}): ${decided.headline}${decided.lines.length ? "\n" + decided.lines.join("\n") : ""}`,
190
+ details: appendAnchoredReview(prose, v),
189
191
  meta: { reviewer: channelKey(reviewer) },
190
192
  };
191
193
  }
@@ -85,12 +85,12 @@ export declare const TelemetryRowSchema: z.ZodObject<{
85
85
  }>>;
86
86
  signalQuality: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<0>, z.ZodLiteral<0.25>, z.ZodLiteral<0.5>, z.ZodLiteral<0.75>, z.ZodLiteral<1>]>>;
87
87
  signalBasis: z.ZodOptional<z.ZodEnum<{
88
+ skipped: "skipped";
88
89
  proved: "proved";
89
90
  "review-agree": "review-agree";
90
91
  "judge-only": "judge-only";
91
92
  legacy: "legacy";
92
93
  vacuous: "vacuous";
93
- skipped: "skipped";
94
94
  }>>;
95
95
  kind: z.ZodOptional<z.ZodLiteral<"judge">>;
96
96
  judgeOutcome: z.ZodOptional<z.ZodEnum<{
@@ -0,0 +1,117 @@
1
+ import { type ReactNode } from "react";
2
+ import { type DemoCaptures } from "./demo.js";
3
+ export type CockpitName = "run" | "setup";
4
+ export type CockpitRenderer = "frame" | "plain";
5
+ export type RendererCaptureOptions = {
6
+ readonly columns: number;
7
+ readonly rows: number;
8
+ readonly colour: boolean;
9
+ };
10
+ export type FrameRenderer = (node: ReactNode, options: RendererCaptureOptions) => Promise<string>;
11
+ export type CockpitEmission = {
12
+ readonly renderer: CockpitRenderer;
13
+ readonly output: string;
14
+ };
15
+ export type CaptureCockpitOptions = {
16
+ readonly cockpit: CockpitName;
17
+ readonly output: NodeJS.WriteStream;
18
+ readonly binaryVersion: string;
19
+ readonly columns?: number;
20
+ readonly rows?: number;
21
+ readonly interactive?: boolean;
22
+ readonly colour?: boolean;
23
+ readonly ci?: boolean;
24
+ readonly captures?: DemoCaptures;
25
+ readonly plainRenderer?: (cockpit: CockpitName, binaryVersion: string, captures: DemoCaptures) => string;
26
+ readonly frameRenderer?: FrameRenderer;
27
+ };
28
+ export type WidthBandCase = {
29
+ readonly arrangement: "stacked" | "folded-keys" | "three-column";
30
+ readonly columns: number;
31
+ };
32
+ export declare const WIDTH_BAND_CASES: readonly [{
33
+ readonly arrangement: "stacked";
34
+ readonly columns: 80;
35
+ }, {
36
+ readonly arrangement: "folded-keys";
37
+ readonly columns: 100;
38
+ }, {
39
+ readonly arrangement: "three-column";
40
+ readonly columns: 140;
41
+ }];
42
+ export declare const HEIGHT_TIER_BOUNDARIES: readonly [14, 18, 24, 40];
43
+ export type GoldenFrameCase = {
44
+ readonly id: string;
45
+ readonly fixture: string;
46
+ readonly cockpit: CockpitName;
47
+ readonly axis: "width" | "height" | "variant";
48
+ readonly columns: number;
49
+ readonly rows: number;
50
+ readonly interactive: boolean;
51
+ readonly colour: boolean;
52
+ readonly ci: boolean;
53
+ };
54
+ export declare const GOLDEN_FRAME_CASES: readonly GoldenFrameCase[];
55
+ export type ColourFrameCase = {
56
+ readonly id: "healthy-colour" | "healthy-no-colour" | "warning-failure-colour";
57
+ readonly fixture: string;
58
+ readonly sourceFileName: string;
59
+ readonly columns: number;
60
+ readonly rows: number;
61
+ readonly colour: boolean;
62
+ };
63
+ /**
64
+ * The colour corpus has a deliberately separate manifest from the layout
65
+ * corpus. Layout cases stay colourless; these cases alone exercise real ink.
66
+ */
67
+ export declare const COLOUR_FRAME_CASES: readonly [{
68
+ readonly id: "healthy-colour";
69
+ readonly fixture: "run-20260718-000943.colour.140x24.txt";
70
+ readonly sourceFileName: "run-20260718-000943.journal.jsonl";
71
+ readonly columns: 140;
72
+ readonly rows: 24;
73
+ readonly colour: true;
74
+ }, {
75
+ readonly id: "healthy-no-colour";
76
+ readonly fixture: "run-20260718-000943.no-colour.140x24.txt";
77
+ readonly sourceFileName: "run-20260718-000943.journal.jsonl";
78
+ readonly columns: 140;
79
+ readonly rows: 24;
80
+ readonly colour: false;
81
+ }, {
82
+ readonly id: "warning-failure-colour";
83
+ readonly fixture: "run-20260725-025004.interrupted.colour.140x24.txt";
84
+ readonly sourceFileName: "run-20260725-025004.interrupted.journal.jsonl";
85
+ readonly columns: 140;
86
+ readonly rows: 24;
87
+ readonly colour: true;
88
+ }];
89
+ /**
90
+ * Capture exactly the bytes Ink writes for one static paint. Debug mode is
91
+ * deliberate: it exposes the complete frame rather than terminal patch
92
+ * instructions, so the committed corpus is the renderer's frame output.
93
+ */
94
+ export declare function captureRendererOutput(node: ReactNode, options: RendererCaptureOptions): Promise<string>;
95
+ /**
96
+ * The single interactive/plain boundary. It decides before mounting Ink, then
97
+ * falls back again if a renderer returns an empty paint.
98
+ */
99
+ export declare function captureCockpitOutput(options: CaptureCockpitOptions): Promise<CockpitEmission>;
100
+ export type RegeneratedGoldenFrame = GoldenFrameCase & {
101
+ readonly renderer: CockpitRenderer;
102
+ readonly output: string;
103
+ readonly emitted: string;
104
+ };
105
+ export declare function regenerateGoldenFrames(binaryVersion?: string): Promise<readonly RegeneratedGoldenFrame[]>;
106
+ export declare function findGoldenFrameMismatches(regenerated: readonly RegeneratedGoldenFrame[], committed: ReadonlyMap<string, string>): string[];
107
+ export type RegeneratedColourFrame = ColourFrameCase & {
108
+ readonly output: string;
109
+ readonly emitted: string;
110
+ };
111
+ /**
112
+ * Regenerate only the colour contract corpus from its own manifest. Historical
113
+ * run sources are read and rendered byte-for-byte with their captured daemon
114
+ * stopped, matching the demo path and avoiding live-process state in committed
115
+ * bytes.
116
+ */
117
+ export declare function regenerateColourFrames(binaryVersion?: string): Promise<readonly RegeneratedColourFrame[]>;
@@ -0,0 +1,329 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { PassThrough } from "node:stream";
4
+ import { fileURLToPath } from "node:url";
5
+ import chalk from "chalk";
6
+ import { render } from "ink";
7
+ import { cloneElement, createElement, isValidElement, } from "react";
8
+ import { GLYPHS } from "../../brand.js";
9
+ import { version } from "../../cli/commands/version.js";
10
+ import { KEYBAR_KEYS } from "./components.js";
11
+ import { loadDemoCaptures } from "./demo.js";
12
+ import { resolveCockpitLayout, } from "./layout.js";
13
+ import { deriveRunCockpitData, RunCockpitFrame, } from "./run-cockpit.js";
14
+ import { deriveSetupCockpitData, SetupCockpitFrame, } from "./setup-cockpit.js";
15
+ export const WIDTH_BAND_CASES = [
16
+ { arrangement: "stacked", columns: 80 },
17
+ { arrangement: "folded-keys", columns: 100 },
18
+ { arrangement: "three-column", columns: 140 },
19
+ ];
20
+ export const HEIGHT_TIER_BOUNDARIES = [14, 18, 24, 40];
21
+ const fixtureName = (cockpit, id, columns, rows) => `${cockpit}.${id}.${columns}x${rows}.txt`;
22
+ const goldenCasesFor = (cockpit) => [
23
+ ...WIDTH_BAND_CASES.map(({ arrangement, columns }) => ({
24
+ id: `${cockpit}-width-${arrangement}`,
25
+ fixture: fixtureName(cockpit, `width-${arrangement}`, columns, 24),
26
+ cockpit,
27
+ axis: "width",
28
+ columns,
29
+ rows: 24,
30
+ interactive: true,
31
+ colour: false,
32
+ ci: false,
33
+ })),
34
+ ...HEIGHT_TIER_BOUNDARIES.map((rows) => ({
35
+ id: `${cockpit}-height-${rows}`,
36
+ fixture: fixtureName(cockpit, `height-${rows}`, 140, rows),
37
+ cockpit,
38
+ axis: "height",
39
+ columns: 140,
40
+ rows,
41
+ interactive: true,
42
+ colour: false,
43
+ ci: false,
44
+ })),
45
+ {
46
+ id: `${cockpit}-no-colour`,
47
+ fixture: fixtureName(cockpit, "no-colour", 140, 24),
48
+ cockpit,
49
+ axis: "variant",
50
+ columns: 140,
51
+ rows: 24,
52
+ interactive: true,
53
+ colour: false,
54
+ ci: false,
55
+ },
56
+ {
57
+ id: `${cockpit}-non-tty`,
58
+ fixture: fixtureName(cockpit, "non-tty", 140, 24),
59
+ cockpit,
60
+ axis: "variant",
61
+ columns: 140,
62
+ rows: 24,
63
+ interactive: false,
64
+ colour: false,
65
+ ci: false,
66
+ },
67
+ {
68
+ id: `${cockpit}-ci`,
69
+ fixture: fixtureName(cockpit, "ci", 140, 24),
70
+ cockpit,
71
+ axis: "variant",
72
+ columns: 140,
73
+ rows: 24,
74
+ interactive: true,
75
+ colour: false,
76
+ ci: true,
77
+ },
78
+ ];
79
+ export const GOLDEN_FRAME_CASES = [
80
+ ...goldenCasesFor("run"),
81
+ ...goldenCasesFor("setup"),
82
+ ];
83
+ /**
84
+ * The colour corpus has a deliberately separate manifest from the layout
85
+ * corpus. Layout cases stay colourless; these cases alone exercise real ink.
86
+ */
87
+ export const COLOUR_FRAME_CASES = [
88
+ {
89
+ id: "healthy-colour",
90
+ fixture: "run-20260718-000943.colour.140x24.txt",
91
+ sourceFileName: "run-20260718-000943.journal.jsonl",
92
+ columns: 140,
93
+ rows: 24,
94
+ colour: true,
95
+ },
96
+ {
97
+ id: "healthy-no-colour",
98
+ fixture: "run-20260718-000943.no-colour.140x24.txt",
99
+ sourceFileName: "run-20260718-000943.journal.jsonl",
100
+ columns: 140,
101
+ rows: 24,
102
+ colour: false,
103
+ },
104
+ {
105
+ id: "warning-failure-colour",
106
+ fixture: "run-20260725-025004.interrupted.colour.140x24.txt",
107
+ sourceFileName: "run-20260725-025004.interrupted.journal.jsonl",
108
+ columns: 140,
109
+ rows: 24,
110
+ colour: true,
111
+ },
112
+ ];
113
+ const COLOUR_SOURCE_DIRECTORY = join(dirname(fileURLToPath(import.meta.url)), "../../../tests/fixtures/cockpit/colour/sources");
114
+ function derivedData(cockpit, binaryVersion, captures) {
115
+ if (cockpit === "setup") {
116
+ return {
117
+ cockpit,
118
+ data: deriveSetupCockpitData(captures, binaryVersion),
119
+ };
120
+ }
121
+ const source = captures.journals.find((capture) => capture.fileName === "run-20260724-231138.journal.jsonl");
122
+ if (!source)
123
+ throw new Error("healthy cockpit capture is missing");
124
+ return {
125
+ cockpit,
126
+ data: deriveRunCockpitData(source, binaryVersion),
127
+ };
128
+ }
129
+ function stateGlyph(state) {
130
+ if (state === "active" || state === "pass")
131
+ return GLYPHS.pass;
132
+ if (state === "inactive")
133
+ return GLYPHS.toggleInactive;
134
+ if (state === "fail")
135
+ return GLYPHS.fail;
136
+ if (state === "warn")
137
+ return GLYPHS.attention;
138
+ return GLYPHS.neutral;
139
+ }
140
+ function defaultPlainRenderer(cockpit, binaryVersion, captures) {
141
+ const derived = derivedData(cockpit, binaryVersion, captures);
142
+ if (derived.cockpit === "run") {
143
+ const data = derived.data;
144
+ return [
145
+ `tickmarkr v${binaryVersion}`,
146
+ `${data.runId} ${data.status} ${data.elapsed}`,
147
+ ...data.statusItems.map((item) => `${stateGlyph(item.state)} ${item.text}`),
148
+ `keys ${KEYBAR_KEYS.run.map((item) => `${item.key} ${item.label}`).join(" · ")}`,
149
+ "",
150
+ ].join("\n");
151
+ }
152
+ const data = derived.data;
153
+ return [
154
+ `tickmarkr v${binaryVersion}`,
155
+ `setup ${data.counts.found} found ${data.counts.authenticated} authed ${data.counts.routable} routable`,
156
+ ...data.harnesses.map((harness) => `${stateGlyph(harness.state)} ${harness.stateWord} ${harness.id}`),
157
+ ...data.deniedChannels.map((denied) => `${GLYPHS.fail} denied ${denied.channel} - ${denied.reason}`),
158
+ `${GLYPHS.pass} base untouched`,
159
+ `${GLYPHS.attention} ${data.stagedChanges.length} changes unsaved`,
160
+ `keys ${KEYBAR_KEYS.setup.map((item) => `${item.key} ${item.label}`).join(" · ")}`,
161
+ "",
162
+ ].join("\n");
163
+ }
164
+ function frameNode(derived, columns, rows) {
165
+ if (derived.cockpit === "run") {
166
+ return createElement(RunCockpitFrame, {
167
+ data: derived.data,
168
+ columns,
169
+ rows,
170
+ });
171
+ }
172
+ return createElement(SetupCockpitFrame, {
173
+ data: derived.data,
174
+ columns,
175
+ rows,
176
+ });
177
+ }
178
+ /**
179
+ * Capture exactly the bytes Ink writes for one static paint. Debug mode is
180
+ * deliberate: it exposes the complete frame rather than terminal patch
181
+ * instructions, so the committed corpus is the renderer's frame output.
182
+ */
183
+ export async function captureRendererOutput(node, options) {
184
+ const sizedNode = isValidElement(node)
185
+ && (node.type === RunCockpitFrame || node.type === SetupCockpitFrame)
186
+ ? cloneElement(node, { rows: options.rows })
187
+ : node;
188
+ const output = new PassThrough();
189
+ output.isTTY = true;
190
+ output.columns = options.columns;
191
+ output.rows = options.rows;
192
+ const writes = [];
193
+ const write = output.write.bind(output);
194
+ output.write = ((chunk, ...args) => {
195
+ writes.push(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8"));
196
+ return Reflect.apply(write, output, [chunk, ...args]);
197
+ });
198
+ const previousColourLevel = chalk.level;
199
+ const previousNoColour = process.env.NO_COLOR;
200
+ chalk.level = options.colour ? 3 : 0;
201
+ if (options.colour)
202
+ delete process.env.NO_COLOR;
203
+ else
204
+ process.env.NO_COLOR = "1";
205
+ let painted;
206
+ const firstPaint = new Promise((resolve) => {
207
+ painted = resolve;
208
+ });
209
+ const app = render(sizedNode, {
210
+ stdout: output,
211
+ debug: true,
212
+ patchConsole: false,
213
+ onRender: painted,
214
+ });
215
+ try {
216
+ await firstPaint;
217
+ return writes.at(-1) ?? "";
218
+ }
219
+ finally {
220
+ app.unmount();
221
+ app.cleanup();
222
+ chalk.level = previousColourLevel;
223
+ if (previousNoColour === undefined)
224
+ delete process.env.NO_COLOR;
225
+ else
226
+ process.env.NO_COLOR = previousNoColour;
227
+ }
228
+ }
229
+ /**
230
+ * The single interactive/plain boundary. It decides before mounting Ink, then
231
+ * falls back again if a renderer returns an empty paint.
232
+ */
233
+ export async function captureCockpitOutput(options) {
234
+ const columns = options.columns ?? options.output.columns ?? 80;
235
+ const rows = options.rows ?? options.output.rows ?? 24;
236
+ const interactive = options.interactive
237
+ ?? options.output.isTTY === true;
238
+ const colour = options.colour
239
+ ?? process.env.NO_COLOR === undefined;
240
+ const ci = options.ci ?? process.env.CI === "true";
241
+ const captures = options.captures ?? loadDemoCaptures();
242
+ const plain = () => (options.plainRenderer ?? defaultPlainRenderer)(options.cockpit, options.binaryVersion, captures);
243
+ const layout = resolveCockpitLayout(columns, rows);
244
+ if (!interactive || options.output.isTTY !== true || ci || layout.renderer === "plain") {
245
+ const plainOutput = plain();
246
+ options.output.write(plainOutput);
247
+ return { renderer: "plain", output: plainOutput };
248
+ }
249
+ const derived = derivedData(options.cockpit, options.binaryVersion, captures);
250
+ const frameOutput = await (options.frameRenderer ?? captureRendererOutput)(frameNode(derived, columns, rows), { columns, rows, colour });
251
+ if (frameOutput.length === 0) {
252
+ const plainOutput = plain();
253
+ options.output.write(plainOutput);
254
+ return { renderer: "plain", output: plainOutput };
255
+ }
256
+ options.output.write(frameOutput);
257
+ return { renderer: "frame", output: frameOutput };
258
+ }
259
+ export async function regenerateGoldenFrames(binaryVersion) {
260
+ const resolvedVersion = binaryVersion ?? await version();
261
+ const captures = loadDemoCaptures();
262
+ const regenerated = [];
263
+ for (const golden of GOLDEN_FRAME_CASES) {
264
+ const output = new PassThrough();
265
+ output.isTTY = golden.interactive;
266
+ output.columns = golden.columns;
267
+ output.rows = golden.rows;
268
+ const writes = [];
269
+ const write = output.write.bind(output);
270
+ output.write = ((chunk, ...args) => {
271
+ writes.push(typeof chunk === "string" ? chunk : Buffer.from(chunk).toString("utf8"));
272
+ return Reflect.apply(write, output, [chunk, ...args]);
273
+ });
274
+ const emission = await captureCockpitOutput({
275
+ cockpit: golden.cockpit,
276
+ output: output,
277
+ binaryVersion: resolvedVersion,
278
+ columns: golden.columns,
279
+ rows: golden.rows,
280
+ interactive: golden.interactive,
281
+ colour: golden.colour,
282
+ ci: golden.ci,
283
+ captures,
284
+ });
285
+ regenerated.push({
286
+ ...golden,
287
+ ...emission,
288
+ emitted: writes.join(""),
289
+ });
290
+ }
291
+ return regenerated;
292
+ }
293
+ export function findGoldenFrameMismatches(regenerated, committed) {
294
+ const expected = new Set(regenerated.map((frame) => frame.fixture));
295
+ return [
296
+ ...regenerated.flatMap((frame) => committed.get(frame.fixture) === frame.output ? [] : [frame.fixture]),
297
+ ...[...committed.keys()].filter((fixture) => !expected.has(fixture)),
298
+ ].sort();
299
+ }
300
+ /**
301
+ * Regenerate only the colour contract corpus from its own manifest. Historical
302
+ * run sources are read and rendered byte-for-byte with their captured daemon
303
+ * stopped, matching the demo path and avoiding live-process state in committed
304
+ * bytes.
305
+ */
306
+ export async function regenerateColourFrames(binaryVersion) {
307
+ const resolvedVersion = binaryVersion ?? await version();
308
+ const regenerated = [];
309
+ for (const colourCase of COLOUR_FRAME_CASES) {
310
+ const source = {
311
+ fileName: colourCase.sourceFileName,
312
+ raw: readFileSync(join(COLOUR_SOURCE_DIRECTORY, colourCase.sourceFileName), "utf8"),
313
+ };
314
+ const data = deriveRunCockpitData(source, resolvedVersion, {
315
+ isDaemonAlive: () => false,
316
+ });
317
+ const output = await captureRendererOutput(frameNode({ cockpit: "run", data }, colourCase.columns, colourCase.rows), {
318
+ columns: colourCase.columns,
319
+ rows: colourCase.rows,
320
+ colour: colourCase.colour,
321
+ });
322
+ regenerated.push({
323
+ ...colourCase,
324
+ output,
325
+ emitted: output,
326
+ });
327
+ }
328
+ return regenerated;
329
+ }
@@ -0,0 +1,179 @@
1
+ import { type ReactElement, type ReactNode } from "react";
2
+ import { TEXT_EMPHASIS_TOKENS } from "./theme.js";
3
+ export { SPARKLINE_BUCKET_WINDOW } from "./derive.js";
4
+ export declare const PANEL_CHROME_ROWS = 3;
5
+ export declare const BAND_CONTINUATION_PREFIX = "\u21B3 ";
6
+ export type BandColumnContent = {
7
+ readonly title: string;
8
+ readonly lines: readonly string[];
9
+ };
10
+ export type BandAllocationOptions = {
11
+ readonly chromeColumns?: number;
12
+ readonly minimumReadableColumns?: number;
13
+ };
14
+ export type TextEmphasis = keyof typeof TEXT_EMPHASIS_TOKENS;
15
+ export type ComponentState = "active" | "inactive" | "pass" | "fail" | "warn" | "neutral";
16
+ export type KeybarSurface = "run" | "setup";
17
+ export type KeybarItem = {
18
+ readonly key: string;
19
+ readonly label: string;
20
+ };
21
+ export declare const KEYBAR_KEYS: {
22
+ readonly run: readonly [{
23
+ readonly key: "↑↓";
24
+ readonly label: "Move";
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: "⏎";
52
+ 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
+ }, {
66
+ readonly key: "␣";
67
+ readonly label: "Toggle";
68
+ }, {
69
+ readonly key: "a";
70
+ readonly label: "All";
71
+ }, {
72
+ readonly key: "r";
73
+ readonly label: "Re-probe";
74
+ }, {
75
+ readonly key: "s";
76
+ readonly label: "Save";
77
+ }, {
78
+ readonly key: "n";
79
+ readonly label: "Next";
80
+ }, {
81
+ readonly key: "p";
82
+ readonly label: "Prev";
83
+ }];
84
+ readonly setupAdditional: readonly [{
85
+ readonly key: "␣";
86
+ readonly label: "Toggle";
87
+ }, {
88
+ readonly key: "a";
89
+ readonly label: "All";
90
+ }, {
91
+ readonly key: "r";
92
+ readonly label: "Re-probe";
93
+ }, {
94
+ readonly key: "s";
95
+ readonly label: "Save";
96
+ }, {
97
+ readonly key: "n";
98
+ readonly label: "Next";
99
+ }, {
100
+ readonly key: "p";
101
+ readonly label: "Prev";
102
+ }];
103
+ };
104
+ export type JournalRow = {
105
+ readonly id?: string;
106
+ readonly time: string;
107
+ readonly state: ComponentState;
108
+ readonly text: string;
109
+ };
110
+ export declare function BodyText({ children, emphasis, }: {
111
+ children: ReactNode;
112
+ emphasis?: TextEmphasis;
113
+ }): ReactElement;
114
+ /**
115
+ * Give each panel the columns its own longest line calls for. Surplus is shared
116
+ * evenly after every line fits; scarcity is apportioned by unmet content need.
117
+ */
118
+ export declare function allocateBandColumns(columns: number, content: readonly BandColumnContent[], options?: BandAllocationOptions): readonly number[];
119
+ /**
120
+ * Compose one data line into visible rows. A fitting line is byte-for-byte
121
+ * untouched. Every wrapped row carries a continuation marker before any
122
+ * separator or data token, so it cannot masquerade as a new item.
123
+ */
124
+ export declare function composeBandLine(text: string, columns: number): readonly string[];
125
+ export declare function BandLines({ lines, columns, emphasis, }: {
126
+ lines: readonly string[];
127
+ columns: number;
128
+ emphasis?: TextEmphasis;
129
+ }): ReactElement;
130
+ /** Rows drawn by a band of bordered panels at the same allocated widths. */
131
+ export declare function measureBandRows(columns: number, content: readonly BandColumnContent[]): number;
132
+ export declare function StateGlyph({ state }: {
133
+ state: ComponentState;
134
+ }): ReactElement;
135
+ export declare function Panel({ title, focused, width, flexGrow, children, }: {
136
+ title: string;
137
+ focused?: boolean;
138
+ width?: number | string;
139
+ flexGrow?: number;
140
+ children: ReactNode;
141
+ }): ReactElement;
142
+ export declare function CockpitGrid({ children, columns, columnContents, allocation, }: {
143
+ children: ReactNode;
144
+ columns?: number;
145
+ columnContents?: readonly BandColumnContent[];
146
+ allocation?: BandAllocationOptions;
147
+ }): ReactElement;
148
+ export declare function Sparkline({ samples, }: {
149
+ samples: readonly (number | null)[];
150
+ }): ReactElement;
151
+ export declare function StatTile({ label, value, samples, focused, width, }: {
152
+ label: string;
153
+ value: ReactNode;
154
+ samples: readonly (number | null)[];
155
+ focused?: boolean;
156
+ width?: number | string;
157
+ }): ReactElement;
158
+ export declare function ProgressMeter({ value, width, }: {
159
+ value: number;
160
+ width?: number;
161
+ }): ReactElement;
162
+ export declare function Keybar({ surface, width, }: {
163
+ surface: KeybarSurface;
164
+ width: number;
165
+ }): ReactElement;
166
+ export type StatusStripItem = {
167
+ readonly state: ComponentState;
168
+ readonly text: string;
169
+ };
170
+ export declare function StatusStrip({ items, width, }: {
171
+ items: readonly StatusStripItem[];
172
+ width: number;
173
+ }): ReactElement;
174
+ export declare function JournalRowPanel({ rows, width, focused, title, }: {
175
+ rows: readonly JournalRow[];
176
+ width?: number | string;
177
+ focused?: boolean;
178
+ title?: string;
179
+ }): ReactElement;