@tangle-network/charts 2.0.6 → 2.0.9

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
@@ -337,9 +337,9 @@ const trace = otlpFlowTrace(spans, { price: catalogPrice(await (await fetch(cata
337
337
  <FlowWaterfall trace={trace} live={running} defaultView="agents" renderIcon={brandMark} />;
338
338
  ```
339
339
 
340
- `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. The card is a table of what the row recorded, every value with its unit: model, duration, tokens in, out and cached (the span's `promptTokens`, `completionTokens` and `cachedTokens`, else the sum of its children's), LLM latency (median and p95 of its model calls), cost, LLM calls, tool calls by tool (the name before " · "), failed steps, and retries and restarts (`retryOf`, `marks` of kind `restart`); a value nobody recorded is left out. `facts` (`[{ label, value }]`) adds the host's rows after it. The card opens at the pointer, above or below its row, whichever side fits the viewport. `onTimeChange` reports where the reader scrubbed (null when live), and markers double as an event track (click, N or P to jump). A partly priced trace shows its floor and coverage (`costCoverage`), and `billedUsd` shows the real charge beside the list price. 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.
340
+ `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. The card is a table of what the row recorded, every value with its unit: model, duration, tokens in, out and cached (the span's `promptTokens`, `completionTokens` and `cachedTokens`, else the sum of its children's), LLM latency (median and p95 of its model calls), cost, LLM calls, tool calls by tool (the name before " · "), failed steps, and retries and restarts (`retryOf`, `marks` of kind `restart`); a value nobody recorded is left out. `facts` (`[{ label, value }]`) supplies authoritative host rows: a matching label replaces the inferred metric, case-insensitively, including explicit unknown or zero values; additional labels append. The last explicit value wins for a repeated label. Agent lane summaries also use `Model calls` and `Tool calls` facts verbatim (`Model calls: 413`), including unknowns and zero; no count is parsed from a formatted fact. Model/Provider identity and Ran for/Cost end labels also honor facts verbatim; full values remain in the card and label title when phone metadata clips. Opaque duration/cost facts do not compete in numeric superlatives. Family lanes summarize agent and failure counts. Synthetic family cards show `Not recorded` for model identity, model/tool counts, model latency, failures or retries when any member supplies that authoritative fact: formatted counts and percentiles cannot be aggregated. Family tokens become unknown if a member supplies a Tokens fact without its own numeric usage. Cost rollups use full numeric span prices when a member supplies a Cost fact; partial known prices retain their lower bound, and an entirely opaque price is Not recorded. Strings and percentiles are never parsed or summed. The card opens at the pointer, above or below its row, whichever side fits the viewport. `onTimeChange` reports where the reader scrubbed (null when live), and markers double as an event track (click, N or P to jump). A partly priced trace shows its floor and coverage (`costCoverage`), and `billedUsd` shows the real charge beside the list price. 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.
341
341
 
342
- 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. `activityAt` supplies trace-relative event times and `medianEventGapMs` preserves producer cadence before tail truncation; without it, child-step activity supplies the samples. Silence beyond five minutes and three median event gaps hatches the tail and names the quiet duration. The Errors filter expands matching agents and steps; its rug locates each error, marking unreported times separately. Containers below 600px use stacked 44px rows and a Key disclosure. Search supports Cmd/Ctrl+F and keeps ancestor context. Token totals are optional: omit unknown counts, retain measured zero, and guard optional token fields when reading a trace. Missing, partial and nonfinite usage displays a dash, including during playback. 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.
342
+ 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. `activityAt` supplies trace-relative event times and `medianEventGapMs` preserves producer cadence before tail truncation; without it, child-step activity supplies the samples. Silence beyond five minutes and three median event gaps hatches the tail and names the quiet duration. The Errors filter expands matching agents and steps; its rug locates each error, marking unreported times separately. Containers below 600px use stacked 52px rows and a Key disclosure. Search supports Cmd/Ctrl+F and keeps ancestor context. Token totals are optional: omit unknown counts, retain measured zero, and guard optional token fields when reading a trace. Missing, partial and nonfinite usage displays a dash, including during playback. 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.
343
343
 
344
344
  `scripts/flow-waterfall-demo.mjs` serves the demo routes (reference-shaped fixtures, recorded runs, a synthetic load trace and OTLP files from `FW_DATA_DIR`); `scripts/flow-waterfall-capture.mjs` captures frames, hover cards, fps, first paint and recordings, and with `FW_LIVE_RELAY` records a live session through `scripts/flow-waterfall-live-relay.mjs` with every span's event-to-pixel time.
345
345
 
@@ -343,7 +343,7 @@ interface FlowSpan {
343
343
  handoffTo?: string;
344
344
  /** The host's verdict on an agent span, such as shipped or crashed: colors its lane and joins the legend. */
345
345
  outcome?: FlowOutcome;
346
- /** Host facts shown as extra rows of the card's table, such as releases or commits; a label the table already has is skipped. */
346
+ /** Authoritative host rows: replace same-label inferred card metrics and lane model/count/duration/cost labels, case-insensitively; unknowns stay unknown. */
347
347
  facts?: readonly {
348
348
  label: string;
349
349
  value: string;
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-CF4Q07cA.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-VOGEAdl8.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. */
@@ -271,7 +271,7 @@
271
271
  left: min(calc(var(--x0) + max(var(--w), 3px) + 4px), calc(100% - var(--fw-end-w, 0px) - 4px)); padding: 0 4px; border-radius: var(--radius-tag);
272
272
  background: linear-gradient(var(--fw-tint, transparent) 0 0), var(--fw-surface); white-space: nowrap; pointer-events: none; }
273
273
  .fw-root .fw-outcome-end { display: inline-flex; pointer-events: auto; }
274
- .fw-root .fw-duration { font-size: 14px; color: var(--fw-muted); font-variant-numeric: tabular-nums; }
274
+ .fw-root .fw-duration { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 14px; color: var(--fw-muted); font-variant-numeric: tabular-nums; }
275
275
  .fw-root .fw-k-failed ~ .fw-end .fw-duration { color: var(--fw-failed); }
276
276
  .fw-root .fw-badge { display: inline-flex; flex: none; height: 18px; padding: 0 6px; border-radius: var(--radius-pill); font-size: 12px; line-height: 18px; font-weight: 500; color: var(--fw-muted); background: transparent; box-shadow: inset 0 0 0 1px var(--fw-border); }
277
277
 
@@ -416,7 +416,7 @@
416
416
  .fw-root .fw-view-agents :is(.fw-subtitle, .fw-seg-label) { display: none; }
417
417
  .fw-root .fw-view-agents .fw-label { padding-left: calc(8px + var(--depth) * 12px); }
418
418
  .fw-root .fw-lane { grid-column: 2; }
419
- .fw-root .fw-end { top: 20px; height: 18px; left: auto; right: 4px; }
419
+ .fw-root .fw-end { top: 20px; height: 18px; left: auto; right: 4px; max-width: calc(100% - 144px); }
420
420
  .fw-root .fw-time { display: block; grid-column: 2; position: absolute; top: -12px; left: var(--fw-progress); transform: translateX(calc(-1 * var(--fw-progress))); width: max-content; max-width: 100%; background: var(--fw-surface); }
421
421
  .fw-root .fw-playback { position: relative; padding: 12px 8px 8px; }
422
422
  .fw-root .fw-overview { margin-top: 12px; grid-template-columns: 0 minmax(0, 1fr); column-gap: 0; }
@@ -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, n as FlowOutcome, 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-CF4Q07cA.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, n as FlowOutcome, 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-VOGEAdl8.js";
2
2
  import { CSSProperties, ReactElement, ReactNode } from "react";
3
3
  //#region src/react/sparkline.d.ts
4
4
  type SparklineDirection = "rising" | "falling" | "flat";
@@ -6500,6 +6500,7 @@ const toolOf = (name) => name?.split(" · ")[0];
6500
6500
  const toneKey = (tone) => tone > 0 && tone <= 5 ? tone : 0;
6501
6501
  const spanFailed = (span) => span.status === "failed";
6502
6502
  const spanRunning = (span) => span.status === "running";
6503
+ const spanFact = (span, label) => new Map((span.facts ?? []).map((fact) => [fact.label.toLowerCase(), fact.value])).get(label.toLowerCase());
6503
6504
  const ordered = (span) => Number.isFinite(span.startMs) && Number.isFinite(span.endMs) && span.endMs >= span.startMs;
6504
6505
  /** 542ms, 6.9s, 23s, 1m 22s, 2h 05m: the precision a reader compares at each scale. */
6505
6506
  function formatSpanDuration(ms) {
@@ -6570,9 +6571,10 @@ function flowWaterfallRows(trace, resolutionMs) {
6570
6571
  tone: state === "running" ? "live" : state === "done" ? "ok" : state === "needs-you" ? "warn" : state === "failed" ? "failed" : "neutral",
6571
6572
  ...state === "unknown" ? { ring: true } : {}
6572
6573
  };
6573
- const span = recorded.outcome ? recorded : {
6574
+ const span = {
6574
6575
  ...recorded,
6575
- outcome
6576
+ outcome: recorded.outcome ?? outcome,
6577
+ detail: spanFact(recorded, recorded.kind === "tool" ? "Provider" : "Model") ?? recorded.detail
6576
6578
  };
6577
6579
  return {
6578
6580
  span,
@@ -6825,12 +6827,14 @@ function agentCallouts(agents, clockMs) {
6825
6827
  for (const row of agents) {
6826
6828
  if (row.durationLabel === "—" || row.span.startMs > clockMs || row.kind !== "pipeline" || row.hasAgents || row.id.startsWith("family:") || row.id.startsWith("run:")) continue;
6827
6829
  const ms = (spanRunning(row.span) ? Math.max(row.span.endMs, clockMs) : Math.min(row.span.endMs, clockMs)) - row.span.startMs;
6828
- timed += 1;
6829
- if (ms > slowestMs) {
6830
- slowest = row;
6831
- slowestMs = ms;
6830
+ if (spanFact(row.span, "Ran for") === void 0) {
6831
+ timed += 1;
6832
+ if (ms > slowestMs) {
6833
+ slowest = row;
6834
+ slowestMs = ms;
6835
+ }
6832
6836
  }
6833
- const cost = row.span.costUsd ?? row.rollupCostUsd;
6837
+ const cost = spanFact(row.span, "Cost") === void 0 ? row.span.costUsd ?? row.rollupCostUsd : void 0;
6834
6838
  if (typeof cost !== "number" || !Number.isFinite(cost)) continue;
6835
6839
  priced += 1;
6836
6840
  if (cost > top) {
@@ -6949,9 +6953,24 @@ function agentFamilies(agents, open) {
6949
6953
  const startMs = Math.min(...members.map((row) => row.span.startMs));
6950
6954
  const endMs = Math.max(...members.map((row) => row.span.endMs));
6951
6955
  const running = members.some((row) => spanRunning(row.span));
6952
- const known = members.map((row) => row.span.costUsd ?? row.rollupCostUsd).filter((cost) => cost !== void 0);
6956
+ const known = members.map((row) => row.span.costUsd ?? (spanFact(row.span, "Cost") === void 0 ? row.rollupCostUsd : void 0)).filter((cost) => cost !== void 0 && Number.isFinite(cost));
6953
6957
  const failed = members.filter((row) => row.span.outcome ? row.span.outcome.tone === "failed" : !row.ok).length;
6954
6958
  const outcome = members.every((row) => row.span.outcome && row.span.outcome.tone === members[0].span.outcome?.tone && !row.span.outcome.ring === !members[0].span.outcome?.ring) ? members[0].span.outcome : void 0;
6959
+ const facts = members.flatMap((row) => (row.span.facts ?? []).filter(({ label }) => [
6960
+ "model",
6961
+ "model calls",
6962
+ "tool calls",
6963
+ "model latency",
6964
+ "failures",
6965
+ "retries"
6966
+ ].includes(label.toLowerCase()) || label.toLowerCase() === "tokens" && ![
6967
+ row.span.promptTokens,
6968
+ row.span.completionTokens,
6969
+ row.span.cachedTokens
6970
+ ].some(Number.isFinite) || label.toLowerCase() === "cost" && known.length === 0)).map(({ label }) => ({
6971
+ label,
6972
+ value: "Not recorded"
6973
+ }));
6955
6974
  out.push({
6956
6975
  costFloor: known.length < members.length || members.some((row) => row.costFloor),
6957
6976
  id,
@@ -6992,7 +7011,8 @@ function agentFamilies(agents, open) {
6992
7011
  activityAt: members.flatMap((row) => row.span.activityAt ?? row.own.flatMap((mark) => [mark.startMs, mark.endMs])),
6993
7012
  status: running ? "running" : failed ? "failed" : "done",
6994
7013
  ...outcome ? { outcome } : {},
6995
- detail: members[0].span.detail,
7014
+ detail: members.every((row) => row.span.detail === members[0].span.detail) ? members[0].span.detail : void 0,
7015
+ facts,
6996
7016
  meta: {
6997
7017
  failedAgents: failed,
6998
7018
  costPartial: known.length < members.length,
@@ -7025,13 +7045,17 @@ function agentSummary(row) {
7025
7045
  const failed = row.own.filter((mark) => mark.kind === "failed").length;
7026
7046
  return [failed ? `${failed} failed` : "all done"];
7027
7047
  }
7028
- const calls = row.counts?.model ?? 0;
7029
- const tools = row.counts?.tool ?? 0;
7030
- return [
7031
- row.span.outcome?.label ?? (spanRunning(row.span) ? "running" : !row.ok ? "failed" : row.kind === "pipeline" ? "" : "done"),
7032
- calls ? `${calls} model ${calls === 1 ? "call" : "calls"}` : "",
7033
- tools ? `${tools} tool ${tools === 1 ? "call" : "calls"}` : ""
7034
- ].filter(Boolean);
7048
+ const facts = new Map((row.span.facts ?? []).map(({ label, value }) => [label.toLowerCase(), value]));
7049
+ const calls = [[
7050
+ "Model calls",
7051
+ row.counts?.model,
7052
+ "model"
7053
+ ], [
7054
+ "Tool calls",
7055
+ row.counts?.tool,
7056
+ "tool"
7057
+ ]].map(([label, count, kind]) => facts.has(label.toLowerCase()) ? `${label}: ${facts.get(label.toLowerCase())}` : count ? `${count} ${kind} ${count === 1 ? "call" : "calls"}` : "");
7058
+ return [row.span.outcome?.label ?? (spanRunning(row.span) ? "running" : !row.ok ? "failed" : row.kind === "pipeline" ? "" : "done"), ...calls].filter(Boolean);
7035
7059
  }
7036
7060
  /**
7037
7061
  * The agent view's rows: families folded until opened, and an opened agent's own steps (its turns and tool calls, not its
@@ -7115,7 +7139,7 @@ function drawnRow(row, visible) {
7115
7139
  } : row;
7116
7140
  }
7117
7141
  /** A row's cost as printed: "≥" when it is a floor. */
7118
- const rowCostText = (row, cost) => `${row.costFloor ? "≥" : ""}${formatCost(cost)}`;
7142
+ const rowCostText = (row, cost) => spanFact(row.span, "Cost") ?? (cost === void 0 ? "" : `${row.costFloor ? "≥" : ""}${formatCost(cost)}`);
7119
7143
  /** Quiet is elapsed silence beyond both five minutes and three median event gaps. */
7120
7144
  function quietActivity(row, now) {
7121
7145
  const times = [...row.span.activityAt ?? row.own.flatMap((mark) => [mark.startMs, mark.endMs])].filter(Number.isFinite).sort((a, b) => a - b);
@@ -7346,8 +7370,8 @@ function metrics(row, clockMs, full) {
7346
7370
  const restarts = span.marks?.filter((mark) => mark.kind === "restart").length ?? 0;
7347
7371
  if (counts?.retried || restarts) out.push(["Retries", [counts?.retried ? plural(counts.retried, "step retried", "steps retried") : "", restarts ? plural(restarts, "restart") : ""].filter(Boolean).join(" · ")]);
7348
7372
  if (full && row.hasChildren) out.push(["Steps", row.descendants.toLocaleString()]);
7349
- for (const fact of span.facts ?? []) if (!out.some(([label]) => label.toLowerCase() === fact.label.toLowerCase())) out.push([fact.label, fact.value]);
7350
- return out;
7373
+ const facts = (span.facts ?? []).map(({ label, value }) => [label, value]);
7374
+ return [...new Map([...out, ...facts].map((row) => [row[0].toLowerCase(), row])).values()];
7351
7375
  }
7352
7376
  function Table({ rows }) {
7353
7377
  return /* @__PURE__ */ jsx("dl", {
@@ -7358,7 +7382,7 @@ function Table({ rows }) {
7358
7382
  /** The span card: what ran, for how long, on what tokens and at what recorded cost, then its input and output. */
7359
7383
  function SpanCard({ id, row, agent, clockMs, left, top, pinned, onClose, renderIcon, compact }) {
7360
7384
  const span = row.span;
7361
- const failure = span.facts?.find((fact) => fact.label.toLowerCase() === "failure")?.value;
7385
+ const failure = spanFact(span, "Failure");
7362
7386
  const state = runStatus(span.status ?? "unknown");
7363
7387
  const meta = statusMeta(state);
7364
7388
  const status = meta.label;
@@ -7572,11 +7596,8 @@ const open = (row, clockMs) => spanRunning(row.span) || row.span.startMs <= cloc
7572
7596
  function endLabel(row, clockMs) {
7573
7597
  const span = row.span;
7574
7598
  if (row.durationLabel === "—") return "—";
7575
- if (row.approx && span.endMs === span.startMs && !spanRunning(span)) return span.costUsd === void 0 ? "" : rowCostText(row, span.costUsd);
7576
- if (!spanRunning(span) && clockMs < span.endMs) return `${formatSpanDuration(Math.max(0, clockMs - span.startMs))}${row.approx ? "~" : ""}`;
7577
- const duration = spanRunning(span) ? `${formatSpanDuration(Math.max(span.endMs, clockMs) - span.startMs)}${row.approx ? "~" : ""}` : row.durationLabel;
7578
- const cost = span.costUsd ?? (row.rollupCostUsd ? row.rollupCostUsd : void 0);
7579
- return cost === void 0 ? duration : `${duration} · ${rowCostText(row, cost)}`;
7599
+ if (row.approx && span.endMs === span.startMs && !spanRunning(span)) return rowCostText(row, span.costUsd);
7600
+ return [spanFact(span, "Ran for") ?? (!spanRunning(span) && clockMs < span.endMs ? `${formatSpanDuration(Math.max(0, clockMs - span.startMs))}${row.approx ? "~" : ""}` : spanRunning(span) ? `${formatSpanDuration(Math.max(span.endMs, clockMs) - span.startMs)}${row.approx ? "~" : ""}` : row.durationLabel), !spanRunning(span) && clockMs < span.endMs && spanFact(span, "Cost") === void 0 ? "" : rowCostText(row, span.costUsd ?? row.rollupCostUsd)].filter(Boolean).join(" · ");
7580
7601
  }
7581
7602
  /** One trace row. Geometry comes from CSS variables, so a row re-renders only when its own state changes. */
7582
7603
  const FlowRow = memo(function FlowRow(props) {
@@ -7826,6 +7847,7 @@ const FlowRow = memo(function FlowRow(props) {
7826
7847
  })
7827
7848
  }), /* @__PURE__ */ jsx("span", {
7828
7849
  className: "fw-duration",
7850
+ title: label,
7829
7851
  children: label
7830
7852
  })]
7831
7853
  })
@@ -9301,7 +9323,7 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
9301
9323
  const errorEnds = Array.from({ length: errors ?? 0 }, (_, index) => Number.isFinite(errorSpans[index]?.endMs) ? errorSpans[index].endMs : void 0);
9302
9324
  const done = !isLive && !trace.spans.some(spanRunning);
9303
9325
  const shown = (row) => row && drawnRow(row, visible);
9304
- const cardRow = shown(card ? byId.get(card.target) : void 0);
9326
+ const cardRow = shown(card ? byId.get(card.target) ?? visible.find((row) => row.id === card.target) : void 0);
9305
9327
  const cardAgent = cardRow && (() => {
9306
9328
  for (let cursor = cardRow; cursor; cursor = cursor.parentId ? byId.get(cursor.parentId) : void 0) if (cursor.kind === "pipeline") return shown(cursor);
9307
9329
  })();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/charts",
3
- "version": "2.0.6",
3
+ "version": "2.0.9",
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",