@tangle-network/charts 0.21.16 → 0.21.18

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.
@@ -259,7 +259,8 @@
259
259
  /* Muted agents (the default): one quiet tone for every agent bar, its steps and handoffs; the slowest agent alone takes amber, and
260
260
  failure keeps its red glyph, so color only ever means state. */
261
261
  .fw-root.fw-fill-muted .fw-view-agents [data-tone] { --tone: var(--fw-h0); }
262
- .fw-root:is(.fw-fill-muted, .fw-fill-agent) .fw-view-agents .fw-row[data-slowest], .fw-root:is(.fw-fill-muted, .fw-fill-agent) .fw-view-agents .fw-row[data-slowest] [data-tone] { --tone: var(--fw-amber); }
262
+ /* With per-agent hues the slowest agent keeps its own hue and says so with the amber pill (round 27: an amber-brown bar read "muddy"). */
263
+ .fw-root.fw-fill-muted .fw-view-agents .fw-row[data-slowest], .fw-root.fw-fill-muted .fw-view-agents .fw-row[data-slowest] [data-tone] { --tone: var(--fw-amber); }
263
264
  .fw-root [data-outcome="ok"], .fw-root [data-outcome="ok"] [data-tone] { --tone: var(--fw-ok); }
264
265
  .fw-root [data-outcome="failed"], .fw-root [data-outcome="failed"] [data-tone] { --tone: var(--fw-failed); }
265
266
  .fw-root [data-outcome="warn"], .fw-root [data-outcome="warn"] [data-tone] { --tone: var(--fw-amber); }
@@ -291,14 +292,30 @@
291
292
  .fw-root:is(.fw-fill-muted, .fw-fill-outcome) .fw-view-agents .fw-seg-model { background: var(--fw-kind-model); box-shadow: none; }
292
293
  .fw-root:is(.fw-fill-muted, .fw-fill-outcome) .fw-view-agents .fw-seg-tool { background: var(--fw-kind-tool); box-shadow: none; }
293
294
  /* Per-agent hues at full saturation (blind round 26: muted per-agent bars "blur together", "wash out on white"): kind is a darker or
294
- lighter step of the agent's own saturated hue, every solid segment at least 3:1 against the card; waiting stays the only pale fill. */
295
- .fw-root.fw-fill-agent .fw-view-agents .fw-seg-model { background: light-dark(color-mix(in oklab, var(--tone) 75%, var(--fw-text)), var(--tone)); box-shadow: none; }
296
- .fw-root.fw-fill-agent .fw-view-agents .fw-seg-tool { background: light-dark(var(--tone), color-mix(in oklab, var(--tone) 72%, var(--fw-text))); box-shadow: none; }
295
+ lighter step of the agent's own saturated hue; waiting stays the only pale fill. Light theme sits one step softer (round 27: the
296
+ saturated light bars read "heavy"). */
297
+ .fw-root.fw-fill-agent .fw-view-agents .fw-seg-model { background: light-dark(color-mix(in oklab, var(--tone) 88%, var(--fw-text)), var(--tone)); box-shadow: none; }
298
+ .fw-root.fw-fill-agent .fw-view-agents .fw-seg-tool { background: light-dark(color-mix(in oklab, var(--tone) 84%, var(--fw-surface)), color-mix(in oklab, var(--tone) 72%, var(--fw-text))); box-shadow: none; }
297
299
  /* A failed turn is still the agent's work: it keeps the agent's saturated hue, and the red cross on the ending turn says it failed. */
