@tangle-network/charts 0.15.1 → 0.16.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.
@@ -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
- /** Hierarchy only; the shared timing primitives own all bar geometry.
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: Number.isFinite(span.startMs) && Number.isFinite(span.endMs) && span.endMs >= span.startMs ? `${((span.endMs - span.startMs) / 1e3).toFixed(1)}s${span.approx ? "~" : ""}` : "—",
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 ordered = [];
2357
- const visit = (row, depth) => {
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.hasChildren = children.has(row.id);
2360
- ordered.push(row);
2361
- for (const child of children.get(row.id) ?? []) visit(child, depth + 1);
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,1054 @@ 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 TONES = {
2392
- pipeline: {
2393
- label: "Orchestrator",
2394
- color: "fw-orchestrator"
2395
- },
2396
- model: {
2397
- label: "Model call",
2398
- color: "fw-model"
2399
- },
2400
- tool: {
2401
- label: "Tool call",
2402
- color: "fw-tool"
2403
- },
2404
- waiting: {
2405
- label: "Waiting",
2406
- color: "fw-waiting"
2407
- },
2408
- handoff: {
2409
- label: "Handoff",
2410
- color: "fw-handoff"
2411
- },
2412
- failed: {
2413
- label: "Failed",
2414
- color: "fw-failed"
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
+ showCursor && /* @__PURE__ */ jsx("div", {
3408
+ className: "fw-cursor-layer",
3409
+ "aria-hidden": true,
3410
+ children: /* @__PURE__ */ jsx("span", { className: "fw-cursor" })
3411
+ })
3412
+ ]
3413
+ }),
3414
+ view === "agents" && axis
3415
+ ]
3416
+ });
3417
+ }
3418
+ /** Ancestors of the first visible row stay pinned while their children scroll beneath them. This layer follows
3419
+ * scroll per row on its own, so the virtualized rows below re-render only per chunk. */
3420
+ function StickyRows({ scroller, rows, index, rowHeight, render }) {
3421
+ const [top, setTop] = useState(0);
3422
+ useEffect(() => {
3423
+ const element = scroller.current;
3424
+ if (!element) return;
3425
+ const scroll = () => setTop(element.scrollTop > 0 ? Math.floor(element.scrollTop / rowHeight) : -1);
3426
+ scroll();
3427
+ element.addEventListener("scroll", scroll, { passive: true });
3428
+ return () => element.removeEventListener("scroll", scroll);
3429
+ }, [scroller, rowHeight]);
3430
+ const sticky = [];
3431
+ if (top >= 0) {
3432
+ const chain = [];
3433
+ let cursor = rows[Math.min(rows.length - 1, top + 1)];
3434
+ while (cursor?.parentId !== void 0) {
3435
+ const parentIndex = index.get(cursor.parentId);
3436
+ if (parentIndex === void 0) break;
3437
+ cursor = rows[parentIndex];
3438
+ if (cursor) chain.unshift(cursor);
3439
+ }
3440
+ for (const [level, row] of chain.slice(-3).entries()) if ((index.get(row.id) ?? 0) < top + level + 1) sticky.push(row);
3441
+ }
3442
+ if (!sticky.length) return null;
3443
+ return /* @__PURE__ */ jsx("div", {
3444
+ className: "fw-sticky",
3445
+ style: { height: sticky.length * rowHeight },
3446
+ children: sticky.map((item, level) => render(item, level, true))
3447
+ });
3448
+ }
3449
+ //#endregion
3450
+ //#region src/react/flow-waterfall-clock.ts
3451
+ const TEXT_INTERVAL_MS = 100;
3452
+ /** Drives the trace clock. Bars, the cursor and the axis read CSS variables written here every frame,
3453
+ * so live growth and playback cost no React render per frame; text updates at 10 Hz. */
3454
+ function useTraceClock(input) {
3455
+ const { root, slider, timeOutput, observedTotal, live, startedAt, snapshot, playing, speed, position, onPlaybackEnd, view, trackPx } = input;
3456
+ const [clockMs, setClockMs] = useState(position ?? observedTotal);
3457
+ const receivedAt = useRef(Date.now());
3458
+ const playhead = useRef(position ?? observedTotal);
3459
+ const latest = useRef(input);
3460
+ latest.current = input;
3461
+ useEffect(() => {
3462
+ receivedAt.current = Date.now();
3463
+ }, [snapshot]);
3464
+ const liveNow = () => {
3465
+ const { observedTotal: total, startedAt: origin, live: isLive } = latest.current;
3466
+ if (!isLive) return total;
3467
+ return Math.max(total, origin === void 0 ? total + Date.now() - receivedAt.current : Date.now() - origin);
3468
+ };
3469
+ const paint = (t, limit = liveNow()) => {
3470
+ const element = root.current;
3471
+ if (!element) return;
3472
+ const { startMs, endMs } = latest.current.view(t);
3473
+ const k = Math.max(1e-9, latest.current.trackPx / Math.max(1, endMs - startMs));
3474
+ element.style.setProperty("--fw-t", String(t));
3475
+ element.style.setProperty("--fw-v0", String(startMs));
3476
+ element.style.setProperty("--fw-k", String(k));
3477
+ if (slider.current) {
3478
+ slider.current.max = String(Math.max(1, limit, t));
3479
+ slider.current.value = String(t);
3480
+ }
3481
+ if (timeOutput.current) timeOutput.current.textContent = formatSpanDuration(t);
3482
+ };
3483
+ useLayoutEffect(() => {
3484
+ if (position !== null) playhead.current = position;
3485
+ if (!(live || playing)) {
3486
+ const t = position ?? observedTotal;
3487
+ paint(t);
3488
+ setClockMs(t);
3489
+ return;
3490
+ }
3491
+ let frame = 0;
3492
+ let last = performance.now();
3493
+ let lastText = 0;
3494
+ const step = (now) => {
3495
+ const delta = now - last;
3496
+ last = now;
3497
+ let t;
3498
+ const limit = liveNow();
3499
+ if (playing) {
3500
+ playhead.current = Math.min(limit, playhead.current + delta * latest.current.speed);
3501
+ t = playhead.current;
3502
+ if (t >= limit && !latest.current.live) {
3503
+ paint(t, limit);
3504
+ setClockMs(t);
3505
+ latest.current.onPlaybackEnd(t);
3506
+ return;
3507
+ }
3508
+ } else t = latest.current.position ?? limit;
3509
+ paint(t, limit);
3510
+ if (now - lastText >= TEXT_INTERVAL_MS) {
3511
+ lastText = now;
3512
+ setClockMs(t);
3513
+ }
3514
+ frame = requestAnimationFrame(step);
3515
+ };
3516
+ frame = requestAnimationFrame(step);
3517
+ return () => cancelAnimationFrame(frame);
3518
+ }, [
3519
+ live,
3520
+ playing,
3521
+ position,
3522
+ observedTotal,
3523
+ snapshot
3524
+ ]);
3525
+ useLayoutEffect(() => {
3526
+ paint(playing ? playhead.current : position ?? (live ? liveNow() : observedTotal));
3527
+ }, [view, trackPx]);
3528
+ return {
3529
+ clockMs,
3530
+ limitMs: liveNow,
3531
+ positionMs: () => playhead.current
3532
+ };
3533
+ }
3534
+ //#endregion
3535
+ //#region src/react/flow-waterfall-playback.tsx
2417
3536
  const SPEEDS = [
2418
3537
  .5,
2419
3538
  1,
@@ -2421,375 +3540,618 @@ const SPEEDS = [
2421
3540
  2,
2422
3541
  4
2423
3542
  ];
2424
- const controlClass = "fw-control";
2425
- const seconds = (ms) => `${(ms / 1e3).toFixed(1)}s`;
2426
- /** Shared trace inspector. Data remains producer-owned; playback only clips its presentation. */
2427
- function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defaultCollapsed = false }) {
3543
+ /** Play, restart, speed, a scrub track over the trace's own activity, zoom and the legend. */
3544
+ function PlaybackBar({ view, playing, limit, clockMs, speed, zoom, marks, slider, timeOutput, followLabel, onTogglePlay, onRestart, onSpeed, onScrub, onFollow, onZoom, onFit }) {
3545
+ return /* @__PURE__ */ jsxs("footer", {
3546
+ className: "fw-playback",
3547
+ children: [
3548
+ /* @__PURE__ */ jsx("button", {
3549
+ type: "button",
3550
+ className: "fw-round-button",
3551
+ "aria-label": playing ? "Pause" : "Play",
3552
+ disabled: limit === 0,
3553
+ onClick: onTogglePlay,
3554
+ children: playing ? /* @__PURE__ */ jsx(PauseGlyph, {}) : /* @__PURE__ */ jsx(PlayGlyph, {})
3555
+ }),
3556
+ /* @__PURE__ */ jsx("button", {
3557
+ type: "button",
3558
+ className: "fw-icon-button",
3559
+ "aria-label": "Restart",
3560
+ title: "Replay from the start",
3561
+ disabled: limit === 0,
3562
+ onClick: onRestart,
3563
+ children: /* @__PURE__ */ jsx(RestartGlyph, {})
3564
+ }),
3565
+ view === "agents" ? /* @__PURE__ */ jsx("div", {
3566
+ className: "fw-segmented",
3567
+ role: "radiogroup",
3568
+ "aria-label": "Playback speed",
3569
+ children: [
3570
+ 1,
3571
+ 2,
3572
+ 4
3573
+ ].map((value) => /* @__PURE__ */ jsxs("button", {
3574
+ type: "button",
3575
+ role: "radio",
3576
+ "aria-checked": speed === value,
3577
+ onClick: () => onSpeed(value),
3578
+ children: [value, "x"]
3579
+ }, value))
3580
+ }) : /* @__PURE__ */ jsxs("label", {
3581
+ className: "fw-speed",
3582
+ children: [/* @__PURE__ */ jsx("span", {
3583
+ className: "fw-sr-only",
3584
+ children: "Playback speed"
3585
+ }), /* @__PURE__ */ jsx("select", {
3586
+ "aria-label": "Playback speed",
3587
+ value: speed,
3588
+ onChange: (event) => onSpeed(Number(event.target.value)),
3589
+ children: SPEEDS.map((value) => /* @__PURE__ */ jsxs("option", {
3590
+ value,
3591
+ children: [value, "x"]
3592
+ }, value))
3593
+ })]
3594
+ }),
3595
+ /* @__PURE__ */ jsxs("div", {
3596
+ className: "fw-overview",
3597
+ children: [/* @__PURE__ */ jsxs("div", {
3598
+ className: "fw-overview-marks",
3599
+ "aria-hidden": true,
3600
+ children: [marks.map((mark, i) => /* @__PURE__ */ jsx("span", {
3601
+ className: `fw-ov fw-seg-${mark.kind}`,
3602
+ "data-tone": toneKey(mark.tone),
3603
+ style: {
3604
+ left: `${mark.startMs / Math.max(1, limit) * 100}%`,
3605
+ width: `${Math.max(.15, (Math.min(mark.endMs, limit) - mark.startMs) / Math.max(1, limit) * 100)}%`
3606
+ }
3607
+ }, i)), zoom && /* @__PURE__ */ jsx("span", {
3608
+ className: "fw-brush",
3609
+ style: {
3610
+ left: `${zoom.startMs / limit * 100}%`,
3611
+ width: `${(zoom.endMs - zoom.startMs) / limit * 100}%`
3612
+ }
3613
+ })]
3614
+ }), /* @__PURE__ */ jsx("input", {
3615
+ ref: slider,
3616
+ "aria-label": "Playback position",
3617
+ type: "range",
3618
+ min: 0,
3619
+ max: Math.max(1, limit),
3620
+ step: "any",
3621
+ defaultValue: clockMs,
3622
+ disabled: limit === 0,
3623
+ className: "fw-slider",
3624
+ style: { "--fw-progress": `${clockMs / Math.max(1, limit) * 100}%` },
3625
+ onChange: (event) => onScrub(Number(event.target.value))
3626
+ })]
3627
+ }),
3628
+ /* @__PURE__ */ jsx("output", {
3629
+ ref: timeOutput,
3630
+ className: "fw-time",
3631
+ "aria-label": "Playback time",
3632
+ children: formatSpanDuration(clockMs)
3633
+ }),
3634
+ followLabel && /* @__PURE__ */ jsx("button", {
3635
+ type: "button",
3636
+ className: "fw-text-button",
3637
+ onClick: onFollow,
3638
+ children: followLabel
3639
+ }),
3640
+ /* @__PURE__ */ jsxs("div", {
3641
+ className: "fw-zoom",
3642
+ role: "group",
3643
+ "aria-label": "Zoom",
3644
+ children: [
3645
+ /* @__PURE__ */ jsx("button", {
3646
+ type: "button",
3647
+ className: "fw-icon-button",
3648
+ "aria-label": "Zoom out",
3649
+ disabled: !zoom,
3650
+ onClick: () => onZoom(2),
3651
+ children: /* @__PURE__ */ jsx(ZoomOutGlyph, {})
3652
+ }),
3653
+ /* @__PURE__ */ jsx("button", {
3654
+ type: "button",
3655
+ className: "fw-icon-button",
3656
+ "aria-label": "Zoom in",
3657
+ disabled: limit === 0,
3658
+ onClick: () => onZoom(.5),
3659
+ children: /* @__PURE__ */ jsx(ZoomInGlyph, {})
3660
+ }),
3661
+ /* @__PURE__ */ jsx("button", {
3662
+ type: "button",
3663
+ className: "fw-icon-button",
3664
+ "aria-label": "Fit the whole trace",
3665
+ disabled: !zoom,
3666
+ onClick: onFit,
3667
+ children: /* @__PURE__ */ jsx(FitGlyph, {})
3668
+ })
3669
+ ]
3670
+ })
3671
+ ]
3672
+ });
3673
+ }
3674
+ //#endregion
3675
+ //#region src/react/flow-waterfall.tsx
3676
+ const LANE_PAD = 12;
3677
+ const END_LABEL_GUTTER = {
3678
+ spans: 120,
3679
+ agents: 16
3680
+ };
3681
+ const NAME_WIDTH = {
3682
+ spans: 320,
3683
+ agents: 240
3684
+ };
3685
+ const headlineSeconds = (ms) => ms < 6e4 ? `${(ms / 1e3).toFixed(1)}s` : formatSpanDuration(ms);
3686
+ /** Shared trace inspector: the agent timeline and the span waterfall over one clock.
3687
+ * Data remains producer-owned; playback, zoom and folding change presentation only. */
3688
+ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defaultCollapsed = false, defaultView = "spans", renderIcon, maxHeight = 640 }) {
2428
3689
  const root = useRef(null);
2429
3690
  const search = useRef(null);
3691
+ const slider = useRef(null);
3692
+ const timeOutput = useRef(null);
3693
+ const hideTimer = useRef(void 0);
2430
3694
  const searchId = useId();
3695
+ const cardId = useId();
3696
+ const [view, setView] = useState(defaultView);
2431
3697
  const [query, setQuery] = useState("");
2432
3698
  const [collapsed, setCollapsed] = useState(() => new Set(defaultCollapsed ? trace.spans.flatMap((span) => span.parentId ? [span.parentId] : []) : []));
2433
3699
  const [speed, setSpeed] = useState(1);
2434
3700
  const [playing, setPlaying] = useState(false);
2435
3701
  const [position, setPosition] = useState(null);
3702
+ const [zoom, setZoom] = useState(null);
3703
+ const [nameWidth, setNameWidth] = useState(NAME_WIDTH[defaultView]);
3704
+ const [laneWidth, setLaneWidth] = useState(0);
3705
+ const [card, setCard] = useState(null);
3706
+ const [activeId, setActiveId] = useState();
3707
+ const [laneElement, setLaneElement] = useState(null);
2436
3708
  const observedTotal = Number.isFinite(trace.totalMs) && trace.totalMs >= 0 ? trace.totalMs : 0;
2437
- const [elapsed, setElapsed] = useState(observedTotal);
2438
3709
  const isLive = live ?? trace.spans.some(spanRunning);
3710
+ const dataEnd = useMemo(() => Math.max(observedTotal, ...trace.spans.map((span) => Number.isFinite(span.endMs) ? span.endMs : 0)), [trace, observedTotal]);
3711
+ const trackPx = Math.max(40, laneWidth - LANE_PAD - Math.min(END_LABEL_GUTTER[view], laneWidth * .3));
2439
3712
  useEffect(() => {
2440
- setElapsed(observedTotal);
2441
- if (!isLive) return;
2442
- const receivedAt = Date.now();
2443
- const tick = () => setElapsed(Math.max(observedTotal, startedAt === void 0 ? observedTotal + Date.now() - receivedAt : Date.now() - startedAt));
2444
- tick();
2445
- const timer = setInterval(tick, 100);
2446
- return () => clearInterval(timer);
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
- };
3713
+ if (!laneElement) return;
3714
+ const observer = new ResizeObserver(([entry]) => setLaneWidth(entry.contentRect.width));
3715
+ observer.observe(laneElement);
3716
+ return () => observer.disconnect();
3717
+ }, [laneElement]);
3718
+ const viewAt = useCallback((t) => zoom ?? {
3719
+ startMs: 0,
3720
+ endMs: Math.max(dataEnd, isLive ? t : 0, 1)
2463
3721
  }, [
2464
- trace,
2465
- observedTotal,
2466
- elapsed,
3722
+ zoom,
3723
+ dataEnd,
2467
3724
  isLive
2468
3725
  ]);
