@tangle-network/charts 0.20.1 → 0.20.3

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,34 +250,45 @@
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); }
259
263
 
260
264
  /* Handoff elbows: 1.5 px stroke, 6 px radius, a 5 px dot where the handoff leaves its sender. */
261
- .fw-root .fw-links { position: absolute; top: 0; bottom: 0; left: var(--fw-label-col); right: 0; overflow: hidden; pointer-events: none; z-index: 1; }
265
+ /* Under the rows (which follow it), so end labels and badges always sit above the connectors. */
266
+ .fw-root .fw-links { position: absolute; top: 0; bottom: 0; left: var(--fw-label-col); right: 0; overflow: hidden; pointer-events: none; z-index: 0; }
262
267
  .fw-root .fw-link {
263
268
  --x: calc(var(--fw-pad) + (var(--s) - var(--fw-v0)) * var(--fw-k) * 1px);
264
269
  position: absolute; width: 8px; border: 0 solid var(--line); border-bottom-width: 1.5px; translate: 0 var(--fw-bar-shift);
265
270
  opacity: clamp(0, (var(--fw-t) - var(--s)) * 1000, 1);
266
271
  }
267
272
  .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); }
273
+ /* Agent-view connectors rest faint, one curved elbow per spawn in the agent's hue (they show who started whom), and come to
274
+ full strength for the agent under the pointer, pinned or focused. */
275
+ .fw-root .fw-view-agents .fw-link:not(.fw-link-hot) { opacity: calc(clamp(0, (var(--fw-t) - var(--s)) * 1000, 1) * .6); }
270
276
  .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; }
277
+ .fw-root .fw-link-down { left: calc(var(--x) - 8px); border-left-width: 1.5px; border-bottom-left-radius: 12px; }
278
+ .fw-root .fw-link-up { left: var(--x); border-right-width: 1.5px; border-bottom-right-radius: 12px; }
273
279
  .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
280
  .fw-root .fw-link-down::before { left: -3.25px; }
275
281
  .fw-root .fw-link-up::before { right: -3.25px; }
276
282
 
277
283
  /* Span card */
278
284
  .fw-root .fw-card-anchor { display: contents; }
285
+ /* Agent view: the card docks under the lanes instead of floating over the plot (judges: "covers the data it describes"). */
286
+ /* Two classes on the root so these win over the base .fw-root .fw-card rule that follows. */
287
+ .fw-root.fw-root-agents .fw-card-anchor { display: block; padding: 0 24px 12px; }
288
+ .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; }
289
+ .fw-root .fw-seg-alt { filter: brightness(.78); }
290
+ /* Agent view: the scrubber track shows how far the run has played, so at rest it reads full, not empty. */
291
+ .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
292
  .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
293
  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