298
- .fw-root.fw-fill-agent .fw-view-agents .fw-seg-failed { background: light-dark(var(--tone), color-mix(in oklab, var(--tone) 72%, var(--fw-text))); box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--fw-failed) 55%, transparent); }
299
- .fw-root .fw-view-agents .fw-seg-tool { container-type: inline-size; justify-content: flex-start; }
300
- .fw-root .fw-seg-name { padding: 0 6px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-size: 12px; line-height: 16px; font-weight: 600; color: light-dark(var(--fw-surface), hsl(var(--hsl-background))); pointer-events: none; }
301
- @container (max-width: 56px) { .fw-root .fw-seg-name { display: none; } }
300
+ .fw-root.fw-fill-agent .fw-view-agents .fw-seg-failed { background: light-dark(color-mix(in oklab, var(--tone) 84%, var(--fw-surface)), color-mix(in oklab, var(--tone) 72%, var(--fw-text))); box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--fw-failed) 55%, transparent); }
301
+ .fw-root .fw-view-agents .fw-seg { container-type: inline-size; justify-content: flex-start; gap: 4px; white-space: nowrap; font-size: 12px; line-height: 16px; color: light-dark(var(--fw-surface), hsl(var(--hsl-background))); }
302
+ .fw-root .fw-view-agents .fw-seg > * { pointer-events: none; }
303
+ /* The inset is the first part's margin, not the segment's padding, so a short step keeps its true width (padding set a 12 px floor). */
304
+ .fw-root .fw-view-agents .fw-seg > :first-child { margin-left: 6px; }
305
+ .fw-root .fw-seg-icon { display: inline-flex; flex: none; }
306
+ .fw-root .fw-seg-icon svg { width: 12px; height: 12px; stroke-width: 1.75; }
307
+ .fw-root .fw-seg-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; font-weight: 600; }
308
+ .fw-root .fw-seg-chip { flex: none; padding: 0 4px; border-radius: var(--radius-pill); background: color-mix(in oklab, currentColor 24%, transparent); font-weight: 600; font-variant-numeric: tabular-nums; }
309
+ .fw-root .fw-seg-meta { flex: none; opacity: .85; font-variant-numeric: tabular-nums; }
310
+ /* A narrowing step drops its cost first, then its duration, then its name and count, then its icon. */
311
+ @container (max-width: 180px) { .fw-root .fw-seg-cost { display: none; } }
312
+ @container (max-width: 96px) { .fw-root .fw-seg-meta { display: none; } }
313
+ @container (max-width: 56px) { .fw-root :is(.fw-seg-name, .fw-seg-chip) { display: none; } }
314
+ @container (max-width: 24px) { .fw-root :is(.fw-seg-icon, .fw-view-agents .fw-seg .fw-glyph) { display: none; } }
315
+ /* Fail and retry read as filled chips inside the bar, red and amber, as the references draw them. */
316
+ .fw-root .fw-view-agents .fw-seg .fw-glyph { flex: none; display: grid; place-items: center; width: 14px; height: 14px; border-radius: 50%; background: var(--fw-failed); color: light-dark(var(--fw-surface), hsl(var(--hsl-background))); }
317
+ .fw-root .fw-view-agents .fw-seg .fw-glyph[data-glyph="retry"] { background: var(--fw-amber); }
318
+ .fw-root .fw-view-agents .fw-seg .fw-glyph svg { width: 9px; height: 9px; stroke-width: 2.5; }
302
319
  .fw-root:is(.fw-fill-muted, .fw-fill-outcome) .fw-view-agents :is(.fw-seg-alt.fw-seg-model, .fw-seg-alt.fw-seg-tool) { filter: brightness(.88); }
303
320
  .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); }
304
321
  /* 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. */
@@ -320,15 +337,15 @@
320
337
  .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; }
321
338
  .fw-root .fw-link {
322
339
  --x: calc(var(--fw-pad) + (var(--s) - var(--fw-v0)) * var(--fw-k) * 1px);
323
- position: absolute; width: 8px; border: 0 solid var(--line); border-bottom-width: 1.5px; translate: 0 var(--fw-bar-shift);
340
+ position: absolute; width: 12px; border: 0 solid var(--line); border-bottom-width: 1.5px; translate: 0 var(--fw-bar-shift);
324
341
  opacity: clamp(0, (var(--fw-t) - var(--s)) * 1000, 1);
325
342
  }
326
343
  .fw-root .fw-view-spans .fw-link { opacity: calc(clamp(0, (var(--fw-t) - var(--s)) * 1000, 1) * .7); }
327
344
  /* Agent-view connectors rest faint, one curved elbow per spawn in the agent's hue (they show who started whom), and come to
328
345
  full strength for the agent under the pointer, pinned or focused. */