2469
- const totalMs = currentTrace.totalMs;
2470
- const playhead = Math.min(position ?? totalMs, totalMs);
2471
- const playbackLimit = useRef(totalMs);
2472
- useEffect(() => {
2473
- playbackLimit.current = totalMs;
2474
- }, [totalMs]);
2475
- useEffect(() => {
2476
- if (!playing) return;
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,
3726
+ const clock = useTraceClock({
3727
+ root,
3728
+ slider,
3729
+ timeOutput,
3730
+ observedTotal: dataEnd,
3731
+ live: isLive,
3732
+ startedAt,
3733
+ snapshot: trace,
2490
3734
  playing,
2491
- totalMs
3735
+ speed,
3736
+ position,
3737
+ onPlaybackEnd: (end) => {
3738
+ setPlaying(false);
3739
+ setPosition(end);
3740
+ },
3741
+ view: viewAt,
3742
+ trackPx
3743
+ });
3744
+ const clockMs = clock.clockMs;
3745
+ const win = viewAt(clockMs);
3746
+ const limit = Math.max(dataEnd, isLive ? clockMs : 0);
3747
+ const resolution = 2 ** Math.round(Math.log2(Math.max(1, (win.endMs - win.startMs) / Math.max(200, trackPx) * 1.5)));
3748
+ const rows = useMemo(() => flowWaterfallRows({
3749
+ ...trace,
3750
+ totalMs: dataEnd
3751
+ }, resolution), [
3752
+ trace,
3753
+ dataEnd,
3754
+ resolution
2492
3755
  ]);
3756
+ const byId = useMemo(() => new Map(rows.map((row) => [row.id, row])), [rows]);
3757
+ const agents = useMemo(() => {
3758
+ const pipelines = rows.filter((row) => row.kind === "pipeline");
3759
+ return pipelines.length ? pipelines : rows.filter((row) => !row.parentId);
3760
+ }, [rows]);
3761
+ const visible = useMemo(() => view === "agents" ? agents : visibleFlowWaterfallRows(rows, query, collapsed), [
3762
+ view,
3763
+ agents,
3764
+ rows,
3765
+ query,
3766
+ collapsed
3767
+ ]);
3768
+ const slowest = view === "spans" ? slowestRow(rows, clockMs) : void 0;
2493
3769
  useEffect(() => {
2494
3770
  const keydown = (event) => {
2495
- if (!(event.metaKey || event.ctrlKey) || event.key.toLowerCase() !== "f") return;
2496
3771
  const active = document.activeElement;
2497
3772
  const ownsFocus = root.current?.contains(active);
2498
- const first = Array.from(document.querySelectorAll("[data-flow-waterfall]")).find((element) => element.getClientRects().length > 0);
2499
- if (!ownsFocus && (active !== document.body || first !== root.current)) return;
2500
- event.preventDefault();
2501
- search.current?.focus();
2502
- search.current?.select();
3773
+ if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "f") {
3774
+ const first = Array.from(document.querySelectorAll("[data-flow-waterfall]")).find((element) => element.getClientRects().length > 0);
3775
+ if (!ownsFocus && (active !== document.body || first !== root.current)) return;
3776
+ event.preventDefault();
3777
+ if (view === "agents") setView("spans");
3778
+ requestAnimationFrame(() => {
3779
+ search.current?.focus();
3780
+ search.current?.select();
3781
+ });
3782
+ return;
3783
+ }
3784
+ if (!ownsFocus || active instanceof HTMLInputElement || active instanceof HTMLSelectElement || event.metaKey || event.ctrlKey || event.altKey) return;
3785
+ if (event.key === "+" || event.key === "=") zoomBy(.5);
3786
+ else if (event.key === "-" || event.key === "_") zoomBy(2);
3787
+ else if (event.key === "0") setZoom(null);
3788
+ else if (event.key === "Escape" && card) closeCard();
2503
3789
  };
2504
3790
  document.addEventListener("keydown", keydown);
2505
3791
  return () => document.removeEventListener("keydown", keydown);
3792
+ });
3793
+ useEffect(() => {
3794
+ if (!card?.pinned) return;
3795
+ const down = (event) => {
3796
+ const target = event.target;
3797
+ if (target?.closest(".fw-card") || target?.closest(".fw-bar, .fw-seg")) return;
3798
+ setCard(null);
3799
+ };
3800
+ document.addEventListener("pointerdown", down);
3801
+ return () => document.removeEventListener("pointerdown", down);
3802
+ }, [card?.pinned]);
3803
+ const zoomBy = (factor, anchor) => setZoom((previous) => {
3804
+ const current = previous ?? viewAt(clockMs);
3805
+ const next = zoomWindow(current, factor, anchor ?? (current.startMs + current.endMs) / 2, limit);
3806
+ return next.endMs - next.startMs >= limit - 1 ? null : next;
3807
+ });
3808
+ const zoomFrame = useRef(0);
3809
+ const zoomTo = (row) => {
3810
+ const span = row.span;
3811
+ const current = viewAt(clockMs);
3812
+ const width = current.endMs - current.startMs;
3813
+ const end = spanRunning(span) ? Math.max(span.endMs, clockMs) : span.endMs;
3814
+ if (view !== "spans" || !Number.isFinite(end) || (end - span.startMs) / Math.max(1, width) > .25) return;
3815
+ const pad = Math.max((end - span.startMs) * .15, 500);
3816
+ const target = clampWindow({
3817
+ startMs: span.startMs - pad,
3818
+ endMs: end + pad
3819
+ }, limit);
3820
+ cancelAnimationFrame(zoomFrame.current);
3821
+ if (globalThis.matchMedia?.("(prefers-reduced-motion: reduce)").matches) {
3822
+ setZoom(target);
3823
+ return;
3824
+ }
3825
+ const began = performance.now();
3826
+ const step = (now) => {
3827
+ const t = Math.min(1, (now - began) / 320);
3828
+ const e = 1 - (1 - t) ** 3;
3829
+ setZoom({
3830
+ startMs: current.startMs + (target.startMs - current.startMs) * e,
3831
+ endMs: current.endMs + (target.endMs - current.endMs) * e
3832
+ });
3833
+ if (t < 1) zoomFrame.current = requestAnimationFrame(step);
3834
+ };
3835
+ zoomFrame.current = requestAnimationFrame(step);
3836
+ };
3837
+ const pan = (deltaMs) => setZoom((previous) => previous && clampWindow({
3838
+ startMs: previous.startMs + deltaMs,
3839
+ endMs: previous.endMs + deltaMs
3840
+ }, limit));
3841
+ const latest = useRef({
3842
+ toggle: (_row) => {},
3843
+ pin: (_target, _element) => {},
3844
+ inspect: (_target, _element) => {}
3845
+ });
3846
+ const toggleNow = (row) => {
3847
+ if (view === "agents") {
3848
+ const keep = /* @__PURE__ */ new Set();
3849
+ for (let cursor = row; cursor; cursor = cursor.parentId ? byId.get(cursor.parentId) : void 0) keep.add(cursor.id);
3850
+ setCollapsed(new Set(rows.filter((item) => item.hasChildren && !keep.has(item.id) && item.kind === "pipeline").map((item) => item.id)));
3851
+ setView("spans");
3852
+ setActiveId(row.id);
3853
+ return;
3854
+ }
3855
+ if (collapsed.has(row.id)) zoomTo(row);
3856
+ setCollapsed((previous) => {
3857
+ const next = new Set(previous);
3858
+ if (next.has(row.id)) next.delete(row.id);
3859
+ else next.add(row.id);
3860
+ return next;
3861
+ });
3862
+ };
3863
+ latest.current.toggle = toggleNow;
3864
+ const toggle = useCallback((row) => latest.current.toggle(row), []);
3865
+ const place = (target, element, pinned) => {
3866
+ const host = root.current?.getBoundingClientRect();
3867
+ if (!host) return;
3868
+ const rect = element.getBoundingClientRect();
3869
+ const above = view === "agents" ? rect.top - host.top > 150 : globalThis.innerHeight - rect.bottom < 260 && rect.top - host.top > 260;
3870
+ const left = Math.max(8, Math.min(rect.left - host.left, host.width - 328));
3871
+ clearTimeout(hideTimer.current);
3872
+ setCard({
3873
+ target,
3874
+ pinned,
3875
+ left,
3876
+ top: above ? rect.top - host.top - 8 : rect.bottom - host.top + 8,
3877
+ above
3878
+ });
3879
+ };
3880
+ latest.current.inspect = (target, element) => {
3881
+ if (!card?.pinned) place(target, element, false);
3882
+ };
3883
+ const inspect = useCallback((target, element) => latest.current.inspect(target, element), []);
3884
+ const leave = useCallback(() => {
3885
+ clearTimeout(hideTimer.current);
3886
+ hideTimer.current = setTimeout(() => setCard((previous) => previous?.pinned ? previous : null), 140);
2506
3887
  }, []);
2507
- const rows = useMemo(() => flowWaterfallRows(currentTrace), [currentTrace]);
2508
- const visible = visibleFlowWaterfallRows(rows, query, collapsed);
2509
- const slowest = rows.filter((row) => !row.hasChildren && row.kind !== "pipeline" && row.durationLabel !== "—").reduce((best, row) => !best || row.span.endMs - row.span.startMs > best.span.endMs - best.span.startMs ? row : best, void 0);
3888
+ useEffect(() => {
3889
+ if (!card || card.pinned) return;
3890
+ const move = (event) => {
3891
+ if (!event.target?.closest(".fw-bar, .fw-seg, .fw-card")) leave();
3892
+ };
3893
+ document.addEventListener("pointermove", move);
3894
+ return () => document.removeEventListener("pointermove", move);
3895
+ }, [card, leave]);
3896
+ latest.current.pin = (target, element) => {
3897
+ place(target, element, true);
3898
+ const row = byId.get(target);
3899
+ if (row) zoomTo(row);
3900
+ };
3901
+ const pin = useCallback((target, element) => latest.current.pin(target, element), []);
3902
+ const closeCard = () => {
3903
+ const target = card?.target;
3904
+ setCard(null);
3905
+ if (target) root.current?.querySelector(`.fw-canvas [data-span-id="${CSS.escape(target)}"]`)?.focus({ preventScroll: true });
3906
+ };
3907
+ const scrubTo = useCallback((ms) => {
3908
+ setPlaying(false);
3909
+ setPosition(Math.min(ms, clock.limitMs()));
3910
+ }, [clock]);
3911
+ const togglePlay = () => {
3912
+ if (playing) {
3913
+ setPosition(clock.positionMs());
3914
+ setPlaying(false);
3915
+ return;
3916
+ }
3917
+ if (position === null || position >= limit - 1) setPosition(0);
3918
+ setPlaying(true);
3919
+ };
3920
+ const scrubbing = position !== null || playing;
3921
+ const at = scrubbing ? totalsAt(trace, clockMs) : void 0;
3922
+ const costUsd = at ? at.costUsd : trace.costUsd;
3923
+ const tokens = at ? at.tokens : trace.promptTokens + trace.completionTokens;
3924
+ const modelCalls = at ? at.modelCalls : trace.modelCalls;
3925
+ const errors = at ? at.errors : trace.errors ?? trace.spans.filter(spanFailed).length;
2510
3926
  const summary = [
2511
- ["Duration", seconds(totalMs)],
2512
- ["Cost", trace.costUsd === void 0 ? "Unknown" : `$${trace.costUsd.toFixed(trace.costUsd < .01 ? 6 : 4)}`],
2513
- ["Tokens", (trace.promptTokens + trace.completionTokens).toLocaleString()],
2514
- ["LLM calls", trace.modelCalls === void 0 ? "Unknown" : String(trace.modelCalls)],
2515
- ["Tool calls", String(trace.toolCalls)],
2516
- ["Errors", String(trace.errors ?? trace.spans.filter(spanFailed).length)]
3927
+ ["Duration", formatSpanDuration(scrubbing ? clockMs : limit)],
3928
+ [
3929
+ trace.costBasis === "list" ? "Cost · list price" : "Cost",
3930
+ costUsd === void 0 ? "Unknown" : formatCost(costUsd),
3931
+ 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
3932
+ ],
3933
+ [
3934
+ "Tokens",
3935
+ formatCount(tokens),
3936
+ `${tokens.toLocaleString()} tokens`
3937
+ ],
3938
+ [
3939
+ "LLM calls",
3940
+ modelCalls === void 0 ? "Unknown" : modelCalls.toLocaleString(),
3941
+ modelCalls === void 0 ? "Model output windows do not establish the number of LLM requests" : void 0
3942
+ ],
3943
+ ["Tool calls", (at ? at.toolCalls : trace.toolCalls).toLocaleString()],
3944
+ ["Errors", errors.toLocaleString()]
2517
3945
  ];
2518
- const toggle = (id) => setCollapsed((previous) => {
2519
- const next = new Set(previous);
2520
- if (next.has(id)) next.delete(id);
2521
- else next.add(id);
2522
- return next;
2523
- });
3946
+ const busy = agents.reduce((sum, row) => sum + busyMs(row, limit), 0);
3947
+ const speedup = limit > 0 ? busy / limit : 0;
3948
+ const done = !isLive && !trace.spans.some(spanRunning);
3949
+ const cardRow = card ? byId.get(card.target) : void 0;
3950
+ const cardAgent = cardRow && (() => {
3951
+ for (let cursor = cardRow; cursor; cursor = cursor.parentId ? byId.get(cursor.parentId) : void 0) if (cursor.kind === "pipeline") return cursor;
3952
+ })();
3953
+ const showCursor = isLive || position !== null || playing;
3954
+ const overviewMarks = useMemo(() => rows.filter((row) => !row.parentId).flatMap((row) => row.hasChildren ? row.segments : row.kind === "pipeline" ? [] : [{
3955
+ startMs: row.span.startMs,
3956
+ endMs: row.span.endMs,
3957
+ kind: row.ok ? "tool" : "failed",
3958
+ tone: row.tone,
3959
+ id: row.id,
3960
+ count: 1,
3961
+ retry: false
3962
+ }]), [rows]);
2524
3963
  return /* @__PURE__ */ jsxs("section", {
2525
3964
  ref: root,
2526
3965
  "data-flow-waterfall": true,
2527
3966
  "aria-label": label,
2528
- className: "fw-root",
3967
+ className: `fw-root fw-root-${view}`,
3968
+ style: {
3969
+ "--fw-name-w": `${nameWidth}px`,
3970
+ "--fw-pad": `${LANE_PAD}px`,
3971
+ "--fw-row-h": `${ROW_HEIGHT[view]}px`
3972
+ },
2529
3973
  children: [
2530
- /* @__PURE__ */ jsxs("div", {
3974
+ /* @__PURE__ */ jsxs("header", {
2531
3975
  className: "fw-heading",
2532
3976
  children: [/* @__PURE__ */ jsxs("div", {
2533
- className: "fw-title",
2534
- children: [label, isLive && /* @__PURE__ */ jsx("span", {
2535
- className: "fw-muted",
2536
- children: "Live"
3977
+ className: "fw-title-block",
3978
+ children: [/* @__PURE__ */ jsxs("div", {
3979
+ className: "fw-title-line",
3980
+ children: [
3981
+ /* @__PURE__ */ jsx("span", {
3982
+ className: "fw-title-glyph",
3983
+ "aria-hidden": true,
3984
+ children: /* @__PURE__ */ jsx(TraceGlyph, {})
3985
+ }),
3986
+ /* @__PURE__ */ jsx("h3", {
3987
+ className: "fw-title",
3988
+ children: label
3989
+ }),
3990
+ isLive && /* @__PURE__ */ jsx("span", {
3991
+ className: "fw-state fw-state-live",
3992
+ children: "Live"
3993
+ })
3994
+ ]
3995
+ }), view === "agents" && /* @__PURE__ */ jsx("p", {
3996
+ className: "fw-headline",
3997
+ "aria-live": "polite",
3998
+ children: done ? /* @__PURE__ */ jsxs(Fragment, { children: [
3999
+ /* @__PURE__ */ jsx("span", {
4000
+ className: "fw-done-mark",
4001
+ "aria-label": "done",
4002
+ children: "✓"
4003
+ }),
4004
+ agents.length,
4005
+ " ",
4006
+ agents.length === 1 ? "agent" : "agents",
4007
+ " · ",
4008
+ headlineSeconds(limit),
4009
+ speedup >= 1.05 ? ` · ${speedup.toFixed(1)}x parallel` : "",
4010
+ trace.costUsd !== void 0 ? ` · ${formatCost(trace.costUsd)}` : ""
4011
+ ] }) : /* @__PURE__ */ jsxs(Fragment, { children: [
4012
+ headlineSeconds(clockMs),
4013
+ " · ",
4014
+ tokens.toLocaleString(),
4015
+ " tokens",
4016
+ trace.costUsd !== void 0 ? ` · ${formatCost(trace.costUsd)}` : ""
4017
+ ] })
2537
4018
  })]
2538
4019
  }), /* @__PURE__ */ jsxs("div", {
2539
- className: "fw-search",
2540
- children: [
2541
- /* @__PURE__ */ jsx("label", {
2542
- htmlFor: searchId,
2543
- className: "fw-sr-only",
2544
- children: "Search trace"
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", {
4020
+ className: "fw-tools",
4021
+ children: [/* @__PURE__ */ jsx("div", {
4022
+ className: "fw-views",
4023
+ role: "radiogroup",
4024
+ "aria-label": "Trace view",
4025
+ children: ["agents", "spans"].map((option) => /* @__PURE__ */ jsx("button", {
2563
4026
  type: "button",
2564
- "aria-label": "Focus trace search",
2565
- title: "Search trace (⌘F / Ctrl+F)",
2566
- onClick: () => search.current?.focus(),
2567
- className: controlClass,
2568
- children: "⌘F"
2569
- })
2570
- ]
4027
+ role: "radio",
4028
+ "aria-checked": view === option,
4029
+ className: "fw-view-button",
4030
+ onClick: () => {
4031
+ setView(option);
4032
+ setNameWidth(NAME_WIDTH[option]);
4033
+ setCard(null);
4034
+ },
4035
+ children: option === "agents" ? "Agents" : "Steps"
4036
+ }, option))
4037
+ }), view === "spans" && /* @__PURE__ */ jsxs("div", {
4038
+ className: "fw-search",
4039
+ children: [
4040
+ /* @__PURE__ */ jsx("label", {
4041
+ htmlFor: searchId,
4042
+ className: "fw-sr-only",
4043
+ children: "Search trace"
4044
+ }),
4045
+ /* @__PURE__ */ jsx("span", {
4046
+ className: "fw-search-glyph",
4047
+ "aria-hidden": true,
4048
+ children: /* @__PURE__ */ jsx(SearchGlyph, {})
4049
+ }),
4050
+ /* @__PURE__ */ jsx("input", {
4051
+ ref: search,
4052
+ id: searchId,
4053
+ type: "search",
4054
+ value: query,
4055
+ placeholder: "Search",
4056
+ "aria-keyshortcuts": "Meta+f Control+f",
4057
+ onChange: (event) => setQuery(event.target.value),
4058
+ onKeyDown: (event) => {
4059
+ if (event.key === "Escape") {
4060
+ setQuery("");
4061
+ event.stopPropagation();
4062
+ }
4063
+ },
4064
+ className: "fw-search-input"
4065
+ })
4066
+ ]
4067
+ })]
2571
4068
  })]
2572
4069
  }),
2573
- /* @__PURE__ */ jsx("dl", {
4070
+ view === "spans" && /* @__PURE__ */ jsx("dl", {
2574
4071
  className: "fw-summary",
2575
- children: summary.map(([name, value]) => /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", {
2576
- className: "fw-muted",
2577
- children: name
2578
- }), /* @__PURE__ */ jsx("dd", {
2579
- className: "fw-value",
2580
- title: name === "LLM calls" && trace.modelCalls === void 0 ? "Model output windows do not establish the number of LLM requests" : void 0,
2581
- children: value
2582
- })] }, name))
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))
4072
+ children: summary.map(([name, value, title]) => /* @__PURE__ */ jsxs("div", {
4073
+ className: name === "Errors" && errors > 0 ? "fw-has-errors" : void 0,
4074
+ children: [/* @__PURE__ */ jsx("dt", { children: name }), /* @__PURE__ */ jsx("dd", {
4075
+ className: "fw-value",
4076
+ title,
4077
+ children: value
4078
+ })]
4079
+ }, name))
2594
4080
  }),
2595
- rows.some((row) => row.hasChildren) && /* @__PURE__ */ jsxs("div", {
2596
- className: "fw-branches",
2597
- children: [/* @__PURE__ */ jsx("button", {
2598
- type: "button",
2599
- className: controlClass,
2600
- onClick: () => setCollapsed(/* @__PURE__ */ new Set()),
2601
- children: "Expand all"
2602
- }), /* @__PURE__ */ jsx("button", {
2603
- type: "button",
2604
- className: controlClass,
2605
- onClick: () => setCollapsed(new Set(rows.filter((row) => row.hasChildren).map((row) => row.id))),
2606
- children: "Collapse all"
2607
- })]
4081
+ /* @__PURE__ */ jsx(FlowBody, {
4082
+ rows: visible,
4083
+ byId,
4084
+ totalRows: rows.length,
4085
+ view,
4086
+ maxHeight,
4087
+ collapsed,
4088
+ searching: Boolean(query.trim()),
4089
+ slowestId: slowest?.id,
4090
+ activeId,
4091
+ selectedId: card?.pinned ? card.target : void 0,
4092
+ clockMs,
4093
+ window: win,
4094
+ trackPx,
4095
+ showCursor,
4096
+ zoomed: zoom !== null,
4097
+ renderIcon,
4098
+ emptyText: rows.length === 0 ? "No spans yet. Waiting for trace events." : `No matching steps for “${query}”.`,
4099
+ onToggle: toggle,
4100
+ onInspect: inspect,
4101
+ onLeave: leave,
4102
+ onPin: pin,
4103
+ onActivate: (row) => setActiveId(row.id),
4104
+ onActiveChange: setActiveId,
4105
+ onScrub: scrubTo,
4106
+ onZoom: zoomBy,
4107
+ onPan: pan,
4108
+ nameWidth,
4109
+ onNameWidth: (px) => setNameWidth(Math.max(140, Math.min(560, px))),
4110
+ onTrack: setLaneElement,
4111
+ onExpandAll: rows.some((row) => row.hasChildren) ? () => setCollapsed(/* @__PURE__ */ new Set()) : void 0,
4112
+ onCollapseAll: () => setCollapsed(new Set(rows.filter((row) => row.hasChildren).map((row) => row.id)))
2608
4113
  }),
2609
- /* @__PURE__ */ jsx("div", {
2610
- className: "fw-timeline",
2611
- role: "region",
2612
- "aria-label": "Trace timeline",
2613
- tabIndex: 0,
2614
- children: /* @__PURE__ */ jsxs("div", {
2615
- className: "fw-track",
2616
- children: [
2617
- /* @__PURE__ */ jsxs("div", {
2618
- className: "fw-axis fw-grid",
2619
- children: [
2620
- /* @__PURE__ */ jsxs("span", { children: [
2621
- "Steps · ",
2622
- visible.length,
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
- ]
4114
+ card && cardRow && /* @__PURE__ */ jsx("div", {
4115
+ className: `fw-card-anchor${card.above ? " fw-card-above" : ""}`,
4116
+ onPointerEnter: () => clearTimeout(hideTimer.current),
4117
+ onPointerLeave: card.pinned ? void 0 : leave,
4118
+ children: /* @__PURE__ */ jsx(SpanCard, {
4119
+ id: cardId,
4120
+ row: cardRow,
4121
+ agent: cardAgent,
4122
+ clockMs,
4123
+ left: card.left,
4124
+ top: card.top,
4125
+ pinned: card.pinned,
4126
+ onClose: closeCard,
4127
+ renderIcon,
4128
+ compact: view === "agents"
2708
4129
  })
2709
4130
  }),
2710
- slowest && /* @__PURE__ */ jsxs("p", {
2711
- className: "fw-slowest-caption",
2712
- children: [
2713
- "Slowest step: ",
2714
- /* @__PURE__ */ jsx("span", {
2715
- className: "fw-text",
2716
- children: slowest.name
2717
- }),
2718
- " · ",
2719
- slowest.durationLabel
2720
- ]
2721
- }),
2722
- /* @__PURE__ */ jsxs("div", {
2723
- className: "fw-playback",
2724
- children: [
2725
- /* @__PURE__ */ jsx("button", {
2726
- type: "button",
2727
- className: controlClass,
2728
- disabled: totalMs === 0,
2729
- onClick: () => {
2730
- if (playing) setPlaying(false);
2731
- else {
2732
- if (position === null || position >= totalMs) setPosition(0);
2733
- setPlaying(true);
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
- ]
4131
+ /* @__PURE__ */ jsx(PlaybackBar, {
4132
+ view,
4133
+ playing,
4134
+ limit,
4135
+ clockMs,
4136
+ speed,
4137
+ zoom,
4138
+ marks: overviewMarks,
4139
+ slider,
4140
+ timeOutput,
4141
+ followLabel: position === null ? void 0 : isLive ? "Follow live" : "Show full trace",
4142
+ onTogglePlay: togglePlay,
4143
+ onRestart: () => {
4144
+ setPosition(0);
4145
+ setPlaying(false);
4146
+ },
4147
+ onSpeed: setSpeed,
4148
+ onScrub: scrubTo,
4149
+ onFollow: () => {
4150
+ setPosition(null);
4151
+ setPlaying(false);
4152
+ },
4153
+ onZoom: zoomBy,
4154
+ onFit: () => setZoom(null)
2793
4155
  })
2794
4156
  ]
2795
4157
  });