294
  /* 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,208 @@ 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
+ members: members.map((row) => row.id)
2668
+ }
2669
+ }
2670
+ });
2671
+ if (open.has(id)) out.push(...members.map((row, i) => ({
2672
+ ...row,
2673
+ family: id,
2674
+ inset: 1,
2675
+ tone,
2676
+ own: row.own.map((mark) => ({
2677
+ ...mark,
2678
+ tone,
2679
+ alt: i % 2 === 1
2680
+ }))
2681
+ })));
2682
+ }
2683
+ return out;
2684
+ }
2685
+ /** 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"). */
2686
+ function agentSummary(row) {
2687
+ if (row.id.startsWith("family:")) {
2688
+ const failed = Number(row.span.meta?.failedAgents ?? 0);
2689
+ return `${row.descendants} agents${failed ? ` · ${failed} failed` : ""}`;
2690
+ }
2691
+ if (row.id.startsWith("run:")) {
2692
+ const failed = row.own.filter((mark) => mark.kind === "failed").length;
2693
+ return failed ? `${failed} failed` : "all done";
2694
+ }
2695
+ const counts = row.counts ?? {
2696
+ model: 0,
2697
+ tool: 0,
2698
+ failed: 0
2699
+ };
2700
+ const turns = counts.model + counts.failed;
2701
+ const tools = counts.tool;
2702
+ const state = row.span.outcome?.label ?? (spanRunning(row.span) ? "running" : row.ok ? "done" : "failed");
2703
+ const unit = row.kind === "pipeline" ? ["turn", "turns"] : ["model call", "model calls"];
2704
+ return [
2705
+ state,
2706
+ turns ? `${turns} ${turns === 1 ? unit[0] : unit[1]}` : "",
2707
+ tools ? `${tools} tool ${tools === 1 ? "call" : "calls"}` : ""
2708
+ ].filter(Boolean).join(" · ");
2709
+ }
2710
+ /**
2711
+ * The agent view's rows: families folded until opened, and an opened agent's own steps (its turns and tool calls, not its
2712
+ * child agents) nested beneath it with their icons, durations and costs, as a reader drills from the run into one agent.
2713
+ */
2714
+ function agentViewRows(agents, rows, openFamilies, openSteps) {
2715
+ const base = agentFamilies(agents, openFamilies);
2716
+ if (openSteps.size === 0) return base;
2717
+ const steps = /* @__PURE__ */ new Map();
2718
+ for (const row of rows) {
2719
+ if (row.parentId === void 0 || !openSteps.has(row.parentId) || row.kind === "pipeline") continue;
2720
+ steps.set(row.parentId, [...steps.get(row.parentId) ?? [], row]);
2721
+ }
2722
+ return base.flatMap((row) => openSteps.has(row.id) ? [row, ...stepRuns(steps.get(row.id) ?? []).map((step) => ({
2723
+ ...step,
2724
+ stepOf: row.id,
2725
+ inset: (row.inset ?? 0) + 1,
2726
+ tone: row.tone,
2727
+ own: step.own.map((mark) => ({
2728
+ ...mark,
2729
+ tone: row.tone
2730
+ }))
2731
+ }))] : [row]);
2732
+ }
2733
+ /** Three or more consecutive steps of one kind and name read as one row ("15 model calls · 4h"), each a mark in its bar. */
2734
+ function stepRuns(steps) {
2735
+ const out = [];
2736
+ for (let i = 0; i < steps.length;) {
2737
+ let j = i;
2738
+ while (j < steps.length && steps[j].kind === steps[i].kind && steps[j].name === steps[i].name) j++;
2739
+ const run = steps.slice(i, j);
2740
+ if (run.length < 3) {
2741
+ out.push(...run);
2742
+ i = j;
2743
+ continue;
2744
+ }
2745
+ const first = run[0];
2746
+ const startMs = Math.min(...run.map((step) => step.span.startMs));
2747
+ const endMs = Math.max(...run.map((step) => step.span.endMs));
2748
+ const failed = run.filter((step) => !step.ok).length;
2749
+ const costs = run.map((step) => step.span.costUsd ?? step.rollupCostUsd).filter((cost) => cost !== void 0);
2750
+ const noun = first.kind === "model" ? "model calls" : first.kind === "tool" ? `${first.name} calls` : `${first.name} steps`;
2751
+ out.push({
2752
+ ...first,
2753
+ id: `run:${first.id}`,
2754
+ name: `${run.length} ${noun}`,
2755
+ durationLabel: formatSpanDuration(endMs - startMs),
2756
+ ok: failed === 0,
2757
+ hasChildren: false,
2758
+ descendants: 0,
2759
+ segments: [],
2760
+ own: run.map((step) => ({
2761
+ startMs: step.span.startMs,
2762
+ endMs: step.span.endMs,
2763
+ kind: step.ok ? step.kind === "model" ? "model" : "tool" : "failed",
2764
+ tone: first.tone,
2765
+ id: step.id,
2766
+ count: 1,
2767
+ retry: step.retry
2768
+ })),
2769
+ ...costs.length ? { rollupCostUsd: costs.reduce((sum, cost) => sum + cost, 0) } : {},
2770
+ span: {
2771
+ ...first.span,
2772
+ id: `run:${first.id}`,
2773
+ name: `${run.length} ${noun}`,
2774
+ startMs,
2775
+ endMs,
2776
+ status: failed ? "failed" : first.span.status,
2777
+ costUsd: void 0
2778
+ }
2779
+ });
2780
+ i = j;
2781
+ }
2782
+ return out;
2783
+ }
2784
+ /** A row as the view draws it: its drawn copy, or for a member of a folded family, itself in the family's hue. */
2785
+ function drawnRow(row, visible) {
2786
+ const drawn = visible.find((item) => item.id === row.id);
2787
+ if (drawn) return drawn;
2788
+ const family = visible.find((item) => (item.span.meta?.members)?.includes(row.id));
2789
+ return family ? {
2790
+ ...row,
2791
+ tone: family.tone
2792
+ } : row;
2793
+ }
2594
2794
  //#endregion