329
- .fw-root .fw-view-agents .fw-link:not(.fw-link-hot) { opacity: calc(clamp(0, (var(--fw-t) - var(--s)) * 1000, 1) * .6); }
346
+ .fw-root .fw-view-agents .fw-link:not(.fw-link-hot) { opacity: calc(clamp(0, (var(--fw-t) - var(--s)) * 1000, 1) * .8); }
330
347
  .fw-root .fw-link { transition: opacity 160ms var(--fw-ease); }
331
- .fw-root .fw-link-down { left: calc(var(--x) - 8px); border-left-width: 1.5px; border-bottom-left-radius: 12px; }
348
+ .fw-root .fw-link-down { left: calc(var(--x) - 12px); border-left-width: 1.5px; border-bottom-left-radius: 12px; }
332
349
  .fw-root .fw-link-up { left: var(--x); border-right-width: 1.5px; border-bottom-right-radius: 12px; }
333
350
  .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); }
334
351
  .fw-root .fw-link-down::before { left: -3.25px; }
@@ -373,6 +373,90 @@ interface DotMapProps {
373
373
  export declare const DOT_MAP_CATEGORIES: readonly string[];
374
374
  export declare function DotMap({ positions, radii, colors, palette, extent, rects, labels, dimmed, selected, minPixelRadius, onHover, onSelect, describe, height, ariaLabel }: DotMapProps): import("react").JSX.Element;
375
375
  //#endregion
376
+ //#region src/react/flow-waterfall-model.d.ts
377
+ /** A minimap mark on a parent row: merged descendant activity of one kind. */
378
+ interface FlowSegment {
379
+ startMs: number;
380
+ /** Infinity while running; the presentation clips it at the clock. */
381
+ endMs: number;
382
+ kind: 'model' | 'tool' | 'failed';
383
+ tone: number;
384
+ /** The first span merged into this mark, and how many were merged. */
385
+ id: string;
386
+ count: number;
387
+ retry: boolean;
388
+ /** In a family row, every other member's marks take a darker shade of the family's one hue. */
389
+ alt?: boolean;
390
+ /** The first merged span's name, e.g. a tool's: shown in the segment's tooltip, and in the bar when one step fits it. */
391
+ name?: string;
392
+ /** The tool every merged step shares (the name before " · "); undefined once the merged steps differ. */
393
+ tool?: string;
394
+ /** What the merged steps cost, when any of them recorded a cost. */
395
+ costUsd?: number;
396
+ /** A failed mark that was a model call, so it draws the model's icon and no tool name. */
397
+ modelCall?: boolean;
398
+ }
399
+ /** A row's direct steps: model and tool calls (failed ones included), the failed and retrying ones, and tool calls by tool. */
400
+ interface StepCounts {
401
+ model: number;
402
+ tool: number;
403
+ failed: number;
404
+ retried: number;
405
+ tools: Record<string, number>;
406
+ }
407
+ /** Tokens in, out and cached; a field nobody recorded stays undefined. */
408
+ interface TokenCounts {
409
+ prompt?: number;
410
+ completion?: number;
411
+ cached?: number;
412
+ }
413
+ interface FlowWaterfallRow {
414
+ id: string;
415
+ parentId?: string;
416
+ name: string;
417
+ kind: FlowSpan['kind'];
418
+ durationLabel: string;
419
+ approx: boolean;
420
+ ok: boolean;
421
+ depth: number;
422
+ hasChildren: boolean;
423
+ span: FlowSpan;
424
+ /** Agent hue: the index of the nearest pipeline ancestor (or self), in first-start order. */
425
+ tone: number;
426
+ /** All descendants, for the collapsed "+N" count. */
427
+ descendants: number;
428
+ /** Parent rows only: merged descendant activity, drawn as the row's minimap. */
429
+ segments: FlowSegment[];
430
+ /** Direct non-agent children: the row's own thinking and tool work, drawn over its waiting extent. */
431
+ own: FlowSegment[];
432
+ /** Direct steps counted from the spans, before marks merge for display (merging depends on zoom). */
433
+ counts?: StepCounts;
434
+ /** Durations of its finished direct model calls, ascending: the card's latency. */
435
+ modelMs?: number[];
436
+ /** The span's own tokens, else the sum of its children's, as its cost rolls up. */
437
+ tokens?: TokenCounts;
438
+ /** Its cost is a floor ("≥"): its own bound, its trace's, or a family with unpriced members. */
439
+ costFloor?: boolean;
440
+ /** The span retries a failed one; its bar carries a retry glyph. */
441
+ retry: boolean;
442
+ /** The row delegates to child agents, so the gaps in its own work are waiting. */
443
+ hasAgents: boolean;
444
+ /** Recorded cost of the row and its descendants, only when every model call below it has a recorded cost. */
445
+ rollupCostUsd?: number;
446
+ /** Agent view: the family row this agent belongs to, when agents sharing a role are folded together. */
447
+ family?: string;
448
+ /** Agent view: the agent whose steps this row is, shown nested under it. */
449
+ stepOf?: string;
450
+ /** Agent view: indent level (members under a family, steps under an agent). */
451
+ inset?: number;
452
+ }
453
+ /** Hierarchy only; presentation owns geometry.
454
+ * Orphaned parents become roots; cycles are broken without dropping any span. */
455
+ export declare function flowWaterfallRows(trace: FlowTrace, resolutionMs?: number): FlowWaterfallRow[];
456
+ /** The chain of steps that gates the trace's end: from each root, descend into the latest-ending child until a leaf; the root whose
457
+ * chain ends last wins. Cutting latency anywhere else leaves the trace's end unchanged. */
458
+ export declare function criticalPathIds(rows: readonly FlowWaterfallRow[]): ReadonlySet<string>;
459
+ //#endregion
376
460
  //#region src/react/flow-waterfall-row.d.ts
377
461
  type FlowView = 'spans' | 'agents';
378
462
  //#endregion
@@ -436,4 +520,4 @@ interface FlowWaterfallProps {
436
520
  * Data remains producer-owned; playback, zoom and folding change presentation only. */
437
521
  export declare function FlowWaterfall({ trace, live, startedAt, label, defaultCollapsed, defaultView, renderIcon, maxHeight, onSelect, markers, onTimeChange, agentFill: fillProp, selectedId, onDeselect, description, summary: showSummary, criticalPath, query: queryProp, onQueryChange, collapsed: collapsedProp, onCollapsedChange, zoom: zoomProp, onZoomChange, matchSpan }: FlowWaterfallProps): import("react").JSX.Element;
438
522
  //#endregion
439
- export type { BandKind, ChartBand, ChartMark, DotMapLabel, DotMapProps, DotMapRect, Figure, FigureAtWidth, FigureViewProps, FlowWaterfallProps, ViewWindow as FlowWaterfallWindow, GapBand, GapProps, 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 };
523
+ export type { BandKind, ChartBand, ChartMark, DotMapLabel, DotMapProps, DotMapRect, Figure, FigureAtWidth, FigureViewProps, FlowWaterfallProps, FlowWaterfallRow, ViewWindow as FlowWaterfallWindow, GapBand, GapProps, 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 };
@@ -2476,6 +2476,7 @@ function mergeSegments(marks, gapMs, cap = 240) {
2476
2476
  last.endMs = Math.max(last.endMs, mark.endMs);
2477
2477
  last.count += mark.count;
2478
2478
  if (last.tool !== mark.tool) last.tool = void 0;
2479
+ if (mark.costUsd !== void 0) last.costUsd = (last.costUsd ?? 0) + mark.costUsd;
2479
2480
  last.kind = share.model >= share.tool ? "model" : "tool";
2480
2481
  } else out.push({ ...mark });
2481
2482
  }
@@ -2656,7 +2657,9 @@ function segmentOf(row) {
2656
2657
  count: 1,
2657
2658
  retry: row.retry,
2658
2659
  name: row.name,
2659
- tool: toolOf(row.name)
2660
+ tool: toolOf(row.name),
2661
+ ...row.span.costUsd === void 0 ? {} : { costUsd: row.span.costUsd },
2662
+ ...!row.ok && row.kind === "model" ? { modelCall: true } : {}
2660
2663
  };
2661
2664
  }
