@tangle-network/charts 0.20.1 → 0.20.2

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.
@@ -382,8 +382,9 @@ interface FlowTrace {
382
382
  costUsd?: number;
383
383
  /** `list`: priced from a model catalog's list prices, not a bill; the summary says so. */
384
384
  costBasis?: 'billed' | 'list';
385
- /** The list cost is at most this: the input counts include cached tokens priced as fresh input. */
386
- costUpperBound?: boolean;
385
+ /** `costUsd` is a bound, not a count: `lower` prints "≥" (a floor, such as the larger of usage and meter per agent),
386
+ * `upper` prints "≤" (list price on inputs that include cached tokens priced as fresh). */
387
+ costBound?: 'lower' | 'upper';
387
388
  /** How many model calls `costUsd` covers; when `priced < total` the total is a lower bound and shown as one. */
388
389
  costCoverage?: {
389
390
  priced: number;
@@ -391,7 +392,8 @@ interface FlowTrace {
391
392
  };
392
393
  /** The real charge when the producer knows it (for example $0 on a subscription seat), shown beside a list price. */
393
394
  billedUsd?: number;
394
- toolCalls: number;
395
+ /** Tool calls the producer counted; absent means unknown and is shown as a dash, never as zero. */
396
+ toolCalls?: number;
395
397
  /** Authoritative LLM request count when supplied; absent means unknown. */
396
398
  modelCalls?: number;
397
399
  errors?: number;
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { A as BreakdownInput, B as Refusal, C as MarkKind, D as YScale, E as XCategory, F as Figure, H as Tier, I as Interval, L as IntervalMethod, M as CostBasis, N as Estimate, O as YTarget, P as Exclusion, R as MatrixCell, S as ChartMark, T as MarkerShape, U as TierCounts, V as Setup, W as isRefusal, _ as checkerColor, a as TimelineLane, b as BandKind, c as TimelineTick, d as ENDING_COLORS, f as ENDING_ORDER, g as TickOutcome, h as RunEnding, i as FlowTrace, j as ComparisonRow, k as YTick, l as timeline, m as OUTCOME_ORDER, n as FlowOutcome, o as TimelineOptions, p as OUTCOME_COLORS, r as FlowSpan, s as TimelineSpan, t as FlowMarker, u as CHECKER_HUES, v as PointFields, w as MarkVerdict, x as ChartBand, y as PointGlyph, z as RateRow } from "./flow-trace-DqCMrnTs.js";
1
+ import { A as BreakdownInput, B as Refusal, C as MarkKind, D as YScale, E as XCategory, F as Figure, H as Tier, I as Interval, L as IntervalMethod, M as CostBasis, N as Estimate, O as YTarget, P as Exclusion, R as MatrixCell, S as ChartMark, T as MarkerShape, U as TierCounts, V as Setup, W as isRefusal, _ as checkerColor, a as TimelineLane, b as BandKind, c as TimelineTick, d as ENDING_COLORS, f as ENDING_ORDER, g as TickOutcome, h as RunEnding, i as FlowTrace, j as ComparisonRow, k as YTick, l as timeline, m as OUTCOME_ORDER, n as FlowOutcome, o as TimelineOptions, p as OUTCOME_COLORS, r as FlowSpan, s as TimelineSpan, t as FlowMarker, u as CHECKER_HUES, v as PointFields, w as MarkVerdict, x as ChartBand, y as PointGlyph, z as RateRow } from "./flow-trace-DfSefpBM.js";
2
2
  //#region src/block.d.ts
3
3
  interface RenderFigureOptions {
4
4
  /** Chart uses a closed data disclosure; plot keeps only the SVG and accessible data. */
package/dist/index.js CHANGED
@@ -3584,7 +3584,7 @@ function otlpFlowTrace(input, options = {}) {
3584
3584
  priced,
3585
3585
  total: priced + unpriced
3586
3586
  },
3587
- ...cacheSplit ? {} : { costUpperBound: true }
3587
+ ...cacheSplit ? {} : { costBound: "upper" }
3588
3588
  } : {}
3589
3589
  };
3590
3590
  }
@@ -65,9 +65,9 @@
65
65
  /* Light fills carry enough of the hue to tell agents apart on white (24% and 42% both read as "pastel, washed out"); the outline is
66
66
  the full hue, which passes 3:1 against the surface, so every mark keeps its edge. */
