@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,265 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sessions tab — the TTL-safe browser (the server reads via raw dir scans,
|
|
3
|
+
* never an evicting store call) and the transcript view. The server's
|
|
4
|
+
* transcript envelope is `{ turns, tools, gutter, otherKinds, truncated,
|
|
5
|
+
* lineCount, tornCount }` — pre-sorted per kind with per-item `line`
|
|
6
|
+
* anchors — and this view interleaves the three streams by line into chat
|
|
7
|
+
* bubbles, collapsible tool cards (with the joined result + error flag),
|
|
8
|
+
* and a metadata gutter, plus a Raw toggle. Unknown kinds are tallied
|
|
9
|
+
* server-side in `otherKinds`; the count renders so nothing is silently
|
|
10
|
+
* dropped — tolerant-reader contract.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { api } from "../api.js";
|
|
14
|
+
import { clear, collapsible, dot, el, emptyState, jsonPre, skeleton, toast } from "../dom.js";
|
|
15
|
+
import { hrefHarness } from "../router.js";
|
|
16
|
+
import { NO_TWIN_NOTES } from "../supervision.js";
|
|
17
|
+
import {
|
|
18
|
+
clampText,
|
|
19
|
+
fmtRelativeTime,
|
|
20
|
+
fmtUsd,
|
|
21
|
+
interleaveTranscript,
|
|
22
|
+
ttlCountdown,
|
|
23
|
+
usdFromMicros,
|
|
24
|
+
} from "../util.js";
|
|
25
|
+
import { cliTwin } from "./control.js";
|
|
26
|
+
|
|
27
|
+
export async function renderSessions(root, ctx) {
|
|
28
|
+
if (ctx.route.sessionId !== undefined) {
|
|
29
|
+
await renderTranscript(root, ctx, ctx.route.sessionId);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
clear(root).appendChild(skeleton(6));
|
|
33
|
+
const data = await api.sessions(ctx.id);
|
|
34
|
+
clear(root);
|
|
35
|
+
const list = Array.isArray(data) ? data : Array.isArray(data?.sessions) ? data.sessions : [];
|
|
36
|
+
if (list.length === 0) {
|
|
37
|
+
root.appendChild(emptyState("No sessions yet", "crewhaus run"));
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
const nowMs = Date.now();
|
|
41
|
+
// Pinned transcripts are the ONE thing that stops both retention enforcers
|
|
42
|
+
// from deleting them, so the pin is a real write with a visible effect.
|
|
43
|
+
const pins = new Set(data && Array.isArray(data.pins) ? data.pins.map(String) : []);
|
|
44
|
+
const reload = () => renderSessions(root, ctx);
|
|
45
|
+
const tbody = el("tbody");
|
|
46
|
+
for (const s of list) {
|
|
47
|
+
const id = String(s.id ?? s.sessionId ?? "");
|
|
48
|
+
const evicted = s.evicted === true;
|
|
49
|
+
const ttl = evicted
|
|
50
|
+
? { label: "summary only" }
|
|
51
|
+
: ttlCountdown(s.updatedAt ?? null, nowMs, s.ttlDays ?? undefined);
|
|
52
|
+
tbody.appendChild(
|
|
53
|
+
el("tr", { class: evicted ? "evicted" : null }, [
|
|
54
|
+
el("td", null, [
|
|
55
|
+
pinToggle(ctx.id, id, pins.has(id), reload),
|
|
56
|
+
el("a", {
|
|
57
|
+
class: "mono name-link",
|
|
58
|
+
href: hrefHarness(ctx.id, "sessions", id),
|
|
59
|
+
text: id,
|
|
60
|
+
}),
|
|
61
|
+
evicted ? el("span", { class: "chip chip-warn", text: "summary only" }) : null,
|
|
62
|
+
]),
|
|
63
|
+
el("td", { text: s.name ? String(s.name) : "—" }),
|
|
64
|
+
el("td", { class: "mono", text: s.model ? String(s.model) : "—" }),
|
|
65
|
+
el("td", { class: "num", text: String(s.turns ?? s.lastTurnIndex ?? "—") }),
|
|
66
|
+
el("td", { text: fmtRelativeTime(s.updatedAt ?? null, nowMs) }),
|
|
67
|
+
el("td", null, [
|
|
68
|
+
evicted ? dot("off", "evicted") : dot(ttl.expired ? "warn" : "ok", ttl.label),
|
|
69
|
+
]),
|
|
70
|
+
]),
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
root.appendChild(
|
|
74
|
+
el("div", { class: "table-scroll" }, [
|
|
75
|
+
el("table", { class: "fleet" }, [
|
|
76
|
+
el(
|
|
77
|
+
"thead",
|
|
78
|
+
null,
|
|
79
|
+
el(
|
|
80
|
+
"tr",
|
|
81
|
+
null,
|
|
82
|
+
["Session", "Name", "Model", "Turns", "Age", "Retention"].map((h) =>
|
|
83
|
+
el("th", { text: h }),
|
|
84
|
+
),
|
|
85
|
+
),
|
|
86
|
+
),
|
|
87
|
+
tbody,
|
|
88
|
+
]),
|
|
89
|
+
]),
|
|
90
|
+
);
|
|
91
|
+
// The pin has no CLI verb yet; say what it writes rather than invent one.
|
|
92
|
+
root.appendChild(cliTwin(null, NO_TWIN_NOTES["pin-session"]));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The retention pin. A pinned transcript survives both enforcers (the
|
|
97
|
+
* `retention sweep` verb and the daemon shapes' boot janitor), so this is
|
|
98
|
+
* the highest-consequence small write in the console — it is a toggle with
|
|
99
|
+
* an immediate, re-readable effect, never a silent one.
|
|
100
|
+
*/
|
|
101
|
+
function pinToggle(harnessId, sessionId, pinned, reload) {
|
|
102
|
+
const btn = el("button", {
|
|
103
|
+
class: `pin${pinned ? " pinned" : ""}`,
|
|
104
|
+
type: "button",
|
|
105
|
+
title: pinned ? "Unpin (retention may delete it again)" : "Pin — retention will not delete it",
|
|
106
|
+
"aria-label": pinned ? `unpin ${sessionId}` : `pin ${sessionId}`,
|
|
107
|
+
text: pinned ? "★" : "☆",
|
|
108
|
+
});
|
|
109
|
+
btn.addEventListener("click", async () => {
|
|
110
|
+
btn.disabled = true;
|
|
111
|
+
try {
|
|
112
|
+
await api.pinSession(harnessId, sessionId, !pinned);
|
|
113
|
+
} catch (err) {
|
|
114
|
+
btn.disabled = false;
|
|
115
|
+
toast(`Pin failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
reload();
|
|
119
|
+
});
|
|
120
|
+
return btn;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function renderTranscript(root, ctx, sessionId) {
|
|
124
|
+
clear(root).appendChild(skeleton(8));
|
|
125
|
+
const data = await api.session(ctx.id, sessionId);
|
|
126
|
+
clear(root);
|
|
127
|
+
root.appendChild(
|
|
128
|
+
el("div", { class: "crumb-line" }, [
|
|
129
|
+
el("a", { href: hrefHarness(ctx.id, "sessions"), text: "← sessions" }),
|
|
130
|
+
el("span", { class: "mono muted", text: sessionId }),
|
|
131
|
+
]),
|
|
132
|
+
);
|
|
133
|
+
if (data === null) {
|
|
134
|
+
root.appendChild(emptyState("Nothing here yet — this session has no readable transcript"));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
if (data.evicted === true) {
|
|
138
|
+
root.appendChild(
|
|
139
|
+
summaryCard(data.summary && typeof data.summary === "object" ? data.summary : {}),
|
|
140
|
+
);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
const entries = interleaveTranscript(data);
|
|
144
|
+
if (entries.length === 0) {
|
|
145
|
+
root.appendChild(emptyState("Transcript is empty"));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const rawPre = jsonPre(data);
|
|
150
|
+
rawPre.hidden = true;
|
|
151
|
+
const rawBtn = el("button", { class: "btn btn-ghost", type: "button", text: "Raw" });
|
|
152
|
+
rawBtn.addEventListener("click", () => {
|
|
153
|
+
rawPre.hidden = !rawPre.hidden;
|
|
154
|
+
rawBtn.classList.toggle("active", !rawPre.hidden);
|
|
155
|
+
});
|
|
156
|
+
root.appendChild(el("div", { class: "transcript-tools" }, [rawBtn]));
|
|
157
|
+
root.appendChild(rawPre);
|
|
158
|
+
|
|
159
|
+
const otherCount = Object.values(
|
|
160
|
+
data.otherKinds && typeof data.otherKinds === "object" ? data.otherKinds : {},
|
|
161
|
+
).reduce((a, b) => (typeof b === "number" ? a + b : a), 0);
|
|
162
|
+
if (
|
|
163
|
+
data.truncated === true ||
|
|
164
|
+
(typeof data.tornCount === "number" && data.tornCount > 0) ||
|
|
165
|
+
otherCount > 0
|
|
166
|
+
) {
|
|
167
|
+
root.appendChild(
|
|
168
|
+
el("div", { class: "transcript-meta" }, [
|
|
169
|
+
data.truncated === true
|
|
170
|
+
? el("span", { class: "chip chip-warn", text: "truncated — long log capped" })
|
|
171
|
+
: null,
|
|
172
|
+
typeof data.tornCount === "number" && data.tornCount > 0
|
|
173
|
+
? el("span", {
|
|
174
|
+
class: "chip",
|
|
175
|
+
text: `${data.tornCount} torn line${data.tornCount === 1 ? "" : "s"} skipped`,
|
|
176
|
+
})
|
|
177
|
+
: null,
|
|
178
|
+
otherCount > 0
|
|
179
|
+
? el("span", {
|
|
180
|
+
class: "chip",
|
|
181
|
+
text: `${otherCount} other event${otherCount === 1 ? "" : "s"} (Raw)`,
|
|
182
|
+
})
|
|
183
|
+
: null,
|
|
184
|
+
]),
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const feed = el("div", { class: "transcript" });
|
|
189
|
+
for (const entry of entries) feed.appendChild(entryNode(entry));
|
|
190
|
+
root.appendChild(feed);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
function summaryCard(summary) {
|
|
194
|
+
return el("div", { class: "card" }, [
|
|
195
|
+
el("h3", { class: "card-title" }, [
|
|
196
|
+
el("span", { text: "Summarized (transcript expired)" }),
|
|
197
|
+
dot("off", "summary only"),
|
|
198
|
+
]),
|
|
199
|
+
el("p", { text: summary.outcome ? String(summary.outcome) : "No outcome recorded." }),
|
|
200
|
+
Array.isArray(summary.keyFacts) && summary.keyFacts.length > 0
|
|
201
|
+
? el(
|
|
202
|
+
"ul",
|
|
203
|
+
null,
|
|
204
|
+
summary.keyFacts.map((f) => el("li", { text: String(f) })),
|
|
205
|
+
)
|
|
206
|
+
: null,
|
|
207
|
+
]);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/** One interleaved entry (see `interleaveTranscript`) → its DOM node. */
|
|
211
|
+
function entryNode(entry) {
|
|
212
|
+
if (entry.type === "turn") {
|
|
213
|
+
const role = entry.role === "user" ? "user" : "assistant";
|
|
214
|
+
return el("div", { class: `bubble bubble-${role === "user" ? "user" : "agent"}` }, [
|
|
215
|
+
el("div", { class: "bubble-role", text: role }),
|
|
216
|
+
el("div", { class: "bubble-body", text: String(entry.text ?? "") }),
|
|
217
|
+
]);
|
|
218
|
+
}
|
|
219
|
+
if (entry.type === "tool") {
|
|
220
|
+
const name = String(entry.name ?? "tool");
|
|
221
|
+
const isError = entry.isError === true;
|
|
222
|
+
return collapsible(
|
|
223
|
+
[
|
|
224
|
+
el("span", { class: "chip chip-tool", text: name }),
|
|
225
|
+
isError ? dot("bad", "error") : dot("ok", "ok"),
|
|
226
|
+
el("span", { class: "muted tool-summary", text: clampText(jsonText(entry.input), 100) }),
|
|
227
|
+
],
|
|
228
|
+
[jsonPre({ input: entry.input, result: entry.result ?? null })],
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
// gutter
|
|
232
|
+
const kind = String(entry.kind ?? "event");
|
|
233
|
+
return el("div", { class: "gutter" }, [
|
|
234
|
+
el("span", { class: "chip", text: kind }),
|
|
235
|
+
gutterDetail(kind, entry.payload),
|
|
236
|
+
]);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function jsonText(value) {
|
|
240
|
+
if (value === null || value === undefined) return "";
|
|
241
|
+
if (typeof value === "string") return value;
|
|
242
|
+
try {
|
|
243
|
+
return JSON.stringify(value);
|
|
244
|
+
} catch {
|
|
245
|
+
return String(value);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function gutterDetail(kind, payload) {
|
|
250
|
+
const p = payload && typeof payload === "object" ? payload : {};
|
|
251
|
+
if (kind === "cost_accrual") {
|
|
252
|
+
return el("span", { class: "muted", text: fmtUsd(usdFromMicros(p.costUsdMicros)) });
|
|
253
|
+
}
|
|
254
|
+
if (kind === "model_route") {
|
|
255
|
+
return el("span", {
|
|
256
|
+
class: "mono muted",
|
|
257
|
+
text: String(p.modelId ?? p.model ?? p.route ?? p.band ?? ""),
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
if (kind === "permission") {
|
|
261
|
+
const outcome = String(p.outcome ?? p.decision ?? "?");
|
|
262
|
+
return dot(outcome === "deny" ? "bad" : outcome === "ask" ? "warn" : "ok", outcome);
|
|
263
|
+
}
|
|
264
|
+
return el("span", { class: "muted", text: clampText(jsonText(payload) || "", 60) });
|
|
265
|
+
}
|