2662
2665
  /** The rows a search term matches by themselves, before ancestor context is added. */
@@ -3374,25 +3377,51 @@ const at = (startMs, endMs) => ({
3374
3377
  });
3375
3378
  const endLabelWidth = (label, slowest, costliest) => label.length * 7 + 8 + (slowest ? 80 : 0) + (costliest ? 90 : 0);
3376
3379
  function Segment({ mark, events, agentView, ending }) {
3377
- const glyph = mark.kind === "failed" ? agentView && !ending ? null : /* @__PURE__ */ jsx(FailGlyph, {}) : mark.retry ? /* @__PURE__ */ jsx(RetryGlyph, {}) : null;
3378
- const named = agentView && mark.kind === "tool" && (mark.count === 1 ? mark.name : mark.tool && `${mark.tool} ×${mark.count}`);
3379
- return /* @__PURE__ */ jsx("span", {
3380
+ const glyph = mark.kind === "failed" ? agentView && !ending ? null : "fail" : mark.retry ? "retry" : null;
3381
+ const model = mark.kind === "model" || mark.modelCall;
3382
+ const name = model ? void 0 : mark.count === 1 ? mark.name : mark.tool;
3383
+ const duration = Number.isFinite(mark.endMs) ? formatSpanDuration(mark.endMs - mark.startMs) : "";
3384
+ const meta = [duration, mark.costUsd ? formatCost(mark.costUsd) : ""].filter(Boolean).join(" · ");
3385
+ return /* @__PURE__ */ jsxs("span", {
3380
3386
  className: `fw-seg fw-seg-${mark.kind}${mark.alt ? " fw-seg-alt" : ""}`,
3381
3387
  "data-tone": toneKey(mark.tone),
3382
3388
  style: at(mark.startMs, mark.endMs),
3383
- title: mark.name ? `${mark.name}${mark.count > 1 ? ` +${mark.count - 1} more` : ""}` : void 0,
3389
+ title: mark.name ? `${mark.name}${mark.count > 1 ? ` +${mark.count - 1} more` : ""}${meta ? ` · ${meta}` : ""}` : void 0,
3384
3390
  onPointerEnter: agentView ? (event) => events.onInspect(mark.id, event.currentTarget, event.clientX) : void 0,
3385
3391
  onClick: agentView ? (event) => {
3386
3392
  event.stopPropagation();
3387
3393
  events.onPin(mark.id, event.currentTarget, event.clientX);
3388
3394
  } : void 0,
3389
- children: glyph ? /* @__PURE__ */ jsx("span", {
3395
+ children: [glyph && /* @__PURE__ */ jsx("span", {
3390
3396
  className: "fw-glyph",
3391
- children: glyph
3392
- }) : named && /* @__PURE__ */ jsx("span", {
3393
- className: "fw-seg-name",
3394
- children: named
3395
- })
3397
+ "data-glyph": glyph,
3398
+ children: glyph === "fail" ? /* @__PURE__ */ jsx(FailGlyph, {}) : /* @__PURE__ */ jsx(RetryGlyph, {})
3399
+ }), agentView && /* @__PURE__ */ jsxs(Fragment, { children: [
3400
+ /* @__PURE__ */ jsx("span", {
3401
+ className: "fw-seg-icon",
3402
+ "aria-hidden": true,
3403
+ children: /* @__PURE__ */ jsx(KindGlyph, {
3404
+ kind: model ? "model" : "tool",
3405
+ name: mark.tool ?? mark.name
3406
+ })
3407
+ }),
3408
+ name && /* @__PURE__ */ jsx("span", {
3409
+ className: "fw-seg-name",
3410
+ children: name
3411
+ }),
3412
+ mark.count > 1 && /* @__PURE__ */ jsxs("span", {
3413
+ className: "fw-seg-chip",
3414
+ children: ["×", mark.count]
3415
+ }),
3416
+ duration && /* @__PURE__ */ jsx("span", {
3417
+ className: "fw-seg-meta",
3418
+ children: duration
3419
+ }),
3420
+ mark.costUsd ? /* @__PURE__ */ jsx("span", {
3421
+ className: "fw-seg-meta fw-seg-cost",
3422
+ children: formatCost(mark.costUsd)
3423
+ }) : null
3424
+ ] })]
3396
3425
  });
3397
3426
  }
3398
3427
  /** A span the clock is inside: it grows live, or playback has not reached its end. */
@@ -3418,7 +3447,7 @@ const FlowRow = memo(function FlowRow(props) {
3418
3447
  const kind = !row.ok ? "failed" : row.kind;
3419
3448
  const marks = isAgent || row.hasChildren || row.id.startsWith("run:") ? folded && !agentView ? row.segments : row.own : [];
3420
3449
  const label = endLabel(row, clockMs);
3421
- const summary = [...span.detail && span.detail !== props.sharedDetail ? [span.detail] : [], ...agentSummary(row)];
3450
+ const summary = [...span.detail && span.detail !== props.sharedDetail ? [span.detail] : [], ...agentSummary(row).filter((part) => part !== props.sharedOutcome)];
3422
3451
  const inspect = (event) => props.onInspect(row.id, event.currentTarget, event.clientX);
3423
3452
  const family = agentView && row.id.startsWith("family:");
3424
3453
  const step = agentView && row.stepOf !== void 0;
@@ -3554,6 +3583,11 @@ const FlowRow = memo(function FlowRow(props) {
3554
3583
  "aria-label": `${row.descendants} hidden steps`,
3555
3584
  children: ["+", row.descendants]
3556
3585
  }),
3586
+ family && !props.expanded && /* @__PURE__ */ jsxs("span", {
3587
+ className: "fw-count",
3588
+ "aria-label": `${row.descendants} agents folded`,
3589
+ children: ["+", row.descendants]
3590
+ }),
3557
3591
  !row.ok && !agentView && /* @__PURE__ */ jsx("span", {
3558
3592
  className: "fw-alert",
3559
3593
  children: /* @__PURE__ */ jsx(AlertGlyph, { label: "failed" })
@@ -3622,7 +3656,7 @@ const FlowRow = memo(function FlowRow(props) {
3622
3656
  ] })
3623
3657
  })]
3624
3658
  });
