omkit 0.8.15 → 0.8.16
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 +411 -411
- package/dist/output/writers/report/report.css +480 -433
- package/dist/output/writers/report/report.js +298 -319
- package/package.json +1 -1
|
@@ -4,15 +4,15 @@
|
|
|
4
4
|
(function () {
|
|
5
5
|
"use strict";
|
|
6
6
|
|
|
7
|
+
const TIP_TAGS = 6; // tag chips a hover tooltip shows before "+N"
|
|
8
|
+
const TREE_TAGS = 4; // …and a timeline tree row
|
|
7
9
|
const ROW_H = 42;
|
|
8
10
|
const BAR_Y = 6;
|
|
9
11
|
const BAR_H = 20;
|
|
10
|
-
const FLAME_ROW = 26;
|
|
11
12
|
|
|
12
13
|
// ── icons (24×24, stroked) ───────────────────────────────
|
|
13
14
|
const ICON = {
|
|
14
15
|
timeline: "M3 17l5-5 4 4 8-8M3 21h18",
|
|
15
|
-
flame: "M12 3c1 4 6 6 6 11a6 6 0 0 1-12 0c0-3 2-4 2-7 2 1 3 3 3 5 1-2 1-6 1-9z",
|
|
16
16
|
flow: "M4 4h7v6H4zM13 14h7v6h-7zM7.5 10v4h9v0M16.5 14v-1",
|
|
17
17
|
zoomIn: "M11 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14zM20 20l-4-4M11 8v6M8 11h6",
|
|
18
18
|
zoomOut: "M11 4a7 7 0 1 0 0 14 7 7 0 0 0 0-14zM20 20l-4-4M8 11h6",
|
|
@@ -43,7 +43,6 @@
|
|
|
43
43
|
info: "M12 3a9 9 0 1 0 0 18 9 9 0 0 0 0-18zM12 11v5M12 8h.01",
|
|
44
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
45
|
code: "M8 7l-5 5 5 5M16 7l5 5-5 5",
|
|
46
|
-
ret: "M9 10l-4 4 4 4M5 14h11a4 4 0 0 0 4-4V5",
|
|
47
46
|
external: "M14 4h6v6M20 4l-9 9M18 14v6H4V6h6",
|
|
48
47
|
};
|
|
49
48
|
const svgIcon = (name, extra) =>
|
|
@@ -78,10 +77,6 @@
|
|
|
78
77
|
};
|
|
79
78
|
const fmtRel = (ms) => `+${fmtDur(Math.max(0, ms)).replace(/^0ms$/, "0ms")}`;
|
|
80
79
|
const pad2 = (n) => String(n).padStart(2, "0");
|
|
81
|
-
const fmtClock = (ts) => {
|
|
82
|
-
const d = new Date(ts);
|
|
83
|
-
return `${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}.${String(d.getMilliseconds()).padStart(3, "0")}`;
|
|
84
|
-
};
|
|
85
80
|
const fmtDate = (ts) => {
|
|
86
81
|
const d = new Date(ts);
|
|
87
82
|
return `${d.getFullYear()}-${pad2(d.getMonth() + 1)}-${pad2(d.getDate())} ${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`;
|
|
@@ -311,7 +306,7 @@
|
|
|
311
306
|
selected: model.root.id,
|
|
312
307
|
collapsed: new Set(),
|
|
313
308
|
zoom: d3.zoomIdentity,
|
|
314
|
-
|
|
309
|
+
detailOpen: new Set(["errors"]), // details-panel sections left open
|
|
315
310
|
errFilter: "all",
|
|
316
311
|
errHl: false,
|
|
317
312
|
errOpen: new Set(),
|
|
@@ -332,7 +327,6 @@
|
|
|
332
327
|
body: app.querySelector(".tl-body"),
|
|
333
328
|
tree: app.querySelector(".tree"),
|
|
334
329
|
chart: app.querySelector(".chart"),
|
|
335
|
-
flame: app.querySelector(".flame-view"),
|
|
336
330
|
flow: app.querySelector(".flow-view"),
|
|
337
331
|
details: app.querySelector(".details"),
|
|
338
332
|
lower: app.querySelector(".lower"),
|
|
@@ -345,10 +339,9 @@
|
|
|
345
339
|
tip.hidden = true;
|
|
346
340
|
document.body.appendChild(tip);
|
|
347
341
|
|
|
348
|
-
let W = 600; // chart pixel width
|
|
342
|
+
let W = 600; // chart pixel width
|
|
349
343
|
let baseX = d3.scaleLinear();
|
|
350
344
|
const xz = () => state.zoom.rescaleX(baseX);
|
|
351
|
-
let syncing = false;
|
|
352
345
|
|
|
353
346
|
// ── header controls ──
|
|
354
347
|
app.querySelectorAll("[data-view]").forEach((b) =>
|
|
@@ -371,6 +364,18 @@
|
|
|
371
364
|
info.addEventListener("mousemove", (ev) => moveTip(ev));
|
|
372
365
|
info.addEventListener("mouseleave", hideTip);
|
|
373
366
|
info.addEventListener("click", () => select(model.root.id));
|
|
367
|
+
const legendBtn = app.querySelector("#legend-btn");
|
|
368
|
+
const legend = app.querySelector("#legend");
|
|
369
|
+
const toggleLegend = (open) => {
|
|
370
|
+
legend.hidden = !open;
|
|
371
|
+
legendBtn.setAttribute("aria-expanded", String(open));
|
|
372
|
+
legendBtn.classList.toggle("active", open);
|
|
373
|
+
};
|
|
374
|
+
legendBtn.addEventListener("click", () => toggleLegend(legend.hidden));
|
|
375
|
+
document.addEventListener("mousedown", (e) => {
|
|
376
|
+
if (!legend.hidden && !legend.contains(e.target) && !legendBtn.contains(e.target)) toggleLegend(false);
|
|
377
|
+
});
|
|
378
|
+
document.addEventListener("keydown", (e) => e.key === "Escape" && !legend.hidden && toggleLegend(false));
|
|
374
379
|
|
|
375
380
|
// ── zoom behaviours ──
|
|
376
381
|
const chartZoom = d3
|
|
@@ -378,28 +383,13 @@
|
|
|
378
383
|
.scaleExtent([1, 5000])
|
|
379
384
|
.filter((e) => (e.type === "wheel" ? e.ctrlKey || e.metaKey : !e.button && e.type !== "dblclick"))
|
|
380
385
|
.on("zoom", (e) => onZoom(e.transform));
|
|
381
|
-
const flameZoom = d3
|
|
382
|
-
.zoom()
|
|
383
|
-
.scaleExtent([1, 5000])
|
|
384
|
-
.filter((e) => (e.type === "wheel" ? e.ctrlKey || e.metaKey : !e.button && e.type !== "dblclick"))
|
|
385
|
-
.on("zoom", (e) => onZoom(e.transform));
|
|
386
386
|
const flowZoom = d3.zoom().scaleExtent([0.1, 4]).on("zoom", (e) => flowG && flowG.attr("transform", e.transform));
|
|
387
387
|
|
|
388
388
|
function activeZoom() {
|
|
389
|
-
return
|
|
390
|
-
? { z: flameZoom, sel: d3.select(el.flame).select("svg") }
|
|
391
|
-
: { z: chartZoom, sel: d3.select(el.chart).select("svg") };
|
|
389
|
+
return { z: chartZoom, sel: d3.select(el.chart).select("svg") };
|
|
392
390
|
}
|
|
393
391
|
function onZoom(t) {
|
|
394
392
|
state.zoom = t;
|
|
395
|
-
if (!syncing) {
|
|
396
|
-
syncing = true;
|
|
397
|
-
// keep the other x-view's internal transform in step
|
|
398
|
-
const other = state.view === "flame" ? d3.select(el.chart).select("svg") : d3.select(el.flame).select("svg");
|
|
399
|
-
const oz = state.view === "flame" ? chartZoom : flameZoom;
|
|
400
|
-
if (!other.empty()) other.call(oz.transform, t);
|
|
401
|
-
syncing = false;
|
|
402
|
-
}
|
|
403
393
|
redrawX();
|
|
404
394
|
}
|
|
405
395
|
function zoomBy(k) {
|
|
@@ -427,19 +417,18 @@
|
|
|
427
417
|
function measure() {
|
|
428
418
|
const sb = el.body.offsetWidth - el.body.clientWidth;
|
|
429
419
|
el.head.style.paddingRight = `${sb}px`;
|
|
430
|
-
W = Math.max(200,
|
|
420
|
+
W = Math.max(200, el.chart.clientWidth);
|
|
431
421
|
const span = model.span;
|
|
432
422
|
baseX = d3.scaleLinear().domain([-span * 0.01, span * 1.015]).range([0, W]);
|
|
433
|
-
|
|
423
|
+
chartZoom.translateExtent([[0, -1e6], [W, 1e6]]).extent([[0, 0], [W, 1]]);
|
|
434
424
|
}
|
|
435
425
|
|
|
436
426
|
function setView(v) {
|
|
437
427
|
state.view = v;
|
|
438
428
|
app.querySelectorAll("[data-view]").forEach((b) => b.classList.toggle("active", b.dataset.view === v));
|
|
439
|
-
el.timeline.classList.toggle("no-tree", v === "flame");
|
|
440
429
|
el.body.hidden = v !== "timeline";
|
|
441
430
|
el.head.hidden = v === "flow";
|
|
442
|
-
el.
|
|
431
|
+
el.timeline.classList.toggle("is-flow", v === "flow");
|
|
443
432
|
el.flow.hidden = v !== "flow";
|
|
444
433
|
el.windowSel.disabled = v === "flow";
|
|
445
434
|
renderAll();
|
|
@@ -479,17 +468,17 @@
|
|
|
479
468
|
? `<button class="twist ${open ? "open" : ""}" title="${open ? "Collapse" : "Expand"}">${svgIcon("chevron")}</button>`
|
|
480
469
|
: '<span class="twist"></span>';
|
|
481
470
|
const color = colorOf(n);
|
|
482
|
-
const
|
|
483
|
-
const tags = rest.length ? `<span class="tagchip">${esc(rest.join(" · "))}</span>` : "";
|
|
471
|
+
const tags = chipsHtml(n.v.tags, TREE_TAGS, 18);
|
|
484
472
|
return `<div class="row ${state.selected === n.id ? "selected" : ""}" data-id="${esc(n.id)}" title="${esc(n.v.path)}">
|
|
485
473
|
<div class="name" style="padding-left:${n.depth ? 4 : 10}px">${guides}${twist}
|
|
486
474
|
<span class="icon" style="color:${color};background:${alpha(color, 0.16)}">${svgIcon(iconOf(n))}</span>
|
|
487
|
-
<span class="
|
|
488
|
-
|
|
489
|
-
|
|
475
|
+
<span class="lines">
|
|
476
|
+
<span class="l1"><span class="label">${labelHtml(n)}</span>${
|
|
477
|
+
model.errorsBy.get(n) ? `<span class="errbadge" title="${model.errorsBy.get(n)} error${model.errorsBy.get(n) > 1 ? "s" : ""}">${model.errorsBy.get(n)}</span>` : ""
|
|
478
|
+
}</span>
|
|
479
|
+
<span class="l2"><span class="st"><span class="dot ${n.status}"></span>${esc(n.status)}</span><span class="sep">·</span><span class="du">${fmtDur(n.duration)}</span>${tags}</span>
|
|
480
|
+
</span>
|
|
490
481
|
</div>
|
|
491
|
-
<div class="status"><span class="dot ${n.status}"></span>${esc(n.status)}</div>
|
|
492
|
-
<div class="dur">${fmtDur(n.duration)}</div>
|
|
493
482
|
</div>`;
|
|
494
483
|
}
|
|
495
484
|
function toggle(n) {
|
|
@@ -527,7 +516,6 @@
|
|
|
527
516
|
updateWindowLabel(x);
|
|
528
517
|
updateOverviewBrush(x);
|
|
529
518
|
if (state.view === "timeline" && chartSvg) drawRows(x);
|
|
530
|
-
if (state.view === "flame") drawFlame(x);
|
|
531
519
|
}
|
|
532
520
|
|
|
533
521
|
function drawRows(x) {
|
|
@@ -575,9 +563,6 @@
|
|
|
575
563
|
const cb = r.append("g").attr("class", "code-btn src");
|
|
576
564
|
cb.append("circle").attr("r", 9);
|
|
577
565
|
cb.append("text").attr("text-anchor", "middle").attr("y", 3.5).text("<>");
|
|
578
|
-
const rb = r.append("g").attr("class", "code-btn ret");
|
|
579
|
-
rb.append("circle").attr("r", 9);
|
|
580
|
-
rb.append("path").attr("d", ICON.ret).attr("transform", "translate(-6,-6) scale(0.5)");
|
|
581
566
|
return r;
|
|
582
567
|
})
|
|
583
568
|
.attr("transform", (_n, i) => `translate(0,${i * ROW_H})`)
|
|
@@ -598,7 +583,7 @@
|
|
|
598
583
|
const t = d3.select(this);
|
|
599
584
|
const x0 = x(n.start);
|
|
600
585
|
const w = x(n.end) - x0;
|
|
601
|
-
const text = `${
|
|
586
|
+
const text = `${boxLabel(n)} (${fmtDur(n.duration)})`;
|
|
602
587
|
const need = text.length * 6.6 + 14;
|
|
603
588
|
const inside = w >= need;
|
|
604
589
|
const clipX = Math.max(x0, 0);
|
|
@@ -617,32 +602,17 @@
|
|
|
617
602
|
// …and, for a block spanning the chart, inside it right after its label
|
|
618
603
|
const cx =
|
|
619
604
|
right + 8 <= W - 10 ? right + 8 : left - 8 >= 10 ? left - 8 : Math.min(W - 12, (flip ? x0 : lx + tw) + 14);
|
|
620
|
-
|
|
621
|
-
const dir = right + 8 <= W - 10 || left - 8 < 10 ? 1 : -1;
|
|
622
|
-
const src = hasCode(n);
|
|
623
|
-
const row = d3.select(this.parentNode);
|
|
624
|
-
row
|
|
605
|
+
d3.select(this.parentNode)
|
|
625
606
|
.select("g.code-btn.src")
|
|
626
|
-
.style("display",
|
|
607
|
+
.style("display", hasPopup(n) ? null : "none")
|
|
627
608
|
.attr("transform", `translate(${cx},${BAR_Y + BAR_H / 2})`);
|
|
628
|
-
row
|
|
629
|
-
.select("g.code-btn.ret")
|
|
630
|
-
.style("display", hasValue(n) ? null : "none")
|
|
631
|
-
.attr("transform", `translate(${src ? cx + dir * 21 : cx},${BAR_Y + BAR_H / 2})`);
|
|
632
609
|
});
|
|
633
610
|
g.select("g.code-btn.src")
|
|
634
611
|
.on("click", (ev, n) => {
|
|
635
612
|
ev.stopPropagation();
|
|
636
613
|
toggleCode(n, ev);
|
|
637
614
|
})
|
|
638
|
-
.on("mouseenter", (ev) => showTip(ev,
|
|
639
|
-
.on("mouseleave", hideTip);
|
|
640
|
-
g.select("g.code-btn.ret")
|
|
641
|
-
.on("click", (ev, n) => {
|
|
642
|
-
ev.stopPropagation();
|
|
643
|
-
toggleCode(n, ev, "value", true);
|
|
644
|
-
})
|
|
645
|
-
.on("mouseenter", (ev, n) => showTip(ev, `Returned: <span class="mono">${esc(valuePreview(n, 80))}</span>`))
|
|
615
|
+
.on("mouseenter", (ev, n) => showTip(ev, popupTip(n)))
|
|
646
616
|
.on("mouseleave", hideTip);
|
|
647
617
|
g.selectAll("rect.hit, text.bar-label").call(bindNode);
|
|
648
618
|
|
|
@@ -721,7 +691,7 @@
|
|
|
721
691
|
function updateAxis(x) {
|
|
722
692
|
const svg = d3.select(el.axis);
|
|
723
693
|
if (state.view === "flow") return;
|
|
724
|
-
const w =
|
|
694
|
+
const w = el.chart.clientWidth || W;
|
|
725
695
|
svg.attr("viewBox", null);
|
|
726
696
|
const g = svg.selectAll("g.axis").data([0]).join("g").attr("class", "axis").attr("transform", "translate(0,33)");
|
|
727
697
|
g.call(d3.axisTop(x).ticks(Math.max(2, Math.floor(w / 90))).tickFormat(fmtTick).tickSizeOuter(0));
|
|
@@ -729,9 +699,8 @@
|
|
|
729
699
|
svg.selectAll("text.cursor-t").data([0]).join("text").attr("class", "cursor-t").attr("y", 12).attr("fill", css("--accent")).attr("font-size", 11);
|
|
730
700
|
}
|
|
731
701
|
function moveCursor(px) {
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
s.select("line.cursor").attr("visibility", px == null ? "hidden" : "visible").attr("x1", px).attr("x2", px);
|
|
702
|
+
if (chartSvg) {
|
|
703
|
+
chartSvg.select("line.cursor").attr("visibility", px == null ? "hidden" : "visible").attr("x1", px).attr("x2", px);
|
|
735
704
|
}
|
|
736
705
|
const label = d3.select(el.axis).select("text.cursor-t");
|
|
737
706
|
if (px == null) label.text("");
|
|
@@ -748,91 +717,6 @@
|
|
|
748
717
|
el.windowSel.value = "0";
|
|
749
718
|
}
|
|
750
719
|
|
|
751
|
-
// ── flame view ──
|
|
752
|
-
let flameSvg = null;
|
|
753
|
-
/**
|
|
754
|
-
* A row per node, packed: a node goes on the first row below its parent's that nothing
|
|
755
|
-
* already placed there overlaps in time. Plain depth rows hid every step that ran beside a
|
|
756
|
-
* long-lived sibling (a daemon, a server) — they shared a row and were drawn over.
|
|
757
|
-
*/
|
|
758
|
-
let flameRows = null;
|
|
759
|
-
function packFlame() {
|
|
760
|
-
const rows = []; // rows[r] = [[start, end], …] already taken
|
|
761
|
-
const out = new Map();
|
|
762
|
-
const free = (r, a, b) => !(rows[r] || []).some(([s, e]) => a < e && s < b);
|
|
763
|
-
(function place(n, parentRow) {
|
|
764
|
-
let r = parentRow + 1;
|
|
765
|
-
// a zero-length node still needs a sliver of time to claim, or it would stack on anything
|
|
766
|
-
const a = n.start;
|
|
767
|
-
const b = Math.max(n.end, n.start + model.span / 2000);
|
|
768
|
-
while (!free(r, a, b)) r++;
|
|
769
|
-
(rows[r] = rows[r] || []).push([a, b]);
|
|
770
|
-
out.set(n.id, r);
|
|
771
|
-
[...n.children].sort((p, q) => p.start - q.start).forEach((c) => place(c, r));
|
|
772
|
-
})(model.root, -1);
|
|
773
|
-
return { rowOf: out, count: rows.length };
|
|
774
|
-
}
|
|
775
|
-
function renderFlame() {
|
|
776
|
-
d3.select(el.flame).selectAll("svg").remove();
|
|
777
|
-
flameRows = flameRows || packFlame();
|
|
778
|
-
flameSvg = d3
|
|
779
|
-
.select(el.flame)
|
|
780
|
-
.append("svg")
|
|
781
|
-
.style("height", `${Math.max(el.flame.clientHeight, 16 + flameRows.count * FLAME_ROW)}px`)
|
|
782
|
-
.on("mousemove", (ev) => moveCursor(d3.pointer(ev)[0]))
|
|
783
|
-
.on("mouseleave", () => moveCursor(null));
|
|
784
|
-
flameSvg.append("g").attr("class", "grid");
|
|
785
|
-
flameSvg.append("g").attr("class", "cells");
|
|
786
|
-
flameSvg.append("line").attr("class", "pin").attr("stroke", css("--accent")).attr("stroke-width", 1.5).attr("y1", 0).attr("y2", 4000);
|
|
787
|
-
flameSvg.append("line").attr("class", "cursor").attr("y1", 0).attr("y2", 4000).attr("visibility", "hidden");
|
|
788
|
-
flameSvg.call(flameZoom).call(flameZoom.transform, state.zoom);
|
|
789
|
-
redrawX();
|
|
790
|
-
}
|
|
791
|
-
function drawFlame(x) {
|
|
792
|
-
if (!flameSvg) return;
|
|
793
|
-
const px = state.pin != null ? x(state.pin) : -10;
|
|
794
|
-
queueMicrotask(() => (app.classList.contains("hl-on") || state.errHl) && applyHl());
|
|
795
|
-
flameSvg.select("line.pin").attr("x1", px).attr("x2", px);
|
|
796
|
-
const H = el.flame.clientHeight;
|
|
797
|
-
const ticks = x.ticks(Math.max(2, Math.floor(W / 90)));
|
|
798
|
-
flameSvg
|
|
799
|
-
.select(".grid")
|
|
800
|
-
.selectAll("line")
|
|
801
|
-
.data(ticks)
|
|
802
|
-
.join("line")
|
|
803
|
-
.attr("x1", (d) => x(d))
|
|
804
|
-
.attr("x2", (d) => x(d))
|
|
805
|
-
.attr("y1", 0)
|
|
806
|
-
.attr("y2", H);
|
|
807
|
-
const cells = flameSvg
|
|
808
|
-
.select(".cells")
|
|
809
|
-
.selectAll("g.cell")
|
|
810
|
-
.data(model.nodes, (n) => n.id)
|
|
811
|
-
.join((enter) => {
|
|
812
|
-
const g = enter.append("g").attr("class", "cell");
|
|
813
|
-
g.append("rect").attr("class", "flame-rect").attr("rx", 3).attr("height", FLAME_ROW - 2);
|
|
814
|
-
g.append("text").attr("class", "flame-text").attr("y", FLAME_ROW / 2 + 3);
|
|
815
|
-
return g;
|
|
816
|
-
})
|
|
817
|
-
.attr("transform", (n) => `translate(0,${8 + flameRows.rowOf.get(n.id) * FLAME_ROW})`);
|
|
818
|
-
cells
|
|
819
|
-
.select("rect")
|
|
820
|
-
.attr("x", (n) => x(n.start))
|
|
821
|
-
.attr("width", (n) => Math.max(1, x(n.end) - x(n.start)))
|
|
822
|
-
.attr("fill", (n) => colorOf(n))
|
|
823
|
-
.attr("fill-opacity", (n) => (n.status === "cancelled" ? 0.55 : 0.92))
|
|
824
|
-
.classed("selected", (n) => n.id === state.selected)
|
|
825
|
-
.call(bindNode);
|
|
826
|
-
cells
|
|
827
|
-
.select("text")
|
|
828
|
-
.attr("x", (n) => Math.max(x(n.start), 0) + 5)
|
|
829
|
-
.text((n) => {
|
|
830
|
-
const w = x(n.end) - Math.max(x(n.start), 0);
|
|
831
|
-
return w < 34 ? "" : truncate(`${displayName(n)} ${fmtDur(n.duration)}`, (w - 10) / 6.2);
|
|
832
|
-
})
|
|
833
|
-
.call(bindNode);
|
|
834
|
-
}
|
|
835
|
-
|
|
836
720
|
// ── flow view ──
|
|
837
721
|
//
|
|
838
722
|
// Each node's children form one *lane*: a column to its right, in start order, joined by
|
|
@@ -842,7 +726,19 @@
|
|
|
842
726
|
let flowG = null;
|
|
843
727
|
let flowPos = new Map();
|
|
844
728
|
let flowBounds = null;
|
|
845
|
-
const FLOW = { CW: 250, CH: 58, GX: 64, GY: 26, LANE_DX: 22, TW: 46, TH: 28 };
|
|
729
|
+
const FLOW = { CW: 250, CH: 58, TAGS_H: 18, GX: 64, GY: 26, LANE_DX: 22, TW: 46, TH: 28 };
|
|
730
|
+
/** A tag chip in SVG, matching the HTML `.chip`; returns its group and width. */
|
|
731
|
+
function svgChip(parent, x, label, color) {
|
|
732
|
+
const g = parent.append("g").attr("class", "tchip").attr("transform", `translate(${x},0)`);
|
|
733
|
+
const rect = g.append("rect").attr("height", 16).attr("rx", 4);
|
|
734
|
+
const text = g.append("text").attr("x", 6).attr("y", 11.5).text(label);
|
|
735
|
+
const w = (text.node().getComputedTextLength() || label.length * 6) + 12;
|
|
736
|
+
rect
|
|
737
|
+
.attr("width", w)
|
|
738
|
+
.attr("fill", color ? alpha(color, 0.14) : "none")
|
|
739
|
+
.attr("stroke", color ? alpha(color, 0.45) : css("--line"));
|
|
740
|
+
return { g, w };
|
|
741
|
+
}
|
|
846
742
|
function renderFlow() {
|
|
847
743
|
d3.select(el.flow).selectAll("svg").remove();
|
|
848
744
|
const svg = d3.select(el.flow).append("svg");
|
|
@@ -860,7 +756,9 @@
|
|
|
860
756
|
.attr("class", "flow-arrowhead")
|
|
861
757
|
.attr("d", "M0,1 L10,5 L0,9 z");
|
|
862
758
|
flowG = svg.append("g");
|
|
863
|
-
const { CW,
|
|
759
|
+
const { CW, GX, GY, LANE_DX, TW, TH } = FLOW;
|
|
760
|
+
// every card gets the tag row when any is tagged, so lanes stay on one rhythm
|
|
761
|
+
const CH = FLOW.CH + (model.nodes.some((n) => n.v.tags && n.v.tags.length) ? FLOW.TAGS_H : 0);
|
|
864
762
|
|
|
865
763
|
const shotsBy = new Map();
|
|
866
764
|
for (const g of model.gallery) {
|
|
@@ -988,7 +886,7 @@
|
|
|
988
886
|
.attr("color", (n) => colorOf(n))
|
|
989
887
|
.html((n) => `<path d="${ICON[iconOf(n)]}" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"/>`);
|
|
990
888
|
card.each(function (n) {
|
|
991
|
-
const lines = wrapName(
|
|
889
|
+
const lines = wrapName(boxLabel(n), 31);
|
|
992
890
|
const t = d3.select(this);
|
|
993
891
|
lines.forEach((line, i) =>
|
|
994
892
|
t
|
|
@@ -1003,6 +901,21 @@
|
|
|
1003
901
|
.attr("x", 42)
|
|
1004
902
|
.attr("y", lines.length > 1 ? 49 : 41)
|
|
1005
903
|
.text(`${nodeKind(n)} · ${n.status} · ${fmtDur(n.duration)} · +${fmtTick(n.start)}`);
|
|
904
|
+
const tags = n.v.tags || [];
|
|
905
|
+
if (!tags.length) return;
|
|
906
|
+
const row = t.append("g").attr("class", "tchips").attr("transform", `translate(42,${lines.length > 1 ? 56 : 51})`);
|
|
907
|
+
const room = CW - 42 - 10;
|
|
908
|
+
let x = 0;
|
|
909
|
+
for (let i = 0; i < tags.length; i++) {
|
|
910
|
+
const c = svgChip(row, x, truncate(tags[i], 22), tagColor(tags[i]));
|
|
911
|
+
// keep room for a "+N" after it, unless it is the last
|
|
912
|
+
if (i > 0 && x + c.w > room - (i < tags.length - 1 ? 30 : 0)) {
|
|
913
|
+
c.g.remove();
|
|
914
|
+
svgChip(row, x, `+${tags.length - i}`, null).g.classed("more", true);
|
|
915
|
+
break;
|
|
916
|
+
}
|
|
917
|
+
x += c.w + 4;
|
|
918
|
+
}
|
|
1006
919
|
});
|
|
1007
920
|
card
|
|
1008
921
|
.append("circle")
|
|
@@ -1011,7 +924,7 @@
|
|
|
1011
924
|
.attr("r", 4)
|
|
1012
925
|
.attr("fill", (n) => css(statusVar(n.status)));
|
|
1013
926
|
const fcb = card
|
|
1014
|
-
.filter((n) =>
|
|
927
|
+
.filter((n) => hasPopup(n))
|
|
1015
928
|
.append("g")
|
|
1016
929
|
.attr("class", "code-btn")
|
|
1017
930
|
.attr("transform", `translate(${CW / 2},0)`)
|
|
@@ -1019,27 +932,12 @@
|
|
|
1019
932
|
ev.stopPropagation();
|
|
1020
933
|
toggleCode(n, ev);
|
|
1021
934
|
})
|
|
1022
|
-
.on("mouseenter", (ev) => {
|
|
935
|
+
.on("mouseenter", (ev, n) => {
|
|
1023
936
|
ev.stopPropagation();
|
|
1024
|
-
showTip(ev,
|
|
937
|
+
showTip(ev, popupTip(n));
|
|
1025
938
|
});
|
|
1026
939
|
fcb.append("circle").attr("r", 9);
|
|
1027
940
|
fcb.append("text").attr("text-anchor", "middle").attr("y", 3.5).text("<>");
|
|
1028
|
-
const frb = card
|
|
1029
|
-
.filter((n) => hasValue(n))
|
|
1030
|
-
.append("g")
|
|
1031
|
-
.attr("class", "code-btn ret")
|
|
1032
|
-
.attr("transform", (n) => `translate(${CW / 2 + (hasCode(n) ? 22 : 0)},0)`)
|
|
1033
|
-
.on("click", (ev, n) => {
|
|
1034
|
-
ev.stopPropagation();
|
|
1035
|
-
toggleCode(n, ev, "value", true);
|
|
1036
|
-
})
|
|
1037
|
-
.on("mouseenter", (ev, n) => {
|
|
1038
|
-
ev.stopPropagation();
|
|
1039
|
-
showTip(ev, `Returned: <span class="mono">${esc(valuePreview(n, 80))}</span>`);
|
|
1040
|
-
});
|
|
1041
|
-
frb.append("circle").attr("r", 9);
|
|
1042
|
-
frb.append("path").attr("d", ICON.ret).attr("transform", "translate(-6,-6) scale(0.5)");
|
|
1043
941
|
|
|
1044
942
|
// snapshot chips — on lane arrows, and in strips under leaf cards
|
|
1045
943
|
const chipLayer = flowG.append("g").attr("class", "flow-shots");
|
|
@@ -1182,7 +1080,7 @@
|
|
|
1182
1080
|
// ── selection + details ──
|
|
1183
1081
|
function select(id, tab) {
|
|
1184
1082
|
state.selected = id;
|
|
1185
|
-
if (tab) state.
|
|
1083
|
+
if (tab) state.detailOpen.add(tab);
|
|
1186
1084
|
state.details = true;
|
|
1187
1085
|
el.main.classList.remove("no-details");
|
|
1188
1086
|
el.tree.querySelectorAll(".row").forEach((r) => r.classList.toggle("selected", r.dataset.id === id));
|
|
@@ -1190,7 +1088,6 @@
|
|
|
1190
1088
|
chartSvg.selectAll("rect.bar").classed("selected", (n) => n.id === id);
|
|
1191
1089
|
chartSvg.selectAll("g.r").classed("sel", (n) => n.id === id);
|
|
1192
1090
|
}
|
|
1193
|
-
if (flameSvg) flameSvg.selectAll("rect.flame-rect").classed("selected", (n) => n.id === id);
|
|
1194
1091
|
d3.select(el.flow).selectAll("g.flow-card").classed("selected", (n) => n.id === id);
|
|
1195
1092
|
renderDetails();
|
|
1196
1093
|
}
|
|
@@ -1240,10 +1137,9 @@
|
|
|
1240
1137
|
flash(matchMark);
|
|
1241
1138
|
}
|
|
1242
1139
|
function flash(matchMark) {
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
s.selectAll("line.pin").classed("flash", true);
|
|
1140
|
+
if (chartSvg) {
|
|
1141
|
+
chartSvg.selectAll("line.pin").classed("flash", false).node()?.getBoundingClientRect();
|
|
1142
|
+
chartSvg.selectAll("line.pin").classed("flash", true);
|
|
1247
1143
|
}
|
|
1248
1144
|
if (chartSvg) {
|
|
1249
1145
|
const hit = chartSvg
|
|
@@ -1266,15 +1162,7 @@
|
|
|
1266
1162
|
function renderDetails() {
|
|
1267
1163
|
const n = model.byId.get(state.selected) || model.root;
|
|
1268
1164
|
const color = colorOf(n);
|
|
1269
|
-
const
|
|
1270
|
-
["overview", "Overview"],
|
|
1271
|
-
["args", "Args"],
|
|
1272
|
-
...(hasValue(n) ? [["value", "Return"]] : []),
|
|
1273
|
-
...(model.errorsBy.get(n) ? [["errors", `Errors (${model.errorsBy.get(n)})`]] : []),
|
|
1274
|
-
["events", `Events`],
|
|
1275
|
-
["logs", "Logs"],
|
|
1276
|
-
["snapshots", "Snapshots"],
|
|
1277
|
-
];
|
|
1165
|
+
const call = callOf(n);
|
|
1278
1166
|
el.details.innerHTML = `
|
|
1279
1167
|
<div class="dh">Action details <button class="x" title="Close">${svgIcon("close", ' width="16" height="16"')}</button></div>
|
|
1280
1168
|
<div class="who">
|
|
@@ -1283,25 +1171,30 @@
|
|
|
1283
1171
|
<span class="pill"><span class="dot ${n.status}"></span>${esc(n.status)}</span>
|
|
1284
1172
|
</div>
|
|
1285
1173
|
<div class="scroll">
|
|
1174
|
+
${call ? `<div class="tcall who-call">${callHtml(n, call)}</div>` : ""}
|
|
1286
1175
|
<div class="kv">
|
|
1287
1176
|
<div class="k">Duration</div><div class="v">${fmtDur(n.duration)}</div>
|
|
1288
|
-
<div class="k">Start</div><div class="v">${fmtRel(n.start)} (${fmtClock(n.startAbs)})</div>
|
|
1289
|
-
<div class="k">End</div><div class="v">${fmtRel(n.end)} (${fmtClock(n.endAbs)})</div>
|
|
1290
1177
|
${n.v.description ? `<div class="k">Summary</div><div class="v">${esc(n.v.description.summary)}</div>` : ""}
|
|
1178
|
+
${n.v.tags.length ? `<div class="k">Tags</div><div class="v">${chipsHtml(n.v.tags, Infinity, 40)}</div>` : ""}
|
|
1291
1179
|
${n.v.mcp ? `<div class="k">MCP</div><div class="v"><span class="badge-inline">${esc(n.v.mcp.mode)}</span></div>` : ""}
|
|
1292
|
-
<div class="k">
|
|
1180
|
+
<div class="k">Id</div><div class="v mono-sm">${esc(n.id)}</div>
|
|
1293
1181
|
${userSite(n) ? `<div class="k">${siteLabel(n)}</div><div class="v defined">
|
|
1294
1182
|
<button class="icon-btn" data-copy-source aria-label="Copy relative path">${svgIcon("copy")}</button>
|
|
1295
|
-
${
|
|
1296
|
-
${hasValue(n) ? `<button class="icon-btn" data-show-value aria-label="Show returned value">${svgIcon("ret")}</button>` : ""}
|
|
1183
|
+
${hasPopup(n) ? `<button class="icon-btn" data-show-code aria-label="Show source">${svgIcon("code")}</button>` : ""}
|
|
1297
1184
|
<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>
|
|
1298
1185
|
<span class="defined-name" >${esc(basename(sourceOf(n)))}</span>
|
|
1299
1186
|
</div>` : ""}
|
|
1300
1187
|
</div>
|
|
1301
|
-
<div class="
|
|
1302
|
-
.map((
|
|
1188
|
+
<div class="acc">${detailSections(n)
|
|
1189
|
+
.map((sec) =>
|
|
1190
|
+
sec.empty
|
|
1191
|
+
? `<div class="acc-sec empty"><div class="acc-h">${svgIcon("chevron")}<span>${esc(sec.title)}</span><span class="acc-n">0</span></div></div>`
|
|
1192
|
+
: `<details class="acc-sec ${sec.key}" data-sec="${sec.key}" ${state.detailOpen.has(sec.key) ? "open" : ""}>
|
|
1193
|
+
<summary class="acc-h">${svgIcon("chevron")}<span>${esc(sec.title)}</span>${sec.count != null ? `<span class="acc-n">${sec.count}</span>` : ""}</summary>
|
|
1194
|
+
<div class="acc-b">${sec.body()}</div>
|
|
1195
|
+
</details>`
|
|
1196
|
+
)
|
|
1303
1197
|
.join("")}</div>
|
|
1304
|
-
<div class="tabbody">${detailTabHtml(n)}</div>
|
|
1305
1198
|
</div>`;
|
|
1306
1199
|
const copySrc = el.details.querySelector("[data-copy-source]");
|
|
1307
1200
|
if (copySrc) {
|
|
@@ -1312,17 +1205,11 @@
|
|
|
1312
1205
|
copySrc.addEventListener("mousemove", (ev) => moveTip(ev));
|
|
1313
1206
|
copySrc.addEventListener("mouseleave", hideTip);
|
|
1314
1207
|
}
|
|
1315
|
-
const showValue = el.details.querySelector("[data-show-value]");
|
|
1316
|
-
if (showValue) {
|
|
1317
|
-
showValue.addEventListener("click", (ev) => toggleCode(n, ev, "value", true));
|
|
1318
|
-
showValue.addEventListener("mouseenter", (ev) => showTip(ev, `Returned: <span class="mono">${esc(valuePreview(n, 80))}</span>`));
|
|
1319
|
-
showValue.addEventListener("mouseleave", hideTip);
|
|
1320
|
-
}
|
|
1321
1208
|
wireValue(el.details, n);
|
|
1322
1209
|
const showCode = el.details.querySelector("[data-show-code]");
|
|
1323
1210
|
if (showCode) {
|
|
1324
1211
|
showCode.addEventListener("click", (ev) => toggleCode(n, ev));
|
|
1325
|
-
showCode.addEventListener("mouseenter", (ev) => showTip(ev,
|
|
1212
|
+
showCode.addEventListener("mouseenter", (ev) => showTip(ev, popupTip(n)));
|
|
1326
1213
|
showCode.addEventListener("mouseleave", hideTip);
|
|
1327
1214
|
}
|
|
1328
1215
|
el.details.querySelectorAll("[data-tip-text]").forEach((a) => {
|
|
@@ -1334,67 +1221,88 @@
|
|
|
1334
1221
|
el.main.classList.add("no-details");
|
|
1335
1222
|
relayout();
|
|
1336
1223
|
});
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1224
|
+
// which sections are open is a preference: it carries over from action to action
|
|
1225
|
+
el.details.querySelectorAll("details[data-sec]").forEach((d) =>
|
|
1226
|
+
d.addEventListener("toggle", () => {
|
|
1227
|
+
if (d.open) state.detailOpen.add(d.dataset.sec);
|
|
1228
|
+
else state.detailOpen.delete(d.dataset.sec);
|
|
1341
1229
|
})
|
|
1342
1230
|
);
|
|
1343
1231
|
wireItems(el.details);
|
|
1344
1232
|
}
|
|
1345
1233
|
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
const kind = ERR_TYPES.find(([k]) => k === e.type)[1];
|
|
1354
|
-
return `<div class="section-title err-kind ${e.type}">${esc(kind)} <span class="muted">${fmtRel(e.t - n.start)}</span></div><pre class="code err-text">${esc(e.text)}</pre>`;
|
|
1355
|
-
})
|
|
1356
|
-
.join("");
|
|
1357
|
-
}
|
|
1358
|
-
if (tab === "value") {
|
|
1359
|
-
return hasValue(n) ? `<div class="code-box">${valueHtml(n)}</div>` : '<div class="muted">Returned nothing.</div>';
|
|
1360
|
-
}
|
|
1361
|
-
if (tab === "args") {
|
|
1362
|
-
const params = schemaFields(n.v.inputSchema);
|
|
1363
|
-
const schema = params.length ? `<div class="section-title">Schema</div><div class="params">${params.map(paramHtml).join("")}</div><div class="section-title">Values</div>` : "";
|
|
1364
|
-
return schema + (n.v.args && n.v.args.length ? `<pre class="code">${esc(json(n.v.args.length === 1 ? n.v.args[0] : n.v.args))}</pre>` : '<div class="muted">No arguments.</div>');
|
|
1365
|
-
}
|
|
1366
|
-
if (tab === "events") {
|
|
1367
|
-
const evs = n.entries.filter((i) => i.kind !== "info");
|
|
1368
|
-
return evs.length ? `<ul class="ev-list">${evs.map((i) => evLi(i, n)).join("")}</ul>` : '<div class="muted">No events.</div>';
|
|
1369
|
-
}
|
|
1370
|
-
if (tab === "logs") {
|
|
1371
|
-
const logs = n.entries.filter((i) => i.kind === "info" || i.kind === "error");
|
|
1372
|
-
return logs.length
|
|
1373
|
-
? `<pre class="code">${logs.map((i) => `<span class="muted">${esc(fmtRel(i.t - n.start).padEnd(8))}</span>${esc(i.e.message)}`).join("\n")}</pre>`
|
|
1374
|
-
: '<div class="muted">No log output.</div>';
|
|
1375
|
-
}
|
|
1376
|
-
if (tab === "snapshots") {
|
|
1377
|
-
const g = model.gallery.filter((x) => x.node === n);
|
|
1378
|
-
return g.length ? `<div class="thumbs">${g.map(thumbHtml).join("")}</div>` : '<div class="muted">No snapshots or files.</div>';
|
|
1379
|
-
}
|
|
1380
|
-
// overview
|
|
1381
|
-
const fails = model.run.failures.filter((f) => f.action === n.id || f.action === n.name || f.action === n.v.path);
|
|
1234
|
+
/** The details panel's accordion: one section per facet of `n`, empty ones shown but shut. */
|
|
1235
|
+
function detailSections(n) {
|
|
1236
|
+
const errs = model.errors.filter((e) => e.node === n);
|
|
1237
|
+
const params = schemaFields(n.v.inputSchema);
|
|
1238
|
+
const evs = n.entries.filter((i) => i.kind !== "info");
|
|
1239
|
+
const logs = n.entries.filter((i) => i.kind === "info" || i.kind === "error");
|
|
1240
|
+
const shots = model.gallery.filter((x) => x.node === n);
|
|
1382
1241
|
const asserts = n.entries.filter((i) => i.kind === "pass" || i.kind === "fail");
|
|
1383
|
-
const
|
|
1384
|
-
return
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1242
|
+
const logLink = `<div class="acc-file">${svgIcon("file", ' width="14" height="14"')}<a href="${esc(fileUrl(n.v.logFile))}" title="${esc(n.v.logFile)}">${esc(basename(n.v.logFile))}</a></div>`;
|
|
1243
|
+
return [
|
|
1244
|
+
{
|
|
1245
|
+
key: "errors",
|
|
1246
|
+
title: "Errors",
|
|
1247
|
+
count: errs.length,
|
|
1248
|
+
empty: !errs.length,
|
|
1249
|
+
body: () =>
|
|
1250
|
+
errs
|
|
1251
|
+
.map((e) => {
|
|
1252
|
+
const kind = ERR_TYPES.find(([k]) => k === e.type)[1];
|
|
1253
|
+
return `<div class="section-title err-kind ${e.type}">${esc(kind)} <span class="muted">${fmtRel(e.t - n.start)}</span></div><pre class="code err-text">${esc(e.text)}</pre>`;
|
|
1254
|
+
})
|
|
1255
|
+
.join(""),
|
|
1256
|
+
},
|
|
1257
|
+
{
|
|
1258
|
+
key: "args",
|
|
1259
|
+
title: "Args",
|
|
1260
|
+
empty: !hasArgs(n) && !params.length,
|
|
1261
|
+
body: () =>
|
|
1262
|
+
(params.length ? `<div class="params">${params.map(paramHtml).join("")}</div>` : "") +
|
|
1263
|
+
(hasArgs(n) ? `<div class="code-box">${argsHtml(n)}</div>` : '<div class="muted">No arguments.</div>'),
|
|
1264
|
+
},
|
|
1265
|
+
{ key: "value", title: "Returned", empty: !hasValue(n), body: () => `<div class="code-box">${valueHtml(n)}</div>` },
|
|
1266
|
+
{
|
|
1267
|
+
key: "snapshots",
|
|
1268
|
+
title: "Snapshots & files",
|
|
1269
|
+
count: shots.length,
|
|
1270
|
+
empty: !shots.length,
|
|
1271
|
+
body: () => `<div class="shots">${shots.map((g) => shotHtml(g, n)).join("")}</div>`,
|
|
1272
|
+
},
|
|
1273
|
+
{
|
|
1274
|
+
key: "events",
|
|
1275
|
+
title: "Events",
|
|
1276
|
+
count: evs.length,
|
|
1277
|
+
empty: !evs.length,
|
|
1278
|
+
body: () => `<ul class="ev-list">${evs.map((i) => evLi(i, n)).join("")}</ul>`,
|
|
1279
|
+
},
|
|
1280
|
+
{
|
|
1281
|
+
key: "logs",
|
|
1282
|
+
title: "Logs",
|
|
1283
|
+
count: logs.length,
|
|
1284
|
+
empty: !logs.length,
|
|
1285
|
+
body: () =>
|
|
1286
|
+
`${logLink}<pre class="code">${logs.map((i) => `<span class="muted">${esc(fmtRel(i.t - n.start).padEnd(8))}</span>${esc(i.e.message)}`).join("\n")}</pre>`,
|
|
1287
|
+
},
|
|
1288
|
+
{
|
|
1289
|
+
key: "assertions",
|
|
1290
|
+
title: "Assertions",
|
|
1291
|
+
count: asserts.length,
|
|
1292
|
+
empty: !asserts.length,
|
|
1293
|
+
body: () => `<ul class="ev-list">${asserts.map((i) => evLi(i, n)).join("")}</ul>`,
|
|
1294
|
+
},
|
|
1295
|
+
];
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
/** A snapshot or file in the details panel: a small thumb with its name and when it landed. */
|
|
1299
|
+
function shotHtml(g, n) {
|
|
1300
|
+
const i = model.gallery.indexOf(g);
|
|
1301
|
+
return `<div class="shot" data-gallery="${i}" title="${esc(g.name)}">
|
|
1302
|
+
${thumbHtml(g)}
|
|
1303
|
+
<span class="nm">${esc(g.name)}</span>
|
|
1304
|
+
${g.t != null ? `<span class="t">${fmtRel(g.t - n.start)}</span>` : ""}
|
|
1305
|
+
</div>`;
|
|
1398
1306
|
}
|
|
1399
1307
|
|
|
1400
1308
|
function evLi(i, n) {
|
|
@@ -1561,7 +1469,6 @@
|
|
|
1561
1469
|
const mark = (sel, idOf) =>
|
|
1562
1470
|
sel.classed("err", (d) => !!set && set.has(idOf(d))).classed("err-path", (d) => !!set && path.has(idOf(d)));
|
|
1563
1471
|
if (chartSvg) mark(chartSvg.selectAll("g.r"), (n) => n.id);
|
|
1564
|
-
if (flameSvg) mark(flameSvg.selectAll("g.cell"), (n) => n.id);
|
|
1565
1472
|
mark(d3.select(el.flow).selectAll("g.flow-card"), (n) => n.id);
|
|
1566
1473
|
el.tree.querySelectorAll(".row").forEach((r) => {
|
|
1567
1474
|
r.classList.toggle("err", !!set && set.has(r.dataset.id));
|
|
@@ -1762,40 +1669,20 @@
|
|
|
1762
1669
|
hovered = n; // the node `C` copies while its tooltip is up
|
|
1763
1670
|
const v = n.v;
|
|
1764
1671
|
const shown = n.depth === 0 ? model.data.name : displayName(n);
|
|
1765
|
-
const
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
n.
|
|
1769
|
-
n.
|
|
1770
|
-
|
|
1771
|
-
const rows = [
|
|
1772
|
-
["Duration", fmtDur(n.duration)],
|
|
1773
|
-
["When", `${fmtRel(n.start)} → ${fmtRel(n.end)}`],
|
|
1774
|
-
v.tags.length ? ["Tags", esc(v.tags.join(" · "))] : null,
|
|
1775
|
-
userSite(n) ? [siteLabel(n) === "Called at" ? "Called" : "Defined", `<span class="mono">${esc(sourceOf(n))}</span>`] : null,
|
|
1776
|
-
hasValue(n) ? ["Returns", `<span class="mono">${esc(valuePreview(n, 90))}</span>`] : null,
|
|
1777
|
-
n.children.length ? ["Children", String(n.children.length)] : null,
|
|
1778
|
-
n.marks.length ? ["Events", String(n.marks.length)] : null,
|
|
1672
|
+
const call = callOf(n);
|
|
1673
|
+
const meta = [
|
|
1674
|
+
`<span class="dot ${n.status}"></span>${esc(n.status)}`,
|
|
1675
|
+
esc(fmtDur(n.duration)),
|
|
1676
|
+
n.depth === 0 ? "om" : v.kind === "step" ? "step" : "",
|
|
1677
|
+
v.mcp ? `<span class="mcp">MCP · ${esc(v.mcp.mode)}</span>` : "",
|
|
1779
1678
|
].filter(Boolean);
|
|
1780
|
-
const args = !promptOf(n) && v.args && v.args.length ? truncate(oneLine(v.args.length === 1 ? v.args[0] : v.args), 220) : "";
|
|
1781
|
-
const params = schemaFields(v.inputSchema);
|
|
1782
|
-
const pr = promptOf(n);
|
|
1783
|
-
const promptRows = pr
|
|
1784
|
-
? `<div class="sec">Prompt</div><div class="tprompt">${esc(truncate(pr.message || "", 200))}</div>${
|
|
1785
|
-
pr.answer ? `<div class="tkv"><span class="k">Answer</span><span class="mono">${esc(truncate(typeof pr.answer.value === "string" ? pr.answer.value : oneLine(pr.answer.value), 120))}</span>${pr.answer.via ? `<span class="k">Via</span><span>${esc(pr.answer.via)}</span>` : ""}</div>` : ""
|
|
1786
|
-
}`
|
|
1787
|
-
: "";
|
|
1788
1679
|
const fails = model.run.failures.filter((f) => f.action === n.id || f.action === n.name || f.action === v.path);
|
|
1789
1680
|
return `<div class="tt">${esc(shown)}</div>
|
|
1790
|
-
${
|
|
1791
|
-
<div class="
|
|
1792
|
-
${v.description ? `<div class="summary">${esc(v.description.summary)}</div>` :
|
|
1793
|
-
|
|
1794
|
-
${
|
|
1795
|
-
${params.length ? `<div class="sec">Args schema</div><div class="params">${params.map(paramHtml).join("")}</div>` : ""}
|
|
1796
|
-
${args ? `<div class="sec">Args</div><div class="mono small">${esc(args)}</div>` : ""}
|
|
1797
|
-
${v.definedAt ? '<div class="hint">Press <kbd>C</kbd> to copy the path</div>' : ""}
|
|
1798
|
-
${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>` : ""}`;
|
|
1681
|
+
${call ? `<div class="tcall">${callHtml(n, call)}</div>` : ""}
|
|
1682
|
+
<div class="tmeta">${meta.join('<span class="sep">·</span>')}</div>
|
|
1683
|
+
${v.description ? `<div class="summary">${esc(truncate(v.description.summary, 240))}</div>` : ""}
|
|
1684
|
+
${chipsHtml(v.tags, TIP_TAGS, 28)}
|
|
1685
|
+
${fails.length ? `<div class="tfail">${esc(truncate(String(fails[0].error).split(String.fromCharCode(10))[0], 160))}</div>` : ""}`;
|
|
1799
1686
|
}
|
|
1800
1687
|
function galleryTip(x) {
|
|
1801
1688
|
const img = x.image ? `<img src="${model.asset(x.f.asset)}" alt="">` : "";
|
|
@@ -1844,19 +1731,89 @@
|
|
|
1844
1731
|
function codeOf(site) {
|
|
1845
1732
|
return site && model.data.code ? model.data.code[site] : undefined;
|
|
1846
1733
|
}
|
|
1847
|
-
/**
|
|
1734
|
+
/**
|
|
1735
|
+
* How the user's code launched `n`, read off the source at its call site: the variable it
|
|
1736
|
+
* was bound to and the callee — `page = chromePage(…)`. Sites carry a line but no column,
|
|
1737
|
+
* so this takes the first call on that line; null when the site is library source.
|
|
1738
|
+
*/
|
|
1739
|
+
function callExpr(n) {
|
|
1740
|
+
const site = n.v.calledAt;
|
|
1741
|
+
const c = site && !isLib(site) && codeOf(site);
|
|
1742
|
+
if (!c) return null;
|
|
1743
|
+
const at = Number((/:(\d+)$/.exec(site) || [])[1]);
|
|
1744
|
+
const lines = c.text.split("\n");
|
|
1745
|
+
const line = lines[at - c.start];
|
|
1746
|
+
if (line == null) return null;
|
|
1747
|
+
let call = null;
|
|
1748
|
+
for (const m of line.matchAll(/(?:^|[^\w$.])([A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*)\s*\(/g)) {
|
|
1749
|
+
if (!/^(if|for|while|switch|return|catch|function|await|typeof|new|async)$/.test(m[1])) {
|
|
1750
|
+
call = { callee: m[1], index: m.index };
|
|
1751
|
+
break;
|
|
1752
|
+
}
|
|
1753
|
+
}
|
|
1754
|
+
if (!call) return null;
|
|
1755
|
+
const DECL = /\b(?:const|let|var)\s+([\w$]+|\{[^}]*\}|\[[^\]]*\])/;
|
|
1756
|
+
const prefix = line.slice(0, call.index);
|
|
1757
|
+
const prev = lines[at - c.start - 1] || "";
|
|
1758
|
+
const m =
|
|
1759
|
+
DECL.exec(prefix) ||
|
|
1760
|
+
/^\s*([\w$.]+)\s*=(?!=)/.exec(prefix) ||
|
|
1761
|
+
(/^\s*(?:await\s+)?$/.test(prefix) && /=\s*(?:await\s*)?$/.test(prev) ? DECL.exec(prev) : null);
|
|
1762
|
+
return { callee: call.callee, bind: m ? m[1].replace(/\s+/g, " ") : "" };
|
|
1763
|
+
}
|
|
1764
|
+
/** The popover's tabs for `n`: what it is (prompt, source), then what went in and came out. */
|
|
1848
1765
|
function codeTabs(n) {
|
|
1849
1766
|
const tabs = [];
|
|
1850
1767
|
if (promptOf(n)) tabs.push({ key: "prompt", label: "Prompt" });
|
|
1851
|
-
if (hasValue(n)) tabs.push({ key: "value", label: "Returned" });
|
|
1852
1768
|
const mine = userSite(n);
|
|
1853
1769
|
if (codeOf(mine)) tabs.push({ key: "user", label: siteLabel(n) === "Called at" ? "Call site" : "Source", site: mine });
|
|
1854
1770
|
const def = n.v.definedAt;
|
|
1855
1771
|
if (def && def !== mine && codeOf(def)) tabs.push({ key: "def", label: isLib(def) ? "Definition (omkit)" : "Definition", site: def });
|
|
1772
|
+
if (hasArgs(n)) tabs.push({ key: "args", label: "Args" });
|
|
1773
|
+
if (hasValue(n)) tabs.push({ key: "value", label: "Returned" });
|
|
1856
1774
|
return tabs;
|
|
1857
1775
|
}
|
|
1858
1776
|
function hasCode(n) {
|
|
1859
|
-
return codeTabs(n).some((t) => t.key !== "value");
|
|
1777
|
+
return codeTabs(n).some((t) => t.key !== "value" && t.key !== "args");
|
|
1778
|
+
}
|
|
1779
|
+
const callMemo = new Map();
|
|
1780
|
+
/** {@link callExpr}, once per node — bar labels ask on every redraw. */
|
|
1781
|
+
function callOf(n) {
|
|
1782
|
+
if (!callMemo.has(n.id)) callMemo.set(n.id, n.depth === 0 ? null : callExpr(n));
|
|
1783
|
+
return callMemo.get(n.id);
|
|
1784
|
+
}
|
|
1785
|
+
/**
|
|
1786
|
+
* An action box's label: the call that launched it, as the tooltip shows it —
|
|
1787
|
+
* `watchBuildDirectory = watchDir(…)` — then omkit's name when the callee does not already
|
|
1788
|
+
* say it (`command(…) · npm run dev`). Just the name when the call is unknown.
|
|
1789
|
+
*/
|
|
1790
|
+
function boxLabel(n) {
|
|
1791
|
+
const c = callOf(n);
|
|
1792
|
+
if (!c) return displayName(n);
|
|
1793
|
+
const call = `${c.bind ? `${c.bind} = ` : ""}${c.callee}(${hasArgs(n) ? "…" : ""})`;
|
|
1794
|
+
return c.callee === displayName(n) ? call : `${call} · ${displayName(n)}`;
|
|
1795
|
+
}
|
|
1796
|
+
function labelHtml(n) {
|
|
1797
|
+
const c = callOf(n);
|
|
1798
|
+
if (!c) return esc(displayName(n));
|
|
1799
|
+
const name = c.callee === displayName(n) ? "" : `<span class="sep"> · </span>${esc(displayName(n))}`;
|
|
1800
|
+
return `<span class="tcall">${callHtml(n, c)}</span>${name}`;
|
|
1801
|
+
}
|
|
1802
|
+
/** `page = chromePage(…)`, from {@link callExpr}. */
|
|
1803
|
+
function callHtml(n, call) {
|
|
1804
|
+
return `${call.bind ? `<span class="b">${esc(call.bind)}</span> = ` : ""}<span class="f">${esc(call.callee)}</span>(${hasArgs(n) ? "…" : ""})`;
|
|
1805
|
+
}
|
|
1806
|
+
function hasPopup(n) {
|
|
1807
|
+
return codeTabs(n).length > 0;
|
|
1808
|
+
}
|
|
1809
|
+
function popupTip(n) {
|
|
1810
|
+
return codeTabs(n).map((t) => esc(t.label)).join(" · ");
|
|
1811
|
+
}
|
|
1812
|
+
function hasArgs(n) {
|
|
1813
|
+
return !!(n.v.args && n.v.args.length);
|
|
1814
|
+
}
|
|
1815
|
+
function argsOf(n) {
|
|
1816
|
+
return n.v.args.length === 1 ? n.v.args[0] : n.v.args;
|
|
1860
1817
|
}
|
|
1861
1818
|
function relOf(site) {
|
|
1862
1819
|
return (model.data.sources && model.data.sources[site]) || String(site).replace(/:\d+(:\d+)?$/, "");
|
|
@@ -1864,27 +1821,32 @@
|
|
|
1864
1821
|
function hasValue(n) {
|
|
1865
1822
|
return n.v.value !== undefined || !!n.v.valueNote;
|
|
1866
1823
|
}
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
}
|
|
1871
|
-
/** The returned value in the code view: pretty JSON, highlighted, numbered, copyable. */
|
|
1872
|
-
function valueHtml(n) {
|
|
1873
|
-
if (n.v.value === undefined) {
|
|
1874
|
-
return `<div class="pp"><div class="muted">${esc(n.v.valueNote || "Returned nothing.")}</div></div>`;
|
|
1875
|
-
}
|
|
1876
|
-
const text = json(n.v.value);
|
|
1824
|
+
/** A value in the code view: pretty JSON, highlighted, numbered, copyable. */
|
|
1825
|
+
function jsonHtml(value, what) {
|
|
1826
|
+
const text = json(value);
|
|
1877
1827
|
const lines = highlight(text);
|
|
1878
|
-
const kind = Array.isArray(
|
|
1828
|
+
const kind = Array.isArray(value) ? `array · ${value.length} items` : value === null ? "null" : typeof value === "object" ? `object · ${Object.keys(value).length} keys` : typeof value;
|
|
1879
1829
|
return `<div class="cp-tools"><span class="muted">${esc(kind)} · ${lines.length} line${lines.length > 1 ? "s" : ""} · ${fmtSize(text.length)}</span>
|
|
1880
|
-
<button class="btn solo small" data-a="copy-json">${svgIcon("copy")}Copy JSON</button></div>
|
|
1830
|
+
<button class="btn solo small" data-a="copy-json" data-what="${what}">${svgIcon("copy")}Copy JSON</button></div>
|
|
1881
1831
|
<div class="cp-code">${lines
|
|
1882
1832
|
.map((html, i) => `<div class="cl"><span class="ln">${i + 1}</span><span class="lc">${html || " "}</span></div>`)
|
|
1883
1833
|
.join("")}</div>`;
|
|
1884
1834
|
}
|
|
1835
|
+
function valueHtml(n) {
|
|
1836
|
+
if (n.v.value === undefined) {
|
|
1837
|
+
return `<div class="pp"><div class="muted">${esc(n.v.valueNote || "Returned nothing.")}</div></div>`;
|
|
1838
|
+
}
|
|
1839
|
+
return jsonHtml(n.v.value, "value");
|
|
1840
|
+
}
|
|
1841
|
+
function argsHtml(n) {
|
|
1842
|
+
return hasArgs(n) ? jsonHtml(argsOf(n), "args") : '<div class="pp"><div class="muted">No arguments.</div></div>';
|
|
1843
|
+
}
|
|
1885
1844
|
function wireValue(root, n) {
|
|
1886
|
-
|
|
1887
|
-
|
|
1845
|
+
root.querySelectorAll('[data-a="copy-json"]').forEach((b) =>
|
|
1846
|
+
b.addEventListener("click", () =>
|
|
1847
|
+
b.dataset.what === "args" ? copy(json(argsOf(n)), "Copied the args") : copy(json(n.v.value), "Copied the returned value")
|
|
1848
|
+
)
|
|
1849
|
+
);
|
|
1888
1850
|
}
|
|
1889
1851
|
function codeHtml(site) {
|
|
1890
1852
|
const c = codeOf(site);
|
|
@@ -1927,7 +1889,7 @@
|
|
|
1927
1889
|
if (codeFor === n.id && (!want || (toggle && codeTab === want))) return closeCode();
|
|
1928
1890
|
const tabs = codeTabs(n);
|
|
1929
1891
|
if (!tabs.length) return;
|
|
1930
|
-
const tab = tabs.find((t) => t.key === want) || tabs.find((t) => t.key !== "value") || tabs[0];
|
|
1892
|
+
const tab = tabs.find((t) => t.key === want) || tabs.find((t) => t.key !== "value" && t.key !== "args") || tabs[0];
|
|
1931
1893
|
codeTab = tab.key;
|
|
1932
1894
|
hideTip();
|
|
1933
1895
|
const reopen = codeFor !== n.id;
|
|
@@ -1943,7 +1905,9 @@
|
|
|
1943
1905
|
<button class="icon-btn" data-a="close" aria-label="Close">${svgIcon("close")}</button>
|
|
1944
1906
|
</div>
|
|
1945
1907
|
${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>` : ""}
|
|
1946
|
-
<div class="cp-body">${
|
|
1908
|
+
<div class="cp-body">${
|
|
1909
|
+
tab.key === "prompt" ? promptHtml(promptOf(n)) : tab.key === "value" ? valueHtml(n) : tab.key === "args" ? argsHtml(n) : codeHtml(site)
|
|
1910
|
+
}</div>`;
|
|
1947
1911
|
wireValue(codeEl, n);
|
|
1948
1912
|
const cp = codeEl.querySelector('[data-a="copy"]');
|
|
1949
1913
|
if (cp) cp.addEventListener("click", () => copy(rel, `Copied ${rel}`));
|
|
@@ -1964,7 +1928,7 @@
|
|
|
1964
1928
|
if (at) at.scrollIntoView({ block: "center" });
|
|
1965
1929
|
if (codeAway) document.removeEventListener("mousedown", codeAway, true);
|
|
1966
1930
|
codeAway = (e) => {
|
|
1967
|
-
if (!codeEl.contains(e.target) && !e.target.closest(".code-btn, [data-show-code]
|
|
1931
|
+
if (!codeEl.contains(e.target) && !e.target.closest(".code-btn, [data-show-code]")) closeCode();
|
|
1968
1932
|
};
|
|
1969
1933
|
setTimeout(() => codeAway && document.addEventListener("mousedown", codeAway, true), 0);
|
|
1970
1934
|
}
|
|
@@ -2020,7 +1984,6 @@
|
|
|
2020
1984
|
function renderAll() {
|
|
2021
1985
|
measure();
|
|
2022
1986
|
if (state.view === "timeline") renderTimeline();
|
|
2023
|
-
if (state.view === "flame") renderFlame();
|
|
2024
1987
|
if (state.view === "flow") renderFlow();
|
|
2025
1988
|
renderOverview();
|
|
2026
1989
|
}
|
|
@@ -2094,7 +2057,6 @@
|
|
|
2094
2057
|
</div>
|
|
2095
2058
|
<div class="btn-group">
|
|
2096
2059
|
<button class="btn active" data-view="timeline">${svgIcon("timeline")}Timeline + Graph</button>
|
|
2097
|
-
<button class="btn" data-view="flame">${svgIcon("flame")}Flame Graph</button>
|
|
2098
2060
|
<button class="btn" data-view="flow">${svgIcon("flow")}Flow</button>
|
|
2099
2061
|
</div>
|
|
2100
2062
|
<div class="btn-group">
|
|
@@ -2108,20 +2070,15 @@
|
|
|
2108
2070
|
<button class="btn solo" id="more" title="More">${svgIcon("more")}</button>
|
|
2109
2071
|
</header>
|
|
2110
2072
|
<div class="main">
|
|
2111
|
-
<section class="card timeline" style="--tree-w:
|
|
2073
|
+
<section class="card timeline" style="--tree-w:360px;--row-h:${ROW_H}px;position:relative">
|
|
2112
2074
|
<div class="tl-head">
|
|
2113
|
-
<div class="cols"><div>Action / Event</div
|
|
2075
|
+
<div class="cols"><div>Action / Event</div></div>
|
|
2114
2076
|
<svg></svg>
|
|
2115
2077
|
</div>
|
|
2116
2078
|
<div class="tl-body"><div class="tree"></div><div class="chart"></div></div>
|
|
2117
|
-
<div class="flame-view" hidden style="top:34px"></div>
|
|
2118
2079
|
<div class="flow-view" hidden></div>
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
</div>
|
|
2122
|
-
<div class="bottom">
|
|
2123
|
-
<section class="card lower"></section>
|
|
2124
|
-
<section class="card legend">
|
|
2080
|
+
<button class="info-btn legend-btn" id="legend-btn" aria-label="Legend" aria-expanded="false" title="Legend">${svgIcon("info")}</button>
|
|
2081
|
+
<div class="legend legend-pop" id="legend" hidden>
|
|
2125
2082
|
<h3>Legend</h3>
|
|
2126
2083
|
<div class="grid2">
|
|
2127
2084
|
${legendItem("--c1", "Action run", "bar")}
|
|
@@ -2134,7 +2091,12 @@
|
|
|
2134
2091
|
${legendItem("--accent", "Pinned time", "line")}
|
|
2135
2092
|
</div>
|
|
2136
2093
|
<div class="muted" style="margin-top:12px;font-size:11px">Ctrl + wheel to zoom · drag to pan · double-click a bar to focus · ↑/↓ to step</div>
|
|
2094
|
+
</div>
|
|
2137
2095
|
</section>
|
|
2096
|
+
<aside class="card details"></aside>
|
|
2097
|
+
</div>
|
|
2098
|
+
<div class="bottom">
|
|
2099
|
+
<section class="card lower"></section>
|
|
2138
2100
|
<section class="card overview"><h3>Overview</h3><div class="range"></div><svg></svg></section>
|
|
2139
2101
|
</div>`;
|
|
2140
2102
|
}
|
|
@@ -2285,6 +2247,23 @@
|
|
|
2285
2247
|
n = Math.max(1, Math.floor(n));
|
|
2286
2248
|
return s.length > n ? `${s.slice(0, Math.max(1, n - 1))}…` : s;
|
|
2287
2249
|
}
|
|
2250
|
+
/** A tag's colour: hashed from its text, so a tag reads the same in every view. */
|
|
2251
|
+
function tagColor(t) {
|
|
2252
|
+
let h = 0;
|
|
2253
|
+
for (let i = 0; i < t.length; i++) h = (h * 31 + t.charCodeAt(i)) | 0;
|
|
2254
|
+
return css(PALETTE[Math.abs(h) % PALETTE.length]);
|
|
2255
|
+
}
|
|
2256
|
+
/** One tag as a chip — the tree, tooltips and details share it; the flow draws the same in SVG. */
|
|
2257
|
+
function chipHtml(t, max) {
|
|
2258
|
+
const c = tagColor(t);
|
|
2259
|
+
return `<span class="chip" style="background:${alpha(c, 0.14)};border-color:${alpha(c, 0.45)}" title="${esc(t)}">${esc(truncate(t, max))}</span>`;
|
|
2260
|
+
}
|
|
2261
|
+
/** Up to `limit` tag chips, then a "+N" chip holding the rest. */
|
|
2262
|
+
function chipsHtml(tags, limit, max) {
|
|
2263
|
+
if (!tags || !tags.length) return "";
|
|
2264
|
+
const more = tags.length > limit ? `<span class="chip more" title="${esc(tags.slice(limit).join(", "))}">+${tags.length - limit}</span>` : "";
|
|
2265
|
+
return `<span class="chips">${tags.slice(0, limit).map((t) => chipHtml(t, max)).join("")}${more}</span>`;
|
|
2266
|
+
}
|
|
2288
2267
|
function displayName(n) {
|
|
2289
2268
|
if (n.depth === 0) return "main";
|
|
2290
2269
|
const p = promptOf(n);
|