@tangle-network/charts 0.18.0 → 0.19.0

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.
package/README.md CHANGED
@@ -335,7 +335,7 @@ const trace = otlpFlowTrace(spans, { price: catalogPrice(await (await fetch(cata
335
335
  <FlowWaterfall trace={trace} live={running} defaultView="agents" renderIcon={brandMark} />;
336
336
  ```
337
337
 
338
- `defaultView="agents"` opens the agent timeline: one row per agent with its model, thinking and tool marks, hatched waiting while it delegates, elbow connectors to each agent it spawned and back, retry and failure glyphs, and a headline with the measured speedup over sequential (agent busy time over wall time). Expanding an agent opens the span view with that agent unfolded. The span view virtualizes rows (5,000 spans scroll at 58+ fps in headless Chromium), pins ancestors while their children scroll, and labels each bar with its duration and recorded cost. Hover shows a span card; click pins it and zooms the axis to a span too narrow to read. `onSelect(span)` hears each opened row and pinned bar, so a host can open its own drill-down; `markers={[{ at, label, tone }]}` draws labelled instants, such as releases, across the rows. Give an agent span an `outcome` (`{ tone, label }`) and its lane takes the outcome color and the header a legend. Ctrl/Cmd+wheel or `+`/`-`/`0` zoom, Shift+wheel pans, the scrub track under the controls shows the whole trace, and arrow keys walk the tree.
338
+ `defaultView="agents"` opens the agent timeline: one row per agent with its model, thinking and tool marks, hatched waiting while it delegates, elbow connectors to each agent it spawned and back, retry and failure glyphs, and a headline with the measured speedup over sequential (agent busy time over wall time). Expanding an agent opens the span view with that agent unfolded. The span view virtualizes rows (5,000 spans scroll at 58+ fps in headless Chromium), pins ancestors while their children scroll, and labels each bar with its duration and recorded cost. Hover shows a span card; click pins it and zooms the axis to a span too narrow to read. `onSelect(span)` hears each opened row and pinned bar, so a host can open its own drill-down; `markers={[{ at, label, tone }]}` draws labelled instants, such as releases, across the rows. Give an agent span an `outcome` (`{ tone, label }`) and its lane takes the outcome color and the header a legend. `detail` becomes the agent's subtitle, and `facts` (`[{ label, value }]`) adds cells to its card. Ctrl/Cmd+wheel or `+`/`-`/`0` zoom, Shift+wheel pans, the scrub track under the controls shows the whole trace, and arrow keys walk the tree.
339
339
 
340
340
  Live views append spans as they arrive: `traces watch <id|run-dir> --format jsonl --spans` emits a `traces.stream.span` event per new or changed span, and `otlpFlowTrace` keeps the last span per `span_id`. Running agent spans grow to the clock between deltas without React renders (CSS variables carry the clock). Search supports Cmd/Ctrl+F and keeps ancestor context. Missing cost and authoritative LLM counts stay Unknown; a cost priced from a catalog is labelled list price. Invalid intervals retain a per-row `—` timing mark. The static phase waterfall remains a separate chart for disjoint phases.
341
341
 
@@ -334,6 +334,11 @@ interface FlowSpan {
334
334
  handoffTo?: string;
335
335
  /** The host's verdict on an agent span, such as shipped or crashed: colors its lane and joins the legend. */
336
336
  outcome?: FlowOutcome;
337
+ /** Up to three host facts shown as extra key/value cells in the span card, such as releases or commits. */
338
+ facts?: readonly {
339
+ label: string;
340
+ value: string;
341
+ }[];
337
342
  meta?: Record<string, unknown>;
338
343
  }
339
344
  /** A host verdict: `label` is the legend text, `tone` the status color. */
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-BYKE-PMb.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-D6RDkVrJ.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. */
@@ -128,7 +128,7 @@
128
128
  .fw-root .fw-sticky { position: absolute; top: 0; left: 0; right: 0; z-index: 2; background: var(--fw-surface); box-shadow: 0 1px 0 var(--fw-border), 0 6px 12px -8px rgb(0 0 0 / .35); }
129
129
 
130
130
  /* Rows */
131
- .fw-root .fw-row { position: absolute; top: 0; left: 0; right: 0; height: var(--fw-row-h); display: grid; grid-template-columns: var(--fw-label-col) minmax(0, 1fr); outline: 0; contain: layout style; }
131
+ .fw-root .fw-row { position: absolute; top: 0; left: 0; right: 0; transform: translateY(var(--row-y)); height: var(--fw-row-h); display: grid; grid-template-columns: var(--fw-label-col) minmax(0, 1fr); outline: 0; contain: layout style; }
132
132
  .fw-root .fw-row:hover { background: var(--fw-hover); }
133
133
  .fw-root .fw-row.fw-active { background: color-mix(in oklab, var(--fw-accent) 7%, transparent); }
134
134
  .fw-root .fw-row[aria-selected="true"] { background: color-mix(in oklab, var(--fw-accent) 11%, transparent); }
@@ -160,6 +160,7 @@
160
160
  .fw-root .fw-view-agents .fw-label { padding-left: 16px; gap: 12px; border-right: 0; }
161
161
  .fw-root .fw-view-agents .fw-names { flex-direction: column; align-items: flex-start; gap: 0; }
162
162
  .fw-root .fw-view-agents .fw-span-name { font-weight: 600; line-height: 18px; }
163
+ .fw-root .fw-view-agents .fw-subtitle { max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--fw-muted); font-size: 12px; line-height: 16px; }
163
164
  .fw-root .fw-view-agents .fw-detail { font-size: 12px; line-height: 16px; }
164
165
  .fw-root .fw-agent-dot { flex: 0 0 8px; width: 8px; height: 8px; border-radius: 50%; background: var(--line, var(--fw-muted)); align-self: flex-start; margin-top: calc((var(--fw-row-h) - 34px) / 2 + 8px); }
165
166
 
@@ -198,6 +199,8 @@
198
199
  .fw-root .fw-legend li { display: inline-flex; align-items: center; gap: 8px; }
199
200
  .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); }
200
201
  .fw-root .fw-legend b { color: var(--fw-text); font-weight: 600; font-variant-numeric: tabular-nums; }
202
+ .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); }
203
+ .fw-root .fw-legend .fw-swatch-step svg { width: 8px; height: 8px; }
201
204
  .fw-root .fw-bar.fw-approx { opacity: calc(var(--on) * .7); }
202
205
  .fw-root .fw-seg { width: max(calc(var(--w) - 2px), calc(var(--on) * 3px)); pointer-events: none; }
203
206
  .fw-root .fw-view-agents .fw-seg { pointer-events: auto; }
@@ -221,6 +224,9 @@
221
224
  opacity: clamp(0, (var(--fw-t) - var(--s)) * 1000, 1);
222
225
  }
223
226
  .fw-root .fw-view-spans .fw-link { opacity: calc(clamp(0, (var(--fw-t) - var(--s)) * 1000, 1) * .7); }
227
+ /* Agent-view connectors stay quiet at 30% until their agent is hovered, pinned or focused. */
228
+ .fw-root .fw-view-agents .fw-link:not(.fw-link-hot) { opacity: calc(clamp(0, (var(--fw-t) - var(--s)) * 1000, 1) * .3); }
229
+ .fw-root .fw-link { transition: opacity 160ms var(--fw-ease); }
224
230
  .fw-root .fw-link-down { left: calc(var(--x) - 8px); border-left-width: 1.5px; border-bottom-left-radius: 6px; }
225
231
  .fw-root .fw-link-up { left: var(--x); border-right-width: 1.5px; border-bottom-right-radius: 6px; }
226
232
  .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); }
@@ -231,6 +237,8 @@
231
237
  .fw-root .fw-card-anchor { display: contents; }
232
238
  .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);
233
239
  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); }
240
+ /* A hover card never covers the next mark: only a pinned card (a dialog) takes the pointer. */
241
+ .fw-root .fw-card[role="tooltip"] { pointer-events: none; }
234
242
  .fw-root .fw-card-above .fw-card { transform: translateY(-100%); animation-name: fw-arrive-above; }
235
243
  .fw-root .fw-card-agent { display: flex; align-items: center; gap: 8px; margin-bottom: 8px; font-size: 12px; font-weight: 600; }
236
244
  .fw-root .fw-card-agent .fw-agent-dot { margin-top: 0; align-self: center; }
@@ -255,7 +263,9 @@
255
263
  .fw-root .fw-card-compact { width: 280px; padding: 12px; }
256
264
  .fw-root .fw-card-compact .fw-card-agent { margin-bottom: 8px; font-size: 14px; line-height: 20px; }
257
265
  .fw-root .fw-card-who { min-width: 0; flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
258
- .fw-root .fw-card-cells { display: grid; grid-auto-flow: column; grid-auto-columns: minmax(0, 1fr); gap: 8px; padding: 8px 12px; border-radius: 8px; background: var(--fw-well); }
266
+ .fw-root .fw-card-detail { margin: -4px 0 8px 16px; color: var(--fw-muted); font-size: 12px; line-height: 16px; overflow-wrap: anywhere; }
267
+ .fw-root .fw-pill-outcome { color: var(--line); background: color-mix(in oklab, var(--tone) 16%, transparent); box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--tone) 35%, transparent); }
268
+ .fw-root .fw-card-cells { display: grid; grid-template-columns: repeat(auto-fit, minmax(72px, 1fr)); gap: 8px; padding: 8px 12px; border-radius: 8px; background: var(--fw-well); }
259
269
  .fw-root .fw-card-cells dt { font-size: 12px; line-height: 16px; color: var(--fw-muted); }
260
270
  .fw-root .fw-card-cells dd { font-size: 14px; line-height: 20px; font-weight: 600; font-variant-numeric: tabular-nums; white-space: nowrap; }
261
271
  .fw-root .fw-card-result { margin-top: 8px; font-size: 14px; line-height: 20px; overflow-wrap: anywhere; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
@@ -327,12 +337,12 @@
327
337
  @container (max-width: 899px) {
328
338
  /* The root is the queried container, so the variables go on its children. */
329
339
  .fw-root-agents > * { --fw-label-col: 0px; --fw-bar-shift: 8px; }
330
- .fw-root-agents .fw-view-agents .fw-label { position: absolute; z-index: 2; top: 4px; left: 0; right: 0; height: 18px; padding: 0 var(--fw-pad); gap: 8px; align-items: center; pointer-events: none; overflow: hidden; }
331
340
  .fw-root-agents .fw-view-agents .fw-names { flex-direction: row; align-items: baseline; gap: 8px; min-width: 0; }
332
341
  .fw-root-agents .fw-view-agents .fw-agent-dot { align-self: center; margin-top: 0; }
333
342
  /* The label leaves the grid flow, so the lane must claim the track column itself. */
334
343
  .fw-root-agents .fw-view-agents .fw-lane { grid-column: 2; }
335
- /* Elbows pass behind the name, not through its letters. */
336
- .fw-root-agents .fw-view-agents .fw-names { padding-right: 4px; border-radius: 4px; background: var(--fw-surface); }
337
- .fw-root-agents .fw-view-agents .fw-row:hover .fw-names { background: transparent; }
344
+ /* Elbows pass behind the name, not through its letters. A transform or layout containment would make each row
345
+ its own stacking context and trap the name under the connector layer, so stacked rows place by top instead. */
346
+ .fw-root-agents .fw-view-agents .fw-row { transform: none; top: var(--row-y); contain: style; }
347
+ .fw-root-agents .fw-view-agents .fw-label { position: absolute; z-index: 3; top: 4px; left: 0; height: 18px; max-width: calc(100% - var(--fw-pad)); padding: 0 8px 0 var(--fw-pad); gap: 8px; align-items: center; pointer-events: none; overflow: hidden; border-radius: 0 4px 4px 0; background: var(--fw-surface); }
338
348
  }
@@ -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-BYKE-PMb.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-D6RDkVrJ.js";
2
2
  import { CSSProperties, ReactElement, ReactNode } from "react";
3
3
  //#region src/react/sparkline.d.ts
4
4
  type SparklineDirection = "rising" | "falling" | "flat";
@@ -2663,16 +2663,22 @@ function SpanCard({ id, row, agent, clockMs, left, top, pinned, onClose, renderI
2663
2663
  const duration = Number.isFinite(end - span.startMs) && end >= span.startMs ? formatSpanDuration(end - span.startMs) : "—";
2664
2664
  if (compact) {
2665
2665
  const who = owner ?? row;
2666
+ const outcome = who.span.outcome;
2666
2667
  const tokenCount = span.promptTokens !== void 0 || span.completionTokens !== void 0 ? (span.promptTokens ?? 0) + (span.completionTokens ?? 0) : void 0;
2667
2668
  const cells = [[span.kind === "model" ? "Thinking" : KIND_LABEL[span.kind], duration]];
2669
+ for (const fact of (span.facts ?? who.span.facts ?? []).slice(0, 3)) cells.push([fact.label, fact.value]);
2670
+ cells.push(["Started", `+${formatSpanDuration(Math.max(0, span.startMs))}`]);
2668
2671
  if (tokenCount !== void 0) cells.push(["Tokens", count(tokenCount)]);
2669
2672
  if (span.costUsd !== void 0) cells.push(["Cost", formatCost(span.costUsd)]);
2673
+ cells.splice(6);
2674
+ const sentence = [span.output, span.id === who.id ? void 0 : span.name].find((text) => text && text !== who.name);
2670
2675
  return /* @__PURE__ */ jsxs("div", {
2671
2676
  id,
2672
2677
  role: pinned ? "dialog" : "tooltip",
2673
2678
  "aria-label": pinned ? `${span.name} details` : void 0,
2674
2679
  className: "fw-card fw-card-compact",
2675
2680
  "data-tone": toneKey(who.tone),
2681
+ "data-outcome": outcome?.tone,
2676
2682
  style: {
2677
2683
  left,
2678
2684
  top
@@ -2695,7 +2701,12 @@ function SpanCard({ id, row, agent, clockMs, left, top, pinned, onClose, renderI
2695
2701
  className: "fw-card-who",
2696
2702
  children: who.name
2697
2703
  }),
2698
- /* @__PURE__ */ jsx("span", {
2704
+ outcome ? /* @__PURE__ */ jsx("span", {
2705
+ className: "fw-pill fw-pill-outcome",
2706
+ "data-tone": "0",
2707
+ "data-outcome": outcome.tone,
2708
+ children: outcome.label
2709
+ }) : /* @__PURE__ */ jsx("span", {
2699
2710
  className: `fw-pill fw-pill-${status.toLowerCase()}`,
2700
2711
  children: status
2701
2712
  }),
@@ -2713,13 +2724,17 @@ function SpanCard({ id, row, agent, clockMs, left, top, pinned, onClose, renderI
2713
2724
  })
2714
2725
  ]
2715
2726
  }),
2727
+ who.span.detail && /* @__PURE__ */ jsx("p", {
2728
+ className: "fw-card-detail",
2729
+ children: who.span.detail
2730
+ }),
2716
2731
  /* @__PURE__ */ jsx("dl", {
2717
2732
  className: "fw-card-cells",
2718
2733
  children: cells.map(([name, value]) => /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", { children: name }), /* @__PURE__ */ jsx("dd", { children: value })] }, name))
2719
2734
  }),
2720
- /* @__PURE__ */ jsx("p", {
2735
+ sentence && /* @__PURE__ */ jsx("p", {
2721
2736
  className: "fw-card-result",
2722
- children: span.output ?? span.name
2737
+ children: sentence
2723
2738
  })
2724
2739
  ]
2725
2740
  });
@@ -2876,7 +2891,7 @@ const FlowRow = memo(function FlowRow(props) {
2876
2891
  "data-tone": toneKey(row.tone),
2877
2892
  "data-outcome": span.outcome?.tone,
2878
2893
  className: `fw-row${active ? " fw-active" : ""}${sticky ? " fw-sticky-row" : ""}${isAgent ? " fw-agent-row" : ""}`,
2879
- style: { transform: `translateY(${top}px)` },
2894
+ style: { "--row-y": `${top}px` },
2880
2895
  onFocus: (event) => {
2881
2896
  if (event.target === event.currentTarget) props.onActivate(row);
2882
2897
  },
@@ -2918,21 +2933,28 @@ const FlowRow = memo(function FlowRow(props) {
2918
2933
  /* @__PURE__ */ jsxs("span", {
2919
2934
  className: "fw-names",
2920
2935
  title: `${row.name}${span.detail ? ` · ${span.detail}` : ""} · ${KIND_LABEL[span.kind]}${running ? " · running" : ""}${row.ok ? "" : " · failed"}`,
2921
- children: [/* @__PURE__ */ jsxs("span", {
2922
- className: `fw-span-name${isAgent || row.hasChildren ? " fw-strong" : ""}`,
2923
- children: [running && /* @__PURE__ */ jsx("span", {
2924
- className: "fw-spinner",
2925
- role: "status",
2926
- "aria-label": "running"
2927
- }), /* @__PURE__ */ jsx("span", {
2928
- className: "fw-name-text",
2929
- children: row.name
2930
- })]
2931
- }), agentView && renderIcon && /* @__PURE__ */ jsx("span", {
2932
- className: "fw-detail-icon",
2933
- "aria-hidden": true,
2934
- children: renderIcon(span)
2935
- })]
2936
+ children: [
2937
+ /* @__PURE__ */ jsxs("span", {
2938
+ className: `fw-span-name${isAgent || row.hasChildren ? " fw-strong" : ""}`,
2939
+ children: [running && /* @__PURE__ */ jsx("span", {
2940
+ className: "fw-spinner",
2941
+ role: "status",
2942
+ "aria-label": "running"
2943
+ }), /* @__PURE__ */ jsx("span", {
2944
+ className: "fw-name-text",
2945
+ children: row.name
2946
+ })]
2947
+ }),
2948
+ agentView && span.detail && /* @__PURE__ */ jsx("span", {
2949
+ className: "fw-subtitle",
2950
+ children: span.detail
2951
+ }),
2952
+ agentView && renderIcon && /* @__PURE__ */ jsx("span", {
2953
+ className: "fw-detail-icon",
2954
+ "aria-hidden": true,
2955
+ children: renderIcon(span)
2956
+ })
2957
+ ]
2936
2958
  }),
2937
2959
  folded && /* @__PURE__ */ jsxs("span", {
2938
2960
  className: "fw-count",
@@ -2992,7 +3014,7 @@ const FlowRow = memo(function FlowRow(props) {
2992
3014
  });
2993
3015
  }, (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.active === b.active && a.focusable === b.focusable && a.selected === b.selected && a.sticky === b.sticky && a.renderIcon === b.renderIcon && (a.clockMs === b.clockMs || !open(a.row, a.clockMs) && !open(b.row, b.clockMs)));
2994
3016
  /** An elbow connector between two rows at one moment, drawn in the row's tone; appears when the clock reaches it. */
2995
- function LinkMark({ link, rowHeight }) {
3017
+ function LinkMark({ link, rowHeight, hot }) {
2996
3018
  const upper = Math.min(link.from, link.to), lower = Math.max(link.from, link.to);
2997
3019
  const style = {
2998
3020
  ...at(link.atMs, link.atMs),
@@ -3000,7 +3022,7 @@ function LinkMark({ link, rowHeight }) {
3000
3022
  height: (lower - upper) * rowHeight - 5
3001
3023
  };
3002
3024
  return /* @__PURE__ */ jsx("span", {
3003
- className: `fw-link fw-link-${link.dir}`,
3025
+ className: `fw-link fw-link-${link.dir}${hot ? " fw-link-hot" : ""}`,
3004
3026
  "data-tone": toneKey(link.tone),
3005
3027
  style,
3006
3028
  "aria-hidden": true
@@ -3397,7 +3419,8 @@ function FlowBody(props) {
3397
3419
  "aria-hidden": true,
3398
3420
  children: links.filter((link) => Math.max(link.from, link.to) >= first && Math.min(link.from, link.to) <= last).map((link) => /* @__PURE__ */ jsx(LinkMark, {
3399
3421
  link,
3400
- rowHeight
3422
+ rowHeight,
3423
+ hot: props.focusId !== void 0 && (rows[link.from]?.id === props.focusId || rows[link.to]?.id === props.focusId)
3401
3424
  }, link.id))
3402
3425
  }), rows.slice(first, last).map((item, offset) => row(item, first + offset))]
3403
3426
  }), rows.length === 0 && /* @__PURE__ */ jsx("p", {
@@ -3718,10 +3741,11 @@ function OutcomeLegend({ agents }) {
3718
3741
  items.set(outcome.label, item);
3719
3742
  }
3720
3743
  if (items.size === 0) return null;
3721
- return /* @__PURE__ */ jsx("ul", {
3744
+ const failedSteps = agents.reduce((sum, row) => sum + row.own.reduce((n, mark) => n + (mark.kind === "failed" ? mark.count : 0), 0), 0);
3745
+ return /* @__PURE__ */ jsxs("ul", {
3722
3746
  className: "fw-legend",
3723
3747
  "aria-label": "Outcomes",
3724
- children: [...items].map(([label, item]) => /* @__PURE__ */ jsxs("li", { children: [
3748
+ children: [[...items].map(([label, item]) => /* @__PURE__ */ jsxs("li", { children: [
3725
3749
  /* @__PURE__ */ jsx("span", {
3726
3750
  className: "fw-swatch",
3727
3751
  "data-tone": "0",
@@ -3731,7 +3755,15 @@ function OutcomeLegend({ agents }) {
3731
3755
  label,
3732
3756
  " ",
3733
3757
  /* @__PURE__ */ jsx("b", { children: item.count })
3734
- ] }, label))
3758
+ ] }, label)), failedSteps > 0 && /* @__PURE__ */ jsxs("li", { children: [
3759
+ /* @__PURE__ */ jsx("span", {
3760
+ className: "fw-swatch-step",
3761
+ "aria-hidden": true,
3762
+ children: /* @__PURE__ */ jsx(FailGlyph, {})
3763
+ }),
3764
+ "Failed step ",
3765
+ /* @__PURE__ */ jsx("b", { children: failedSteps })
3766
+ ] })]
3735
3767
  });
3736
3768
  }
3737
3769
  const headlineSeconds = (ms) => ms < 6e4 ? `${(ms / 1e3).toFixed(1)}s` : formatSpanDuration(ms);
@@ -4160,6 +4192,7 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
4160
4192
  slowestId: slowest?.id,
4161
4193
  activeId,
4162
4194
  selectedId: card?.pinned ? card.target : void 0,
4195
+ focusId: cardAgent?.id ?? (view === "agents" ? activeId : void 0),
4163
4196
  clockMs,
4164
4197
  window: win,
4165
4198
  trackPx,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/charts",
3
- "version": "0.18.0",
3
+ "version": "0.19.0",
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",