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.
- package/dist/adapters/claude-code.d.ts +11 -0
- package/dist/adapters/claude-code.js +105 -16
- package/dist/adapters/kimi.d.ts +6 -0
- package/dist/adapters/kimi.js +88 -26
- package/dist/adapters/registry.js +6 -1
- package/dist/brand.d.ts +4 -0
- package/dist/brand.js +4 -0
- package/dist/cli/commands/doctor.d.ts +2 -0
- package/dist/cli/commands/doctor.js +23 -0
- package/dist/cli/commands/status.d.ts +4 -0
- package/dist/cli/commands/status.js +237 -18
- package/dist/cli/commands/ui.d.ts +1 -1
- package/dist/cli/commands/ui.js +11 -1
- package/dist/drivers/herdr.js +36 -7
- package/dist/drivers/types.d.ts +22 -1
- package/dist/gates/acceptance.d.ts +5 -0
- package/dist/gates/acceptance.js +5 -3
- package/dist/gates/llm.d.ts +9 -0
- package/dist/gates/llm.js +74 -0
- package/dist/gates/review.d.ts +5 -0
- package/dist/gates/review.js +5 -3
- package/dist/run/journal.d.ts +1 -1
- package/dist/tui/cockpit/capture.d.ts +117 -0
- package/dist/tui/cockpit/capture.js +329 -0
- package/dist/tui/cockpit/components.d.ts +179 -0
- package/dist/tui/cockpit/components.js +280 -0
- package/dist/tui/cockpit/demo.d.ts +41 -0
- package/dist/tui/cockpit/demo.js +144 -0
- package/dist/tui/cockpit/derive.d.ts +39 -0
- package/dist/tui/cockpit/derive.js +390 -0
- package/dist/tui/cockpit/layout.d.ts +76 -0
- package/dist/tui/cockpit/layout.js +84 -0
- package/dist/tui/cockpit/run-cockpit.d.ts +19 -0
- package/dist/tui/cockpit/run-cockpit.js +194 -0
- package/dist/tui/cockpit/setup-cockpit.d.ts +41 -0
- package/dist/tui/cockpit/setup-cockpit.js +488 -0
- package/dist/tui/cockpit/theme.d.ts +161 -0
- package/dist/tui/cockpit/theme.js +170 -0
- package/package.json +2 -1
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
export const SPARKLINE_BUCKET_WINDOW = 12;
|
|
2
|
+
const MINUTE_MS = 60_000;
|
|
3
|
+
const SPARKLINE_BUCKET_WIDTH_LADDER_MINUTES = [
|
|
4
|
+
1,
|
|
5
|
+
5,
|
|
6
|
+
10,
|
|
7
|
+
30,
|
|
8
|
+
60,
|
|
9
|
+
120,
|
|
10
|
+
360,
|
|
11
|
+
720,
|
|
12
|
+
1_440,
|
|
13
|
+
];
|
|
14
|
+
export function selectSparklineBucketWidthMs(elapsedMs) {
|
|
15
|
+
const safeElapsedMs = Math.max(0, Number.isFinite(elapsedMs) ? elapsedMs : 0);
|
|
16
|
+
const requiredMinutes = safeElapsedMs / SPARKLINE_BUCKET_WINDOW / MINUTE_MS;
|
|
17
|
+
const selected = SPARKLINE_BUCKET_WIDTH_LADDER_MINUTES.find((minutes) => minutes >= requiredMinutes);
|
|
18
|
+
if (selected !== undefined)
|
|
19
|
+
return selected * MINUTE_MS;
|
|
20
|
+
let coarsened = SPARKLINE_BUCKET_WIDTH_LADDER_MINUTES.at(-1);
|
|
21
|
+
while (coarsened < requiredMinutes)
|
|
22
|
+
coarsened *= 2;
|
|
23
|
+
return coarsened * MINUTE_MS;
|
|
24
|
+
}
|
|
25
|
+
function recordValue(value) {
|
|
26
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
27
|
+
? value
|
|
28
|
+
: undefined;
|
|
29
|
+
}
|
|
30
|
+
function parseSource(raw) {
|
|
31
|
+
const events = [];
|
|
32
|
+
const defects = [];
|
|
33
|
+
for (const [index, line] of raw.split("\n").entries()) {
|
|
34
|
+
if (!line.trim())
|
|
35
|
+
continue;
|
|
36
|
+
try {
|
|
37
|
+
const parsed = recordValue(JSON.parse(line));
|
|
38
|
+
const data = recordValue(parsed?.data);
|
|
39
|
+
if (!parsed
|
|
40
|
+
|| typeof parsed.ts !== "string"
|
|
41
|
+
|| typeof parsed.event !== "string"
|
|
42
|
+
|| !data
|
|
43
|
+
|| (parsed.taskId !== undefined && typeof parsed.taskId !== "string")) {
|
|
44
|
+
defects.push({ line: index + 1, raw: line });
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
events.push({
|
|
48
|
+
line: index + 1,
|
|
49
|
+
ts: parsed.ts,
|
|
50
|
+
event: parsed.event,
|
|
51
|
+
...(typeof parsed.taskId === "string" ? { taskId: parsed.taskId } : {}),
|
|
52
|
+
data,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
defects.push({ line: index + 1, raw: line });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { events, defects };
|
|
60
|
+
}
|
|
61
|
+
function assignmentFrom(value) {
|
|
62
|
+
const assignment = recordValue(value);
|
|
63
|
+
return assignment
|
|
64
|
+
&& typeof assignment.adapter === "string"
|
|
65
|
+
&& typeof assignment.model === "string"
|
|
66
|
+
? { adapter: assignment.adapter, model: assignment.model }
|
|
67
|
+
: undefined;
|
|
68
|
+
}
|
|
69
|
+
function elapsedReading(first, last) {
|
|
70
|
+
const seconds = Math.max(0, Math.floor((Date.parse(last) - Date.parse(first)) / 1_000));
|
|
71
|
+
const hours = Math.floor(seconds / 3_600);
|
|
72
|
+
const minutes = Math.floor((seconds % 3_600) / 60);
|
|
73
|
+
const remainder = seconds % 60;
|
|
74
|
+
const padded = (part) => String(part).padStart(2, "0");
|
|
75
|
+
return `${padded(hours)}:${padded(minutes)}:${padded(remainder)}`;
|
|
76
|
+
}
|
|
77
|
+
function defaultDaemonLiveness(pid) {
|
|
78
|
+
try {
|
|
79
|
+
process.kill(pid, 0);
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function latestLifecycle(events) {
|
|
87
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
88
|
+
const event = events[index].event;
|
|
89
|
+
if (event === "superseded")
|
|
90
|
+
return "superseded";
|
|
91
|
+
if (event === "run-end")
|
|
92
|
+
return "completed";
|
|
93
|
+
if (event === "run-start" || event === "run-resume")
|
|
94
|
+
return "active";
|
|
95
|
+
}
|
|
96
|
+
return "active";
|
|
97
|
+
}
|
|
98
|
+
function daemonPid(events) {
|
|
99
|
+
for (let index = events.length - 1; index >= 0; index -= 1) {
|
|
100
|
+
const event = events[index];
|
|
101
|
+
if ((event.event === "run-start" || event.event === "run-resume")
|
|
102
|
+
&& typeof event.data.pid === "number") {
|
|
103
|
+
return event.data.pid;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
function deriveTasks(events, interrupted) {
|
|
109
|
+
const tasks = new Map();
|
|
110
|
+
const task = (taskId, index, ts) => {
|
|
111
|
+
const existing = tasks.get(taskId);
|
|
112
|
+
if (existing)
|
|
113
|
+
return existing;
|
|
114
|
+
const created = {
|
|
115
|
+
id: taskId,
|
|
116
|
+
dispatches: [],
|
|
117
|
+
state: "pending",
|
|
118
|
+
lastIndex: index,
|
|
119
|
+
lastTs: ts,
|
|
120
|
+
phase: "pending",
|
|
121
|
+
};
|
|
122
|
+
tasks.set(taskId, created);
|
|
123
|
+
return created;
|
|
124
|
+
};
|
|
125
|
+
for (const [index, event] of events.entries()) {
|
|
126
|
+
if (!event.taskId)
|
|
127
|
+
continue;
|
|
128
|
+
if (event.event === "task-dispatch") {
|
|
129
|
+
const fact = task(event.taskId, index, event.ts);
|
|
130
|
+
const recordedAttempt = event.data.attempt;
|
|
131
|
+
fact.dispatches.push({
|
|
132
|
+
index,
|
|
133
|
+
ts: event.ts,
|
|
134
|
+
attempt: typeof recordedAttempt === "number"
|
|
135
|
+
? Math.max(1, Math.floor(recordedAttempt) + 1)
|
|
136
|
+
: fact.dispatches.length + 1,
|
|
137
|
+
assignment: assignmentFrom(event.data.assignment),
|
|
138
|
+
});
|
|
139
|
+
fact.state = "running";
|
|
140
|
+
fact.phase = "worker";
|
|
141
|
+
fact.lastIndex = index;
|
|
142
|
+
fact.lastTs = event.ts;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (event.event === "phase-start") {
|
|
146
|
+
const fact = task(event.taskId, index, event.ts);
|
|
147
|
+
fact.phase = typeof event.data.phase === "string" ? event.data.phase : fact.phase;
|
|
148
|
+
fact.lastIndex = index;
|
|
149
|
+
fact.lastTs = event.ts;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const state = {
|
|
153
|
+
"task-done": "done",
|
|
154
|
+
"task-failed": "failed",
|
|
155
|
+
"task-human": "human",
|
|
156
|
+
"task-approved": "pending",
|
|
157
|
+
}[event.event];
|
|
158
|
+
if (state) {
|
|
159
|
+
const fact = task(event.taskId, index, event.ts);
|
|
160
|
+
fact.state = state;
|
|
161
|
+
fact.lastIndex = index;
|
|
162
|
+
fact.lastTs = event.ts;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (interrupted) {
|
|
166
|
+
for (const fact of tasks.values()) {
|
|
167
|
+
if (fact.state === "running" || fact.state === "pending") {
|
|
168
|
+
fact.state = "interrupted";
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return tasks;
|
|
173
|
+
}
|
|
174
|
+
function taskAttempt(fact) {
|
|
175
|
+
return fact.dispatches.at(-1)?.attempt ?? Math.max(1, fact.dispatches.length);
|
|
176
|
+
}
|
|
177
|
+
function taskActor(fact) {
|
|
178
|
+
const assignment = fact.dispatches.at(-1)?.assignment;
|
|
179
|
+
return assignment ? `${assignment.adapter}:${assignment.model}` : "unassigned";
|
|
180
|
+
}
|
|
181
|
+
function changedActingAdapter(fact) {
|
|
182
|
+
const first = fact.dispatches[0]?.assignment?.adapter;
|
|
183
|
+
const last = fact.dispatches.at(-1)?.assignment?.adapter;
|
|
184
|
+
return first !== undefined && last !== undefined && first !== last;
|
|
185
|
+
}
|
|
186
|
+
function spotlightTask(tasks) {
|
|
187
|
+
return [...tasks.values()]
|
|
188
|
+
.filter((fact) => fact.dispatches.length > 0)
|
|
189
|
+
.sort((left, right) => Number(changedActingAdapter(right)) - Number(changedActingAdapter(left))
|
|
190
|
+
|| taskAttempt(right) - taskAttempt(left)
|
|
191
|
+
|| right.lastIndex - left.lastIndex)[0];
|
|
192
|
+
}
|
|
193
|
+
function defectRow(defect) {
|
|
194
|
+
const excerpt = defect.raw.trim().replace(/\s+/g, " ").slice(0, 64);
|
|
195
|
+
return {
|
|
196
|
+
id: `defect:${defect.line}`,
|
|
197
|
+
time: `L${defect.line}`,
|
|
198
|
+
state: "fail",
|
|
199
|
+
text: `fail · DEFECT · source line ${defect.line} unparseable${excerpt ? ` · ${excerpt}` : ""}`,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function eventTime(ts) {
|
|
203
|
+
const parsed = Date.parse(ts);
|
|
204
|
+
return Number.isFinite(parsed)
|
|
205
|
+
? new Date(parsed).toISOString().slice(11, 19)
|
|
206
|
+
: ts.slice(0, 8);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* The outcome an event records about itself, in every form the committed
|
|
210
|
+
* sources use: an unsuccessful result flag, an unsuccessful outcome flag, an
|
|
211
|
+
* event name reporting a failure, a non-zero exit status, or a run-end whose
|
|
212
|
+
* tip verification reads failed.
|
|
213
|
+
*/
|
|
214
|
+
function recordsFailure(event) {
|
|
215
|
+
return event.event.includes("failed")
|
|
216
|
+
|| event.data.pass === false
|
|
217
|
+
|| event.data.ok === false
|
|
218
|
+
|| (typeof event.data.exitCode === "number" && event.data.exitCode !== 0)
|
|
219
|
+
|| (event.event === "run-end" && event.data.tipVerify === "failed");
|
|
220
|
+
}
|
|
221
|
+
function recordsSuccess(event) {
|
|
222
|
+
return event.event === "task-done"
|
|
223
|
+
|| event.event === "merge"
|
|
224
|
+
|| event.data.pass === true
|
|
225
|
+
|| event.data.ok === true
|
|
226
|
+
|| (event.event === "run-end" && event.data.tipVerify !== "failed");
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* A row depicts the event it names: the event's own recorded outcome decides
|
|
230
|
+
* the state, never the state its task reached later. Only a task with no
|
|
231
|
+
* terminal event in a run whose daemon is gone lends its interruption, because
|
|
232
|
+
* no event records that ending — the interruption is the run's present truth.
|
|
233
|
+
* Every state carries its word, so meaning never rides on hue alone.
|
|
234
|
+
*/
|
|
235
|
+
function eventPresentation(event, task) {
|
|
236
|
+
if (event.event === "escalation" || event.event === "task-human") {
|
|
237
|
+
return { state: "warn", word: "warn" };
|
|
238
|
+
}
|
|
239
|
+
if (recordsFailure(event))
|
|
240
|
+
return { state: "fail", word: "fail" };
|
|
241
|
+
if (recordsSuccess(event))
|
|
242
|
+
return { state: "pass", word: "pass" };
|
|
243
|
+
if (task?.state === "interrupted") {
|
|
244
|
+
return { state: "warn", word: "interrupted" };
|
|
245
|
+
}
|
|
246
|
+
return { state: "neutral", word: "neutral" };
|
|
247
|
+
}
|
|
248
|
+
function historyRow(event, tasks, spotlight, newest, runStatus) {
|
|
249
|
+
const task = event.taskId === undefined ? undefined : tasks.get(event.taskId);
|
|
250
|
+
const { state, word } = eventPresentation(event, task);
|
|
251
|
+
// The spotlight entry must keep the task, its attempt and its acting adapter
|
|
252
|
+
// inside the narrowest band's width budget — that budget cannot pay for the
|
|
253
|
+
// separators and the event name, so the state word and the run's status
|
|
254
|
+
// (what the newest event recorded) ride after the adapter instead.
|
|
255
|
+
const text = newest && spotlight !== undefined
|
|
256
|
+
? `${spotlight.id} attempt ${taskAttempt(spotlight)} ${taskActor(spotlight)} ${word}${word === runStatus ? "" : ` ${runStatus}`}`
|
|
257
|
+
: task !== undefined
|
|
258
|
+
? `${task.id} ${word} · ${event.event}`
|
|
259
|
+
: `${word} · ${event.event}`;
|
|
260
|
+
return {
|
|
261
|
+
id: `event:${event.line}`,
|
|
262
|
+
time: eventTime(event.ts),
|
|
263
|
+
state,
|
|
264
|
+
text,
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
function journalRows(events, tasks, defects, runStatus) {
|
|
268
|
+
const spotlight = spotlightTask(tasks);
|
|
269
|
+
const history = [...events].reverse().map((event, index) => historyRow(event, tasks, spotlight, index === 0, runStatus));
|
|
270
|
+
return [...[...defects].reverse().map(defectRow), ...history];
|
|
271
|
+
}
|
|
272
|
+
function bucketedMetricSamples(events) {
|
|
273
|
+
const taskSamples = Array.from({ length: SPARKLINE_BUCKET_WINDOW }, () => null);
|
|
274
|
+
const gateTotals = Array.from({ length: SPARKLINE_BUCKET_WINDOW }, () => 0);
|
|
275
|
+
const gatePasses = Array.from({ length: SPARKLINE_BUCKET_WINDOW }, () => 0);
|
|
276
|
+
const firstMs = Date.parse(events[0].ts);
|
|
277
|
+
const lastMs = Date.parse(events.at(-1).ts);
|
|
278
|
+
const bucketWidthMs = selectSparklineBucketWidthMs(lastMs - firstMs);
|
|
279
|
+
const windowStartMs = lastMs - bucketWidthMs * SPARKLINE_BUCKET_WINDOW;
|
|
280
|
+
const completedTasks = new Set();
|
|
281
|
+
for (const event of events) {
|
|
282
|
+
const eventMs = Date.parse(event.ts);
|
|
283
|
+
if (!Number.isFinite(eventMs))
|
|
284
|
+
continue;
|
|
285
|
+
const bucket = Math.max(0, Math.min(SPARKLINE_BUCKET_WINDOW - 1, Math.floor((eventMs - windowStartMs) / bucketWidthMs)));
|
|
286
|
+
if (event.event === "task-done"
|
|
287
|
+
&& event.taskId
|
|
288
|
+
&& !completedTasks.has(event.taskId)) {
|
|
289
|
+
completedTasks.add(event.taskId);
|
|
290
|
+
taskSamples[bucket] = (taskSamples[bucket] ?? 0) + 1;
|
|
291
|
+
}
|
|
292
|
+
if (event.event === "gate-result") {
|
|
293
|
+
gateTotals[bucket] = gateTotals[bucket] + 1;
|
|
294
|
+
if (event.data.pass === true) {
|
|
295
|
+
gatePasses[bucket] = gatePasses[bucket] + 1;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
return {
|
|
300
|
+
tasks: taskSamples,
|
|
301
|
+
gates: gateTotals.map((total, index) => total === 0 ? null : gatePasses[index]),
|
|
302
|
+
passRate: gateTotals.map((total, index) => total === 0 ? null : Math.round((gatePasses[index] / total) * 100)),
|
|
303
|
+
};
|
|
304
|
+
}
|
|
305
|
+
function tipVerificationPassed(events) {
|
|
306
|
+
const eventVerdicts = events.flatMap((event) => {
|
|
307
|
+
if (event.event === "tip-verify-failed")
|
|
308
|
+
return [false];
|
|
309
|
+
if (event.event === "tip-verify")
|
|
310
|
+
return [event.data.pass === true];
|
|
311
|
+
return [];
|
|
312
|
+
});
|
|
313
|
+
const endVerdicts = events.flatMap((event) => event.event === "run-end" && typeof event.data.tipVerify === "string"
|
|
314
|
+
? [event.data.tipVerify === "passed"]
|
|
315
|
+
: []);
|
|
316
|
+
return eventVerdicts.length > 0
|
|
317
|
+
&& endVerdicts.length > 0
|
|
318
|
+
&& eventVerdicts.every(Boolean)
|
|
319
|
+
&& endVerdicts.every(Boolean);
|
|
320
|
+
}
|
|
321
|
+
export function deriveRunCockpitData(source, binaryVersion, options = {}) {
|
|
322
|
+
const { events, defects } = parseSource(source.raw);
|
|
323
|
+
if (events.length === 0)
|
|
324
|
+
throw new Error(`empty cockpit capture: ${source.fileName}`);
|
|
325
|
+
const start = events.find((event) => event.event === "run-start");
|
|
326
|
+
if (!start)
|
|
327
|
+
throw new Error(`capture has no run-start: ${source.fileName}`);
|
|
328
|
+
const lifecycle = latestLifecycle(events);
|
|
329
|
+
const pid = daemonPid(events);
|
|
330
|
+
const alive = lifecycle !== "active"
|
|
331
|
+
? false
|
|
332
|
+
: pid !== undefined && (options.isDaemonAlive ?? defaultDaemonLiveness)(pid);
|
|
333
|
+
const interrupted = !alive;
|
|
334
|
+
const tasks = deriveTasks(events, interrupted);
|
|
335
|
+
const taskFacts = [...tasks.values()];
|
|
336
|
+
const done = taskFacts.filter((task) => task.state === "done").length;
|
|
337
|
+
const failed = taskFacts.filter((task) => task.state === "failed").length;
|
|
338
|
+
const human = taskFacts.filter((task) => task.state === "human").length;
|
|
339
|
+
const hasInterruptedTask = taskFacts.some((task) => task.state === "interrupted");
|
|
340
|
+
const gateEvents = events.filter((event) => event.event === "gate-result");
|
|
341
|
+
const passed = gateEvents.filter((event) => event.data.pass === true).length;
|
|
342
|
+
const passRate = gateEvents.length === 0 ? 0 : Math.round((passed / gateEvents.length) * 100);
|
|
343
|
+
const progress = tasks.size === 0 ? 0 : Math.round((done / tasks.size) * 100);
|
|
344
|
+
const escalated = events.filter((event) => event.event === "escalation").length;
|
|
345
|
+
const tipPassed = tipVerificationPassed(events);
|
|
346
|
+
const spotlight = spotlightTask(tasks);
|
|
347
|
+
const samples = bucketedMetricSamples(events);
|
|
348
|
+
const runStatus = failed > 0
|
|
349
|
+
? "failed"
|
|
350
|
+
: lifecycle === "superseded" || hasInterruptedTask
|
|
351
|
+
? "interrupted"
|
|
352
|
+
: lifecycle === "completed"
|
|
353
|
+
? "done"
|
|
354
|
+
: interrupted
|
|
355
|
+
? "interrupted"
|
|
356
|
+
: "running";
|
|
357
|
+
return {
|
|
358
|
+
binaryVersion,
|
|
359
|
+
runId: source.fileName.replace(/(?:\.interrupted)?\.journal\.jsonl$/, ""),
|
|
360
|
+
branch: typeof start.data.branch === "string" ? start.data.branch : "unknown",
|
|
361
|
+
status: runStatus,
|
|
362
|
+
elapsed: elapsedReading(events[0].ts, events.at(-1).ts),
|
|
363
|
+
tasks: {
|
|
364
|
+
done,
|
|
365
|
+
total: tasks.size,
|
|
366
|
+
samples: samples.tasks,
|
|
367
|
+
},
|
|
368
|
+
gates: {
|
|
369
|
+
passed,
|
|
370
|
+
total: gateEvents.length,
|
|
371
|
+
samples: samples.gates,
|
|
372
|
+
},
|
|
373
|
+
passRate: {
|
|
374
|
+
value: passRate,
|
|
375
|
+
samples: samples.passRate,
|
|
376
|
+
},
|
|
377
|
+
progress,
|
|
378
|
+
progressCaption: spotlight
|
|
379
|
+
? `${spotlight.id} · ${spotlight.phase} · attempt ${taskAttempt(spotlight)} · ${taskActor(spotlight)}`
|
|
380
|
+
: "- · pending · attempt 1 · unassigned",
|
|
381
|
+
journalRows: journalRows(events, tasks, defects, runStatus),
|
|
382
|
+
statusItems: [
|
|
383
|
+
{ state: tipPassed ? "pass" : "fail", text: `tip-verify ${tipPassed ? "passed" : "FAILED"}` },
|
|
384
|
+
{ state: "pass", text: `done ${done}` },
|
|
385
|
+
{ state: failed > 0 ? "fail" : "neutral", text: `failed ${failed}` },
|
|
386
|
+
{ state: human > 0 ? "warn" : "neutral", text: `human ${human}` },
|
|
387
|
+
{ state: escalated > 0 ? "warn" : "neutral", text: `escalated ${escalated}` },
|
|
388
|
+
],
|
|
389
|
+
};
|
|
390
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export declare const COCKPIT_ROW_FLOOR = 14;
|
|
2
|
+
export declare const COCKPIT_COLUMN_FLOOR = 40;
|
|
3
|
+
export declare const FULL_JOURNAL_ROWS = 3;
|
|
4
|
+
/**
|
|
5
|
+
* Ordered from highest priority (last surrendered) to lowest (first
|
|
6
|
+
* surrendered). A minimum row preserves an element in the compact frame; its
|
|
7
|
+
* preferred rows restore the full presentation before the next step receives
|
|
8
|
+
* any rows. The stats minimum is its one-line summary, not the full tiles.
|
|
9
|
+
*/
|
|
10
|
+
declare const ORDERED_POLICY: readonly [{
|
|
11
|
+
readonly element: "keybar";
|
|
12
|
+
readonly minimumRows: 1;
|
|
13
|
+
readonly preferredRows: 1;
|
|
14
|
+
}, {
|
|
15
|
+
readonly element: "statusStrip";
|
|
16
|
+
readonly minimumRows: 1;
|
|
17
|
+
readonly preferredRows: 1;
|
|
18
|
+
}, {
|
|
19
|
+
readonly element: "primaryHeader";
|
|
20
|
+
readonly minimumRows: 1;
|
|
21
|
+
readonly preferredRows: 1;
|
|
22
|
+
}, {
|
|
23
|
+
readonly element: "progressBar";
|
|
24
|
+
readonly minimumRows: 1;
|
|
25
|
+
readonly preferredRows: 1;
|
|
26
|
+
}, {
|
|
27
|
+
readonly element: "journal";
|
|
28
|
+
readonly minimumRows: 1;
|
|
29
|
+
readonly preferredRows: 3;
|
|
30
|
+
}, {
|
|
31
|
+
readonly element: "progressCaption";
|
|
32
|
+
readonly minimumRows: 0;
|
|
33
|
+
readonly preferredRows: 1;
|
|
34
|
+
}, {
|
|
35
|
+
readonly element: "statTiles";
|
|
36
|
+
readonly minimumRows: 1;
|
|
37
|
+
readonly preferredRows: 2;
|
|
38
|
+
}, {
|
|
39
|
+
readonly element: "secondaryHeader";
|
|
40
|
+
readonly minimumRows: 0;
|
|
41
|
+
readonly preferredRows: 1;
|
|
42
|
+
}];
|
|
43
|
+
type PolicyElement = (typeof ORDERED_POLICY)[number]["element"];
|
|
44
|
+
type RowAllocation = Record<PolicyElement, number>;
|
|
45
|
+
export declare const LAYOUT_PRIORITY: readonly PolicyElement[];
|
|
46
|
+
export declare const FULL_LAYOUT_ROWS: number;
|
|
47
|
+
export type CockpitArrangement = "stacked" | "folded-keys" | "three-column";
|
|
48
|
+
export interface FrameCockpitLayout {
|
|
49
|
+
renderer: "frame";
|
|
50
|
+
arrangement: CockpitArrangement;
|
|
51
|
+
elements: {
|
|
52
|
+
version: true;
|
|
53
|
+
keybar: true;
|
|
54
|
+
statusStrip: true;
|
|
55
|
+
primaryHeader: true;
|
|
56
|
+
progressBar: boolean;
|
|
57
|
+
progressCaption: boolean;
|
|
58
|
+
secondaryHeader: boolean;
|
|
59
|
+
};
|
|
60
|
+
stats: {
|
|
61
|
+
mode: "tiles" | "summary";
|
|
62
|
+
rows: number;
|
|
63
|
+
figures: readonly ["tasks", "gates", "pass"];
|
|
64
|
+
};
|
|
65
|
+
journalRows: number;
|
|
66
|
+
rowAllocation: {
|
|
67
|
+
chrome: number;
|
|
68
|
+
} & RowAllocation;
|
|
69
|
+
}
|
|
70
|
+
export interface PlainCockpitLayout {
|
|
71
|
+
renderer: "plain";
|
|
72
|
+
arrangement: "plain";
|
|
73
|
+
}
|
|
74
|
+
export type CockpitLayout = FrameCockpitLayout | PlainCockpitLayout;
|
|
75
|
+
export declare function resolveCockpitLayout(columns: number, rows: number): CockpitLayout;
|
|
76
|
+
export {};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
export const COCKPIT_ROW_FLOOR = 14;
|
|
2
|
+
export const COCKPIT_COLUMN_FLOOR = 40;
|
|
3
|
+
export const FULL_JOURNAL_ROWS = 3;
|
|
4
|
+
const FRAME_CHROME_ROWS = 8;
|
|
5
|
+
/**
|
|
6
|
+
* Ordered from highest priority (last surrendered) to lowest (first
|
|
7
|
+
* surrendered). A minimum row preserves an element in the compact frame; its
|
|
8
|
+
* preferred rows restore the full presentation before the next step receives
|
|
9
|
+
* any rows. The stats minimum is its one-line summary, not the full tiles.
|
|
10
|
+
*/
|
|
11
|
+
const ORDERED_POLICY = [
|
|
12
|
+
{ element: "keybar", minimumRows: 1, preferredRows: 1 },
|
|
13
|
+
{ element: "statusStrip", minimumRows: 1, preferredRows: 1 },
|
|
14
|
+
{ element: "primaryHeader", minimumRows: 1, preferredRows: 1 },
|
|
15
|
+
{ element: "progressBar", minimumRows: 1, preferredRows: 1 },
|
|
16
|
+
{ element: "journal", minimumRows: 1, preferredRows: FULL_JOURNAL_ROWS },
|
|
17
|
+
{ element: "progressCaption", minimumRows: 0, preferredRows: 1 },
|
|
18
|
+
{ element: "statTiles", minimumRows: 1, preferredRows: 2 },
|
|
19
|
+
{ element: "secondaryHeader", minimumRows: 0, preferredRows: 1 },
|
|
20
|
+
];
|
|
21
|
+
export const LAYOUT_PRIORITY = ORDERED_POLICY.map(({ element }) => element);
|
|
22
|
+
export const FULL_LAYOUT_ROWS = FRAME_CHROME_ROWS +
|
|
23
|
+
ORDERED_POLICY.reduce((rows, step) => rows + step.preferredRows, 0);
|
|
24
|
+
function arrangementFor(columns) {
|
|
25
|
+
if (columns >= 120)
|
|
26
|
+
return "three-column";
|
|
27
|
+
if (columns >= 90)
|
|
28
|
+
return "folded-keys";
|
|
29
|
+
return "stacked";
|
|
30
|
+
}
|
|
31
|
+
function allocateRows(rows) {
|
|
32
|
+
const allocation = Object.fromEntries(ORDERED_POLICY.map(({ element }) => [element, 0]));
|
|
33
|
+
let available = rows - FRAME_CHROME_ROWS;
|
|
34
|
+
// Preserve each element's compact form. At the contracted floor these
|
|
35
|
+
// minima consume the budget exactly.
|
|
36
|
+
for (const step of ORDERED_POLICY) {
|
|
37
|
+
const granted = Math.min(step.minimumRows, available);
|
|
38
|
+
allocation[step.element] = granted;
|
|
39
|
+
available -= granted;
|
|
40
|
+
}
|
|
41
|
+
// Restore full forms in priority order, never funding a lower-priority
|
|
42
|
+
// element while a higher-priority element remains compact.
|
|
43
|
+
for (const step of ORDERED_POLICY) {
|
|
44
|
+
const wanted = step.preferredRows - allocation[step.element];
|
|
45
|
+
const granted = Math.min(wanted, available);
|
|
46
|
+
allocation[step.element] += granted;
|
|
47
|
+
available -= granted;
|
|
48
|
+
}
|
|
49
|
+
// Once every element is full, journal history is the sole useful sink.
|
|
50
|
+
allocation.journal += available;
|
|
51
|
+
return allocation;
|
|
52
|
+
}
|
|
53
|
+
export function resolveCockpitLayout(columns, rows) {
|
|
54
|
+
if (!Number.isFinite(columns) ||
|
|
55
|
+
!Number.isFinite(rows) ||
|
|
56
|
+
columns < COCKPIT_COLUMN_FLOOR ||
|
|
57
|
+
rows < COCKPIT_ROW_FLOOR) {
|
|
58
|
+
return { renderer: "plain", arrangement: "plain" };
|
|
59
|
+
}
|
|
60
|
+
const allocation = allocateRows(Math.floor(rows));
|
|
61
|
+
return {
|
|
62
|
+
renderer: "frame",
|
|
63
|
+
arrangement: arrangementFor(Math.floor(columns)),
|
|
64
|
+
elements: {
|
|
65
|
+
version: true,
|
|
66
|
+
keybar: true,
|
|
67
|
+
statusStrip: true,
|
|
68
|
+
primaryHeader: true,
|
|
69
|
+
progressBar: allocation.progressBar > 0,
|
|
70
|
+
progressCaption: allocation.progressCaption > 0,
|
|
71
|
+
secondaryHeader: allocation.secondaryHeader > 0,
|
|
72
|
+
},
|
|
73
|
+
stats: {
|
|
74
|
+
mode: allocation.statTiles > 1 ? "tiles" : "summary",
|
|
75
|
+
rows: allocation.statTiles,
|
|
76
|
+
figures: ["tasks", "gates", "pass"],
|
|
77
|
+
},
|
|
78
|
+
journalRows: allocation.journal,
|
|
79
|
+
rowAllocation: {
|
|
80
|
+
chrome: FRAME_CHROME_ROWS,
|
|
81
|
+
...allocation,
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type ReactElement } from "react";
|
|
2
|
+
import type { RunCockpitData } from "./derive.js";
|
|
3
|
+
export { deriveRunCockpitData } from "./derive.js";
|
|
4
|
+
export type { RunCockpitData } from "./derive.js";
|
|
5
|
+
export { PANEL_CHROME_ROWS } from "./components.js";
|
|
6
|
+
/**
|
|
7
|
+
* How many rows one line of body text occupies once the renderer wraps it into
|
|
8
|
+
* `columns`. This mirrors the renderer's own word wrap — greedy, and breaking a
|
|
9
|
+
* word that cannot fit on a line of its own — so a frame can charge a panel the
|
|
10
|
+
* rows it will really occupy instead of a hand-tuned constant.
|
|
11
|
+
*/
|
|
12
|
+
export declare function wrappedRows(text: string, columns: number): number;
|
|
13
|
+
/** The rows a bordered panel occupies when its body wraps into `columns`. */
|
|
14
|
+
export declare function panelRows(lines: readonly string[], columns: number): number;
|
|
15
|
+
export declare function RunCockpitFrame({ data, columns, rows, }: {
|
|
16
|
+
data: RunCockpitData;
|
|
17
|
+
columns: number;
|
|
18
|
+
rows?: number;
|
|
19
|
+
}): ReactElement;
|