@tangle-network/charts 0.21.14 → 0.21.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2422,6 +2422,7 @@ function DotMap({ positions, radii, colors, palette, extent, rects, labels, dimm
2422
2422
  }
2423
2423
  //#endregion
2424
2424
  //#region src/react/flow-waterfall-model.ts
2425
+ const toolOf = (name) => name?.split(" · ")[0];
2425
2426
  /** The CSS hue slot: 0 is the root agent's neutral, then five agent hues cycle. */
2426
2427
  const toneKey = (tone) => tone <= 0 ? 0 : (tone - 1) % 5 + 1;
2427
2428
  const spanFailed = (span) => span.status === "failed";
@@ -2474,6 +2475,7 @@ function mergeSegments(marks, gapMs, cap = 240) {
2474
2475
  covered.set(last, share);
2475
2476
  last.endMs = Math.max(last.endMs, mark.endMs);
2476
2477
  last.count += mark.count;
2478
+ if (last.tool !== mark.tool) last.tool = void 0;
2477
2479
  last.kind = share.model >= share.tool ? "model" : "tool";
2478
2480
  } else out.push({ ...mark });
2479
2481
  }
@@ -2544,6 +2546,7 @@ function flowWaterfallRows(trace, resolutionMs) {
2544
2546
  row.hasChildren = kids.length > 0;
2545
2547
  ordered_.push(row);
2546
2548
  const marks = [];
2549
+ const own = row.span.promptTokens !== void 0 || row.span.completionTokens !== void 0 || row.span.cachedTokens !== void 0;
2547
2550
  let cost = row.span.costUsd ?? 0;
2548
2551
  let costKnown = row.span.kind !== "model" || row.span.costUsd !== void 0;
2549
2552
  for (const child of kids) {
@@ -2554,18 +2557,15 @@ function flowWaterfallRows(trace, resolutionMs) {
2554
2557
  if (childCost === void 0) {
2555
2558
  if (child.kind === "model" || child.hasChildren) costKnown = false;
2556
2559
  } else cost += childCost;
2557
- if (child.kind !== "pipeline" && child.kind !== "waiting" && ordered(child.span)) {
2558
- row.own.push(segmentOf(child));
2559
- const counts = row.counts ??= {
2560
- model: 0,
2561
- tool: 0,
2562
- failed: 0
2563
- };
2564
- if (!child.ok) counts.failed++;
2565
- else if (child.kind === "model") counts.model++;
2566
- else counts.tool++;
2567
- }
2560
+ if (child.kind !== "pipeline" && child.kind !== "waiting" && ordered(child.span)) row.own.push(segmentOf(child));
2568
2561
  }
2562
+ Object.assign(row, stepStats(kids));
2563
+ const tokens = own ? {
2564
+ prompt: row.span.promptTokens,
2565
+ completion: row.span.completionTokens,
2566
+ cached: row.span.cachedTokens
2567
+ } : sumTokens(kids);
2568
+ if (tokens) row.tokens = tokens;
2569
2569
  if (row.hasChildren && costKnown) row.rollupCostUsd = cost;
2570
2570
  if (row.hasChildren) {
2571
2571
  row.segments = mergeSegments(marks.map((mark) => ({ ...mark })), gap);
@@ -2578,6 +2578,74 @@ function flowWaterfallRows(trace, resolutionMs) {
2578
2578
  for (const row of rows) if (!row.parentId) visit(row, 0, -1);
2579
2579
  return ordered_;
2580
2580
  }
2581
+ /** What a row's direct steps add up to; nothing when it has none. */
2582
+ function stepStats(steps) {
2583
+ let counts;
2584
+ const modelMs = [];
2585
+ for (const step of steps) {
2586
+ if (step.kind === "pipeline" || step.kind === "waiting" || !ordered(step.span)) continue;
2587
+ counts ??= {
2588
+ model: 0,
2589
+ tool: 0,
2590
+ failed: 0,
2591
+ retried: 0,
2592
+ tools: {}
2593
+ };
2594
+ if (!step.ok) counts.failed++;
2595
+ if (step.retry) counts.retried++;
2596
+ if (step.kind === "model") {
2597
+ counts.model++;
2598
+ if (!spanRunning(step.span)) modelMs.push(step.span.endMs - step.span.startMs);
2599
+ } else {
2600
+ counts.tool++;
2601
+ const tool = toolOf(step.name) || step.kind;
2602
+ counts.tools[tool] = (counts.tools[tool] ?? 0) + 1;
2603
+ }
2604
+ }
2605
+ return counts ? {
2606
+ counts,
2607
+ modelMs: modelMs.sort((a, b) => a - b)
2608
+ } : {};
2609
+ }
2610
+ /** Several rows' steps as one: a family's members or a folded run. */
2611
+ function mergeStats(rows) {
2612
+ let counts;
2613
+ for (const row of rows) {
2614
+ if (!row.counts) continue;
2615
+ counts ??= {
2616
+ model: 0,
2617
+ tool: 0,
2618
+ failed: 0,
2619
+ retried: 0,
2620
+ tools: {}
2621
+ };
2622
+ counts.model += row.counts.model;
2623
+ counts.tool += row.counts.tool;
2624
+ counts.failed += row.counts.failed;
2625
+ counts.retried += row.counts.retried;
2626
+ for (const [tool, n] of Object.entries(row.counts.tools)) counts.tools[tool] = (counts.tools[tool] ?? 0) + n;
2627
+ }
2628
+ const tokens = sumTokens(rows);
2629
+ return {
2630
+ ...counts ? {
2631
+ counts,
2632
+ modelMs: rows.flatMap((row) => row.modelMs ?? []).sort((a, b) => a - b)
2633
+ } : {},
2634
+ ...tokens ? { tokens } : {}
2635
+ };
2636
+ }
2637
+ function sumTokens(rows) {
2638
+ let out;
2639
+ for (const row of rows) for (const key of [
2640
+ "prompt",
2641
+ "completion",
2642
+ "cached"
2643
+ ]) {
2644
+ const n = row.tokens?.[key];
2645
+ if (n !== void 0) (out ??= {})[key] = (out[key] ?? 0) + n;
2646
+ }
2647
+ return out;
2648
+ }
2581
2649
  function segmentOf(row) {
2582
2650
  return {
2583
2651
  startMs: row.span.startMs,
@@ -2587,7 +2655,8 @@ function segmentOf(row) {
2587
2655
  id: row.id,
2588
2656
  count: 1,
2589
2657
  retry: row.retry,
2590
- name: row.name
2658
+ name: row.name,
2659
+ tool: toolOf(row.name)
2591
2660
  };
2592
2661
  }
2593
2662
  /** The rows a search term matches by themselves, before ancestor context is added. */
@@ -2786,7 +2855,7 @@ function agentFamilies(agents, open) {
2786
2855
  const endMs = Math.max(...members.map((row) => row.span.endMs));
2787
2856
  const running = members.some((row) => spanRunning(row.span));
2788
2857
  const known = members.map((row) => row.span.costUsd ?? row.rollupCostUsd).filter((cost) => cost !== void 0);
2789
- const failed = members.filter((row) => !row.ok).length;
2858
+ const failed = members.filter((row) => row.span.outcome ? row.span.outcome.tone === "failed" : !row.ok).length;
2790
2859
  const outcome = members.every((row) => row.span.outcome && row.span.outcome.tone === members[0].span.outcome?.tone) ? members[0].span.outcome : void 0;
2791
2860
  out.push({
2792
2861
  costFloor: known.length < members.length || members.some((row) => row.costFloor),
@@ -2810,6 +2879,7 @@ function agentFamilies(agents, open) {
2810
2879
  alt: i % 2 === 1
2811
2880
  }))).sort((a, b) => a.startMs - b.startMs),
2812
2881
  ...known.length ? { rollupCostUsd: known.reduce((sum, cost) => sum + cost, 0) } : {},
2882
+ ...mergeStats(members),
2813
2883
  span: {
2814
2884
  id,
2815
2885
  kind: "pipeline",
@@ -2840,35 +2910,24 @@ function agentFamilies(agents, open) {
2840
2910
  }
2841
2911
  return out;
2842
2912
  }
2843
- /** The second line of an agent row: how it ended and how much it did ("failed · 3 turns · 412 tool calls", "6 agents · 4 failed"). */
2913
+ /** The second line of an agent row in parts: how it ended and how much it did (failed, 3 LLM calls, 412 tool calls; 6 agents, 4 failed).
2914
+ * The row shows the parts in order and drops the last ones whole when the line is full, never half a word. */
2844
2915
  function agentSummary(row) {
2845
2916
  if (row.id.startsWith("family:")) {
2846
2917
  const failed = Number(row.span.meta?.failedAgents ?? 0);
2847
- return `${row.descendants} agents${failed ? ` · ${failed} failed` : ""}`;
2918
+ return [`${row.descendants} agents`, ...failed ? [`${failed} failed`] : []];
2848
2919
  }
2849
2920
  if (row.id.startsWith("run:")) {
2850
2921
  const failed = row.own.filter((mark) => mark.kind === "failed").length;
2851
- return failed ? `${failed} failed` : "all done";
2922
+ return [failed ? `${failed} failed` : "all done"];
2852
2923
  }
2853
- const counts = row.counts ?? {
2854
- model: 0,
2855
- tool: 0,
2856
- failed: 0
2857
- };
2858
- const turns = counts.model + counts.failed;
2859
- const tools = counts.tool;
2860
- const state = row.span.outcome?.label ?? (spanRunning(row.span) ? "running" : row.ok ? "done" : "failed");
2861
- const unit = row.kind === "pipeline" ? ["turn", "turns"] : ["model call", "model calls"];
2924
+ const calls = row.counts?.model ?? 0;
2925
+ const tools = row.counts?.tool ?? 0;
2862
2926
  return [
2863
- state,
2864
- turns ? `${turns} ${turns === 1 ? unit[0] : unit[1]}` : "",
2927
+ row.span.outcome?.label ?? (spanRunning(row.span) ? "running" : !row.ok ? "failed" : row.kind === "pipeline" ? "" : "done"),
2928
+ calls ? `${calls} LLM ${calls === 1 ? "call" : "calls"}` : "",
2865
2929
  tools ? `${tools} tool ${tools === 1 ? "call" : "calls"}` : ""
2866
- ].filter(Boolean).join(" · ");
2867
- }
2868
- /** The summary split where it may be cut: the tool count goes first when the line does not fit, never half a word. */
2869
- function agentSummaryParts(row) {
2870
- const tools = / · (\d+ tool calls?)$/.exec(agentSummary(row));
2871
- return tools ? [agentSummary(row).slice(0, tools.index), tools[1]] : [agentSummary(row)];
2930
+ ].filter(Boolean);
2872
2931
  }
2873
2932
  /**
2874
2933
  * The agent view's rows: families folded until opened, and an opened agent's own steps (its turns and tool calls, not its
@@ -2920,6 +2979,8 @@ function stepRuns(steps) {
2920
2979
  hasChildren: false,
2921
2980
  descendants: 0,
2922
2981
  segments: [],
2982
+ ...stepStats(run),
2983
+ tokens: sumTokens(run),
2923
2984
  own: run.map((step) => ({
2924
2985
  startMs: step.span.startMs,
2925
2986
  endMs: step.span.endMs,
@@ -2928,7 +2989,8 @@ function stepRuns(steps) {
2928
2989
  id: step.id,
2929
2990
  count: 1,
2930
2991
  retry: step.retry,
2931
- name: step.name
2992
+ name: step.name,
2993
+ tool: toolOf(step.name)
2932
2994
  })),
2933
2995
  ...costs.length ? { rollupCostUsd: costs.reduce((sum, cost) => sum + cost, 0) } : {},
2934
2996
  span: {
@@ -3110,58 +3172,110 @@ const KIND_LABEL = {
3110
3172
  waiting: "Waiting",
3111
3173
  handoff: "Handoff"
3112
3174
  };
3113
- const unknown = /* @__PURE__ */ jsx("span", {
3114
- className: "fw-unknown",
3115
- title: "Not recorded by the harness",
3116
- children: "—"
3175
+ const count = (n, unit) => /* @__PURE__ */ jsxs("span", {
3176
+ title: `${n.toLocaleString()} ${unit}`,
3177
+ children: [
3178
+ formatCount(n),
3179
+ " ",
3180
+ unit
3181
+ ]
3117
3182
  });
3118
- const count = (n) => n === void 0 ? unknown : /* @__PURE__ */ jsx("span", {
3119
- title: n.toLocaleString(),
3120
- children: formatCount(n)
3121
- });
3122
- /** The span card: what ran, for how long, at what recorded cost, on what input. */
3183
+ const plural = (n, one, many = `${one}s`) => `${n.toLocaleString()} ${n === 1 ? one : many}`;
3184
+ /** Nearest-rank percentile of ascending values. */
3185
+ const percentile = (sorted, p) => sorted[Math.min(sorted.length - 1, Math.ceil(p * sorted.length) - 1)];
3186
+ const TOOLS_SHOWN = 4;
3187
+ /** The card's table: what the row recorded, every value with its unit. A value nobody recorded is left out, never printed as zero. */
3188
+ function metrics(row, clockMs, full) {
3189
+ const span = row.span;
3190
+ const out = [];
3191
+ if (span.detail) out.push([span.kind === "tool" ? "Provider" : "Model", span.detail]);
3192
+ const stop = spanRunning(span) ? Math.max(span.endMs, clockMs) : span.endMs;
3193
+ out.push(["Duration", Number.isFinite(stop - span.startMs) && stop >= span.startMs ? formatSpanDuration(stop - span.startMs) : "—"]);
3194
+ if (full) out.push(["Started", `+${formatSpanDuration(Math.max(0, span.startMs))}`]);
3195
+ const tokens = row.tokens;
3196
+ if (tokens) {
3197
+ const parts = [
3198
+ ["prompt", "in"],
3199
+ ["completion", "out"],
3200
+ ["cached", "cached"]
3201
+ ].flatMap(([key, unit]) => tokens[key] === void 0 ? [] : [count(tokens[key], unit)]);
3202
+ out.push(["Tokens", /* @__PURE__ */ jsx("span", {
3203
+ className: "fw-card-parts",
3204
+ children: parts.map((part, i) => /* @__PURE__ */ jsx("span", { children: part }, i))
3205
+ })]);
3206
+ }
3207
+ const latency = row.modelMs;
3208
+ if (latency?.length) out.push(["LLM latency", /* @__PURE__ */ jsxs("span", {
3209
+ className: "fw-card-parts",
3210
+ children: [/* @__PURE__ */ jsxs("span", { children: [formatSpanDuration(percentile(latency, .5)), " median"] }), latency.length >= 10 && /* @__PURE__ */ jsxs("span", { children: [formatSpanDuration(percentile(latency, .95)), " p95"] })]
3211
+ })]);
3212
+ const cost = span.costUsd ?? row.rollupCostUsd;
3213
+ if (cost !== void 0) out.push(["Cost", rowCostText(row, cost)]);
3214
+ const counts = row.counts;
3215
+ if (counts?.model) out.push(["LLM calls", counts.model.toLocaleString()]);
3216
+ if (counts?.tool) {
3217
+ const tools = Object.entries(counts.tools).sort(([a, x], [b, y]) => y - x || a.localeCompare(b));
3218
+ const rest = tools.length - TOOLS_SHOWN;
3219
+ out.push(["Tool calls", /* @__PURE__ */ jsxs(Fragment, { children: [counts.tool.toLocaleString(), /* @__PURE__ */ jsxs("span", {
3220
+ className: "fw-card-tools",
3221
+ title: tools.map(([tool, n]) => `${tool} ${n}`).join(", "),
3222
+ children: [tools.slice(0, rest > 1 ? TOOLS_SHOWN : tools.length).map(([tool, n]) => /* @__PURE__ */ jsxs("span", { children: [/* @__PURE__ */ jsx("span", { children: tool }), /* @__PURE__ */ jsx("span", { children: n.toLocaleString() })] }, tool)), rest > 1 && /* @__PURE__ */ jsxs("span", { children: [/* @__PURE__ */ jsxs("span", { children: [rest, " more tools"] }), /* @__PURE__ */ jsx("span", { children: tools.slice(TOOLS_SHOWN).reduce((sum, [, n]) => sum + n, 0).toLocaleString() })] })]
3223
+ })] })]);
3224
+ }
3225
+ if (counts?.failed) out.push(["Failures", plural(counts.failed, "failed step")]);
3226
+ const restarts = span.marks?.filter((mark) => mark.kind === "restart").length ?? 0;
3227
+ if (counts?.retried || restarts) out.push(["Retries", [counts?.retried ? plural(counts.retried, "step retried", "steps retried") : "", restarts ? plural(restarts, "restart") : ""].filter(Boolean).join(" · ")]);
3228
+ if (full && row.hasChildren) out.push(["Steps", row.descendants.toLocaleString()]);
3229
+ for (const fact of span.facts ?? []) if (!out.some(([label]) => label.toLowerCase() === fact.label.toLowerCase())) out.push([fact.label, fact.value]);
3230
+ return out;
3231
+ }
3232
+ function Table({ rows }) {
3233
+ return /* @__PURE__ */ jsx("dl", {
3234
+ className: "fw-card-table",
3235
+ children: rows.map(([name, value]) => /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", { children: name }), /* @__PURE__ */ jsx("dd", { children: value })] }, name))
3236
+ });
3237
+ }
3238
+ /** The span card: what ran, for how long, on what tokens and at what recorded cost, then its input and output. */
3123
3239
  function SpanCard({ id, row, agent, clockMs, left, top, pinned, onClose, renderIcon, compact }) {
3124
3240
  const span = row.span;
3125
3241
  const status = spanRunning(span) ? "Running" : row.ok ? "Done" : "Failed";
3126
- const tokens = span.kind === "model" || span.kind === "pipeline" || span.promptTokens !== void 0;
3127
3242
  const owner = agent && agent.id !== row.id ? agent : void 0;
3128
- const lasted = (s) => {
3129
- const stop = spanRunning(s) ? Math.max(s.endMs, clockMs) : s.endMs;
3130
- return Number.isFinite(stop - s.startMs) && stop >= s.startMs ? formatSpanDuration(stop - s.startMs) : "—";
3243
+ const frame = {
3244
+ id,
3245
+ role: pinned ? "dialog" : "tooltip",
3246
+ "aria-label": pinned ? `${span.name} details` : void 0,
3247
+ style: {
3248
+ left,
3249
+ top
3250
+ },
3251
+ onKeyDown: (event) => {
3252
+ if (event.key === "Escape") {
3253
+ event.stopPropagation();
3254
+ onClose();
3255
+ }
3256
+ }
3131
3257
  };
3132
- const duration = lasted(span);
3258
+ const close = pinned && /* @__PURE__ */ jsx("button", {
3259
+ type: "button",
3260
+ className: "fw-icon-button fw-card-close",
3261
+ "aria-label": "Close details",
3262
+ onClick: onClose,
3263
+ autoFocus: true,
3264
+ children: /* @__PURE__ */ jsx(CloseGlyph, {})
3265
+ });
3133
3266
  if (compact) {
3134
3267
  const who = owner ?? row;
3135
- const subject = who.span;
3136
- const outcome = subject.outcome;
3137
- const tokenCount = subject.promptTokens !== void 0 || subject.completionTokens !== void 0 ? (subject.promptTokens ?? 0) + (subject.completionTokens ?? 0) : void 0;
3138
- const cost = subject.costUsd ?? who.rollupCostUsd;
3139
- const cells = [[subject.kind === "model" ? "Thinking" : KIND_LABEL[subject.kind], lasted(subject)]];
3140
- for (const fact of (subject.facts ?? []).slice(0, 3)) cells.push([fact.label, fact.value]);
3141
- cells.push(["Started", `+${formatSpanDuration(Math.max(0, subject.startMs))}`]);
3142
- if (tokenCount !== void 0) cells.push(["Tokens", count(tokenCount)]);
3143
- if (cost !== void 0) cells.push(["Cost", rowCostText(who, cost)]);
3144
- cells.splice(6);
3268
+ const outcome = who.span.outcome;
3145
3269
  const kind = span.kind === "model" ? "Thinking" : KIND_LABEL[span.kind];
3146
- const step = owner && span.endMs > span.startMs ? `${kind}${span.name && span.name !== kind && span.name.toLowerCase() !== span.kind ? ` ${span.name}` : ""} · ${duration}${row.ok ? "" : " · failed"}` : void 0;
3270
+ const named = span.name && span.name.toLowerCase() !== kind.toLowerCase() && span.name.toLowerCase() !== span.kind ? ` ${span.name}` : "";
3271
+ const stop = spanRunning(span) ? Math.max(span.endMs, clockMs) : span.endMs;
3272
+ const step = owner && span.endMs > span.startMs ? `${kind}${named} · ${formatSpanDuration(stop - span.startMs)}${row.ok ? "" : " · failed"}` : void 0;
3147
3273
  const sentence = !owner && span.output && span.output !== who.name ? span.output : void 0;
3148
3274
  return /* @__PURE__ */ jsxs("div", {
3149
- id,
3150
- role: pinned ? "dialog" : "tooltip",
3151
- "aria-label": pinned ? `${span.name} details` : void 0,
3275
+ ...frame,
3152
3276
  className: "fw-card fw-card-compact",
3153
3277
  "data-tone": toneKey(who.tone),
3154
3278
  "data-outcome": outcome?.tone,
3155
- style: {
3156
- left,
3157
- top
3158
- },
3159
- onKeyDown: (event) => {
3160
- if (event.key === "Escape") {
3161
- event.stopPropagation();
3162
- onClose();
3163
- }
3164
- },
3165
3279
  children: [
3166
3280
  /* @__PURE__ */ jsxs("div", {
3167
3281
  className: "fw-card-agent",
@@ -3183,28 +3297,10 @@ function SpanCard({ id, row, agent, clockMs, left, top, pinned, onClose, renderI
3183
3297
  className: `fw-pill fw-pill-${status.toLowerCase()}`,
3184
3298
  children: status
3185
3299
  }),
3186
- pinned && /* @__PURE__ */ jsx("button", {
3187
- type: "button",
3188
- className: "fw-icon-button",
3189
- style: {
3190
- width: 20,
3191
- height: 20
3192
- },
3193
- "aria-label": "Close details",
3194
- onClick: onClose,
3195
- autoFocus: true,
3196
- children: /* @__PURE__ */ jsx(CloseGlyph, {})
3197
- })
3300
+ close
3198
3301
  ]
3199
3302
  }),
3200
- who.span.detail && /* @__PURE__ */ jsx("p", {
3201
- className: "fw-card-detail",
3202
- children: who.span.detail
3203
- }),
3204
- /* @__PURE__ */ jsx("dl", {
3205
- className: "fw-card-cells",
3206
- children: cells.map(([name, value]) => /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", { children: name }), /* @__PURE__ */ jsx("dd", { children: value })] }, name))
3207
- }),
3303
+ /* @__PURE__ */ jsx(Table, { rows: metrics(who, clockMs, false) }),
3208
3304
  step && /* @__PURE__ */ jsx("p", {
3209
3305
  className: "fw-card-step",
3210
3306
  children: step
@@ -3217,35 +3313,16 @@ function SpanCard({ id, row, agent, clockMs, left, top, pinned, onClose, renderI
3217
3313
  });
3218
3314
  }
3219
3315
  return /* @__PURE__ */ jsxs("div", {
3220
- id,
3221
- role: pinned ? "dialog" : "tooltip",
3222
- "aria-label": pinned ? `${span.name} details` : void 0,
3316
+ ...frame,
3223
3317
  className: "fw-card",
3224
3318
  "data-tone": toneKey(row.tone),
3225
- style: {
3226
- left,
3227
- top
3228
- },
3229
- onKeyDown: (event) => {
3230
- if (event.key === "Escape") {
3231
- event.stopPropagation();
3232
- onClose();
3233
- }
3234
- },
3235
3319
  children: [
3236
3320
  owner && /* @__PURE__ */ jsxs("div", {
3237
3321
  className: "fw-card-agent",
3238
- children: [
3239
- /* @__PURE__ */ jsx("span", {
3240
- className: "fw-agent-dot",
3241
- "aria-hidden": true
3242
- }),
3243
- owner.name,
3244
- owner.span.detail && /* @__PURE__ */ jsxs("span", {
3245
- className: "fw-muted",
3246
- children: [" ", owner.span.detail]
3247
- })
3248
- ]
3322
+ children: [/* @__PURE__ */ jsx("span", {
3323
+ className: "fw-agent-dot",
3324
+ "aria-hidden": true
3325
+ }), owner.name]
3249
3326
  }),
3250
3327
  /* @__PURE__ */ jsxs("div", {
3251
3328
  className: "fw-card-head",
@@ -3258,42 +3335,18 @@ function SpanCard({ id, row, agent, clockMs, left, top, pinned, onClose, renderI
3258
3335
  name: span.name
3259
3336
  })
3260
3337
  }),
3261
- /* @__PURE__ */ jsxs("div", {
3262
- className: "fw-card-title",
3263
- children: [/* @__PURE__ */ jsx("div", {
3264
- className: "fw-card-name",
3265
- children: keepWords(span.name)
3266
- }), span.detail && /* @__PURE__ */ jsx("div", {
3267
- className: "fw-muted",
3268
- children: span.detail
3269
- })]
3338
+ /* @__PURE__ */ jsx("div", {
3339
+ className: "fw-card-name",
3340
+ children: keepWords(span.name)
3270
3341
  }),
3271
3342
  /* @__PURE__ */ jsx("span", {
3272
3343
  className: `fw-pill fw-pill-${status.toLowerCase()}`,
3273
3344
  children: status
3274
3345
  }),
3275
- pinned && /* @__PURE__ */ jsx("button", {
3276
- type: "button",
3277
- className: "fw-icon-button",
3278
- "aria-label": "Close details",
3279
- onClick: onClose,
3280
- autoFocus: true,
3281
- children: /* @__PURE__ */ jsx(CloseGlyph, {})
3282
- })
3283
- ]
3284
- }),
3285
- /* @__PURE__ */ jsxs("dl", {
3286
- className: "fw-card-stats",
3287
- children: [
3288
- /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", { children: "Duration" }), /* @__PURE__ */ jsx("dd", { children: duration })] }),
3289
- /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", { children: "Started" }), /* @__PURE__ */ jsxs("dd", { children: ["+", formatSpanDuration(Math.max(0, span.startMs))] })] }),
3290
- span.costUsd !== void 0 && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", { children: "Cost" }), /* @__PURE__ */ jsx("dd", { children: rowCostText(row, span.costUsd) })] }),
3291
- tokens && span.promptTokens !== void 0 && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", { children: "Tokens in" }), /* @__PURE__ */ jsx("dd", { children: count(span.promptTokens) })] }),
3292
- tokens && span.completionTokens !== void 0 && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", { children: "Tokens out" }), /* @__PURE__ */ jsx("dd", { children: count(span.completionTokens) })] }),
3293
- (span.facts ?? []).slice(0, 3).map((fact) => /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", { children: fact.label }), /* @__PURE__ */ jsx("dd", { children: fact.value })] }, fact.label)),
3294
- row.hasChildren && /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", { children: "Steps" }), /* @__PURE__ */ jsx("dd", { children: row.descendants })] })
3346
+ close
3295
3347
  ]
3296
3348
  }),
3349
+ /* @__PURE__ */ jsx(Table, { rows: metrics(row, clockMs, true) }),
3297
3350
  span.input && /* @__PURE__ */ jsxs("div", {
3298
3351
  className: "fw-card-text",
3299
3352
  children: [/* @__PURE__ */ jsx("div", {
@@ -3319,26 +3372,26 @@ const at = (startMs, endMs) => ({
3319
3372
  "--s": startMs,
3320
3373
  "--e": Number.isFinite(endMs) ? endMs : OPEN_END
3321
3374
  });
3322
- const endLabelWidth = (label, slowest, costliest) => label.length * 7 + 8 + (slowest ? 74 : 0) + (costliest ? 84 : 0);
3375
+ const endLabelWidth = (label, slowest, costliest) => label.length * 7 + 8 + (slowest ? 80 : 0) + (costliest ? 90 : 0);
3323
3376
  function Segment({ mark, events, agentView, ending }) {
3324
3377
  const glyph = mark.kind === "failed" ? agentView && !ending ? null : /* @__PURE__ */ jsx(FailGlyph, {}) : mark.retry ? /* @__PURE__ */ jsx(RetryGlyph, {}) : null;
3325
- const named = agentView && mark.kind === "tool" && mark.count === 1 && mark.name;
3378
+ const named = agentView && mark.kind === "tool" && (mark.count === 1 ? mark.name : mark.tool && `${mark.tool} ×${mark.count}`);
3326
3379
  return /* @__PURE__ */ jsx("span", {
3327
3380
  className: `fw-seg fw-seg-${mark.kind}${mark.alt ? " fw-seg-alt" : ""}`,
3328
3381
  "data-tone": toneKey(mark.tone),
3329
3382
  style: at(mark.startMs, mark.endMs),
3330
3383
  title: mark.name ? `${mark.name}${mark.count > 1 ? ` +${mark.count - 1} more` : ""}` : void 0,
3331
- onPointerEnter: agentView ? (event) => events.onInspect(mark.id, event.currentTarget) : void 0,
3384
+ onPointerEnter: agentView ? (event) => events.onInspect(mark.id, event.currentTarget, event.clientX) : void 0,
3332
3385
  onClick: agentView ? (event) => {
3333
3386
  event.stopPropagation();
3334
- events.onPin(mark.id, event.currentTarget);
3387
+ events.onPin(mark.id, event.currentTarget, event.clientX);
3335
3388
  } : void 0,
3336
3389
  children: glyph ? /* @__PURE__ */ jsx("span", {
3337
3390
  className: "fw-glyph",
3338
3391
  children: glyph
3339
3392
  }) : named && /* @__PURE__ */ jsx("span", {
3340
3393
  className: "fw-seg-name",
3341
- children: mark.name
3394
+ children: named
3342
3395
  })
3343
3396
  });
3344
3397
  }
@@ -3365,8 +3418,8 @@ const FlowRow = memo(function FlowRow(props) {
3365
3418
  const kind = !row.ok ? "failed" : row.kind;
3366
3419
  const marks = isAgent || row.hasChildren || row.id.startsWith("run:") ? folded && !agentView ? row.segments : row.own : [];
3367
3420
  const label = endLabel(row, clockMs);
3368
- const summary = agentSummaryParts(row);
3369
- const inspect = (event) => props.onInspect(row.id, event.currentTarget);
3421
+ const summary = [...span.detail && span.detail !== props.sharedDetail ? [span.detail] : [], ...agentSummary(row)];
3422
+ const inspect = (event) => props.onInspect(row.id, event.currentTarget, event.clientX);
3370
3423
  const family = agentView && row.id.startsWith("family:");
3371
3424
  const step = agentView && row.stepOf !== void 0;
3372
3425
  const opens = family || agentView && isAgent && row.own.length > 0;
@@ -3388,7 +3441,8 @@ const FlowRow = memo(function FlowRow(props) {
3388
3441
  className: `fw-row${active ? " fw-active" : ""}${sticky ? " fw-sticky-row" : ""}${isAgent ? " fw-agent-row" : ""}${step ? " fw-step" : ""}${motion?.enter !== void 0 ? " fw-enter" : ""}${motion?.settle ? " fw-settle" : ""}`,
3389
3442
  style: {
3390
3443
  "--row-y": `${top}px`,
3391
- "--fw-enter-delay": `${motion?.enter ?? 0}ms`
3444
+ "--fw-enter-delay": `${motion?.enter ?? 0}ms`,
3445
+ "--fw-end-w": `${endLabelWidth(label, slowest, costliest)}px`
3392
3446
  },
3393
3447
  onFocus: (event) => {
3394
3448
  if (event.target === event.currentTarget) props.onActivate(row);
@@ -3400,7 +3454,7 @@ const FlowRow = memo(function FlowRow(props) {
3400
3454
  return;
3401
3455
  }
3402
3456
  const bar = agentView ? event.currentTarget.querySelector(".fw-bar") : null;
3403
- if (bar) props.onPin(row.id, bar);
3457
+ if (bar) props.onPin(row.id, bar, event.target instanceof Element && event.target.closest(".fw-lane") ? event.clientX : void 0);
3404
3458
  },
3405
3459
  children: [/* @__PURE__ */ jsxs("div", {
3406
3460
  role: "gridcell",
@@ -3479,21 +3533,20 @@ const FlowRow = memo(function FlowRow(props) {
3479
3533
  ]
3480
3534
  }), agentView && /* @__PURE__ */ jsxs("span", {
3481
3535
  className: "fw-subtitle",
3482
- children: [
3483
- renderIcon && /* @__PURE__ */ jsx("span", {
3484
- className: "fw-detail-icon",
3485
- "aria-hidden": true,
3486
- children: renderIcon(span)
3487
- }),
3488
- /* @__PURE__ */ jsx("span", {
3536
+ children: [renderIcon && /* @__PURE__ */ jsx("span", {
3537
+ className: "fw-detail-icon",
3538
+ "aria-hidden": true,
3539
+ children: renderIcon(span)
3540
+ }), /* @__PURE__ */ jsxs("span", {
3541
+ className: "fw-subtitle-tail",
3542
+ children: [/* @__PURE__ */ jsx("span", {
3489
3543
  className: "fw-subtitle-text",
3490
- children: span.detail && span.detail !== props.sharedDetail ? `${span.detail} · ${summary[0]}` : summary[0]
3491
- }),
3492
- summary[1] && /* @__PURE__ */ jsxs("span", {
3544
+ children: summary[0]
3545
+ }), summary.slice(1).map((part) => /* @__PURE__ */ jsxs("span", {
3493
3546
  className: "fw-subtitle-extra",
3494
- children: ["· ", summary[1]]
3495
- })
3496
- ]
3547
+ children: ["· ", part]
3548
+ }, part))]
3549
+ })]
3497
3550
  })]
3498
3551
  }),
3499
3552
  folded && /* @__PURE__ */ jsxs("span", {
@@ -3532,7 +3585,7 @@ const FlowRow = memo(function FlowRow(props) {
3532
3585
  onClick: (event) => {
3533
3586
  event.stopPropagation();
3534
3587
  if (family) props.onToggle(row);
3535
- else props.onPin(row.id, event.currentTarget);
3588
+ else props.onPin(row.id, event.currentTarget, event.clientX);
3536
3589
  },
3537
3590
  children: [!isAgent && !row.ok && /* @__PURE__ */ jsx("span", {
3538
3591
  className: "fw-glyph",
@@ -3550,10 +3603,7 @@ const FlowRow = memo(function FlowRow(props) {
3550
3603
  }, `${mark.id}-${i}`)),
3551
3604
  /* @__PURE__ */ jsxs("span", {
3552
3605
  className: "fw-end",
3553
- style: {
3554
- ...at(span.startMs, running ? OPEN_END : span.endMs),
3555
- "--fw-end-w": `${endLabelWidth(label, slowest, costliest)}px`
3556
- },
3606
+ style: at(span.startMs, running ? OPEN_END : span.endMs),
3557
3607
  children: [
3558
3608
  /* @__PURE__ */ jsx("span", {
3559
3609
  className: "fw-duration",
@@ -3817,22 +3867,6 @@ function FlowBody(props) {
3817
3867
  trackPx,
3818
3868
  view
3819
3869
  ]);
3820
- const pauseLabels = useMemo(() => {
3821
- const shown = /* @__PURE__ */ new Set();
3822
- let freeFrom = -Infinity;
3823
- for (const pause of [...props.pauses ?? []].sort((a, b) => a.startMs - b.startMs)) {
3824
- const width = duration(pause.endMs - pause.startMs).length * 7 + 24;
3825
- const centre = ((pause.startMs + pause.endMs) / 2 - win.startMs) / msPerPx;
3826
- if (centre - width / 2 < freeFrom) continue;
3827
- shown.add(pause);
3828
- freeFrom = centre + width / 2 + 8;
3829
- }
3830
- return shown;
3831
- }, [
3832
- props.pauses,
3833
- win.startMs,
3834
- msPerPx
3835
- ]);
3836
3870
  useEffect(() => {
3837
3871
  const element = scroller.current;
3838
3872
  if (!element) return;
@@ -4014,18 +4048,21 @@ function FlowBody(props) {
4014
4048
  props.onScrub(marker.at);
4015
4049
  }
4016
4050
  }, `e-${marker.at}-${marker.label}`)),
4017
- props.pauses?.filter((pause) => pauseLabels.has(pause)).map((pause) => /* @__PURE__ */ jsx("span", {
4018
- className: "fw-pause-label",
4019
- style: {
4020
- "--s": pause.startMs,
4021
- "--e": pause.endMs
4022
- },
4023
- title: pause.detail ? `${pause.label}: ${pause.detail}` : pause.label,
4024
- tabIndex: 0,
4025
- "aria-label": `${pause.label}, ${duration(pause.endMs - pause.startMs)}${pause.detail ? `: ${pause.detail}` : ""}`,
4026
- onPointerDown: (event) => event.stopPropagation(),
4027
- children: /* @__PURE__ */ jsx("b", { children: duration(pause.endMs - pause.startMs) })
4028
- }, `pl-${pause.startMs}`)),
4051
+ props.pauses?.map((pause) => {
4052
+ const said = `${pause.label} ${duration(pause.endMs - pause.startMs)}${pause.detail ? `: ${pause.detail}` : ""}`;
4053
+ return /* @__PURE__ */ jsx("span", {
4054
+ className: "fw-pause-mark",
4055
+ style: {
4056
+ "--s": pause.startMs,
4057
+ "--e": pause.endMs
4058
+ },
4059
+ title: said,
4060
+ "aria-label": said,
4061
+ role: "img",
4062
+ tabIndex: 0,
4063
+ onPointerDown: (event) => event.stopPropagation()
4064
+ }, `pm-${pause.startMs}`);
4065
+ }),
4029
4066
  jumped && /* @__PURE__ */ jsx("span", {
4030
4067
  className: "fw-event-label",
4031
4068
  "data-tone": jumped.tone ?? "neutral",
@@ -4074,14 +4111,24 @@ function FlowBody(props) {
4074
4111
  /* @__PURE__ */ jsxs("div", {
4075
4112
  className: "fw-gridlayer",
4076
4113
  "aria-hidden": true,
4077
- children: [ticks.map((tick) => /* @__PURE__ */ jsx("span", {
4078
- className: "fw-gridline",
4079
- style: { "--s": tick }
4080
- }, tick)), props.markers?.map((marker) => /* @__PURE__ */ jsx("span", {
4081
- className: "fw-marker-line",
4082
- "data-tone": marker.tone ?? "neutral",
4083
- style: { "--s": marker.at }
4084
- }, `l-${marker.at}-${marker.label}`))]
4114
+ children: [
4115
+ props.pauses?.map((pause) => /* @__PURE__ */ jsx("span", {
4116
+ className: "fw-pause",
4117
+ style: {
4118
+ "--s": pause.startMs,
4119
+ "--e": pause.endMs
4120
+ }
4121
+ }, `p-${pause.startMs}`)),
4122
+ ticks.map((tick) => /* @__PURE__ */ jsx("span", {
4123
+ className: "fw-gridline",
4124
+ style: { "--s": tick }
4125
+ }, tick)),
4126
+ props.markers?.map((marker) => /* @__PURE__ */ jsx("span", {
4127
+ className: "fw-marker-line",
4128
+ "data-tone": marker.tone ?? "neutral",
4129
+ style: { "--s": marker.at }
4130
+ }, `l-${marker.at}-${marker.label}`))
4131
+ ]
4085
4132
  }),
4086
4133
  /* @__PURE__ */ jsxs("div", {
4087
4134
  ref: scroller,
@@ -4126,16 +4173,9 @@ function FlowBody(props) {
4126
4173
  rowHeight,
4127
4174
  render: row
4128
4175
  }),
4129
- props.markers?.length || props.pauses?.length ? /* @__PURE__ */ jsxs("div", {
4176
+ props.markers?.length ? /* @__PURE__ */ jsx("div", {
4130
4177
  className: "fw-marker-layer",
4131
- children: [props.pauses?.map((pause) => /* @__PURE__ */ jsx("span", {
4132
- className: "fw-pause",
4133
- style: {
4134
- "--s": pause.startMs,
4135
- "--e": pause.endMs
4136
- },
4137
- title: `${pause.label}, ${duration(pause.endMs - pause.startMs)}`
4138
- }, `p-${pause.startMs}`)), props.markers?.map((marker) => /* @__PURE__ */ jsx("span", {
4178
+ children: props.markers.map((marker) => /* @__PURE__ */ jsx("span", {
4139
4179
  className: "fw-marker",
4140
4180
  "data-tone": marker.tone ?? "neutral",
4141
4181
  style: { "--s": marker.at },
@@ -4144,7 +4184,7 @@ function FlowBody(props) {
4144
4184
  className: `fw-marker-label${markerLabels.get(marker) === "left" ? " fw-marker-label-left" : ""}`,
4145
4185
  children: marker.label
4146
4186
  })
4147
- }, `${marker.at}-${marker.label}`))]
4187
+ }, `${marker.at}-${marker.label}`))
4148
4188
  }) : null,
4149
4189
  showCursor && props.cursorLine !== false && /* @__PURE__ */ jsx("div", {
4150
4190
  className: "fw-cursor-layer",
@@ -4440,6 +4480,7 @@ function useHostable(value, onChange, initial) {
4440
4480
  report.current?.(resolved);
4441
4481
  }, [controlled])];
4442
4482
  }
4483
+ const CARD_WIDTH = 320;
4443
4484
  /** The summary strip: as of the playhead while scrubbing, the producer's totals otherwise. */
4444
4485
  function summaryCells(trace, at, durationMs) {
4445
4486
  const cost = costSummary(at ? at.costUsd : trace.costUsd, trace);
@@ -4617,7 +4658,8 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
4617
4658
  const pipelines = rows.filter((row) => row.kind === "pipeline");
4618
4659
  return pipelines.length ? pipelines : rows.filter((row) => !row.parentId);
4619
4660
  }, [rows]);
4620
- const sharedDetail = agents.length > 1 && agents.every((row) => row.span.detail && row.span.detail === agents[0]?.span.detail) ? agents[0]?.span.detail : void 0;
4661
+ const details = new Set(agents.flatMap((row) => row.span.detail ? [row.span.detail] : []));
4662
+ const sharedDetail = agents.length > 1 && details.size === 1 ? [...details][0] : void 0;
4621
4663
  const [flippedFamilies, setFlippedFamilies] = useState(/* @__PURE__ */ new Set());
4622
4664
  const openFamilies = useMemo(() => defaultOpenFamilies(agents, UNFOLD_ROWS, flippedFamilies), [agents, flippedFamilies]);
4623
4665
  const [openSteps, setOpenSteps] = useState(/* @__PURE__ */ new Set());
@@ -4732,8 +4774,8 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
4732
4774
  }, limit));
4733
4775
  const latest = useRef({
4734
4776
  toggle: (_row) => {},
4735
- pin: (_target, _element) => {},
4736
- inspect: (_target, _element) => {}
4777
+ pin: (_target, _element, _x) => {},
4778
+ inspect: (_target, _element, _x) => {}
4737
4779
  });
4738
4780
  const toggleNow = (row) => {
4739
4781
  const flip = (open) => new Set(open.has(row.id) ? [...open].filter((id) => id !== row.id) : [...open, row.id]);
@@ -4757,25 +4799,43 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
4757
4799
  };
4758
4800
  latest.current.toggle = toggleNow;
4759
4801
  const toggle = useCallback((row) => latest.current.toggle(row), []);
4760
- const place = (target, element, pinned) => {
4802
+ const place = (target, element, pinned, x) => {
4761
4803
  const host = root.current?.getBoundingClientRect();
4762
4804
  if (!host) return;
4763
4805
  const rect = element.getBoundingClientRect();
4764
- const above = view === "agents" ? rect.top - host.top > 150 : globalThis.innerHeight - rect.bottom < 260 && rect.top - host.top > 260;
4765
- const left = Math.max(8, Math.min(rect.left - host.left, host.width - 328));
4806
+ const width = Math.min(CARD_WIDTH, host.width - 16);
4807
+ const left = Math.max(8, Math.min((x ?? rect.left + 24) - host.left - 24, host.width - width - 8));
4766
4808
  clearTimeout(hideTimer.current);
4767
4809
  setCard({
4768
4810
  target,
4769
4811
  pinned,
4770
4812
  left,
4771
- top: above ? rect.top - host.top - 8 : rect.bottom - host.top + 8,
4772
- above
4813
+ anchorTop: rect.top - host.top,
4814
+ anchorBottom: rect.bottom - host.top,
4815
+ above: view === "agents"
4773
4816
  });
4774
4817
  };
4775
- latest.current.inspect = (target, element) => {
4776
- if (!card?.pinned) place(target, element, false);
4818
+ useLayoutEffect(() => {
4819
+ if (!card || card.fitted || !root.current) return;
4820
+ const box = document.getElementById(cardId)?.getBoundingClientRect();
4821
+ if (!box) return;
4822
+ const hostTop = root.current.getBoundingClientRect().top;
4823
+ const room = {
4824
+ above: hostTop + card.anchorTop - 16,
4825
+ below: globalThis.innerHeight - (hostTop + card.anchorBottom) - 16
4826
+ };
4827
+ const preferred = card.above ? room.above : room.below;
4828
+ const other = card.above ? room.below : room.above;
4829
+ setCard({
4830
+ ...card,
4831
+ fitted: true,
4832
+ above: box.height <= preferred || box.height > other && preferred >= other ? card.above : !card.above
4833
+ });
4834
+ }, [card, cardId]);
4835
+ latest.current.inspect = (target, element, x) => {
4836
+ if (!card?.pinned) place(target, element, false, x);
4777
4837
  };
4778
- const inspect = useCallback((target, element) => latest.current.inspect(target, element), []);
4838
+ const inspect = useCallback((target, element, x) => latest.current.inspect(target, element, x), []);
4779
4839
  const leave = useCallback(() => {
4780
4840
  clearTimeout(hideTimer.current);
4781
4841
  hideTimer.current = setTimeout(() => setCard((previous) => previous?.pinned ? previous : null), 140);
@@ -4788,19 +4848,19 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
4788
4848
  document.addEventListener("pointermove", move);
4789
4849
  return () => document.removeEventListener("pointermove", move);
4790
4850
  }, [card, leave]);
4791
- latest.current.pin = (target, element) => {
4851
+ latest.current.pin = (target, element, x) => {
4792
4852
  if (card?.pinned && card.target === target) {
4793
4853
  closeCard();
4794
4854
  return;
4795
4855
  }
4796
- place(target, element, true);
4856
+ place(target, element, true, x);
4797
4857
  const row = byId.get(target);
4798
4858
  if (row) {
4799
4859
  onSelect?.(row.span);
4800
4860
  zoomTo(row);
4801
4861
  }
4802
4862
  };
4803
- const pin = useCallback((target, element) => latest.current.pin(target, element), []);
4863
+ const pin = useCallback((target, element, x) => latest.current.pin(target, element, x), []);
4804
4864
  const selection = selectedId ?? (card?.pinned ? card.target : void 0);
4805
4865
  const closeCard = () => {
4806
4866
  const target = card?.target;
@@ -5053,7 +5113,7 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
5053
5113
  agent: cardAgent,
5054
5114
  clockMs,
5055
5115
  left: card.left,
5056
- top: card.top,
5116
+ top: card.above ? card.anchorTop - 8 : card.anchorBottom + 8,
5057
5117
  pinned: card.pinned,
5058
5118
  onClose: closeCard,
5059
5119
  renderIcon,