3625
- }, (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.critical === b.critical && a.matched === b.matched && 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)));
3659
+ }, (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.critical === b.critical && a.matched === b.matched && 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.sharedOutcome === b.sharedOutcome && a.expanded === b.expanded && (a.clockMs === b.clockMs || !open(a.row, a.clockMs) && !open(b.row, b.clockMs)));
3626
3660
  /** An elbow connector between two rows at one moment, drawn in the row's tone; appears when the clock reaches it. */
3627
3661
  function LinkMark({ link, rowHeight, hot }) {
3628
3662
  const upper = Math.min(link.from, link.to), lower = Math.max(link.from, link.to);
@@ -4095,6 +4129,7 @@ function FlowBody(props) {
4095
4129
  renderIcon: props.renderIcon,
4096
4130
  fillByAgent: props.fillByAgent,
4097
4131
  sharedDetail: props.sharedDetail,
4132
+ sharedOutcome: props.sharedOutcome,
4098
4133
  expanded: props.expandedIds?.has(item.id),
4099
4134
  onToggle: props.onToggle,
4100
4135
  onInspect: props.onInspect,
@@ -4481,6 +4516,13 @@ function useHostable(value, onChange, initial) {
4481
4516
  }, [controlled])];
4482
4517
  }
4483
4518
  const CARD_WIDTH = 320;
