omkit 0.8.11 → 0.8.12
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
CHANGED
|
@@ -189,7 +189,7 @@ Every action's output, events, asserts, snapshots, and lifecycle land on **one t
|
|
|
189
189
|
- `raw.jsonl` — every entry, machine-readable.
|
|
190
190
|
- `main.log` + one `.log` per action — the human-readable timelines.
|
|
191
191
|
- `events.log` / `asserts.log` / `snapshots.log` / `artifacts.log` — cross-cutting rollups.
|
|
192
|
-
- `report.html` — the whole run as one self-contained page: a timeline of every action with its events, a flame graph, a flow graph of the tree, per-action details, logs, assertions,
|
|
192
|
+
- `report.html` — the whole run as one self-contained page: a timeline of every action with its events, a flame graph, a flow graph of the tree, per-action details, logs, assertions, the run's snapshots and images, and each action's source (the `<>` button next to a block). Everything is inlined (data, base64 images, d3), so it opens straight from disk and survives being zipped or attached.
|
|
193
193
|
|
|
194
194
|
`ctx` also gives each action `assert(cond, msg)` (tallies into the run's verdict), `snapshot(name, value)` (captures JSON state to the run folder), and `artifact(name, file, opts?)` (labels a file the action wrote — name, optional description, MIME inferred from the extension — and returns its absolute path) — so a run is an inspectable artifact, not just an exit code. Write the files you label under `ctx.artifactsFolder`, the run's own output folder.
|
|
195
195
|
|
|
@@ -40,6 +40,7 @@ export async function writeReport(file, input) {
|
|
|
40
40
|
folder: input.folder,
|
|
41
41
|
projectRoot: root,
|
|
42
42
|
sources: definitionSources(input.run.root, root),
|
|
43
|
+
code: await definitionCode(input.run.root),
|
|
43
44
|
generatedAt: Date.now(),
|
|
44
45
|
run: input.run,
|
|
45
46
|
entries: input.entries,
|
|
@@ -58,29 +59,29 @@ function renderPage(input, payload, assets, code) {
|
|
|
58
59
|
const blobs = assets
|
|
59
60
|
.map((a) => `<script type="text/plain" id="${a.id}" data-mime="${a.mime}">${a.data}</script>`)
|
|
60
61
|
.join("\n");
|
|
61
|
-
return `<!doctype html>
|
|
62
|
-
<html lang="en">
|
|
63
|
-
<head>
|
|
64
|
-
<meta charset="utf-8">
|
|
65
|
-
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
66
|
-
<title>${escapeHtml(title)}</title>
|
|
67
|
-
<style>${code.css}</style>
|
|
68
|
-
</head>
|
|
69
|
-
<body>
|
|
70
|
-
<div id="loader" role="status" aria-live="polite">
|
|
71
|
-
<div class="loader-card">
|
|
72
|
-
<div class="loader-spin"></div>
|
|
73
|
-
<div class="loader-title">${escapeHtml(title)}</div>
|
|
74
|
-
<div class="loader-step" id="loader-step">Loading the run record…</div>
|
|
75
|
-
</div>
|
|
76
|
-
</div>
|
|
77
|
-
<div id="app" hidden></div>
|
|
78
|
-
<script id="omkit-data" type="application/json">${inertJson(payload)}</script>
|
|
79
|
-
${blobs}
|
|
80
|
-
<script>${inertScript(code.d3)}</script>
|
|
81
|
-
<script>${inertScript(code.client)}</script>
|
|
82
|
-
</body>
|
|
83
|
-
</html>
|
|
62
|
+
return `<!doctype html>
|
|
63
|
+
<html lang="en">
|
|
64
|
+
<head>
|
|
65
|
+
<meta charset="utf-8">
|
|
66
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
67
|
+
<title>${escapeHtml(title)}</title>
|
|
68
|
+
<style>${code.css}</style>
|
|
69
|
+
</head>
|
|
70
|
+
<body>
|
|
71
|
+
<div id="loader" role="status" aria-live="polite">
|
|
72
|
+
<div class="loader-card">
|
|
73
|
+
<div class="loader-spin"></div>
|
|
74
|
+
<div class="loader-title">${escapeHtml(title)}</div>
|
|
75
|
+
<div class="loader-step" id="loader-step">Loading the run record…</div>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
<div id="app" hidden></div>
|
|
79
|
+
<script id="omkit-data" type="application/json">${inertJson(payload)}</script>
|
|
80
|
+
${blobs}
|
|
81
|
+
<script>${inertScript(code.d3)}</script>
|
|
82
|
+
<script>${inertScript(code.client)}</script>
|
|
83
|
+
</body>
|
|
84
|
+
</html>
|
|
84
85
|
`;
|
|
85
86
|
}
|
|
86
87
|
async function collectFiles(input, assets) {
|
|
@@ -120,7 +121,11 @@ async function collectFiles(input, assets) {
|
|
|
120
121
|
out.push({ ...base, size, text: await readFile(abs, "utf8") });
|
|
121
122
|
}
|
|
122
123
|
else {
|
|
123
|
-
out.push({
|
|
124
|
+
out.push({
|
|
125
|
+
...base,
|
|
126
|
+
size,
|
|
127
|
+
omitted: isText(meta.mime) ? `larger than ${mb(TEXT_CAP)}` : "binary",
|
|
128
|
+
});
|
|
124
129
|
}
|
|
125
130
|
};
|
|
126
131
|
for (const a of input.artifacts) {
|
|
@@ -153,7 +158,14 @@ async function collectSnapshots(input) {
|
|
|
153
158
|
continue;
|
|
154
159
|
const name = e.message.slice(0, split);
|
|
155
160
|
const abs = e.message.slice(split + 3);
|
|
156
|
-
const base = {
|
|
161
|
+
const base = {
|
|
162
|
+
seq: ++seq,
|
|
163
|
+
name,
|
|
164
|
+
nodeId: e.nodeId,
|
|
165
|
+
ts: e.ts,
|
|
166
|
+
file: abs,
|
|
167
|
+
rel: relTo(input.folder, abs),
|
|
168
|
+
};
|
|
157
169
|
try {
|
|
158
170
|
const raw = await readFile(abs, "utf8");
|
|
159
171
|
if (raw.length > SNAPSHOT_CAP) {
|
|
@@ -220,6 +232,66 @@ function definitionSources(root, projectDir) {
|
|
|
220
232
|
visit(root);
|
|
221
233
|
return out;
|
|
222
234
|
}
|
|
235
|
+
/** Longest excerpt shown per definition; a body past this is cut, and says so. */
|
|
236
|
+
const CODE_LINES = 300;
|
|
237
|
+
/**
|
|
238
|
+
* Each distinct `definedAt` site → the source of the definition there: the whole top-level
|
|
239
|
+
* statement holding that line (`export const x = action(…)…run(…)`, or the `om(…).run(…)` call),
|
|
240
|
+
* found with the TypeScript parser so it is the real extent, not a guess at one. A site whose
|
|
241
|
+
* file is gone or unparseable gets a window of lines around it instead, or nothing.
|
|
242
|
+
*/
|
|
243
|
+
async function definitionCode(root) {
|
|
244
|
+
const sites = new Set();
|
|
245
|
+
const visit = (n) => {
|
|
246
|
+
if (n.definedAt)
|
|
247
|
+
sites.add(n.definedAt);
|
|
248
|
+
n.children.forEach(visit);
|
|
249
|
+
};
|
|
250
|
+
visit(root);
|
|
251
|
+
if (sites.size === 0)
|
|
252
|
+
return {};
|
|
253
|
+
// Loaded only here: the parser is heavy, and a run that never writes a report never pays for it.
|
|
254
|
+
const ts = (await import("typescript")).default;
|
|
255
|
+
const out = {};
|
|
256
|
+
const texts = new Map();
|
|
257
|
+
for (const site of sites) {
|
|
258
|
+
const m = /^(.*):(\d+)(?::\d+)?$/.exec(site);
|
|
259
|
+
if (!m)
|
|
260
|
+
continue;
|
|
261
|
+
const file = m[1];
|
|
262
|
+
const line = Number(m[2]);
|
|
263
|
+
if (!texts.has(file))
|
|
264
|
+
texts.set(file, await readFile(file, "utf8").catch(() => null));
|
|
265
|
+
const text = texts.get(file);
|
|
266
|
+
if (!text)
|
|
267
|
+
continue;
|
|
268
|
+
const lang = /\.[cm]?tsx?$/i.test(file) ? "ts" : "js";
|
|
269
|
+
const lines = text.split(/\r?\n/);
|
|
270
|
+
let from = Math.max(1, line - 10);
|
|
271
|
+
let to = Math.min(lines.length, line + 20);
|
|
272
|
+
try {
|
|
273
|
+
const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true);
|
|
274
|
+
const pos = sf.getPositionOfLineAndCharacter(Math.min(line, sf.getLineStarts().length) - 1, 0);
|
|
275
|
+
const stmt = sf.statements.find((s) => s.getStart(sf) <= pos && pos < s.getEnd()) ??
|
|
276
|
+
sf.statements.find((s) => s.getFullStart() <= pos && pos < s.getEnd());
|
|
277
|
+
if (stmt) {
|
|
278
|
+
from = sf.getLineAndCharacterOfPosition(stmt.getStart(sf, true)).line + 1;
|
|
279
|
+
to = sf.getLineAndCharacterOfPosition(stmt.getEnd()).line + 1;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
/* fall back to the window around the line */
|
|
284
|
+
}
|
|
285
|
+
const truncated = to - from + 1 > CODE_LINES;
|
|
286
|
+
out[site] = {
|
|
287
|
+
start: from,
|
|
288
|
+
text: lines.slice(from - 1, truncated ? from - 1 + CODE_LINES : to).join("\n"),
|
|
289
|
+
lang,
|
|
290
|
+
...(truncated ? { truncated } : {}),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
return out;
|
|
294
|
+
}
|
|
223
295
|
async function walk(dir, depth = 0) {
|
|
224
296
|
if (depth > 4)
|
|
225
297
|
return [];
|
|
@@ -292,6 +292,52 @@ table.list tr:hover td { background: var(--raise); }
|
|
|
292
292
|
.menu button:hover { background: var(--raise); }
|
|
293
293
|
.toast { position: fixed; bottom: 18px; left: 50%; transform: translateX(-50%); background: var(--raise); border: 1px solid var(--line); padding: 7px 14px; border-radius: 8px; z-index: 70; }
|
|
294
294
|
|
|
295
|
+
/* ── title info, locate, defined-at, code popover ───────── */
|
|
296
|
+
.title-row { display: flex; align-items: center; gap: 8px; min-width: 0; }
|
|
297
|
+
.title-row h1 { min-width: 0; }
|
|
298
|
+
.info-btn { flex: none; width: 22px; height: 22px; border: 0; border-radius: 50%; background: none; color: var(--text-3); display: grid; place-items: center; cursor: help; padding: 0; }
|
|
299
|
+
.info-btn:hover, .info-btn:focus-visible { color: var(--accent); background: var(--accent-soft); }
|
|
300
|
+
.info-btn svg { width: 17px; height: 17px; }
|
|
301
|
+
.gal-item .cap .locate { margin-left: auto; width: 22px; height: 20px; }
|
|
302
|
+
.kv .v.defined { flex-wrap: wrap; }
|
|
303
|
+
.kv .v.defined .defined-name { color: var(--text-2); font-family: var(--mono); font-size: 12px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
304
|
+
a.icon-btn { text-decoration: none; }
|
|
305
|
+
.chart .code-btn, .flow-card .code-btn { cursor: pointer; opacity: 0; transition: opacity 0.12s; }
|
|
306
|
+
.chart g.r:hover .code-btn, .chart g.r.sel .code-btn, .flow-card:hover .code-btn, .flow-card.selected .code-btn { opacity: 1; }
|
|
307
|
+
.code-btn circle { fill: var(--panel); stroke: var(--text-3); stroke-width: 1.2; }
|
|
308
|
+
.code-btn text { fill: var(--text-2); font: 700 9px var(--mono); pointer-events: none; }
|
|
309
|
+
.code-btn:hover circle { stroke: var(--accent); }
|
|
310
|
+
.code-btn:hover text { fill: var(--accent); }
|
|
311
|
+
.chart line.pin.flash, .flame-view line.pin.flash { animation: pin-flash 0.6s ease-in-out 3; }
|
|
312
|
+
.chart circle.mark.flash { animation: mark-flash 0.6s ease-in-out 3; transform-box: fill-box; transform-origin: center; }
|
|
313
|
+
@keyframes pin-flash { 50% { stroke-width: 5; } }
|
|
314
|
+
@keyframes mark-flash { 50% { transform: scale(1.9); } }
|
|
315
|
+
.code-pop {
|
|
316
|
+
position: fixed; z-index: 58; width: 640px; max-width: 92vw; max-height: 70vh; display: flex; flex-direction: column;
|
|
317
|
+
background: var(--panel); border: 1px solid var(--line); border-radius: 10px; box-shadow: var(--shadow); overflow: hidden;
|
|
318
|
+
}
|
|
319
|
+
.code-pop .cp-head { display: flex; align-items: center; gap: 6px; padding: 6px 8px 6px 12px; background: var(--panel-2); border-bottom: 1px solid var(--line); }
|
|
320
|
+
.code-pop .cp-path { font: 12px var(--mono); color: var(--text-2); min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
321
|
+
.code-pop .cp-name { flex: 1; text-align: right; color: var(--text-3); font-size: 12px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
322
|
+
.code-pop .cp-body { overflow: auto; min-height: 0; }
|
|
323
|
+
.code-pop .cp-code { font: 11.5px/1.65 var(--mono); padding: 8px 0; min-width: max-content; }
|
|
324
|
+
.code-pop .cl { display: grid; grid-template-columns: 48px auto; white-space: pre; }
|
|
325
|
+
.code-pop .cl.at { background: var(--accent-soft); }
|
|
326
|
+
.code-pop .ln { color: var(--text-3); text-align: right; padding-right: 14px; user-select: none; }
|
|
327
|
+
.code-pop .lc { padding-right: 16px; }
|
|
328
|
+
.code-pop .cp-more { padding: 6px 14px 10px; font-size: 12px; }
|
|
329
|
+
:root { --tk-c: #6f7a99; --tk-s: #7ee0a3; --tk-n: #f0b26b; --tk-k: #c792ea; --tk-f: #82aaff; --tk-t: #ffcb6b; }
|
|
330
|
+
:root[data-theme="light"] { --tk-c: #6a737d; --tk-s: #116329; --tk-n: #b35900; --tk-k: #cf222e; --tk-f: #6639ba; --tk-t: #953800; }
|
|
331
|
+
@media (prefers-color-scheme: light) {
|
|
332
|
+
:root:not([data-theme="dark"]) { --tk-c: #6a737d; --tk-s: #116329; --tk-n: #b35900; --tk-k: #cf222e; --tk-f: #6639ba; --tk-t: #953800; }
|
|
333
|
+
}
|
|
334
|
+
.tk-c { color: var(--tk-c); font-style: italic; }
|
|
335
|
+
.tk-s { color: var(--tk-s); }
|
|
336
|
+
.tk-n { color: var(--tk-n); }
|
|
337
|
+
.tk-k { color: var(--tk-k); }
|
|
338
|
+
.tk-f { color: var(--tk-f); }
|
|
339
|
+
.tk-t { color: var(--tk-t); }
|
|
340
|
+
|
|
295
341
|
@media (max-width: 1100px) {
|
|
296
342
|
.bottom { grid-template-columns: minmax(0, 1fr); }
|
|
297
343
|
.legend, .overview { display: none; }
|
|
@@ -40,6 +40,10 @@
|
|
|
40
40
|
download: "M12 4v11M7 10l5 5 5-5M5 20h14",
|
|
41
41
|
copy: "M9 9h11v11H9zM5 15V4h11",
|
|
42
42
|
stop: "M6 6h12v12H6z",
|
|
43
|
+
info: "M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18zM12 11v5M12 8h.01",
|
|
44
|
+
locate: "M12 5a7 7 0 1 0 0 14 7 7 0 0 0 0-14zM12 2v3M12 19v3M2 12h3M19 12h3M12 10.5a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3z",
|
|
45
|
+
code: "M8 7l-5 5 5 5M16 7l5 5-5 5",
|
|
46
|
+
external: "M14 4h6v6M20 4l-9 9M18 14v6H4V6h6",
|
|
43
47
|
};
|
|
44
48
|
const svgIcon = (name, extra) =>
|
|
45
49
|
`<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"${
|
|
@@ -297,6 +301,11 @@
|
|
|
297
301
|
zoomToDomain(Math.max(0, d0), Math.max(0, d0) + ms);
|
|
298
302
|
});
|
|
299
303
|
app.querySelector("#more").addEventListener("click", (ev) => openMenu(ev.currentTarget));
|
|
304
|
+
const info = app.querySelector("#om-info");
|
|
305
|
+
info.addEventListener("mouseenter", (ev) => showTip(ev, omTip(), true));
|
|
306
|
+
info.addEventListener("mousemove", (ev) => moveTip(ev));
|
|
307
|
+
info.addEventListener("mouseleave", hideTip);
|
|
308
|
+
info.addEventListener("click", () => select(model.root.id));
|
|
300
309
|
|
|
301
310
|
// ── zoom behaviours ──
|
|
302
311
|
const chartZoom = d3
|
|
@@ -496,9 +505,13 @@
|
|
|
496
505
|
r.append("rect").attr("class", "bar").attr("rx", 5).attr("y", BAR_Y).attr("height", BAR_H);
|
|
497
506
|
r.append("text").attr("class", "bar-label").attr("y", BAR_Y + BAR_H / 2 + 4);
|
|
498
507
|
r.append("g").attr("class", "marks");
|
|
508
|
+
const cb = r.append("g").attr("class", "code-btn");
|
|
509
|
+
cb.append("circle").attr("r", 9);
|
|
510
|
+
cb.append("text").attr("text-anchor", "middle").attr("y", 3.5).text("<>");
|
|
499
511
|
return r;
|
|
500
512
|
})
|
|
501
|
-
.attr("transform", (_n, i) => `translate(0,${i * ROW_H})`)
|
|
513
|
+
.attr("transform", (_n, i) => `translate(0,${i * ROW_H})`)
|
|
514
|
+
.classed("sel", (n) => n.id === state.selected);
|
|
502
515
|
|
|
503
516
|
g.select("rect.bar")
|
|
504
517
|
.attr("x", (n) => x(n.start))
|
|
@@ -530,7 +543,22 @@
|
|
|
530
543
|
const left = Math.min(x0, flip ? lx - tw : lx) - 6;
|
|
531
544
|
const right = Math.max(x0 + Math.max(w, 3), flip ? x0 : lx + tw) + 6;
|
|
532
545
|
d3.select(this.parentNode).select("rect.hit").attr("x", left).attr("width", Math.max(16, right - left));
|
|
546
|
+
// the <> sits just past the block (or just before it, when that would leave the chart)
|
|
547
|
+
// …and, for a block spanning the chart, inside it right after its label
|
|
548
|
+
const cx =
|
|
549
|
+
right + 8 <= W - 10 ? right + 8 : left - 8 >= 10 ? left - 8 : Math.min(W - 12, (flip ? x0 : lx + tw) + 14);
|
|
550
|
+
d3.select(this.parentNode)
|
|
551
|
+
.select("g.code-btn")
|
|
552
|
+
.style("display", hasCode(n) ? null : "none")
|
|
553
|
+
.attr("transform", `translate(${cx},${BAR_Y + BAR_H / 2})`);
|
|
533
554
|
});
|
|
555
|
+
g.select("g.code-btn")
|
|
556
|
+
.on("click", (ev, n) => {
|
|
557
|
+
ev.stopPropagation();
|
|
558
|
+
toggleCode(n, ev);
|
|
559
|
+
})
|
|
560
|
+
.on("mouseenter", (ev) => showTip(ev, "Show source"))
|
|
561
|
+
.on("mouseleave", hideTip);
|
|
534
562
|
g.selectAll("rect.hit, text.bar-label").call(bindNode);
|
|
535
563
|
|
|
536
564
|
g.select("g.marks").each(function (n) {
|
|
@@ -645,12 +673,15 @@
|
|
|
645
673
|
.on("mouseleave", () => moveCursor(null));
|
|
646
674
|
flameSvg.append("g").attr("class", "grid");
|
|
647
675
|
flameSvg.append("g").attr("class", "cells");
|
|
676
|
+
flameSvg.append("line").attr("class", "pin").attr("stroke", css("--accent")).attr("stroke-width", 1.5).attr("y1", 0).attr("y2", 4000);
|
|
648
677
|
flameSvg.append("line").attr("class", "cursor").attr("y1", 0).attr("y2", 4000).attr("visibility", "hidden");
|
|
649
678
|
flameSvg.call(flameZoom).call(flameZoom.transform, state.zoom);
|
|
650
679
|
redrawX();
|
|
651
680
|
}
|
|
652
681
|
function drawFlame(x) {
|
|
653
682
|
if (!flameSvg) return;
|
|
683
|
+
const px = state.pin != null ? x(state.pin) : -10;
|
|
684
|
+
flameSvg.select("line.pin").attr("x1", px).attr("x2", px);
|
|
654
685
|
const H = el.flame.clientHeight;
|
|
655
686
|
const ticks = x.ticks(Math.max(2, Math.floor(W / 90)));
|
|
656
687
|
flameSvg
|
|
@@ -693,6 +724,7 @@
|
|
|
693
724
|
|
|
694
725
|
// ── flow view ──
|
|
695
726
|
let flowG = null;
|
|
727
|
+
let flowPos = new Map();
|
|
696
728
|
let flowBounds = null;
|
|
697
729
|
function renderFlow() {
|
|
698
730
|
d3.select(el.flow).selectAll("svg").remove();
|
|
@@ -752,6 +784,22 @@
|
|
|
752
784
|
.attr("cy", 14)
|
|
753
785
|
.attr("r", 4)
|
|
754
786
|
.attr("fill", (d) => css(statusVar(d.data.status)));
|
|
787
|
+
const fcb = card
|
|
788
|
+
.filter((d) => hasCode(d.data))
|
|
789
|
+
.append("g")
|
|
790
|
+
.attr("class", "code-btn")
|
|
791
|
+
.attr("transform", `translate(${CW / 2},0)`)
|
|
792
|
+
.on("click", (ev, d) => {
|
|
793
|
+
ev.stopPropagation();
|
|
794
|
+
toggleCode(d.data, ev);
|
|
795
|
+
})
|
|
796
|
+
.on("mouseenter", (ev) => {
|
|
797
|
+
ev.stopPropagation();
|
|
798
|
+
showTip(ev, "Show source");
|
|
799
|
+
});
|
|
800
|
+
fcb.append("circle").attr("r", 9);
|
|
801
|
+
fcb.append("text").attr("text-anchor", "middle").attr("y", 3.5).text("<>");
|
|
802
|
+
flowPos = new Map(h.descendants().map((d) => [d.data.id, { x: d.y + CW / 2, y: d.x }]));
|
|
755
803
|
svg.call(flowZoom);
|
|
756
804
|
fitFlow(true);
|
|
757
805
|
}
|
|
@@ -820,7 +868,10 @@
|
|
|
820
868
|
state.details = true;
|
|
821
869
|
el.main.classList.remove("no-details");
|
|
822
870
|
el.tree.querySelectorAll(".row").forEach((r) => r.classList.toggle("selected", r.dataset.id === id));
|
|
823
|
-
if (chartSvg)
|
|
871
|
+
if (chartSvg) {
|
|
872
|
+
chartSvg.selectAll("rect.bar").classed("selected", (n) => n.id === id);
|
|
873
|
+
chartSvg.selectAll("g.r").classed("sel", (n) => n.id === id);
|
|
874
|
+
}
|
|
824
875
|
if (flameSvg) flameSvg.selectAll("rect.flame-rect").classed("selected", (n) => n.id === id);
|
|
825
876
|
d3.select(el.flow).selectAll("g.flow-card").classed("selected", (d) => d.data.id === id);
|
|
826
877
|
renderDetails();
|
|
@@ -829,7 +880,7 @@
|
|
|
829
880
|
const pad = Math.max(n.end - n.start, 50) * 0.08;
|
|
830
881
|
zoomToDomain(n.start - pad, n.end + pad);
|
|
831
882
|
}
|
|
832
|
-
function revealRow(n) {
|
|
883
|
+
function revealRow(n, block) {
|
|
833
884
|
let p = n.parent;
|
|
834
885
|
let changed = false;
|
|
835
886
|
while (p) {
|
|
@@ -838,7 +889,55 @@
|
|
|
838
889
|
}
|
|
839
890
|
if (changed && state.view === "timeline") renderTimeline();
|
|
840
891
|
const row = el.tree.querySelector(`.row[data-id="${CSS.escape(n.id)}"]`);
|
|
841
|
-
if (row) row.scrollIntoView({ block: "nearest" });
|
|
892
|
+
if (row) row.scrollIntoView({ block: block || "nearest" });
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* Bring a gallery item into view in whichever graph is showing: select its node, pin its
|
|
897
|
+
* time, pan (keeping the zoom) so that time is on screen, and pulse its mark.
|
|
898
|
+
*/
|
|
899
|
+
function locate(g) {
|
|
900
|
+
if (!g) return;
|
|
901
|
+
if (g.node) select(g.node.id, "snapshots");
|
|
902
|
+
if (state.view === "flow") {
|
|
903
|
+
if (g.node) centerFlowOn(g.node);
|
|
904
|
+
return;
|
|
905
|
+
}
|
|
906
|
+
if (g.t != null) {
|
|
907
|
+
state.pin = g.t;
|
|
908
|
+
const [a, b] = xz().domain();
|
|
909
|
+
if (g.t < a || g.t > b) {
|
|
910
|
+
const k = state.zoom.k;
|
|
911
|
+
const t = d3.zoomIdentity.translate(W / 2 - k * baseX(g.t), 0).scale(k);
|
|
912
|
+
const { z, sel } = activeZoom();
|
|
913
|
+
sel.call(z.transform, t);
|
|
914
|
+
} else redrawX();
|
|
915
|
+
}
|
|
916
|
+
if (g.node && state.view === "timeline") revealRow(g.node, "center");
|
|
917
|
+
flash(g);
|
|
918
|
+
}
|
|
919
|
+
function flash(g) {
|
|
920
|
+
const svgs = [chartSvg, flameSvg].filter(Boolean);
|
|
921
|
+
for (const s of svgs) {
|
|
922
|
+
s.selectAll("line.pin").classed("flash", false).node()?.getBoundingClientRect();
|
|
923
|
+
s.selectAll("line.pin").classed("flash", true);
|
|
924
|
+
}
|
|
925
|
+
if (chartSvg) {
|
|
926
|
+
const hit = chartSvg
|
|
927
|
+
.selectAll("circle.mark")
|
|
928
|
+
.filter((d) => d.m.gallery === g || d.more.some((m) => m.gallery === g));
|
|
929
|
+
hit.classed("flash", false).node()?.getBoundingClientRect();
|
|
930
|
+
hit.classed("flash", true);
|
|
931
|
+
}
|
|
932
|
+
setTimeout(() => d3.selectAll(".flash").classed("flash", false), 1800);
|
|
933
|
+
}
|
|
934
|
+
function centerFlowOn(n) {
|
|
935
|
+
const svg = d3.select(el.flow).select("svg");
|
|
936
|
+
const p = flowPos.get(n.id);
|
|
937
|
+
if (svg.empty() || !p) return;
|
|
938
|
+
const k = Math.max(0.8, d3.zoomTransform(svg.node()).k);
|
|
939
|
+
const t = d3.zoomIdentity.translate(el.flow.clientWidth / 2 - k * p.x, el.flow.clientHeight / 2 - k * p.y).scale(k);
|
|
940
|
+
svg.transition().duration(300).call(flowZoom.transform, t);
|
|
842
941
|
}
|
|
843
942
|
|
|
844
943
|
function renderDetails() {
|
|
@@ -866,7 +965,12 @@
|
|
|
866
965
|
${n.v.description ? `<div class="k">Summary</div><div class="v">${esc(n.v.description.summary)}</div>` : ""}
|
|
867
966
|
${n.v.mcp ? `<div class="k">MCP</div><div class="v"><span class="badge-inline">${esc(n.v.mcp.mode)}</span></div>` : ""}
|
|
868
967
|
<div class="k">Path</div><div class="v">${esc(n.v.path)}</div>
|
|
869
|
-
${n.v.definedAt ? `<div class="k">Defined at</div><div class="v defined"
|
|
968
|
+
${n.v.definedAt ? `<div class="k">Defined at</div><div class="v defined">
|
|
969
|
+
<button class="icon-btn" data-copy-source aria-label="Copy relative path">${svgIcon("copy")}</button>
|
|
970
|
+
${hasCode(n) ? `<button class="icon-btn" data-show-code aria-label="Show source">${svgIcon("code")}</button>` : ""}
|
|
971
|
+
<a class="icon-btn" href="${esc(editorUrl(n.v.definedAt))}" aria-label="Open in VS Code" data-tip-text="Open in VS Code">${svgIcon("external")}</a>
|
|
972
|
+
<span class="defined-name" >${esc(basename(sourceOf(n)))}</span>
|
|
973
|
+
</div>` : ""}
|
|
870
974
|
</div>
|
|
871
975
|
<div class="tabs">${tabs
|
|
872
976
|
.map(([k, l]) => `<button class="tab ${state.detailTab === k ? "active" : ""}" data-tab="${k}">${l}</button>`)
|
|
@@ -874,7 +978,24 @@
|
|
|
874
978
|
<div class="tabbody">${detailTabHtml(n)}</div>
|
|
875
979
|
</div>`;
|
|
876
980
|
const copySrc = el.details.querySelector("[data-copy-source]");
|
|
877
|
-
if (copySrc)
|
|
981
|
+
if (copySrc) {
|
|
982
|
+
copySrc.addEventListener("click", () => copySource(n));
|
|
983
|
+
copySrc.addEventListener("mouseenter", (ev) =>
|
|
984
|
+
showTip(ev, `<div class="mono">${esc(sourceOf(n))}</div><div class="hint">Click (or press <kbd>C</kbd>) to copy</div>`)
|
|
985
|
+
);
|
|
986
|
+
copySrc.addEventListener("mousemove", (ev) => moveTip(ev));
|
|
987
|
+
copySrc.addEventListener("mouseleave", hideTip);
|
|
988
|
+
}
|
|
989
|
+
const showCode = el.details.querySelector("[data-show-code]");
|
|
990
|
+
if (showCode) {
|
|
991
|
+
showCode.addEventListener("click", (ev) => toggleCode(n, ev));
|
|
992
|
+
showCode.addEventListener("mouseenter", (ev) => showTip(ev, "Show source"));
|
|
993
|
+
showCode.addEventListener("mouseleave", hideTip);
|
|
994
|
+
}
|
|
995
|
+
el.details.querySelectorAll("[data-tip-text]").forEach((a) => {
|
|
996
|
+
a.addEventListener("mouseenter", (ev) => showTip(ev, esc(a.dataset.tipText)));
|
|
997
|
+
a.addEventListener("mouseleave", hideTip);
|
|
998
|
+
});
|
|
878
999
|
el.details.querySelector(".x").addEventListener("click", () => {
|
|
879
1000
|
state.details = false;
|
|
880
1001
|
el.main.classList.add("no-details");
|
|
@@ -1002,6 +1123,14 @@
|
|
|
1002
1123
|
body.querySelectorAll("tr[data-row]").forEach((tr) =>
|
|
1003
1124
|
tr.addEventListener("click", () => openItem(model.items[+tr.dataset.row]))
|
|
1004
1125
|
);
|
|
1126
|
+
body.querySelectorAll("[data-locate]").forEach((b) => {
|
|
1127
|
+
b.addEventListener("click", (ev) => {
|
|
1128
|
+
ev.stopPropagation();
|
|
1129
|
+
locate(model.gallery[+b.dataset.locate]);
|
|
1130
|
+
});
|
|
1131
|
+
b.addEventListener("mouseenter", (ev) => showTip(ev, `Show in the ${state.view === "flow" ? "flow" : "graph"}`));
|
|
1132
|
+
b.addEventListener("mouseleave", hideTip);
|
|
1133
|
+
});
|
|
1005
1134
|
}
|
|
1006
1135
|
function matches(...parts) {
|
|
1007
1136
|
const q = state.filter.trim().toLowerCase();
|
|
@@ -1015,7 +1144,8 @@
|
|
|
1015
1144
|
return `<div class="gallery">${g
|
|
1016
1145
|
.map((x) => {
|
|
1017
1146
|
const sub = x.type === "snapshot" ? `snapshot · ${x.node ? esc(displayName(x.node)) : ""}` : `${esc(x.f.mime)} · ${fmtSize(x.f.size)}${x.f.omitted ? " · not embedded" : ""}`;
|
|
1018
|
-
|
|
1147
|
+
const at = x.t != null || x.node ? `<button class="icon-btn locate" data-locate="${model.gallery.indexOf(x)}" aria-label="Show in graph">${svgIcon("locate")}</button>` : "";
|
|
1148
|
+
return `<div class="gal-item">${thumbHtml(x)}<div class="cap"><span class="t">${x.t == null ? "–" : fmtTick(x.t)}</span><span class="n" title="${esc(x.name)}">${esc(x.name)}</span>${at}</div><div class="sz">${sub}</div></div>`;
|
|
1019
1149
|
})
|
|
1020
1150
|
.join("")}</div>`;
|
|
1021
1151
|
}
|
|
@@ -1144,7 +1274,7 @@
|
|
|
1144
1274
|
function nodeTip(n) {
|
|
1145
1275
|
hovered = n; // the node `C` copies while its tooltip is up
|
|
1146
1276
|
const v = n.v;
|
|
1147
|
-
const shown = displayName(n);
|
|
1277
|
+
const shown = n.depth === 0 ? model.data.name : displayName(n);
|
|
1148
1278
|
const badges = [
|
|
1149
1279
|
`<span class="badge"><span class="dot ${n.status}"></span>${esc(n.status)}</span>`,
|
|
1150
1280
|
v.mcp ? `<span class="badge mcp" title="Exposed over MCP">MCP · ${esc(v.mcp.mode)}</span>` : "",
|
|
@@ -1171,6 +1301,10 @@
|
|
|
1171
1301
|
${v.definedAt ? '<div class="hint">Press <kbd>C</kbd> to copy the path</div>' : ""}
|
|
1172
1302
|
${fails.length ? `<div class="sec fail">Failed</div><div class="mono small">${esc(truncate(String(fails[0].error).split(String.fromCharCode(10))[0], 200))}</div>` : ""}`;
|
|
1173
1303
|
}
|
|
1304
|
+
/** The om itself: its node tooltip, plus where the run's record lives. */
|
|
1305
|
+
function omTip() {
|
|
1306
|
+
return `${nodeTip(model.root)}<div class="sec">Run folder</div><div class="mono small">${esc(model.data.folder)}</div>`;
|
|
1307
|
+
}
|
|
1174
1308
|
function markTip(d) {
|
|
1175
1309
|
const m = d.m;
|
|
1176
1310
|
const mk = MARK[m.kind] || MARK.event;
|
|
@@ -1179,6 +1313,63 @@
|
|
|
1179
1313
|
const extra = m.gallery && m.gallery.image ? `<div class="ts">${esc(m.gallery.f.rel)} · ${fmtSize(m.gallery.f.size)}</div>` : "";
|
|
1180
1314
|
return `<div class="tt">${esc(mk.label)} · ${fmtRel(m.t)}</div>${img}<div>${esc(truncate(m.kind === "snapshot" || m.kind === "artifact" ? m.label : m.e.message, 260))}</div>${extra}${more}`;
|
|
1181
1315
|
}
|
|
1316
|
+
// ── source popover (the <> buttons) — click-toggled, like the tskb explorer's ──
|
|
1317
|
+
const codeEl = document.createElement("div");
|
|
1318
|
+
codeEl.className = "code-pop";
|
|
1319
|
+
codeEl.hidden = true;
|
|
1320
|
+
document.body.appendChild(codeEl);
|
|
1321
|
+
let codeFor = null;
|
|
1322
|
+
let codeAway = null;
|
|
1323
|
+
function hasCode(n) {
|
|
1324
|
+
return !!(n.v.definedAt && model.data.code && model.data.code[n.v.definedAt]);
|
|
1325
|
+
}
|
|
1326
|
+
function toggleCode(n, ev) {
|
|
1327
|
+
if (codeFor === n.id) return closeCode();
|
|
1328
|
+
const site = n.v.definedAt;
|
|
1329
|
+
const c = model.data.code && model.data.code[site];
|
|
1330
|
+
if (!c) return;
|
|
1331
|
+
hideTip();
|
|
1332
|
+
codeFor = n.id;
|
|
1333
|
+
const line = Number((/:(\d+)(?::\d+)?$/.exec(site) || [])[1]);
|
|
1334
|
+
const rel = sourceOf(n);
|
|
1335
|
+
const lines = highlight(c.text).map(
|
|
1336
|
+
(html, i) => `<div class="cl${c.start + i === line ? " at" : ""}"><span class="ln">${c.start + i}</span><span class="lc">${html || " "}</span></div>`
|
|
1337
|
+
);
|
|
1338
|
+
codeEl.innerHTML = `<div class="cp-head">
|
|
1339
|
+
<span class="cp-path" title="${esc(site)}">${esc(rel)}<span class="muted">:${line || c.start}</span></span>
|
|
1340
|
+
<button class="icon-btn" data-a="copy" aria-label="Copy path">${svgIcon("copy")}</button>
|
|
1341
|
+
<a class="icon-btn" href="${esc(editorUrl(site))}" aria-label="Open in VS Code">${svgIcon("external")}</a>
|
|
1342
|
+
<span class="cp-name">${esc(displayName(n))}</span>
|
|
1343
|
+
<button class="icon-btn" data-a="close" aria-label="Close">${svgIcon("close")}</button>
|
|
1344
|
+
</div>
|
|
1345
|
+
<div class="cp-body"><div class="cp-code">${lines.join("")}</div>${
|
|
1346
|
+
c.truncated ? '<div class="muted cp-more">Cut short — open the file for the rest.</div>' : ""
|
|
1347
|
+
}</div>`;
|
|
1348
|
+
codeEl.querySelector('[data-a="copy"]').addEventListener("click", () => copy(rel, `Copied ${rel}`));
|
|
1349
|
+
codeEl.querySelector('[data-a="close"]').addEventListener("click", closeCode);
|
|
1350
|
+
codeEl.hidden = false;
|
|
1351
|
+
const x = ev && ev.clientX != null ? ev.clientX : window.innerWidth / 2;
|
|
1352
|
+
const y = ev && ev.clientY != null ? ev.clientY : window.innerHeight / 3;
|
|
1353
|
+
const w = codeEl.offsetWidth;
|
|
1354
|
+
const h = codeEl.offsetHeight;
|
|
1355
|
+
const left = x + 14 + w > window.innerWidth - 8 ? x - w - 14 : x + 14;
|
|
1356
|
+
const top = y + 14 + h > window.innerHeight - 8 ? y - h - 14 : y + 14;
|
|
1357
|
+
codeEl.style.left = `${Math.max(8, left)}px`;
|
|
1358
|
+
codeEl.style.top = `${Math.max(8, top)}px`;
|
|
1359
|
+
const at = codeEl.querySelector(".cl.at");
|
|
1360
|
+
if (at) at.scrollIntoView({ block: "center" });
|
|
1361
|
+
if (codeAway) document.removeEventListener("mousedown", codeAway, true);
|
|
1362
|
+
codeAway = (e) => {
|
|
1363
|
+
if (!codeEl.contains(e.target) && !e.target.closest(".code-btn, [data-show-code]")) closeCode();
|
|
1364
|
+
};
|
|
1365
|
+
setTimeout(() => codeAway && document.addEventListener("mousedown", codeAway, true), 0);
|
|
1366
|
+
}
|
|
1367
|
+
function closeCode() {
|
|
1368
|
+
codeFor = null;
|
|
1369
|
+
codeEl.hidden = true;
|
|
1370
|
+
if (codeAway) document.removeEventListener("mousedown", codeAway, true);
|
|
1371
|
+
codeAway = null;
|
|
1372
|
+
}
|
|
1182
1373
|
let tipTimer = 0;
|
|
1183
1374
|
let hovered = null;
|
|
1184
1375
|
/** The file where `n` is defined, relative to the project root — no line. */
|
|
@@ -1242,6 +1433,7 @@
|
|
|
1242
1433
|
resizeTimer = setTimeout(relayout, 120);
|
|
1243
1434
|
});
|
|
1244
1435
|
document.addEventListener("keydown", (e) => {
|
|
1436
|
+
if (e.key === "Escape" && codeFor) return closeCode();
|
|
1245
1437
|
if (e.target.closest && e.target.closest("input")) return;
|
|
1246
1438
|
if (e.key === "+" || e.key === "=") zoomBy(1.6);
|
|
1247
1439
|
else if (e.key === "-") zoomBy(1 / 1.6);
|
|
@@ -1278,7 +1470,10 @@
|
|
|
1278
1470
|
return `
|
|
1279
1471
|
<header class="top">
|
|
1280
1472
|
<div class="titles">
|
|
1281
|
-
<
|
|
1473
|
+
<div class="title-row">
|
|
1474
|
+
<h1 title="${esc(model.data.folder)}">Run: ${esc(model.data.name)}</h1>
|
|
1475
|
+
<button class="info-btn" id="om-info" aria-label="About this om">${svgIcon("info")}</button>
|
|
1476
|
+
</div>
|
|
1282
1477
|
<div class="meta"><span class="dot ${ok ? "ok" : "failed"}"></span><span>${ok ? "passed" : "failed"}</span>
|
|
1283
1478
|
<span class="sep">•</span><span>${fmtDate(run.startedAt)}</span>
|
|
1284
1479
|
<span class="sep">•</span><span title="body ${fmtDur(run.duration)} · record ${fmtDur(model.span)}">${fmtDur(run.duration)}</span>
|
|
@@ -1358,6 +1553,41 @@
|
|
|
1358
1553
|
c.opacity = a;
|
|
1359
1554
|
return c.formatRgb();
|
|
1360
1555
|
}
|
|
1556
|
+
const KEYWORDS = new Set(
|
|
1557
|
+
"abstract as async await break case catch class const continue default delete do else enum export extends false finally for from function get if implements import in instanceof interface keyof let new null of private protected public readonly return satisfies set static super switch this throw true try type typeof undefined var void while yield".split(" ")
|
|
1558
|
+
);
|
|
1559
|
+
const TOKEN =
|
|
1560
|
+
/(\/\/[^\n]*|\/\*[\s\S]*?\*\/)|(`(?:\\[\s\S]|[^`\\])*`|"(?:\\.|[^"\\\n])*"|'(?:\\.|[^'\\\n])*')|(\b\d[\d_]*(?:\.\d+)?(?:[eE][+-]?\d+)?n?\b)|([A-Za-z_$][\w$]*)/g;
|
|
1561
|
+
/** Highlighted HTML per source line. Tokens that span lines are closed and reopened at each break. */
|
|
1562
|
+
function highlight(text) {
|
|
1563
|
+
const parts = [];
|
|
1564
|
+
let last = 0;
|
|
1565
|
+
let m;
|
|
1566
|
+
TOKEN.lastIndex = 0;
|
|
1567
|
+
while ((m = TOKEN.exec(text))) {
|
|
1568
|
+
if (m.index > last) parts.push([null, text.slice(last, m.index)]);
|
|
1569
|
+
let cls = null;
|
|
1570
|
+
if (m[1]) cls = "tk-c";
|
|
1571
|
+
else if (m[2]) cls = "tk-s";
|
|
1572
|
+
else if (m[3]) cls = "tk-n";
|
|
1573
|
+
else if (m[4]) {
|
|
1574
|
+
if (KEYWORDS.has(m[4])) cls = "tk-k";
|
|
1575
|
+
else if (/^\s*\(/.test(text.slice(TOKEN.lastIndex, TOKEN.lastIndex + 40))) cls = "tk-f";
|
|
1576
|
+
else if (/^[A-Z]/.test(m[4])) cls = "tk-t";
|
|
1577
|
+
}
|
|
1578
|
+
parts.push([cls, m[0]]);
|
|
1579
|
+
last = TOKEN.lastIndex;
|
|
1580
|
+
}
|
|
1581
|
+
if (last < text.length) parts.push([null, text.slice(last)]);
|
|
1582
|
+
const lines = [""];
|
|
1583
|
+
for (const [cls, chunk] of parts) {
|
|
1584
|
+
chunk.split("\n").forEach((piece, i) => {
|
|
1585
|
+
if (i > 0) lines.push("");
|
|
1586
|
+
if (piece) lines[lines.length - 1] += cls ? `<span class="${cls}">${esc(piece)}</span>` : esc(piece);
|
|
1587
|
+
});
|
|
1588
|
+
}
|
|
1589
|
+
return lines;
|
|
1590
|
+
}
|
|
1361
1591
|
function oneLine(v) {
|
|
1362
1592
|
try {
|
|
1363
1593
|
return typeof v === "string" ? v : JSON.stringify(v);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "omkit",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.12",
|
|
4
4
|
"description": "A tiny runtime for developer workflows: typed actions that pass runtime capabilities and emit typed events, under one run and one shared log. The operational/runtime layer to tskb's knowledge layer.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|