2595
2795
  //#region src/react/flow-waterfall-icons.tsx
2596
2796
  const svg = (children, label) => /* @__PURE__ */ jsx("svg", {
@@ -2608,7 +2808,52 @@ const svg = (children, label) => /* @__PURE__ */ jsx("svg", {
2608
2808
  children
2609
2809
  });
2610
2810
  /** Kind glyphs drawn when the host supplies no `renderIcon`. Brand marks belong to the host. */
2611
- function KindGlyph({ kind }) {
2811
+ /** Generic tool identity by name: a terminal, a file, a pencil, a lens, a globe or a branch, never a vendor mark. */
2812
+ function toolGlyph(name) {
2813
+ const n = (name ?? "").toLowerCase();
2814
+ if (/bash|shell|exec|terminal|command|run/.test(n)) return svg(/* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("rect", {
2815
+ x: "2",
2816
+ y: "3",
2817
+ width: "12",
2818
+ height: "10",
2819
+ rx: "2"
2820
+ }), /* @__PURE__ */ jsx("path", { d: "m5 7 2 1.5L5 10M8.5 10.5h2.5" })] }));
2821
+ 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" }));
2822
+ if (/grep|search|find|query/.test(n)) return svg(/* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("circle", {
2823
+ cx: "7",
2824
+ cy: "7",
2825
+ r: "4.25"
2826
+ }), /* @__PURE__ */ jsx("path", { d: "m10.25 10.25 3.25 3.25" })] }));
2827
+ 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" })] }));
2828
+ if (/web|fetch|http|browser|url/.test(n)) return svg(/* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx("circle", {
2829
+ cx: "8",
2830
+ cy: "8",
2831
+ r: "5.5"
2832
+ }), /* @__PURE__ */ jsx("path", { d: "M2.5 8h11M8 2.5c2 2 2 9 0 11M8 2.5c-2 2-2 9 0 11" })] }));
2833
+ if (/git|commit|branch|merge/.test(n)) return svg(/* @__PURE__ */ jsxs(Fragment, { children: [
2834
+ /* @__PURE__ */ jsx("circle", {
2835
+ cx: "5",
2836
+ cy: "4",
2837
+ r: "1.5"
2838
+ }),
2839
+ /* @__PURE__ */ jsx("circle", {
2840
+ cx: "5",
2841
+ cy: "12",
2842
+ r: "1.5"
2843
+ }),
2844
+ /* @__PURE__ */ jsx("circle", {
2845
+ cx: "11",
2846
+ cy: "6",
2847
+ r: "1.5"
2848
+ }),
2849
+ /* @__PURE__ */ jsx("path", { d: "M5 5.5v5M11 7.5c0 2-3 2-6 3" })
2850
+ ] }));
2851
+ }
2852
+ function KindGlyph({ kind, name }) {
2853
+ if (kind === "tool") {
2854
+ const glyph = toolGlyph(name);
2855
+ if (glyph) return glyph;
2856
+ }
2612
2857
  switch (kind) {
2613
2858
  case "pipeline": return svg(/* @__PURE__ */ jsxs(Fragment, { children: [
2614
2859
  /* @__PURE__ */ jsx("circle", {
@@ -2732,7 +2977,7 @@ function SpanCard({ id, row, agent, clockMs, left, top, pinned, onClose, renderI
2732
2977
  if (cost !== void 0) cells.push(["Cost", formatCost(cost)]);
2733
2978
  cells.splice(6);
2734
2979
  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;
2980
+ 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
2981
  const sentence = !owner && span.output && span.output !== who.name ? span.output : void 0;
2737
2982
  return /* @__PURE__ */ jsxs("div", {
2738
2983
  id,
@@ -2842,7 +3087,10 @@ function SpanCard({ id, row, agent, clockMs, left, top, pinned, onClose, renderI
2842
3087
  /* @__PURE__ */ jsx("span", {
2843
3088
  className: "fw-icon-tile",
2844
3089
  "aria-hidden": true,
2845
- children: renderIcon?.(span) ?? /* @__PURE__ */ jsx(KindGlyph, { kind: span.kind })
3090
+ children: renderIcon?.(span) ?? /* @__PURE__ */ jsx(KindGlyph, {
3091
+ kind: span.kind,
3092
+ name: span.name
3093
+ })
2846
3094
  }),
2847
3095
  /* @__PURE__ */ jsxs("div", {
2848
3096
  className: "fw-card-title",
@@ -2905,10 +3153,10 @@ const at = (startMs, endMs) => ({
2905
3153
  "--s": startMs,
2906
3154
  "--e": Number.isFinite(endMs) ? endMs : OPEN_END
2907
3155
  });
2908
- function Segment({ mark, events, agentView }) {
2909
- const glyph = mark.kind === "failed" ? agentView ? null : /* @__PURE__ */ jsx(FailGlyph, {}) : mark.retry ? /* @__PURE__ */ jsx(RetryGlyph, {}) : null;
3156
+ function Segment({ mark, events, agentView, ending }) {
3157
+ const glyph = mark.kind === "failed" ? agentView && !ending ? null : /* @__PURE__ */ jsx(FailGlyph, {}) : mark.retry ? /* @__PURE__ */ jsx(RetryGlyph, {}) : null;
2910
3158
  return /* @__PURE__ */ jsx("span", {
2911
- className: `fw-seg fw-seg-${mark.kind}`,
3159
+ className: `fw-seg fw-seg-${mark.kind}${mark.alt ? " fw-seg-alt" : ""}`,
2912
3160
  "data-tone": toneKey(mark.tone),
2913
3161
  style: at(mark.startMs, mark.endMs),
2914
3162
  onPointerEnter: agentView ? (event) => events.onInspect(mark.id, event.currentTarget) : void 0,
@@ -2931,7 +3179,7 @@ function endLabel(row, clockMs) {
2931
3179
  if (!spanRunning(span) && clockMs < span.endMs) return `${formatSpanDuration(Math.max(0, clockMs - span.startMs))}${row.approx ? "~" : ""}`;
2932
3180
  const duration = spanRunning(span) ? `${formatSpanDuration(Math.max(span.endMs, clockMs) - span.startMs)}${row.approx ? "~" : ""}` : row.durationLabel;
2933
3181
  const cost = span.costUsd ?? (row.rollupCostUsd ? row.rollupCostUsd : void 0);
2934
- return cost === void 0 ? duration : `${duration} · ${formatCost(cost)}`;
3182
+ return cost === void 0 ? duration : `${duration} · ${span.meta?.costPartial ? "≥" : ""}${formatCost(cost)}`;
2935
3183
  }
2936
3184
  /** One trace row. Geometry comes from CSS variables, so a row re-renders only when its own state changes. */
2937
3185
  const FlowRow = memo(function FlowRow(props) {
@@ -2943,9 +3191,13 @@ const FlowRow = memo(function FlowRow(props) {
2943
3191
  const invalid = row.durationLabel === "—";
2944
3192
  const isAgent = row.kind === "pipeline";
2945
3193
  const kind = !row.ok ? "failed" : row.kind;
2946
- const marks = isAgent || row.hasChildren ? folded && !agentView ? row.segments : row.own : [];
3194
+ const marks = isAgent || row.hasChildren || row.id.startsWith("run:") ? folded && !agentView ? row.segments : row.own : [];
2947
3195
  const label = endLabel(row, clockMs);
2948
3196
  const inspect = (event) => props.onInspect(row.id, event.currentTarget);
3197
+ const family = agentView && row.id.startsWith("family:");
3198
+ const step = agentView && row.stepOf !== void 0;
3199
+ const opens = family || agentView && isAgent && row.own.length > 0;
3200
+ const ending = agentView && !row.ok ? marks.map((mark) => mark.kind).lastIndexOf("failed") : -1;
2949
3201
  return /* @__PURE__ */ jsxs("div", {
2950
3202
  role: "row",
2951
3203
  "data-span-id": row.id,
@@ -2958,7 +3210,7 @@ const FlowRow = memo(function FlowRow(props) {
2958
3210
  tabIndex: focusable ? 0 : -1,
2959
3211
  "data-tone": toneKey(row.tone),
2960
3212
  "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" : ""}`,
3213
+ 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
3214
  style: {
2963
3215
  "--row-y": `${top}px`,
2964
3216
  "--fw-enter-delay": `${motion?.enter ?? 0}ms`
@@ -2968,14 +3220,30 @@ const FlowRow = memo(function FlowRow(props) {
2968
3220
  },
2969
3221
  onClick: (event) => {
2970
3222
  props.onActivate(row);
3223
+ if (family) {
3224
+ props.onToggle(row);
3225
+ return;
3226
+ }
2971
3227
  const bar = agentView ? event.currentTarget.querySelector(".fw-bar") : null;
2972
3228
  if (bar) props.onPin(row.id, bar);
2973
3229
  },
2974
3230
  children: [/* @__PURE__ */ jsxs("div", {
2975
3231
  role: "gridcell",
2976
3232
  className: "fw-label",
2977
- style: { "--depth": agentView ? 0 : row.depth },
3233
+ style: { "--depth": agentView ? row.inset ?? 0 : row.depth },
2978
3234
  children: [
3235
+ opens && /* @__PURE__ */ jsx("button", {
3236
+ type: "button",
3237
+ tabIndex: -1,
3238
+ className: `fw-toggle${props.expanded ? " fw-open" : ""}`,
3239
+ "aria-label": `${props.expanded ? "Collapse" : "Expand"} ${row.name}`,
3240
+ "aria-expanded": Boolean(props.expanded),
3241
+ onClick: (event) => {
3242
+ event.stopPropagation();
3243
+ props.onToggle(row);
3244
+ },
3245
+ children: /* @__PURE__ */ jsx(ChevronGlyph, {})
3246
+ }),
2979
3247
  !agentView && /* @__PURE__ */ jsx("span", {
2980
3248
  className: "fw-guides",
2981
3249
  "aria-hidden": true
@@ -2996,10 +3264,13 @@ const FlowRow = memo(function FlowRow(props) {
2996
3264
  className: "fw-toggle-space",
2997
3265
  "aria-hidden": true
2998
3266
  })),
2999
- !agentView && /* @__PURE__ */ jsx("span", {
3267
+ (!agentView || step) && /* @__PURE__ */ jsx("span", {
3000
3268
  className: "fw-icon-tile",
3001
3269
  "aria-hidden": true,
3002
- children: renderIcon?.(span) ?? /* @__PURE__ */ jsx(KindGlyph, { kind: span.kind })
3270
+ children: renderIcon?.(span) ?? /* @__PURE__ */ jsx(KindGlyph, {
3271
+ kind: span.kind,
3272
+ name: span.name
3273
+ })
3003
3274
  }),
3004
3275
  /* @__PURE__ */ jsxs("span", {
3005
3276
  className: "fw-names",
@@ -3014,7 +3285,7 @@ const FlowRow = memo(function FlowRow(props) {
3014
3285
  "data-outcome": span.outcome.tone,
3015
3286
  title: span.outcome.label,
3016
3287
  "aria-hidden": true
3017
- }) : agentView && /* @__PURE__ */ jsx("span", {
3288
+ }) : agentView && !step && /* @__PURE__ */ jsx("span", {
3018
3289
  className: "fw-agent-dot",
3019
3290
  "aria-hidden": true
3020
3291
  }),
@@ -3029,9 +3300,9 @@ const FlowRow = memo(function FlowRow(props) {
3029
3300
  })
3030
3301
  ]
3031
3302
  }),
3032
- agentView && span.detail && /* @__PURE__ */ jsx("span", {
3303
+ agentView && /* @__PURE__ */ jsx("span", {
3033
3304
  className: "fw-subtitle",
3034
- children: span.detail
3305
+ children: span.detail && span.detail !== props.sharedDetail ? `${span.detail} · ${agentSummary(row)}` : agentSummary(row)
3035
3306
  }),
3036
3307
  agentView && renderIcon && /* @__PURE__ */ jsx("span", {
3037
3308
  className: "fw-detail-icon",
@@ -3075,7 +3346,8 @@ const FlowRow = memo(function FlowRow(props) {
3075
3346
  onPointerLeave: props.onLeave,
3076
3347
  onClick: (event) => {
3077
3348
  event.stopPropagation();
3078
- props.onPin(row.id, event.currentTarget);
3349
+ if (family) props.onToggle(row);
3350
+ else props.onPin(row.id, event.currentTarget);
3079
3351
  },
3080
3352
  children: [!isAgent && !row.ok && /* @__PURE__ */ jsx("span", {
3081
3353
  className: "fw-glyph",
@@ -3088,7 +3360,8 @@ const FlowRow = memo(function FlowRow(props) {
3088
3360
  marks.map((mark, i) => /* @__PURE__ */ jsx(Segment, {
3089
3361
  mark,
3090
3362
  events: props,
3091
- agentView
3363
+ agentView,
3364
+ ending: i === ending
3092
3365
  }, `${mark.id}-${i}`)),
3093
3366
  /* @__PURE__ */ jsxs("span", {
3094
3367
  className: "fw-end",
@@ -3111,7 +3384,7 @@ const FlowRow = memo(function FlowRow(props) {
3111
3384
  ] })
3112
3385
  })]
3113
3386
  });
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)));
3387
+ }, (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
3388
  /** An elbow connector between two rows at one moment, drawn in the row's tone; appears when the clock reaches it. */
3116
3389
  function LinkMark({ link, rowHeight, hot }) {
3117
3390
  const upper = Math.min(link.from, link.to), lower = Math.max(link.from, link.to);
@@ -3315,7 +3588,7 @@ function FlowBody(props) {
3315
3588
  const [viewport, setViewport] = useState(maxHeight);
3316
3589
  const latest = useRef(props);
3317
3590
  latest.current = props;
3318
- const height = Math.min(maxHeight, Math.max(rows.length, 1) * rowHeight);
3591
+ const height = props.fixedHeight ? maxHeight : Math.min(maxHeight, Math.max(rows.length, 1) * rowHeight);
3319
3592
  const links = useMemo(() => linksFor(rows, props.byId, view), [
3320
3593
  rows,
3321
3594
  props.byId,
@@ -3323,6 +3596,7 @@ function FlowBody(props) {
3323
3596
  ]);
3324
3597
  const index = useMemo(() => new Map(rows.map((row, i) => [row.id, i])), [rows]);
3325
3598
  const motion = useRowMotion(rows);
3599
+ const [jumped, setJumped] = useState();
3326
3600
  const msPerPx = (win.endMs - win.startMs) / Math.max(1, trackPx);
3327
3601
  const tickEnd = win.startMs + (trackPx + (view === "spans" ? 72 : 0)) * msPerPx;
3328
3602
  const ticks = clockTicks(win.startMs, tickEnd, Math.max(1, trackPx + (view === "spans" ? 72 : 0)));
@@ -3504,9 +3778,19 @@ function FlowBody(props) {
3504
3778
  style: { "--s": marker.at },
3505
3779
  title: marker.kind ? `${marker.kind}: ${marker.label}` : marker.label,
3506
3780
  "aria-label": `Jump to ${marker.kind ? `${marker.kind} ` : ""}${marker.label}`,
3781
+ "aria-pressed": jumped === marker,
3507
3782
  onPointerDown: (event) => event.stopPropagation(),
3508
- onClick: () => props.onScrub(marker.at)
3783
+ onClick: () => {
3784
+ setJumped(marker);
3785
+ props.onScrub(marker.at);
3786
+ }
3509
3787
  }, `e-${marker.at}-${marker.label}`)),
3788
+ jumped && /* @__PURE__ */ jsx("span", {
3789
+ className: "fw-event-label",
3790
+ "data-tone": jumped.tone ?? "neutral",
3791
+ style: { "--s": jumped.at },
3792
+ children: jumped.label
3793
+ }),
3510
3794
  showCursor && /* @__PURE__ */ jsx("span", {
3511
3795
  className: "fw-cursor-handle",
3512
3796
  "aria-hidden": true
@@ -3530,6 +3814,8 @@ function FlowBody(props) {
3530
3814
  clockMs,
3531
3815
  renderIcon: props.renderIcon,
3532
3816
  fillByAgent: props.fillByAgent,
3817
+ sharedDetail: props.sharedDetail,
3818
+ expanded: props.expandedIds?.has(item.id),
3533
3819
  onToggle: props.onToggle,
3534
3820
  onInspect: props.onInspect,
3535
3821
  onLeave: props.onLeave,
@@ -3617,7 +3903,7 @@ function FlowBody(props) {
3617
3903
  })
3618
3904
  }, `${marker.at}-${marker.label}`))]
3619
3905
  }) : null,
3620
- showCursor && /* @__PURE__ */ jsx("div", {
3906
+ showCursor && props.cursorLine !== false && /* @__PURE__ */ jsx("div", {
3621
3907
  className: "fw-cursor-layer",
3622
3908
  "aria-hidden": true,
3623
3909
  children: /* @__PURE__ */ jsx("span", { className: "fw-cursor" })
@@ -3905,11 +4191,11 @@ function summaryCells(trace, at, durationMs) {
3905
4191
  errors,
3906
4192
  summary: [
3907
4193
  ["Duration", formatSpanDuration(durationMs)],
3908
- [
4194
+ ...cost.value === "—" ? [] : [[
3909
4195
  cost.label,
3910
4196
  cost.value,
3911
4197
  cost.title
3912
- ],
4198
+ ]],
3913
4199
  [
3914
4200
  "Tokens",
3915
4201
  formatCount(tokens),
@@ -3920,7 +4206,11 @@ function summaryCells(trace, at, durationMs) {
3920
4206
  modelCalls === void 0 ? "—" : modelCalls.toLocaleString(),
3921
4207
  modelCalls === void 0 ? "Model output windows do not establish the number of LLM requests" : void 0
3922
4208
  ],
3923
- ["Tool calls", (at ? at.toolCalls : trace.toolCalls).toLocaleString()],
4209
+ [
4210
+ "Tool calls",
4211
+ (at ? at.toolCalls : trace.toolCalls)?.toLocaleString() ?? "—",
4212
+ (at ? at.toolCalls : trace.toolCalls) === void 0 ? "The producer did not count tool calls" : void 0
4213
+ ],
3924
4214
  ["Errors", errors.toLocaleString()]
3925
4215
  ]
3926
4216
  };
@@ -3939,8 +4229,7 @@ function KindLegend({ rows }) {
3939
4229
  ["model", "Model call"],
3940
4230
  ["tool", "Tool call"],
3941
4231
  ["waiting", "Waiting"],
3942
- ["handoff", "Handoff"],
3943
- ["failed", "Failed turn"]
4232
+ ["handoff", "Handoff"]
3944
4233
  ].filter(([kind]) => kind === "waiting" ? rows.some((row) => row.kind === "pipeline") : kind === "handoff" ? rows.some((row) => row.span.handoffTo || row.hasAgents) : has(kind));
3945
4234
  if (shown.length === 0) return null;
3946
4235
  return /* @__PURE__ */ jsx("ul", {
@@ -3992,7 +4281,7 @@ function OutcomeLegend({ agents }) {
3992
4281
  }
3993
4282
  /** Shared trace inspector: the agent timeline and the span waterfall over one clock.
3994
4283
  * 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 }) {
4284
+ 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
4285
  const root = useRef(null);
3997
4286
  const search = useRef(null);
3998
4287
  const slider = useRef(null);
@@ -4065,17 +4354,22 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
4065
4354
  const pipelines = rows.filter((row) => row.kind === "pipeline");
4066
4355
  return pipelines.length ? pipelines : rows.filter((row) => !row.parentId);
4067
4356
  }, [rows]);
4068
- const visible = useMemo(() => view === "agents" ? agents : visibleFlowWaterfallRows(rows, query, collapsed), [
4357
+ const sharedDetail = agents.length > 1 && agents.every((row) => row.span.detail && row.span.detail === agents[0]?.span.detail) ? agents[0]?.span.detail : void 0;
4358
+ const [openFamilies, setOpenFamilies] = useState(/* @__PURE__ */ new Set());
4359
+ const [openSteps, setOpenSteps] = useState(/* @__PURE__ */ new Set());
4360
+ const visible = useMemo(() => view === "agents" ? agentViewRows(agents, rows, openFamilies, openSteps) : visibleFlowWaterfallRows(rows, query, collapsed), [
4069
4361
  view,
4070
4362
  agents,
4071
4363
  rows,
4364
+ openFamilies,
4365
+ openSteps,
4072
4366
  query,
4073
4367
  collapsed
4074
4368
  ]);
4075
4369
  const slowest = view === "spans" ? slowestRow(rows, clockMs) : void 0;
4076
- const callouts = useMemo(() => view === "agents" ? agentCallouts(agents, clockMs) : {}, [
4370
+ const callouts = useMemo(() => view === "agents" ? agentCallouts(visible, clockMs) : {}, [
4077
4371
  view,
4078
- agents,
4372
+ visible,
4079
4373
  clockMs
4080
4374
  ]);
4081
4375
  const rowsHeight = maxHeight ?? (view === "agents" ? ROW_HEIGHT.agents * 20 : 640);
@@ -4177,12 +4471,14 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
4177
4471
  inspect: (_target, _element) => {}
4178
4472
  });
4179
4473
  const toggleNow = (row) => {
4474
+ const flip = (open) => new Set(open.has(row.id) ? [...open].filter((id) => id !== row.id) : [...open, row.id]);
4475
+ if (row.id.startsWith("family:")) {
4476
+ setOpenFamilies(flip);
4477
+ return;
4478
+ }
4180
4479
  onSelect?.(row.span);
4181
4480
  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");
4481
+ setOpenSteps(flip);
4186
4482
  setActiveId(row.id);
4187
4483
  return;
4188
4484
  }
@@ -4265,11 +4561,13 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
4265
4561
  const busy = agents.reduce((sum, row) => sum + busyMs(row, limit), 0);
4266
4562
  const speedup = limit > 0 ? busy / limit : 0;
4267
4563
  const done = !isLive && !trace.spans.some(spanRunning);
4268
- const cardRow = card ? byId.get(card.target) : void 0;
4564
+ const shown = (row) => row && drawnRow(row, visible);
4565
+ const cardRow = shown(card ? byId.get(card.target) : void 0);
4269
4566
  const cardAgent = cardRow && (() => {
4270
- for (let cursor = cardRow; cursor; cursor = cursor.parentId ? byId.get(cursor.parentId) : void 0) if (cursor.kind === "pipeline") return cursor;
4567
+ for (let cursor = cardRow; cursor; cursor = cursor.parentId ? byId.get(cursor.parentId) : void 0) if (cursor.kind === "pipeline") return shown(cursor);
4271
4568
  })();
4272
- const showCursor = isLive || position !== null || playing;
4569
+ const moving = isLive || position !== null || playing;
4570
+ const showCursor = moving || view === "agents";
4273
4571
  const overviewMarks = useMemo(() => rows.filter((row) => !row.parentId).flatMap((row) => row.hasChildren ? row.segments : row.kind === "pipeline" ? [] : [{
4274
4572
  startMs: row.span.startMs,
4275
4573
  endMs: row.span.endMs,
@@ -4356,6 +4654,7 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
4356
4654
  agents.length,
4357
4655
  " ",
4358
4656
  agents.length === 1 ? "agent" : "agents",
4657
+ sharedDetail ? ` · ${sharedDetail}` : "",
4359
4658
  done && speedup >= 1.05 ? ` · ${speedup.toFixed(1)}x parallel` : ""
4360
4659
  ]
4361
4660
  }),
@@ -4413,7 +4712,7 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
4413
4712
  })]
4414
4713
  })]
4415
4714
  }),
4416
- /* @__PURE__ */ jsx("dl", {
4715
+ showSummary && /* @__PURE__ */ jsx("dl", {
4417
4716
  className: "fw-summary",
4418
4717
  children: summary.map(([name, value, title]) => /* @__PURE__ */ jsxs("div", {
4419
4718
  className: name === "Errors" && errors > 0 ? "fw-has-errors" : void 0,
@@ -4425,6 +4724,7 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
4425
4724
  }, name))
4426
4725
  }),
4427
4726
  view === "agents" && playback,
4727
+ view === "agents" && /* @__PURE__ */ jsx(KindLegend, { rows }),
4428
4728
  /* @__PURE__ */ jsx(FlowBody, {
4429
4729
  rows: visible,
4430
4730
  byId,
@@ -4434,6 +4734,10 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
4434
4734
  collapsed,
4435
4735
  searching: Boolean(query.trim()),
4436
4736
  pauses: trace.pauses,
4737
+ cursorLine: moving,
4738
+ sharedDetail,
4739
+ expandedIds: view === "agents" ? /* @__PURE__ */ new Set([...openFamilies, ...openSteps]) : void 0,
4740
+ fixedHeight: isLive,
4437
4741
  slowestId: slowest?.id ?? callouts.slowestId,
4438
4742
  costliestId: callouts.costliestId,
4439
4743
  activeId,
@@ -4483,7 +4787,6 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
4483
4787
  compact: view === "agents"
4484
4788
  })
4485
4789
  }),
4486
- view === "agents" && /* @__PURE__ */ jsx(KindLegend, { rows }),
4487
4790
  view === "spans" && playback
4488
4791
  ]
4489
4792
  });
@@ -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.3",
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",