4519
+ /** The value at least half of several agents share, if one does. */
4520
+ function usual(values) {
4521
+ const counts = /* @__PURE__ */ new Map();
4522
+ for (const value of values) if (value) counts.set(value, (counts.get(value) ?? 0) + 1);
4523
+ const [top, n] = [...counts].sort((a, b) => b[1] - a[1])[0] ?? [];
4524
+ return values.length > 1 && n !== void 0 && n >= 2 && n * 2 >= values.length ? top : void 0;
4525
+ }
4484
4526
  /** The summary strip: as of the playhead while scrubbing, the producer's totals otherwise. */
4485
4527
  function summaryCells(trace, at, durationMs) {
4486
4528
  const cost = costSummary(at ? at.costUsd : trace.costUsd, trace);
@@ -4658,8 +4700,8 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
4658
4700
  const pipelines = rows.filter((row) => row.kind === "pipeline");
4659
4701
  return pipelines.length ? pipelines : rows.filter((row) => !row.parentId);
4660
4702
  }, [rows]);
4661
- const details = new Set(agents.flatMap((row) => row.span.detail ? [row.span.detail] : []));
4662
- const sharedDetail = agents.length > 1 && details.size === 1 ? [...details][0] : void 0;
4703
+ const sharedDetail = usual(agents.map((row) => row.span.detail));
4704
+ const sharedOutcome = usual(agents.map((row) => row.span.outcome?.label));
4663
4705
  const [flippedFamilies, setFlippedFamilies] = useState(/* @__PURE__ */ new Set());
