@tangle-network/charts 0.15.1 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -6
- package/dist/{flow-trace-B5dlYNsD.d.ts → flow-trace-Cti_6fZP.d.ts} +25 -1
- package/dist/index.d.ts +56 -2
- package/dist/index.js +234 -1
- package/dist/react/flow-waterfall.css +297 -68
- package/dist/react/index.d.ts +17 -3
- package/dist/react/index.js +1747 -368
- package/package.json +1 -1
package/dist/react/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { G as hatchPath, J as markerPath, b as isRefusal, h as eventsGap, t as timeline, u as MARK_HEADS, y as markName } from "../timeline-CVXBpZzt.js";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
-
import { Fragment as Fragment$1, useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
3
|
+
import { Fragment as Fragment$1, memo, useCallback, useEffect, useId, useLayoutEffect, useMemo, useRef, useState } from "react";
|
|
4
4
|
//#region src/react/sparkline.tsx
|
|
5
5
|
const DEFAULT_SPARKLINE_WIDTH = 96;
|
|
6
6
|
const DEFAULT_SPARKLINE_HEIGHT = 24;
|
|
@@ -652,9 +652,9 @@ const durationStyle = {
|
|
|
652
652
|
fontFamily: "var(--font-mono, monospace)",
|
|
653
653
|
fontVariantNumeric: "tabular-nums"
|
|
654
654
|
};
|
|
655
|
-
const ordered = (start, end) => Number.isFinite(start) && Number.isFinite(end) && end >= start;
|
|
655
|
+
const ordered$1 = (start, end) => Number.isFinite(start) && Number.isFinite(end) && end >= start;
|
|
656
656
|
function WaterfallTimeAxis({ windowMs, originMs = 0, widthPx = 208, formatDuration = formatWaterfallDuration, className, fluid = false }) {
|
|
657
|
-
if (!ordered(0, windowMs) || !Number.isFinite(originMs)) return null;
|
|
657
|
+
if (!ordered$1(0, windowMs) || !Number.isFinite(originMs)) return null;
|
|
658
658
|
const ticks = waterfallTicks(windowMs, widthPx);
|
|
659
659
|
if (windowMs === 0) return null;
|
|
660
660
|
return /* @__PURE__ */ jsx("div", {
|
|
@@ -706,7 +706,7 @@ function WaterfallBar({ startMs, endMs, window, tone = "default", formatDuration
|
|
|
706
706
|
minWidth: 0,
|
|
707
707
|
flex: "1 1 0%"
|
|
708
708
|
} : timingCell;
|
|
709
|
-
if (!ordered(startMs, endMs) || !ordered(window.startMs, window.endMs)) return /* @__PURE__ */ jsx("span", {
|
|
709
|
+
if (!ordered$1(startMs, endMs) || !ordered$1(window.startMs, window.endMs)) return /* @__PURE__ */ jsx("span", {
|
|
710
710
|
className,
|
|
711
711
|
"data-waterfall-track": "",
|
|
712
712
|
"data-unavailable": "true",
|
|
@@ -779,7 +779,7 @@ function WaterfallRow({ label, attribution, startMs, endMs, window, tone, format
|
|
|
779
779
|
"data-waterfall-duration": "",
|
|
780
780
|
className: durationClassName,
|
|
781
781
|
style: durationStyle,
|
|
782
|
-
children: durationLabel ?? (ordered(startMs, endMs) ? formatDuration(endMs - startMs) : "—")
|
|
782
|
+
children: durationLabel ?? (ordered$1(startMs, endMs) ? formatDuration(endMs - startMs) : "—")
|
|
783
783
|
})
|
|
784
784
|
]
|
|
785
785
|
});
|
|
@@ -2306,11 +2306,68 @@ function DotMap({ positions, radii, colors, palette, extent, rects, labels, dimm
|
|
|
2306
2306
|
}
|
|
2307
2307
|
//#endregion
|
|
2308
2308
|
//#region src/react/flow-waterfall-model.ts
|
|
2309
|
+
/** The CSS hue slot: 0 is the root agent's neutral, then five agent hues cycle. */
|
|
2310
|
+
const toneKey = (tone) => tone <= 0 ? 0 : (tone - 1) % 5 + 1;
|
|
2309
2311
|
const spanFailed = (span) => span.status === "failed";
|
|
2310
2312
|
const spanRunning = (span) => span.status === "running";
|
|
2311
|
-
|
|
2313
|
+
const ordered = (span) => Number.isFinite(span.startMs) && Number.isFinite(span.endMs) && span.endMs >= span.startMs;
|
|
2314
|
+
/** 542ms, 6.9s, 23s, 1m 22s, 2h 05m: the precision a reader compares at each scale. */
|
|
2315
|
+
function formatSpanDuration(ms) {
|
|
2316
|
+
if (!Number.isFinite(ms) || ms < 0) return "—";
|
|
2317
|
+
if (ms < 1e3) return `${Math.round(ms)}ms`;
|
|
2318
|
+
if (ms < 1e4) return `${(ms / 1e3).toFixed(1)}s`;
|
|
2319
|
+
if (ms < 6e4) return `${Math.round(ms / 1e3)}s`;
|
|
2320
|
+
if (ms < 36e5) {
|
|
2321
|
+
const m = Math.floor(ms / 6e4), s = Math.round(ms % 6e4 / 1e3);
|
|
2322
|
+
return s === 60 ? `${m + 1}m` : s ? `${m}m ${s}s` : `${m}m`;
|
|
2323
|
+
}
|
|
2324
|
+
const h = Math.floor(ms / 36e5), m = Math.round(ms % 36e5 / 6e4);
|
|
2325
|
+
return m === 60 ? `${h + 1}h` : `${h}h ${String(m).padStart(2, "0")}m`;
|
|
2326
|
+
}
|
|
2327
|
+
/** $0.12, $0.005, $12.40; never rounds a recorded nonzero cost to $0.00. */
|
|
2328
|
+
function formatCost(usd) {
|
|
2329
|
+
if (usd === 0) return "$0.00";
|
|
2330
|
+
if (usd < .01) return `$${usd.toPrecision(1).replace(/0+$/, "")}`;
|
|
2331
|
+
return `$${usd.toFixed(2)}`;
|
|
2332
|
+
}
|
|
2333
|
+
/** 9.8k, 754.4k, 1.2M with the exact count left to a title. */
|
|
2334
|
+
function formatCount(n) {
|
|
2335
|
+
if (n < 1e3) return String(n);
|
|
2336
|
+
if (n < 1e6) return `${(n / 1e3).toFixed(1)}k`;
|
|
2337
|
+
return `${(n / 1e6).toFixed(1)}M`;
|
|
2338
|
+
}
|
|
2339
|
+
/** Merge marks closer than `gapMs` so a dense row stays a readable strip of at most `cap` marks. Marks narrower
|
|
2340
|
+
* than the gap merge across kinds and take the kind that covers more time; failures and retries stay distinct. */
|
|
2341
|
+
function mergeSegments(marks, gapMs, cap = 240) {
|
|
2342
|
+
marks.sort((a, b) => a.startMs - b.startMs);
|
|
2343
|
+
let gap = gapMs;
|
|
2344
|
+
for (let pass = 0; pass < 40; pass++, gap *= 2) {
|
|
2345
|
+
const out = [];
|
|
2346
|
+
const covered = /* @__PURE__ */ new Map();
|
|
2347
|
+
for (const mark of marks) {
|
|
2348
|
+
const last = out[out.length - 1];
|
|
2349
|
+
const width = Math.min(mark.endMs, mark.startMs + gap * 4) - mark.startMs;
|
|
2350
|
+
const distinct = (m) => m.retry || m.kind === "failed";
|
|
2351
|
+
const small = width < gap || last !== void 0 && last.endMs - last.startMs < gap;
|
|
2352
|
+
if (last && !distinct(last) && !distinct(mark) && last.tone === mark.tone && mark.startMs - last.endMs <= gap && (last.kind === mark.kind || small)) {
|
|
2353
|
+
const share = covered.get(last) ?? {
|
|
2354
|
+
model: last.kind === "model" ? last.endMs - last.startMs : 0,
|
|
2355
|
+
tool: last.kind === "tool" ? last.endMs - last.startMs : 0
|
|
2356
|
+
};
|
|
2357
|
+
share[mark.kind === "model" ? "model" : "tool"] += width;
|
|
2358
|
+
covered.set(last, share);
|
|
2359
|
+
last.endMs = Math.max(last.endMs, mark.endMs);
|
|
2360
|
+
last.count += mark.count;
|
|
2361
|
+
last.kind = share.model >= share.tool ? "model" : "tool";
|
|
2362
|
+
} else out.push({ ...mark });
|
|
2363
|
+
}
|
|
2364
|
+
if (out.length <= cap) return out;
|
|
2365
|
+
}
|
|
2366
|
+
return marks;
|
|
2367
|
+
}
|
|
2368
|
+
/** Hierarchy only; presentation owns geometry.
|
|
2312
2369
|
* Orphaned parents become roots; cycles are broken without dropping any span. */
|
|
2313
|
-
function flowWaterfallRows(trace) {
|
|
2370
|
+
function flowWaterfallRows(trace, resolutionMs) {
|
|
2314
2371
|
const ids = /* @__PURE__ */ new Set();
|
|
2315
2372
|
const rows = [...trace.spans].map((span, i) => ({
|
|
2316
2373
|
span,
|
|
@@ -2325,12 +2382,18 @@ function flowWaterfallRows(trace) {
|
|
|
2325
2382
|
parentId: span.parentId,
|
|
2326
2383
|
name: span.name,
|
|
2327
2384
|
kind: span.kind,
|
|
2328
|
-
durationLabel:
|
|
2385
|
+
durationLabel: ordered(span) ? `${formatSpanDuration(span.endMs - span.startMs)}${span.approx ? "~" : ""}` : "—",
|
|
2329
2386
|
approx: span.approx === true,
|
|
2330
2387
|
ok: !spanFailed(span),
|
|
2331
2388
|
depth: 0,
|
|
2332
2389
|
hasChildren: false,
|
|
2333
|
-
span
|
|
2390
|
+
span,
|
|
2391
|
+
tone: -1,
|
|
2392
|
+
descendants: 0,
|
|
2393
|
+
segments: [],
|
|
2394
|
+
own: [],
|
|
2395
|
+
retry: span.retryOf !== void 0,
|
|
2396
|
+
hasAgents: false
|
|
2334
2397
|
};
|
|
2335
2398
|
});
|
|
2336
2399
|
const byId = new Map(rows.map((row) => [row.id, row]));
|
|
@@ -2353,15 +2416,51 @@ function flowWaterfallRows(trace) {
|
|
|
2353
2416
|
children.set(row.parentId, siblings);
|
|
2354
2417
|
}
|
|
2355
2418
|
}
|
|
2356
|
-
const
|
|
2357
|
-
const
|
|
2419
|
+
const total = Math.max(1, Number.isFinite(trace.totalMs) ? trace.totalMs : 1);
|
|
2420
|
+
const gap = resolutionMs ?? total / 900;
|
|
2421
|
+
const ordered_ = [];
|
|
2422
|
+
let agents = 0;
|
|
2423
|
+
const visit = (row, depth, tone) => {
|
|
2358
2424
|
row.depth = depth;
|
|
2359
|
-
row.
|
|
2360
|
-
|
|
2361
|
-
|
|
2425
|
+
row.tone = row.kind === "pipeline" ? agents++ : tone;
|
|
2426
|
+
const kids = children.get(row.id) ?? [];
|
|
2427
|
+
row.hasChildren = kids.length > 0;
|
|
2428
|
+
ordered_.push(row);
|
|
2429
|
+
const marks = [];
|
|
2430
|
+
let cost = row.span.costUsd ?? 0;
|
|
2431
|
+
let costKnown = row.span.kind !== "model" || row.span.costUsd !== void 0;
|
|
2432
|
+
for (const child of kids) {
|
|
2433
|
+
for (const mark of visit(child, depth + 1, row.tone)) marks.push(mark);
|
|
2434
|
+
row.descendants += 1 + child.descendants;
|
|
2435
|
+
if (child.kind === "pipeline") row.hasAgents = true;
|
|
2436
|
+
const childCost = child.hasChildren ? child.rollupCostUsd : child.span.costUsd;
|
|
2437
|
+
if (childCost === void 0) {
|
|
2438
|
+
if (child.kind === "model" || child.hasChildren) costKnown = false;
|
|
2439
|
+
} else cost += childCost;
|
|
2440
|
+
if (child.kind !== "pipeline" && child.kind !== "waiting" && ordered(child.span)) row.own.push(segmentOf(child));
|
|
2441
|
+
}
|
|
2442
|
+
if (row.hasChildren && costKnown) row.rollupCostUsd = cost;
|
|
2443
|
+
if (row.hasChildren) {
|
|
2444
|
+
row.segments = mergeSegments(marks.map((mark) => ({ ...mark })), gap);
|
|
2445
|
+
row.own = mergeSegments(row.own, gap);
|
|
2446
|
+
}
|
|
2447
|
+
if (row.kind === "pipeline" || row.kind === "waiting" || !ordered(row.span)) return marks;
|
|
2448
|
+
marks.push(segmentOf(row));
|
|
2449
|
+
return marks;
|
|
2450
|
+
};
|
|
2451
|
+
for (const row of rows) if (!row.parentId) visit(row, 0, -1);
|
|
2452
|
+
return ordered_;
|
|
2453
|
+
}
|
|
2454
|
+
function segmentOf(row) {
|
|
2455
|
+
return {
|
|
2456
|
+
startMs: row.span.startMs,
|
|
2457
|
+
endMs: spanRunning(row.span) ? Number.POSITIVE_INFINITY : row.span.endMs,
|
|
2458
|
+
kind: !row.ok ? "failed" : row.kind === "model" ? "model" : "tool",
|
|
2459
|
+
tone: row.tone,
|
|
2460
|
+
id: row.id,
|
|
2461
|
+
count: 1,
|
|
2462
|
+
retry: row.retry
|
|
2362
2463
|
};
|
|
2363
|
-
for (const row of rows) if (!row.parentId) visit(row, 0);
|
|
2364
|
-
return ordered;
|
|
2365
2464
|
}
|
|
2366
2465
|
/** Search keeps ancestor context and deliberately reveals matches inside collapsed branches. */
|
|
2367
2466
|
function visibleFlowWaterfallRows(rows, query, collapsed) {
|
|
@@ -2370,7 +2469,7 @@ function visibleFlowWaterfallRows(rows, query, collapsed) {
|
|
|
2370
2469
|
if (term) {
|
|
2371
2470
|
const matching = /* @__PURE__ */ new Set();
|
|
2372
2471
|
for (const row of rows) {
|
|
2373
|
-
if (!`${row.name} ${row.kind} ${row.ok ? "" : "failed"} ${row.span.status ?? ""}`.toLowerCase().includes(term)) continue;
|
|
2472
|
+
if (!`${row.name} ${row.span.detail ?? ""} ${row.kind} ${row.ok ? "" : "failed"} ${row.span.status ?? ""}`.toLowerCase().includes(term)) continue;
|
|
2374
2473
|
let cursor = row;
|
|
2375
2474
|
while (cursor && !matching.has(cursor.id)) {
|
|
2376
2475
|
matching.add(cursor.id);
|
|
@@ -2386,34 +2485,1066 @@ function visibleFlowWaterfallRows(rows, query, collapsed) {
|
|
|
2386
2485
|
return !isHidden;
|
|
2387
2486
|
});
|
|
2388
2487
|
}
|
|
2488
|
+
/** The leaf a reader should look at first: the longest completed or running non-agent step. */
|
|
2489
|
+
function slowestRow(rows, clockMs) {
|
|
2490
|
+
let best;
|
|
2491
|
+
let bestMs = -1;
|
|
2492
|
+
for (const row of rows) {
|
|
2493
|
+
if (row.hasChildren || row.kind === "pipeline" || row.kind === "waiting" || row.durationLabel === "—") continue;
|
|
2494
|
+
if (row.span.startMs > clockMs) continue;
|
|
2495
|
+
const ms = (spanRunning(row.span) ? Math.max(row.span.endMs, clockMs) : Math.min(row.span.endMs, clockMs)) - row.span.startMs;
|
|
2496
|
+
if (ms > bestMs) {
|
|
2497
|
+
best = row;
|
|
2498
|
+
bestMs = ms;
|
|
2499
|
+
}
|
|
2500
|
+
}
|
|
2501
|
+
return best;
|
|
2502
|
+
}
|
|
2503
|
+
/** Busy time per agent row: the union of its own work. Their sum over wall time is the parallel speedup. */
|
|
2504
|
+
function busyMs(row, clockMs) {
|
|
2505
|
+
let total = 0, end = -Infinity;
|
|
2506
|
+
for (const mark of row.own) {
|
|
2507
|
+
const a = Math.max(mark.startMs, end), b = Math.min(mark.endMs, clockMs);
|
|
2508
|
+
if (b > a) total += b - a;
|
|
2509
|
+
end = Math.max(end, Math.min(mark.endMs, clockMs));
|
|
2510
|
+
}
|
|
2511
|
+
return total;
|
|
2512
|
+
}
|
|
2513
|
+
/** Totals as of a playback position, from per-span records. A total the spans do not carry stays the trace's own. */
|
|
2514
|
+
function totalsAt(trace, clockMs) {
|
|
2515
|
+
let cost = 0, tokens = 0, models = 0, tools = 0, errors = 0;
|
|
2516
|
+
let costs = false, counted = false;
|
|
2517
|
+
for (const span of trace.spans) {
|
|
2518
|
+
if (span.startMs > clockMs) continue;
|
|
2519
|
+
if (span.kind === "model") models++;
|
|
2520
|
+
if (span.kind === "tool") tools++;
|
|
2521
|
+
if (span.endMs > clockMs) continue;
|
|
2522
|
+
if (spanFailed(span)) errors++;
|
|
2523
|
+
if (span.costUsd !== void 0) {
|
|
2524
|
+
cost += span.costUsd;
|
|
2525
|
+
costs = true;
|
|
2526
|
+
}
|
|
2527
|
+
if (span.promptTokens !== void 0 || span.completionTokens !== void 0) {
|
|
2528
|
+
tokens += (span.promptTokens ?? 0) + (span.completionTokens ?? 0);
|
|
2529
|
+
counted = true;
|
|
2530
|
+
}
|
|
2531
|
+
}
|
|
2532
|
+
return {
|
|
2533
|
+
costUsd: trace.costUsd === void 0 ? void 0 : costs ? cost : trace.costUsd,
|
|
2534
|
+
tokens: counted ? tokens : trace.promptTokens + trace.completionTokens,
|
|
2535
|
+
modelCalls: trace.modelCalls === void 0 ? void 0 : models,
|
|
2536
|
+
toolCalls: tools,
|
|
2537
|
+
errors
|
|
2538
|
+
};
|
|
2539
|
+
}
|
|
2389
2540
|
//#endregion
|
|
2390
|
-
//#region src/react/flow-waterfall.tsx
|
|
2391
|
-
const
|
|
2392
|
-
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2408
|
-
|
|
2409
|
-
|
|
2410
|
-
|
|
2411
|
-
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2541
|
+
//#region src/react/flow-waterfall-icons.tsx
|
|
2542
|
+
const svg = (children, label) => /* @__PURE__ */ jsx("svg", {
|
|
2543
|
+
viewBox: "0 0 16 16",
|
|
2544
|
+
width: "14",
|
|
2545
|
+
height: "14",
|
|
2546
|
+
fill: "none",
|
|
2547
|
+
stroke: "currentColor",
|
|
2548
|
+
strokeWidth: "1.5",
|
|
2549
|
+
strokeLinecap: "round",
|
|
2550
|
+
strokeLinejoin: "round",
|
|
2551
|
+
"aria-hidden": label ? void 0 : true,
|
|
2552
|
+
role: label ? "img" : void 0,
|
|
2553
|
+
"aria-label": label,
|
|
2554
|
+
children
|
|
2555
|
+
});
|
|
2556
|
+
/** Kind glyphs drawn when the host supplies no `renderIcon`. Brand marks belong to the host. */
|
|
2557
|
+
function KindGlyph({ kind }) {
|
|
2558
|
+
switch (kind) {
|
|
2559
|
+
case "pipeline": return svg(/* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2560
|
+
/* @__PURE__ */ jsx("circle", {
|
|
2561
|
+
cx: "8",
|
|
2562
|
+
cy: "4",
|
|
2563
|
+
r: "1.75"
|
|
2564
|
+
}),
|
|
2565
|
+
/* @__PURE__ */ jsx("circle", {
|
|
2566
|
+
cx: "4",
|
|
2567
|
+
cy: "12",
|
|
2568
|
+
r: "1.75"
|
|
2569
|
+
}),
|
|
2570
|
+
/* @__PURE__ */ jsx("circle", {
|
|
2571
|
+
cx: "12",
|
|
2572
|
+
cy: "12",
|
|
2573
|
+
r: "1.75"
|
|
2574
|
+
}),
|
|
2575
|
+
/* @__PURE__ */ jsx("path", { d: "M8 5.75v2.5M8 8.25 4.8 10.6M8 8.25l3.2 2.35" })
|
|
2576
|
+
] }));
|
|
2577
|
+
case "model": return svg(/* @__PURE__ */ jsx("path", { d: "M8 2.5c.5 2.8 2.2 4.6 5 5.5-2.8.9-4.5 2.7-5 5.5-.5-2.8-2.2-4.6-5-5.5 2.8-.9 4.5-2.7 5-5.5Z" }));
|
|
2578
|
+
case "waiting": return svg(/* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("path", { d: "M4.5 2.5h7M4.5 13.5h7" }), /* @__PURE__ */ jsx("path", { d: "M5.5 2.5c0 3 5 3 5 5.5s-5 2.5-5 5.5M10.5 2.5c0 3-5 3-5 5.5" })] }));
|
|
2579
|
+
case "handoff": return svg(/* @__PURE__ */ jsx("path", { d: "M3 3v5.5a2 2 0 0 0 2 2h8M10.5 8l2.5 2.5-2.5 2.5" }));
|
|
2580
|
+
default: return svg(/* @__PURE__ */ jsx("path", { d: "M10.2 2.8a3.2 3.2 0 0 0-4 4L2.8 10.2a1.4 1.4 0 0 0 2 2l3.4-3.4a3.2 3.2 0 0 0 4-4l-1.9 1.9-1.6-.4-.4-1.6 1.9-1.9Z" }));
|
|
2415
2581
|
}
|
|
2582
|
+
}
|
|
2583
|
+
const ChevronGlyph = () => svg(/* @__PURE__ */ jsx("path", { d: "m6 4 4 4-4 4" }));
|
|
2584
|
+
const SearchGlyph = () => svg(/* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("circle", {
|
|
2585
|
+
cx: "7",
|
|
2586
|
+
cy: "7",
|
|
2587
|
+
r: "4.25"
|
|
2588
|
+
}), /* @__PURE__ */ jsx("path", { d: "m10.25 10.25 3.25 3.25" })] }));
|
|
2589
|
+
const PlayGlyph = () => /* @__PURE__ */ jsx("svg", {
|
|
2590
|
+
viewBox: "0 0 16 16",
|
|
2591
|
+
width: "14",
|
|
2592
|
+
height: "14",
|
|
2593
|
+
"aria-hidden": true,
|
|
2594
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
2595
|
+
d: "M4.5 3.2v9.6a.6.6 0 0 0 .9.5l7.6-4.8a.6.6 0 0 0 0-1L5.4 2.7a.6.6 0 0 0-.9.5Z",
|
|
2596
|
+
fill: "currentColor"
|
|
2597
|
+
})
|
|
2598
|
+
});
|
|
2599
|
+
const PauseGlyph = () => /* @__PURE__ */ jsxs("svg", {
|
|
2600
|
+
viewBox: "0 0 16 16",
|
|
2601
|
+
width: "14",
|
|
2602
|
+
height: "14",
|
|
2603
|
+
"aria-hidden": true,
|
|
2604
|
+
children: [/* @__PURE__ */ jsx("rect", {
|
|
2605
|
+
x: "4",
|
|
2606
|
+
y: "3",
|
|
2607
|
+
width: "2.75",
|
|
2608
|
+
height: "10",
|
|
2609
|
+
rx: ".8",
|
|
2610
|
+
fill: "currentColor"
|
|
2611
|
+
}), /* @__PURE__ */ jsx("rect", {
|
|
2612
|
+
x: "9.25",
|
|
2613
|
+
y: "3",
|
|
2614
|
+
width: "2.75",
|
|
2615
|
+
height: "10",
|
|
2616
|
+
rx: ".8",
|
|
2617
|
+
fill: "currentColor"
|
|
2618
|
+
})]
|
|
2619
|
+
});
|
|
2620
|
+
const RestartGlyph = () => svg(/* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("path", { d: "M3 8a5 5 0 1 0 1.5-3.55" }), /* @__PURE__ */ jsx("path", { d: "M3 2.75V5.5h2.75" })] }));
|
|
2621
|
+
const RetryGlyph = ({ label }) => svg(/* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("path", { d: "M12.5 8A4.5 4.5 0 1 1 11 4.6" }), /* @__PURE__ */ jsx("path", { d: "M11.5 2v3h-3" })] }), label);
|
|
2622
|
+
const FailGlyph = ({ label }) => svg(/* @__PURE__ */ jsx("path", { d: "m4.5 4.5 7 7m0-7-7 7" }), label);
|
|
2623
|
+
const CloseGlyph = () => svg(/* @__PURE__ */ jsx("path", { d: "m4.5 4.5 7 7m0-7-7 7" }));
|
|
2624
|
+
const TraceGlyph = () => svg(/* @__PURE__ */ jsx("path", { d: "M8 1.5v3M8 11.5v3M1.5 8h3M11.5 8h3M3.4 3.4l2.1 2.1M10.5 10.5l2.1 2.1M3.4 12.6l2.1-2.1M10.5 5.5l2.1-2.1" }));
|
|
2625
|
+
const AlertGlyph = ({ label }) => svg(/* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("path", { d: "M8 2.5 14 13H2L8 2.5Z" }), /* @__PURE__ */ jsx("path", { d: "M8 6.5v3M8 11.25v.01" })] }), label);
|
|
2626
|
+
const ZoomInGlyph = () => svg(/* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("circle", {
|
|
2627
|
+
cx: "7",
|
|
2628
|
+
cy: "7",
|
|
2629
|
+
r: "4.25"
|
|
2630
|
+
}), /* @__PURE__ */ jsx("path", { d: "m10.25 10.25 3.25 3.25M5 7h4M7 5v4" })] }));
|
|
2631
|
+
const ZoomOutGlyph = () => svg(/* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("circle", {
|
|
2632
|
+
cx: "7",
|
|
2633
|
+
cy: "7",
|
|
2634
|
+
r: "4.25"
|
|
2635
|
+
}), /* @__PURE__ */ jsx("path", { d: "m10.25 10.25 3.25 3.25M5 7h4" })] }));
|
|
2636
|
+
const FitGlyph = () => svg(/* @__PURE__ */ jsx("path", { d: "M2.5 6V3.5a1 1 0 0 1 1-1H6M10 2.5h2.5a1 1 0 0 1 1 1V6M13.5 10v2.5a1 1 0 0 1-1 1H10M6 13.5H3.5a1 1 0 0 1-1-1V10" }));
|
|
2637
|
+
const ReturnGlyph = () => svg(/* @__PURE__ */ jsx("path", { d: "M6 3.5 3 6.5l3 3M3 6.5h6.5a3.5 3.5 0 0 1 3.5 3.5v2" }));
|
|
2638
|
+
//#endregion
|
|
2639
|
+
//#region src/react/flow-waterfall-card.tsx
|
|
2640
|
+
const KIND_LABEL = {
|
|
2641
|
+
pipeline: "Agent",
|
|
2642
|
+
model: "LLM call",
|
|
2643
|
+
tool: "Tool call",
|
|
2644
|
+
waiting: "Waiting",
|
|
2645
|
+
handoff: "Handoff"
|
|
2416
2646
|
};
|
|
2647
|
+
const unknown = /* @__PURE__ */ jsx("span", {
|
|
2648
|
+
className: "fw-unknown",
|
|
2649
|
+
title: "Not recorded by the harness",
|
|
2650
|
+
children: "Unknown"
|
|
2651
|
+
});
|
|
2652
|
+
const count = (n) => n === void 0 ? unknown : /* @__PURE__ */ jsx("span", {
|
|
2653
|
+
title: n.toLocaleString(),
|
|
2654
|
+
children: formatCount(n)
|
|
2655
|
+
});
|
|
2656
|
+
/** The span card: what ran, for how long, at what recorded cost, on what input. */
|
|
2657
|
+
function SpanCard({ id, row, agent, clockMs, left, top, pinned, onClose, renderIcon, compact }) {
|
|
2658
|
+
const span = row.span;
|
|
2659
|
+
const running = spanRunning(span);
|
|
2660
|
+
const end = running ? Math.max(span.endMs, clockMs) : span.endMs;
|
|
2661
|
+
const status = running ? "Running" : row.ok ? "Done" : "Failed";
|
|
2662
|
+
const tokens = span.kind === "model" || span.kind === "pipeline" || span.promptTokens !== void 0;
|
|
2663
|
+
const owner = agent && agent.id !== row.id ? agent : void 0;
|
|
2664
|
+
const duration = Number.isFinite(end - span.startMs) && end >= span.startMs ? formatSpanDuration(end - span.startMs) : "—";
|
|
2665
|
+
if (compact) {
|
|
2666
|
+
const who = owner ?? row;
|
|
2667
|
+
const tokenCount = span.promptTokens !== void 0 || span.completionTokens !== void 0 ? (span.promptTokens ?? 0) + (span.completionTokens ?? 0) : void 0;
|
|
2668
|
+
const meta = [
|
|
2669
|
+
span.kind === "model" ? "Thinking" : KIND_LABEL[span.kind],
|
|
2670
|
+
duration,
|
|
2671
|
+
tokenCount !== void 0 ? `${tokenCount.toLocaleString()} tokens` : void 0,
|
|
2672
|
+
span.costUsd !== void 0 ? formatCost(span.costUsd) : void 0
|
|
2673
|
+
];
|
|
2674
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2675
|
+
id,
|
|
2676
|
+
role: pinned ? "dialog" : "tooltip",
|
|
2677
|
+
"aria-label": pinned ? `${span.name} details` : void 0,
|
|
2678
|
+
className: "fw-card fw-card-compact",
|
|
2679
|
+
"data-tone": toneKey(who.tone),
|
|
2680
|
+
style: {
|
|
2681
|
+
left,
|
|
2682
|
+
top
|
|
2683
|
+
},
|
|
2684
|
+
onKeyDown: (event) => {
|
|
2685
|
+
if (event.key === "Escape") {
|
|
2686
|
+
event.stopPropagation();
|
|
2687
|
+
onClose();
|
|
2688
|
+
}
|
|
2689
|
+
},
|
|
2690
|
+
children: [
|
|
2691
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2692
|
+
className: "fw-card-agent",
|
|
2693
|
+
children: [
|
|
2694
|
+
/* @__PURE__ */ jsx("span", {
|
|
2695
|
+
className: "fw-agent-dot",
|
|
2696
|
+
"aria-hidden": true
|
|
2697
|
+
}),
|
|
2698
|
+
who.name,
|
|
2699
|
+
who.span.detail && /* @__PURE__ */ jsxs("span", {
|
|
2700
|
+
className: "fw-muted",
|
|
2701
|
+
children: [" ", who.span.detail]
|
|
2702
|
+
}),
|
|
2703
|
+
!row.ok && /* @__PURE__ */ jsx("span", {
|
|
2704
|
+
className: "fw-pill fw-pill-failed",
|
|
2705
|
+
style: { marginLeft: "auto" },
|
|
2706
|
+
children: "Failed"
|
|
2707
|
+
}),
|
|
2708
|
+
pinned && /* @__PURE__ */ jsx("button", {
|
|
2709
|
+
type: "button",
|
|
2710
|
+
className: "fw-icon-button",
|
|
2711
|
+
style: {
|
|
2712
|
+
marginLeft: row.ok ? "auto" : 0,
|
|
2713
|
+
width: 20,
|
|
2714
|
+
height: 20
|
|
2715
|
+
},
|
|
2716
|
+
"aria-label": "Close details",
|
|
2717
|
+
onClick: onClose,
|
|
2718
|
+
autoFocus: true,
|
|
2719
|
+
children: /* @__PURE__ */ jsx(CloseGlyph, {})
|
|
2720
|
+
})
|
|
2721
|
+
]
|
|
2722
|
+
}),
|
|
2723
|
+
/* @__PURE__ */ jsx("div", {
|
|
2724
|
+
className: "fw-card-result",
|
|
2725
|
+
children: span.output ?? span.name
|
|
2726
|
+
}),
|
|
2727
|
+
/* @__PURE__ */ jsx("div", {
|
|
2728
|
+
className: "fw-card-meta",
|
|
2729
|
+
children: meta.filter(Boolean).join(" · ")
|
|
2730
|
+
}),
|
|
2731
|
+
span.input && /* @__PURE__ */ jsxs("div", {
|
|
2732
|
+
className: "fw-card-message",
|
|
2733
|
+
children: [/* @__PURE__ */ jsx(ReturnGlyph, {}), /* @__PURE__ */ jsx("span", { children: span.input })]
|
|
2734
|
+
})
|
|
2735
|
+
]
|
|
2736
|
+
});
|
|
2737
|
+
}
|
|
2738
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2739
|
+
id,
|
|
2740
|
+
role: pinned ? "dialog" : "tooltip",
|
|
2741
|
+
"aria-label": pinned ? `${span.name} details` : void 0,
|
|
2742
|
+
className: "fw-card",
|
|
2743
|
+
"data-tone": toneKey(row.tone),
|
|
2744
|
+
style: {
|
|
2745
|
+
left,
|
|
2746
|
+
top
|
|
2747
|
+
},
|
|
2748
|
+
onKeyDown: (event) => {
|
|
2749
|
+
if (event.key === "Escape") {
|
|
2750
|
+
event.stopPropagation();
|
|
2751
|
+
onClose();
|
|
2752
|
+
}
|
|
2753
|
+
},
|
|
2754
|
+
children: [
|
|
2755
|
+
owner && /* @__PURE__ */ jsxs("div", {
|
|
2756
|
+
className: "fw-card-agent",
|
|
2757
|
+
children: [
|
|
2758
|
+
/* @__PURE__ */ jsx("span", {
|
|
2759
|
+
className: "fw-agent-dot",
|
|
2760
|
+
"aria-hidden": true
|
|
2761
|
+
}),
|
|
2762
|
+
owner.name,
|
|
2763
|
+
owner.span.detail && /* @__PURE__ */ jsxs("span", {
|
|
2764
|
+
className: "fw-muted",
|
|
2765
|
+
children: [" ", owner.span.detail]
|
|
2766
|
+
})
|
|
2767
|
+
]
|
|
2768
|
+
}),
|
|
2769
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2770
|
+
className: "fw-card-head",
|
|
2771
|
+
children: [
|
|
2772
|
+
/* @__PURE__ */ jsx("span", {
|
|
2773
|
+
className: "fw-icon-tile",
|
|
2774
|
+
"aria-hidden": true,
|
|
2775
|
+
children: renderIcon?.(span) ?? /* @__PURE__ */ jsx(KindGlyph, { kind: span.kind })
|
|
2776
|
+
}),
|
|
2777
|
+
/* @__PURE__ */ jsxs("div", {
|
|
2778
|
+
className: "fw-card-title",
|
|
2779
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
2780
|
+
className: "fw-card-name",
|
|
2781
|
+
children: span.name
|
|
2782
|
+
}), span.detail && /* @__PURE__ */ jsx("div", {
|
|
2783
|
+
className: "fw-muted",
|
|
2784
|
+
children: span.detail
|
|
2785
|
+
})]
|
|
2786
|
+
}),
|
|
2787
|
+
/* @__PURE__ */ jsx("span", {
|
|
2788
|
+
className: `fw-pill fw-pill-${status.toLowerCase()}`,
|
|
2789
|
+
children: status
|
|
2790
|
+
}),
|
|
2791
|
+
pinned && /* @__PURE__ */ jsx("button", {
|
|
2792
|
+
type: "button",
|
|
2793
|
+
className: "fw-icon-button",
|
|
2794
|
+
"aria-label": "Close details",
|
|
2795
|
+
onClick: onClose,
|
|
2796
|
+
autoFocus: true,
|
|
2797
|
+
children: /* @__PURE__ */ jsx(CloseGlyph, {})
|
|
2798
|
+
})
|
|
2799
|
+
]
|
|
2800
|
+
}),
|
|
2801
|
+
/* @__PURE__ */ jsxs("dl", {
|
|
2802
|
+
className: "fw-card-stats",
|
|
2803
|
+
children: [
|
|
2804
|
+
/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", { children: "Duration" }), /* @__PURE__ */ jsx("dd", { children: duration })] }),
|
|
2805
|
+
/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", { children: "Started" }), /* @__PURE__ */ jsxs("dd", { children: ["+", formatSpanDuration(Math.max(0, span.startMs))] })] }),
|
|
2806
|
+
/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", { children: "Cost" }), /* @__PURE__ */ jsx("dd", { children: span.costUsd === void 0 ? unknown : formatCost(span.costUsd) })] }),
|
|
2807
|
+
tokens && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", { children: "Tokens in" }), /* @__PURE__ */ jsx("dd", { children: count(span.promptTokens) })] }),
|
|
2808
|
+
tokens && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", { children: "Tokens out" }), /* @__PURE__ */ jsx("dd", { children: count(span.completionTokens) })] }),
|
|
2809
|
+
row.hasChildren && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", { children: "Steps" }), /* @__PURE__ */ jsx("dd", { children: row.descendants })] })
|
|
2810
|
+
]
|
|
2811
|
+
}),
|
|
2812
|
+
span.input && /* @__PURE__ */ jsxs("div", {
|
|
2813
|
+
className: "fw-card-text",
|
|
2814
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
2815
|
+
className: "fw-card-label",
|
|
2816
|
+
children: "Input"
|
|
2817
|
+
}), /* @__PURE__ */ jsx("p", { children: span.input })]
|
|
2818
|
+
}),
|
|
2819
|
+
span.output && /* @__PURE__ */ jsxs("div", {
|
|
2820
|
+
className: "fw-card-text",
|
|
2821
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
2822
|
+
className: "fw-card-label",
|
|
2823
|
+
children: row.ok ? "Output" : "Error"
|
|
2824
|
+
}), /* @__PURE__ */ jsx("p", { children: span.output })]
|
|
2825
|
+
})
|
|
2826
|
+
]
|
|
2827
|
+
});
|
|
2828
|
+
}
|
|
2829
|
+
//#endregion
|
|
2830
|
+
//#region src/react/flow-waterfall-row.tsx
|
|
2831
|
+
/** Running ends are drawn to the clock; CSS clips with min(), so a far-future end stands in for "still open". */
|
|
2832
|
+
const OPEN_END = 0x9184e72a000;
|
|
2833
|
+
const at = (startMs, endMs) => ({
|
|
2834
|
+
"--s": startMs,
|
|
2835
|
+
"--e": Number.isFinite(endMs) ? endMs : OPEN_END
|
|
2836
|
+
});
|
|
2837
|
+
function Segment({ mark, events, agentView }) {
|
|
2838
|
+
const glyph = mark.kind === "failed" ? /* @__PURE__ */ jsx(FailGlyph, {}) : mark.retry ? /* @__PURE__ */ jsx(RetryGlyph, {}) : null;
|
|
2839
|
+
return /* @__PURE__ */ jsx("span", {
|
|
2840
|
+
className: `fw-seg fw-seg-${mark.kind}`,
|
|
2841
|
+
"data-tone": toneKey(mark.tone),
|
|
2842
|
+
style: at(mark.startMs, mark.endMs),
|
|
2843
|
+
onPointerEnter: agentView ? (event) => events.onInspect(mark.id, event.currentTarget) : void 0,
|
|
2844
|
+
onClick: agentView ? (event) => {
|
|
2845
|
+
event.stopPropagation();
|
|
2846
|
+
events.onPin(mark.id, event.currentTarget);
|
|
2847
|
+
} : void 0,
|
|
2848
|
+
children: glyph && /* @__PURE__ */ jsx("span", {
|
|
2849
|
+
className: "fw-glyph",
|
|
2850
|
+
children: glyph
|
|
2851
|
+
})
|
|
2852
|
+
});
|
|
2853
|
+
}
|
|
2854
|
+
/** A span the clock is inside: it grows live, or playback has not reached its end. */
|
|
2855
|
+
const open = (row, clockMs) => spanRunning(row.span) || row.span.startMs <= clockMs && clockMs < row.span.endMs;
|
|
2856
|
+
function endLabel(row, clockMs) {
|
|
2857
|
+
const span = row.span;
|
|
2858
|
+
if (row.durationLabel === "—") return "—";
|
|
2859
|
+
if (row.approx && span.endMs === span.startMs && !spanRunning(span)) return span.costUsd === void 0 ? "" : formatCost(span.costUsd);
|
|
2860
|
+
if (!spanRunning(span) && clockMs < span.endMs) return `${formatSpanDuration(Math.max(0, clockMs - span.startMs))}${row.approx ? "~" : ""}`;
|
|
2861
|
+
const duration = spanRunning(span) ? `${formatSpanDuration(Math.max(span.endMs, clockMs) - span.startMs)}${row.approx ? "~" : ""}` : row.durationLabel;
|
|
2862
|
+
const cost = span.costUsd ?? (row.rollupCostUsd ? row.rollupCostUsd : void 0);
|
|
2863
|
+
return cost === void 0 ? duration : `${duration} · ${formatCost(cost)}`;
|
|
2864
|
+
}
|
|
2865
|
+
/** One trace row. Geometry comes from CSS variables, so a row re-renders only when its own state changes. */
|
|
2866
|
+
const FlowRow = memo(function FlowRow(props) {
|
|
2867
|
+
const { row, view, top, collapsed, searching, slowest, active, focusable, selected, sticky, clockMs, renderIcon } = props;
|
|
2868
|
+
const span = row.span;
|
|
2869
|
+
const running = spanRunning(span);
|
|
2870
|
+
const agentView = view === "agents";
|
|
2871
|
+
const folded = row.hasChildren && collapsed && !searching;
|
|
2872
|
+
const invalid = row.durationLabel === "—";
|
|
2873
|
+
const isAgent = row.kind === "pipeline";
|
|
2874
|
+
const kind = !row.ok ? "failed" : row.kind;
|
|
2875
|
+
const marks = isAgent || row.hasChildren ? folded && !agentView ? row.segments : row.own : [];
|
|
2876
|
+
const label = endLabel(row, clockMs);
|
|
2877
|
+
const inspect = (event) => props.onInspect(row.id, event.currentTarget);
|
|
2878
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
2879
|
+
role: "row",
|
|
2880
|
+
"data-span-id": row.id,
|
|
2881
|
+
"data-span-kind": kind,
|
|
2882
|
+
"data-slowest": slowest || void 0,
|
|
2883
|
+
"aria-level": agentView ? void 0 : row.depth + 1,
|
|
2884
|
+
"aria-expanded": row.hasChildren && !agentView ? !folded : void 0,
|
|
2885
|
+
"aria-selected": selected,
|
|
2886
|
+
tabIndex: focusable ? 0 : -1,
|
|
2887
|
+
"data-tone": toneKey(row.tone),
|
|
2888
|
+
className: `fw-row${active ? " fw-active" : ""}${sticky ? " fw-sticky-row" : ""}${isAgent ? " fw-agent-row" : ""}`,
|
|
2889
|
+
style: { transform: `translateY(${top}px)` },
|
|
2890
|
+
onFocus: (event) => {
|
|
2891
|
+
if (event.target === event.currentTarget) props.onActivate(row);
|
|
2892
|
+
},
|
|
2893
|
+
onClick: () => props.onActivate(row),
|
|
2894
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
2895
|
+
role: "gridcell",
|
|
2896
|
+
className: "fw-label",
|
|
2897
|
+
style: { "--depth": agentView ? 0 : row.depth },
|
|
2898
|
+
children: [
|
|
2899
|
+
!agentView && /* @__PURE__ */ jsx("span", {
|
|
2900
|
+
className: "fw-guides",
|
|
2901
|
+
"aria-hidden": true
|
|
2902
|
+
}),
|
|
2903
|
+
!agentView && (row.hasChildren ? /* @__PURE__ */ jsx("button", {
|
|
2904
|
+
type: "button",
|
|
2905
|
+
tabIndex: -1,
|
|
2906
|
+
className: `fw-toggle${folded ? "" : " fw-open"}`,
|
|
2907
|
+
disabled: searching,
|
|
2908
|
+
"aria-label": `${folded ? "Expand" : "Collapse"} ${row.name}`,
|
|
2909
|
+
"aria-expanded": !folded,
|
|
2910
|
+
onClick: (event) => {
|
|
2911
|
+
event.stopPropagation();
|
|
2912
|
+
props.onToggle(row);
|
|
2913
|
+
},
|
|
2914
|
+
children: /* @__PURE__ */ jsx(ChevronGlyph, {})
|
|
2915
|
+
}) : /* @__PURE__ */ jsx("span", {
|
|
2916
|
+
className: "fw-toggle-space",
|
|
2917
|
+
"aria-hidden": true
|
|
2918
|
+
})),
|
|
2919
|
+
agentView && /* @__PURE__ */ jsx("span", {
|
|
2920
|
+
className: "fw-agent-dot",
|
|
2921
|
+
"aria-hidden": true
|
|
2922
|
+
}),
|
|
2923
|
+
!agentView && /* @__PURE__ */ jsx("span", {
|
|
2924
|
+
className: "fw-icon-tile",
|
|
2925
|
+
"aria-hidden": true,
|
|
2926
|
+
children: renderIcon?.(span) ?? /* @__PURE__ */ jsx(KindGlyph, { kind: span.kind })
|
|
2927
|
+
}),
|
|
2928
|
+
/* @__PURE__ */ jsxs("span", {
|
|
2929
|
+
className: "fw-names",
|
|
2930
|
+
title: `${row.name}${span.detail ? ` · ${span.detail}` : ""} · ${KIND_LABEL[span.kind]}${running ? " · running" : ""}${row.ok ? "" : " · failed"}`,
|
|
2931
|
+
children: [/* @__PURE__ */ jsxs("span", {
|
|
2932
|
+
className: `fw-span-name${isAgent || row.hasChildren ? " fw-strong" : ""}`,
|
|
2933
|
+
children: [running && /* @__PURE__ */ jsx("span", {
|
|
2934
|
+
className: "fw-spinner",
|
|
2935
|
+
role: "status",
|
|
2936
|
+
"aria-label": "running"
|
|
2937
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
2938
|
+
className: "fw-name-text",
|
|
2939
|
+
children: row.name
|
|
2940
|
+
})]
|
|
2941
|
+
}), agentView && renderIcon && /* @__PURE__ */ jsx("span", {
|
|
2942
|
+
className: "fw-detail-icon",
|
|
2943
|
+
"aria-hidden": true,
|
|
2944
|
+
children: renderIcon(span)
|
|
2945
|
+
})]
|
|
2946
|
+
}),
|
|
2947
|
+
folded && /* @__PURE__ */ jsxs("span", {
|
|
2948
|
+
className: "fw-count",
|
|
2949
|
+
"aria-label": `${row.descendants} hidden steps`,
|
|
2950
|
+
children: ["+", row.descendants]
|
|
2951
|
+
}),
|
|
2952
|
+
!row.ok && /* @__PURE__ */ jsx("span", {
|
|
2953
|
+
className: "fw-alert",
|
|
2954
|
+
children: /* @__PURE__ */ jsx(AlertGlyph, { label: "failed" })
|
|
2955
|
+
})
|
|
2956
|
+
]
|
|
2957
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
2958
|
+
role: "gridcell",
|
|
2959
|
+
className: "fw-lane",
|
|
2960
|
+
"data-waterfall-track": true,
|
|
2961
|
+
"aria-label": `${KIND_LABEL[span.kind]}, ${label}${slowest ? ", slowest step" : ""}`,
|
|
2962
|
+
children: invalid ? /* @__PURE__ */ jsx("span", {
|
|
2963
|
+
className: "fw-invalid",
|
|
2964
|
+
children: "—"
|
|
2965
|
+
}) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
2966
|
+
/* @__PURE__ */ jsxs("span", {
|
|
2967
|
+
className: `fw-bar fw-k-${isAgent ? "pipeline" : kind}${row.approx ? " fw-approx" : ""}${marks.length ? " fw-has-marks" : ""}${row.hasAgents ? " fw-has-agents" : ""}`,
|
|
2968
|
+
"data-tone": toneKey(row.tone),
|
|
2969
|
+
style: at(span.startMs, running ? OPEN_END : span.endMs),
|
|
2970
|
+
onPointerEnter: inspect,
|
|
2971
|
+
onPointerLeave: props.onLeave,
|
|
2972
|
+
onClick: (event) => {
|
|
2973
|
+
event.stopPropagation();
|
|
2974
|
+
props.onPin(row.id, event.currentTarget);
|
|
2975
|
+
},
|
|
2976
|
+
children: [!isAgent && !row.ok && /* @__PURE__ */ jsx("span", {
|
|
2977
|
+
className: "fw-glyph",
|
|
2978
|
+
children: /* @__PURE__ */ jsx(FailGlyph, {})
|
|
2979
|
+
}), !isAgent && row.ok && row.retry && /* @__PURE__ */ jsx("span", {
|
|
2980
|
+
className: "fw-glyph",
|
|
2981
|
+
children: /* @__PURE__ */ jsx(RetryGlyph, {})
|
|
2982
|
+
})]
|
|
2983
|
+
}),
|
|
2984
|
+
marks.map((mark, i) => /* @__PURE__ */ jsx(Segment, {
|
|
2985
|
+
mark,
|
|
2986
|
+
events: props,
|
|
2987
|
+
agentView
|
|
2988
|
+
}, `${mark.id}-${i}`)),
|
|
2989
|
+
!agentView && /* @__PURE__ */ jsxs("span", {
|
|
2990
|
+
className: "fw-end",
|
|
2991
|
+
style: at(span.startMs, running ? 0x9184e72a000 : span.endMs),
|
|
2992
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
2993
|
+
className: "fw-duration",
|
|
2994
|
+
children: label
|
|
2995
|
+
}), slowest && /* @__PURE__ */ jsx("span", {
|
|
2996
|
+
className: "fw-badge",
|
|
2997
|
+
children: "Slowest"
|
|
2998
|
+
})]
|
|
2999
|
+
})
|
|
3000
|
+
] })
|
|
3001
|
+
})]
|
|
3002
|
+
});
|
|
3003
|
+
}, (a, b) => a.row === b.row && a.view === b.view && a.top === b.top && a.collapsed === b.collapsed && a.searching === b.searching && a.slowest === b.slowest && a.active === b.active && a.focusable === b.focusable && a.selected === b.selected && a.sticky === b.sticky && a.renderIcon === b.renderIcon && (a.clockMs === b.clockMs || !open(a.row, a.clockMs) && !open(b.row, b.clockMs)));
|
|
3004
|
+
/** An elbow connector between two rows at one moment, drawn in the row's tone; appears when the clock reaches it. */
|
|
3005
|
+
function LinkMark({ link, rowHeight }) {
|
|
3006
|
+
const upper = Math.min(link.from, link.to), lower = Math.max(link.from, link.to);
|
|
3007
|
+
const style = {
|
|
3008
|
+
...at(link.atMs, link.atMs),
|
|
3009
|
+
top: upper * rowHeight + rowHeight / 2 + 5,
|
|
3010
|
+
height: (lower - upper) * rowHeight - 5
|
|
3011
|
+
};
|
|
3012
|
+
return /* @__PURE__ */ jsx("span", {
|
|
3013
|
+
className: `fw-link fw-link-${link.dir}`,
|
|
3014
|
+
"data-tone": toneKey(link.tone),
|
|
3015
|
+
style,
|
|
3016
|
+
"aria-hidden": true
|
|
3017
|
+
});
|
|
3018
|
+
}
|
|
3019
|
+
//#endregion
|
|
3020
|
+
//#region src/react/flow-waterfall-time.ts
|
|
3021
|
+
/** Time-axis geometry for the trace inspector: clock-aligned steps, labels and the zoom window. */
|
|
3022
|
+
const STEPS = [
|
|
3023
|
+
1,
|
|
3024
|
+
2,
|
|
3025
|
+
5,
|
|
3026
|
+
10,
|
|
3027
|
+
20,
|
|
3028
|
+
50,
|
|
3029
|
+
100,
|
|
3030
|
+
200,
|
|
3031
|
+
500,
|
|
3032
|
+
1e3,
|
|
3033
|
+
2e3,
|
|
3034
|
+
5e3,
|
|
3035
|
+
1e4,
|
|
3036
|
+
15e3,
|
|
3037
|
+
3e4,
|
|
3038
|
+
6e4,
|
|
3039
|
+
12e4,
|
|
3040
|
+
3e5,
|
|
3041
|
+
6e5,
|
|
3042
|
+
9e5,
|
|
3043
|
+
18e5,
|
|
3044
|
+
36e5,
|
|
3045
|
+
72e5,
|
|
3046
|
+
108e5,
|
|
3047
|
+
216e5,
|
|
3048
|
+
432e5,
|
|
3049
|
+
864e5
|
|
3050
|
+
];
|
|
3051
|
+
/** The smallest clock-aligned step that keeps labels at least `minGapPx` apart. */
|
|
3052
|
+
function clockStepMs(msPerPx, minGapPx = 72) {
|
|
3053
|
+
const want = msPerPx * minGapPx;
|
|
3054
|
+
return STEPS.find((step) => step >= want) ?? Math.ceil(want / 864e5) * 864e5;
|
|
3055
|
+
}
|
|
3056
|
+
/** Tick positions covering [startMs, endMs]. */
|
|
3057
|
+
function clockTicks(startMs, endMs, widthPx, minGapPx = 72) {
|
|
3058
|
+
if (!(widthPx > 0) || !(endMs > startMs)) return [0];
|
|
3059
|
+
const step = clockStepMs((endMs - startMs) / widthPx, minGapPx);
|
|
3060
|
+
const ticks = [];
|
|
3061
|
+
for (let t = Math.ceil(startMs / step) * step; t <= endMs && ticks.length < 400; t += step) ticks.push(t);
|
|
3062
|
+
return ticks;
|
|
3063
|
+
}
|
|
3064
|
+
/** 0s, 500ms, 20s, 1m 30s, 2h, 1h 15m: an axis label at the step's own precision. */
|
|
3065
|
+
function formatClock(ms) {
|
|
3066
|
+
if (ms === 0) return "0s";
|
|
3067
|
+
if (ms < 1e3) return `${Math.round(ms)}ms`;
|
|
3068
|
+
if (ms < 6e4) return `${+(ms / 1e3).toFixed(1)}s`;
|
|
3069
|
+
if (ms < 36e5) {
|
|
3070
|
+
const m = Math.floor(ms / 6e4), s = Math.round(ms % 6e4 / 1e3);
|
|
3071
|
+
return s ? `${m}m ${s}s` : `${m}m`;
|
|
3072
|
+
}
|
|
3073
|
+
const h = Math.floor(ms / 36e5), m = Math.round(ms % 36e5 / 6e4);
|
|
3074
|
+
return m ? `${h}h ${m}m` : `${h}h`;
|
|
3075
|
+
}
|
|
3076
|
+
/** Zoom about an anchor time; the window never inverts, never narrows below 50 ms and never leaves [0, limit]. */
|
|
3077
|
+
function zoomWindow(view, factor, anchorMs, limitMs) {
|
|
3078
|
+
const span = view.endMs - view.startMs;
|
|
3079
|
+
const next = Math.min(Math.max(50, span * factor), Math.max(limitMs, 50));
|
|
3080
|
+
const ratio = span > 0 ? (anchorMs - view.startMs) / span : .5;
|
|
3081
|
+
return clampWindow({
|
|
3082
|
+
startMs: anchorMs - ratio * next,
|
|
3083
|
+
endMs: anchorMs - ratio * next + next
|
|
3084
|
+
}, limitMs);
|
|
3085
|
+
}
|
|
3086
|
+
function clampWindow(view, limitMs) {
|
|
3087
|
+
const span = view.endMs - view.startMs;
|
|
3088
|
+
let start = Math.max(0, view.startMs);
|
|
3089
|
+
if (start + span > limitMs) start = Math.max(0, limitMs - span);
|
|
3090
|
+
return {
|
|
3091
|
+
startMs: start,
|
|
3092
|
+
endMs: start + span
|
|
3093
|
+
};
|
|
3094
|
+
}
|
|
3095
|
+
//#endregion
|
|
3096
|
+
//#region src/react/flow-waterfall-body.tsx
|
|
3097
|
+
const ROW_HEIGHT = {
|
|
3098
|
+
spans: 36,
|
|
3099
|
+
agents: 52
|
|
3100
|
+
};
|
|
3101
|
+
const OVERSCAN = 8;
|
|
3102
|
+
/** Rows re-render once per CHUNK rows scrolled; overscan covers the rows in between. */
|
|
3103
|
+
const CHUNK = 4;
|
|
3104
|
+
const STICKY_LEVELS = 3;
|
|
3105
|
+
/** Spawn links from each agent to its parent agent, and explicit handoffs to their targets; agent view adds returns. */
|
|
3106
|
+
function linksFor(rows, byId, view) {
|
|
3107
|
+
const index = new Map(rows.map((row, i) => [row.id, i]));
|
|
3108
|
+
const owner = (row) => {
|
|
3109
|
+
let id = row.parentId;
|
|
3110
|
+
while (id !== void 0) {
|
|
3111
|
+
const i = index.get(id);
|
|
3112
|
+
if (i !== void 0 && (view === "spans" || rows[i].kind === "pipeline")) return i;
|
|
3113
|
+
id = byId.get(id)?.parentId;
|
|
3114
|
+
}
|
|
3115
|
+
};
|
|
3116
|
+
const links = [];
|
|
3117
|
+
const explicit = /* @__PURE__ */ new Set();
|
|
3118
|
+
for (const row of rows) {
|
|
3119
|
+
const target = row.span.handoffTo !== void 0 ? index.get(row.span.handoffTo) : void 0;
|
|
3120
|
+
const from = row.kind === "pipeline" ? index.get(row.id) : owner(row);
|
|
3121
|
+
if (target === void 0 || from === void 0 || target === from) continue;
|
|
3122
|
+
explicit.add(rows[target].id);
|
|
3123
|
+
links.push({
|
|
3124
|
+
id: `h-${row.id}`,
|
|
3125
|
+
from,
|
|
3126
|
+
to: target,
|
|
3127
|
+
atMs: row.span.startMs,
|
|
3128
|
+
tone: rows[target].tone,
|
|
3129
|
+
dir: target > from ? "down" : "up"
|
|
3130
|
+
});
|
|
3131
|
+
}
|
|
3132
|
+
for (const [i, row] of rows.entries()) {
|
|
3133
|
+
if (row.kind !== "pipeline" || row.parentId === void 0) continue;
|
|
3134
|
+
const parent = owner(row);
|
|
3135
|
+
if (parent === void 0) continue;
|
|
3136
|
+
if (!explicit.has(row.id)) links.push({
|
|
3137
|
+
id: `s-${row.id}`,
|
|
3138
|
+
from: parent,
|
|
3139
|
+
to: i,
|
|
3140
|
+
atMs: row.span.startMs,
|
|
3141
|
+
tone: row.tone,
|
|
3142
|
+
dir: "down"
|
|
3143
|
+
});
|
|
3144
|
+
if (view === "agents" && !spanRunning(row.span) && Number.isFinite(row.span.endMs)) links.push({
|
|
3145
|
+
id: `r-${row.id}`,
|
|
3146
|
+
from: i,
|
|
3147
|
+
to: parent,
|
|
3148
|
+
atMs: row.span.endMs,
|
|
3149
|
+
tone: row.tone,
|
|
3150
|
+
dir: "up"
|
|
3151
|
+
});
|
|
3152
|
+
}
|
|
3153
|
+
return links;
|
|
3154
|
+
}
|
|
3155
|
+
/** The scrolling tree: rows are virtualized, ancestors stay pinned, the axis and cursor stay in view. */
|
|
3156
|
+
function FlowBody(props) {
|
|
3157
|
+
const { rows, view, maxHeight, collapsed, searching, slowestId, activeId, selectedId, clockMs, window: win, trackPx, showCursor, zoomed } = props;
|
|
3158
|
+
const rowHeight = ROW_HEIGHT[view];
|
|
3159
|
+
const scroller = useRef(null);
|
|
3160
|
+
const surface = useRef(null);
|
|
3161
|
+
const [chunk, setChunk] = useState(0);
|
|
3162
|
+
const [viewport, setViewport] = useState(maxHeight);
|
|
3163
|
+
const latest = useRef(props);
|
|
3164
|
+
latest.current = props;
|
|
3165
|
+
const height = Math.min(maxHeight, Math.max(rows.length, 1) * rowHeight);
|
|
3166
|
+
const links = useMemo(() => linksFor(rows, props.byId, view), [
|
|
3167
|
+
rows,
|
|
3168
|
+
props.byId,
|
|
3169
|
+
view
|
|
3170
|
+
]);
|
|
3171
|
+
const index = useMemo(() => new Map(rows.map((row, i) => [row.id, i])), [rows]);
|
|
3172
|
+
const msPerPx = (win.endMs - win.startMs) / Math.max(1, trackPx);
|
|
3173
|
+
const tickEnd = win.startMs + (trackPx + (view === "spans" ? 72 : 0)) * msPerPx;
|
|
3174
|
+
const ticks = clockTicks(win.startMs, tickEnd, Math.max(1, trackPx + (view === "spans" ? 72 : 0)));
|
|
3175
|
+
useEffect(() => {
|
|
3176
|
+
const element = scroller.current;
|
|
3177
|
+
if (!element) return;
|
|
3178
|
+
const observer = new ResizeObserver(() => setViewport(element.clientHeight));
|
|
3179
|
+
observer.observe(element);
|
|
3180
|
+
return () => observer.disconnect();
|
|
3181
|
+
}, []);
|
|
3182
|
+
useEffect(() => {
|
|
3183
|
+
const element = surface.current;
|
|
3184
|
+
if (!element) return;
|
|
3185
|
+
const wheel = (event) => {
|
|
3186
|
+
const { window: w, trackPx: px, zoomed: isZoomed } = latest.current;
|
|
3187
|
+
const lane = element.querySelector("[data-fw-track]");
|
|
3188
|
+
if (!lane) return;
|
|
3189
|
+
const rect = lane.getBoundingClientRect();
|
|
3190
|
+
const msPerPx = (w.endMs - w.startMs) / Math.max(1, px);
|
|
3191
|
+
if (event.ctrlKey || event.metaKey) {
|
|
3192
|
+
event.preventDefault();
|
|
3193
|
+
const anchor = w.startMs + (event.clientX - rect.left - 12) * msPerPx;
|
|
3194
|
+
latest.current.onZoom(Math.exp(event.deltaY * .0025), anchor);
|
|
3195
|
+
} else if (isZoomed && (event.shiftKey || Math.abs(event.deltaX) > Math.abs(event.deltaY))) {
|
|
3196
|
+
event.preventDefault();
|
|
3197
|
+
latest.current.onPan((event.shiftKey ? event.deltaY : event.deltaX) * msPerPx);
|
|
3198
|
+
}
|
|
3199
|
+
};
|
|
3200
|
+
element.addEventListener("wheel", wheel, { passive: false });
|
|
3201
|
+
return () => element.removeEventListener("wheel", wheel);
|
|
3202
|
+
}, []);
|
|
3203
|
+
const first = Math.max(0, chunk * CHUNK - OVERSCAN);
|
|
3204
|
+
const last = Math.min(rows.length, chunk * CHUNK + Math.ceil(viewport / rowHeight) + CHUNK + OVERSCAN);
|
|
3205
|
+
const focusRow = (id) => {
|
|
3206
|
+
const i = index.get(id);
|
|
3207
|
+
const element = scroller.current;
|
|
3208
|
+
if (i === void 0 || !element) return;
|
|
3209
|
+
const offset = (view === "spans" ? Math.min(STICKY_LEVELS, rows[i].depth) : 0) * rowHeight;
|
|
3210
|
+
if (i * rowHeight - offset < element.scrollTop) element.scrollTop = Math.max(0, i * rowHeight - offset);
|
|
3211
|
+
else if ((i + 1) * rowHeight > element.scrollTop + element.clientHeight) element.scrollTop = (i + 1) * rowHeight - element.clientHeight;
|
|
3212
|
+
props.onActiveChange(id);
|
|
3213
|
+
requestAnimationFrame(() => element.querySelector(`.fw-canvas [data-span-id="${CSS.escape(id)}"]`)?.focus({ preventScroll: true }));
|
|
3214
|
+
};
|
|
3215
|
+
const keydown = (event) => {
|
|
3216
|
+
const current = index.get(activeId ?? "") ?? 0;
|
|
3217
|
+
const row = rows[current];
|
|
3218
|
+
if (!row) return;
|
|
3219
|
+
const page = Math.max(1, Math.floor(viewport / rowHeight) - 1);
|
|
3220
|
+
const move = (i) => {
|
|
3221
|
+
event.preventDefault();
|
|
3222
|
+
focusRow(rows[Math.max(0, Math.min(rows.length - 1, i))].id);
|
|
3223
|
+
};
|
|
3224
|
+
const folded = collapsed.has(row.id) && !searching;
|
|
3225
|
+
switch (event.key) {
|
|
3226
|
+
case "ArrowDown": return move(current + 1);
|
|
3227
|
+
case "ArrowUp": return move(current - 1);
|
|
3228
|
+
case "PageDown": return move(current + page);
|
|
3229
|
+
case "PageUp": return move(current - page);
|
|
3230
|
+
case "Home": return move(0);
|
|
3231
|
+
case "End": return move(rows.length - 1);
|
|
3232
|
+
case "ArrowRight":
|
|
3233
|
+
if (view === "spans" && row.hasChildren && folded) {
|
|
3234
|
+
event.preventDefault();
|
|
3235
|
+
props.onToggle(row);
|
|
3236
|
+
} else if (view === "agents" && row.hasChildren) {
|
|
3237
|
+
event.preventDefault();
|
|
3238
|
+
props.onToggle(row);
|
|
3239
|
+
} else if (row.hasChildren) move(current + 1);
|
|
3240
|
+
return;
|
|
3241
|
+
case "ArrowLeft":
|
|
3242
|
+
if (view === "spans" && row.hasChildren && !folded) {
|
|
3243
|
+
event.preventDefault();
|
|
3244
|
+
props.onToggle(row);
|
|
3245
|
+
} else if (row.parentId !== void 0 && index.has(row.parentId)) move(index.get(row.parentId));
|
|
3246
|
+
return;
|
|
3247
|
+
case "Enter":
|
|
3248
|
+
case " ": {
|
|
3249
|
+
event.preventDefault();
|
|
3250
|
+
const bar = scroller.current?.querySelector(`[data-span-id="${CSS.escape(row.id)}"] .fw-bar`);
|
|
3251
|
+
if (bar) props.onPin(row.id, bar);
|
|
3252
|
+
}
|
|
3253
|
+
}
|
|
3254
|
+
};
|
|
3255
|
+
const scrub = (event) => {
|
|
3256
|
+
const lane = event.currentTarget;
|
|
3257
|
+
const rect = lane.getBoundingClientRect();
|
|
3258
|
+
const toMs = (x) => Math.max(0, win.startMs + (x - rect.left - 12) * (win.endMs - win.startMs) / Math.max(1, trackPx));
|
|
3259
|
+
props.onScrub(toMs(event.clientX));
|
|
3260
|
+
lane.setPointerCapture(event.pointerId);
|
|
3261
|
+
const moveScrub = (e) => props.onScrub(toMs(e.clientX));
|
|
3262
|
+
const up = () => {
|
|
3263
|
+
lane.removeEventListener("pointermove", moveScrub);
|
|
3264
|
+
lane.removeEventListener("pointerup", up);
|
|
3265
|
+
};
|
|
3266
|
+
lane.addEventListener("pointermove", moveScrub);
|
|
3267
|
+
lane.addEventListener("pointerup", up);
|
|
3268
|
+
};
|
|
3269
|
+
const resize = (event) => {
|
|
3270
|
+
const handle = event.currentTarget;
|
|
3271
|
+
const startX = event.clientX, startW = props.nameWidth;
|
|
3272
|
+
handle.setPointerCapture(event.pointerId);
|
|
3273
|
+
const moveResize = (e) => props.onNameWidth(startW + e.clientX - startX);
|
|
3274
|
+
const up = () => {
|
|
3275
|
+
handle.removeEventListener("pointermove", moveResize);
|
|
3276
|
+
handle.removeEventListener("pointerup", up);
|
|
3277
|
+
};
|
|
3278
|
+
handle.addEventListener("pointermove", moveResize);
|
|
3279
|
+
handle.addEventListener("pointerup", up);
|
|
3280
|
+
};
|
|
3281
|
+
const axis = /* @__PURE__ */ jsxs("div", {
|
|
3282
|
+
className: `fw-axis fw-axis-${view === "agents" ? "bottom" : "top"}`,
|
|
3283
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
3284
|
+
className: "fw-axis-label",
|
|
3285
|
+
children: [view === "spans" && /* @__PURE__ */ jsx(Fragment, { children: props.onExpandAll && /* @__PURE__ */ jsxs("span", {
|
|
3286
|
+
className: "fw-branch-controls",
|
|
3287
|
+
children: [/* @__PURE__ */ jsx("button", {
|
|
3288
|
+
type: "button",
|
|
3289
|
+
className: "fw-text-button",
|
|
3290
|
+
onClick: props.onExpandAll,
|
|
3291
|
+
children: "Expand all"
|
|
3292
|
+
}), /* @__PURE__ */ jsx("button", {
|
|
3293
|
+
type: "button",
|
|
3294
|
+
className: "fw-text-button",
|
|
3295
|
+
onClick: props.onCollapseAll,
|
|
3296
|
+
children: "Collapse all"
|
|
3297
|
+
})]
|
|
3298
|
+
}) }), /* @__PURE__ */ jsx("span", {
|
|
3299
|
+
role: "separator",
|
|
3300
|
+
"aria-orientation": "vertical",
|
|
3301
|
+
"aria-label": "Resize the step column",
|
|
3302
|
+
tabIndex: 0,
|
|
3303
|
+
className: "fw-resize",
|
|
3304
|
+
"aria-valuenow": Math.round(props.nameWidth),
|
|
3305
|
+
"aria-valuemin": 140,
|
|
3306
|
+
"aria-valuemax": 560,
|
|
3307
|
+
onPointerDown: resize,
|
|
3308
|
+
onKeyDown: (event) => {
|
|
3309
|
+
if (event.key === "ArrowLeft" || event.key === "ArrowRight") {
|
|
3310
|
+
event.preventDefault();
|
|
3311
|
+
props.onNameWidth(props.nameWidth + (event.key === "ArrowLeft" ? -16 : 16));
|
|
3312
|
+
}
|
|
3313
|
+
}
|
|
3314
|
+
})]
|
|
3315
|
+
}), /* @__PURE__ */ jsxs("div", {
|
|
3316
|
+
className: "fw-axis-track",
|
|
3317
|
+
"data-fw-track": true,
|
|
3318
|
+
ref: props.onTrack,
|
|
3319
|
+
onPointerDown: scrub,
|
|
3320
|
+
title: "Drag to scrub",
|
|
3321
|
+
children: [ticks.map((tick) => /* @__PURE__ */ jsx("span", {
|
|
3322
|
+
className: "fw-tick",
|
|
3323
|
+
style: { "--s": tick },
|
|
3324
|
+
children: formatClock(tick)
|
|
3325
|
+
}, tick)), showCursor && /* @__PURE__ */ jsx("span", {
|
|
3326
|
+
className: "fw-cursor-handle",
|
|
3327
|
+
"aria-hidden": true
|
|
3328
|
+
})]
|
|
3329
|
+
})]
|
|
3330
|
+
});
|
|
3331
|
+
const row = (item, i, isSticky = false) => /* @__PURE__ */ jsx(FlowRow, {
|
|
3332
|
+
row: item,
|
|
3333
|
+
view,
|
|
3334
|
+
top: i * rowHeight,
|
|
3335
|
+
collapsed: collapsed.has(item.id),
|
|
3336
|
+
searching,
|
|
3337
|
+
slowest: item.id === slowestId,
|
|
3338
|
+
active: item.id === activeId,
|
|
3339
|
+
focusable: !isSticky && (item.id === activeId || (activeId === void 0 || !index.has(activeId)) && i === 0),
|
|
3340
|
+
selected: item.id === selectedId,
|
|
3341
|
+
sticky: isSticky,
|
|
3342
|
+
clockMs,
|
|
3343
|
+
renderIcon: props.renderIcon,
|
|
3344
|
+
onToggle: props.onToggle,
|
|
3345
|
+
onInspect: props.onInspect,
|
|
3346
|
+
onLeave: props.onLeave,
|
|
3347
|
+
onPin: props.onPin,
|
|
3348
|
+
onActivate: (item) => props.onActiveChange(item.id)
|
|
3349
|
+
}, isSticky ? `sticky-${item.id}` : item.id);
|
|
3350
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
3351
|
+
className: `fw-body fw-view-${view}${zoomed ? " fw-zoomed" : ""}`,
|
|
3352
|
+
ref: surface,
|
|
3353
|
+
children: [
|
|
3354
|
+
view === "spans" && axis,
|
|
3355
|
+
/* @__PURE__ */ jsxs("div", {
|
|
3356
|
+
className: "fw-viewport",
|
|
3357
|
+
children: [
|
|
3358
|
+
/* @__PURE__ */ jsx("div", {
|
|
3359
|
+
className: "fw-gridlayer",
|
|
3360
|
+
"aria-hidden": true,
|
|
3361
|
+
children: ticks.map((tick) => /* @__PURE__ */ jsx("span", {
|
|
3362
|
+
className: "fw-gridline",
|
|
3363
|
+
style: { "--s": tick }
|
|
3364
|
+
}, tick))
|
|
3365
|
+
}),
|
|
3366
|
+
/* @__PURE__ */ jsxs("div", {
|
|
3367
|
+
ref: scroller,
|
|
3368
|
+
role: "treegrid",
|
|
3369
|
+
"aria-label": "Trace timeline",
|
|
3370
|
+
"aria-rowcount": rows.length,
|
|
3371
|
+
"aria-multiselectable": false,
|
|
3372
|
+
tabIndex: -1,
|
|
3373
|
+
className: "fw-scroller",
|
|
3374
|
+
style: {
|
|
3375
|
+
height,
|
|
3376
|
+
maxHeight
|
|
3377
|
+
},
|
|
3378
|
+
onKeyDown: keydown,
|
|
3379
|
+
onScroll: (event) => {
|
|
3380
|
+
const next = Math.floor(event.currentTarget.scrollTop / rowHeight / CHUNK);
|
|
3381
|
+
if (next !== chunk) setChunk(next);
|
|
3382
|
+
},
|
|
3383
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
3384
|
+
className: "fw-canvas",
|
|
3385
|
+
style: { height: rows.length * rowHeight },
|
|
3386
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
3387
|
+
className: "fw-links",
|
|
3388
|
+
"aria-hidden": true,
|
|
3389
|
+
children: links.filter((link) => Math.max(link.from, link.to) >= first && Math.min(link.from, link.to) <= last).map((link) => /* @__PURE__ */ jsx(LinkMark, {
|
|
3390
|
+
link,
|
|
3391
|
+
rowHeight
|
|
3392
|
+
}, link.id))
|
|
3393
|
+
}), rows.slice(first, last).map((item, offset) => row(item, first + offset))]
|
|
3394
|
+
}), rows.length === 0 && /* @__PURE__ */ jsx("p", {
|
|
3395
|
+
role: "status",
|
|
3396
|
+
className: "fw-empty",
|
|
3397
|
+
children: props.emptyText
|
|
3398
|
+
})]
|
|
3399
|
+
}),
|
|
3400
|
+
view === "spans" && /* @__PURE__ */ jsx(StickyRows, {
|
|
3401
|
+
scroller,
|
|
3402
|
+
rows,
|
|
3403
|
+
index,
|
|
3404
|
+
rowHeight,
|
|
3405
|
+
render: row
|
|
3406
|
+
}),
|
|
3407
|
+
props.markers?.length ? /* @__PURE__ */ jsx("div", {
|
|
3408
|
+
className: "fw-marker-layer",
|
|
3409
|
+
children: props.markers.map((marker) => /* @__PURE__ */ jsx("span", {
|
|
3410
|
+
className: "fw-marker",
|
|
3411
|
+
"data-tone": marker.tone ?? "neutral",
|
|
3412
|
+
style: { "--s": marker.at },
|
|
3413
|
+
children: /* @__PURE__ */ jsx("span", {
|
|
3414
|
+
className: "fw-marker-label",
|
|
3415
|
+
children: marker.label
|
|
3416
|
+
})
|
|
3417
|
+
}, `${marker.at}-${marker.label}`))
|
|
3418
|
+
}) : null,
|
|
3419
|
+
showCursor && /* @__PURE__ */ jsx("div", {
|
|
3420
|
+
className: "fw-cursor-layer",
|
|
3421
|
+
"aria-hidden": true,
|
|
3422
|
+
children: /* @__PURE__ */ jsx("span", { className: "fw-cursor" })
|
|
3423
|
+
})
|
|
3424
|
+
]
|
|
3425
|
+
}),
|
|
3426
|
+
view === "agents" && axis
|
|
3427
|
+
]
|
|
3428
|
+
});
|
|
3429
|
+
}
|
|
3430
|
+
/** Ancestors of the first visible row stay pinned while their children scroll beneath them. This layer follows
|
|
3431
|
+
* scroll per row on its own, so the virtualized rows below re-render only per chunk. */
|
|
3432
|
+
function StickyRows({ scroller, rows, index, rowHeight, render }) {
|
|
3433
|
+
const [top, setTop] = useState(0);
|
|
3434
|
+
useEffect(() => {
|
|
3435
|
+
const element = scroller.current;
|
|
3436
|
+
if (!element) return;
|
|
3437
|
+
const scroll = () => setTop(element.scrollTop > 0 ? Math.floor(element.scrollTop / rowHeight) : -1);
|
|
3438
|
+
scroll();
|
|
3439
|
+
element.addEventListener("scroll", scroll, { passive: true });
|
|
3440
|
+
return () => element.removeEventListener("scroll", scroll);
|
|
3441
|
+
}, [scroller, rowHeight]);
|
|
3442
|
+
const sticky = [];
|
|
3443
|
+
if (top >= 0) {
|
|
3444
|
+
const chain = [];
|
|
3445
|
+
let cursor = rows[Math.min(rows.length - 1, top + 1)];
|
|
3446
|
+
while (cursor?.parentId !== void 0) {
|
|
3447
|
+
const parentIndex = index.get(cursor.parentId);
|
|
3448
|
+
if (parentIndex === void 0) break;
|
|
3449
|
+
cursor = rows[parentIndex];
|
|
3450
|
+
if (cursor) chain.unshift(cursor);
|
|
3451
|
+
}
|
|
3452
|
+
for (const [level, row] of chain.slice(-3).entries()) if ((index.get(row.id) ?? 0) < top + level + 1) sticky.push(row);
|
|
3453
|
+
}
|
|
3454
|
+
if (!sticky.length) return null;
|
|
3455
|
+
return /* @__PURE__ */ jsx("div", {
|
|
3456
|
+
className: "fw-sticky",
|
|
3457
|
+
style: { height: sticky.length * rowHeight },
|
|
3458
|
+
children: sticky.map((item, level) => render(item, level, true))
|
|
3459
|
+
});
|
|
3460
|
+
}
|
|
3461
|
+
//#endregion
|
|
3462
|
+
//#region src/react/flow-waterfall-clock.ts
|
|
3463
|
+
const TEXT_INTERVAL_MS = 100;
|
|
3464
|
+
/** Drives the trace clock. Bars, the cursor and the axis read CSS variables written here every frame,
|
|
3465
|
+
* so live growth and playback cost no React render per frame; text updates at 10 Hz. */
|
|
3466
|
+
function useTraceClock(input) {
|
|
3467
|
+
const { root, slider, timeOutput, observedTotal, live, startedAt, snapshot, playing, speed, position, onPlaybackEnd, view, trackPx } = input;
|
|
3468
|
+
const [clockMs, setClockMs] = useState(position ?? observedTotal);
|
|
3469
|
+
const receivedAt = useRef(Date.now());
|
|
3470
|
+
const playhead = useRef(position ?? observedTotal);
|
|
3471
|
+
const latest = useRef(input);
|
|
3472
|
+
latest.current = input;
|
|
3473
|
+
useEffect(() => {
|
|
3474
|
+
receivedAt.current = Date.now();
|
|
3475
|
+
}, [snapshot]);
|
|
3476
|
+
const liveNow = () => {
|
|
3477
|
+
const { observedTotal: total, startedAt: origin, live: isLive } = latest.current;
|
|
3478
|
+
if (!isLive) return total;
|
|
3479
|
+
return Math.max(total, origin === void 0 ? total + Date.now() - receivedAt.current : Date.now() - origin);
|
|
3480
|
+
};
|
|
3481
|
+
const paint = (t, limit = liveNow()) => {
|
|
3482
|
+
const element = root.current;
|
|
3483
|
+
if (!element) return;
|
|
3484
|
+
const { startMs, endMs } = latest.current.view(t);
|
|
3485
|
+
const k = Math.max(1e-9, latest.current.trackPx / Math.max(1, endMs - startMs));
|
|
3486
|
+
element.style.setProperty("--fw-t", String(t));
|
|
3487
|
+
element.style.setProperty("--fw-v0", String(startMs));
|
|
3488
|
+
element.style.setProperty("--fw-k", String(k));
|
|
3489
|
+
if (slider.current) {
|
|
3490
|
+
slider.current.max = String(Math.max(1, limit, t));
|
|
3491
|
+
slider.current.value = String(t);
|
|
3492
|
+
}
|
|
3493
|
+
if (timeOutput.current) timeOutput.current.textContent = formatSpanDuration(t);
|
|
3494
|
+
};
|
|
3495
|
+
useLayoutEffect(() => {
|
|
3496
|
+
if (position !== null) playhead.current = position;
|
|
3497
|
+
if (!(live || playing)) {
|
|
3498
|
+
const t = position ?? observedTotal;
|
|
3499
|
+
paint(t);
|
|
3500
|
+
setClockMs(t);
|
|
3501
|
+
return;
|
|
3502
|
+
}
|
|
3503
|
+
let frame = 0;
|
|
3504
|
+
let last = performance.now();
|
|
3505
|
+
let lastText = 0;
|
|
3506
|
+
const step = (now) => {
|
|
3507
|
+
const delta = now - last;
|
|
3508
|
+
last = now;
|
|
3509
|
+
let t;
|
|
3510
|
+
const limit = liveNow();
|
|
3511
|
+
if (playing) {
|
|
3512
|
+
playhead.current = Math.min(limit, playhead.current + delta * latest.current.speed);
|
|
3513
|
+
t = playhead.current;
|
|
3514
|
+
if (t >= limit && !latest.current.live) {
|
|
3515
|
+
paint(t, limit);
|
|
3516
|
+
setClockMs(t);
|
|
3517
|
+
latest.current.onPlaybackEnd(t);
|
|
3518
|
+
return;
|
|
3519
|
+
}
|
|
3520
|
+
} else t = latest.current.position ?? limit;
|
|
3521
|
+
paint(t, limit);
|
|
3522
|
+
if (now - lastText >= TEXT_INTERVAL_MS) {
|
|
3523
|
+
lastText = now;
|
|
3524
|
+
setClockMs(t);
|
|
3525
|
+
}
|
|
3526
|
+
frame = requestAnimationFrame(step);
|
|
3527
|
+
};
|
|
3528
|
+
frame = requestAnimationFrame(step);
|
|
3529
|
+
return () => cancelAnimationFrame(frame);
|
|
3530
|
+
}, [
|
|
3531
|
+
live,
|
|
3532
|
+
playing,
|
|
3533
|
+
position,
|
|
3534
|
+
observedTotal,
|
|
3535
|
+
snapshot
|
|
3536
|
+
]);
|
|
3537
|
+
useLayoutEffect(() => {
|
|
3538
|
+
paint(playing ? playhead.current : position ?? (live ? liveNow() : observedTotal));
|
|
3539
|
+
}, [view, trackPx]);
|
|
3540
|
+
return {
|
|
3541
|
+
clockMs,
|
|
3542
|
+
limitMs: liveNow,
|
|
3543
|
+
positionMs: () => playhead.current
|
|
3544
|
+
};
|
|
3545
|
+
}
|
|
3546
|
+
//#endregion
|
|
3547
|
+
//#region src/react/flow-waterfall-playback.tsx
|
|
2417
3548
|
const SPEEDS = [
|
|
2418
3549
|
.5,
|
|
2419
3550
|
1,
|
|
@@ -2421,375 +3552,623 @@ const SPEEDS = [
|
|
|
2421
3552
|
2,
|
|
2422
3553
|
4
|
|
2423
3554
|
];
|
|
2424
|
-
|
|
2425
|
-
|
|
2426
|
-
|
|
2427
|
-
|
|
3555
|
+
/** Play, restart, speed, a scrub track over the trace's own activity, zoom and the legend. */
|
|
3556
|
+
function PlaybackBar({ view, playing, limit, clockMs, speed, zoom, marks, slider, timeOutput, followLabel, onTogglePlay, onRestart, onSpeed, onScrub, onFollow, onZoom, onFit }) {
|
|
3557
|
+
return /* @__PURE__ */ jsxs("footer", {
|
|
3558
|
+
className: "fw-playback",
|
|
3559
|
+
children: [
|
|
3560
|
+
/* @__PURE__ */ jsx("button", {
|
|
3561
|
+
type: "button",
|
|
3562
|
+
className: "fw-round-button",
|
|
3563
|
+
"aria-label": playing ? "Pause" : "Play",
|
|
3564
|
+
disabled: limit === 0,
|
|
3565
|
+
onClick: onTogglePlay,
|
|
3566
|
+
children: playing ? /* @__PURE__ */ jsx(PauseGlyph, {}) : /* @__PURE__ */ jsx(PlayGlyph, {})
|
|
3567
|
+
}),
|
|
3568
|
+
/* @__PURE__ */ jsx("button", {
|
|
3569
|
+
type: "button",
|
|
3570
|
+
className: "fw-icon-button",
|
|
3571
|
+
"aria-label": "Restart",
|
|
3572
|
+
title: "Replay from the start",
|
|
3573
|
+
disabled: limit === 0,
|
|
3574
|
+
onClick: onRestart,
|
|
3575
|
+
children: /* @__PURE__ */ jsx(RestartGlyph, {})
|
|
3576
|
+
}),
|
|
3577
|
+
view === "agents" ? /* @__PURE__ */ jsx("div", {
|
|
3578
|
+
className: "fw-segmented",
|
|
3579
|
+
role: "radiogroup",
|
|
3580
|
+
"aria-label": "Playback speed",
|
|
3581
|
+
children: [
|
|
3582
|
+
1,
|
|
3583
|
+
2,
|
|
3584
|
+
4
|
|
3585
|
+
].map((value) => /* @__PURE__ */ jsxs("button", {
|
|
3586
|
+
type: "button",
|
|
3587
|
+
role: "radio",
|
|
3588
|
+
"aria-checked": speed === value,
|
|
3589
|
+
onClick: () => onSpeed(value),
|
|
3590
|
+
children: [value, "x"]
|
|
3591
|
+
}, value))
|
|
3592
|
+
}) : /* @__PURE__ */ jsxs("label", {
|
|
3593
|
+
className: "fw-speed",
|
|
3594
|
+
children: [/* @__PURE__ */ jsx("span", {
|
|
3595
|
+
className: "fw-sr-only",
|
|
3596
|
+
children: "Playback speed"
|
|
3597
|
+
}), /* @__PURE__ */ jsx("select", {
|
|
3598
|
+
"aria-label": "Playback speed",
|
|
3599
|
+
value: speed,
|
|
3600
|
+
onChange: (event) => onSpeed(Number(event.target.value)),
|
|
3601
|
+
children: SPEEDS.map((value) => /* @__PURE__ */ jsxs("option", {
|
|
3602
|
+
value,
|
|
3603
|
+
children: [value, "x"]
|
|
3604
|
+
}, value))
|
|
3605
|
+
})]
|
|
3606
|
+
}),
|
|
3607
|
+
/* @__PURE__ */ jsxs("div", {
|
|
3608
|
+
className: "fw-overview",
|
|
3609
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
3610
|
+
className: "fw-overview-marks",
|
|
3611
|
+
"aria-hidden": true,
|
|
3612
|
+
children: [marks.map((mark, i) => /* @__PURE__ */ jsx("span", {
|
|
3613
|
+
className: `fw-ov fw-seg-${mark.kind}`,
|
|
3614
|
+
"data-tone": toneKey(mark.tone),
|
|
3615
|
+
style: {
|
|
3616
|
+
left: `${mark.startMs / Math.max(1, limit) * 100}%`,
|
|
3617
|
+
width: `${Math.max(.15, (Math.min(mark.endMs, limit) - mark.startMs) / Math.max(1, limit) * 100)}%`
|
|
3618
|
+
}
|
|
3619
|
+
}, i)), zoom && /* @__PURE__ */ jsx("span", {
|
|
3620
|
+
className: "fw-brush",
|
|
3621
|
+
style: {
|
|
3622
|
+
left: `${zoom.startMs / limit * 100}%`,
|
|
3623
|
+
width: `${(zoom.endMs - zoom.startMs) / limit * 100}%`
|
|
3624
|
+
}
|
|
3625
|
+
})]
|
|
3626
|
+
}), /* @__PURE__ */ jsx("input", {
|
|
3627
|
+
ref: slider,
|
|
3628
|
+
"aria-label": "Playback position",
|
|
3629
|
+
type: "range",
|
|
3630
|
+
min: 0,
|
|
3631
|
+
max: Math.max(1, limit),
|
|
3632
|
+
step: "any",
|
|
3633
|
+
defaultValue: clockMs,
|
|
3634
|
+
disabled: limit === 0,
|
|
3635
|
+
className: "fw-slider",
|
|
3636
|
+
style: { "--fw-progress": `${clockMs / Math.max(1, limit) * 100}%` },
|
|
3637
|
+
onChange: (event) => onScrub(Number(event.target.value))
|
|
3638
|
+
})]
|
|
3639
|
+
}),
|
|
3640
|
+
/* @__PURE__ */ jsx("output", {
|
|
3641
|
+
ref: timeOutput,
|
|
3642
|
+
className: "fw-time",
|
|
3643
|
+
"aria-label": "Playback time",
|
|
3644
|
+
children: formatSpanDuration(clockMs)
|
|
3645
|
+
}),
|
|
3646
|
+
followLabel && /* @__PURE__ */ jsx("button", {
|
|
3647
|
+
type: "button",
|
|
3648
|
+
className: "fw-text-button",
|
|
3649
|
+
onClick: onFollow,
|
|
3650
|
+
children: followLabel
|
|
3651
|
+
}),
|
|
3652
|
+
/* @__PURE__ */ jsxs("div", {
|
|
3653
|
+
className: "fw-zoom",
|
|
3654
|
+
role: "group",
|
|
3655
|
+
"aria-label": "Zoom",
|
|
3656
|
+
children: [
|
|
3657
|
+
/* @__PURE__ */ jsx("button", {
|
|
3658
|
+
type: "button",
|
|
3659
|
+
className: "fw-icon-button",
|
|
3660
|
+
"aria-label": "Zoom out",
|
|
3661
|
+
disabled: !zoom,
|
|
3662
|
+
onClick: () => onZoom(2),
|
|
3663
|
+
children: /* @__PURE__ */ jsx(ZoomOutGlyph, {})
|
|
3664
|
+
}),
|
|
3665
|
+
/* @__PURE__ */ jsx("button", {
|
|
3666
|
+
type: "button",
|
|
3667
|
+
className: "fw-icon-button",
|
|
3668
|
+
"aria-label": "Zoom in",
|
|
3669
|
+
disabled: limit === 0,
|
|
3670
|
+
onClick: () => onZoom(.5),
|
|
3671
|
+
children: /* @__PURE__ */ jsx(ZoomInGlyph, {})
|
|
3672
|
+
}),
|
|
3673
|
+
/* @__PURE__ */ jsx("button", {
|
|
3674
|
+
type: "button",
|
|
3675
|
+
className: "fw-icon-button",
|
|
3676
|
+
"aria-label": "Fit the whole trace",
|
|
3677
|
+
disabled: !zoom,
|
|
3678
|
+
onClick: onFit,
|
|
3679
|
+
children: /* @__PURE__ */ jsx(FitGlyph, {})
|
|
3680
|
+
})
|
|
3681
|
+
]
|
|
3682
|
+
})
|
|
3683
|
+
]
|
|
3684
|
+
});
|
|
3685
|
+
}
|
|
3686
|
+
//#endregion
|
|
3687
|
+
//#region src/react/flow-waterfall.tsx
|
|
3688
|
+
const LANE_PAD = 12;
|
|
3689
|
+
const END_LABEL_GUTTER = {
|
|
3690
|
+
spans: 120,
|
|
3691
|
+
agents: 16
|
|
3692
|
+
};
|
|
3693
|
+
const NAME_WIDTH = {
|
|
3694
|
+
spans: 320,
|
|
3695
|
+
agents: 240
|
|
3696
|
+
};
|
|
3697
|
+
const headlineSeconds = (ms) => ms < 6e4 ? `${(ms / 1e3).toFixed(1)}s` : formatSpanDuration(ms);
|
|
3698
|
+
/** Shared trace inspector: the agent timeline and the span waterfall over one clock.
|
|
3699
|
+
* Data remains producer-owned; playback, zoom and folding change presentation only. */
|
|
3700
|
+
function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defaultCollapsed = false, defaultView = "spans", renderIcon, maxHeight = 640, onSelect, markers }) {
|
|
2428
3701
|
const root = useRef(null);
|
|
2429
3702
|
const search = useRef(null);
|
|
3703
|
+
const slider = useRef(null);
|
|
3704
|
+
const timeOutput = useRef(null);
|
|
3705
|
+
const hideTimer = useRef(void 0);
|
|
2430
3706
|
const searchId = useId();
|
|
3707
|
+
const cardId = useId();
|
|
3708
|
+
const [view, setView] = useState(defaultView);
|
|
2431
3709
|
const [query, setQuery] = useState("");
|
|
2432
3710
|
const [collapsed, setCollapsed] = useState(() => new Set(defaultCollapsed ? trace.spans.flatMap((span) => span.parentId ? [span.parentId] : []) : []));
|
|
2433
3711
|
const [speed, setSpeed] = useState(1);
|
|
2434
3712
|
const [playing, setPlaying] = useState(false);
|
|
2435
3713
|
const [position, setPosition] = useState(null);
|
|
3714
|
+
const [zoom, setZoom] = useState(null);
|
|
3715
|
+
const [nameWidth, setNameWidth] = useState(NAME_WIDTH[defaultView]);
|
|
3716
|
+
const [laneWidth, setLaneWidth] = useState(0);
|
|
3717
|
+
const [card, setCard] = useState(null);
|
|
3718
|
+
const [activeId, setActiveId] = useState();
|
|
3719
|
+
const [laneElement, setLaneElement] = useState(null);
|
|
2436
3720
|
const observedTotal = Number.isFinite(trace.totalMs) && trace.totalMs >= 0 ? trace.totalMs : 0;
|
|
2437
|
-
const [elapsed, setElapsed] = useState(observedTotal);
|
|
2438
3721
|
const isLive = live ?? trace.spans.some(spanRunning);
|
|
3722
|
+
const dataEnd = useMemo(() => Math.max(observedTotal, ...trace.spans.map((span) => Number.isFinite(span.endMs) ? span.endMs : 0)), [trace, observedTotal]);
|
|
3723
|
+
const trackPx = Math.max(40, laneWidth - LANE_PAD - Math.min(END_LABEL_GUTTER[view], laneWidth * .3));
|
|
2439
3724
|
useEffect(() => {
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
|
|
2448
|
-
trace,
|
|
2449
|
-
observedTotal,
|
|
2450
|
-
isLive,
|
|
2451
|
-
startedAt
|
|
2452
|
-
]);
|
|
2453
|
-
const currentTrace = useMemo(() => {
|
|
2454
|
-
const totalMs = Math.max(observedTotal, isLive ? elapsed : 0, ...trace.spans.map((span) => Number.isFinite(span.endMs) ? span.endMs : 0));
|
|
2455
|
-
return {
|
|
2456
|
-
...trace,
|
|
2457
|
-
totalMs,
|
|
2458
|
-
spans: trace.spans.map((span) => isLive && spanRunning(span) && Number.isFinite(span.endMs) && span.endMs >= span.startMs ? {
|
|
2459
|
-
...span,
|
|
2460
|
-
endMs: Math.max(span.endMs, totalMs)
|
|
2461
|
-
} : span)
|
|
2462
|
-
};
|
|
3725
|
+
if (!laneElement) return;
|
|
3726
|
+
const observer = new ResizeObserver(([entry]) => setLaneWidth(entry.contentRect.width));
|
|
3727
|
+
observer.observe(laneElement);
|
|
3728
|
+
return () => observer.disconnect();
|
|
3729
|
+
}, [laneElement]);
|
|
3730
|
+
const viewAt = useCallback((t) => zoom ?? {
|
|
3731
|
+
startMs: 0,
|
|
3732
|
+
endMs: Math.max(dataEnd, isLive ? t : 0, 1)
|
|
2463
3733
|
}, [
|
|
2464
|
-
|
|
2465
|
-
|
|
2466
|
-
elapsed,
|
|
3734
|
+
zoom,
|
|
3735
|
+
dataEnd,
|
|
2467
3736
|
isLive
|
|
2468
3737
|
]);
|
|
2469
|
-
const
|
|
2470
|
-
|
|
2471
|
-
|
|
2472
|
-
|
|
2473
|
-
|
|
2474
|
-
|
|
2475
|
-
|
|
2476
|
-
|
|
2477
|
-
let last = performance.now();
|
|
2478
|
-
const timer = setInterval(() => {
|
|
2479
|
-
const now = performance.now();
|
|
2480
|
-
const delta = (now - last) * speed;
|
|
2481
|
-
last = now;
|
|
2482
|
-
setPosition((previous) => Math.min(playbackLimit.current, (previous ?? 0) + delta));
|
|
2483
|
-
}, 50);
|
|
2484
|
-
return () => clearInterval(timer);
|
|
2485
|
-
}, [playing, speed]);
|
|
2486
|
-
useEffect(() => {
|
|
2487
|
-
if (playing && position !== null && position >= totalMs) setPlaying(false);
|
|
2488
|
-
}, [
|
|
2489
|
-
position,
|
|
3738
|
+
const clock = useTraceClock({
|
|
3739
|
+
root,
|
|
3740
|
+
slider,
|
|
3741
|
+
timeOutput,
|
|
3742
|
+
observedTotal: dataEnd,
|
|
3743
|
+
live: isLive,
|
|
3744
|
+
startedAt,
|
|
3745
|
+
snapshot: trace,
|
|
2490
3746
|
playing,
|
|
2491
|
-
|
|
3747
|
+
speed,
|
|
3748
|
+
position,
|
|
3749
|
+
onPlaybackEnd: (end) => {
|
|
3750
|
+
setPlaying(false);
|
|
3751
|
+
setPosition(end);
|
|
3752
|
+
},
|
|
3753
|
+
view: viewAt,
|
|
3754
|
+
trackPx
|
|
3755
|
+
});
|
|
3756
|
+
const clockMs = clock.clockMs;
|
|
3757
|
+
const win = viewAt(clockMs);
|
|
3758
|
+
const limit = Math.max(dataEnd, isLive ? clockMs : 0);
|
|
3759
|
+
const resolution = 2 ** Math.round(Math.log2(Math.max(1, (win.endMs - win.startMs) / Math.max(200, trackPx) * 1.5)));
|
|
3760
|
+
const rows = useMemo(() => flowWaterfallRows({
|
|
3761
|
+
...trace,
|
|
3762
|
+
totalMs: dataEnd
|
|
3763
|
+
}, resolution), [
|
|
3764
|
+
trace,
|
|
3765
|
+
dataEnd,
|
|
3766
|
+
resolution
|
|
2492
3767
|
]);
|
|
3768
|
+
const byId = useMemo(() => new Map(rows.map((row) => [row.id, row])), [rows]);
|
|
3769
|
+
const agents = useMemo(() => {
|
|
3770
|
+
const pipelines = rows.filter((row) => row.kind === "pipeline");
|
|
3771
|
+
return pipelines.length ? pipelines : rows.filter((row) => !row.parentId);
|
|
3772
|
+
}, [rows]);
|
|
3773
|
+
const visible = useMemo(() => view === "agents" ? agents : visibleFlowWaterfallRows(rows, query, collapsed), [
|
|
3774
|
+
view,
|
|
3775
|
+
agents,
|
|
3776
|
+
rows,
|
|
3777
|
+
query,
|
|
3778
|
+
collapsed
|
|
3779
|
+
]);
|
|
3780
|
+
const slowest = view === "spans" ? slowestRow(rows, clockMs) : void 0;
|
|
2493
3781
|
useEffect(() => {
|
|
2494
3782
|
const keydown = (event) => {
|
|
2495
|
-
if (!(event.metaKey || event.ctrlKey) || event.key.toLowerCase() !== "f") return;
|
|
2496
3783
|
const active = document.activeElement;
|
|
2497
3784
|
const ownsFocus = root.current?.contains(active);
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
2501
|
-
|
|
2502
|
-
|
|
3785
|
+
if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "f") {
|
|
3786
|
+
const first = Array.from(document.querySelectorAll("[data-flow-waterfall]")).find((element) => element.getClientRects().length > 0);
|
|
3787
|
+
if (!ownsFocus && (active !== document.body || first !== root.current)) return;
|
|
3788
|
+
event.preventDefault();
|
|
3789
|
+
if (view === "agents") setView("spans");
|
|
3790
|
+
requestAnimationFrame(() => {
|
|
3791
|
+
search.current?.focus();
|
|
3792
|
+
search.current?.select();
|
|
3793
|
+
});
|
|
3794
|
+
return;
|
|
3795
|
+
}
|
|
3796
|
+
if (!ownsFocus || active instanceof HTMLInputElement || active instanceof HTMLSelectElement || event.metaKey || event.ctrlKey || event.altKey) return;
|
|
3797
|
+
if (event.key === "+" || event.key === "=") zoomBy(.5);
|
|
3798
|
+
else if (event.key === "-" || event.key === "_") zoomBy(2);
|
|
3799
|
+
else if (event.key === "0") setZoom(null);
|
|
3800
|
+
else if (event.key === "Escape" && card) closeCard();
|
|
2503
3801
|
};
|
|
2504
3802
|
document.addEventListener("keydown", keydown);
|
|
2505
3803
|
return () => document.removeEventListener("keydown", keydown);
|
|
3804
|
+
});
|
|
3805
|
+
useEffect(() => {
|
|
3806
|
+
if (!card?.pinned) return;
|
|
3807
|
+
const down = (event) => {
|
|
3808
|
+
const target = event.target;
|
|
3809
|
+
if (target?.closest(".fw-card") || target?.closest(".fw-bar, .fw-seg")) return;
|
|
3810
|
+
setCard(null);
|
|
3811
|
+
};
|
|
3812
|
+
document.addEventListener("pointerdown", down);
|
|
3813
|
+
return () => document.removeEventListener("pointerdown", down);
|
|
3814
|
+
}, [card?.pinned]);
|
|
3815
|
+
const zoomBy = (factor, anchor) => setZoom((previous) => {
|
|
3816
|
+
const current = previous ?? viewAt(clockMs);
|
|
3817
|
+
const next = zoomWindow(current, factor, anchor ?? (current.startMs + current.endMs) / 2, limit);
|
|
3818
|
+
return next.endMs - next.startMs >= limit - 1 ? null : next;
|
|
3819
|
+
});
|
|
3820
|
+
const zoomFrame = useRef(0);
|
|
3821
|
+
const zoomTo = (row) => {
|
|
3822
|
+
const span = row.span;
|
|
3823
|
+
const current = viewAt(clockMs);
|
|
3824
|
+
const width = current.endMs - current.startMs;
|
|
3825
|
+
const end = spanRunning(span) ? Math.max(span.endMs, clockMs) : span.endMs;
|
|
3826
|
+
if (view !== "spans" || !Number.isFinite(end) || (end - span.startMs) / Math.max(1, width) > .25) return;
|
|
3827
|
+
const pad = Math.max((end - span.startMs) * .15, 500);
|
|
3828
|
+
const target = clampWindow({
|
|
3829
|
+
startMs: span.startMs - pad,
|
|
3830
|
+
endMs: end + pad
|
|
3831
|
+
}, limit);
|
|
3832
|
+
cancelAnimationFrame(zoomFrame.current);
|
|
3833
|
+
if (globalThis.matchMedia?.("(prefers-reduced-motion: reduce)").matches) {
|
|
3834
|
+
setZoom(target);
|
|
3835
|
+
return;
|
|
3836
|
+
}
|
|
3837
|
+
const began = performance.now();
|
|
3838
|
+
const step = (now) => {
|
|
3839
|
+
const t = Math.min(1, (now - began) / 320);
|
|
3840
|
+
const e = 1 - (1 - t) ** 3;
|
|
3841
|
+
setZoom({
|
|
3842
|
+
startMs: current.startMs + (target.startMs - current.startMs) * e,
|
|
3843
|
+
endMs: current.endMs + (target.endMs - current.endMs) * e
|
|
3844
|
+
});
|
|
3845
|
+
if (t < 1) zoomFrame.current = requestAnimationFrame(step);
|
|
3846
|
+
};
|
|
3847
|
+
zoomFrame.current = requestAnimationFrame(step);
|
|
3848
|
+
};
|
|
3849
|
+
const pan = (deltaMs) => setZoom((previous) => previous && clampWindow({
|
|
3850
|
+
startMs: previous.startMs + deltaMs,
|
|
3851
|
+
endMs: previous.endMs + deltaMs
|
|
3852
|
+
}, limit));
|
|
3853
|
+
const latest = useRef({
|
|
3854
|
+
toggle: (_row) => {},
|
|
3855
|
+
pin: (_target, _element) => {},
|
|
3856
|
+
inspect: (_target, _element) => {}
|
|
3857
|
+
});
|
|
3858
|
+
const toggleNow = (row) => {
|
|
3859
|
+
onSelect?.(row.span);
|
|
3860
|
+
if (view === "agents") {
|
|
3861
|
+
const keep = /* @__PURE__ */ new Set();
|
|
3862
|
+
for (let cursor = row; cursor; cursor = cursor.parentId ? byId.get(cursor.parentId) : void 0) keep.add(cursor.id);
|
|
3863
|
+
setCollapsed(new Set(rows.filter((item) => item.hasChildren && !keep.has(item.id) && item.kind === "pipeline").map((item) => item.id)));
|
|
3864
|
+
setView("spans");
|
|
3865
|
+
setActiveId(row.id);
|
|
3866
|
+
return;
|
|
3867
|
+
}
|
|
3868
|
+
if (collapsed.has(row.id)) zoomTo(row);
|
|
3869
|
+
setCollapsed((previous) => {
|
|
3870
|
+
const next = new Set(previous);
|
|
3871
|
+
if (next.has(row.id)) next.delete(row.id);
|
|
3872
|
+
else next.add(row.id);
|
|
3873
|
+
return next;
|
|
3874
|
+
});
|
|
3875
|
+
};
|
|
3876
|
+
latest.current.toggle = toggleNow;
|
|
3877
|
+
const toggle = useCallback((row) => latest.current.toggle(row), []);
|
|
3878
|
+
const place = (target, element, pinned) => {
|
|
3879
|
+
const host = root.current?.getBoundingClientRect();
|
|
3880
|
+
if (!host) return;
|
|
3881
|
+
const rect = element.getBoundingClientRect();
|
|
3882
|
+
const above = view === "agents" ? rect.top - host.top > 150 : globalThis.innerHeight - rect.bottom < 260 && rect.top - host.top > 260;
|
|
3883
|
+
const left = Math.max(8, Math.min(rect.left - host.left, host.width - 328));
|
|
3884
|
+
clearTimeout(hideTimer.current);
|
|
3885
|
+
setCard({
|
|
3886
|
+
target,
|
|
3887
|
+
pinned,
|
|
3888
|
+
left,
|
|
3889
|
+
top: above ? rect.top - host.top - 8 : rect.bottom - host.top + 8,
|
|
3890
|
+
above
|
|
3891
|
+
});
|
|
3892
|
+
};
|
|
3893
|
+
latest.current.inspect = (target, element) => {
|
|
3894
|
+
if (!card?.pinned) place(target, element, false);
|
|
3895
|
+
};
|
|
3896
|
+
const inspect = useCallback((target, element) => latest.current.inspect(target, element), []);
|
|
3897
|
+
const leave = useCallback(() => {
|
|
3898
|
+
clearTimeout(hideTimer.current);
|
|
3899
|
+
hideTimer.current = setTimeout(() => setCard((previous) => previous?.pinned ? previous : null), 140);
|
|
2506
3900
|
}, []);
|
|
2507
|
-
|
|
2508
|
-
|
|
2509
|
-
|
|
3901
|
+
useEffect(() => {
|
|
3902
|
+
if (!card || card.pinned) return;
|
|
3903
|
+
const move = (event) => {
|
|
3904
|
+
if (!event.target?.closest(".fw-bar, .fw-seg, .fw-card")) leave();
|
|
3905
|
+
};
|
|
3906
|
+
document.addEventListener("pointermove", move);
|
|
3907
|
+
return () => document.removeEventListener("pointermove", move);
|
|
3908
|
+
}, [card, leave]);
|
|
3909
|
+
latest.current.pin = (target, element) => {
|
|
3910
|
+
place(target, element, true);
|
|
3911
|
+
const row = byId.get(target);
|
|
3912
|
+
if (row) {
|
|
3913
|
+
onSelect?.(row.span);
|
|
3914
|
+
zoomTo(row);
|
|
3915
|
+
}
|
|
3916
|
+
};
|
|
3917
|
+
const pin = useCallback((target, element) => latest.current.pin(target, element), []);
|
|
3918
|
+
const closeCard = () => {
|
|
3919
|
+
const target = card?.target;
|
|
3920
|
+
setCard(null);
|
|
3921
|
+
if (target) root.current?.querySelector(`.fw-canvas [data-span-id="${CSS.escape(target)}"]`)?.focus({ preventScroll: true });
|
|
3922
|
+
};
|
|
3923
|
+
const scrubTo = useCallback((ms) => {
|
|
3924
|
+
setPlaying(false);
|
|
3925
|
+
setPosition(Math.min(ms, clock.limitMs()));
|
|
3926
|
+
}, [clock]);
|
|
3927
|
+
const togglePlay = () => {
|
|
3928
|
+
if (playing) {
|
|
3929
|
+
setPosition(clock.positionMs());
|
|
3930
|
+
setPlaying(false);
|
|
3931
|
+
return;
|
|
3932
|
+
}
|
|
3933
|
+
if (position === null || position >= limit - 1) setPosition(0);
|
|
3934
|
+
setPlaying(true);
|
|
3935
|
+
};
|
|
3936
|
+
const scrubbing = position !== null || playing;
|
|
3937
|
+
const at = scrubbing ? totalsAt(trace, clockMs) : void 0;
|
|
3938
|
+
const costUsd = at ? at.costUsd : trace.costUsd;
|
|
3939
|
+
const tokens = at ? at.tokens : trace.promptTokens + trace.completionTokens;
|
|
3940
|
+
const modelCalls = at ? at.modelCalls : trace.modelCalls;
|
|
3941
|
+
const errors = at ? at.errors : trace.errors ?? trace.spans.filter(spanFailed).length;
|
|
2510
3942
|
const summary = [
|
|
2511
|
-
["Duration",
|
|
2512
|
-
[
|
|
2513
|
-
|
|
2514
|
-
|
|
2515
|
-
|
|
2516
|
-
|
|
3943
|
+
["Duration", formatSpanDuration(scrubbing ? clockMs : limit)],
|
|
3944
|
+
[
|
|
3945
|
+
trace.costBasis === "list" ? "Cost · list price" : "Cost",
|
|
3946
|
+
costUsd === void 0 ? "Unknown" : formatCost(costUsd),
|
|
3947
|
+
costUsd === void 0 ? "The harness recorded no cost for this trace" : trace.costBasis === "list" ? "Input and output tokens at the model catalog list price; not a bill" : void 0
|
|
3948
|
+
],
|
|
3949
|
+
[
|
|
3950
|
+
"Tokens",
|
|
3951
|
+
formatCount(tokens),
|
|
3952
|
+
`${tokens.toLocaleString()} tokens`
|
|
3953
|
+
],
|
|
3954
|
+
[
|
|
3955
|
+
"LLM calls",
|
|
3956
|
+
modelCalls === void 0 ? "Unknown" : modelCalls.toLocaleString(),
|
|
3957
|
+
modelCalls === void 0 ? "Model output windows do not establish the number of LLM requests" : void 0
|
|
3958
|
+
],
|
|
3959
|
+
["Tool calls", (at ? at.toolCalls : trace.toolCalls).toLocaleString()],
|
|
3960
|
+
["Errors", errors.toLocaleString()]
|
|
2517
3961
|
];
|
|
2518
|
-
const
|
|
2519
|
-
|
|
2520
|
-
|
|
2521
|
-
|
|
2522
|
-
|
|
2523
|
-
|
|
3962
|
+
const busy = agents.reduce((sum, row) => sum + busyMs(row, limit), 0);
|
|
3963
|
+
const speedup = limit > 0 ? busy / limit : 0;
|
|
3964
|
+
const done = !isLive && !trace.spans.some(spanRunning);
|
|
3965
|
+
const cardRow = card ? byId.get(card.target) : void 0;
|
|
3966
|
+
const cardAgent = cardRow && (() => {
|
|
3967
|
+
for (let cursor = cardRow; cursor; cursor = cursor.parentId ? byId.get(cursor.parentId) : void 0) if (cursor.kind === "pipeline") return cursor;
|
|
3968
|
+
})();
|
|
3969
|
+
const showCursor = isLive || position !== null || playing;
|
|
3970
|
+
const overviewMarks = useMemo(() => rows.filter((row) => !row.parentId).flatMap((row) => row.hasChildren ? row.segments : row.kind === "pipeline" ? [] : [{
|
|
3971
|
+
startMs: row.span.startMs,
|
|
3972
|
+
endMs: row.span.endMs,
|
|
3973
|
+
kind: row.ok ? "tool" : "failed",
|
|
3974
|
+
tone: row.tone,
|
|
3975
|
+
id: row.id,
|
|
3976
|
+
count: 1,
|
|
3977
|
+
retry: false
|
|
3978
|
+
}]), [rows]);
|
|
2524
3979
|
return /* @__PURE__ */ jsxs("section", {
|
|
2525
3980
|
ref: root,
|
|
2526
3981
|
"data-flow-waterfall": true,
|
|
2527
3982
|
"aria-label": label,
|
|
2528
|
-
className:
|
|
3983
|
+
className: `fw-root fw-root-${view}`,
|
|
3984
|
+
style: {
|
|
3985
|
+
"--fw-name-w": `${nameWidth}px`,
|
|
3986
|
+
"--fw-pad": `${LANE_PAD}px`,
|
|
3987
|
+
"--fw-row-h": `${ROW_HEIGHT[view]}px`
|
|
3988
|
+
},
|
|
2529
3989
|
children: [
|
|
2530
|
-
/* @__PURE__ */ jsxs("
|
|
3990
|
+
/* @__PURE__ */ jsxs("header", {
|
|
2531
3991
|
className: "fw-heading",
|
|
2532
3992
|
children: [/* @__PURE__ */ jsxs("div", {
|
|
2533
|
-
className: "fw-title",
|
|
2534
|
-
children: [
|
|
2535
|
-
className: "fw-
|
|
2536
|
-
children:
|
|
3993
|
+
className: "fw-title-block",
|
|
3994
|
+
children: [/* @__PURE__ */ jsxs("div", {
|
|
3995
|
+
className: "fw-title-line",
|
|
3996
|
+
children: [
|
|
3997
|
+
/* @__PURE__ */ jsx("span", {
|
|
3998
|
+
className: "fw-title-glyph",
|
|
3999
|
+
"aria-hidden": true,
|
|
4000
|
+
children: /* @__PURE__ */ jsx(TraceGlyph, {})
|
|
4001
|
+
}),
|
|
4002
|
+
/* @__PURE__ */ jsx("h3", {
|
|
4003
|
+
className: "fw-title",
|
|
4004
|
+
children: label
|
|
4005
|
+
}),
|
|
4006
|
+
isLive && /* @__PURE__ */ jsx("span", {
|
|
4007
|
+
className: "fw-state fw-state-live",
|
|
4008
|
+
children: "Live"
|
|
4009
|
+
})
|
|
4010
|
+
]
|
|
4011
|
+
}), view === "agents" && /* @__PURE__ */ jsx("p", {
|
|
4012
|
+
className: "fw-headline",
|
|
4013
|
+
"aria-live": "polite",
|
|
4014
|
+
children: done ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
4015
|
+
/* @__PURE__ */ jsx("span", {
|
|
4016
|
+
className: "fw-done-mark",
|
|
4017
|
+
"aria-label": "done",
|
|
4018
|
+
children: "✓"
|
|
4019
|
+
}),
|
|
4020
|
+
agents.length,
|
|
4021
|
+
" ",
|
|
4022
|
+
agents.length === 1 ? "agent" : "agents",
|
|
4023
|
+
" · ",
|
|
4024
|
+
headlineSeconds(limit),
|
|
4025
|
+
speedup >= 1.05 ? ` · ${speedup.toFixed(1)}x parallel` : "",
|
|
4026
|
+
trace.costUsd !== void 0 ? ` · ${formatCost(trace.costUsd)}` : ""
|
|
4027
|
+
] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
4028
|
+
headlineSeconds(clockMs),
|
|
4029
|
+
" · ",
|
|
4030
|
+
tokens.toLocaleString(),
|
|
4031
|
+
" tokens",
|
|
4032
|
+
trace.costUsd !== void 0 ? ` · ${formatCost(trace.costUsd)}` : ""
|
|
4033
|
+
] })
|
|
2537
4034
|
})]
|
|
2538
4035
|
}), /* @__PURE__ */ jsxs("div", {
|
|
2539
|
-
className: "fw-
|
|
2540
|
-
children: [
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
}),
|
|
2546
|
-
/* @__PURE__ */ jsx("input", {
|
|
2547
|
-
ref: search,
|
|
2548
|
-
id: searchId,
|
|
2549
|
-
type: "search",
|
|
2550
|
-
value: query,
|
|
2551
|
-
placeholder: "Search trace…",
|
|
2552
|
-
"aria-keyshortcuts": "Meta+f Control+f",
|
|
2553
|
-
onChange: (event) => setQuery(event.target.value),
|
|
2554
|
-
onKeyDown: (event) => {
|
|
2555
|
-
if (event.key === "Escape") {
|
|
2556
|
-
setQuery("");
|
|
2557
|
-
event.stopPropagation();
|
|
2558
|
-
}
|
|
2559
|
-
},
|
|
2560
|
-
className: `${controlClass} fw-search-input`
|
|
2561
|
-
}),
|
|
2562
|
-
/* @__PURE__ */ jsx("button", {
|
|
4036
|
+
className: "fw-tools",
|
|
4037
|
+
children: [/* @__PURE__ */ jsx("div", {
|
|
4038
|
+
className: "fw-views",
|
|
4039
|
+
role: "radiogroup",
|
|
4040
|
+
"aria-label": "Trace view",
|
|
4041
|
+
children: ["agents", "spans"].map((option) => /* @__PURE__ */ jsx("button", {
|
|
2563
4042
|
type: "button",
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
|
|
2569
|
-
|
|
2570
|
-
|
|
4043
|
+
role: "radio",
|
|
4044
|
+
"aria-checked": view === option,
|
|
4045
|
+
className: "fw-view-button",
|
|
4046
|
+
onClick: () => {
|
|
4047
|
+
setView(option);
|
|
4048
|
+
setNameWidth(NAME_WIDTH[option]);
|
|
4049
|
+
setCard(null);
|
|
4050
|
+
},
|
|
4051
|
+
children: option === "agents" ? "Agents" : "Steps"
|
|
4052
|
+
}, option))
|
|
4053
|
+
}), view === "spans" && /* @__PURE__ */ jsxs("div", {
|
|
4054
|
+
className: "fw-search",
|
|
4055
|
+
children: [
|
|
4056
|
+
/* @__PURE__ */ jsx("label", {
|
|
4057
|
+
htmlFor: searchId,
|
|
4058
|
+
className: "fw-sr-only",
|
|
4059
|
+
children: "Search trace"
|
|
4060
|
+
}),
|
|
4061
|
+
/* @__PURE__ */ jsx("span", {
|
|
4062
|
+
className: "fw-search-glyph",
|
|
4063
|
+
"aria-hidden": true,
|
|
4064
|
+
children: /* @__PURE__ */ jsx(SearchGlyph, {})
|
|
4065
|
+
}),
|
|
4066
|
+
/* @__PURE__ */ jsx("input", {
|
|
4067
|
+
ref: search,
|
|
4068
|
+
id: searchId,
|
|
4069
|
+
type: "search",
|
|
4070
|
+
value: query,
|
|
4071
|
+
placeholder: "Search",
|
|
4072
|
+
"aria-keyshortcuts": "Meta+f Control+f",
|
|
4073
|
+
onChange: (event) => setQuery(event.target.value),
|
|
4074
|
+
onKeyDown: (event) => {
|
|
4075
|
+
if (event.key === "Escape") {
|
|
4076
|
+
setQuery("");
|
|
4077
|
+
event.stopPropagation();
|
|
4078
|
+
}
|
|
4079
|
+
},
|
|
4080
|
+
className: "fw-search-input"
|
|
4081
|
+
})
|
|
4082
|
+
]
|
|
4083
|
+
})]
|
|
2571
4084
|
})]
|
|
2572
4085
|
}),
|
|
2573
|
-
/* @__PURE__ */ jsx("dl", {
|
|
4086
|
+
view === "spans" && /* @__PURE__ */ jsx("dl", {
|
|
2574
4087
|
className: "fw-summary",
|
|
2575
|
-
children: summary.map(([name, value]) => /* @__PURE__ */ jsxs("div", {
|
|
2576
|
-
className: "fw-
|
|
2577
|
-
children: name
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
}
|
|
2583
|
-
}),
|
|
2584
|
-
/* @__PURE__ */ jsx("div", {
|
|
2585
|
-
className: "fw-legend",
|
|
2586
|
-
"aria-label": "Span types",
|
|
2587
|
-
children: Object.entries(TONES).map(([kind, tone]) => /* @__PURE__ */ jsxs("span", {
|
|
2588
|
-
className: "fw-legend-item",
|
|
2589
|
-
children: [/* @__PURE__ */ jsx("span", {
|
|
2590
|
-
"aria-hidden": true,
|
|
2591
|
-
className: `fw-dot ${tone.color}`
|
|
2592
|
-
}), tone.label]
|
|
2593
|
-
}, kind))
|
|
4088
|
+
children: summary.map(([name, value, title]) => /* @__PURE__ */ jsxs("div", {
|
|
4089
|
+
className: name === "Errors" && errors > 0 ? "fw-has-errors" : void 0,
|
|
4090
|
+
children: [/* @__PURE__ */ jsx("dt", { children: name }), /* @__PURE__ */ jsx("dd", {
|
|
4091
|
+
className: "fw-value",
|
|
4092
|
+
title,
|
|
4093
|
+
children: value
|
|
4094
|
+
})]
|
|
4095
|
+
}, name))
|
|
2594
4096
|
}),
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
|
|
2607
|
-
|
|
4097
|
+
/* @__PURE__ */ jsx(FlowBody, {
|
|
4098
|
+
rows: visible,
|
|
4099
|
+
byId,
|
|
4100
|
+
totalRows: rows.length,
|
|
4101
|
+
view,
|
|
4102
|
+
maxHeight,
|
|
4103
|
+
collapsed,
|
|
4104
|
+
searching: Boolean(query.trim()),
|
|
4105
|
+
slowestId: slowest?.id,
|
|
4106
|
+
activeId,
|
|
4107
|
+
selectedId: card?.pinned ? card.target : void 0,
|
|
4108
|
+
clockMs,
|
|
4109
|
+
window: win,
|
|
4110
|
+
trackPx,
|
|
4111
|
+
showCursor,
|
|
4112
|
+
markers,
|
|
4113
|
+
zoomed: zoom !== null,
|
|
4114
|
+
renderIcon,
|
|
4115
|
+
emptyText: rows.length === 0 ? "No spans yet. Waiting for trace events." : `No matching steps for “${query}”.`,
|
|
4116
|
+
onToggle: toggle,
|
|
4117
|
+
onInspect: inspect,
|
|
4118
|
+
onLeave: leave,
|
|
4119
|
+
onPin: pin,
|
|
4120
|
+
onActivate: (row) => setActiveId(row.id),
|
|
4121
|
+
onActiveChange: setActiveId,
|
|
4122
|
+
onScrub: scrubTo,
|
|
4123
|
+
onZoom: zoomBy,
|
|
4124
|
+
onPan: pan,
|
|
4125
|
+
nameWidth,
|
|
4126
|
+
onNameWidth: (px) => setNameWidth(Math.max(140, Math.min(560, px))),
|
|
4127
|
+
onTrack: setLaneElement,
|
|
4128
|
+
onExpandAll: rows.some((row) => row.hasChildren) ? () => setCollapsed(/* @__PURE__ */ new Set()) : void 0,
|
|
4129
|
+
onCollapseAll: () => setCollapsed(new Set(rows.filter((row) => row.hasChildren).map((row) => row.id)))
|
|
2608
4130
|
}),
|
|
2609
|
-
/* @__PURE__ */ jsx("div", {
|
|
2610
|
-
className: "fw-
|
|
2611
|
-
|
|
2612
|
-
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
rows.length
|
|
2625
|
-
] }),
|
|
2626
|
-
/* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(WaterfallTimeAxis, {
|
|
2627
|
-
fluid: true,
|
|
2628
|
-
windowMs: totalMs,
|
|
2629
|
-
widthPx: 240,
|
|
2630
|
-
formatDuration: seconds
|
|
2631
|
-
}) }),
|
|
2632
|
-
/* @__PURE__ */ jsx("span", {
|
|
2633
|
-
className: "fw-align-right",
|
|
2634
|
-
children: "Duration"
|
|
2635
|
-
})
|
|
2636
|
-
]
|
|
2637
|
-
}),
|
|
2638
|
-
visible.length === 0 && /* @__PURE__ */ jsx("p", {
|
|
2639
|
-
role: "status",
|
|
2640
|
-
className: "fw-empty",
|
|
2641
|
-
children: rows.length === 0 ? "No spans yet. Waiting for trace events." : `No matching steps for “${query}”.`
|
|
2642
|
-
}),
|
|
2643
|
-
visible.map((row) => {
|
|
2644
|
-
const tone = !row.ok ? TONES.failed : TONES[row.kind];
|
|
2645
|
-
const pending = row.span.startMs > playhead;
|
|
2646
|
-
const running = spanRunning(row.span);
|
|
2647
|
-
return /* @__PURE__ */ jsx("div", {
|
|
2648
|
-
"data-span-id": row.id,
|
|
2649
|
-
"data-span-kind": !row.ok ? "failed" : row.kind,
|
|
2650
|
-
"data-slowest": row.id === slowest?.id || void 0,
|
|
2651
|
-
className: `fw-row ${row.id === slowest?.id ? "fw-slowest" : ""} ${pending ? "fw-pending" : ""}`,
|
|
2652
|
-
children: /* @__PURE__ */ jsx(WaterfallRow, {
|
|
2653
|
-
className: "fw-grid",
|
|
2654
|
-
style: { display: "grid" },
|
|
2655
|
-
startMs: row.span.startMs,
|
|
2656
|
-
endMs: row.span.endMs,
|
|
2657
|
-
window: {
|
|
2658
|
-
startMs: 0,
|
|
2659
|
-
endMs: totalMs
|
|
2660
|
-
},
|
|
2661
|
-
fluid: true,
|
|
2662
|
-
color: "var(--fw-bar-color)",
|
|
2663
|
-
barClassName: `fw-bar ${tone.color} ${row.approx ? "fw-approx" : ""}`,
|
|
2664
|
-
timingClassName: "fw-bar-track",
|
|
2665
|
-
durationClassName: "fw-duration",
|
|
2666
|
-
durationLabel: row.durationLabel,
|
|
2667
|
-
formatDuration: seconds,
|
|
2668
|
-
clipAtMs: position === null ? void 0 : playhead,
|
|
2669
|
-
overlay: position !== null && /* @__PURE__ */ jsx("span", {
|
|
2670
|
-
className: "fw-playhead",
|
|
2671
|
-
style: { left: `${playhead / (totalMs || 1) * 100}%` }
|
|
2672
|
-
}),
|
|
2673
|
-
label: /* @__PURE__ */ jsxs("div", {
|
|
2674
|
-
className: "fw-name",
|
|
2675
|
-
style: { paddingLeft: Math.min(row.depth, 8) * 12 },
|
|
2676
|
-
children: [row.hasChildren ? /* @__PURE__ */ jsx("button", {
|
|
2677
|
-
type: "button",
|
|
2678
|
-
"aria-label": `${collapsed.has(row.id) && !query ? "Expand" : "Collapse"} ${row.name}`,
|
|
2679
|
-
"aria-expanded": query ? true : !collapsed.has(row.id),
|
|
2680
|
-
onClick: () => toggle(row.id),
|
|
2681
|
-
disabled: Boolean(query.trim()),
|
|
2682
|
-
className: "fw-toggle",
|
|
2683
|
-
children: collapsed.has(row.id) && !query ? "›" : "⌄"
|
|
2684
|
-
}) : /* @__PURE__ */ jsx("span", {
|
|
2685
|
-
className: "fw-leaf",
|
|
2686
|
-
"aria-hidden": true,
|
|
2687
|
-
children: "·"
|
|
2688
|
-
}), /* @__PURE__ */ jsxs("span", {
|
|
2689
|
-
title: `${row.name} · ${tone.label}${running ? " · running" : ""}`,
|
|
2690
|
-
className: "fw-span-name",
|
|
2691
|
-
children: [
|
|
2692
|
-
row.name,
|
|
2693
|
-
!row.ok && /* @__PURE__ */ jsx("span", {
|
|
2694
|
-
className: "fw-error-text",
|
|
2695
|
-
children: " · failed"
|
|
2696
|
-
}),
|
|
2697
|
-
running && /* @__PURE__ */ jsx("span", {
|
|
2698
|
-
className: "fw-muted",
|
|
2699
|
-
children: " · running"
|
|
2700
|
-
})
|
|
2701
|
-
]
|
|
2702
|
-
})]
|
|
2703
|
-
})
|
|
2704
|
-
})
|
|
2705
|
-
}, row.id);
|
|
2706
|
-
})
|
|
2707
|
-
]
|
|
4131
|
+
card && cardRow && /* @__PURE__ */ jsx("div", {
|
|
4132
|
+
className: `fw-card-anchor${card.above ? " fw-card-above" : ""}`,
|
|
4133
|
+
onPointerEnter: () => clearTimeout(hideTimer.current),
|
|
4134
|
+
onPointerLeave: card.pinned ? void 0 : leave,
|
|
4135
|
+
children: /* @__PURE__ */ jsx(SpanCard, {
|
|
4136
|
+
id: cardId,
|
|
4137
|
+
row: cardRow,
|
|
4138
|
+
agent: cardAgent,
|
|
4139
|
+
clockMs,
|
|
4140
|
+
left: card.left,
|
|
4141
|
+
top: card.top,
|
|
4142
|
+
pinned: card.pinned,
|
|
4143
|
+
onClose: closeCard,
|
|
4144
|
+
renderIcon,
|
|
4145
|
+
compact: view === "agents"
|
|
2708
4146
|
})
|
|
2709
4147
|
}),
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
}
|
|
2735
|
-
},
|
|
2736
|
-
children: playing ? "Pause" : "Play"
|
|
2737
|
-
}),
|
|
2738
|
-
/* @__PURE__ */ jsx("button", {
|
|
2739
|
-
type: "button",
|
|
2740
|
-
className: controlClass,
|
|
2741
|
-
disabled: totalMs === 0,
|
|
2742
|
-
onClick: () => {
|
|
2743
|
-
setPosition(0);
|
|
2744
|
-
setPlaying(false);
|
|
2745
|
-
},
|
|
2746
|
-
children: "Restart"
|
|
2747
|
-
}),
|
|
2748
|
-
/* @__PURE__ */ jsxs("label", {
|
|
2749
|
-
className: "fw-speed",
|
|
2750
|
-
children: ["Speed", /* @__PURE__ */ jsx("select", {
|
|
2751
|
-
"aria-label": "Playback speed",
|
|
2752
|
-
className: controlClass,
|
|
2753
|
-
value: speed,
|
|
2754
|
-
onChange: (event) => setSpeed(Number(event.target.value)),
|
|
2755
|
-
children: SPEEDS.map((value) => /* @__PURE__ */ jsxs("option", {
|
|
2756
|
-
value,
|
|
2757
|
-
children: [value, "x"]
|
|
2758
|
-
}, value))
|
|
2759
|
-
})]
|
|
2760
|
-
}),
|
|
2761
|
-
/* @__PURE__ */ jsxs("label", {
|
|
2762
|
-
className: "fw-position",
|
|
2763
|
-
children: ["Position", /* @__PURE__ */ jsx("input", {
|
|
2764
|
-
"aria-label": "Playback position",
|
|
2765
|
-
type: "range",
|
|
2766
|
-
min: 0,
|
|
2767
|
-
max: totalMs || 1,
|
|
2768
|
-
step: "any",
|
|
2769
|
-
value: playhead,
|
|
2770
|
-
disabled: totalMs === 0,
|
|
2771
|
-
onChange: (event) => {
|
|
2772
|
-
setPosition(Number(event.target.value));
|
|
2773
|
-
setPlaying(false);
|
|
2774
|
-
},
|
|
2775
|
-
className: "fw-slider"
|
|
2776
|
-
})]
|
|
2777
|
-
}),
|
|
2778
|
-
/* @__PURE__ */ jsx("output", {
|
|
2779
|
-
className: "fw-time",
|
|
2780
|
-
"aria-label": "Playback time",
|
|
2781
|
-
children: seconds(playhead)
|
|
2782
|
-
}),
|
|
2783
|
-
position !== null && /* @__PURE__ */ jsx("button", {
|
|
2784
|
-
type: "button",
|
|
2785
|
-
className: controlClass,
|
|
2786
|
-
onClick: () => {
|
|
2787
|
-
setPosition(null);
|
|
2788
|
-
setPlaying(false);
|
|
2789
|
-
},
|
|
2790
|
-
children: isLive ? "Follow live" : "Show full trace"
|
|
2791
|
-
})
|
|
2792
|
-
]
|
|
4148
|
+
/* @__PURE__ */ jsx(PlaybackBar, {
|
|
4149
|
+
view,
|
|
4150
|
+
playing,
|
|
4151
|
+
limit,
|
|
4152
|
+
clockMs,
|
|
4153
|
+
speed,
|
|
4154
|
+
zoom,
|
|
4155
|
+
marks: overviewMarks,
|
|
4156
|
+
slider,
|
|
4157
|
+
timeOutput,
|
|
4158
|
+
followLabel: position === null ? void 0 : isLive ? "Follow live" : "Show full trace",
|
|
4159
|
+
onTogglePlay: togglePlay,
|
|
4160
|
+
onRestart: () => {
|
|
4161
|
+
setPosition(0);
|
|
4162
|
+
setPlaying(false);
|
|
4163
|
+
},
|
|
4164
|
+
onSpeed: setSpeed,
|
|
4165
|
+
onScrub: scrubTo,
|
|
4166
|
+
onFollow: () => {
|
|
4167
|
+
setPosition(null);
|
|
4168
|
+
setPlaying(false);
|
|
4169
|
+
},
|
|
4170
|
+
onZoom: zoomBy,
|
|
4171
|
+
onFit: () => setZoom(null)
|
|
2793
4172
|
})
|
|
2794
4173
|
]
|
|
2795
4174
|
});
|