@tangle-network/charts 0.20.0 → 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,6 +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
+ /** `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';
385
388
  /** How many model calls `costUsd` covers; when `priced < total` the total is a lower bound and shown as one. */
386
389
  costCoverage?: {
387
390
  priced: number;
@@ -389,7 +392,8 @@ interface FlowTrace {
389
392
  };
390
393
  /** The real charge when the producer knows it (for example $0 on a subscription seat), shown beside a list price. */
391
394
  billedUsd?: number;
392
- toolCalls: number;
395
+ /** Tool calls the producer counted; absent means unknown and is shown as a dash, never as zero. */
396
+ toolCalls?: number;
393
397
  /** Authoritative LLM request count when supplied; absent means unknown. */
394
398
  modelCalls?: number;
395
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-Sr8GtLm0.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
@@ -3337,6 +3337,13 @@ function unitDotLayout(groups, width, height, o = {}) {
3337
3337
  }
3338
3338
  //#endregion
3339
3339
  //#region src/otlp-trace.ts
3340
+ /** Attributes that carry cached input apart from fresh input (OpenTelemetry GenAI and Anthropic-style names). */
3341
+ const CACHE_KEYS = [
3342
+ "gen_ai.usage.cache_read_input_tokens",
3343
+ "gen_ai.usage.cache_creation_input_tokens",
3344
+ "gen_ai.usage.input_tokens.cached",
3345
+ "tangle.llm.cache_read_tokens"
3346
+ ];
3340
3347
  /** Price model calls from an OpenAI-compatible model catalog (`data[].pricing.prompt|completion`, USD per token), such as
3341
3348
  * the Router's `/v1/models`. Read the catalog at runtime; never copy its numbers. */
3342
3349
  function catalogPrice(catalog) {
@@ -3475,6 +3482,7 @@ function otlpFlowTrace(input, options = {}) {
3475
3482
  let cost = 0;
3476
3483
  let unpriced = 0;
3477
3484
  let priced = 0;
3485
+ let cacheSplit = false;
3478
3486
  const offset = (iso) => time(iso) - origin;
3479
3487
  const isSpawn = (span) => span.kind === "TOOL" && (attrs(span)["traces.agent.spawn"] === true || /spawn/i.test(str(attrs(span)["gen_ai.tool.name"]) ?? span.name));
3480
3488
  const groupOfSpan = (span) => groupOf.get(rootOf(span).span_id);
@@ -3541,6 +3549,7 @@ function otlpFlowTrace(input, options = {}) {
3541
3549
  if (listPrice !== void 0) {
3542
3550
  cost += listPrice;
3543
3551
  priced++;
3552
+ if (CACHE_KEYS.some((key) => a[key] !== void 0)) cacheSplit = true;
3544
3553
  } else if (kind === "model" && !aggregate && (prompt || completion)) unpriced++;
3545
3554
  out.push({
3546
3555
  id: span.span_id,
@@ -3574,7 +3583,8 @@ function otlpFlowTrace(input, options = {}) {
3574
3583
  costCoverage: {
3575
3584
  priced,
3576
3585
  total: priced + unpriced
3577
- }
3586
+ },
3587
+ ...cacheSplit ? {} : { costBound: "upper" }
3578
3588
  } : {}
3579
3589
  };
3580
3590
  }
@@ -62,12 +62,12 @@
62
62
  .fw-root [data-tone="3"] { --tone: var(--fw-h3); }
63
63
  .fw-root [data-tone="4"] { --tone: var(--fw-h4); }
64
64
  .fw-root [data-tone="5"] { --tone: var(--fw-h5); }
65
- /* Light fills carry enough of the hue to tell agents apart on white (round 8: 24% read as "pale mint on gray"); the outline is
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) 42%, #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) 22%, #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,6 +233,13 @@
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; }
236
+ .fw-root .fw-kinds { margin: 0; padding: 4px 24px 8px; justify-content: flex-start; color: var(--fw-text); }
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)); }
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); }
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); }
240
+ .fw-root .fw-kinds .fw-swatch-handoff { width: 10px; height: 8px; border-left: 1.5px solid var(--fw-muted); border-bottom: 1.5px solid var(--fw-muted); border-bottom-left-radius: 3px; }
241
+ .fw-root .fw-kinds .fw-swatch-failed { width: 12px; height: 8px; border-radius: 2px; box-shadow: inset 0 0 0 1px var(--fw-failed); }
242
+ .fw-root .fw-description { margin: 0; color: var(--fw-muted); font-size: 14px; line-height: 20px; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
234
243
  .fw-root .fw-legend .fw-swatch { width: 12px; height: 8px; border-radius: 2px; background: var(--fill); box-shadow: inset 0 0 0 1px var(--line); }
235
244
  .fw-root .fw-legend b { color: var(--fw-text); font-weight: 600; font-variant-numeric: tabular-nums; }
236
245
  .fw-root .fw-legend .fw-swatch-step { display: inline-grid; place-items: center; width: 12px; height: 12px; border-radius: 3px; background: var(--fw-well); color: var(--fw-failed); box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--fw-failed) 50%, transparent); }
@@ -241,11 +250,13 @@
241
250
  .fw-root .fw-seg-model { background: var(--fill); box-shadow: inset 0 0 0 1px var(--line); }
242
251
  .fw-root .fw-seg-tool { background: var(--dim); box-shadow: inset 0 0 0 1px var(--dim-line); }
243
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); }
244
- .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); }
245
256
  .fw-root .fw-glyph { display: inline-flex; color: currentColor; }
246
257
  .fw-root .fw-glyph svg { width: 11px; height: 11px; stroke-width: 2; }
247
258
  .fw-root .fw-seg-model .fw-glyph, .fw-root .fw-k-model .fw-glyph { color: light-dark(var(--tone), #fff); }
248
- .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; }
249
260
  .fw-root .fw-duration { font-size: 12px; color: var(--fw-muted); font-variant-numeric: tabular-nums; }
250
261
  .fw-root .fw-k-failed ~ .fw-end .fw-duration { color: var(--fw-failed); }
251
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); }
@@ -258,17 +269,25 @@
258
269
  opacity: clamp(0, (var(--fw-t) - var(--s)) * 1000, 1);
259
270
  }
260
271
  .fw-root .fw-view-spans .fw-link { opacity: calc(clamp(0, (var(--fw-t) - var(--s)) * 1000, 1) * .7); }
261
- /* Agent-view connectors stay quiet at 30% until their agent is hovered, pinned or focused. */
262
- .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); }
263
275
  .fw-root .fw-link { transition: opacity 160ms var(--fw-ease); }
264
- .fw-root .fw-link-down { left: calc(var(--x) - 8px); border-left-width: 1.5px; border-bottom-left-radius: 6px; }
265
- .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; }
266
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); }
267
279
  .fw-root .fw-link-down::before { left: -3.25px; }
