@inerrata-corporation/errata 2.0.19 → 2.0.20-dev.3682

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.
@@ -0,0 +1,521 @@
1
+ // src/server.ts
2
+ import { createServer } from "node:http";
3
+ import { randomBytes } from "node:crypto";
4
+ import { readFile as readFile2, mkdir, writeFile } from "node:fs/promises";
5
+ import { join as join2, resolve as resolve2 } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { parseArgs } from "node:util";
8
+
9
+ // src/source.ts
10
+ import { createHash } from "node:crypto";
11
+ import { readFile, readdir, stat as stat2 } from "node:fs/promises";
12
+ import { basename, join, resolve } from "node:path";
13
+ import { homedir } from "node:os";
14
+
15
+ // src/context.ts
16
+ function parsePriors(body, handles) {
17
+ const nodes = /* @__PURE__ */ new Map();
18
+ let section = "";
19
+ let fenced = false;
20
+ for (const line of body.split(/\r?\n/)) {
21
+ if (/^\s*```/.test(line)) {
22
+ fenced = !fenced;
23
+ continue;
24
+ }
25
+ if (fenced) continue;
26
+ if (/^###? /.test(line)) {
27
+ section = line.replace(/^#+\s*/, "");
28
+ continue;
29
+ }
30
+ if (!/^(Active workspace profile|Recently observed problems|Prior knowledge|From the Errata Network|Priors for|Current intent|Relevant priors)/i.test(
31
+ section
32
+ ))
33
+ continue;
34
+ const references = [...line.matchAll(/`\[([\w@:/.-]+)\]`/g)];
35
+ for (const match of references) {
36
+ const handle = match[1];
37
+ const mapped = handles[handle];
38
+ const prefix = line.slice(0, match.index);
39
+ const explicitId = /`([^`\s]+)`\s*$/.exec(prefix)?.[1];
40
+ const id = explicitId ?? mapped?.id ?? handle;
41
+ const label = /^- \*\*([A-Za-z]+):\*\*/.exec(line)?.[1] ?? mapped?.label ?? (/observed problems/i.test(section) ? "Problem" : "Prior");
42
+ let text = prefix.replace(/^\s*-\s*/, "").replace(/^\*\*[A-Za-z]+:\*\*\s*/, "").replace(/\s*[—–]\s*`[^`]+`\s*$/, "").replace(/\*\*/g, "").trim();
43
+ if (/Active workspace profile/.test(section)) {
44
+ text = prefix.split(/,\s*/).at(-1).replace(/^.*?:\s*/, "").replace(/`|\*\*/g, "").trim();
45
+ }
46
+ nodes.set(id, {
47
+ id,
48
+ label,
49
+ handle,
50
+ text: text || handle,
51
+ role: "prior",
52
+ provenance: /Errata Network/.test(section) ? "collective" : "local"
53
+ });
54
+ }
55
+ }
56
+ return [...nodes.values()].slice(0, 100);
57
+ }
58
+
59
+ // src/graph.ts
60
+ import { DatabaseSync } from "node:sqlite";
61
+
62
+ // src/types.ts
63
+ function record(value) {
64
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
65
+ }
66
+
67
+ // src/graph.ts
68
+ var SEMANTIC_EDGES = [
69
+ "CAUSED_BY",
70
+ "FIXED_BY",
71
+ "SOLVED_BY",
72
+ "INSTANCE_OF",
73
+ "GENERALIZES",
74
+ "CONTRADICTS",
75
+ "SUPPORTS",
76
+ "CONFIRMS",
77
+ "REFUTES",
78
+ "ANCHORED_AT",
79
+ "CONCERNS"
80
+ ];
81
+ var PreviewGraph = class {
82
+ db;
83
+ constructor(path) {
84
+ this.db = new DatabaseSync(path, { readOnly: true });
85
+ this.db.exec("PRAGMA query_only = ON; PRAGMA busy_timeout = 250;");
86
+ }
87
+ close() {
88
+ this.db.close();
89
+ }
90
+ slice(priors, bursts) {
91
+ const nodes = /* @__PURE__ */ new Map();
92
+ const edges = /* @__PURE__ */ new Map();
93
+ const getNode = this.db.prepare(
94
+ "SELECT id, label, description, extraction_confidence, attrs_json FROM nodes WHERE id = ? AND valid_to IS NULL"
95
+ );
96
+ const enrich = (n) => {
97
+ const row = getNode.get(n.id);
98
+ if (!row) return n;
99
+ let attrs = {};
100
+ try {
101
+ attrs = record(JSON.parse(String(row.attrs_json)));
102
+ } catch {
103
+ }
104
+ return {
105
+ ...n,
106
+ label: String(row.label),
107
+ // Primed prose is the text the agent actually received; keep it intact.
108
+ text: n.role === "prior" ? n.text : String(row.description),
109
+ confidence: Number(row.extraction_confidence),
110
+ ...typeof attrs.relPath === "string" ? { path: attrs.relPath } : {}
111
+ };
112
+ };
113
+ for (const n of priors) nodes.set(n.id, enrich(n));
114
+ for (const burst of [...bursts].reverse()) {
115
+ for (const n of burst.nodes)
116
+ if (!nodes.has(n.id) && nodes.size < 220) nodes.set(n.id, enrich(n));
117
+ for (const e of burst.edges) edges.set(`${e.source}|${e.type}|${e.target}`, e);
118
+ }
119
+ const seedIds = [...nodes.keys()];
120
+ const neighbors = (direction, index) => this.db.prepare(`
121
+ SELECT e.from_id, e.to_id, e.type FROM edges e
122
+ INDEXED BY ${index}
123
+ WHERE e.valid_to IS NULL AND e.type IN (${SEMANTIC_EDGES.map(() => "?").join(",")})
124
+ AND e.${direction} = ?
125
+ ORDER BY e.confidence DESC, e.id LIMIT 5`);
126
+ const outgoingNeighbors = neighbors("from_id", "edges_from");
127
+ const incomingNeighbors = neighbors("to_id", "edges_to");
128
+ let added = 0;
129
+ for (const id of priors.map((n) => n.id)) {
130
+ for (const e of [
131
+ ...outgoingNeighbors.all(...SEMANTIC_EDGES, id),
132
+ ...incomingNeighbors.all(...SEMANTIC_EDGES, id)
133
+ ]) {
134
+ const other = String(e.from_id === id ? e.to_id : e.from_id);
135
+ if (!nodes.has(other) && added < 36 && nodes.size < 256) {
136
+ const row = getNode.get(other);
137
+ if (row) {
138
+ nodes.set(
139
+ other,
140
+ enrich({
141
+ id: other,
142
+ label: String(row.label),
143
+ text: String(row.description),
144
+ provenance: "local",
145
+ role: "neighbor"
146
+ })
147
+ );
148
+ added++;
149
+ }
150
+ }
151
+ }
152
+ }
153
+ const outgoing = this.db.prepare(
154
+ "SELECT from_id, to_id, type FROM edges INDEXED BY edges_from WHERE from_id = ? AND valid_to IS NULL LIMIT 300"
155
+ );
156
+ for (const id of [.../* @__PURE__ */ new Set([...seedIds, ...nodes.keys()])]) {
157
+ for (const e of outgoing.all(id)) {
158
+ const edge = { source: String(e.from_id), target: String(e.to_id), type: String(e.type) };
159
+ if (nodes.has(edge.target)) edges.set(`${edge.source}|${edge.type}|${edge.target}`, edge);
160
+ }
161
+ }
162
+ return {
163
+ nodes: [...nodes.values()],
164
+ edges: [...edges.values()].filter((e) => nodes.has(e.source) && nodes.has(e.target)).slice(0, 600)
165
+ };
166
+ }
167
+ };
168
+ function snapshotOnly(priors, bursts) {
169
+ const nodes = new Map(priors.map((n) => [n.id, n]));
170
+ for (const burst of [...bursts].reverse())
171
+ for (const n of burst.nodes) {
172
+ if (!nodes.has(n.id) && nodes.size < 220) nodes.set(n.id, n);
173
+ }
174
+ const edges = /* @__PURE__ */ new Map();
175
+ for (const burst of bursts)
176
+ for (const e of burst.edges) {
177
+ if (nodes.has(e.source) && nodes.has(e.target))
178
+ edges.set(`${e.source}|${e.type}|${e.target}`, e);
179
+ }
180
+ return { nodes: [...nodes.values()], edges: [...edges.values()] };
181
+ }
182
+
183
+ // src/transcript.ts
184
+ import { open, stat } from "node:fs/promises";
185
+ import { StringDecoder } from "node:string_decoder";
186
+ function textContent(value) {
187
+ if (typeof value === "string") return value;
188
+ if (!Array.isArray(value)) return "";
189
+ return value.map((v) => typeof record(v).text === "string" ? record(v).text : "").join("\n");
190
+ }
191
+ function parseBurst(value, id, at) {
192
+ const result = record(value);
193
+ if (result.isError === true) return null;
194
+ let data = record(result.structuredContent);
195
+ if (!Array.isArray(data.results)) {
196
+ try {
197
+ data = record(JSON.parse(textContent(result.content ?? value)));
198
+ } catch {
199
+ data = result;
200
+ }
201
+ }
202
+ if (!Array.isArray(data.results) || typeof data.seed !== "string") return null;
203
+ const nodes = [];
204
+ const edges = [];
205
+ for (const raw of data.results.slice(0, 100)) {
206
+ const n = record(raw);
207
+ if (typeof n.id !== "string") continue;
208
+ const via = record(n.via);
209
+ const traversal = typeof via.parentId === "string" && typeof via.edgeType === "string" && typeof via.forward === "boolean" ? { parentId: via.parentId, edgeType: via.edgeType, forward: via.forward } : void 0;
210
+ nodes.push({
211
+ id: n.id,
212
+ label: typeof n.label === "string" ? n.label : "Prior",
213
+ text: typeof n.name === "string" ? n.name : n.id,
214
+ provenance: typeof n.provenance === "string" ? n.provenance : "unknown",
215
+ role: "burst",
216
+ ...typeof n.score === "number" && Number.isFinite(n.score) ? { score: n.score } : {},
217
+ ...typeof n.hops === "number" && Number.isInteger(n.hops) && n.hops >= 0 ? { hops: n.hops } : {},
218
+ ...traversal ? { via: traversal } : {}
219
+ });
220
+ if (traversal) {
221
+ edges.push({
222
+ source: traversal.forward ? traversal.parentId : n.id,
223
+ target: traversal.forward ? n.id : traversal.parentId,
224
+ type: traversal.edgeType
225
+ });
226
+ }
227
+ }
228
+ if (!nodes.length) return null;
229
+ for (const raw of Array.isArray(data.edges) ? data.edges.slice(0, 300) : []) {
230
+ const e = record(raw);
231
+ const source2 = e.from ?? e.source;
232
+ const target = e.to ?? e.target;
233
+ const type = e.type ?? e.edgeType;
234
+ if (typeof source2 === "string" && typeof target === "string" && typeof type === "string")
235
+ edges.push({ source: source2, target, type });
236
+ }
237
+ const seed = nodes.find((n) => n.id === data.seed);
238
+ if (!seed)
239
+ nodes.unshift({
240
+ id: data.seed,
241
+ text: data.seed,
242
+ label: "Seed",
243
+ role: "burst",
244
+ provenance: typeof data.seedProvenance === "string" ? data.seedProvenance : "unknown"
245
+ });
246
+ return { id, at, seed: data.seed, title: seed?.text ?? data.seed, nodes, edges };
247
+ }
248
+ var TranscriptState = class {
249
+ context = null;
250
+ bursts = [];
251
+ seen = /* @__PURE__ */ new Set();
252
+ calls = /* @__PURE__ */ new Set();
253
+ accept(value) {
254
+ const row = record(value);
255
+ const p = record(row.payload);
256
+ const at = typeof row.timestamp === "string" ? row.timestamp : "";
257
+ if (row.type === "response_item" && p.type === "message" && p.role === "developer") {
258
+ const body = textContent(p.content);
259
+ if (body.includes("## Errata context") && /Errata loaded the maintained|errata daemon has updated your live/i.test(body)) {
260
+ this.context = { body: body.slice(body.indexOf("## Errata context")), at, source: "task" };
261
+ }
262
+ }
263
+ if (row.type === "event_msg" && p.type === "item_completed") {
264
+ const item = record(p.item);
265
+ if (item.type === "McpToolCall" && item.server === "errata" && /^errata[._]burst$/.test(String(item.tool)) && item.status === "completed") {
266
+ this.addBurst(item.result, String(item.id), at);
267
+ }
268
+ }
269
+ if (row.type !== "response_item") return;
270
+ if (p.type === "function_call" && /^(?:mcp__errata__)?errata[._]burst$/.test(String(p.name))) {
271
+ this.calls.add(String(p.call_id));
272
+ if (this.calls.size > 128) this.calls.delete(this.calls.values().next().value);
273
+ }
274
+ if (p.type === "function_call_output" && this.calls.delete(String(p.call_id))) {
275
+ this.addBurst(p.output, String(p.call_id), at);
276
+ }
277
+ }
278
+ addBurst(value, id, at) {
279
+ if (this.seen.has(id)) return;
280
+ const burst = parseBurst(value, id, at);
281
+ if (!burst) return;
282
+ this.seen.add(id);
283
+ if (this.seen.size > 256) this.seen.delete(this.seen.values().next().value);
284
+ this.bursts = [...this.bursts, burst].slice(-6);
285
+ }
286
+ };
287
+ var TranscriptTail = class {
288
+ constructor(path) {
289
+ this.path = path;
290
+ }
291
+ path;
292
+ state = new TranscriptState();
293
+ offset = 0;
294
+ pending = "";
295
+ decoder = new StringDecoder("utf8");
296
+ inode = null;
297
+ async update() {
298
+ const info = await stat(this.path);
299
+ if (info.size < this.offset || this.inode !== null && info.ino !== this.inode) {
300
+ this.offset = 0;
301
+ this.pending = "";
302
+ this.decoder = new StringDecoder("utf8");
303
+ this.state = new TranscriptState();
304
+ }
305
+ this.inode = info.ino;
306
+ if (info.size === this.offset) return;
307
+ const file = await open(this.path, "r");
308
+ try {
309
+ const buffer = Buffer.alloc(Math.min(info.size - this.offset, 16 * 1024 * 1024));
310
+ const { bytesRead } = await file.read(buffer, 0, buffer.length, this.offset);
311
+ this.offset += bytesRead;
312
+ const lines = (this.pending + this.decoder.write(buffer.subarray(0, bytesRead))).split("\n");
313
+ this.pending = lines.pop() ?? "";
314
+ for (const line of lines) {
315
+ try {
316
+ this.state.accept(JSON.parse(line));
317
+ } catch {
318
+ }
319
+ }
320
+ if (this.pending.length > 4 * 1024 * 1024) this.pending = "";
321
+ } finally {
322
+ await file.close();
323
+ }
324
+ }
325
+ };
326
+
327
+ // src/source.ts
328
+ function createSource(workspace, sessionId2, transcript) {
329
+ const root2 = resolve(workspace);
330
+ if (sessionId2 && !/^[\w-]{1,100}$/.test(sessionId2)) throw new Error("Invalid task id");
331
+ let transcriptPath = transcript ? resolve(transcript) : null;
332
+ let tail = null;
333
+ let graph = null;
334
+ let nextDiscovery = 0;
335
+ let updating = null;
336
+ async function findTranscript(dir) {
337
+ let entries;
338
+ try {
339
+ entries = await readdir(dir, { withFileTypes: true });
340
+ } catch {
341
+ return null;
342
+ }
343
+ for (const e of entries.sort((a, b) => b.name.localeCompare(a.name))) {
344
+ if (e.isFile() && e.name.endsWith(`-${sessionId2}.jsonl`)) return join(dir, e.name);
345
+ if (e.isDirectory()) {
346
+ const found = await findTranscript(join(dir, e.name));
347
+ if (found) return found;
348
+ }
349
+ }
350
+ return null;
351
+ }
352
+ async function refresh() {
353
+ const warnings = [];
354
+ if (!transcriptPath && sessionId2 && Date.now() > nextDiscovery) {
355
+ nextDiscovery = Date.now() + 15e3;
356
+ transcriptPath = await findTranscript(
357
+ join(process.env.CODEX_HOME ?? join(homedir(), ".codex"), "sessions")
358
+ );
359
+ }
360
+ if (transcriptPath) {
361
+ tail ??= new TranscriptTail(transcriptPath);
362
+ try {
363
+ await tail.update();
364
+ } catch {
365
+ warnings.push("Task transcript is temporarily unavailable.");
366
+ }
367
+ }
368
+ let context = tail?.state.context ?? null;
369
+ if (!context) {
370
+ try {
371
+ const path = join(root2, ".errata", "context.md");
372
+ context = {
373
+ body: await readFile(path, "utf8"),
374
+ at: (await stat2(path)).mtime.toISOString(),
375
+ source: "workspace"
376
+ };
377
+ } catch {
378
+ }
379
+ }
380
+ let handles = {};
381
+ try {
382
+ const raw = record(
383
+ JSON.parse(await readFile(join(root2, ".errata", "priming-handles.json"), "utf8"))
384
+ );
385
+ for (const [key, value] of Object.entries(raw)) {
386
+ const entry = record(value);
387
+ if (typeof entry.id === "string" && typeof entry.label === "string")
388
+ handles[key] = { id: entry.id, label: entry.label };
389
+ }
390
+ } catch {
391
+ }
392
+ const priors = parsePriors(context?.body ?? "", handles);
393
+ const bursts = tail?.state.bursts ?? [];
394
+ let slice = snapshotOnly(priors, bursts);
395
+ try {
396
+ graph ??= new PreviewGraph(join(root2, ".errata", "castalia.db"));
397
+ slice = graph.slice(priors, bursts);
398
+ } catch {
399
+ warnings.push("Local graph unavailable; showing recorded priors and burst results.");
400
+ }
401
+ if (sessionId2 && !transcriptPath)
402
+ warnings.push("Waiting for this task\u2019s transcript; showing the workspace snapshot.");
403
+ const byId = new Map(slice.nodes.map((n) => [n.id, n]));
404
+ const displayBursts = bursts.map((b) => ({ ...b, title: byId.get(b.seed)?.text ?? b.title }));
405
+ const content = {
406
+ workspace: basename(root2),
407
+ sessionId: sessionId2,
408
+ context: {
409
+ at: context?.at ?? "",
410
+ source: context?.source ?? "waiting",
411
+ priorIds: priors.map((n) => n.id)
412
+ },
413
+ ...slice,
414
+ bursts: displayBursts,
415
+ warnings
416
+ };
417
+ return {
418
+ revision: createHash("sha256").update(JSON.stringify(content)).digest("hex").slice(0, 16),
419
+ ...content
420
+ };
421
+ }
422
+ return {
423
+ read() {
424
+ updating ??= refresh().finally(() => {
425
+ updating = null;
426
+ });
427
+ return updating;
428
+ },
429
+ close() {
430
+ graph?.close();
431
+ }
432
+ };
433
+ }
434
+
435
+ // src/server.ts
436
+ var { values } = parseArgs({
437
+ options: {
438
+ root: { type: "string" },
439
+ session: { type: "string" },
440
+ transcript: { type: "string" },
441
+ port: { type: "string", default: "0" }
442
+ }
443
+ });
444
+ var root = resolve2(values.root ?? process.cwd());
445
+ var sessionId = values.session ?? process.env.CODEX_THREAD_ID ?? null;
446
+ if (sessionId && !/^[\w-]{1,100}$/.test(sessionId)) throw new Error("Invalid task id");
447
+ var port = Number(values.port);
448
+ if (!Number.isInteger(port) || port < 0 || port > 65535) throw new Error("Invalid port");
449
+ var dist = fileURLToPath(new URL(".", import.meta.url));
450
+ var token = randomBytes(24).toString("hex");
451
+ var source = createSource(root, sessionId, values.transcript);
452
+ await source.read();
453
+ var server = createServer(async (req, res) => {
454
+ const address = server.address();
455
+ const authority = `127.0.0.1:${typeof address === "object" && address ? address.port : port}`;
456
+ if (req.headers.host !== authority || req.headers.origin && req.headers.origin !== `http://${authority}`) {
457
+ res.writeHead(403).end();
458
+ return;
459
+ }
460
+ if (req.method !== "GET") {
461
+ res.writeHead(405).end();
462
+ return;
463
+ }
464
+ const url = new URL(req.url ?? "/", `http://${authority}`);
465
+ const prefix = `/${token}/`;
466
+ if (!url.pathname.startsWith(prefix)) {
467
+ res.writeHead(404).end();
468
+ return;
469
+ }
470
+ const name = url.pathname.slice(prefix.length);
471
+ res.setHeader("Cache-Control", "no-store");
472
+ res.setHeader("X-Content-Type-Options", "nosniff");
473
+ res.setHeader("Referrer-Policy", "no-referrer");
474
+ res.setHeader(
475
+ "Content-Security-Policy",
476
+ "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; connect-src 'self'; object-src 'none'; frame-ancestors 'none'"
477
+ );
478
+ try {
479
+ if (name === "api/state") {
480
+ const state = await source.read();
481
+ res.setHeader("Content-Type", "application/json");
482
+ res.end(JSON.stringify(state));
483
+ return;
484
+ }
485
+ if (name === "health") {
486
+ res.setHeader("Content-Type", "application/json");
487
+ res.end(JSON.stringify({ ok: true, root, sessionId, pid: process.pid }));
488
+ return;
489
+ }
490
+ const asset = name === "" ? "index.html" : name;
491
+ const mime = {
492
+ "index.html": "text/html; charset=utf-8",
493
+ "client.js": "text/javascript",
494
+ "style.css": "text/css"
495
+ };
496
+ if (!mime[asset]) {
497
+ res.writeHead(404).end();
498
+ return;
499
+ }
500
+ res.setHeader("Content-Type", mime[asset]);
501
+ res.end(await readFile2(join2(dist, asset)));
502
+ } catch (error) {
503
+ console.error(error instanceof Error ? error.message : "Preview read failed");
504
+ res.writeHead(503).end("Preview temporarily unavailable");
505
+ }
506
+ });
507
+ server.listen(port, "127.0.0.1", async () => {
508
+ const address = server.address();
509
+ if (!address || typeof address === "string") return;
510
+ const url = `http://127.0.0.1:${address.port}/${token}/`;
511
+ const info = { url, root, sessionId, pid: process.pid };
512
+ const output = join2(root, ".errata", "context-preview");
513
+ await mkdir(output, { recursive: true });
514
+ await writeFile(join2(output, `${sessionId ?? "workspace"}.json`), JSON.stringify(info, null, 2));
515
+ console.log(JSON.stringify(info));
516
+ });
517
+ for (const signal of ["SIGINT", "SIGTERM"])
518
+ process.on(signal, () => {
519
+ source.close();
520
+ server.close(() => process.exit(0));
521
+ });
@@ -0,0 +1,189 @@
1
+ :root {
2
+ color-scheme: dark;
3
+ --surface: #181818;
4
+ --ink: #deddda;
5
+ --muted: #888784;
6
+ --causal-ink: #f2c788;
7
+ font: 11px/1.45
8
+ var(--font-sans, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif);
9
+ background: var(--color-background-primary, var(--surface));
10
+ color: var(--color-text-primary, var(--ink));
11
+ }
12
+ * {
13
+ box-sizing: border-box;
14
+ }
15
+ html,
16
+ body {
17
+ margin: 0;
18
+ width: 100%;
19
+ height: 100%;
20
+ overflow: hidden;
21
+ }
22
+ body {
23
+ min-height: 240px;
24
+ }
25
+ [hidden] {
26
+ display: none !important;
27
+ }
28
+ #viewport {
29
+ position: absolute;
30
+ inset: 0;
31
+ outline: none;
32
+ touch-action: none;
33
+ }
34
+ canvas {
35
+ display: block;
36
+ width: 100%;
37
+ height: 100%;
38
+ }
39
+ #viewport:focus-visible {
40
+ outline: 1px solid var(--muted);
41
+ outline-offset: -2px;
42
+ }
43
+ #empty {
44
+ position: absolute;
45
+ inset: 0;
46
+ display: grid;
47
+ place-items: center;
48
+ color: var(--color-text-secondary, var(--muted));
49
+ pointer-events: none;
50
+ }
51
+ #floating-labels {
52
+ position: absolute;
53
+ inset: 0;
54
+ pointer-events: none;
55
+ overflow: hidden;
56
+ }
57
+ .floating-label {
58
+ position: absolute;
59
+ left: 0;
60
+ top: 0;
61
+ height: 16px;
62
+ line-height: 16px;
63
+ font-size: 10px;
64
+ letter-spacing: 0.01em;
65
+ white-space: nowrap;
66
+ text-overflow: ellipsis;
67
+ overflow: hidden;
68
+ opacity: 0;
69
+ color: var(--color-text-primary, var(--ink));
70
+ text-shadow: 0 1px 4px var(--color-background-primary, var(--surface));
71
+ transition: opacity 180ms ease;
72
+ will-change: transform;
73
+ }
74
+ .floating-label.focused {
75
+ font-weight: 500;
76
+ }
77
+ .floating-label[data-causal='true'] {
78
+ color: var(--causal-ink);
79
+ font-weight: 600;
80
+ }
81
+ @media (prefers-reduced-motion: reduce) {
82
+ .floating-label {
83
+ transition: none;
84
+ }
85
+ }
86
+ #node-label {
87
+ position: absolute;
88
+ left: 0;
89
+ top: 0;
90
+ width: max-content;
91
+ max-width: min(270px, calc(100vw - 24px));
92
+ padding: 8px 10px;
93
+ border-radius: 7px;
94
+ background: color-mix(in srgb, var(--color-background-primary, var(--surface)) 93%, transparent);
95
+ border: 1px solid #8882;
96
+ font-size: 11px;
97
+ line-height: 1.5;
98
+ pointer-events: none;
99
+ overflow-wrap: anywhere;
100
+ white-space: pre-line;
101
+ box-shadow: 0 4px 22px #0002;
102
+ }
103
+ #activity {
104
+ position: absolute;
105
+ bottom: 14px;
106
+ left: 16px;
107
+ right: 16px;
108
+ max-width: 430px;
109
+ pointer-events: none;
110
+ color: var(--color-text-secondary, var(--muted));
111
+ font-size: 10px;
112
+ line-height: 1.6;
113
+ }
114
+ #log {
115
+ display: flex;
116
+ flex-direction: column;
117
+ gap: 2px;
118
+ }
119
+ #log button {
120
+ appearance: none;
121
+ display: flex;
122
+ align-items: baseline;
123
+ gap: 8px;
124
+ width: 100%;
125
+ border: 0;
126
+ background: none;
127
+ padding: 0;
128
+ text-align: left;
129
+ font: inherit;
130
+ color: inherit;
131
+ cursor: pointer;
132
+ pointer-events: auto;
133
+ }
134
+ #log button:hover,
135
+ #log button:focus-visible {
136
+ color: var(--color-text-primary, var(--ink));
137
+ }
138
+ #log time {
139
+ font-variant-numeric: tabular-nums;
140
+ opacity: 0.6;
141
+ flex: none;
142
+ }
143
+ #log span {
144
+ white-space: nowrap;
145
+ overflow: hidden;
146
+ text-overflow: ellipsis;
147
+ }
148
+ #log button:first-child {
149
+ opacity: 0.5;
150
+ }
151
+ #log button:last-child {
152
+ opacity: 1;
153
+ }
154
+ #log button.active span::before {
155
+ content: '·';
156
+ margin-right: 6px;
157
+ color: #c8a84e;
158
+ }
159
+ #notice {
160
+ margin-bottom: 5px;
161
+ max-width: 38ch;
162
+ color: var(--color-text-secondary, var(--muted));
163
+ }
164
+ #notice:empty {
165
+ display: none;
166
+ }
167
+ @media (prefers-color-scheme: light) {
168
+ :root {
169
+ color-scheme: light;
170
+ --surface: #faf9f7;
171
+ --ink: #292825;
172
+ --muted: #777570;
173
+ --causal-ink: #835313;
174
+ }
175
+ }
176
+ :root[data-theme='dark'] {
177
+ color-scheme: dark;
178
+ --surface: #181818;
179
+ --ink: #deddda;
180
+ --muted: #888784;
181
+ --causal-ink: #f2c788;
182
+ }
183
+ :root[data-theme='light'] {
184
+ color-scheme: light;
185
+ --surface: #faf9f7;
186
+ --ink: #292825;
187
+ --muted: #777570;
188
+ --causal-ink: #835313;
189
+ }