4664
4706
  const openFamilies = useMemo(() => defaultOpenFamilies(agents, UNFOLD_ROWS, flippedFamilies), [agents, flippedFamilies]);
4665
4707
  const [openSteps, setOpenSteps] = useState(/* @__PURE__ */ new Set());
@@ -4982,7 +5024,7 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
4982
5024
  agents.length,
4983
5025
  " ",
4984
5026
  agents.length === 1 ? "agent" : "agents",
4985
- sharedDetail ? ` · ${sharedDetail}` : "",
5027
+ sharedDetail ? ` · ${agents.every((row) => row.span.detail === sharedDetail) ? "" : "mostly "}${sharedDetail}` : "",
4986
5028
  done && speedup >= 1.05 ? ` · ${speedup.toFixed(1)}x parallel` : ""
4987
5029
  ]
4988
5030
  }),
@@ -5068,6 +5110,7 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
5068
5110
  pauses: trace.pauses,
5069
5111
  cursorLine: position !== null || playing,
5070
5112
  sharedDetail,
5113
+ sharedOutcome,
5071
5114
  expandedIds: view === "agents" ? /* @__PURE__ */ new Set([...openFamilies, ...openSteps]) : void 0,
5072
5115
  holdHeight: isLive,
5073
5116
  slowestId: slowest?.id ?? callouts.slowestId,
@@ -5125,4 +5168,4 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
5125
5168
  });
5126
5169
  }
5127
5170
  //#endregion
5128
- export { DEFAULT_SPARKLINE_EMPTY_LABEL, DEFAULT_SPARKLINE_HEIGHT, DEFAULT_SPARKLINE_LABEL, DEFAULT_SPARKLINE_UNAVAILABLE_LABEL, DEFAULT_SPARKLINE_WIDTH, DOT_MAP_CATEGORIES, DayBars, DotMap, Events, FigureView, FlowWaterfall, Gantt, Gap, HBars, Histogram, LIVE_SERIES, Lines, Meter, Spark, Sparkline, StackedBarChart, TimelineView, WaterfallBar, WaterfallHeader, WaterfallRow, WaterfallTimeAxis, formatCompact, formatSparklineValue, formatWaterfallDuration, sparklineGeometry, sparklineLabel, sparklinePointsAttribute, sparklineReadings, waterfallSpanGeometry, waterfallTicks };
5171
+ export { DEFAULT_SPARKLINE_EMPTY_LABEL, DEFAULT_SPARKLINE_HEIGHT, DEFAULT_SPARKLINE_LABEL, DEFAULT_SPARKLINE_UNAVAILABLE_LABEL, DEFAULT_SPARKLINE_WIDTH, DOT_MAP_CATEGORIES, DayBars, DotMap, Events, FigureView, FlowWaterfall, Gantt, Gap, HBars, Histogram, LIVE_SERIES, Lines, Meter, Spark, Sparkline, StackedBarChart, TimelineView, WaterfallBar, WaterfallHeader, WaterfallRow, WaterfallTimeAxis, criticalPathIds, flowWaterfallRows, formatCompact, formatSparklineValue, formatWaterfallDuration, sparklineGeometry, sparklineLabel, sparklinePointsAttribute, sparklineReadings, waterfallSpanGeometry, waterfallTicks };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/charts",
3
- "version": "0.21.16",
3
+ "version": "0.21.18",
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",