67
67
  .fw-root [data-tone] {
68
- --fill: light-dark(color-mix(in oklab, var(--tone) 58%, #fff), color-mix(in oklab, var(--tone) 86%, #000));
68
+ --fill: light-dark(var(--tone), color-mix(in oklab, var(--tone) 94%, #000));
69
69
  --line: light-dark(var(--tone), var(--tone));
70
- --dim: light-dark(color-mix(in oklab, var(--tone) 30%, #fff), color-mix(in oklab, var(--tone) 40%, #17171d));
70
+ --dim: light-dark(color-mix(in oklab, var(--tone) 62%, #fff), color-mix(in oklab, var(--tone) 40%, #17171d));
71
71
  --dim-line: light-dark(color-mix(in oklab, var(--tone) 70%, #fff), color-mix(in oklab, var(--tone) 55%, #17171d));
72
72
  }
73
73
 
@@ -138,6 +138,8 @@
138
138
  /* The event track: one dot per marker on the axis; click or N/P moves the cursor there. */
139
139
  .fw-root .fw-event { --fw-marker: var(--fw-muted); position: absolute; bottom: 2px; width: 8px; height: 8px; margin-left: -4px; padding: 0; border: 0; border-radius: 50%; background: var(--fw-marker); box-shadow: 0 0 0 2px var(--fw-surface); cursor: pointer; z-index: 4; left: calc(var(--fw-pad) + (var(--s) - var(--fw-v0)) * var(--fw-k) * 1px); }
140
140
  .fw-root .fw-event[data-tone="ok"] { --fw-marker: var(--fw-ok); }
141
+ .fw-root .fw-event[aria-pressed="true"] { box-shadow: 0 0 0 2px var(--fw-surface), 0 0 0 4px var(--fw-marker); }
142
+ .fw-root .fw-event-label { position: absolute; bottom: 14px; z-index: 5; transform: translateX(-50%); left: calc(var(--fw-pad) + (var(--s) - var(--fw-v0)) * var(--fw-k) * 1px); padding: 0 8px; border-radius: 4px; background: var(--fw-surface); box-shadow: inset 0 0 0 1px var(--fw-border); color: var(--fw-text); font-size: 12px; line-height: 20px; white-space: nowrap; pointer-events: none; }
141
143
  .fw-root .fw-event[data-tone="failed"] { --fw-marker: var(--fw-failed); }
142
144
  .fw-root .fw-event:hover, .fw-root .fw-event:focus-visible { transform: scale(1.4); outline: 0; }
143
145
  .fw-root .fw-cursor { position: absolute; top: 0; bottom: 0; width: 1px; background: color-mix(in oklab, var(--fw-text) 70%, transparent); left: calc(var(--fw-pad) + (var(--fw-t) - var(--fw-v0)) * var(--fw-k) * 1px); }
@@ -231,7 +233,7 @@
231
233
  .fw-root [data-outcome="neutral"], .fw-root [data-outcome="neutral"] [data-tone] { --tone: var(--fw-muted); }
232
234
  .fw-root .fw-legend { display: flex; flex-wrap: wrap; gap: 4px 16px; margin: 8px 0 0; padding: 0; list-style: none; color: var(--fw-muted); font-size: 12px; line-height: 16px; }
233
235
  .fw-root .fw-legend li { display: inline-flex; align-items: center; gap: 8px; }
234
- .fw-root .fw-kinds { margin: 0; padding: 8px 24px 12px; justify-content: flex-end; }
236
+ .fw-root .fw-kinds { margin: 0; padding: 4px 24px 8px; justify-content: flex-start; color: var(--fw-text); }
235
237
  .fw-root .fw-kinds .fw-swatch-model { width: 12px; height: 8px; border-radius: 2px; background: color-mix(in oklab, var(--fw-text) 40%, var(--fw-surface)); }
236
238
  .fw-root .fw-kinds .fw-swatch-tool { width: 12px; height: 8px; border-radius: 2px; background: color-mix(in oklab, var(--fw-text) 16%, var(--fw-surface)); box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--fw-text) 30%, transparent); }
237
239
  .fw-root .fw-kinds .fw-swatch-wait { width: 12px; height: 8px; border-radius: 2px; background: repeating-linear-gradient(-45deg, var(--fw-hatch) 0 1px, transparent 1px 4px); box-shadow: inset 0 0 0 1px var(--fw-hatch); }
@@ -248,11 +250,13 @@
248
250
  .fw-root .fw-seg-model { background: var(--fill); box-shadow: inset 0 0 0 1px var(--line); }
249
251
  .fw-root .fw-seg-tool { background: var(--dim); box-shadow: inset 0 0 0 1px var(--dim-line); }
250
252
  .fw-root .fw-seg-failed { background: color-mix(in oklab, var(--fw-failed) 18%, var(--fw-surface)); box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--fw-failed) 70%, transparent); color: var(--fw-failed); }
251
- .fw-root .fw-view-agents .fw-seg-failed { background: var(--dim); box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--fw-failed) 60%, var(--dim-line)); }
253
+ /* In the agent view a failed turn draws like any turn; the agent's own failure shows once, by its name and its end label. */
254
+ .fw-root .fw-view-agents .fw-seg-failed { background: var(--dim); box-shadow: inset 0 0 0 1px var(--dim-line); }
255
+ .fw-root .fw-view-agents .fw-alert { color: var(--fw-muted); }
252
256
  .fw-root .fw-glyph { display: inline-flex; color: currentColor; }
253
257
  .fw-root .fw-glyph svg { width: 11px; height: 11px; stroke-width: 2; }
254
258
  .fw-root .fw-seg-model .fw-glyph, .fw-root .fw-k-model .fw-glyph { color: light-dark(var(--tone), #fff); }
255
- .fw-root .fw-end { display: inline-flex; align-items: center; gap: 8px; top: calc(50% + var(--fw-bar-shift) - var(--fw-bar-h) / 2); height: var(--fw-bar-h); left: calc(var(--x0) + max(var(--w), 3px) + 8px); white-space: nowrap; pointer-events: none; }
259
+ .fw-root .fw-end { z-index: 2; display: inline-flex; align-items: center; gap: 8px; top: calc(50% + var(--fw-bar-shift) - var(--fw-bar-h) / 2); height: var(--fw-bar-h); left: calc(var(--x0) + max(var(--w), 3px) + 8px); white-space: nowrap; pointer-events: none; }
256
260
  .fw-root .fw-duration { font-size: 12px; color: var(--fw-muted); font-variant-numeric: tabular-nums; }
257
261
  .fw-root .fw-k-failed ~ .fw-end .fw-duration { color: var(--fw-failed); }
258
262
  .fw-root .fw-badge { height: 18px; padding: 0 8px; border-radius: 999px; font-size: 12px; line-height: 16px; font-weight: 600; color: var(--fw-amber); background: color-mix(in oklab, var(--fw-amber) 13%, transparent); border: 1px solid color-mix(in oklab, var(--fw-amber) 28%, transparent); }
@@ -265,17 +269,25 @@
265
269
  opacity: clamp(0, (var(--fw-t) - var(--s)) * 1000, 1);
266
270
  }
267
271
  .fw-root .fw-view-spans .fw-link { opacity: calc(clamp(0, (var(--fw-t) - var(--s)) * 1000, 1) * .7); }
268
- /* Agent-view connectors stay quiet at 30% until their agent is hovered, pinned or focused. */
269
- .fw-root .fw-view-agents .fw-link:not(.fw-link-hot) { opacity: calc(clamp(0, (var(--fw-t) - var(--s)) * 1000, 1) * .3); }
272
+ /* Agent-view connectors rest faint, one curved elbow per spawn in the agent's hue (they show who started whom), and come to
273
+ full strength for the agent under the pointer, pinned or focused. */
274
+ .fw-root .fw-view-agents .fw-link:not(.fw-link-hot) { opacity: calc(clamp(0, (var(--fw-t) - var(--s)) * 1000, 1) * .6); }
270
275
  .fw-root .fw-link { transition: opacity 160ms var(--fw-ease); }
271
- .fw-root .fw-link-down { left: calc(var(--x) - 8px); border-left-width: 1.5px; border-bottom-left-radius: 6px; }
272
- .fw-root .fw-link-up { left: var(--x); border-right-width: 1.5px; border-bottom-right-radius: 6px; }
276
+ .fw-root .fw-link-down { left: calc(var(--x) - 8px); border-left-width: 1.5px; border-bottom-left-radius: 12px; }
277
+ .fw-root .fw-link-up { left: var(--x); border-right-width: 1.5px; border-bottom-right-radius: 12px; }
273
278
  .fw-root .fw-link::before { content: ""; position: absolute; top: -3.5px; width: 5px; height: 5px; border-radius: 50%; background: var(--line); box-shadow: 0 0 0 1.5px var(--fw-surface); }
274
279
  .fw-root .fw-link-down::before { left: -3.25px; }
275
280
  .fw-root .fw-link-up::before { right: -3.25px; }
276
281
 
277
282
  /* Span card */
278
283
  .fw-root .fw-card-anchor { display: contents; }
284
+ /* Agent view: the card docks under the lanes instead of floating over the plot (judges: "covers the data it describes"). */
285
+ /* Two classes on the root so these win over the base .fw-root .fw-card rule that follows. */
286
+ .fw-root.fw-root-agents .fw-card-anchor { display: block; padding: 0 24px 12px; }
287
+ .fw-root.fw-root-agents .fw-card, .fw-root.fw-root-agents .fw-card-above .fw-card { position: relative; left: auto !important; top: auto !important; transform: none; animation-name: fw-arrive; width: auto; max-width: 560px; }
288
+ .fw-root .fw-seg-alt { filter: brightness(.78); }
289
+ /* Agent view: the scrubber track shows how far the run has played, so at rest it reads full, not empty. */
290
+ .fw-root-agents .fw-slider { background: linear-gradient(90deg, color-mix(in oklab, var(--fw-accent) 45%, transparent) var(--fw-progress), var(--fw-well) var(--fw-progress)) 0 50% / 100% 4px no-repeat; }
279
291
  .fw-root .fw-card { position: absolute; z-index: 20; width: 320px; padding: 12px; border-radius: 12px; background: var(--fw-raised); border: 1px solid var(--fw-border);
280
292
  box-shadow: 0 12px 32px -8px rgb(0 0 0 / .28), 0 2px 6px rgb(0 0 0 / .08); animation: fw-arrive 140ms var(--fw-ease); }
281
293
  /* A hover card never covers the next mark: only a pinned card (a dialog) takes the pointer. */
@@ -1,4 +1,4 @@
1
- import { B as Refusal, C as MarkKind, D as YScale, E as XCategory, F as Figure, O as YTarget, S as ChartMark, T as MarkerShape, a as TimelineLane, b as BandKind, c as TimelineTick, g as TickOutcome, i as FlowTrace, k as YTick, o as TimelineOptions, r as FlowSpan, s as TimelineSpan, t as FlowMarker, v as PointFields, w as MarkVerdict, x as ChartBand, y as PointGlyph } from "../flow-trace-DqCMrnTs.js";
1
+ import { B as Refusal, C as MarkKind, D as YScale, E as XCategory, F as Figure, O as YTarget, S as ChartMark, T as MarkerShape, a as TimelineLane, b as BandKind, c as TimelineTick, g as TickOutcome, i as FlowTrace, k as YTick, o as TimelineOptions, r as FlowSpan, s as TimelineSpan, t as FlowMarker, v as PointFields, w as MarkVerdict, x as ChartBand, y as PointGlyph } from "../flow-trace-DfSefpBM.js";
2
2
  import { CSSProperties, ReactElement, ReactNode } from "react";
3
3
  //#region src/react/sparkline.d.ts
4
4
  type SparklineDirection = "rising" | "falling" | "flat";
@@ -384,9 +384,11 @@ interface FlowWaterfallProps {
384
384
  selectedId?: string;
385
385
  /** Escape clears the host's selection, after closing any open card. */
386
386
  onDeselect?: () => void;
387
+ /** Show the stat strip (default); a page with its own stat row passes false. */
388
+ summary?: boolean;
387
389
  }
388
390
  /** Shared trace inspector: the agent timeline and the span waterfall over one clock.
389
391
  * Data remains producer-owned; playback, zoom and folding change presentation only. */
390
- export declare function FlowWaterfall({ trace, live, startedAt, label, defaultCollapsed, defaultView, renderIcon, maxHeight, onSelect, markers, onTimeChange, agentFill, selectedId, onDeselect, description }: FlowWaterfallProps): import("react").JSX.Element;
392
+ export declare function FlowWaterfall({ trace, live, startedAt, label, defaultCollapsed, defaultView, renderIcon, maxHeight, onSelect, markers, onTimeChange, agentFill, selectedId, onDeselect, description, summary: showSummary }: FlowWaterfallProps): import("react").JSX.Element;
391
393
  //#endregion
392
394
  export type { BandKind, ChartBand, ChartMark, DotMapLabel, DotMapProps, DotMapRect, Figure, FigureAtWidth, FigureViewProps, FlowWaterfallProps, LineSeries, MarkKind, MarkVerdict, MarkerShape, MeterSegment, PointFields, PointGlyph, Refusal, SparklineDirection, SparklineGeometry, SparklineGeometryOptions, SparklineLabelOptions, SparklinePoint, SparklineProps, SparklineValues, StackedBarBucket, StackedBarChartProps, StackedBarSegment, StackedBarSeries, TickOutcome, TimelineLane, TimelineOptions, TimelineSpan, TimelineTick, TimelineViewProps, WaterfallBarProps, WaterfallHeaderProps, WaterfallRowProps, WaterfallSpanGeometry, WaterfallTimeAxisProps, WaterfallWindow, XCategory, YScale, YTarget, YTick };
@@ -2441,7 +2441,17 @@ function flowWaterfallRows(trace, resolutionMs) {
2441
2441
  if (childCost === void 0) {
2442
2442
  if (child.kind === "model" || child.hasChildren) costKnown = false;
2443
2443
  } else cost += childCost;
2444
- if (child.kind !== "pipeline" && child.kind !== "waiting" && ordered(child.span)) row.own.push(segmentOf(child));
2444
+ if (child.kind !== "pipeline" && child.kind !== "waiting" && ordered(child.span)) {
2445
+ row.own.push(segmentOf(child));
2446
+ const counts = row.counts ??= {
2447
+ model: 0,
2448
+ tool: 0,
2449
+ failed: 0
2450
+ };
2451
+ if (!child.ok) counts.failed++;
2452
+ else if (child.kind === "model") counts.model++;
2453
+ else counts.tool++;
2454
+ }
2445
2455
  }
2446
2456
  if (row.hasChildren && costKnown) row.rollupCostUsd = cost;
2447
2457
  if (row.hasChildren) {
@@ -2560,7 +2570,7 @@ function totalsAt(trace, clockMs) {
2560
2570
  costUsd: trace.costUsd === void 0 ? void 0 : costs ? cost : trace.costUsd,
2561
2571
  tokens: counted ? tokens : trace.promptTokens + trace.completionTokens,
2562
2572
  modelCalls: trace.modelCalls === void 0 ? void 0 : models,
2563
- toolCalls: tools,
2573
+ toolCalls: trace.toolCalls === void 0 ? void 0 : tools,
2564
2574
  errors
2565
2575
  };
2566
2576
  }
@@ -2579,18 +2589,197 @@ function costSummary(costUsd, trace) {
2579
2589
  };
2580
2590
  const coverage = trace.costCoverage;
2581
2591
  const partial = coverage !== void 0 && coverage.priced < coverage.total;
2582
- const upper = !partial && trace.costUpperBound === true;
2592
+ const upper = !partial && trace.costBound === "upper";
2583
2593
  return {
2584
- value: `${partial ? "≥" : upper ? "≤" : list ? "≈" : ""}${formatCost(costUsd)}${trace.billedUsd !== void 0 ? ` · ${formatCost(trace.billedUsd)} billed` : ""}`,
2594
+ value: `${partial || trace.costBound === "lower" ? "≥" : upper ? "≤" : list ? "≈" : ""}${formatCost(costUsd)}${trace.billedUsd !== void 0 ? ` · ${formatCost(trace.billedUsd)} billed` : ""}`,
2585
2595
  title: [
2586
2596
  partial ? `${coverage.priced} of ${coverage.total} model calls priced` : coverage ? `all ${coverage.total} model calls priced` : "",
2587
2597
  list ? "at the model catalog list price; not a bill" : "",
2588
2598
  upper ? "cached input is not split out, so it is priced as fresh input: at most this" : "",
2599
+ trace.costBound === "lower" ? "a floor: the producer reports at least this" : "",
2589
2600
  trace.billedUsd !== void 0 ? `billed ${formatCost(trace.billedUsd)} (the seat charge)` : ""
2590
2601
  ].filter(Boolean).join("; "),
2591
2602
  label
2592
2603
  };
2593
2604
  }
2605
+ /** The role an agent plays: its name without a trailing counter ("memscale-engineer-3", "Subagent 2"). */
2606
+ const roleOf = (name) => name.replace(/[-\s]\d+$/, "");
2607
+ /**
2608
+ * Agents sharing a role fold into one family row until opened: a 17-agent run reads as its few roles, and the bar of a
2609
+ * family carries every member's work in each member's hue. Members follow their opened family, marked with `family`.
2610
+ */
2611
+ function agentFamilies(agents, open) {
2612
+ const groups = /* @__PURE__ */ new Map();
2613
+ for (const row of agents) {
2614
+ const key = row.parentId === void 0 ? `root:${row.id}` : roleOf(row.name);
2615
+ groups.set(key, [...groups.get(key) ?? [], row]);
2616
+ }
2617
+ const out = [];
2618
+ let next = 0;
2619
+ const hue = (row) => row.tone <= 0 ? row.tone : ++next;
2620
+ for (const [role, members] of groups) {
2621
+ if (members.length < 2) {
2622
+ out.push(...members.map((row) => ({
2623
+ ...row,
2624
+ tone: hue(row)
2625
+ })));
2626
+ continue;
2627
+ }
2628
+ const tone = hue(members[0]);
2629
+ const id = `family:${role}`;
2630
+ const startMs = Math.min(...members.map((row) => row.span.startMs));
2631
+ const endMs = Math.max(...members.map((row) => row.span.endMs));
2632
+ const running = members.some((row) => spanRunning(row.span));
2633
+ const known = members.map((row) => row.span.costUsd ?? row.rollupCostUsd).filter((cost) => cost !== void 0);
2634
+ const failed = members.filter((row) => !row.ok).length;
2635
+ out.push({
2636
+ id,
2637
+ parentId: members[0].parentId,
2638
+ name: `${role} ×${members.length}`,
2639
+ kind: "pipeline",
2640
+ durationLabel: formatSpanDuration(endMs - startMs),
2641
+ approx: members.some((row) => row.approx),
2642
+ ok: failed === 0,
2643
+ depth: 0,
2644
+ hasChildren: true,
2645
+ tone,
2646
+ descendants: members.length,
2647
+ segments: [],
2648
+ retry: false,
2649
+ hasAgents: false,
2650
+ own: members.flatMap((row, i) => row.own.map((mark) => ({
2651
+ ...mark,
2652
+ tone,
2653
+ alt: i % 2 === 1
2654
+ }))).sort((a, b) => a.startMs - b.startMs),
2655
+ ...known.length ? { rollupCostUsd: known.reduce((sum, cost) => sum + cost, 0) } : {},
2656
+ span: {
2657
+ id,
2658
+ kind: "pipeline",
2659
+ name: role,
2660
+ startMs,
2661
+ endMs,
2662
+ status: running ? "running" : failed ? "failed" : "done",
2663
+ detail: members[0].span.detail,
2664
+ meta: {
2665
+ failedAgents: failed,
2666
+ costPartial: known.length < members.length
2667
+ }
2668
+ }
2669
+ });
2670
+ if (open.has(id)) out.push(...members.map((row, i) => ({
2671
+ ...row,
2672
+ family: id,
2673
+ inset: 1,
2674
+ tone,
2675
+ own: row.own.map((mark) => ({
2676
+ ...mark,
2677
+ tone,
2678
+ alt: i % 2 === 1
2679
+ }))
2680
+ })));
2681
+ }
2682
+ return out;
2683
+ }
2684
+ /** 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"). */
2685
+ function agentSummary(row) {
2686
+ if (row.id.startsWith("family:")) {
2687
+ const failed = Number(row.span.meta?.failedAgents ?? 0);
2688
+ return `${row.descendants} agents${failed ? ` · ${failed} failed` : ""}`;
2689
+ }
2690
+ if (row.id.startsWith("run:")) {
2691
+ const failed = row.own.filter((mark) => mark.kind === "failed").length;
2692
+ return failed ? `${failed} failed` : "all done";
2693
+ }
2694
+ const counts = row.counts ?? {
2695
+ model: 0,
2696
+ tool: 0,
2697
+ failed: 0
2698
+ };
2699
+ const turns = counts.model + counts.failed;
2700
+ const tools = counts.tool;
2701
+ const state = row.span.outcome?.label ?? (spanRunning(row.span) ? "running" : row.ok ? "done" : "failed");
2702
+ const unit = row.kind === "pipeline" ? ["turn", "turns"] : ["model call", "model calls"];
2703
+ return [
2704
+ state,
2705
+ turns ? `${turns} ${turns === 1 ? unit[0] : unit[1]}` : "",
2706
+ tools ? `${tools} tool ${tools === 1 ? "call" : "calls"}` : ""
2707
+ ].filter(Boolean).join(" · ");
2708
+ }
2709
+ /**
2710
+ * The agent view's rows: families folded until opened, and an opened agent's own steps (its turns and tool calls, not its
2711
+ * child agents) nested beneath it with their icons, durations and costs, as a reader drills from the run into one agent.
2712
+ */
2713
+ function agentViewRows(agents, rows, openFamilies, openSteps) {
2714
+ const base = agentFamilies(agents, openFamilies);
2715
+ if (openSteps.size === 0) return base;
2716
+ const steps = /* @__PURE__ */ new Map();
2717
+ for (const row of rows) {
2718
+ if (row.parentId === void 0 || !openSteps.has(row.parentId) || row.kind === "pipeline") continue;
2719
+ steps.set(row.parentId, [...steps.get(row.parentId) ?? [], row]);
2720
+ }
2721
+ return base.flatMap((row) => openSteps.has(row.id) ? [row, ...stepRuns(steps.get(row.id) ?? []).map((step) => ({
2722
+ ...step,
2723
+ stepOf: row.id,
2724
+ inset: (row.inset ?? 0) + 1,
2725
+ tone: row.tone,
2726
+ own: step.own.map((mark) => ({
2727
+ ...mark,
2728
+ tone: row.tone
2729
+ }))
2730
+ }))] : [row]);
2731
+ }
2732
+ /** Three or more consecutive steps of one kind and name read as one row ("15 model calls · 4h"), each a mark in its bar. */
2733
+ function stepRuns(steps) {
2734
+ const out = [];
2735
+ for (let i = 0; i < steps.length;) {
2736
+ let j = i;
2737
+ while (j < steps.length && steps[j].kind === steps[i].kind && steps[j].name === steps[i].name) j++;
2738
+ const run = steps.slice(i, j);
2739
+ if (run.length < 3) {
2740
+ out.push(...run);
2741
+ i = j;
2742
+ continue;
2743
+ }
2744
+ const first = run[0];
2745
+ const startMs = Math.min(...run.map((step) => step.span.startMs));
2746
+ const endMs = Math.max(...run.map((step) => step.span.endMs));
2747
+ const failed = run.filter((step) => !step.ok).length;
2748
+ const costs = run.map((step) => step.span.costUsd ?? step.rollupCostUsd).filter((cost) => cost !== void 0);
2749
+ const noun = first.kind === "model" ? "model calls" : first.kind === "tool" ? `${first.name} calls` : `${first.name} steps`;
2750
+ out.push({
2751
+ ...first,
2752
+ id: `run:${first.id}`,
2753
+ name: `${run.length} ${noun}`,
2754
+ durationLabel: formatSpanDuration(endMs - startMs),
2755
+ ok: failed === 0,
2756
+ hasChildren: false,
2757
+ descendants: 0,
2758
+ segments: [],
2759
+ own: run.map((step) => ({
2760
+ startMs: step.span.startMs,
2761
+ endMs: step.span.endMs,
2762
+ kind: step.ok ? step.kind === "model" ? "model" : "tool" : "failed",
2763
+ tone: first.tone,
2764
+ id: step.id,
2765
+ count: 1,
2766
+ retry: step.retry
2767
+ })),
2768
+ ...costs.length ? { rollupCostUsd: costs.reduce((sum, cost) => sum + cost, 0) } : {},
2769
+ span: {
2770
+ ...first.span,
2771
+ id: `run:${first.id}`,
2772
+ name: `${run.length} ${noun}`,
2773
+ startMs,
2774
+ endMs,
2775
+ status: failed ? "failed" : first.span.status,
2776
+ costUsd: void 0
2777
+ }
2778
+ });
2779
+ i = j;
2780
+ }
2781
+ return out;
2782
+ }
2594
2783
  //#endregion
2595
2784
  //#region src/react/flow-waterfall-icons.tsx
2596
2785
  const svg = (children, label) => /* @__PURE__ */ jsx("svg", {
@@ -2608,7 +2797,52 @@ const svg = (children, label) => /* @__PURE__ */ jsx("svg", {
2608
2797
  children
2609
2798
  });
2610
2799
  /** Kind glyphs drawn when the host supplies no `renderIcon`. Brand marks belong to the host. */
2611
- function KindGlyph({ kind }) {
2800
+ /** Generic tool identity by name: a terminal, a file, a pencil, a lens, a globe or a branch, never a vendor mark. */
2801
+ function toolGlyph(name) {
2802
+ const n = (name ?? "").toLowerCase();
2803
+ if (/bash|shell|exec|terminal|command|run/.test(n)) return svg(/* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("rect", {
2804
+ x: "2",
2805
+ y: "3",
2806
+ width: "12",
2807
+ height: "10",
2808
+ rx: "2"
2809
+ }), /* @__PURE__ */ jsx("path", { d: "m5 7 2 1.5L5 10M8.5 10.5h2.5" })] }));
2810
+ if (/edit|write|patch|apply/.test(n)) return svg(/* @__PURE__ */ jsx("path", { d: "M3 13l1-3.5L10.5 3a1.4 1.4 0 0 1 2 2L6 11.5 3 13ZM9.5 4l2 2" }));
2811
+ if (/grep|search|find|query/.test(n)) return svg(/* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("circle", {
2812
+ cx: "7",
2813
+ cy: "7",
2814
+ r: "4.25"
2815
+ }), /* @__PURE__ */ jsx("path", { d: "m10.25 10.25 3.25 3.25" })] }));
2816
+ if (/read|glob|list|ls|cat|file|view/.test(n)) return svg(/* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("path", { d: "M4 2.5h5l3 3v8H4z" }), /* @__PURE__ */ jsx("path", { d: "M9 2.5v3h3M6 9h4M6 11h3" })] }));
2817
+ if (/web|fetch|http|browser|url/.test(n)) return svg(/* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("circle", {
2818
+ cx: "8",
2819
+ cy: "8",
2820
+ r: "5.5"
2821
+ }), /* @__PURE__ */ jsx("path", { d: "M2.5 8h11M8 2.5c2 2 2 9 0 11M8 2.5c-2 2-2 9 0 11" })] }));
2822
+ if (/git|commit|branch|merge/.test(n)) return svg(/* @__PURE__ */ jsxs(Fragment, { children: [
2823
+ /* @__PURE__ */ jsx("circle", {
2824
+ cx: "5",
2825
+ cy: "4",
2826
+ r: "1.5"
2827
+ }),
2828
+ /* @__PURE__ */ jsx("circle", {
2829
+ cx: "5",
2830
+ cy: "12",
2831
+ r: "1.5"
2832
+ }),
2833
+ /* @__PURE__ */ jsx("circle", {
2834
+ cx: "11",
2835
+ cy: "6",
2836
+ r: "1.5"
2837
+ }),
2838
+ /* @__PURE__ */ jsx("path", { d: "M5 5.5v5M11 7.5c0 2-3 2-6 3" })
2839
+ ] }));
2840
+ }
2841
+ function KindGlyph({ kind, name }) {
2842
+ if (kind === "tool") {
2843
+ const glyph = toolGlyph(name);
2844
+ if (glyph) return glyph;
2845
+ }
2612
2846
  switch (kind) {
2613
2847
  case "pipeline": return svg(/* @__PURE__ */ jsxs(Fragment, { children: [
2614
2848
  /* @__PURE__ */ jsx("circle", {
@@ -2732,7 +2966,7 @@ function SpanCard({ id, row, agent, clockMs, left, top, pinned, onClose, renderI
2732
2966
  if (cost !== void 0) cells.push(["Cost", formatCost(cost)]);
2733
2967
  cells.splice(6);
2734
2968
  const kind = span.kind === "model" ? "Thinking" : KIND_LABEL[span.kind];
2735
- const step = owner ? `${kind}${span.name && span.name !== kind && span.name.toLowerCase() !== span.kind ? ` ${span.name}` : ""} · ${duration}${row.ok ? "" : " · failed"}` : void 0;
2969
+ 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;
2736
2970
  const sentence = !owner && span.output && span.output !== who.name ? span.output : void 0;
2737
2971
  return /* @__PURE__ */ jsxs("div", {
2738
2972
  id,
@@ -2842,7 +3076,10 @@ function SpanCard({ id, row, agent, clockMs, left, top, pinned, onClose, renderI
2842
3076
  /* @__PURE__ */ jsx("span", {
2843
3077
  className: "fw-icon-tile",
2844
3078
  "aria-hidden": true,
2845
- children: renderIcon?.(span) ?? /* @__PURE__ */ jsx(KindGlyph, { kind: span.kind })
3079
+ children: renderIcon?.(span) ?? /* @__PURE__ */ jsx(KindGlyph, {
3080
+ kind: span.kind,
3081
+ name: span.name
3082
+ })
2846
3083
  }),
2847
3084
  /* @__PURE__ */ jsxs("div", {
2848
3085
  className: "fw-card-title",
@@ -2905,10 +3142,10 @@ const at = (startMs, endMs) => ({
2905
3142
  "--s": startMs,
2906
3143
  "--e": Number.isFinite(endMs) ? endMs : OPEN_END
2907
3144
  });
2908
- function Segment({ mark, events, agentView }) {
2909
- const glyph = mark.kind === "failed" ? agentView ? null : /* @__PURE__ */ jsx(FailGlyph, {}) : mark.retry ? /* @__PURE__ */ jsx(RetryGlyph, {}) : null;
3145
+ function Segment({ mark, events, agentView, ending }) {
3146
+ const glyph = mark.kind === "failed" ? agentView && !ending ? null : /* @__PURE__ */ jsx(FailGlyph, {}) : mark.retry ? /* @__PURE__ */ jsx(RetryGlyph, {}) : null;
2910
3147
  return /* @__PURE__ */ jsx("span", {
2911
- className: `fw-seg fw-seg-${mark.kind}`,
3148
+ className: `fw-seg fw-seg-${mark.kind}${mark.alt ? " fw-seg-alt" : ""}`,
2912
3149
  "data-tone": toneKey(mark.tone),
2913
3150
  style: at(mark.startMs, mark.endMs),
2914
3151
  onPointerEnter: agentView ? (event) => events.onInspect(mark.id, event.currentTarget) : void 0,
@@ -2931,7 +3168,7 @@ function endLabel(row, clockMs) {
2931
3168
  if (!spanRunning(span) && clockMs < span.endMs) return `${formatSpanDuration(Math.max(0, clockMs - span.startMs))}${row.approx ? "~" : ""}`;
2932
3169
  const duration = spanRunning(span) ? `${formatSpanDuration(Math.max(span.endMs, clockMs) - span.startMs)}${row.approx ? "~" : ""}` : row.durationLabel;
2933
3170
  const cost = span.costUsd ?? (row.rollupCostUsd ? row.rollupCostUsd : void 0);
2934
- return cost === void 0 ? duration : `${duration} · ${formatCost(cost)}`;
3171
+ return cost === void 0 ? duration : `${duration} · ${span.meta?.costPartial ? "≥" : ""}${formatCost(cost)}`;
2935
3172
  }
2936
3173
  /** One trace row. Geometry comes from CSS variables, so a row re-renders only when its own state changes. */
2937
3174
  const FlowRow = memo(function FlowRow(props) {
@@ -2943,9 +3180,13 @@ const FlowRow = memo(function FlowRow(props) {
2943
3180
  const invalid = row.durationLabel === "—";
2944
3181
  const isAgent = row.kind === "pipeline";
2945
3182
  const kind = !row.ok ? "failed" : row.kind;
2946
- const marks = isAgent || row.hasChildren ? folded && !agentView ? row.segments : row.own : [];
3183
+ const marks = isAgent || row.hasChildren || row.id.startsWith("run:") ? folded && !agentView ? row.segments : row.own : [];
2947
3184
  const label = endLabel(row, clockMs);
2948
3185
  const inspect = (event) => props.onInspect(row.id, event.currentTarget);
3186
+ const family = agentView && row.id.startsWith("family:");
3187
+ const step = agentView && row.stepOf !== void 0;
3188
+ const opens = family || agentView && isAgent && row.own.length > 0;
3189
+ const ending = agentView && !row.ok ? marks.map((mark) => mark.kind).lastIndexOf("failed") : -1;
2949
3190
  return /* @__PURE__ */ jsxs("div", {
2950
3191
  role: "row",
2951
3192
  "data-span-id": row.id,
@@ -2958,7 +3199,7 @@ const FlowRow = memo(function FlowRow(props) {
2958
3199
  tabIndex: focusable ? 0 : -1,
2959
3200
  "data-tone": toneKey(row.tone),
2960
3201
  "data-outcome": fillByAgent ? void 0 : span.outcome?.tone,
2961
- className: `fw-row${active ? " fw-active" : ""}${sticky ? " fw-sticky-row" : ""}${isAgent ? " fw-agent-row" : ""}${motion?.enter !== void 0 ? " fw-enter" : ""}${motion?.settle ? " fw-settle" : ""}`,
3202
+ 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" : ""}`,
2962
3203
  style: {
2963
3204
  "--row-y": `${top}px`,
2964
3205
  "--fw-enter-delay": `${motion?.enter ?? 0}ms`
@@ -2968,14 +3209,30 @@ const FlowRow = memo(function FlowRow(props) {
2968
3209
  },
2969
3210
  onClick: (event) => {
2970
3211
  props.onActivate(row);
3212
+ if (family) {
3213
+ props.onToggle(row);
3214
+ return;
3215
+ }
2971
3216
  const bar = agentView ? event.currentTarget.querySelector(".fw-bar") : null;
2972
3217
  if (bar) props.onPin(row.id, bar);
2973
3218
  },
2974
3219
  children: [/* @__PURE__ */ jsxs("div", {
2975
3220
  role: "gridcell",
2976
3221
  className: "fw-label",
2977
- style: { "--depth": agentView ? 0 : row.depth },
3222
+ style: { "--depth": agentView ? row.inset ?? 0 : row.depth },
2978
3223
  children: [
3224
+ opens && /* @__PURE__ */ jsx("button", {
3225
+ type: "button",
3226
+ tabIndex: -1,
3227
+ className: `fw-toggle${props.expanded ? " fw-open" : ""}`,
3228
+ "aria-label": `${props.expanded ? "Collapse" : "Expand"} ${row.name}`,
3229
+ "aria-expanded": Boolean(props.expanded),
3230
+ onClick: (event) => {
3231
+ event.stopPropagation();
3232
+ props.onToggle(row);
3233
+ },
3234
+ children: /* @__PURE__ */ jsx(ChevronGlyph, {})
3235
+ }),
2979
3236
  !agentView && /* @__PURE__ */ jsx("span", {
2980
3237
  className: "fw-guides",
2981
3238
  "aria-hidden": true
@@ -2996,10 +3253,13 @@ const FlowRow = memo(function FlowRow(props) {
2996
3253
  className: "fw-toggle-space",
2997
3254
  "aria-hidden": true
2998
3255
  })),
2999
- !agentView && /* @__PURE__ */ jsx("span", {
3256
+ (!agentView || step) && /* @__PURE__ */ jsx("span", {
3000
3257
  className: "fw-icon-tile",
3001
3258
  "aria-hidden": true,
3002
- children: renderIcon?.(span) ?? /* @__PURE__ */ jsx(KindGlyph, { kind: span.kind })
3259
+ children: renderIcon?.(span) ?? /* @__PURE__ */ jsx(KindGlyph, {
3260
+ kind: span.kind,
3261
+ name: span.name
3262
+ })
3003
3263
  }),
3004
3264
  /* @__PURE__ */ jsxs("span", {
3005
3265
  className: "fw-names",
@@ -3014,7 +3274,7 @@ const FlowRow = memo(function FlowRow(props) {
3014
3274
  "data-outcome": span.outcome.tone,
3015
3275
  title: span.outcome.label,
3016
3276
  "aria-hidden": true
3017
- }) : agentView && /* @__PURE__ */ jsx("span", {
3277
+ }) : agentView && !step && /* @__PURE__ */ jsx("span", {
3018
3278
  className: "fw-agent-dot",
3019
3279
  "aria-hidden": true
3020
3280
  }),
@@ -3029,9 +3289,9 @@ const FlowRow = memo(function FlowRow(props) {
3029
3289
  })
3030
3290
  ]
3031
3291
  }),
3032
- agentView && span.detail && /* @__PURE__ */ jsx("span", {
3292
+ agentView && /* @__PURE__ */ jsx("span", {
3033
3293
  className: "fw-subtitle",
3034
- children: span.detail
3294
+ children: span.detail && span.detail !== props.sharedDetail ? `${span.detail} · ${agentSummary(row)}` : agentSummary(row)
3035
3295
  }),
3036
3296
  agentView && renderIcon && /* @__PURE__ */ jsx("span", {
3037
3297
  className: "fw-detail-icon",
@@ -3075,7 +3335,8 @@ const FlowRow = memo(function FlowRow(props) {
3075
3335
  onPointerLeave: props.onLeave,
3076
3336
  onClick: (event) => {
3077
3337
  event.stopPropagation();
3078
- props.onPin(row.id, event.currentTarget);
3338
+ if (family) props.onToggle(row);
3339
+ else props.onPin(row.id, event.currentTarget);
3079
3340
  },
3080
3341
  children: [!isAgent && !row.ok && /* @__PURE__ */ jsx("span", {
3081
3342
  className: "fw-glyph",
@@ -3088,7 +3349,8 @@ const FlowRow = memo(function FlowRow(props) {
3088
3349
  marks.map((mark, i) => /* @__PURE__ */ jsx(Segment, {
3089
3350
  mark,
3090
3351
  events: props,
3091
- agentView
3352
+ agentView,
3353
+ ending: i === ending
3092
3354
  }, `${mark.id}-${i}`)),
3093
3355
  /* @__PURE__ */ jsxs("span", {
3094
3356
  className: "fw-end",
@@ -3111,7 +3373,7 @@ const FlowRow = memo(function FlowRow(props) {
3111
3373
  ] })
3112
3374
  })]
3113
3375
  });
3114
- }, (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.costliest === b.costliest && a.active === b.active && a.focusable === b.focusable && a.selected === b.selected && a.sticky === b.sticky && a.motion === b.motion && a.renderIcon === b.renderIcon && a.fillByAgent === b.fillByAgent && (a.clockMs === b.clockMs || !open(a.row, a.clockMs) && !open(b.row, b.clockMs)));
3376
+ }, (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.costliest === b.costliest && a.active === b.active && a.focusable === b.focusable && a.selected === b.selected && a.sticky === b.sticky && a.motion === b.motion && a.renderIcon === b.renderIcon && a.fillByAgent === b.fillByAgent && a.sharedDetail === b.sharedDetail && a.expanded === b.expanded && (a.clockMs === b.clockMs || !open(a.row, a.clockMs) && !open(b.row, b.clockMs)));
3115
3377
  /** An elbow connector between two rows at one moment, drawn in the row's tone; appears when the clock reaches it. */
3116
3378
  function LinkMark({ link, rowHeight, hot }) {
3117
3379
  const upper = Math.min(link.from, link.to), lower = Math.max(link.from, link.to);
@@ -3315,7 +3577,7 @@ function FlowBody(props) {
3315
3577
  const [viewport, setViewport] = useState(maxHeight);
3316
3578
  const latest = useRef(props);
3317
3579
  latest.current = props;
3318
- const height = Math.min(maxHeight, Math.max(rows.length, 1) * rowHeight);
3580
+ const height = props.fixedHeight ? maxHeight : Math.min(maxHeight, Math.max(rows.length, 1) * rowHeight);
3319
3581
  const links = useMemo(() => linksFor(rows, props.byId, view), [
3320
3582
  rows,
3321
3583
  props.byId,
@@ -3323,6 +3585,7 @@ function FlowBody(props) {
3323
3585
  ]);
3324
3586
  const index = useMemo(() => new Map(rows.map((row, i) => [row.id, i])), [rows]);
3325
3587
  const motion = useRowMotion(rows);
3588
+ const [jumped, setJumped] = useState();
3326
3589
  const msPerPx = (win.endMs - win.startMs) / Math.max(1, trackPx);
3327
3590
  const tickEnd = win.startMs + (trackPx + (view === "spans" ? 72 : 0)) * msPerPx;
3328
3591
  const ticks = clockTicks(win.startMs, tickEnd, Math.max(1, trackPx + (view === "spans" ? 72 : 0)));
@@ -3504,9 +3767,19 @@ function FlowBody(props) {
3504
3767
  style: { "--s": marker.at },
3505
3768
  title: marker.kind ? `${marker.kind}: ${marker.label}` : marker.label,
3506
3769
  "aria-label": `Jump to ${marker.kind ? `${marker.kind} ` : ""}${marker.label}`,
3770
+ "aria-pressed": jumped === marker,
3507
3771
  onPointerDown: (event) => event.stopPropagation(),
3508
- onClick: () => props.onScrub(marker.at)
3772
+ onClick: () => {
3773
+ setJumped(marker);
3774
+ props.onScrub(marker.at);
3775
+ }
3509
3776
  }, `e-${marker.at}-${marker.label}`)),
3777
+ jumped && /* @__PURE__ */ jsx("span", {
3778
+ className: "fw-event-label",
3779
+ "data-tone": jumped.tone ?? "neutral",
3780
+ style: { "--s": jumped.at },
3781
+ children: jumped.label
3782
+ }),
3510
3783
  showCursor && /* @__PURE__ */ jsx("span", {
3511
3784
  className: "fw-cursor-handle",
3512
3785
  "aria-hidden": true
@@ -3530,6 +3803,8 @@ function FlowBody(props) {
3530
3803
  clockMs,
3531
3804
  renderIcon: props.renderIcon,
3532
3805
  fillByAgent: props.fillByAgent,
3806
+ sharedDetail: props.sharedDetail,
3807
+ expanded: props.expandedIds?.has(item.id),
3533
3808
  onToggle: props.onToggle,
3534
3809
  onInspect: props.onInspect,
3535
3810
  onLeave: props.onLeave,
@@ -3617,7 +3892,7 @@ function FlowBody(props) {
3617
3892
  })
3618
3893
  }, `${marker.at}-${marker.label}`))]
3619
3894
  }) : null,
3620
- showCursor && /* @__PURE__ */ jsx("div", {
3895
+ showCursor && props.cursorLine !== false && /* @__PURE__ */ jsx("div", {
3621
3896
  className: "fw-cursor-layer",
3622
3897
  "aria-hidden": true,
3623
3898
  children: /* @__PURE__ */ jsx("span", { className: "fw-cursor" })
@@ -3905,11 +4180,11 @@ function summaryCells(trace, at, durationMs) {
3905
4180
  errors,
3906
4181
  summary: [
3907
4182
  ["Duration", formatSpanDuration(durationMs)],
3908
- [
4183
+ ...cost.value === "—" ? [] : [[
3909
4184
  cost.label,
3910
4185
  cost.value,
3911
4186
  cost.title
3912
- ],
4187
+ ]],
3913
4188
  [
3914
4189
  "Tokens",
3915
4190
  formatCount(tokens),
@@ -3920,7 +4195,11 @@ function summaryCells(trace, at, durationMs) {
3920
4195
  modelCalls === void 0 ? "—" : modelCalls.toLocaleString(),
3921
4196
  modelCalls === void 0 ? "Model output windows do not establish the number of LLM requests" : void 0
3922
4197
  ],
3923
- ["Tool calls", (at ? at.toolCalls : trace.toolCalls).toLocaleString()],
4198
+ [
4199
+ "Tool calls",
4200
+ (at ? at.toolCalls : trace.toolCalls)?.toLocaleString() ?? "—",
4201
+ (at ? at.toolCalls : trace.toolCalls) === void 0 ? "The producer did not count tool calls" : void 0
4202
+ ],
3924
4203
  ["Errors", errors.toLocaleString()]
3925
4204
  ]
3926
4205
  };
@@ -3939,8 +4218,7 @@ function KindLegend({ rows }) {
3939
4218
  ["model", "Model call"],
3940
4219
  ["tool", "Tool call"],
3941
4220
  ["waiting", "Waiting"],
3942
- ["handoff", "Handoff"],
3943
- ["failed", "Failed turn"]
4221
+ ["handoff", "Handoff"]
3944
4222
  ].filter(([kind]) => kind === "waiting" ? rows.some((row) => row.kind === "pipeline") : kind === "handoff" ? rows.some((row) => row.span.handoffTo || row.hasAgents) : has(kind));
3945
4223
  if (shown.length === 0) return null;
3946
4224
  return /* @__PURE__ */ jsx("ul", {
@@ -3992,7 +4270,7 @@ function OutcomeLegend({ agents }) {
3992
4270
  }
3993
4271
  /** Shared trace inspector: the agent timeline and the span waterfall over one clock.
3994
4272
  * Data remains producer-owned; playback, zoom and folding change presentation only. */
3995
- function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defaultCollapsed = false, defaultView = "spans", renderIcon, maxHeight, onSelect, markers, onTimeChange, agentFill = "agent", selectedId, onDeselect, description }) {
4273
+ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defaultCollapsed = false, defaultView = "spans", renderIcon, maxHeight, onSelect, markers, onTimeChange, agentFill = "agent", selectedId, onDeselect, description, summary: showSummary = true }) {
3996
4274
  const root = useRef(null);
3997
4275
  const search = useRef(null);
3998
4276
  const slider = useRef(null);
@@ -4065,17 +4343,22 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
4065
4343
  const pipelines = rows.filter((row) => row.kind === "pipeline");
4066
4344
  return pipelines.length ? pipelines : rows.filter((row) => !row.parentId);
4067
4345
  }, [rows]);
4068
- const visible = useMemo(() => view === "agents" ? agents : visibleFlowWaterfallRows(rows, query, collapsed), [
4346
+ const sharedDetail = agents.length > 1 && agents.every((row) => row.span.detail && row.span.detail === agents[0]?.span.detail) ? agents[0]?.span.detail : void 0;
4347
+ const [openFamilies, setOpenFamilies] = useState(/* @__PURE__ */ new Set());
4348
+ const [openSteps, setOpenSteps] = useState(/* @__PURE__ */ new Set());
4349
+ const visible = useMemo(() => view === "agents" ? agentViewRows(agents, rows, openFamilies, openSteps) : visibleFlowWaterfallRows(rows, query, collapsed), [
4069
4350
  view,
4070
4351
  agents,
4071
4352
  rows,
4353
+ openFamilies,
4354
+ openSteps,
4072
4355
  query,
4073
4356
  collapsed
4074
4357
  ]);
4075
4358
  const slowest = view === "spans" ? slowestRow(rows, clockMs) : void 0;
4076
- const callouts = useMemo(() => view === "agents" ? agentCallouts(agents, clockMs) : {}, [
4359
+ const callouts = useMemo(() => view === "agents" ? agentCallouts(visible, clockMs) : {}, [
4077
4360
  view,
4078
- agents,
4361
+ visible,
4079
4362
  clockMs
4080
4363
  ]);
4081
4364
  const rowsHeight = maxHeight ?? (view === "agents" ? ROW_HEIGHT.agents * 20 : 640);
@@ -4177,12 +4460,14 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
4177
4460
  inspect: (_target, _element) => {}
4178
4461
  });
4179
4462
  const toggleNow = (row) => {
4463
+ const flip = (open) => new Set(open.has(row.id) ? [...open].filter((id) => id !== row.id) : [...open, row.id]);
4464
+ if (row.id.startsWith("family:")) {
4465
+ setOpenFamilies(flip);
4466
+ return;
4467
+ }
4180
4468
  onSelect?.(row.span);
4181
4469
  if (view === "agents") {
4182
- const keep = /* @__PURE__ */ new Set();
4183
- for (let cursor = row; cursor; cursor = cursor.parentId ? byId.get(cursor.parentId) : void 0) keep.add(cursor.id);
4184
- setCollapsed(new Set(rows.filter((item) => item.hasChildren && !keep.has(item.id) && item.kind === "pipeline").map((item) => item.id)));
4185
- setView("spans");
4470
+ setOpenSteps(flip);
4186
4471
  setActiveId(row.id);
4187
4472
  return;
4188
4473
  }
@@ -4265,11 +4550,13 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
4265
4550
  const busy = agents.reduce((sum, row) => sum + busyMs(row, limit), 0);
4266
4551
  const speedup = limit > 0 ? busy / limit : 0;
4267
4552
  const done = !isLive && !trace.spans.some(spanRunning);
4268
- const cardRow = card ? byId.get(card.target) : void 0;
4553
+ const shown = (row) => row && (visible.find((item) => item.id === row.id) ?? row);
4554
+ const cardRow = shown(card ? byId.get(card.target) : void 0);
4269
4555
  const cardAgent = cardRow && (() => {
4270
- for (let cursor = cardRow; cursor; cursor = cursor.parentId ? byId.get(cursor.parentId) : void 0) if (cursor.kind === "pipeline") return cursor;
4556
+ for (let cursor = cardRow; cursor; cursor = cursor.parentId ? byId.get(cursor.parentId) : void 0) if (cursor.kind === "pipeline") return shown(cursor);
4271
4557
  })();
4272
- const showCursor = isLive || position !== null || playing;
4558
+ const moving = isLive || position !== null || playing;
4559
+ const showCursor = moving || view === "agents";
4273
4560
  const overviewMarks = useMemo(() => rows.filter((row) => !row.parentId).flatMap((row) => row.hasChildren ? row.segments : row.kind === "pipeline" ? [] : [{
4274
4561
  startMs: row.span.startMs,
4275
4562
  endMs: row.span.endMs,
@@ -4356,6 +4643,7 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
4356
4643
  agents.length,
4357
4644
  " ",
4358
4645
  agents.length === 1 ? "agent" : "agents",
4646
+ sharedDetail ? ` · ${sharedDetail}` : "",
4359
4647
  done && speedup >= 1.05 ? ` · ${speedup.toFixed(1)}x parallel` : ""
4360
4648
  ]
4361
4649
  }),
@@ -4413,7 +4701,7 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
4413
4701
  })]
4414
4702
  })]
4415
4703
  }),
4416
- /* @__PURE__ */ jsx("dl", {
4704
+ showSummary && /* @__PURE__ */ jsx("dl", {
4417
4705
  className: "fw-summary",
4418
4706
  children: summary.map(([name, value, title]) => /* @__PURE__ */ jsxs("div", {
4419
4707
  className: name === "Errors" && errors > 0 ? "fw-has-errors" : void 0,
@@ -4425,6 +4713,7 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
4425
4713
  }, name))
4426
4714
  }),
4427
4715
  view === "agents" && playback,
4716
+ view === "agents" && /* @__PURE__ */ jsx(KindLegend, { rows }),
4428
4717
  /* @__PURE__ */ jsx(FlowBody, {
4429
4718
  rows: visible,
4430
4719
  byId,
@@ -4434,6 +4723,10 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
4434
4723
  collapsed,
4435
4724
  searching: Boolean(query.trim()),
4436
4725
  pauses: trace.pauses,
4726
+ cursorLine: moving,
4727
+ sharedDetail,
4728
+ expandedIds: view === "agents" ? /* @__PURE__ */ new Set([...openFamilies, ...openSteps]) : void 0,
4729
+ fixedHeight: isLive,
4437
4730
  slowestId: slowest?.id ?? callouts.slowestId,
4438
4731
  costliestId: callouts.costliestId,
4439
4732
  activeId,
@@ -4483,7 +4776,6 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
4483
4776
  compact: view === "agents"
4484
4777
  })
4485
4778
  }),
4486
- view === "agents" && /* @__PURE__ */ jsx(KindLegend, { rows }),
4487
4779
  view === "spans" && playback
4488
4780
  ]
4489
4781
  });
@@ -59,7 +59,7 @@ line.ov-gridline { stroke: color-mix(in srgb, var(--chart-live-faint, #777) 22%,
59
59
  .ov-gantt-open { stroke: var(--chart-live-fg, var(--chart-ink)); stroke-width: 1.5; stroke-dasharray: 3 2; }
60
60
  .ov-events, .ov-key, .ov-meter-key { list-style: none; margin: 4px 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: 4px 16px; font-size: 12px; line-height: 16px; color: var(--chart-live-muted, var(--chart-ink-muted)); }
61
61
  .ov-key li, .ov-events li, .ov-meter-key li { display: inline-flex; align-items: center; gap: 8px; }
62
- .ov-meter-key b { font-weight: 500; color: var(--chart-live-fg, var(--chart-ink)); font-family: var(--chart-live-font-mono, monospace); font-variant-numeric: tabular-nums; }
62
+ .ov-meter-key b { font-weight: 600; color: var(--chart-live-fg, var(--chart-ink)); font-family: var(--chart-live-font-mono, monospace); font-variant-numeric: tabular-nums; }
63
63
  .ov-meter-key i, .ov-event-swatch { display: inline-block; width: 12px; height: 12px; border-radius: 2px; }
64
64
  [data-segmented] > .ov-meter-track { display: flex; }
65
65
  [data-segmented] > .ov-meter-track > span { flex: none; border-radius: 0; }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/charts",
3
- "version": "0.20.1",
3
+ "version": "0.20.2",
4
4
  "description": "Static SVG and HTML figures for benchmarks and operational timing. Draws from producer data and refuses what it cannot draw honestly.",
5
5
  "type": "module",
6
6
  "license": "MIT",