268
280
  .fw-root .fw-link-up::before { right: -3.25px; }
269
281
 
270
282
  /* Span card */
271
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; }
272
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);
273
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); }
274
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-Sr8GtLm0.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";
@@ -175,6 +175,7 @@ export declare function HBars({ rows, format, color, max, limit }: {
175
175
  note?: string;
176
176
  color?: string;
177
177
  href?: string;
178
+ title?: string;
178
179
  }[];
179
180
  format?: (v: number) => string;
180
181
  color?: string;
@@ -376,14 +377,18 @@ interface FlowWaterfallProps {
376
377
  /** What a bar's color means: the agent (default), whose hue its steps and handoffs share, with the outcome on the row dot, legend and
377
378
  * crash glyph; or its `outcome`. */
378
379
  agentFill?: 'outcome' | 'agent';
380
+ /** One line beside the title saying what the run was for, such as its goal. */
381
+ description?: string;
379
382
  /** The row the host has selected, such as the agent whose transcript is open: it lifts and, in the agent view, its peers dim.
380
383
  * A pinned card alone only highlights its row, so the rest of the run stays readable around it. */
381
384
  selectedId?: string;
382
385
  /** Escape clears the host's selection, after closing any open card. */
383
386
  onDeselect?: () => void;
387
+ /** Show the stat strip (default); a page with its own stat row passes false. */
388
+ summary?: boolean;
384
389
  }
385
390
  /** Shared trace inspector: the agent timeline and the span waterfall over one clock.
386
391
  * Data remains producer-owned; playback, zoom and folding change presentation only. */
387
- export declare function FlowWaterfall({ trace, live, startedAt, label, defaultCollapsed, defaultView, renderIcon, maxHeight, onSelect, markers, onTimeChange, agentFill, selectedId, onDeselect }: 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;
388
393
  //#endregion
389
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 };
@@ -1001,10 +1001,10 @@ function HBars({ rows, format = formatCompact, color = LIVE_SERIES[0], max, limi
1001
1001
  return /* @__PURE__ */ jsxs("ol", {
1002
1002
  className: "ov-hbars",
1003
1003
  children: [rows.slice(0, limit).map((row) => {
1004
+ const title = row.title ?? `${row.label}: ${format(row.value)}`;
1004
1005
  const content = /* @__PURE__ */ jsxs(Fragment, { children: [
1005
1006
  /* @__PURE__ */ jsx("span", {
1006
1007
  className: "ov-hbar-label",
1007
- title: row.label,
1008
1008
  children: row.label
1009
1009
  }),
1010
1010
  /* @__PURE__ */ jsx("span", {
@@ -1025,8 +1025,12 @@ function HBars({ rows, format = formatCompact, color = LIVE_SERIES[0], max, limi
1025
1025
  ] });
1026
1026
  return /* @__PURE__ */ jsx("li", { children: row.href ? /* @__PURE__ */ jsx("a", {
1027
1027
  href: row.href,
1028
+ title,
1028
1029
  children: content
1029
- }) : /* @__PURE__ */ jsx("div", { children: content }) }, row.label);
1030
+ }) : /* @__PURE__ */ jsx("div", {
1031
+ title,
1032
+ children: content
1033
+ }) }, row.label);
1030
1034
  }), rest.length > 0 && /* @__PURE__ */ jsxs("li", {
1031
1035
  className: "ov-hbars-more",
1032
1036
  children: [
@@ -2437,7 +2441,17 @@ function flowWaterfallRows(trace, resolutionMs) {
2437
2441
  if (childCost === void 0) {
2438
2442
  if (child.kind === "model" || child.hasChildren) costKnown = false;
2439
2443
  } else cost += childCost;
2440
- 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
+ }
2441
2455
  }
2442
2456
  if (row.hasChildren && costKnown) row.rollupCostUsd = cost;
2443
2457
  if (row.hasChildren) {
@@ -2556,7 +2570,7 @@ function totalsAt(trace, clockMs) {
2556
2570
  costUsd: trace.costUsd === void 0 ? void 0 : costs ? cost : trace.costUsd,
2557
2571
  tokens: counted ? tokens : trace.promptTokens + trace.completionTokens,
2558
2572
  modelCalls: trace.modelCalls === void 0 ? void 0 : models,
2559
- toolCalls: tools,
2573
+ toolCalls: trace.toolCalls === void 0 ? void 0 : tools,
2560
2574
  errors
2561
2575
  };
2562
2576
  }
@@ -2575,16 +2589,197 @@ function costSummary(costUsd, trace) {
2575
2589
  };
2576
2590
  const coverage = trace.costCoverage;
2577
2591
  const partial = coverage !== void 0 && coverage.priced < coverage.total;
2592
+ const upper = !partial && trace.costBound === "upper";
2578
2593
  return {
2579
- value: `${partial ? "≥" : 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` : ""}`,
2580
2595
  title: [
2581
2596
  partial ? `${coverage.priced} of ${coverage.total} model calls priced` : coverage ? `all ${coverage.total} model calls priced` : "",
2582
2597
  list ? "at the model catalog list price; not a bill" : "",
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" : "",
2583
2600
  trace.billedUsd !== void 0 ? `billed ${formatCost(trace.billedUsd)} (the seat charge)` : ""
2584
2601
  ].filter(Boolean).join("; "),
2585
2602
  label
2586
2603
  };
2587
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
+ }
2588
2783
  //#endregion
2589
2784
  //#region src/react/flow-waterfall-icons.tsx
2590
2785
  const svg = (children, label) => /* @__PURE__ */ jsx("svg", {
@@ -2602,7 +2797,52 @@ const svg = (children, label) => /* @__PURE__ */ jsx("svg", {
2602
2797
  children
2603
2798
  });
2604
2799
  /** Kind glyphs drawn when the host supplies no `renderIcon`. Brand marks belong to the host. */
2605
- 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
+ }
2606
2846
  switch (kind) {
2607
2847
  case "pipeline": return svg(/* @__PURE__ */ jsxs(Fragment, { children: [
2608
2848
  /* @__PURE__ */ jsx("circle", {
@@ -2684,6 +2924,8 @@ const ZoomOutGlyph = () => svg(/* @__PURE__ */ jsxs(Fragment, { children: [/* @_
2684
2924
  const FitGlyph = () => svg(/* @__PURE__ */ jsx("path", { d: "M2.5 6V3.5a1 1 0 0 1 1-1H6M10 2.5h2.5a1 1 0 0 1 1 1V6M13.5 10v2.5a1 1 0 0 1-1 1H10M6 13.5H3.5a1 1 0 0 1-1-1V10" }));
2685
2925
  //#endregion
2686
2926
  //#region src/react/flow-waterfall-card.tsx
2927
+ /** Names wrap between words, never at their hyphens ("memscale-engineer-" / "2" read as two names). */
2928
+ const keepWords = (name) => name.replace(/-/g, "‑");
2687
2929
  const KIND_LABEL = {
2688
2930
  pipeline: "Agent",
2689
2931
  model: "LLM call",
@@ -2724,7 +2966,7 @@ function SpanCard({ id, row, agent, clockMs, left, top, pinned, onClose, renderI
2724
2966
  if (cost !== void 0) cells.push(["Cost", formatCost(cost)]);
2725
2967
  cells.splice(6);
2726
2968
  const kind = span.kind === "model" ? "Thinking" : KIND_LABEL[span.kind];
2727
- 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;
2728
2970
  const sentence = !owner && span.output && span.output !== who.name ? span.output : void 0;
2729
2971
  return /* @__PURE__ */ jsxs("div", {
2730
2972
  id,
@@ -2753,7 +2995,7 @@ function SpanCard({ id, row, agent, clockMs, left, top, pinned, onClose, renderI
2753
2995
  }),
2754
2996
  /* @__PURE__ */ jsx("span", {
2755
2997
  className: "fw-card-who",
2756
- children: who.name
2998
+ children: keepWords(who.name)
2757
2999
  }),
2758
3000
  outcome ? /* @__PURE__ */ jsx("span", {
2759
3001
  className: "fw-pill fw-pill-outcome",
@@ -2834,13 +3076,16 @@ function SpanCard({ id, row, agent, clockMs, left, top, pinned, onClose, renderI
2834
3076
  /* @__PURE__ */ jsx("span", {
2835
3077
  className: "fw-icon-tile",
2836
3078
  "aria-hidden": true,
2837
- 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
+ })
2838
3083
  }),
2839
3084
  /* @__PURE__ */ jsxs("div", {
2840
3085
  className: "fw-card-title",
2841
3086
  children: [/* @__PURE__ */ jsx("div", {
2842
3087
  className: "fw-card-name",
2843
- children: span.name
3088
+ children: keepWords(span.name)
2844
3089
  }), span.detail && /* @__PURE__ */ jsx("div", {
2845
3090
  className: "fw-muted",
2846
3091
  children: span.detail
@@ -2897,10 +3142,10 @@ const at = (startMs, endMs) => ({
2897
3142
  "--s": startMs,
2898
3143
  "--e": Number.isFinite(endMs) ? endMs : OPEN_END
2899
3144
  });
2900
- function Segment({ mark, events, agentView }) {
2901
- const glyph = mark.kind === "failed" ? /* @__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;
2902
3147
  return /* @__PURE__ */ jsx("span", {
2903
- className: `fw-seg fw-seg-${mark.kind}`,
3148
+ className: `fw-seg fw-seg-${mark.kind}${mark.alt ? " fw-seg-alt" : ""}`,
2904
3149
  "data-tone": toneKey(mark.tone),
2905
3150
  style: at(mark.startMs, mark.endMs),
2906
3151
  onPointerEnter: agentView ? (event) => events.onInspect(mark.id, event.currentTarget) : void 0,
@@ -2923,7 +3168,7 @@ function endLabel(row, clockMs) {
2923
3168
  if (!spanRunning(span) && clockMs < span.endMs) return `${formatSpanDuration(Math.max(0, clockMs - span.startMs))}${row.approx ? "~" : ""}`;
2924
3169
  const duration = spanRunning(span) ? `${formatSpanDuration(Math.max(span.endMs, clockMs) - span.startMs)}${row.approx ? "~" : ""}` : row.durationLabel;
2925
3170
  const cost = span.costUsd ?? (row.rollupCostUsd ? row.rollupCostUsd : void 0);
2926
- return cost === void 0 ? duration : `${duration} · ${formatCost(cost)}`;
3171
+ return cost === void 0 ? duration : `${duration} · ${span.meta?.costPartial ? "≥" : ""}${formatCost(cost)}`;
2927
3172
  }
2928
3173
  /** One trace row. Geometry comes from CSS variables, so a row re-renders only when its own state changes. */
2929
3174
  const FlowRow = memo(function FlowRow(props) {
@@ -2935,9 +3180,13 @@ const FlowRow = memo(function FlowRow(props) {
2935
3180
  const invalid = row.durationLabel === "—";
2936
3181
  const isAgent = row.kind === "pipeline";
2937
3182
  const kind = !row.ok ? "failed" : row.kind;
2938
- 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 : [];
2939
3184
  const label = endLabel(row, clockMs);
2940
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;
2941
3190
  return /* @__PURE__ */ jsxs("div", {
2942
3191
  role: "row",
2943
3192
  "data-span-id": row.id,
@@ -2950,7 +3199,7 @@ const FlowRow = memo(function FlowRow(props) {
2950
3199
  tabIndex: focusable ? 0 : -1,
2951
3200
  "data-tone": toneKey(row.tone),
2952
3201
  "data-outcome": fillByAgent ? void 0 : span.outcome?.tone,
2953
- 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" : ""}`,
2954
3203
  style: {
2955
3204
  "--row-y": `${top}px`,
2956
3205
  "--fw-enter-delay": `${motion?.enter ?? 0}ms`
@@ -2960,14 +3209,30 @@ const FlowRow = memo(function FlowRow(props) {
2960
3209
  },
2961
3210
  onClick: (event) => {
2962
3211
  props.onActivate(row);
3212
+ if (family) {
3213
+ props.onToggle(row);
3214
+ return;
3215
+ }
2963
3216
  const bar = agentView ? event.currentTarget.querySelector(".fw-bar") : null;
2964
3217
  if (bar) props.onPin(row.id, bar);
2965
3218
  },
2966
3219
  children: [/* @__PURE__ */ jsxs("div", {
2967
3220
  role: "gridcell",
2968
3221
  className: "fw-label",
2969
- style: { "--depth": agentView ? 0 : row.depth },
3222
+ style: { "--depth": agentView ? row.inset ?? 0 : row.depth },
2970
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
+ }),
2971
3236
  !agentView && /* @__PURE__ */ jsx("span", {
2972
3237
  className: "fw-guides",
2973
3238
  "aria-hidden": true
@@ -2988,10 +3253,13 @@ const FlowRow = memo(function FlowRow(props) {
2988
3253
  className: "fw-toggle-space",
2989
3254
  "aria-hidden": true
2990
3255
  })),
2991
- !agentView && /* @__PURE__ */ jsx("span", {
3256
+ (!agentView || step) && /* @__PURE__ */ jsx("span", {
2992
3257
  className: "fw-icon-tile",
2993
3258
  "aria-hidden": true,
2994
- 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
+ })
2995
3263
  }),
2996
3264
  /* @__PURE__ */ jsxs("span", {
2997
3265
  className: "fw-names",
@@ -3006,7 +3274,7 @@ const FlowRow = memo(function FlowRow(props) {
3006
3274
  "data-outcome": span.outcome.tone,
3007
3275
  title: span.outcome.label,
3008
3276
  "aria-hidden": true
3009
- }) : agentView && /* @__PURE__ */ jsx("span", {
3277
+ }) : agentView && !step && /* @__PURE__ */ jsx("span", {
3010
3278
  className: "fw-agent-dot",
3011
3279
  "aria-hidden": true
3012
3280
  }),
@@ -3021,9 +3289,9 @@ const FlowRow = memo(function FlowRow(props) {
3021
3289
  })
3022
3290
  ]
3023
3291
  }),
3024
- agentView && span.detail && /* @__PURE__ */ jsx("span", {
3292
+ agentView && /* @__PURE__ */ jsx("span", {
3025
3293
  className: "fw-subtitle",
3026
- children: span.detail
3294
+ children: span.detail && span.detail !== props.sharedDetail ? `${span.detail} · ${agentSummary(row)}` : agentSummary(row)
3027
3295
  }),
3028
3296
  agentView && renderIcon && /* @__PURE__ */ jsx("span", {
3029
3297
  className: "fw-detail-icon",
@@ -3067,7 +3335,8 @@ const FlowRow = memo(function FlowRow(props) {
3067
3335
  onPointerLeave: props.onLeave,
3068
3336
  onClick: (event) => {
3069
3337
  event.stopPropagation();
3070
- props.onPin(row.id, event.currentTarget);
3338
+ if (family) props.onToggle(row);
3339
+ else props.onPin(row.id, event.currentTarget);
3071
3340
  },
3072
3341
  children: [!isAgent && !row.ok && /* @__PURE__ */ jsx("span", {
3073
3342
  className: "fw-glyph",
@@ -3080,7 +3349,8 @@ const FlowRow = memo(function FlowRow(props) {
3080
3349
  marks.map((mark, i) => /* @__PURE__ */ jsx(Segment, {
3081
3350
  mark,
3082
3351
  events: props,
3083
- agentView
3352
+ agentView,
3353
+ ending: i === ending
3084
3354
  }, `${mark.id}-${i}`)),
3085
3355
  /* @__PURE__ */ jsxs("span", {
3086
3356
  className: "fw-end",
@@ -3103,7 +3373,7 @@ const FlowRow = memo(function FlowRow(props) {
3103
3373
  ] })
3104
3374
  })]
3105
3375
  });
3106
- }, (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)));
3107
3377
  /** An elbow connector between two rows at one moment, drawn in the row's tone; appears when the clock reaches it. */
3108
3378
  function LinkMark({ link, rowHeight, hot }) {
3109
3379
  const upper = Math.min(link.from, link.to), lower = Math.max(link.from, link.to);
@@ -3307,7 +3577,7 @@ function FlowBody(props) {
3307
3577
  const [viewport, setViewport] = useState(maxHeight);
3308
3578
  const latest = useRef(props);
3309
3579
  latest.current = props;
3310
- 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);
3311
3581
  const links = useMemo(() => linksFor(rows, props.byId, view), [
3312
3582
  rows,
3313
3583
  props.byId,
@@ -3315,6 +3585,7 @@ function FlowBody(props) {
3315
3585
  ]);
3316
3586
  const index = useMemo(() => new Map(rows.map((row, i) => [row.id, i])), [rows]);
3317
3587
  const motion = useRowMotion(rows);
3588
+ const [jumped, setJumped] = useState();
3318
3589
  const msPerPx = (win.endMs - win.startMs) / Math.max(1, trackPx);
3319
3590
  const tickEnd = win.startMs + (trackPx + (view === "spans" ? 72 : 0)) * msPerPx;
3320
3591
  const ticks = clockTicks(win.startMs, tickEnd, Math.max(1, trackPx + (view === "spans" ? 72 : 0)));
@@ -3496,9 +3767,19 @@ function FlowBody(props) {
3496
3767
  style: { "--s": marker.at },
3497
3768
  title: marker.kind ? `${marker.kind}: ${marker.label}` : marker.label,
3498
3769
  "aria-label": `Jump to ${marker.kind ? `${marker.kind} ` : ""}${marker.label}`,
3770
+ "aria-pressed": jumped === marker,
3499
3771
  onPointerDown: (event) => event.stopPropagation(),
3500
- onClick: () => props.onScrub(marker.at)
3772
+ onClick: () => {
3773
+ setJumped(marker);
3774
+ props.onScrub(marker.at);
3775
+ }
3501
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
+ }),
3502
3783
  showCursor && /* @__PURE__ */ jsx("span", {
3503
3784
  className: "fw-cursor-handle",
3504
3785
  "aria-hidden": true
@@ -3522,6 +3803,8 @@ function FlowBody(props) {
3522
3803
  clockMs,
3523
3804
  renderIcon: props.renderIcon,
3524
3805
  fillByAgent: props.fillByAgent,
3806
+ sharedDetail: props.sharedDetail,
3807
+ expanded: props.expandedIds?.has(item.id),
3525
3808
  onToggle: props.onToggle,
3526
3809
  onInspect: props.onInspect,
3527
3810
  onLeave: props.onLeave,
@@ -3609,7 +3892,7 @@ function FlowBody(props) {
3609
3892
  })
3610
3893
  }, `${marker.at}-${marker.label}`))]
3611
3894
  }) : null,
3612
- showCursor && /* @__PURE__ */ jsx("div", {
3895
+ showCursor && props.cursorLine !== false && /* @__PURE__ */ jsx("div", {
3613
3896
  className: "fw-cursor-layer",
3614
3897
  "aria-hidden": true,
3615
3898
  children: /* @__PURE__ */ jsx("span", { className: "fw-cursor" })
@@ -3897,11 +4180,11 @@ function summaryCells(trace, at, durationMs) {
3897
4180
  errors,
3898
4181
  summary: [
3899
4182
  ["Duration", formatSpanDuration(durationMs)],
3900
- [
4183
+ ...cost.value === "—" ? [] : [[
3901
4184
  cost.label,
3902
4185
  cost.value,
3903
4186
  cost.title
3904
- ],
4187
+ ]],
3905
4188
  [
3906
4189
  "Tokens",
3907
4190
  formatCount(tokens),
@@ -3912,7 +4195,11 @@ function summaryCells(trace, at, durationMs) {
3912
4195
  modelCalls === void 0 ? "—" : modelCalls.toLocaleString(),
3913
4196
  modelCalls === void 0 ? "Model output windows do not establish the number of LLM requests" : void 0
3914
4197
  ],
3915
- ["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
+ ],
3916
4203
  ["Errors", errors.toLocaleString()]
3917
4204
  ]
3918
4205
  };
@@ -3924,6 +4211,25 @@ const TONE_ORDER = [
3924
4211
  "failed",
3925
4212
  "neutral"
3926
4213
  ];
4214
+ /** What each mark means, as the agent view draws it; a key only for kinds the run has. */
4215
+ function KindLegend({ rows }) {
4216
+ const has = (kind) => rows.some((row) => row.own.some((mark) => mark.kind === kind) || row.span.kind === kind);
4217
+ const shown = [
4218
+ ["model", "Model call"],
4219
+ ["tool", "Tool call"],
4220
+ ["waiting", "Waiting"],
4221
+ ["handoff", "Handoff"]
4222
+ ].filter(([kind]) => kind === "waiting" ? rows.some((row) => row.kind === "pipeline") : kind === "handoff" ? rows.some((row) => row.span.handoffTo || row.hasAgents) : has(kind));
4223
+ if (shown.length === 0) return null;
4224
+ return /* @__PURE__ */ jsx("ul", {
4225
+ className: "fw-legend fw-kinds",
4226
+ "aria-label": "Key",
4227
+ children: shown.map(([kind, name]) => /* @__PURE__ */ jsxs("li", { children: [/* @__PURE__ */ jsx("span", {
4228
+ className: `fw-swatch-${kind === "waiting" ? "wait" : kind}`,
4229
+ "aria-hidden": true
4230
+ }), name] }, kind))
4231
+ });
4232
+ }
3927
4233
  function OutcomeLegend({ agents }) {
3928
4234
  const items = /* @__PURE__ */ new Map();
3929
4235
  for (const row of agents) {
@@ -3964,7 +4270,7 @@ function OutcomeLegend({ agents }) {
3964
4270
  }
3965
4271
  /** Shared trace inspector: the agent timeline and the span waterfall over one clock.
3966
4272
  * Data remains producer-owned; playback, zoom and folding change presentation only. */
3967
- function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defaultCollapsed = false, defaultView = "spans", renderIcon, maxHeight, onSelect, markers, onTimeChange, agentFill = "agent", selectedId, onDeselect }) {
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 }) {
3968
4274
  const root = useRef(null);
3969
4275
  const search = useRef(null);
3970
4276
  const slider = useRef(null);
@@ -4037,17 +4343,22 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
4037
4343
  const pipelines = rows.filter((row) => row.kind === "pipeline");
4038
4344
  return pipelines.length ? pipelines : rows.filter((row) => !row.parentId);
4039
4345
  }, [rows]);
4040
- 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), [
4041
4350
  view,
4042
4351
  agents,
4043
4352
  rows,
4353
+ openFamilies,
4354
+ openSteps,
4044
4355
  query,
4045
4356
  collapsed
4046
4357
  ]);
4047
4358
  const slowest = view === "spans" ? slowestRow(rows, clockMs) : void 0;
4048
- const callouts = useMemo(() => view === "agents" ? agentCallouts(agents, clockMs) : {}, [
4359
+ const callouts = useMemo(() => view === "agents" ? agentCallouts(visible, clockMs) : {}, [
4049
4360
  view,
4050
- agents,
4361
+ visible,
4051
4362
  clockMs
4052
4363
  ]);
4053
4364
  const rowsHeight = maxHeight ?? (view === "agents" ? ROW_HEIGHT.agents * 20 : 640);
@@ -4149,12 +4460,14 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
4149
4460
  inspect: (_target, _element) => {}
4150
4461
  });
4151
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
+ }
4152
4468
  onSelect?.(row.span);
4153
4469
  if (view === "agents") {
4154
- const keep = /* @__PURE__ */ new Set();
4155
- for (let cursor = row; cursor; cursor = cursor.parentId ? byId.get(cursor.parentId) : void 0) keep.add(cursor.id);
4156
- setCollapsed(new Set(rows.filter((item) => item.hasChildren && !keep.has(item.id) && item.kind === "pipeline").map((item) => item.id)));
4157
- setView("spans");
4470
+ setOpenSteps(flip);
4158
4471
  setActiveId(row.id);
4159
4472
  return;
4160
4473
  }
@@ -4237,11 +4550,13 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
4237
4550
  const busy = agents.reduce((sum, row) => sum + busyMs(row, limit), 0);
4238
4551
  const speedup = limit > 0 ? busy / limit : 0;
4239
4552
  const done = !isLive && !trace.spans.some(spanRunning);
4240
- 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);
4241
4555
  const cardAgent = cardRow && (() => {
4242
- 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);
4243
4557
  })();
4244
- const showCursor = isLive || position !== null || playing;
4558
+ const moving = isLive || position !== null || playing;
4559
+ const showCursor = moving || view === "agents";
4245
4560
  const overviewMarks = useMemo(() => rows.filter((row) => !row.parentId).flatMap((row) => row.hasChildren ? row.segments : row.kind === "pipeline" ? [] : [{
4246
4561
  startMs: row.span.startMs,
4247
4562
  endMs: row.span.endMs,
@@ -4305,6 +4620,11 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
4305
4620
  className: "fw-title",
4306
4621
  children: label
4307
4622
  }),
4623
+ description && /* @__PURE__ */ jsx("p", {
4624
+ className: "fw-description",
4625
+ title: description,
4626
+ children: description
4627
+ }),
4308
4628
  isLive && /* @__PURE__ */ jsx("span", {
4309
4629
  className: "fw-state fw-state-live",
4310
4630
  children: "Live"
@@ -4323,6 +4643,7 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
4323
4643
  agents.length,
4324
4644
  " ",
4325
4645
  agents.length === 1 ? "agent" : "agents",
4646
+ sharedDetail ? ` · ${sharedDetail}` : "",
4326
4647
  done && speedup >= 1.05 ? ` · ${speedup.toFixed(1)}x parallel` : ""
4327
4648
  ]
4328
4649
  }),
@@ -4380,7 +4701,7 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
4380
4701
  })]
4381
4702
  })]
4382
4703
  }),
4383
- /* @__PURE__ */ jsx("dl", {
4704
+ showSummary && /* @__PURE__ */ jsx("dl", {
4384
4705
  className: "fw-summary",
4385
4706
  children: summary.map(([name, value, title]) => /* @__PURE__ */ jsxs("div", {
4386
4707
  className: name === "Errors" && errors > 0 ? "fw-has-errors" : void 0,
@@ -4392,6 +4713,7 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
4392
4713
  }, name))
4393
4714
  }),
4394
4715
  view === "agents" && playback,
4716
+ view === "agents" && /* @__PURE__ */ jsx(KindLegend, { rows }),
4395
4717
  /* @__PURE__ */ jsx(FlowBody, {
4396
4718
  rows: visible,
4397
4719
  byId,
@@ -4401,6 +4723,10 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
4401
4723
  collapsed,
4402
4724
  searching: Boolean(query.trim()),
4403
4725
  pauses: trace.pauses,
4726
+ cursorLine: moving,
4727
+ sharedDetail,
4728
+ expandedIds: view === "agents" ? /* @__PURE__ */ new Set([...openFamilies, ...openSteps]) : void 0,
4729
+ fixedHeight: isLive,
4404
4730
  slowestId: slowest?.id ?? callouts.slowestId,
4405
4731
  costliestId: callouts.costliestId,
4406
4732
  activeId,
@@ -1,30 +1,31 @@
1
- /* Shared live charts. Consumers may set --chart-live-* tokens on an ancestor. */
2
- .ov-empty { margin: 0; color: var(--chart-live-faint, #777); font-size: .9em; }
1
+ /* Shared live charts. Consumers may set --chart-live-* tokens on an ancestor. Sizes are px on the 12/14 type scale and a
2
+ 4 px grid, so a host's font size never rescales them. */
3
+ .ov-empty { margin: 0; color: var(--chart-live-faint, #777); font-size: 12px; }
3
4
  .ov-plot { position: relative; min-width: 0; }
4
5
  .ov-plot svg { display: block; overflow: visible; }
5
6
  line.ov-gridline { stroke: color-mix(in srgb, var(--chart-live-faint, #777) 22%, transparent); stroke-width: 1; }
6
- .ov-axis { fill: var(--chart-live-muted, #666); font: .8em var(--chart-live-font-mono, monospace); font-variant-numeric: tabular-nums; }
7
+ .ov-axis { fill: var(--chart-live-muted, #666); font: 12px/16px var(--chart-live-font-mono, monospace); font-variant-numeric: tabular-nums; }
7
8
  .ov-crosshair { stroke: var(--chart-live-faint, #777); stroke-dasharray: 3 3; }
8
- .ov-tip { position: absolute; z-index: 4; display: grid; gap: .25em; min-width: 12em; max-width: 18em; padding: .6em .75em; background: var(--chart-live-raised, #fff); border: 1px solid var(--chart-live-border, #aaa); border-radius: 8px; box-shadow: 0 10px 24px -12px #000; font-size: .85em; pointer-events: none; }
9
- .ov-tip-line { display: grid; grid-template-columns: auto 1fr auto; gap: .6em; align-items: center; color: var(--chart-live-muted, #666); }
9
+ .ov-tip { position: absolute; z-index: 4; display: grid; gap: 4px; min-width: 160px; max-width: 240px; padding: 8px 12px; background: var(--chart-live-raised, #fff); border: 1px solid var(--chart-live-border, #aaa); border-radius: 8px; box-shadow: 0 10px 24px -12px #000; font-size: 12px; line-height: 16px; pointer-events: none; }
10
+ .ov-tip-line { display: grid; grid-template-columns: auto 1fr auto; gap: 8px; align-items: center; color: var(--chart-live-muted, #666); }
10
11
  .ov-tip-line b { color: var(--chart-live-fg, #222); font-variant-numeric: tabular-nums; }
11
- .ov-hbars { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: minmax(0, max-content) minmax(33%, 1fr) max-content; gap: .25em .7em; }
12
+ .ov-hbars { list-style: none; margin: 0; padding: 0; display: grid; grid-template-columns: minmax(0, max-content) minmax(33%, 1fr) max-content; gap: 4px 12px; }
12
13
  .ov-hbars li { display: grid; grid-column: 1 / -1; grid-template-columns: subgrid; }
13
- .ov-hbars li > a, .ov-hbars li > div { display: grid; grid-column: 1 / -1; grid-template-columns: subgrid; row-gap: .15em; align-items: center; padding: .3em .4em; border-radius: 6px; color: inherit; text-decoration: none; }
14
- .ov-hbars-more { grid-column: 1 / -1; padding: .1em .4em; font-size: .8em; color: var(--chart-live-faint, #777); }
14
+ .ov-hbars li > a, .ov-hbars li > div { display: grid; grid-column: 1 / -1; grid-template-columns: subgrid; row-gap: 0; align-items: center; padding: 4px; border-radius: 6px; color: inherit; text-decoration: none; }
15
+ .ov-hbars-more { grid-column: 1 / -1; padding: 0 4px; font-size: 12px; color: var(--chart-live-faint, #777); }
15
16
  .ov-hbars li > a:hover, .ov-hbars li > div:hover { background: var(--chart-live-hover, #eee); text-decoration: none; }
16
- .ov-hbar-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: .95em; color: var(--chart-live-fg, #222); }
17
- .ov-hbar-track { height: .7em; border-radius: 3px; background: color-mix(in srgb, var(--chart-live-faint, #777) 14%, transparent); overflow: hidden; }
17
+ .ov-hbar-label { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 14px; line-height: 20px; color: var(--chart-live-fg, #222); }
18
+ .ov-hbar-track { height: 8px; border-radius: 3px; background: color-mix(in srgb, var(--chart-live-faint, #777) 14%, transparent); overflow: hidden; }
18
19
  .ov-hbar-track > span { display: block; height: 100%; border-radius: 3px; }
19
- .ov-hbar-value { font: .9em var(--chart-live-font-mono, monospace); font-variant-numeric: tabular-nums; text-align: right; color: var(--chart-live-fg, #222); }
20
- .ov-hbar-note { grid-column: 1 / -1; font-size: .78em; color: var(--chart-live-faint, #777); }
21
- .ov-meter { display: grid; gap: .3em; min-width: 0; }
22
- .ov-meter-track { display: block; height: .7em; border-radius: 3px; background: color-mix(in srgb, var(--chart-live-faint, #777) 14%, transparent); overflow: hidden; }
20
+ .ov-hbar-value { font: 12px/16px var(--chart-live-font-mono, monospace); font-variant-numeric: tabular-nums; text-align: right; color: var(--chart-live-fg, #222); }
21
+ .ov-hbar-note { grid-column: 1 / -1; font-size: 12px; color: var(--chart-live-faint, #777); }
22
+ .ov-meter { display: grid; gap: 4px; min-width: 0; }
23
+ .ov-meter-track { display: block; height: 8px; border-radius: 3px; background: color-mix(in srgb, var(--chart-live-faint, #777) 14%, transparent); overflow: hidden; }
23
24
  .ov-meter-track > span { display: block; height: 100%; border-radius: 3px; }
24
- .ov-meter-text { font: .85em var(--chart-live-font-mono, monospace); font-variant-numeric: tabular-nums; color: var(--chart-live-muted, #666); }
25
- .ov-spark { display: block; width: 8em; height: 1.6em; }
26
- .ov-spark-empty { font-size: .8em; color: var(--chart-live-faint, #777); }
27
- .ov-tip-line i { display: inline-block; width: .7em; height: .7em; border-radius: 3px; margin-right: .4em; vertical-align: -.05em; }
25
+ .ov-meter-text { font: 12px/16px var(--chart-live-font-mono, monospace); font-variant-numeric: tabular-nums; color: var(--chart-live-muted, #666); }
26
+ .ov-spark { display: block; width: 112px; height: 24px; }
27
+ .ov-spark-empty { font-size: 12px; color: var(--chart-live-faint, #777); }
28
+ .ov-tip-line i { display: inline-block; width: 8px; height: 8px; border-radius: 2px; margin-right: 4px; vertical-align: 0; }
28
29
  /* Recorded events and spans. Colours come from Brand tokens; kinds also differ by stroke pattern and glyph. */
29
30
  .ov-mark { stroke-width: 1.5; fill: none; }
30
31
  .ov-mark-pass { stroke: var(--chart-live-pass, var(--chart-pass, var(--status-running))); }
@@ -44,7 +45,7 @@ line.ov-gridline { stroke: color-mix(in srgb, var(--chart-live-faint, #777) 22%,
44
45
  .ov-verdict-fail .ov-mark-head { fill: var(--chart-live-danger, var(--chart-breach, var(--status-error))); }
45
46
  .ov-verdict-unknown .ov-mark { stroke: var(--chart-live-faint, var(--chart-muted)); }
46
47
  .ov-verdict-unknown .ov-mark-head { fill: var(--chart-live-faint, var(--chart-muted)); }
47
- .ov-mark-label { fill: var(--chart-live-fg, var(--chart-ink)); font-size: .75em; paint-order: stroke; stroke: var(--chart-live-surface, var(--chart-canvas)); stroke-width: 3px; stroke-linejoin: round; }
48
+ .ov-mark-label { fill: var(--chart-live-fg, var(--chart-ink)); font-size: 12px; paint-order: stroke; stroke: var(--chart-live-surface, var(--chart-canvas)); stroke-width: 3px; stroke-linejoin: round; }
48
49
  .ov-band-after-pass { fill: color-mix(in srgb, var(--chart-live-pass, var(--chart-pass, var(--status-running))) 12%, transparent); }
49
50
  .ov-band-idle { fill: var(--chart-live-hover, var(--chart-shade)); }
50
51
  .ov-band-blocked { fill: color-mix(in srgb, var(--chart-live-danger, var(--chart-breach, var(--status-error))) 9%, transparent); }
@@ -56,10 +57,10 @@ line.ov-gridline { stroke: color-mix(in srgb, var(--chart-live-faint, #777) 22%,
56
57
  .ov-hatched { background: repeating-linear-gradient(135deg, var(--chart-live-faint, var(--chart-muted)) 0 1px, transparent 1px 5px), color-mix(in srgb, var(--chart-live-faint, var(--chart-muted)) 10%, transparent); }
57
58
  .ov-now { stroke: var(--chart-live-fg, var(--chart-ink)); stroke-width: 1.5; }
58
59
  .ov-gantt-open { stroke: var(--chart-live-fg, var(--chart-ink)); stroke-width: 1.5; stroke-dasharray: 3 2; }
59
- .ov-events, .ov-key, .ov-meter-key { list-style: none; margin: .4em 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: .2em 1em; font-size: .8em; color: var(--chart-live-muted, var(--chart-ink-muted)); }
60
- .ov-key li, .ov-events li, .ov-meter-key li { display: inline-flex; align-items: center; gap: .4em; }
61
- .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 i, .ov-event-swatch { display: inline-block; width: .8em; height: .8em; border-radius: 2px; }
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
+ .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: 600; color: var(--chart-live-fg, var(--chart-ink)); font-family: var(--chart-live-font-mono, monospace); font-variant-numeric: tabular-nums; }
63
+ .ov-meter-key i, .ov-event-swatch { display: inline-block; width: 12px; height: 12px; border-radius: 2px; }
63
64
  [data-segmented] > .ov-meter-track { display: flex; }
64
65
  [data-segmented] > .ov-meter-track > span { flex: none; border-radius: 0; }
65
66
  .ov-event-pass { border-left: 2px solid var(--chart-live-pass, var(--chart-pass, var(--status-running))); border-radius: 0; width: 0; }
@@ -72,7 +73,7 @@ line.ov-gridline { stroke: color-mix(in srgb, var(--chart-live-faint, #777) 22%,
72
73
  .ov-event-after-pass { background: color-mix(in srgb, var(--chart-live-pass, var(--chart-pass, var(--status-running))) 25%, transparent); }
73
74
  .ov-event-idle { background: var(--chart-live-hover, var(--chart-shade)); }
74
75
  .ov-event-missing { background: repeating-linear-gradient(135deg, var(--chart-live-faint, var(--chart-muted)) 0 1px, transparent 1px 4px); }
75
- .ov-refusal { margin: 0; color: var(--chart-live-muted, var(--chart-ink-muted)); font-size: .9em; }
76
+ .ov-refusal { margin: 0; color: var(--chart-live-muted, var(--chart-ink-muted)); font-size: 12px; }
76
77
 
77
78
  /*
78
79
  * DotMap: thousands of dots on one canvas. A categorical lens names at most three categories in colour, the first
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/charts",
3
- "version": "0.20.0",
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",