@crewhaus/hangar-ui 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +309 -0
- package/assets/hangar.css +1726 -0
- package/assets/index.html +21 -0
- package/assets/js/api.js +368 -0
- package/assets/js/app.js +734 -0
- package/assets/js/dom.js +228 -0
- package/assets/js/keys.js +159 -0
- package/assets/js/markdown.js +131 -0
- package/assets/js/notify.js +152 -0
- package/assets/js/omnibox.js +307 -0
- package/assets/js/router.js +280 -0
- package/assets/js/routes.js +793 -0
- package/assets/js/shapes.js +36 -0
- package/assets/js/supervision.js +1334 -0
- package/assets/js/util.js +426 -0
- package/assets/js/views/activity.js +140 -0
- package/assets/js/views/approvals.js +155 -0
- package/assets/js/views/channels.js +455 -0
- package/assets/js/views/control.js +253 -0
- package/assets/js/views/costs.js +103 -0
- package/assets/js/views/creds.js +610 -0
- package/assets/js/views/data.js +456 -0
- package/assets/js/views/deploy.js +106 -0
- package/assets/js/views/evals-lab.js +1029 -0
- package/assets/js/views/evals.js +342 -0
- package/assets/js/views/feedback.js +555 -0
- package/assets/js/views/health.js +176 -0
- package/assets/js/views/inbox.js +103 -0
- package/assets/js/views/inspect.js +751 -0
- package/assets/js/views/jobs.js +110 -0
- package/assets/js/views/library.js +605 -0
- package/assets/js/views/memory-fabric.js +2026 -0
- package/assets/js/views/memory.js +228 -0
- package/assets/js/views/onboarding.js +253 -0
- package/assets/js/views/overview.js +196 -0
- package/assets/js/views/panes.js +132 -0
- package/assets/js/views/proc.js +280 -0
- package/assets/js/views/review.js +164 -0
- package/assets/js/views/runs.js +452 -0
- package/assets/js/views/runtime.js +450 -0
- package/assets/js/views/schedulers.js +133 -0
- package/assets/js/views/security.js +925 -0
- package/assets/js/views/sessions.js +265 -0
- package/assets/js/views/settings.js +489 -0
- package/assets/js/views/spec-edit.js +1289 -0
- package/assets/js/views/spec.js +66 -0
- package/assets/js/views/thredz.js +1853 -0
- package/assets/js/views/token.js +49 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +145 -0
- package/package.json +36 -0
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runs & daemons — the fleet supervision board (`#/runs`), one harness's run
|
|
3
|
+
* ledger, and the run console with the live SSE feed.
|
|
4
|
+
*
|
|
5
|
+
* THE BOARD paints twice, like the Library: instantly from the fleet feed
|
|
6
|
+
* (whose rows carry `supervision` and `pendingApprovals` — a cheap fold, no
|
|
7
|
+
* transcript opened), then again as each harness's `/proc` answers with pid,
|
|
8
|
+
* uptime, restart countdown, control availability and last exit. Under the
|
|
9
|
+
* table sit the two fleet-level reads an operator actually acts on: the job
|
|
10
|
+
* queue, and the failure board that turns three red rows into one incident.
|
|
11
|
+
*
|
|
12
|
+
* THE CONSOLE relies on the SSE grammar rather than branching on liveness:
|
|
13
|
+
* `replay` (a whole RunDetail) opens every stream and `done` always closes
|
|
14
|
+
* it, so a finished run replays and terminates through the same code path a
|
|
15
|
+
* live one streams through. Trace events render as feed cards in the M1
|
|
16
|
+
* transcript vocabulary — same chips, same folds — with interleaved prose
|
|
17
|
+
* from the captured (scrubbed) log and a stats/cost HUD folded from the
|
|
18
|
+
* frames as they arrive.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { api, streamRunEvents } from "../api.js";
|
|
22
|
+
import { clear, collapsible, dot, el, emptyState, jsonPre, skeleton } from "../dom.js";
|
|
23
|
+
import { hrefHarness } from "../router.js";
|
|
24
|
+
import { shapeAccent, shapeLabel } from "../shapes.js";
|
|
25
|
+
import {
|
|
26
|
+
failureBoard,
|
|
27
|
+
ledgerExit,
|
|
28
|
+
procRow,
|
|
29
|
+
runHud,
|
|
30
|
+
runLedgerRow,
|
|
31
|
+
sseFrameToFeed,
|
|
32
|
+
} from "../supervision.js";
|
|
33
|
+
import { dirTail, fmtUsd, normalizeRows } from "../util.js";
|
|
34
|
+
import { actionTwin } from "./control.js";
|
|
35
|
+
import { renderJobs } from "./jobs.js";
|
|
36
|
+
import { procActionBar } from "./proc.js";
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// #/runs — the fleet board
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
const BOARD_COLUMNS = [
|
|
43
|
+
"Harness",
|
|
44
|
+
"State",
|
|
45
|
+
"PID",
|
|
46
|
+
"Uptime",
|
|
47
|
+
"Restarts",
|
|
48
|
+
"Last exit",
|
|
49
|
+
"Control",
|
|
50
|
+
"Actions",
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
export async function renderRunsBoard(root) {
|
|
54
|
+
clear(root).appendChild(skeleton(6));
|
|
55
|
+
const feed = await api.harnesses();
|
|
56
|
+
const rows = normalizeRows(feed).filter((r) => r.missingSince === null);
|
|
57
|
+
clear(root);
|
|
58
|
+
root.appendChild(
|
|
59
|
+
el("div", { class: "rollup" }, [
|
|
60
|
+
el("span", { text: `${rows.length} registered · supervision board` }),
|
|
61
|
+
el("span", { class: "muted", text: "· loading process detail…" }),
|
|
62
|
+
]),
|
|
63
|
+
);
|
|
64
|
+
if (rows.length === 0) {
|
|
65
|
+
root.appendChild(emptyState("No harnesses registered yet", "crewhaus harness add <dir>"));
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const tbody = el("tbody");
|
|
70
|
+
const table = el("div", { class: "table-scroll" }, [
|
|
71
|
+
el("table", { class: "fleet" }, [
|
|
72
|
+
el(
|
|
73
|
+
"thead",
|
|
74
|
+
null,
|
|
75
|
+
el(
|
|
76
|
+
"tr",
|
|
77
|
+
null,
|
|
78
|
+
BOARD_COLUMNS.map((h) => el("th", { text: h })),
|
|
79
|
+
),
|
|
80
|
+
),
|
|
81
|
+
tbody,
|
|
82
|
+
]),
|
|
83
|
+
]);
|
|
84
|
+
root.appendChild(table);
|
|
85
|
+
|
|
86
|
+
const failures = el("section", { class: "card" }, [
|
|
87
|
+
el("h3", { class: "card-title", text: "Failure board" }),
|
|
88
|
+
el("p", { class: "muted", text: "loading…" }),
|
|
89
|
+
]);
|
|
90
|
+
const jobsCard = el("section", { class: "card" }, [
|
|
91
|
+
el("h3", { class: "card-title", text: "Job queue" }),
|
|
92
|
+
]);
|
|
93
|
+
const jobsBody = el("div");
|
|
94
|
+
jobsCard.appendChild(jobsBody);
|
|
95
|
+
root.appendChild(el("div", { class: "ov-grid" }, [failures, jobsCard]));
|
|
96
|
+
renderJobs(jobsBody);
|
|
97
|
+
|
|
98
|
+
const reload = () => renderRunsBoard(root);
|
|
99
|
+
const nowMs = Date.now();
|
|
100
|
+
const placeholders = new Map();
|
|
101
|
+
for (const row of rows) {
|
|
102
|
+
const tr = boardRow(row, null, nowMs, reload);
|
|
103
|
+
placeholders.set(row.id, tr);
|
|
104
|
+
tbody.appendChild(tr);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Hydrate each row with its process detail. One request per harness, all
|
|
108
|
+
// in flight together; a failing one leaves its cold row standing.
|
|
109
|
+
const details = await Promise.allSettled(rows.map((r) => api.proc(r.id)));
|
|
110
|
+
if (!root.isConnected) return;
|
|
111
|
+
const exits = [];
|
|
112
|
+
rows.forEach((row, i) => {
|
|
113
|
+
const settled = details[i];
|
|
114
|
+
const payload = settled.status === "fulfilled" ? settled.value : null;
|
|
115
|
+
const tr = placeholders.get(row.id);
|
|
116
|
+
if (tr === undefined) return;
|
|
117
|
+
tr.replaceWith(boardRow(row, payload, Date.now(), reload));
|
|
118
|
+
// The live snapshot when this manager supervised the run; the harness's
|
|
119
|
+
// own ledger when it did not (a console restart must not erase failures).
|
|
120
|
+
const exit = payload === null ? null : (payload.lastExit ?? ledgerExit(payload.recentRuns));
|
|
121
|
+
if (exit !== null) {
|
|
122
|
+
exits.push({ id: row.id, name: row.specName || dirTail(row.dir, 1), lastExit: exit });
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
const rollup = root.querySelector(".rollup .muted");
|
|
126
|
+
if (rollup !== null) rollup.remove();
|
|
127
|
+
clear(failures).appendChild(el("h3", { class: "card-title", text: "Failure board" }));
|
|
128
|
+
const groups = failureBoard(exits);
|
|
129
|
+
if (groups.length === 0) {
|
|
130
|
+
failures.appendChild(
|
|
131
|
+
el("p", { class: "muted", text: "No harness has a failing last exit — nothing to group." }),
|
|
132
|
+
);
|
|
133
|
+
} else {
|
|
134
|
+
for (const group of groups) {
|
|
135
|
+
failures.appendChild(
|
|
136
|
+
el("div", { class: "fail-group" }, [
|
|
137
|
+
el("div", { class: "fail-head" }, [
|
|
138
|
+
dot(group.dot, group.label),
|
|
139
|
+
// The classification's own sentence, when it says more than the
|
|
140
|
+
// count line already does.
|
|
141
|
+
group.title !== "" && !group.label.includes(group.title)
|
|
142
|
+
? el("span", { class: "muted", text: group.title })
|
|
143
|
+
: null,
|
|
144
|
+
]),
|
|
145
|
+
group.remediation !== null
|
|
146
|
+
? el("div", { class: "muted reason", text: group.remediation })
|
|
147
|
+
: null,
|
|
148
|
+
el(
|
|
149
|
+
"div",
|
|
150
|
+
{ class: "fail-members" },
|
|
151
|
+
group.harnesses.map((h) =>
|
|
152
|
+
el("a", { class: "chip", href: hrefHarness(h.id, "runs"), text: h.name }),
|
|
153
|
+
),
|
|
154
|
+
),
|
|
155
|
+
]),
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function boardRow(feedRow, proc, nowMs, reload) {
|
|
162
|
+
const name = feedRow.specName || dirTail(feedRow.dir, 1) || feedRow.id;
|
|
163
|
+
// The fleet feed's `supervision` is the instant answer; `/proc` refines it.
|
|
164
|
+
const row = procRow(proc ?? { state: feedRow.supervision ?? "stopped" }, nowMs);
|
|
165
|
+
const cold = proc === null;
|
|
166
|
+
return el("tr", { class: "fleet-row" }, [
|
|
167
|
+
el("td", { class: "cell-name" }, [
|
|
168
|
+
el("div", { class: "name-line" }, [
|
|
169
|
+
el("span", {
|
|
170
|
+
class: "shape-badge",
|
|
171
|
+
style: { "--accent": shapeAccent(feedRow.target) },
|
|
172
|
+
text: shapeLabel(feedRow.target),
|
|
173
|
+
}),
|
|
174
|
+
el("a", { class: "name-link", href: hrefHarness(feedRow.id, "runs"), text: name }),
|
|
175
|
+
]),
|
|
176
|
+
el("div", { class: "sub mono", title: feedRow.dir, text: dirTail(feedRow.dir, 2) }),
|
|
177
|
+
feedRow.pendingApprovals > 0
|
|
178
|
+
? el("a", {
|
|
179
|
+
class: "chip chip-warn",
|
|
180
|
+
href: "#/approvals",
|
|
181
|
+
text: `${feedRow.pendingApprovals} approval${feedRow.pendingApprovals === 1 ? "" : "s"} waiting`,
|
|
182
|
+
})
|
|
183
|
+
: null,
|
|
184
|
+
]),
|
|
185
|
+
el("td", null, [
|
|
186
|
+
dot(row.pill.dot, row.pill.label),
|
|
187
|
+
row.adopted ? el("span", { class: "chip", text: "adopted" }) : null,
|
|
188
|
+
row.draining ? el("span", { class: "chip chip-warn", text: "draining" }) : null,
|
|
189
|
+
]),
|
|
190
|
+
el("td", { class: "num", text: row.pid === null ? "—" : String(row.pid) }),
|
|
191
|
+
el("td", { class: "num", text: cold ? "…" : row.uptime }),
|
|
192
|
+
el("td", { class: "num" }, [
|
|
193
|
+
el("span", { text: String(row.restartsInWindow) }),
|
|
194
|
+
row.restart !== null ? el("div", { class: "chip chip-warn", text: row.restart.label }) : null,
|
|
195
|
+
]),
|
|
196
|
+
el("td", null, [
|
|
197
|
+
row.lastExit === null
|
|
198
|
+
? el("span", { class: "muted", text: cold ? "…" : "—" })
|
|
199
|
+
: el("span", null, [
|
|
200
|
+
dot(row.lastExit.dot, row.lastExit.title),
|
|
201
|
+
row.lastExit.classLabel !== null
|
|
202
|
+
? el("span", { class: "chip chip-warn", text: row.lastExit.classLabel })
|
|
203
|
+
: null,
|
|
204
|
+
row.lastExit.fromLedger
|
|
205
|
+
? el("span", {
|
|
206
|
+
class: "chip",
|
|
207
|
+
title: "recorded in the harness's run ledger, not observed by this manager",
|
|
208
|
+
text: "from the ledger",
|
|
209
|
+
})
|
|
210
|
+
: null,
|
|
211
|
+
]),
|
|
212
|
+
]),
|
|
213
|
+
el("td", null, [
|
|
214
|
+
dot(row.control.dot, row.control.label),
|
|
215
|
+
!row.control.available && row.control.reason !== null
|
|
216
|
+
? el("div", { class: "muted reason", text: row.control.reason })
|
|
217
|
+
: null,
|
|
218
|
+
]),
|
|
219
|
+
el("td", { class: "cell-actions" }, [
|
|
220
|
+
cold
|
|
221
|
+
? el("span", { class: "muted", text: "…" })
|
|
222
|
+
: procActionBar(feedRow.id, feedRow.dir, row, reload),
|
|
223
|
+
]),
|
|
224
|
+
]);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ---------------------------------------------------------------------------
|
|
228
|
+
// #/h/:id/runs — the ledger, and one run's console
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
|
|
231
|
+
export async function renderRuns(root, ctx) {
|
|
232
|
+
if (ctx.route.runId !== undefined) {
|
|
233
|
+
await renderRunConsole(root, ctx, ctx.route.runId);
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
clear(root).appendChild(skeleton(6));
|
|
237
|
+
const [procRes, runsRes] = await Promise.allSettled([api.proc(ctx.id), api.runs(ctx.id)]);
|
|
238
|
+
clear(root);
|
|
239
|
+
const reload = () => renderRuns(root, ctx);
|
|
240
|
+
|
|
241
|
+
if (procRes.status === "fulfilled" && procRes.value !== null) {
|
|
242
|
+
const row = procRow(procRes.value, Date.now());
|
|
243
|
+
root.appendChild(
|
|
244
|
+
el("section", { class: "card" }, [
|
|
245
|
+
el("h3", { class: "card-title" }, [
|
|
246
|
+
el("span", { text: "Supervision" }),
|
|
247
|
+
dot(row.pill.dot, row.pill.label),
|
|
248
|
+
row.runId !== null
|
|
249
|
+
? el("a", {
|
|
250
|
+
class: "mono",
|
|
251
|
+
href: hrefHarness(ctx.id, "runs", row.runId),
|
|
252
|
+
text: "watch the live run →",
|
|
253
|
+
})
|
|
254
|
+
: null,
|
|
255
|
+
]),
|
|
256
|
+
procActionBar(ctx.id, ctx.dir ?? "", row, reload),
|
|
257
|
+
]),
|
|
258
|
+
);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const data = runsRes.status === "fulfilled" ? runsRes.value : null;
|
|
262
|
+
const runs = data && Array.isArray(data.runs) ? data.runs : [];
|
|
263
|
+
if (runs.length === 0) {
|
|
264
|
+
root.appendChild(emptyState("No runs recorded yet", "crewhaus daemon start"));
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
const nowMs = Date.now();
|
|
268
|
+
const tbody = el("tbody");
|
|
269
|
+
for (const entry of runs) {
|
|
270
|
+
const row = runLedgerRow(entry, nowMs);
|
|
271
|
+
tbody.appendChild(
|
|
272
|
+
el("tr", null, [
|
|
273
|
+
el("td", null, [
|
|
274
|
+
el("a", {
|
|
275
|
+
class: "mono name-link",
|
|
276
|
+
href: hrefHarness(ctx.id, "runs", row.runId),
|
|
277
|
+
text: row.runId,
|
|
278
|
+
}),
|
|
279
|
+
]),
|
|
280
|
+
el("td", { class: "mono", text: row.kind }),
|
|
281
|
+
el("td", { text: row.startedLabel }),
|
|
282
|
+
el("td", { class: "num", text: row.duration }),
|
|
283
|
+
el("td", null, [
|
|
284
|
+
dot(row.dot, row.exitLabel),
|
|
285
|
+
row.forced ? el("span", { class: "chip chip-warn", text: "forced" }) : null,
|
|
286
|
+
]),
|
|
287
|
+
]),
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
root.appendChild(
|
|
291
|
+
el("div", { class: "table-scroll" }, [
|
|
292
|
+
el("table", { class: "fleet" }, [
|
|
293
|
+
el(
|
|
294
|
+
"thead",
|
|
295
|
+
null,
|
|
296
|
+
el(
|
|
297
|
+
"tr",
|
|
298
|
+
null,
|
|
299
|
+
["Run", "Kind", "Started", "Duration", "Outcome"].map((h) => el("th", { text: h })),
|
|
300
|
+
),
|
|
301
|
+
),
|
|
302
|
+
tbody,
|
|
303
|
+
]),
|
|
304
|
+
]),
|
|
305
|
+
);
|
|
306
|
+
if (data && data.truncated === true) {
|
|
307
|
+
root.appendChild(
|
|
308
|
+
el("p", { class: "muted", text: "Long ledger — showing the most recent runs only." }),
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/**
|
|
314
|
+
* One run's console. Opens the SSE feed, renders `replay` history and any
|
|
315
|
+
* live frames into the same feed, and closes the stream when the view is
|
|
316
|
+
* replaced (a navigation must not leave a reader dangling).
|
|
317
|
+
*/
|
|
318
|
+
async function renderRunConsole(root, ctx, runId) {
|
|
319
|
+
clear(root);
|
|
320
|
+
root.appendChild(
|
|
321
|
+
el("div", { class: "crumb-line" }, [
|
|
322
|
+
el("a", { href: hrefHarness(ctx.id, "runs"), text: "← runs" }),
|
|
323
|
+
el("span", { class: "mono muted", text: runId }),
|
|
324
|
+
actionTwin("logs", { dir: ctx.dir ?? "", runId }),
|
|
325
|
+
]),
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
const hudNode = el("div", { class: "hud" });
|
|
329
|
+
const banner = el("div", { class: "run-banner" });
|
|
330
|
+
const feed = el("div", { class: "transcript run-feed" });
|
|
331
|
+
root.appendChild(hudNode);
|
|
332
|
+
root.appendChild(banner);
|
|
333
|
+
root.appendChild(el("div", { class: "feed-wrap" }, [feed]));
|
|
334
|
+
|
|
335
|
+
const items = [];
|
|
336
|
+
const drawHud = () => {
|
|
337
|
+
const hud = runHud(items);
|
|
338
|
+
clear(hudNode);
|
|
339
|
+
const stat = (label, value) =>
|
|
340
|
+
el("div", { class: "mini" }, [
|
|
341
|
+
el("div", { class: "mini-num", text: value }),
|
|
342
|
+
el("div", { class: "mini-label", text: label }),
|
|
343
|
+
]);
|
|
344
|
+
hudNode.appendChild(
|
|
345
|
+
el("div", { class: "mini-row" }, [
|
|
346
|
+
stat("events", String(hud.events)),
|
|
347
|
+
stat("turns", String(hud.turns)),
|
|
348
|
+
stat(
|
|
349
|
+
"tool calls",
|
|
350
|
+
`${hud.tools}${hud.toolErrors > 0 ? ` / ${hud.toolErrors} failed` : ""}`,
|
|
351
|
+
),
|
|
352
|
+
stat("cost", fmtUsd(hud.costUsd)),
|
|
353
|
+
stat("tokens in/out", `${hud.inputTokens}/${hud.outputTokens}`),
|
|
354
|
+
]),
|
|
355
|
+
);
|
|
356
|
+
};
|
|
357
|
+
drawHud();
|
|
358
|
+
|
|
359
|
+
let done = false;
|
|
360
|
+
const push = (item) => {
|
|
361
|
+
items.push(item);
|
|
362
|
+
feed.appendChild(feedNode(item));
|
|
363
|
+
if (item.type === "exit" && item.banner !== null) {
|
|
364
|
+
clear(banner).appendChild(exitBannerNode(item.banner));
|
|
365
|
+
}
|
|
366
|
+
if (item.type === "done") {
|
|
367
|
+
done = true;
|
|
368
|
+
banner.appendChild(el("span", { class: "chip", text: `stream closed — ${item.detail}` }));
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
|
|
372
|
+
// The stream is bound to this node: the first frame that arrives after the
|
|
373
|
+
// router replaced the view aborts the reader instead of leaving it holding
|
|
374
|
+
// a connection.
|
|
375
|
+
const stream = streamRunEvents(ctx.id, runId, (frame) => {
|
|
376
|
+
if (!feed.isConnected) {
|
|
377
|
+
stream.close();
|
|
378
|
+
return;
|
|
379
|
+
}
|
|
380
|
+
if (frame.event === "error") {
|
|
381
|
+
const message = frame.data?.message ? String(frame.data.message) : "stream failed";
|
|
382
|
+
clear(banner).appendChild(dot("bad", message));
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
if (frame.event === "closed") {
|
|
386
|
+
// Only worth saying when the server never sent its terminal `done` —
|
|
387
|
+
// that is the difference between "finished" and "dropped".
|
|
388
|
+
if (!done) {
|
|
389
|
+
banner.appendChild(el("span", { class: "chip chip-warn", text: "connection dropped" }));
|
|
390
|
+
}
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
for (const item of sseFrameToFeed(frame.event, frame.data)) push(item);
|
|
394
|
+
drawHud();
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function exitBannerNode(banner) {
|
|
399
|
+
return el("div", { class: "exit-banner" }, [
|
|
400
|
+
dot(banner.dot, banner.title),
|
|
401
|
+
banner.classLabel !== null
|
|
402
|
+
? el("span", { class: "chip chip-warn", text: banner.classLabel })
|
|
403
|
+
: null,
|
|
404
|
+
banner.exitCode !== null
|
|
405
|
+
? el("span", { class: "chip", text: `exit ${banner.exitCode}` })
|
|
406
|
+
: null,
|
|
407
|
+
banner.restartable
|
|
408
|
+
? el("span", { class: "chip", text: "restartable" })
|
|
409
|
+
: el("span", { class: "chip chip-warn", text: "will not auto-restart" }),
|
|
410
|
+
banner.remediation !== null
|
|
411
|
+
? el("div", { class: "muted reason", text: banner.remediation })
|
|
412
|
+
: null,
|
|
413
|
+
]);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/** One feed item → its card, in the M1 transcript vocabulary. */
|
|
417
|
+
function feedNode(item) {
|
|
418
|
+
if (item.type === "prose") {
|
|
419
|
+
return el("pre", { class: "prose-pre feed-prose", text: item.lines.join("\n") });
|
|
420
|
+
}
|
|
421
|
+
if (item.type === "marker") {
|
|
422
|
+
return el("div", { class: "gutter" }, [
|
|
423
|
+
el("span", { class: "chip", text: item.title }),
|
|
424
|
+
item.detail ? el("span", { class: "muted", text: item.detail }) : null,
|
|
425
|
+
]);
|
|
426
|
+
}
|
|
427
|
+
if (item.type === "state") {
|
|
428
|
+
return el("div", { class: "gutter" }, [dot(item.dot, item.title)]);
|
|
429
|
+
}
|
|
430
|
+
if (item.type === "exit") {
|
|
431
|
+
return el("div", { class: "gutter" }, [dot(item.dot, `exited — ${item.title}`)]);
|
|
432
|
+
}
|
|
433
|
+
if (item.type === "done") {
|
|
434
|
+
return el("div", { class: "gutter" }, [
|
|
435
|
+
el("span", { class: "chip", text: "done" }),
|
|
436
|
+
el("span", { class: "muted", text: item.detail }),
|
|
437
|
+
]);
|
|
438
|
+
}
|
|
439
|
+
// A trace event: a fold whose summary reads at a glance and whose body is
|
|
440
|
+
// the raw (already masked + scrubbed) payload.
|
|
441
|
+
return collapsible(
|
|
442
|
+
[
|
|
443
|
+
el("span", { class: "chip chip-tool", text: item.kind }),
|
|
444
|
+
item.dot !== null ? dot(item.dot, item.title) : el("span", { text: item.title }),
|
|
445
|
+
el("span", { class: "muted tool-summary", text: item.detail ?? "" }),
|
|
446
|
+
],
|
|
447
|
+
[
|
|
448
|
+
item.remediation ? el("p", null, [el("strong", { text: "Fix: " }), item.remediation]) : null,
|
|
449
|
+
jsonPre(item.payload),
|
|
450
|
+
],
|
|
451
|
+
);
|
|
452
|
+
}
|