omkit 0.8.20 → 0.8.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/output/writers/report/report.js +546 -160
- package/package.json +1 -1
|
@@ -34,14 +34,16 @@
|
|
|
34
34
|
root: "M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8zM12 2v3M12 19v3M2 12h3M19 12h3",
|
|
35
35
|
chat: "M4 5h16v11H9l-5 4z",
|
|
36
36
|
flask: "M9 3h6M10 3v6l-5 9a2 2 0 0 0 2 3h10a2 2 0 0 0 2-3l-5-9V3",
|
|
37
|
-
braces:
|
|
37
|
+
braces:
|
|
38
|
+
"M8 4c-2 0-3 1-3 3v2c0 1-1 2-2 3 1 1 2 2 2 3v2c0 2 1 3 3 3M16 4c2 0 3 1 3 3v2c0 1 1 2 2 3-1 1-2 2-2 3v2c0 2-1 3-3 3",
|
|
38
39
|
image: "M4 5h16v14H4zM4 16l5-5 4 4 2-2 5 5M15 9h.01",
|
|
39
40
|
sun: "M12 8a4 4 0 1 0 0 8 4 4 0 0 0 0-8zM12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4",
|
|
40
41
|
download: "M12 4v11M7 10l5 5 5-5M5 20h14",
|
|
41
42
|
copy: "M9 9h11v11H9zM5 15V4h11",
|
|
42
43
|
stop: "M6 6h12v12H6z",
|
|
43
44
|
info: "M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18zM12 11v5M12 8h.01",
|
|
44
|
-
locate:
|
|
45
|
+
locate:
|
|
46
|
+
"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
47
|
code: "M8 7l-5 5 5 5M16 7l5 5-5 5",
|
|
46
48
|
external: "M14 4h6v6M20 4l-9 9M18 14v6H4V6h6",
|
|
47
49
|
};
|
|
@@ -83,7 +85,11 @@
|
|
|
83
85
|
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`;
|
|
84
86
|
};
|
|
85
87
|
const fmtSize = (b) =>
|
|
86
|
-
b < 1024
|
|
88
|
+
b < 1024
|
|
89
|
+
? `${b} B`
|
|
90
|
+
: b < 1048576
|
|
91
|
+
? `${Math.round(b / 1024)} KB`
|
|
92
|
+
: `${(b / 1048576).toFixed(1)} MB`;
|
|
87
93
|
const fmtTick = (ms) => {
|
|
88
94
|
if (ms === 0) return "0s";
|
|
89
95
|
const a = Math.abs(ms);
|
|
@@ -92,9 +98,20 @@
|
|
|
92
98
|
return `${Math.floor(ms / 60000)}m${pad2(Math.round((ms % 60000) / 1000))}s`;
|
|
93
99
|
};
|
|
94
100
|
const esc = (s) =>
|
|
95
|
-
String(s).replace(
|
|
101
|
+
String(s).replace(
|
|
102
|
+
/[&<>"']/g,
|
|
103
|
+
(c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c]
|
|
104
|
+
);
|
|
96
105
|
const basename = (p) => String(p).split(/[\\/]/).pop();
|
|
97
|
-
const fileUrl = (p) =>
|
|
106
|
+
const fileUrl = (p) =>
|
|
107
|
+
"file:///" +
|
|
108
|
+
String(p)
|
|
109
|
+
.replace(/\\/g, "/")
|
|
110
|
+
.replace(/^\/+/, "")
|
|
111
|
+
.split("/")
|
|
112
|
+
.map(encodeURIComponent)
|
|
113
|
+
.join("/")
|
|
114
|
+
.replace(/^([A-Za-z])%3A/, "$1:");
|
|
98
115
|
const editorUrl = (site) => {
|
|
99
116
|
const m = /^(.*):(\d+)$/.exec(site);
|
|
100
117
|
const file = (m ? m[1] : site).replace(/\\/g, "/");
|
|
@@ -127,7 +144,11 @@
|
|
|
127
144
|
depth,
|
|
128
145
|
branch: depth === 0 ? -1 : b,
|
|
129
146
|
children: [],
|
|
130
|
-
status: ["ok", "failed", "cancelled"].includes(v.status)
|
|
147
|
+
status: ["ok", "failed", "cancelled"].includes(v.status)
|
|
148
|
+
? v.status
|
|
149
|
+
: v.endedAt
|
|
150
|
+
? v.status
|
|
151
|
+
: "running",
|
|
131
152
|
startAbs: v.startedAt || t0,
|
|
132
153
|
endAbs: v.endedAt || 0,
|
|
133
154
|
entries: [],
|
|
@@ -153,7 +174,14 @@
|
|
|
153
174
|
for (const e of data.entries) {
|
|
154
175
|
if (e.level === "child" || e.level === "tag") continue;
|
|
155
176
|
const kind = kindOf(e);
|
|
156
|
-
const item = {
|
|
177
|
+
const item = {
|
|
178
|
+
idx: items.length,
|
|
179
|
+
e,
|
|
180
|
+
kind,
|
|
181
|
+
t: e.ts - t0,
|
|
182
|
+
label: labelOf(e, kind),
|
|
183
|
+
node: byId.get(e.nodeId) || null,
|
|
184
|
+
};
|
|
157
185
|
items.push(item);
|
|
158
186
|
if (item.node) {
|
|
159
187
|
item.node.entries.push(item);
|
|
@@ -174,12 +202,20 @@
|
|
|
174
202
|
});
|
|
175
203
|
}
|
|
176
204
|
for (const s of data.snapshots) {
|
|
177
|
-
gallery.push({
|
|
205
|
+
gallery.push({
|
|
206
|
+
type: "snapshot",
|
|
207
|
+
s,
|
|
208
|
+
name: s.name,
|
|
209
|
+
t: s.ts - t0,
|
|
210
|
+
node: byId.get(s.nodeId) || null,
|
|
211
|
+
image: false,
|
|
212
|
+
});
|
|
178
213
|
}
|
|
179
214
|
gallery.sort((a, b) => (a.t ?? Infinity) - (b.t ?? Infinity));
|
|
180
215
|
// Let a mark find its gallery item (snapshot by file, artifact by file).
|
|
181
216
|
const galleryByFile = new Map();
|
|
182
|
-
for (const g of gallery)
|
|
217
|
+
for (const g of gallery)
|
|
218
|
+
galleryByFile.set(String(g.type === "file" ? g.f.file : g.s.file).toLowerCase(), g);
|
|
183
219
|
for (const it of items) {
|
|
184
220
|
if (it.kind === "snapshot" || it.kind === "artifact") {
|
|
185
221
|
const i = it.e.message.lastIndexOf(" → ");
|
|
@@ -193,12 +229,31 @@
|
|
|
193
229
|
const l = it.kind === "link" && it.node && it.e.link;
|
|
194
230
|
const from = l && byId.get(l.from);
|
|
195
231
|
if (!from) continue;
|
|
196
|
-
links.push({
|
|
232
|
+
links.push({
|
|
233
|
+
item: it,
|
|
234
|
+
op: l.op,
|
|
235
|
+
from,
|
|
236
|
+
to: it.node,
|
|
237
|
+
t: it.t,
|
|
238
|
+
t0: it.t - (l.ms || 0),
|
|
239
|
+
key: l.key || "",
|
|
240
|
+
ms: l.ms,
|
|
241
|
+
outcome: l.outcome || "",
|
|
242
|
+
site: l.site || "",
|
|
243
|
+
});
|
|
197
244
|
}
|
|
198
245
|
// …drawn as a point on the row of the action that made it: what it did, when it did it
|
|
199
246
|
for (const l of links) {
|
|
200
247
|
l.label = linkLabel(l);
|
|
201
|
-
l.from.marks.push({
|
|
248
|
+
l.from.marks.push({
|
|
249
|
+
idx: l.item.idx,
|
|
250
|
+
e: l.item.e,
|
|
251
|
+
kind: "link",
|
|
252
|
+
t: l.t,
|
|
253
|
+
label: l.label,
|
|
254
|
+
node: l.from,
|
|
255
|
+
link: l,
|
|
256
|
+
});
|
|
202
257
|
}
|
|
203
258
|
for (const l of links) l.from.marks.sort((a, b) => a.t - b.t);
|
|
204
259
|
|
|
@@ -253,9 +308,23 @@
|
|
|
253
308
|
const open = new Map(); // `${nodeId}\0${source}` → the output block still accepting lines
|
|
254
309
|
for (const it of items) {
|
|
255
310
|
if (it.kind === "fail") {
|
|
256
|
-
out.push({
|
|
311
|
+
out.push({
|
|
312
|
+
type: "assert",
|
|
313
|
+
node: it.node,
|
|
314
|
+
t: it.t,
|
|
315
|
+
title: it.label,
|
|
316
|
+
text: it.e.message,
|
|
317
|
+
items: [it],
|
|
318
|
+
});
|
|
257
319
|
} else if (it.kind === "error" && it.e.source === "error") {
|
|
258
|
-
out.push({
|
|
320
|
+
out.push({
|
|
321
|
+
type: "failure",
|
|
322
|
+
node: it.node,
|
|
323
|
+
t: it.t,
|
|
324
|
+
title: firstLine(it.e.message),
|
|
325
|
+
text: it.e.message,
|
|
326
|
+
items: [it],
|
|
327
|
+
});
|
|
259
328
|
} else if (it.kind === "error") {
|
|
260
329
|
const key = `${it.e.nodeId}\0${it.e.source}`;
|
|
261
330
|
const block = open.get(key);
|
|
@@ -266,18 +335,38 @@
|
|
|
266
335
|
block.text = block.lines.join("\n");
|
|
267
336
|
continue;
|
|
268
337
|
}
|
|
269
|
-
const fresh = {
|
|
338
|
+
const fresh = {
|
|
339
|
+
type: "output",
|
|
340
|
+
node: it.node,
|
|
341
|
+
t: it.t,
|
|
342
|
+
tEnd: it.t,
|
|
343
|
+
source: it.e.source,
|
|
344
|
+
title: firstLine(it.e.message),
|
|
345
|
+
lines: [it.e.message],
|
|
346
|
+
text: it.e.message,
|
|
347
|
+
items: [it],
|
|
348
|
+
};
|
|
270
349
|
open.set(key, fresh);
|
|
271
350
|
out.push(fresh);
|
|
272
351
|
}
|
|
273
352
|
}
|
|
274
353
|
// a failure the verdict holds but no node logged (e.g. the body threw outside any action)
|
|
275
354
|
for (const f of run.failures || []) {
|
|
276
|
-
const n =
|
|
355
|
+
const n =
|
|
356
|
+
nodes.find((x) => x.v.path === f.action || x.id === f.action || x.name === f.action) ||
|
|
357
|
+
null;
|
|
277
358
|
if (out.some((e) => e.type === "failure" && e.node === n)) continue;
|
|
278
359
|
// a failed assert is already its own entry; the verdict just lists it again as a fault
|
|
279
|
-
if (out.some((e) => e.type === "assert" && e.node === n && String(f.error).includes(e.title)))
|
|
280
|
-
|
|
360
|
+
if (out.some((e) => e.type === "assert" && e.node === n && String(f.error).includes(e.title)))
|
|
361
|
+
continue;
|
|
362
|
+
out.push({
|
|
363
|
+
type: "failure",
|
|
364
|
+
node: n,
|
|
365
|
+
t: n ? n.end : span,
|
|
366
|
+
title: firstLine(f.error),
|
|
367
|
+
text: f.error,
|
|
368
|
+
items: [],
|
|
369
|
+
});
|
|
281
370
|
}
|
|
282
371
|
out.sort((a, b) => a.t - b.t);
|
|
283
372
|
out.forEach((e, i) => (e.idx = i));
|
|
@@ -340,7 +429,8 @@
|
|
|
340
429
|
details: true,
|
|
341
430
|
};
|
|
342
431
|
// Deep, wide runs start collapsed below the second level so the first screen stays readable.
|
|
343
|
-
if (model.nodes.length > 60)
|
|
432
|
+
if (model.nodes.length > 60)
|
|
433
|
+
for (const n of model.nodes) if (n.depth >= 2 && n.children.length) state.collapsed.add(n.id);
|
|
344
434
|
|
|
345
435
|
app.innerHTML = shellHtml(model);
|
|
346
436
|
const el = {
|
|
@@ -368,9 +458,9 @@
|
|
|
368
458
|
const xz = () => state.zoom.rescaleX(baseX);
|
|
369
459
|
|
|
370
460
|
// ── header controls ──
|
|
371
|
-
app
|
|
372
|
-
|
|
373
|
-
|
|
461
|
+
app
|
|
462
|
+
.querySelectorAll("[data-view]")
|
|
463
|
+
.forEach((b) => b.addEventListener("click", () => setView(b.dataset.view)));
|
|
374
464
|
app.querySelector("#zin").addEventListener("click", () => zoomBy(1.6));
|
|
375
465
|
app.querySelector("#zout").addEventListener("click", () => zoomBy(1 / 1.6));
|
|
376
466
|
app.querySelector("#fit").addEventListener("click", fit);
|
|
@@ -397,17 +487,26 @@
|
|
|
397
487
|
};
|
|
398
488
|
legendBtn.addEventListener("click", () => toggleLegend(legend.hidden));
|
|
399
489
|
document.addEventListener("mousedown", (e) => {
|
|
400
|
-
if (!legend.hidden && !legend.contains(e.target) && !legendBtn.contains(e.target))
|
|
490
|
+
if (!legend.hidden && !legend.contains(e.target) && !legendBtn.contains(e.target))
|
|
491
|
+
toggleLegend(false);
|
|
401
492
|
});
|
|
402
|
-
document.addEventListener(
|
|
493
|
+
document.addEventListener(
|
|
494
|
+
"keydown",
|
|
495
|
+
(e) => e.key === "Escape" && !legend.hidden && toggleLegend(false)
|
|
496
|
+
);
|
|
403
497
|
|
|
404
498
|
// ── zoom behaviours ──
|
|
405
499
|
const chartZoom = d3
|
|
406
500
|
.zoom()
|
|
407
501
|
.scaleExtent([1, 5000])
|
|
408
|
-
.filter((e) =>
|
|
502
|
+
.filter((e) =>
|
|
503
|
+
e.type === "wheel" ? e.ctrlKey || e.metaKey : !e.button && e.type !== "dblclick"
|
|
504
|
+
)
|
|
409
505
|
.on("zoom", (e) => onZoom(e.transform));
|
|
410
|
-
const flowZoom = d3
|
|
506
|
+
const flowZoom = d3
|
|
507
|
+
.zoom()
|
|
508
|
+
.scaleExtent([0.1, 4])
|
|
509
|
+
.on("zoom", (e) => flowG && flowG.attr("transform", e.transform));
|
|
411
510
|
|
|
412
511
|
function activeZoom() {
|
|
413
512
|
return { z: chartZoom, sel: d3.select(el.chart).select("svg") };
|
|
@@ -417,7 +516,13 @@
|
|
|
417
516
|
redrawX();
|
|
418
517
|
}
|
|
419
518
|
function zoomBy(k) {
|
|
420
|
-
if (state.view === "flow")
|
|
519
|
+
if (state.view === "flow")
|
|
520
|
+
return d3
|
|
521
|
+
.select(el.flow)
|
|
522
|
+
.select("svg")
|
|
523
|
+
.transition()
|
|
524
|
+
.duration(200)
|
|
525
|
+
.call(flowZoom.scaleBy, k);
|
|
421
526
|
const { z, sel } = activeZoom();
|
|
422
527
|
const [cx] = state.pin != null ? [xz()(state.pin)] : [W / 2];
|
|
423
528
|
sel.transition().duration(200).call(z.scaleBy, k, [cx, 0]);
|
|
@@ -443,13 +548,26 @@
|
|
|
443
548
|
el.head.style.paddingRight = `${sb}px`;
|
|
444
549
|
W = Math.max(200, el.chart.clientWidth);
|
|
445
550
|
const span = model.span;
|
|
446
|
-
baseX = d3
|
|
447
|
-
|
|
551
|
+
baseX = d3
|
|
552
|
+
.scaleLinear()
|
|
553
|
+
.domain([-span * 0.01, span * 1.015])
|
|
554
|
+
.range([0, W]);
|
|
555
|
+
chartZoom
|
|
556
|
+
.translateExtent([
|
|
557
|
+
[0, -1e6],
|
|
558
|
+
[W, 1e6],
|
|
559
|
+
])
|
|
560
|
+
.extent([
|
|
561
|
+
[0, 0],
|
|
562
|
+
[W, 1],
|
|
563
|
+
]);
|
|
448
564
|
}
|
|
449
565
|
|
|
450
566
|
function setView(v) {
|
|
451
567
|
state.view = v;
|
|
452
|
-
app
|
|
568
|
+
app
|
|
569
|
+
.querySelectorAll("[data-view]")
|
|
570
|
+
.forEach((b) => b.classList.toggle("active", b.dataset.view === v));
|
|
453
571
|
el.body.hidden = v !== "timeline";
|
|
454
572
|
el.head.hidden = v === "flow";
|
|
455
573
|
el.timeline.classList.toggle("is-flow", v === "flow");
|
|
@@ -498,7 +616,9 @@
|
|
|
498
616
|
<span class="icon" style="color:${color};background:${alpha(color, 0.16)}">${svgIcon(iconOf(n))}</span>
|
|
499
617
|
<span class="lines">
|
|
500
618
|
<span class="l1"><span class="label">${labelHtml(n)}</span>${
|
|
501
|
-
model.errorsBy.get(n)
|
|
619
|
+
model.errorsBy.get(n)
|
|
620
|
+
? `<span class="errbadge" title="${model.errorsBy.get(n)} error${model.errorsBy.get(n) > 1 ? "s" : ""}">${model.errorsBy.get(n)}</span>`
|
|
621
|
+
: ""
|
|
502
622
|
}</span>
|
|
503
623
|
<span class="l2">${tags}<span class="meta"><span class="st"><span class="dot ${n.status}"></span>${esc(n.status)}</span><span class="sep">·</span><span class="du">${fmtDur(n.duration)}</span></span></span>
|
|
504
624
|
</span>
|
|
@@ -529,8 +649,19 @@
|
|
|
529
649
|
chartSvg.append("g").attr("class", "links");
|
|
530
650
|
chartSvg.append("g").attr("class", "xwires");
|
|
531
651
|
chartSvg.append("g").attr("class", "rows");
|
|
532
|
-
chartSvg
|
|
533
|
-
|
|
652
|
+
chartSvg
|
|
653
|
+
.append("line")
|
|
654
|
+
.attr("class", "pin")
|
|
655
|
+
.attr("stroke", css("--accent"))
|
|
656
|
+
.attr("stroke-width", 1.5)
|
|
657
|
+
.attr("y1", 0)
|
|
658
|
+
.attr("y2", H);
|
|
659
|
+
chartSvg
|
|
660
|
+
.append("line")
|
|
661
|
+
.attr("class", "cursor")
|
|
662
|
+
.attr("y1", 0)
|
|
663
|
+
.attr("y2", H)
|
|
664
|
+
.attr("visibility", "hidden");
|
|
534
665
|
chartSvg.call(chartZoom).call(chartZoom.transform, state.zoom);
|
|
535
666
|
redrawX();
|
|
536
667
|
}
|
|
@@ -581,9 +712,18 @@
|
|
|
581
712
|
.join((enter) => {
|
|
582
713
|
const r = enter.append("g").attr("class", "r");
|
|
583
714
|
// A transparent target spanning the bar *and* its label, so a 2px bar is still hoverable.
|
|
584
|
-
r.append("rect")
|
|
585
|
-
|
|
586
|
-
|
|
715
|
+
r.append("rect")
|
|
716
|
+
.attr("class", "hit")
|
|
717
|
+
.attr("y", BAR_Y - 4)
|
|
718
|
+
.attr("height", BAR_H + 8);
|
|
719
|
+
r.append("rect")
|
|
720
|
+
.attr("class", "bar")
|
|
721
|
+
.attr("rx", 5)
|
|
722
|
+
.attr("y", BAR_Y)
|
|
723
|
+
.attr("height", BAR_H);
|
|
724
|
+
r.append("text")
|
|
725
|
+
.attr("class", "bar-label")
|
|
726
|
+
.attr("y", BAR_Y + BAR_H / 2 + 4);
|
|
587
727
|
r.append("g").attr("class", "marks");
|
|
588
728
|
const cb = r.append("g").attr("class", "code-btn src");
|
|
589
729
|
cb.append("circle").attr("r", 9);
|
|
@@ -617,16 +757,30 @@
|
|
|
617
757
|
const tw = shown.length * 6.6;
|
|
618
758
|
// an outside label that would run off the chart hangs left of its bar instead
|
|
619
759
|
const flip = outside && x0 + Math.max(w, 3) + 6 + tw > W;
|
|
620
|
-
const lx = flip
|
|
621
|
-
|
|
760
|
+
const lx = flip
|
|
761
|
+
? x0 - 6
|
|
762
|
+
: outside
|
|
763
|
+
? x0 + Math.max(w, 3) + 6
|
|
764
|
+
: Math.min(clipX + 8, x0 + w - 20);
|
|
765
|
+
t.text(shown)
|
|
766
|
+
.classed("outside", outside)
|
|
767
|
+
.attr("x", lx)
|
|
768
|
+
.attr("text-anchor", flip ? "end" : "start");
|
|
622
769
|
// the hit target covers bar + label, padded so a sliver of a bar is still easy to reach
|
|
623
770
|
const left = Math.min(x0, flip ? lx - tw : lx) - 6;
|
|
624
771
|
const right = Math.max(x0 + Math.max(w, 3), flip ? x0 : lx + tw) + 6;
|
|
625
|
-
d3.select(this.parentNode)
|
|
772
|
+
d3.select(this.parentNode)
|
|
773
|
+
.select("rect.hit")
|
|
774
|
+
.attr("x", left)
|
|
775
|
+
.attr("width", Math.max(16, right - left));
|
|
626
776
|
// the <> sits just past the block (or just before it, when that would leave the chart)
|
|
627
777
|
// …and, for a block spanning the chart, inside it right after its label
|
|
628
778
|
const cx =
|
|
629
|
-
right + 8 <= W - 10
|
|
779
|
+
right + 8 <= W - 10
|
|
780
|
+
? right + 8
|
|
781
|
+
: left - 8 >= 10
|
|
782
|
+
? left - 8
|
|
783
|
+
: Math.min(W - 12, (flip ? x0 : lx + tw) + 14);
|
|
630
784
|
d3.select(this.parentNode)
|
|
631
785
|
.select("g.code-btn.src")
|
|
632
786
|
.style("display", hasPopup(n) ? null : "none")
|
|
@@ -647,7 +801,10 @@
|
|
|
647
801
|
rowIndex = index;
|
|
648
802
|
drawWires(x);
|
|
649
803
|
|
|
650
|
-
chartSvg
|
|
804
|
+
chartSvg
|
|
805
|
+
.select("line.pin")
|
|
806
|
+
.attr("x1", state.pin != null ? x(state.pin) : -10)
|
|
807
|
+
.attr("x2", state.pin != null ? x(state.pin) : -10);
|
|
651
808
|
if (app.classList.contains("hl-on") || state.errHl) applyHl();
|
|
652
809
|
}
|
|
653
810
|
|
|
@@ -658,7 +815,12 @@
|
|
|
658
815
|
function linkTip(l, count) {
|
|
659
816
|
const from = esc(displayName(l.from));
|
|
660
817
|
const to = esc(displayName(l.to));
|
|
661
|
-
const title =
|
|
818
|
+
const title =
|
|
819
|
+
l.op === "once"
|
|
820
|
+
? `once · ${esc(l.key)}`
|
|
821
|
+
: l.op === "cancel"
|
|
822
|
+
? "cancel"
|
|
823
|
+
: `ref · ${esc(l.key)}()`;
|
|
662
824
|
const what =
|
|
663
825
|
l.op === "once"
|
|
664
826
|
? `<b>${from}</b> waited for <b>${esc(l.key)}</b> on <b>${to}</b>`
|
|
@@ -779,7 +941,8 @@
|
|
|
779
941
|
}
|
|
780
942
|
function worst(d) {
|
|
781
943
|
const all = [d.m, ...d.more].map((m) => m.kind);
|
|
782
|
-
for (const k of ["fail", "error", "cancel", "snapshot", "artifact", "pass", "event"])
|
|
944
|
+
for (const k of ["fail", "error", "cancel", "snapshot", "artifact", "pass", "event"])
|
|
945
|
+
if (all.includes(k)) return k;
|
|
783
946
|
return all[0];
|
|
784
947
|
}
|
|
785
948
|
|
|
@@ -789,14 +952,36 @@
|
|
|
789
952
|
if (state.view === "flow") return;
|
|
790
953
|
const w = el.chart.clientWidth || W;
|
|
791
954
|
svg.attr("viewBox", null);
|
|
792
|
-
const g = svg
|
|
793
|
-
|
|
955
|
+
const g = svg
|
|
956
|
+
.selectAll("g.axis")
|
|
957
|
+
.data([0])
|
|
958
|
+
.join("g")
|
|
959
|
+
.attr("class", "axis")
|
|
960
|
+
.attr("transform", "translate(0,33)");
|
|
961
|
+
g.call(
|
|
962
|
+
d3
|
|
963
|
+
.axisTop(x)
|
|
964
|
+
.ticks(Math.max(2, Math.floor(w / 90)))
|
|
965
|
+
.tickFormat(fmtTick)
|
|
966
|
+
.tickSizeOuter(0)
|
|
967
|
+
);
|
|
794
968
|
g.selectAll(".tick text").attr("dy", "-0.5em");
|
|
795
|
-
svg
|
|
969
|
+
svg
|
|
970
|
+
.selectAll("text.cursor-t")
|
|
971
|
+
.data([0])
|
|
972
|
+
.join("text")
|
|
973
|
+
.attr("class", "cursor-t")
|
|
974
|
+
.attr("y", 12)
|
|
975
|
+
.attr("fill", css("--accent"))
|
|
976
|
+
.attr("font-size", 11);
|
|
796
977
|
}
|
|
797
978
|
function moveCursor(px) {
|
|
798
979
|
if (chartSvg) {
|
|
799
|
-
chartSvg
|
|
980
|
+
chartSvg
|
|
981
|
+
.select("line.cursor")
|
|
982
|
+
.attr("visibility", px == null ? "hidden" : "visible")
|
|
983
|
+
.attr("x1", px)
|
|
984
|
+
.attr("x2", px);
|
|
800
985
|
}
|
|
801
986
|
const label = d3.select(el.axis).select("text.cursor-t");
|
|
802
987
|
if (px == null) label.text("");
|
|
@@ -878,13 +1063,25 @@
|
|
|
878
1063
|
for (const l of model.links) {
|
|
879
1064
|
const list = shotsBy.get(l.from) || shotsBy.set(l.from, []).get(l.from);
|
|
880
1065
|
const prev = list[list.length - 1];
|
|
881
|
-
if (
|
|
1066
|
+
if (
|
|
1067
|
+
l.op === "ref" &&
|
|
1068
|
+
prev &&
|
|
1069
|
+
prev.type === "link" &&
|
|
1070
|
+
prev.l.op === "ref" &&
|
|
1071
|
+
prev.l.to === l.to &&
|
|
1072
|
+
prev.l.key === l.key
|
|
1073
|
+
) {
|
|
882
1074
|
prev.n++;
|
|
883
1075
|
continue;
|
|
884
1076
|
}
|
|
885
|
-
|
|
1077
|
+
// Placed by when the wait / call began, and never before its target started. By its end, a
|
|
1078
|
+
// wait ties with the next step's start (the target ends, the next step launches); by its
|
|
1079
|
+
// start alone, it ties with its target's. Clamping to the target, with ties sorting after
|
|
1080
|
+
// a card, puts it right under what it waited on.
|
|
1081
|
+
list.push({ type: "link", l, n: 1, t: l.t, at: Math.max(l.t0, l.to.start), name: l.label });
|
|
886
1082
|
}
|
|
887
|
-
const
|
|
1083
|
+
const at = (x) => x.at ?? x.t ?? Infinity; // where a chip sits in its lane
|
|
1084
|
+
const byTime = (a, b) => at(a) - at(b);
|
|
888
1085
|
const pos = new Map();
|
|
889
1086
|
const lanes = [];
|
|
890
1087
|
const chipRows = [];
|
|
@@ -906,7 +1103,7 @@
|
|
|
906
1103
|
let si = 0;
|
|
907
1104
|
for (const k of kids) {
|
|
908
1105
|
const before = [];
|
|
909
|
-
while (si < shots.length && (shots[si]
|
|
1106
|
+
while (si < shots.length && at(shots[si]) < k.start) before.push(shots[si++]);
|
|
910
1107
|
if (before.length) seq.push({ shots: before });
|
|
911
1108
|
seq.push({ node: k });
|
|
912
1109
|
}
|
|
@@ -1002,7 +1199,10 @@
|
|
|
1002
1199
|
.attr("class", "flow-icon")
|
|
1003
1200
|
.attr("transform", "translate(15,15) scale(0.58)")
|
|
1004
1201
|
.attr("color", (n) => colorOf(n))
|
|
1005
|
-
.html(
|
|
1202
|
+
.html(
|
|
1203
|
+
(n) =>
|
|
1204
|
+
`<path d="${ICON[iconOf(n)]}" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>`
|
|
1205
|
+
);
|
|
1006
1206
|
card.each(function (n) {
|
|
1007
1207
|
const lines = wrapName(boxLabel(n), 31);
|
|
1008
1208
|
const t = d3.select(this);
|
|
@@ -1021,7 +1221,10 @@
|
|
|
1021
1221
|
.text(`${nodeKind(n)} · ${n.status} · ${fmtDur(n.duration)} · +${fmtTick(n.start)}`);
|
|
1022
1222
|
const tags = n.v.tags || [];
|
|
1023
1223
|
if (!tags.length) return;
|
|
1024
|
-
const row = t
|
|
1224
|
+
const row = t
|
|
1225
|
+
.append("g")
|
|
1226
|
+
.attr("class", "tchips")
|
|
1227
|
+
.attr("transform", `translate(42,${lines.length > 1 ? 56 : 51})`);
|
|
1025
1228
|
const room = CW - 42 - 10;
|
|
1026
1229
|
let x = 0;
|
|
1027
1230
|
for (let i = 0; i < tags.length; i++) {
|
|
@@ -1100,7 +1303,11 @@
|
|
|
1100
1303
|
select(x2.l.from.id, "links");
|
|
1101
1304
|
})
|
|
1102
1305
|
.on("mouseenter", (ev, x2) => {
|
|
1103
|
-
showTip(
|
|
1306
|
+
showTip(
|
|
1307
|
+
ev,
|
|
1308
|
+
x2.type === "link" ? linkTip(x2.l, x2.n) : galleryTip(x2),
|
|
1309
|
+
x2.type === "link"
|
|
1310
|
+
);
|
|
1104
1311
|
if (x2.type === "link") drawFlowWires(x2);
|
|
1105
1312
|
})
|
|
1106
1313
|
.on("mousemove", (ev) => moveTip(ev))
|
|
@@ -1109,7 +1316,8 @@
|
|
|
1109
1316
|
if (x2.type === "link") drawFlowWires(null);
|
|
1110
1317
|
});
|
|
1111
1318
|
shown.forEach((x2, i) => {
|
|
1112
|
-
if (x2.type === "link")
|
|
1319
|
+
if (x2.type === "link")
|
|
1320
|
+
linkChips.push({ x2, cx: x + i * (TW + 4) + TW / 2, cy: y + TH / 2 });
|
|
1113
1321
|
});
|
|
1114
1322
|
cell
|
|
1115
1323
|
.append("rect")
|
|
@@ -1132,7 +1340,9 @@
|
|
|
1132
1340
|
.attr("x", TW / 2)
|
|
1133
1341
|
.attr("y", TH / 2 + 3.5)
|
|
1134
1342
|
.attr("text-anchor", "middle")
|
|
1135
|
-
.text((x2) =>
|
|
1343
|
+
.text((x2) =>
|
|
1344
|
+
x2.type === "link" ? linkGlyph(x2) : x2.type === "snapshot" ? "{ }" : "file"
|
|
1345
|
+
);
|
|
1136
1346
|
let end = shown.length * (TW + 4);
|
|
1137
1347
|
if (shots.length > shown.length) {
|
|
1138
1348
|
const more = g
|
|
@@ -1158,7 +1368,9 @@
|
|
|
1158
1368
|
.attr("class", "flow-shot-t")
|
|
1159
1369
|
.attr("x", end + 2)
|
|
1160
1370
|
.attr("y", TH / 2 + 4)
|
|
1161
|
-
.text(
|
|
1371
|
+
.text(
|
|
1372
|
+
`${fmtRel(t)} · ${shots.length > 1 ? chipCount(shots) : truncate(shots[0].name, 22)}`
|
|
1373
|
+
);
|
|
1162
1374
|
}
|
|
1163
1375
|
};
|
|
1164
1376
|
for (const r of chipRows) {
|
|
@@ -1187,7 +1399,9 @@
|
|
|
1187
1399
|
const k = mode === "start" ? Math.max(0.62, Math.min(1, fitK)) : Math.min(1.2, fitK);
|
|
1188
1400
|
const fits = (b.x1 - b.x0) * k <= w && (b.y1 - b.y0) * k <= h;
|
|
1189
1401
|
const t = fits
|
|
1190
|
-
? d3.zoomIdentity
|
|
1402
|
+
? d3.zoomIdentity
|
|
1403
|
+
.translate(w / 2 - (k * (b.x0 + b.x1)) / 2, h / 2 - (k * (b.y0 + b.y1)) / 2)
|
|
1404
|
+
.scale(k)
|
|
1191
1405
|
: d3.zoomIdentity.translate(-k * b.x0, -k * b.y0).scale(k);
|
|
1192
1406
|
(instant ? svg : svg.transition().duration(250)).call(flowZoom.transform, t);
|
|
1193
1407
|
}
|
|
@@ -1216,7 +1430,10 @@
|
|
|
1216
1430
|
.attr("fill", (n) => colorOf(n));
|
|
1217
1431
|
ovBrush = d3
|
|
1218
1432
|
.brushX()
|
|
1219
|
-
.extent([
|
|
1433
|
+
.extent([
|
|
1434
|
+
[0, 0],
|
|
1435
|
+
[w, h],
|
|
1436
|
+
])
|
|
1220
1437
|
.on("start", (ev) => {
|
|
1221
1438
|
if (ev.sourceEvent) brushing = true;
|
|
1222
1439
|
})
|
|
@@ -1243,12 +1460,16 @@
|
|
|
1243
1460
|
if (tab) state.detailOpen.add(tab);
|
|
1244
1461
|
state.details = true;
|
|
1245
1462
|
el.main.classList.remove("no-details");
|
|
1246
|
-
el.tree
|
|
1463
|
+
el.tree
|
|
1464
|
+
.querySelectorAll(".row")
|
|
1465
|
+
.forEach((r) => r.classList.toggle("selected", r.dataset.id === id));
|
|
1247
1466
|
if (chartSvg) {
|
|
1248
1467
|
chartSvg.selectAll("rect.bar").classed("selected", (n) => n.id === id);
|
|
1249
1468
|
chartSvg.selectAll("g.r").classed("sel", (n) => n.id === id);
|
|
1250
1469
|
}
|
|
1251
|
-
d3.select(el.flow)
|
|
1470
|
+
d3.select(el.flow)
|
|
1471
|
+
.selectAll("g.flow-card")
|
|
1472
|
+
.classed("selected", (n) => n.id === id);
|
|
1252
1473
|
renderDetails();
|
|
1253
1474
|
}
|
|
1254
1475
|
function focusNode(n) {
|
|
@@ -1315,7 +1536,9 @@
|
|
|
1315
1536
|
const p = flowPos.get(n.id);
|
|
1316
1537
|
if (svg.empty() || !p) return;
|
|
1317
1538
|
const k = Math.max(0.8, d3.zoomTransform(svg.node()).k);
|
|
1318
|
-
const t = d3.zoomIdentity
|
|
1539
|
+
const t = d3.zoomIdentity
|
|
1540
|
+
.translate(el.flow.clientWidth / 2 - k * p.x, el.flow.clientHeight / 2 - k * p.y)
|
|
1541
|
+
.scale(k);
|
|
1319
1542
|
svg.transition().duration(300).call(flowZoom.transform, t);
|
|
1320
1543
|
}
|
|
1321
1544
|
|
|
@@ -1338,12 +1561,16 @@
|
|
|
1338
1561
|
${n.v.tags.length ? `<div class="k">Tags</div><div class="v">${chipsHtml(n.v.tags, Infinity, 40)}</div>` : ""}
|
|
1339
1562
|
${n.v.mcp ? `<div class="k">MCP</div><div class="v"><span class="badge-inline">${esc(n.v.mcp.mode)}</span></div>` : ""}
|
|
1340
1563
|
<div class="k">Id</div><div class="v mono-sm">${esc(n.id)}</div>
|
|
1341
|
-
${
|
|
1564
|
+
${
|
|
1565
|
+
userSite(n)
|
|
1566
|
+
? `<div class="k">${siteLabel(n)}</div><div class="v defined">
|
|
1342
1567
|
<button class="icon-btn" data-copy-source aria-label="Copy relative path">${svgIcon("copy")}</button>
|
|
1343
1568
|
${hasPopup(n) ? `<button class="icon-btn" data-show-code aria-label="Show source">${svgIcon("code")}</button>` : ""}
|
|
1344
1569
|
<a class="icon-btn" href="${esc(editorUrl(userSite(n)))}" aria-label="Open in VS Code" data-tip-text="Open in VS Code">${svgIcon("external")}</a>
|
|
1345
1570
|
<span class="defined-name" >${esc(basename(sourceOf(n)))}</span>
|
|
1346
|
-
</div>`
|
|
1571
|
+
</div>`
|
|
1572
|
+
: ""
|
|
1573
|
+
}
|
|
1347
1574
|
</div>
|
|
1348
1575
|
<div class="acc">${detailSections(n)
|
|
1349
1576
|
.map((sec) =>
|
|
@@ -1360,7 +1587,10 @@
|
|
|
1360
1587
|
if (copySrc) {
|
|
1361
1588
|
copySrc.addEventListener("click", () => copySource(n));
|
|
1362
1589
|
copySrc.addEventListener("mouseenter", (ev) =>
|
|
1363
|
-
showTip(
|
|
1590
|
+
showTip(
|
|
1591
|
+
ev,
|
|
1592
|
+
`<div class="mono">${esc(sourceOf(n))}</div><div class="hint">Click (or press <kbd>C</kbd>) to copy</div>`
|
|
1593
|
+
)
|
|
1364
1594
|
);
|
|
1365
1595
|
copySrc.addEventListener("mousemove", (ev) => moveTip(ev));
|
|
1366
1596
|
copySrc.addEventListener("mouseleave", hideTip);
|
|
@@ -1421,9 +1651,16 @@
|
|
|
1421
1651
|
empty: !hasArgs(n) && !params.length,
|
|
1422
1652
|
body: () =>
|
|
1423
1653
|
(params.length ? `<div class="params">${params.map(paramHtml).join("")}</div>` : "") +
|
|
1424
|
-
(hasArgs(n)
|
|
1654
|
+
(hasArgs(n)
|
|
1655
|
+
? `<div class="code-box">${argsHtml(n)}</div>`
|
|
1656
|
+
: '<div class="muted">No arguments.</div>'),
|
|
1657
|
+
},
|
|
1658
|
+
{
|
|
1659
|
+
key: "value",
|
|
1660
|
+
title: "Returned",
|
|
1661
|
+
empty: !hasValue(n),
|
|
1662
|
+
body: () => `<div class="code-box">${valueHtml(n)}</div>`,
|
|
1425
1663
|
},
|
|
1426
|
-
{ key: "value", title: "Returned", empty: !hasValue(n), body: () => `<div class="code-box">${valueHtml(n)}</div>` },
|
|
1427
1664
|
{
|
|
1428
1665
|
key: "snapshots",
|
|
1429
1666
|
title: "Snapshots & files",
|
|
@@ -1529,7 +1766,8 @@
|
|
|
1529
1766
|
|
|
1530
1767
|
function thumbHtml(g) {
|
|
1531
1768
|
const i = model.gallery.indexOf(g);
|
|
1532
|
-
if (g.image)
|
|
1769
|
+
if (g.image)
|
|
1770
|
+
return `<div class="thumb" data-gallery="${i}" title="${esc(g.name)}"><img alt="${esc(g.name)}" data-asset="${esc(g.f.asset)}"></div>`;
|
|
1533
1771
|
const label = g.type === "snapshot" ? "{ }" : g.f.mime.split("/")[1] || "file";
|
|
1534
1772
|
return `<div class="thumb" data-gallery="${i}" title="${esc(g.name)}"><span class="json-ico">${esc(label)}</span></div>`;
|
|
1535
1773
|
}
|
|
@@ -1538,15 +1776,17 @@
|
|
|
1538
1776
|
root.querySelectorAll("img[data-asset]").forEach((img) => {
|
|
1539
1777
|
if (!img.src) img.src = model.asset(img.dataset.asset);
|
|
1540
1778
|
});
|
|
1541
|
-
root
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
t.addEventListener("click", () =>
|
|
1549
|
-
|
|
1779
|
+
root
|
|
1780
|
+
.querySelectorAll("[data-gallery]")
|
|
1781
|
+
.forEach((t) =>
|
|
1782
|
+
t.addEventListener("click", () => openGallery(model.gallery[+t.dataset.gallery]))
|
|
1783
|
+
);
|
|
1784
|
+
root
|
|
1785
|
+
.querySelectorAll("[data-link]")
|
|
1786
|
+
.forEach((t) => t.addEventListener("click", () => setPin(model.links[+t.dataset.link].t)));
|
|
1787
|
+
root
|
|
1788
|
+
.querySelectorAll("[data-item]")
|
|
1789
|
+
.forEach((t) => t.addEventListener("click", () => openItem(model.items[+t.dataset.item])));
|
|
1550
1790
|
}
|
|
1551
1791
|
|
|
1552
1792
|
function openItem(item) {
|
|
@@ -1561,7 +1801,6 @@
|
|
|
1561
1801
|
redrawX();
|
|
1562
1802
|
}
|
|
1563
1803
|
|
|
1564
|
-
|
|
1565
1804
|
// ── errors: the tab, and highlighting them in the graphs ──
|
|
1566
1805
|
const ERR_TYPES = [
|
|
1567
1806
|
["failure", "Failures", "x"],
|
|
@@ -1580,11 +1819,15 @@
|
|
|
1580
1819
|
);
|
|
1581
1820
|
}
|
|
1582
1821
|
function errorsHtml() {
|
|
1583
|
-
if (!model.errors.length)
|
|
1822
|
+
if (!model.errors.length)
|
|
1823
|
+
return '<div class="muted">No errors: nothing failed, no assertion failed, and nothing wrote error output.</div>';
|
|
1584
1824
|
const count = (t) => model.errors.filter((e) => t === "all" || e.type === t).length;
|
|
1585
1825
|
const seg = [["all", "All"], ...ERR_TYPES.map(([k, l]) => [k, l])]
|
|
1586
1826
|
.filter(([k]) => k === "all" || count(k))
|
|
1587
|
-
.map(
|
|
1827
|
+
.map(
|
|
1828
|
+
([k, l]) =>
|
|
1829
|
+
`<button class="seg ${state.errFilter === k ? "active" : ""}" data-err-filter="${k}">${l} <span class="muted">${count(k)}</span></button>`
|
|
1830
|
+
)
|
|
1588
1831
|
.join("");
|
|
1589
1832
|
const shown = errorsShown();
|
|
1590
1833
|
const sections = ERR_TYPES.map(([type, label, icon]) => {
|
|
@@ -1599,8 +1842,14 @@
|
|
|
1599
1842
|
function errEntryHtml(e) {
|
|
1600
1843
|
const open = state.errOpen.has(e.idx);
|
|
1601
1844
|
const who = e.node ? displayName(e.node) : "run";
|
|
1602
|
-
const extra =
|
|
1603
|
-
|
|
1845
|
+
const extra =
|
|
1846
|
+
e.type === "output" && e.lines.length > 1
|
|
1847
|
+
? `<span class="err-n">${e.lines.length} lines</span>`
|
|
1848
|
+
: "";
|
|
1849
|
+
const src =
|
|
1850
|
+
e.type === "output" && e.source
|
|
1851
|
+
? `<span class="err-src">${esc(truncate(e.source, 40))}</span>`
|
|
1852
|
+
: "";
|
|
1604
1853
|
return `<div class="err-entry ${e.type}${open ? " open" : ""}" data-err="${e.idx}">
|
|
1605
1854
|
<div class="err-row">
|
|
1606
1855
|
<span class="twist ${open ? "open" : ""}">${svgIcon("chevron")}</span>
|
|
@@ -1642,9 +1891,13 @@
|
|
|
1642
1891
|
b.addEventListener("mouseenter", (ev) => showTip(ev, "Show in graph"));
|
|
1643
1892
|
b.addEventListener("mouseleave", hideTip);
|
|
1644
1893
|
});
|
|
1645
|
-
body
|
|
1646
|
-
|
|
1647
|
-
|
|
1894
|
+
body
|
|
1895
|
+
.querySelectorAll("[data-err-copy]")
|
|
1896
|
+
.forEach((b) =>
|
|
1897
|
+
b.addEventListener("click", () =>
|
|
1898
|
+
copy(model.errors[+b.dataset.errCopy].text, "Copied the error")
|
|
1899
|
+
)
|
|
1900
|
+
);
|
|
1648
1901
|
}
|
|
1649
1902
|
function refreshErrors() {
|
|
1650
1903
|
if (state.lowerTab !== "errors") return;
|
|
@@ -1683,7 +1936,9 @@
|
|
|
1683
1936
|
const path = only ? pathOf(only) : model.errPath;
|
|
1684
1937
|
app.classList.toggle("hl-on", !!set);
|
|
1685
1938
|
const mark = (sel, idOf) =>
|
|
1686
|
-
sel
|
|
1939
|
+
sel
|
|
1940
|
+
.classed("err", (d) => !!set && set.has(idOf(d)))
|
|
1941
|
+
.classed("err-path", (d) => !!set && path.has(idOf(d)));
|
|
1687
1942
|
if (chartSvg) mark(chartSvg.selectAll("g.r"), (n) => n.id);
|
|
1688
1943
|
mark(d3.select(el.flow).selectAll("g.flow-card"), (n) => n.id);
|
|
1689
1944
|
el.tree.querySelectorAll(".row").forEach((r) => {
|
|
@@ -1702,14 +1957,22 @@
|
|
|
1702
1957
|
function renderLower() {
|
|
1703
1958
|
const tabs = [
|
|
1704
1959
|
["snapshots", `Snapshots (${model.gallery.length})`],
|
|
1705
|
-
[
|
|
1960
|
+
[
|
|
1961
|
+
"errors",
|
|
1962
|
+
`Errors <span class="tab-count${seriousCount() ? " bad" : model.errors.length ? " warn" : ""}">${model.errors.length}</span>`,
|
|
1963
|
+
],
|
|
1706
1964
|
["logs", "Logs"],
|
|
1707
1965
|
["asserts", `Assertions (${model.asserts.length})`],
|
|
1708
1966
|
["events", "Timeline events"],
|
|
1709
1967
|
];
|
|
1710
1968
|
el.lower.innerHTML = `<div class="tabs">${tabs
|
|
1711
|
-
.map(
|
|
1712
|
-
|
|
1969
|
+
.map(
|
|
1970
|
+
([k, l]) =>
|
|
1971
|
+
`<button class="tab ${state.lowerTab === k ? "active" : ""}" data-tab="${k}">${l}</button>`
|
|
1972
|
+
)
|
|
1973
|
+
.join(
|
|
1974
|
+
""
|
|
1975
|
+
)}<span class="spacer"></span><input class="filter" placeholder="Filter…" value="${esc(state.filter)}"></div>
|
|
1713
1976
|
<div class="body">${lowerBodyHtml()}</div>`;
|
|
1714
1977
|
el.lower.querySelectorAll(".tab").forEach((b) =>
|
|
1715
1978
|
b.addEventListener("click", () => {
|
|
@@ -1729,32 +1992,50 @@
|
|
|
1729
1992
|
const body = el.lower.querySelector(".body");
|
|
1730
1993
|
if (state.lowerTab === "errors") wireErrors(body);
|
|
1731
1994
|
wireItems(body);
|
|
1732
|
-
body
|
|
1733
|
-
|
|
1734
|
-
|
|
1995
|
+
body
|
|
1996
|
+
.querySelectorAll("tr[data-row]")
|
|
1997
|
+
.forEach((tr) =>
|
|
1998
|
+
tr.addEventListener("click", () => openItem(model.items[+tr.dataset.row]))
|
|
1999
|
+
);
|
|
1735
2000
|
body.querySelectorAll("[data-locate]").forEach((b) => {
|
|
1736
2001
|
b.addEventListener("click", (ev) => {
|
|
1737
2002
|
ev.stopPropagation();
|
|
1738
2003
|
locate(model.gallery[+b.dataset.locate]);
|
|
1739
2004
|
});
|
|
1740
|
-
b.addEventListener("mouseenter", (ev) =>
|
|
2005
|
+
b.addEventListener("mouseenter", (ev) =>
|
|
2006
|
+
showTip(ev, `Show in the ${state.view === "flow" ? "flow" : "graph"}`)
|
|
2007
|
+
);
|
|
1741
2008
|
b.addEventListener("mouseleave", hideTip);
|
|
1742
2009
|
});
|
|
1743
2010
|
}
|
|
1744
2011
|
function matches(...parts) {
|
|
1745
2012
|
const q = state.filter.trim().toLowerCase();
|
|
1746
|
-
return
|
|
2013
|
+
return (
|
|
2014
|
+
!q ||
|
|
2015
|
+
parts.some((p) =>
|
|
2016
|
+
String(p || "")
|
|
2017
|
+
.toLowerCase()
|
|
2018
|
+
.includes(q)
|
|
2019
|
+
)
|
|
2020
|
+
);
|
|
1747
2021
|
}
|
|
1748
2022
|
function lowerBodyHtml() {
|
|
1749
2023
|
const tab = state.lowerTab;
|
|
1750
2024
|
if (tab === "errors") return errorsHtml();
|
|
1751
2025
|
if (tab === "snapshots") {
|
|
1752
2026
|
const g = model.gallery.filter((x) => matches(x.name, x.node && x.node.v.path));
|
|
1753
|
-
if (!g.length)
|
|
2027
|
+
if (!g.length)
|
|
2028
|
+
return '<div class="muted">No snapshots, artifacts, or images in this run.</div>';
|
|
1754
2029
|
return `<div class="gallery">${g
|
|
1755
2030
|
.map((x) => {
|
|
1756
|
-
const sub =
|
|
1757
|
-
|
|
2031
|
+
const sub =
|
|
2032
|
+
x.type === "snapshot"
|
|
2033
|
+
? `snapshot · ${x.node ? esc(displayName(x.node)) : ""}`
|
|
2034
|
+
: `${esc(x.f.mime)} · ${fmtSize(x.f.size)}${x.f.omitted ? " · not embedded" : ""}`;
|
|
2035
|
+
const at =
|
|
2036
|
+
x.t != null || x.node
|
|
2037
|
+
? `<button class="icon-btn locate" data-locate="${model.gallery.indexOf(x)}" aria-label="Show in graph">${svgIcon("locate")}</button>`
|
|
2038
|
+
: "";
|
|
1758
2039
|
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>`;
|
|
1759
2040
|
})
|
|
1760
2041
|
.join("")}</div>`;
|
|
@@ -1773,7 +2054,8 @@
|
|
|
1773
2054
|
rows.push({ i, idx });
|
|
1774
2055
|
if (rows.length >= 3000) break;
|
|
1775
2056
|
}
|
|
1776
|
-
if (!rows.length)
|
|
2057
|
+
if (!rows.length)
|
|
2058
|
+
return `<div class="muted">${tab === "asserts" ? "No assertions in this run." : "Nothing matches."}</div>`;
|
|
1777
2059
|
return `<table class="list"><tbody>${rows
|
|
1778
2060
|
.map(({ i, idx }) => {
|
|
1779
2061
|
const mk = MARK[i.kind];
|
|
@@ -1782,7 +2064,9 @@
|
|
|
1782
2064
|
: "";
|
|
1783
2065
|
return `<tr data-row="${idx}" class="${i.kind === "error" || i.kind === "fail" ? "err" : ""}"><td class="t">${fmtTick(i.t)}</td><td>${ico}</td><td class="p" title="${esc(i.e.path)}">${esc(i.node ? displayName(i.node) : i.e.path)}</td><td class="m">${esc(i.e.message)}</td></tr>`;
|
|
1784
2066
|
})
|
|
1785
|
-
.join(
|
|
2067
|
+
.join(
|
|
2068
|
+
""
|
|
2069
|
+
)}</tbody></table>${rows.length >= 3000 ? '<div class="muted">First 3000 rows — narrow with the filter.</div>' : ""}`;
|
|
1786
2070
|
}
|
|
1787
2071
|
|
|
1788
2072
|
// ── modal ──
|
|
@@ -1797,11 +2081,18 @@
|
|
|
1797
2081
|
body = `<img alt="${esc(g.name)}" src="${src}">`;
|
|
1798
2082
|
actions = `<a class="btn solo" download="${esc(basename(g.f.file))}" href="${src}">${svgIcon("download")}Download</a>`;
|
|
1799
2083
|
} else if (g.type === "file") {
|
|
1800
|
-
body =
|
|
2084
|
+
body =
|
|
2085
|
+
g.f.text != null
|
|
2086
|
+
? `<pre class="code">${esc(g.f.text)}</pre>`
|
|
2087
|
+
: `<div class="muted">Not embedded (${esc(g.f.omitted || "binary")}). Open it from the run folder.</div>`;
|
|
1801
2088
|
} else {
|
|
1802
2089
|
body = `<pre class="code">${esc(g.s.value !== undefined ? json(g.s.value) : g.s.text)}</pre>${g.s.truncated ? '<div class="muted">Truncated — the snapshot file holds the rest.</div>' : ""}`;
|
|
1803
2090
|
}
|
|
1804
|
-
const sub = [
|
|
2091
|
+
const sub = [
|
|
2092
|
+
g.t != null ? fmtRel(g.t) : null,
|
|
2093
|
+
g.node ? g.node.v.path : null,
|
|
2094
|
+
g.type === "file" ? fmtSize(g.f.size) : "snapshot",
|
|
2095
|
+
]
|
|
1805
2096
|
.filter(Boolean)
|
|
1806
2097
|
.join(" · ");
|
|
1807
2098
|
const m = document.createElement("div");
|
|
@@ -1850,14 +2141,20 @@
|
|
|
1850
2141
|
renderDetails();
|
|
1851
2142
|
renderLower();
|
|
1852
2143
|
}
|
|
1853
|
-
if (a === "data")
|
|
2144
|
+
if (a === "data")
|
|
2145
|
+
download(
|
|
2146
|
+
`${model.data.name}-run.json`,
|
|
2147
|
+
JSON.stringify(model.data, null, 2),
|
|
2148
|
+
"application/json"
|
|
2149
|
+
);
|
|
1854
2150
|
if (a === "folder") copy(model.data.folder);
|
|
1855
2151
|
if (a === "expand") {
|
|
1856
2152
|
state.collapsed.clear();
|
|
1857
2153
|
renderAll();
|
|
1858
2154
|
}
|
|
1859
2155
|
if (a === "collapse") {
|
|
1860
|
-
for (const n of model.nodes)
|
|
2156
|
+
for (const n of model.nodes)
|
|
2157
|
+
if (n.depth >= 1 && n.children.length) state.collapsed.add(n.id);
|
|
1861
2158
|
renderAll();
|
|
1862
2159
|
}
|
|
1863
2160
|
});
|
|
@@ -1892,7 +2189,9 @@
|
|
|
1892
2189
|
n.depth === 0 ? "om" : v.kind === "step" ? "step" : "",
|
|
1893
2190
|
v.mcp ? `<span class="mcp">MCP · ${esc(v.mcp.mode)}</span>` : "",
|
|
1894
2191
|
].filter(Boolean);
|
|
1895
|
-
const fails = model.run.failures.filter(
|
|
2192
|
+
const fails = model.run.failures.filter(
|
|
2193
|
+
(f) => f.action === n.id || f.action === n.name || f.action === v.path
|
|
2194
|
+
);
|
|
1896
2195
|
return `<div class="tt">${esc(shown)}</div>
|
|
1897
2196
|
${call ? `<div class="tcall">${callHtml(n, call)}</div>` : ""}
|
|
1898
2197
|
<div class="tmeta">${meta.join('<span class="sep">·</span>')}</div>
|
|
@@ -1902,9 +2201,10 @@
|
|
|
1902
2201
|
}
|
|
1903
2202
|
function galleryTip(x) {
|
|
1904
2203
|
const img = x.image ? `<img src="${model.asset(x.f.asset)}" alt="">` : "";
|
|
1905
|
-
const body =
|
|
1906
|
-
|
|
1907
|
-
|
|
2204
|
+
const body =
|
|
2205
|
+
x.type === "snapshot"
|
|
2206
|
+
? `<div class="mono small">${esc(truncate(x.s.value !== undefined ? oneLine(x.s.value) : x.s.text || "", 200))}</div>`
|
|
2207
|
+
: `<div class="ts">${esc(x.f.mime)} · ${fmtSize(x.f.size)}</div>`;
|
|
1908
2208
|
return `<div class="tt">${esc(x.name)}${x.t != null ? ` · ${fmtRel(x.t)}` : ""}</div>${img}${body}`;
|
|
1909
2209
|
}
|
|
1910
2210
|
/** The om itself: its node tooltip, plus where the run's record lives. */
|
|
@@ -1914,12 +2214,23 @@
|
|
|
1914
2214
|
function markTip(d) {
|
|
1915
2215
|
const m = d.m;
|
|
1916
2216
|
if (m.kind === "link") {
|
|
1917
|
-
return
|
|
2217
|
+
return (
|
|
2218
|
+
linkTip(m.link) +
|
|
2219
|
+
(d.more.length
|
|
2220
|
+
? `<div class="ts" style="margin-top:4px">+${d.more.length} more within a few ms</div>`
|
|
2221
|
+
: "")
|
|
2222
|
+
);
|
|
1918
2223
|
}
|
|
1919
2224
|
const mk = MARK[m.kind] || MARK.event;
|
|
1920
|
-
const img =
|
|
1921
|
-
|
|
1922
|
-
const
|
|
2225
|
+
const img =
|
|
2226
|
+
m.gallery && m.gallery.image ? `<img src="${model.asset(m.gallery.f.asset)}" alt="">` : "";
|
|
2227
|
+
const more = d.more.length
|
|
2228
|
+
? `<div class="ts">+${d.more.length} more within a few ms</div>`
|
|
2229
|
+
: "";
|
|
2230
|
+
const extra =
|
|
2231
|
+
m.gallery && m.gallery.image
|
|
2232
|
+
? `<div class="ts">${esc(m.gallery.f.rel)} · ${fmtSize(m.gallery.f.size)}</div>`
|
|
2233
|
+
: "";
|
|
1923
2234
|
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}`;
|
|
1924
2235
|
}
|
|
1925
2236
|
// ── source popover (the <> buttons) — click-toggled, like the tskb explorer's ──
|
|
@@ -1964,7 +2275,9 @@
|
|
|
1964
2275
|
const line = lines[at - c.start];
|
|
1965
2276
|
if (line == null) return null;
|
|
1966
2277
|
let call = null;
|
|
1967
|
-
for (const m of line.matchAll(
|
|
2278
|
+
for (const m of line.matchAll(
|
|
2279
|
+
/(?:^|[^\w$.])([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\s*\(/g
|
|
2280
|
+
)) {
|
|
1968
2281
|
if (!/^(if|for|while|switch|return|catch|function|await|typeof|new|async)$/.test(m[1])) {
|
|
1969
2282
|
call = { callee: m[1], index: m.index };
|
|
1970
2283
|
break;
|
|
@@ -1977,7 +2290,9 @@
|
|
|
1977
2290
|
const m =
|
|
1978
2291
|
DECL.exec(prefix) ||
|
|
1979
2292
|
/^\s*([\w$.]+)\s*=(?!=)/.exec(prefix) ||
|
|
1980
|
-
(/^\s*(?:await\s+)?$/.test(prefix) && /=\s*(?:await\s*)?$/.test(prev)
|
|
2293
|
+
(/^\s*(?:await\s+)?$/.test(prefix) && /=\s*(?:await\s*)?$/.test(prev)
|
|
2294
|
+
? DECL.exec(prev)
|
|
2295
|
+
: null);
|
|
1981
2296
|
return { callee: call.callee, bind: m ? m[1].replace(/\s+/g, " ") : "" };
|
|
1982
2297
|
}
|
|
1983
2298
|
/** The popover's tabs for `n`: what it is (prompt, source), then what went in and came out. */
|
|
@@ -1985,9 +2300,19 @@
|
|
|
1985
2300
|
const tabs = [];
|
|
1986
2301
|
if (promptOf(n)) tabs.push({ key: "prompt", label: "Prompt" });
|
|
1987
2302
|
const mine = userSite(n);
|
|
1988
|
-
if (codeOf(mine))
|
|
2303
|
+
if (codeOf(mine))
|
|
2304
|
+
tabs.push({
|
|
2305
|
+
key: "user",
|
|
2306
|
+
label: siteLabel(n) === "Called at" ? "Call site" : "Source",
|
|
2307
|
+
site: mine,
|
|
2308
|
+
});
|
|
1989
2309
|
const def = n.v.definedAt;
|
|
1990
|
-
if (def && def !== mine && codeOf(def))
|
|
2310
|
+
if (def && def !== mine && codeOf(def))
|
|
2311
|
+
tabs.push({
|
|
2312
|
+
key: "def",
|
|
2313
|
+
label: isLib(def) ? "Definition (omkit)" : "Definition",
|
|
2314
|
+
site: def,
|
|
2315
|
+
});
|
|
1991
2316
|
if (hasArgs(n)) tabs.push({ key: "args", label: "Args" });
|
|
1992
2317
|
if (hasValue(n)) tabs.push({ key: "value", label: "Returned" });
|
|
1993
2318
|
return tabs;
|
|
@@ -2015,7 +2340,8 @@
|
|
|
2015
2340
|
function labelHtml(n) {
|
|
2016
2341
|
const c = callOf(n);
|
|
2017
2342
|
if (!c) return esc(displayName(n));
|
|
2018
|
-
const name =
|
|
2343
|
+
const name =
|
|
2344
|
+
c.callee === displayName(n) ? "" : `<span class="sep"> · </span>${esc(displayName(n))}`;
|
|
2019
2345
|
return `<span class="tcall">${callHtml(n, c)}</span>${name}`;
|
|
2020
2346
|
}
|
|
2021
2347
|
/** `page = chromePage(…)`, from {@link callExpr}. */
|
|
@@ -2026,7 +2352,9 @@
|
|
|
2026
2352
|
return codeTabs(n).length > 0;
|
|
2027
2353
|
}
|
|
2028
2354
|
function popupTip(n) {
|
|
2029
|
-
return codeTabs(n)
|
|
2355
|
+
return codeTabs(n)
|
|
2356
|
+
.map((t) => esc(t.label))
|
|
2357
|
+
.join(" · ");
|
|
2030
2358
|
}
|
|
2031
2359
|
function hasArgs(n) {
|
|
2032
2360
|
return !!(n.v.args && n.v.args.length);
|
|
@@ -2035,7 +2363,9 @@
|
|
|
2035
2363
|
return n.v.args.length === 1 ? n.v.args[0] : n.v.args;
|
|
2036
2364
|
}
|
|
2037
2365
|
function relOf(site) {
|
|
2038
|
-
return (
|
|
2366
|
+
return (
|
|
2367
|
+
(model.data.sources && model.data.sources[site]) || String(site).replace(/:\d+(:\d+)?$/, "")
|
|
2368
|
+
);
|
|
2039
2369
|
}
|
|
2040
2370
|
function hasValue(n) {
|
|
2041
2371
|
return n.v.value !== undefined || !!n.v.valueNote;
|
|
@@ -2044,11 +2374,20 @@
|
|
|
2044
2374
|
function jsonHtml(value, what) {
|
|
2045
2375
|
const text = json(value);
|
|
2046
2376
|
const lines = highlight(text);
|
|
2047
|
-
const kind = Array.isArray(value)
|
|
2377
|
+
const kind = Array.isArray(value)
|
|
2378
|
+
? `array · ${value.length} items`
|
|
2379
|
+
: value === null
|
|
2380
|
+
? "null"
|
|
2381
|
+
: typeof value === "object"
|
|
2382
|
+
? `object · ${Object.keys(value).length} keys`
|
|
2383
|
+
: typeof value;
|
|
2048
2384
|
return `<div class="cp-tools"><span class="muted">${esc(kind)} · ${lines.length} line${lines.length > 1 ? "s" : ""} · ${fmtSize(text.length)}</span>
|
|
2049
2385
|
<button class="btn solo small" data-a="copy-json" data-what="${what}">${svgIcon("copy")}Copy JSON</button></div>
|
|
2050
2386
|
<div class="cp-code">${lines
|
|
2051
|
-
.map(
|
|
2387
|
+
.map(
|
|
2388
|
+
(html, i) =>
|
|
2389
|
+
`<div class="cl"><span class="ln">${i + 1}</span><span class="lc">${html || " "}</span></div>`
|
|
2390
|
+
)
|
|
2052
2391
|
.join("")}</div>`;
|
|
2053
2392
|
}
|
|
2054
2393
|
function valueHtml(n) {
|
|
@@ -2058,34 +2397,42 @@
|
|
|
2058
2397
|
return jsonHtml(n.v.value, "value");
|
|
2059
2398
|
}
|
|
2060
2399
|
function argsHtml(n) {
|
|
2061
|
-
return hasArgs(n)
|
|
2400
|
+
return hasArgs(n)
|
|
2401
|
+
? jsonHtml(argsOf(n), "args")
|
|
2402
|
+
: '<div class="pp"><div class="muted">No arguments.</div></div>';
|
|
2062
2403
|
}
|
|
2063
2404
|
function wireValue(root, n) {
|
|
2064
|
-
root
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2405
|
+
root
|
|
2406
|
+
.querySelectorAll('[data-a="copy-json"]')
|
|
2407
|
+
.forEach((b) =>
|
|
2408
|
+
b.addEventListener("click", () =>
|
|
2409
|
+
b.dataset.what === "args"
|
|
2410
|
+
? copy(json(argsOf(n)), "Copied the args")
|
|
2411
|
+
: copy(json(n.v.value), "Copied the returned value")
|
|
2412
|
+
)
|
|
2413
|
+
);
|
|
2069
2414
|
}
|
|
2070
2415
|
function codeHtml(site) {
|
|
2071
2416
|
const c = codeOf(site);
|
|
2072
2417
|
const line = Number((/:(\d+)(?::\d+)?$/.exec(site) || [])[1]);
|
|
2073
2418
|
const lines = highlight(c.text).map(
|
|
2074
|
-
(html, i) =>
|
|
2419
|
+
(html, i) =>
|
|
2420
|
+
`<div class="cl${c.start + i === line ? " at" : ""}"><span class="ln">${c.start + i}</span><span class="lc">${html || " "}</span></div>`
|
|
2075
2421
|
);
|
|
2076
2422
|
return `<div class="cp-code">${lines.join("")}</div>${c.truncated ? '<div class="muted cp-more">Cut short — open the file for the rest.</div>' : ""}`;
|
|
2077
2423
|
}
|
|
2078
2424
|
function promptHtml(p) {
|
|
2079
|
-
const choices =
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2425
|
+
const choices =
|
|
2426
|
+
p.choices && p.choices.length
|
|
2427
|
+
? `<div class="pp-k">Choices</div><div class="pp-v"><ol class="pp-choices">${p.choices
|
|
2428
|
+
.map((c) => {
|
|
2429
|
+
const value = typeof c === "string" ? c : c.value;
|
|
2430
|
+
const label = typeof c === "string" ? c : c.label;
|
|
2431
|
+
const picked = p.answer && p.answer.value === value;
|
|
2432
|
+
return `<li class="${picked ? "picked" : ""}">${esc(label)}${label !== value ? ` <span class="muted">(${esc(value)})</span>` : ""}</li>`;
|
|
2433
|
+
})
|
|
2434
|
+
.join("")}</ol></div>`
|
|
2435
|
+
: "";
|
|
2089
2436
|
const answer = p.answer
|
|
2090
2437
|
? `<div class="pp-answer"><span class="pp-k">Answer</span><pre class="code">${esc(typeof p.answer.value === "string" ? p.answer.value : json(p.answer.value))}</pre>${
|
|
2091
2438
|
p.answer.via ? `<span class="badge-inline">via ${esc(p.answer.via)}</span>` : ""
|
|
@@ -2108,7 +2455,10 @@
|
|
|
2108
2455
|
if (codeFor === n.id && (!want || (toggle && codeTab === want))) return closeCode();
|
|
2109
2456
|
const tabs = codeTabs(n);
|
|
2110
2457
|
if (!tabs.length) return;
|
|
2111
|
-
const tab =
|
|
2458
|
+
const tab =
|
|
2459
|
+
tabs.find((t) => t.key === want) ||
|
|
2460
|
+
tabs.find((t) => t.key !== "value" && t.key !== "args") ||
|
|
2461
|
+
tabs[0];
|
|
2112
2462
|
codeTab = tab.key;
|
|
2113
2463
|
hideTip();
|
|
2114
2464
|
const reopen = codeFor !== n.id;
|
|
@@ -2117,21 +2467,33 @@
|
|
|
2117
2467
|
const rel = site ? relOf(site) : "";
|
|
2118
2468
|
const line = site ? (/:(\d+)(?::\d+)?$/.exec(site) || [])[1] : "";
|
|
2119
2469
|
codeEl.innerHTML = `<div class="cp-head">
|
|
2120
|
-
${
|
|
2470
|
+
${
|
|
2471
|
+
site
|
|
2472
|
+
? `<span class="cp-path" title="${esc(site)}">${esc(rel)}<span class="muted">${line ? ":" + line : ""}</span></span>
|
|
2121
2473
|
<button class="icon-btn" data-a="copy" aria-label="Copy path">${svgIcon("copy")}</button>
|
|
2122
|
-
<a class="icon-btn" href="${esc(editorUrl(site))}" aria-label="Open in VS Code">${svgIcon("external")}</a>`
|
|
2474
|
+
<a class="icon-btn" href="${esc(editorUrl(site))}" aria-label="Open in VS Code">${svgIcon("external")}</a>`
|
|
2475
|
+
: ""
|
|
2476
|
+
}
|
|
2123
2477
|
<span class="cp-name">${esc(displayName(n))}</span>
|
|
2124
2478
|
<button class="icon-btn" data-a="close" aria-label="Close">${svgIcon("close")}</button>
|
|
2125
2479
|
</div>
|
|
2126
2480
|
${tabs.length > 1 ? `<div class="cp-tabs">${tabs.map((t) => `<button class="tab ${t === tab ? "active" : ""}" data-tab="${t.key}">${esc(t.label)}</button>`).join("")}</div>` : ""}
|
|
2127
2481
|
<div class="cp-body">${
|
|
2128
|
-
tab.key === "prompt"
|
|
2482
|
+
tab.key === "prompt"
|
|
2483
|
+
? promptHtml(promptOf(n))
|
|
2484
|
+
: tab.key === "value"
|
|
2485
|
+
? valueHtml(n)
|
|
2486
|
+
: tab.key === "args"
|
|
2487
|
+
? argsHtml(n)
|
|
2488
|
+
: codeHtml(site)
|
|
2129
2489
|
}</div>`;
|
|
2130
2490
|
wireValue(codeEl, n);
|
|
2131
2491
|
const cp = codeEl.querySelector('[data-a="copy"]');
|
|
2132
2492
|
if (cp) cp.addEventListener("click", () => copy(rel, `Copied ${rel}`));
|
|
2133
2493
|
codeEl.querySelector('[data-a="close"]').addEventListener("click", closeCode);
|
|
2134
|
-
codeEl
|
|
2494
|
+
codeEl
|
|
2495
|
+
.querySelectorAll(".cp-tabs .tab")
|
|
2496
|
+
.forEach((b) => b.addEventListener("click", () => toggleCode(n, null, b.dataset.tab)));
|
|
2135
2497
|
codeEl.hidden = false;
|
|
2136
2498
|
if (reopen) {
|
|
2137
2499
|
const x = ev && ev.clientX != null ? ev.clientX : window.innerWidth / 2;
|
|
@@ -2147,7 +2509,8 @@
|
|
|
2147
2509
|
if (at) at.scrollIntoView({ block: "center" });
|
|
2148
2510
|
if (codeAway) document.removeEventListener("mousedown", codeAway, true);
|
|
2149
2511
|
codeAway = (e) => {
|
|
2150
|
-
if (!codeEl.contains(e.target) && !e.target.closest(".code-btn, [data-show-code]"))
|
|
2512
|
+
if (!codeEl.contains(e.target) && !e.target.closest(".code-btn, [data-show-code]"))
|
|
2513
|
+
closeCode();
|
|
2151
2514
|
};
|
|
2152
2515
|
setTimeout(() => codeAway && document.addEventListener("mousedown", codeAway, true), 0);
|
|
2153
2516
|
}
|
|
@@ -2225,11 +2588,11 @@
|
|
|
2225
2588
|
else if (e.key === "0") fit();
|
|
2226
2589
|
else if ((e.key === "c" || e.key === "C") && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
|
2227
2590
|
copySource(hovered || model.byId.get(state.selected));
|
|
2228
|
-
}
|
|
2229
|
-
else if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
|
2591
|
+
} else if (e.key === "ArrowDown" || e.key === "ArrowUp") {
|
|
2230
2592
|
const rows = visible();
|
|
2231
2593
|
const i = rows.findIndex((n) => n.id === state.selected);
|
|
2232
|
-
const next =
|
|
2594
|
+
const next =
|
|
2595
|
+
rows[Math.max(0, Math.min(rows.length - 1, i + (e.key === "ArrowDown" ? 1 : -1)))];
|
|
2233
2596
|
if (next) {
|
|
2234
2597
|
e.preventDefault();
|
|
2235
2598
|
select(next.id);
|
|
@@ -2259,7 +2622,9 @@
|
|
|
2259
2622
|
const fails = parts.length
|
|
2260
2623
|
? `<span class="sep">•</span><button class="linkish ${serious ? "bad" : "warn"}" id="go-errors" title="Open the Errors tab and highlight them in the graphs">${parts.join(" · ")}</button>`
|
|
2261
2624
|
: "";
|
|
2262
|
-
const windows = [0, 100, 500, 1000, 5000, 10000, 30000, 60000, 300000].filter(
|
|
2625
|
+
const windows = [0, 100, 500, 1000, 5000, 10000, 30000, 60000, 300000].filter(
|
|
2626
|
+
(w) => w === 0 || w < model.span
|
|
2627
|
+
);
|
|
2263
2628
|
return `
|
|
2264
2629
|
<header class="top">
|
|
2265
2630
|
<div class="titles">
|
|
@@ -2328,10 +2693,10 @@
|
|
|
2328
2693
|
: shape === "ring"
|
|
2329
2694
|
? `<span class="dot" style="width:11px;height:11px;background:var(--panel);box-shadow:inset 0 0 0 2px ${c}"></span>`
|
|
2330
2695
|
: shape === "dash"
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2696
|
+
? `<span style="width:16px;height:0;border-top:2px dashed ${c}"></span>`
|
|
2697
|
+
: shape === "line"
|
|
2698
|
+
? `<span style="width:2px;height:14px;background:${c};margin:0 7px"></span>`
|
|
2699
|
+
: `<span class="dot" style="background:${c};width:11px;height:11px"></span>`;
|
|
2335
2700
|
return `<div class="li">${sw}${esc(label)}</div>`;
|
|
2336
2701
|
}
|
|
2337
2702
|
|
|
@@ -2340,7 +2705,13 @@
|
|
|
2340
2705
|
return n.branch < 0 ? css("--c1") : css(PALETTE[n.branch]);
|
|
2341
2706
|
}
|
|
2342
2707
|
function statusVar(s) {
|
|
2343
|
-
return s === "ok"
|
|
2708
|
+
return s === "ok"
|
|
2709
|
+
? "--ok"
|
|
2710
|
+
: s === "failed"
|
|
2711
|
+
? "--fail"
|
|
2712
|
+
: s === "cancelled"
|
|
2713
|
+
? "--cancel"
|
|
2714
|
+
: "--running";
|
|
2344
2715
|
}
|
|
2345
2716
|
function alpha(color, a) {
|
|
2346
2717
|
const c = d3.color(color);
|
|
@@ -2349,7 +2720,9 @@
|
|
|
2349
2720
|
return c.formatRgb();
|
|
2350
2721
|
}
|
|
2351
2722
|
const KEYWORDS = new Set(
|
|
2352
|
-
"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(
|
|
2723
|
+
"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(
|
|
2724
|
+
" "
|
|
2725
|
+
)
|
|
2353
2726
|
);
|
|
2354
2727
|
const TOKEN =
|
|
2355
2728
|
/(\/\/[^\n]*|\/\*[\s\S]*?\*\/)|(`(?:\\[\s\S]|[^`\\])*`|"(?:\\.|[^"\\\n])*"|'(?:\\.|[^'\\\n])*')|(\b\d[\d_]*(?:\.\d+)?(?:[eE][+-]?\d+)?n?\b)|([A-Za-z_$][\w$]*)/g;
|
|
@@ -2363,7 +2736,8 @@
|
|
|
2363
2736
|
if (m.index > last) parts.push([null, text.slice(last, m.index)]);
|
|
2364
2737
|
let cls = null;
|
|
2365
2738
|
if (m[1]) cls = "tk-c";
|
|
2366
|
-
else if (m[2])
|
|
2739
|
+
else if (m[2])
|
|
2740
|
+
cls = /^\s*:/.test(text.slice(TOKEN.lastIndex, TOKEN.lastIndex + 8)) ? "tk-p" : "tk-s";
|
|
2367
2741
|
else if (m[3]) cls = "tk-n";
|
|
2368
2742
|
else if (m[4]) {
|
|
2369
2743
|
if (KEYWORDS.has(m[4])) cls = "tk-k";
|
|
@@ -2378,7 +2752,8 @@
|
|
|
2378
2752
|
for (const [cls, chunk] of parts) {
|
|
2379
2753
|
chunk.split("\n").forEach((piece, i) => {
|
|
2380
2754
|
if (i > 0) lines.push("");
|
|
2381
|
-
if (piece)
|
|
2755
|
+
if (piece)
|
|
2756
|
+
lines[lines.length - 1] += cls ? `<span class="${cls}">${esc(piece)}</span>` : esc(piece);
|
|
2382
2757
|
});
|
|
2383
2758
|
}
|
|
2384
2759
|
return lines;
|
|
@@ -2425,7 +2800,8 @@
|
|
|
2425
2800
|
if (n._prompt !== undefined) return n._prompt;
|
|
2426
2801
|
n._prompt = null;
|
|
2427
2802
|
if (n.name !== "prompt" || n.depth === 0) return null;
|
|
2428
|
-
const opts =
|
|
2803
|
+
const opts =
|
|
2804
|
+
(n.v.args && n.v.args[0] && typeof n.v.args[0] === "object" ? n.v.args[0] : {}) || {};
|
|
2429
2805
|
let message = opts.message;
|
|
2430
2806
|
let answer = null;
|
|
2431
2807
|
for (const it of n.entries) {
|
|
@@ -2483,8 +2859,14 @@
|
|
|
2483
2859
|
/** Up to `limit` tag chips, then a "+N" chip holding the rest. */
|
|
2484
2860
|
function chipsHtml(tags, limit, max) {
|
|
2485
2861
|
if (!tags || !tags.length) return "";
|
|
2486
|
-
const more =
|
|
2487
|
-
|
|
2862
|
+
const more =
|
|
2863
|
+
tags.length > limit
|
|
2864
|
+
? `<span class="chip more" title="${esc(tags.slice(limit).join(", "))}">+${tags.length - limit}</span>`
|
|
2865
|
+
: "";
|
|
2866
|
+
return `<span class="chips">${tags
|
|
2867
|
+
.slice(0, limit)
|
|
2868
|
+
.map((t) => chipHtml(t, max))
|
|
2869
|
+
.join("")}${more}</span>`;
|
|
2488
2870
|
}
|
|
2489
2871
|
function displayName(n) {
|
|
2490
2872
|
if (n.depth === 0) return "main";
|
|
@@ -2498,7 +2880,8 @@
|
|
|
2498
2880
|
function iconOf(n) {
|
|
2499
2881
|
if (n.depth === 0) return "root";
|
|
2500
2882
|
const s = `${n.name} ${(n.v.tags || []).join(" ")}`.toLowerCase();
|
|
2501
|
-
if (/^(npm|npx|node|pnpm|yarn|tsc|git|bun)\b/.test(n.name) || /command|shell|exec/.test(s))
|
|
2883
|
+
if (/^(npm|npx|node|pnpm|yarn|tsc|git|bun)\b/.test(n.name) || /command|shell|exec/.test(s))
|
|
2884
|
+
return "terminal";
|
|
2502
2885
|
if (/chromepage|page:|tab/.test(s)) return "window";
|
|
2503
2886
|
if (/browser|chrome/.test(s)) return "globe";
|
|
2504
2887
|
if (/health|ready|gate|wait|port/.test(s)) return "pulse";
|
|
@@ -2511,7 +2894,8 @@
|
|
|
2511
2894
|
}
|
|
2512
2895
|
function copy(text, message) {
|
|
2513
2896
|
const done = () => toast(message || "Copied");
|
|
2514
|
-
if (navigator.clipboard && window.isSecureContext)
|
|
2897
|
+
if (navigator.clipboard && window.isSecureContext)
|
|
2898
|
+
navigator.clipboard.writeText(text).then(done, () => fallbackCopy(text, message));
|
|
2515
2899
|
else fallbackCopy(text, message);
|
|
2516
2900
|
}
|
|
2517
2901
|
function fallbackCopy(text, message) {
|
|
@@ -2543,7 +2927,9 @@
|
|
|
2543
2927
|
}
|
|
2544
2928
|
function toggleTheme() {
|
|
2545
2929
|
const root = document.documentElement;
|
|
2546
|
-
const dark = root.dataset.theme
|
|
2930
|
+
const dark = root.dataset.theme
|
|
2931
|
+
? root.dataset.theme === "dark"
|
|
2932
|
+
: !matchMedia("(prefers-color-scheme: light)").matches;
|
|
2547
2933
|
root.dataset.theme = dark ? "light" : "dark";
|
|
2548
2934
|
try {
|
|
2549
2935
|
localStorage.setItem("omkit-report-theme", root.dataset.theme);
|