@tangle-network/charts 2.0.0 → 2.0.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.
package/README.md CHANGED
@@ -6,6 +6,8 @@ The React entry also exports `DayBars`, `HBars`, `Histogram`, `Lines`, `Meter`,
6
6
 
7
7
  `FlowWaterfall` from `@tangle-network/charts/react` is the shared live trace inspector: an agent timeline (one row per agent, elbow handoffs, hatched waiting) and a virtualized span waterfall over one clock. It owns hierarchy layout, folding, search, playback, zoom, span cards and running-span growth. Producers own identity, `parentId`, explicit `status` and observed totals. Import `@tangle-network/charts/react/flow-waterfall.css`; its `fw-*` classes and six-field summary support hosts without a Tailwind build. Every color, radius and shadow in it is a Brand role (text, card, hairline, well, chart grid, status inks, the categorical tones for agents), so the host loads `@tangle-network/brand` `tokens.css` (or `legacy-light.css`) and sets its theme; the stylesheet has no color of its own. The React-free root exports the `FlowSpan` and `FlowTrace` types.
8
8
 
9
+ `FlowWaterfall` accepts optional `graph` and `latestEvents` React nodes. Supplying `graph` adds Graph to its existing Agents/Steps view control, including `defaultView="graph"`. The trace clock and controlled selection survive switches. Graph hides waterfall-only controls; `latestEvents` stays mounted in the header across all views.
10
+
9
11
  `WaterfallHeader`, `WaterfallTimeAxis`, `WaterfallBar` and `WaterfallRow` remain available for existing consumers that own their own hierarchy and selection. All use the same `waterfallSpanGeometry` and `waterfallTicks`. Existing fixed-width cells, tones, formatting, attribution slots and unordered-time `—` marks are preserved. Optional fluid tracks, inner-bar colour/class, overlay and clipping fields support the shared inspector without changing existing calls. The primitives were extracted from Intelligence under [brand #211](https://github.com/tangle-network/brand/pull/211); the shared inspector extends that presentation ownership while leaving failure interpretation with producers.
10
12
 
11
13
  Five figures cover the decisions a benchmark reader makes:
@@ -350,3 +352,4 @@ CHARTS_REACT_VERSION=19 CHARTS_PROOF_DIR=/tmp/charts-react19 node scripts/charts
350
352
  ```
351
353
 
352
354
  Consumers: agent-record adapts record provenance; agent-app supplies application trace producers; Creative consumes both. Intelligence already uses the row/axis primitives with its controlled virtualized tree. Tangle Website’s `RecordedRunWaterfall` uses the Header/Row primitives; Blueprint Agent’s execution timeline is a separately owned follow-up. Keep these existing primitives exported. Agent-record’s earlier agent-app dependency increased its installed graph from 134 to 256 package versions (tangle-tools #1674); adopting the lower chart owner removes that framework dependency, with the actual new graph measured by the deployment owner.
355
+
@@ -21,8 +21,8 @@
21
21
  --fw-h1: light-dark(oklch(from var(--tone-blue-icon) .58 .11 calc(h + 39)), oklch(from var(--tone-blue-icon) .67 .11 calc(h + 39)));
22
22
  --fw-h2: light-dark(oklch(from var(--tone-violet-icon) .55 .11 h), oklch(from var(--tone-violet-icon) .70 .11 h));
23
23
  --fw-h3: light-dark(oklch(from var(--tone-pink-icon) .55 .11 h), oklch(from var(--tone-pink-icon) .67 .11 calc(h - 16)));
24
- --fw-h4: light-dark(oklch(from var(--tone-cyan-icon) .60 .11 calc(h - 16.256)), oklch(from var(--tone-cyan-icon) .67 .11 calc(h - 26.148)));
25
- --fw-h5: light-dark(oklch(from var(--tone-blue-icon) .58 .03 calc(h + 37.6)), oklch(from var(--tone-blue-icon) .67 .03 calc(h + 29.5)));
24
+ --fw-h4: light-dark(oklch(from var(--tone-cyan-icon) .578 .11 calc(h - 22.894)), oklch(from var(--tone-cyan-icon) .67 .11 calc(h - 26.148)));
25
+ --fw-h5: light-dark(oklch(from var(--tone-blue-icon) .58 .11 calc(h + 37.6)), oklch(from var(--tone-blue-icon) .67 .11 calc(h + 29.5)));
26
26
  --fw-ease: var(--ease-standard, cubic-bezier(.2,.8,.2,1));
27
27
  --fw-bar-h: 20px;
28
28
  --fw-label-col: min(var(--fw-name-w), 42%);
@@ -83,11 +83,13 @@
83
83
  .fw-root .fw-search-input::placeholder { color: var(--fw-muted); }
84
84
  .fw-root .fw-search-input::-webkit-search-cancel-button { display: none; }
85
85
 
86
- .fw-root .fw-summary { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); border-bottom: 1px solid var(--fw-border); }
86
+ .fw-root .fw-summary { display: flex; flex-wrap: wrap; align-items: center; border-bottom: 1px solid var(--fw-border); }
87
87
  .fw-root .fw-summary > div { padding: 12px 16px; min-width: 0; }
88
- .fw-root .fw-summary > div + div { border-left: 1px solid var(--fw-border); }
89
- .fw-root .fw-summary dt { font-size: 14px; line-height: 18px; color: var(--fw-muted); }
90
- .fw-root .fw-value { margin-top: 4px; font-size: 18px; line-height: 24px; font-weight: 600; letter-spacing: -.01em; font-variant-numeric: tabular-nums; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
88
+ .fw-root .fw-summary > div:nth-child(-n+2) { flex: 1; }
89
+ .fw-root .fw-summary > div:nth-child(n+3) { display: flex; align-items: baseline; gap: 4px; padding: 8px 6px; color: var(--fw-muted); }
90
+ .fw-root .fw-summary dt { font-size: 14px; line-height: 20px; color: var(--fw-muted); }
91
+ .fw-root .fw-value { margin-top: 4px; font-size: 28px; line-height: 34px; font-weight: 600; letter-spacing: -.02em; font-variant-numeric: tabular-nums; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
92
+ .fw-root .fw-summary > div:nth-child(n+3) .fw-value { margin: 0; font-size: 14px; line-height: 20px; font-weight: 500; letter-spacing: normal; }
91
93
  .fw-root .fw-has-errors .fw-value { color: var(--fw-failed); }
92
94
 
93
95
  .fw-root .fw-axis { display: grid; grid-template-columns: var(--fw-label-col) minmax(0, 1fr); height: 32px; color: var(--fw-muted); font-size: 14px; }
@@ -203,7 +205,7 @@
203
205
  .fw-root :is(.fw-bar, .fw-seg):hover { filter: brightness(1.18); box-shadow: inset 0 0 0 1px var(--line), 0 0 0 2px color-mix(in oklab, var(--line) 55%, transparent); z-index: 1; }
204
206
  .fw-root .fw-bar.fw-k-tool { background: var(--dim); box-shadow: inset 0 0 0 1px var(--dim-line); }
205
207
  .fw-root .fw-bar.fw-k-handoff { background: var(--line); height: 8px; top: calc(50% + var(--fw-bar-shift) - 4px); box-shadow: none; }
206
- .fw-root .fw-bar.fw-k-waiting { background: repeating-linear-gradient(-45deg, var(--fw-hatch) 0 1px, transparent 1px 5px); box-shadow: inset 0 0 0 1px var(--fw-hatch); }
208
+ .fw-root .fw-bar.fw-k-waiting, .fw-root .fw-kinds .fw-swatch-wait { background: repeating-linear-gradient(-45deg, var(--fw-hatch) 0 1px, transparent 1px 5px); box-shadow: inset 0 0 0 1px var(--fw-hatch); }
207
209
  .fw-root .fw-bar.fw-k-failed { background: color-mix(in oklab, var(--fw-failed) 22%, var(--fw-surface)); box-shadow: inset 0 0 0 1px var(--fw-failed); color: var(--fw-failed); }
208
210
  .fw-root .fw-bar.fw-k-pipeline { background: linear-gradient(90deg, var(--fw-neutral-bar), color-mix(in oklab, var(--fw-neutral-bar) 55%, var(--fw-surface))); box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--fw-text) 12%, transparent); }
209
211
  .fw-root .fw-bar.fw-k-pipeline.fw-has-marks { background: repeating-linear-gradient(-45deg, var(--fw-hatch) 0 1px, transparent 1px 5px); box-shadow: inset 0 0 0 1px var(--fw-hatch); }
@@ -227,7 +229,7 @@
227
229
  .fw-root.fw-fill-agent .fw-kinds .fw-swatch-model { background: var(--model-fill); }
228
230
  .fw-root.fw-fill-agent .fw-kinds .fw-swatch-tool { background: var(--tool-fill); }
229
231
  .fw-root .fw-kinds .fw-swatch-tool { width: 12px; height: 8px; border-radius: 2px; background: var(--fw-kind-tool); }
230
- .fw-root .fw-kinds .fw-swatch-wait { width: 12px; height: 8px; border-radius: 2px; background: color-mix(in oklab, var(--fw-text) 12%, transparent); box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--fw-text) 18%, transparent); }
232
+ .fw-root .fw-kinds .fw-swatch-wait { width: 12px; height: 8px; border-radius: 2px; }
231
233
  .fw-root .fw-kinds .fw-swatch-event { width: 8px; height: 8px; border-radius: 50%; background: var(--fw-muted); box-shadow: 0 0 0 2px var(--fw-surface); }
232
234
  .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; }
233
235
  .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; }
@@ -266,11 +268,11 @@
266
268
  .fw-root .fw-glyph svg { width: 11px; height: 11px; stroke-width: 2; }
267
269
  .fw-root .fw-seg-model .fw-glyph, .fw-root .fw-k-model .fw-glyph { color: light-dark(var(--tone), var(--fw-text)); }
268
270
  .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);
269
- left: min(calc(var(--x0) + max(var(--w), 3px) + 4px), calc(100% - var(--fw-end-w, 0px) - var(--fw-badge-w, 0px) - 4px)); padding: 0 4px; border-radius: var(--radius-tag);
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);
270
272
  background: linear-gradient(var(--fw-tint, transparent) 0 0), var(--fw-surface); white-space: nowrap; pointer-events: none; }
271
273
  .fw-root .fw-duration { font-size: 14px; color: var(--fw-muted); font-variant-numeric: tabular-nums; }
272
274
  .fw-root .fw-k-failed ~ .fw-end .fw-duration { color: var(--fw-failed); }
273
- .fw-root .fw-badge { height: 20px; padding: 0 8px; border-radius: var(--radius-pill); font-size: 14px; line-height: 20px; font-weight: 500; color: var(--fw-muted); background: transparent; box-shadow: inset 0 0 0 1px var(--fw-border); }
275
+ .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); }
274
276
 
275
277
  .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; }
276
278
  .fw-root .fw-link {
@@ -384,10 +386,7 @@
384
386
  .fw-root .fw-card .fw-agent-dot:not([data-outcome]) { background: transparent; box-shadow: inset 0 0 0 1.5px var(--fw-muted); }
385
387
  @container flow-waterfall (max-width: 599px) {
386
388
  .fw-root > * { --fw-label-col: 0px; --fw-bar-shift: 8px; --fw-bar-h: 8px; --fw-row-h: 44px; }
387
- .fw-root .fw-summary { grid-template-columns: repeat(3, minmax(0, 1fr)); }
388
- .fw-root .fw-summary > div { padding: 8px; }
389
- .fw-root .fw-summary > div:nth-child(4) { border-left: 0; }
390
- .fw-root .fw-summary > div:nth-child(n+4) { border-top: 1px solid var(--fw-border); }
389
+ .fw-root .fw-summary > div:nth-child(-n+2) { flex-basis: 50%; padding: 6px 8px; }
391
390
  .fw-root :is(.fw-zoom, .fw-branch-controls, .fw-resize, .fw-links, .fw-gridlayer, .fw-marker-layer, .fw-subtitle, .fw-badge, .fw-icon-tile) { display: none; }
392
391
  .fw-root .fw-row-state { display: inline; }
393
392
  .fw-root .fw-key > summary { display: flex; align-items: center; min-height: 32px; cursor: pointer; }
@@ -459,7 +459,7 @@ export declare function flowWaterfallRows(trace: FlowTrace, resolutionMs?: numbe
459
459
  export declare function criticalPathIds(rows: readonly FlowWaterfallRow[]): ReadonlySet<string>;
460
460
  //#endregion
461
461
  //#region src/react/flow-waterfall-row.d.ts
462
- type FlowView = 'spans' | 'agents';
462
+ type FlowView = 'spans' | 'agents' | 'graph';
463
463
  //#endregion
464
464
  //#region src/react/flow-waterfall-time.d.ts
465
465
  interface ViewWindow {
@@ -480,6 +480,10 @@ interface FlowWaterfallProps {
480
480
  defaultCollapsed?: boolean;
481
481
  /** `agents` shows one row per agent (the agent timeline); expanding a row opens the full span tree. */
482
482
  defaultView?: FlowView;
483
+ /** Host graph rendered by the same view control, retaining the trace clock and selection. */
484
+ graph?: ReactNode;
485
+ /** Shared header feed, kept mounted across Agents, Steps and Graph. */
486
+ latestEvents?: ReactNode;
483
487
  /** Host brand marks by span; the inspector draws kind glyphs without it. */
484
488
  renderIcon?: (span: FlowSpan) => ReactNode;
485
489
  /** Scroll height of the rows; the tree is virtualized beyond it. Defaults to 20 agent rows, or 640 px of steps. */
@@ -519,7 +523,7 @@ interface FlowWaterfallProps {
519
523
  }
520
524
  /** Shared trace inspector: the agent timeline and the span waterfall over one clock.
521
525
  * Data remains producer-owned; playback, zoom and folding change presentation only. */
522
- 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;
526
+ export declare function FlowWaterfall({ trace, live, startedAt, label, defaultCollapsed, defaultView, graph, latestEvents, 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;
523
527
  //#endregion
524
528
  //#region src/react/flow-waterfall-icons.d.ts
525
529
  /** Kind glyphs drawn when the host supplies no `renderIcon`. Brand marks belong to the host. */
@@ -6822,17 +6822,15 @@ function criticalPathIds(rows) {
6822
6822
  function agentCallouts(agents, clockMs) {
6823
6823
  let slowest, slowestMs = -1, costliest, top = -1, priced = 0, timed = 0;
6824
6824
  for (const row of agents) {
6825
- if (row.durationLabel === "—" || row.span.startMs > clockMs || row.kind !== "pipeline" || row.id.startsWith("family:") || row.id.startsWith("run:")) continue;
6826
- if (row.parentId !== void 0) {
6827
- 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;
6832
- }
6825
+ if (row.durationLabel === "—" || row.span.startMs > clockMs || row.kind !== "pipeline" || row.hasAgents || row.id.startsWith("family:") || row.id.startsWith("run:")) continue;
6826
+ const ms = (spanRunning(row.span) ? Math.max(row.span.endMs, clockMs) : Math.min(row.span.endMs, clockMs)) - row.span.startMs;
6827
+ timed += 1;
6828
+ if (ms > slowestMs) {
6829
+ slowest = row;
6830
+ slowestMs = ms;
6833
6831
  }
6834
6832
  const cost = row.span.costUsd ?? row.rollupCostUsd;
6835
- if (cost === void 0) continue;
6833
+ if (typeof cost !== "number" || !Number.isFinite(cost)) continue;
6836
6834
  priced += 1;
6837
6835
  if (cost > top) {
6838
6836
  costliest = row;
@@ -6934,10 +6932,15 @@ function agentFamilies(agents, open) {
6934
6932
  const hue = (row) => row.tone <= 0 || ++next > 5 ? 0 : 1 + [...roleOf(row.name)].reduce((hash, c) => hash * 31 + c.charCodeAt(0) >>> 0, 0) % 5;
6935
6933
  for (const [role, members] of groups) {
6936
6934
  if (members.length < 2) {
6937
- out.push(...members.map((row) => ({
6935
+ const row = members[0], tone = hue(row);
6936
+ out.push({
6938
6937
  ...row,
6939
- tone: hue(row)
6940
- })));
6938
+ tone,
6939
+ own: row.own.map((mark) => ({
6940
+ ...mark,
6941
+ tone
6942
+ }))
6943
+ });
6941
6944
  continue;
6942
6945
  }
6943
6946
  const tone = hue(members[0]);
@@ -7361,6 +7364,7 @@ function Table({ rows }) {
7361
7364
  /** The span card: what ran, for how long, on what tokens and at what recorded cost, then its input and output. */
7362
7365
  function SpanCard({ id, row, agent, clockMs, left, top, pinned, onClose, renderIcon, compact }) {
7363
7366
  const span = row.span;
7367
+ const failure = span.facts?.find((fact) => fact.label.toLowerCase() === "failure")?.value;
7364
7368
  const state = runStatus(span.status ?? "unknown");
7365
7369
  const meta = statusMeta(state);
7366
7370
  const status = meta.label;
@@ -7433,14 +7437,14 @@ function SpanCard({ id, row, agent, clockMs, left, top, pinned, onClose, renderI
7433
7437
  close
7434
7438
  ]
7435
7439
  }),
7436
- /* @__PURE__ */ jsx(Table, { rows: metrics(who, clockMs, false) }),
7440
+ /* @__PURE__ */ jsx(Table, { rows: metrics(who, clockMs, false).filter(([label]) => label.toLowerCase() !== "failure") }),
7437
7441
  step && /* @__PURE__ */ jsx("p", {
7438
7442
  className: "fw-card-step",
7439
7443
  children: step
7440
7444
  }),
7441
- !row.ok && /* @__PURE__ */ jsxs("p", {
7445
+ !row.ok && /* @__PURE__ */ jsx("p", {
7442
7446
  className: "fw-card-error",
7443
- children: ["Error: ", span.output ?? who.span.output ?? "No error detail reported"]
7447
+ children: failure ? `Failure: ${failure}` : span.output ? `Recorded output: ${span.output}` : "Failure detail not reported"
7444
7448
  }),
7445
7449
  sentence && row.ok && /* @__PURE__ */ jsx("p", {
7446
7450
  className: "fw-card-result",
@@ -7487,6 +7491,10 @@ function SpanCard({ id, row, agent, clockMs, left, top, pinned, onClose, renderI
7487
7491
  ]
7488
7492
  }),
7489
7493
  /* @__PURE__ */ jsx(Table, { rows: metrics(row, clockMs, true) }),
7494
+ !row.ok && !span.output && !failure && /* @__PURE__ */ jsx("p", {
7495
+ className: "fw-card-error",
7496
+ children: "Failure detail not reported"
7497
+ }),
7490
7498
  span.input && /* @__PURE__ */ jsxs("div", {
7491
7499
  className: "fw-card-text",
7492
7500
  children: [/* @__PURE__ */ jsx("div", {
@@ -7498,7 +7506,7 @@ function SpanCard({ id, row, agent, clockMs, left, top, pinned, onClose, renderI
7498
7506
  className: "fw-card-text",
7499
7507
  children: [/* @__PURE__ */ jsx("div", {
7500
7508
  className: "fw-card-label",
7501
- children: row.ok ? "Output" : "Error"
7509
+ children: row.ok ? "Output" : "Recorded output"
7502
7510
  }), /* @__PURE__ */ jsx("p", { children: span.output })]
7503
7511
  })
7504
7512
  ]
@@ -7597,6 +7605,15 @@ const FlowRow = memo(function FlowRow(props) {
7597
7605
  const step = agentView && row.stepOf !== void 0;
7598
7606
  const opens = family || agentView && isAgent && row.own.length > 0;
7599
7607
  const outcomes = row.outcomes ?? (span.outcome ? [span.outcome] : []);
7608
+ const callouts = /* @__PURE__ */ jsxs(Fragment, { children: [slowest && /* @__PURE__ */ jsx("span", {
7609
+ className: "fw-badge",
7610
+ title: agentView ? "Longest elapsed leaf agent" : "Longest elapsed leaf step",
7611
+ children: agentView ? "Longest leaf" : "Longest step"
7612
+ }), costliest && /* @__PURE__ */ jsx("span", {
7613
+ className: "fw-badge",
7614
+ title: "Highest reported cost among priced leaf agents",
7615
+ children: "Costliest leaf"
7616
+ })] });
7600
7617
  return /* @__PURE__ */ jsxs("div", {
7601
7618
  role: "row",
7602
7619
  "data-span-id": row.id,
@@ -7616,8 +7633,7 @@ const FlowRow = memo(function FlowRow(props) {
7616
7633
  style: {
7617
7634
  "--row-y": `${top}px`,
7618
7635
  "--fw-enter-delay": `${motion?.enter ?? 0}ms`,
7619
- "--fw-end-w": `${endLabelWidth(label)}px`,
7620
- "--fw-badge-w": `${(slowest ? 80 : 0) + (costliest ? 90 : 0)}px`
7636
+ "--fw-end-w": `${endLabelWidth(label)}px`
7621
7637
  },
7622
7638
  onFocus: (event) => {
7623
7639
  if (event.target === event.currentTarget) props.onActivate(row);
@@ -7708,7 +7724,8 @@ const FlowRow = memo(function FlowRow(props) {
7708
7724
  className: "fw-name-text",
7709
7725
  children: row.name
7710
7726
  }),
7711
- (state === "failed" || state === "needs-you") && /* @__PURE__ */ jsx("span", {
7727
+ !agentView && callouts,
7728
+ state !== "unknown" && !activity.quiet && /* @__PURE__ */ jsx("span", {
7712
7729
  className: "fw-row-state",
7713
7730
  style: { color: `var(${meta.token})` },
7714
7731
  children: meta.label
@@ -7726,13 +7743,17 @@ const FlowRow = memo(function FlowRow(props) {
7726
7743
  children: renderIcon(span)
7727
7744
  }), /* @__PURE__ */ jsxs("span", {
7728
7745
  className: "fw-subtitle-tail",
7729
- children: [/* @__PURE__ */ jsx("span", {
7730
- className: "fw-subtitle-text",
7731
- children: summary[0]
7732
- }), summary.slice(1).map((part) => /* @__PURE__ */ jsxs("span", {
7733
- className: "fw-subtitle-extra",
7734
- children: ["· ", part]
7735
- }, part))]
7746
+ children: [
7747
+ callouts,
7748
+ /* @__PURE__ */ jsx("span", {
7749
+ className: "fw-subtitle-text",
7750
+ children: summary[0]
7751
+ }),
7752
+ summary.slice(1).map((part) => /* @__PURE__ */ jsxs("span", {
7753
+ className: "fw-subtitle-extra",
7754
+ children: ["· ", part]
7755
+ }, part))
7756
+ ]
7736
7757
  })]
7737
7758
  })]
7738
7759
  }),
@@ -7750,7 +7771,7 @@ const FlowRow = memo(function FlowRow(props) {
7750
7771
  role: "gridcell",
7751
7772
  className: "fw-lane",
7752
7773
  "data-waterfall-track": true,
7753
- "aria-label": `${KIND_LABEL[span.kind]}, ${label}${slowest ? `, slowest ${agentView ? "agent" : "step"}` : ""}${costliest ? ", costliest agent" : ""}${critical ? ", critical path" : ""}`,
7774
+ "aria-label": `${KIND_LABEL[span.kind]}, ${label}${slowest ? `, longest leaf ${agentView ? "agent" : "step"}` : ""}${costliest ? ", highest reported leaf-agent cost" : ""}${critical ? ", critical path" : ""}`,
7754
7775
  children: invalid ? /* @__PURE__ */ jsx("span", {
7755
7776
  className: "fw-invalid",
7756
7777
  children: "—"
@@ -7792,23 +7813,13 @@ const FlowRow = memo(function FlowRow(props) {
7792
7813
  events: props,
7793
7814
  agentView
7794
7815
  }, `${mark.id}-${i}`)),
7795
- /* @__PURE__ */ jsxs("span", {
7816
+ /* @__PURE__ */ jsx("span", {
7796
7817
  className: "fw-end",
7797
7818
  style: at(span.startMs, running ? OPEN_END : span.endMs),
7798
- children: [
7799
- /* @__PURE__ */ jsx("span", {
7800
- className: "fw-duration",
7801
- children: label
7802
- }),
7803
- slowest && /* @__PURE__ */ jsx("span", {
7804
- className: "fw-badge",
7805
- children: "Slowest"
7806
- }),
7807
- costliest && /* @__PURE__ */ jsx("span", {
7808
- className: "fw-badge",
7809
- children: "Costliest"
7810
- })
7811
- ]
7819
+ children: /* @__PURE__ */ jsx("span", {
7820
+ className: "fw-duration",
7821
+ children: label
7822
+ })
7812
7823
  })
7813
7824
  ] })
7814
7825
  })]
@@ -7960,7 +7971,8 @@ function clampWindow(view, limitMs) {
7960
7971
  //#region src/react/flow-waterfall-body.tsx
7961
7972
  const ROW_HEIGHT = {
7962
7973
  spans: 36,
7963
- agents: 40
7974
+ agents: 40,
7975
+ graph: 40
7964
7976
  };
7965
7977
  const OVERSCAN = 8;
7966
7978
  /** Rows re-render once per CHUNK rows scrolled; overscan covers the rows in between. */
@@ -8690,11 +8702,13 @@ const AGENT_ROWS = 20;
8690
8702
  const UNFOLD_ROWS = 12;
8691
8703
  const END_LABEL_GUTTER = {
8692
8704
  spans: 120,
8693
- agents: 176
8705
+ agents: 176,
8706
+ graph: 176
8694
8707
  };
8695
8708
  const NAME_WIDTH = {
8696
8709
  spans: 320,
8697
- agents: 240
8710
+ agents: 240,
8711
+ graph: 240
8698
8712
  };
8699
8713
  /** State the host may own: its value wins when given, and every change is reported either way. */
8700
8714
  function useHostable(value, onChange, initial) {
@@ -8771,7 +8785,7 @@ const TONE_ORDER = [
8771
8785
  "ok"
8772
8786
  ];
8773
8787
  /** What each mark means, as the agent view draws it; a key only for kinds the run has. */
8774
- function KindLegend({ rows }) {
8788
+ function KindLegend({ rows, fill }) {
8775
8789
  const has = (kind) => rows.some((row) => row.own.some((mark) => mark.kind === kind) || row.span.kind === kind);
8776
8790
  const shown = [
8777
8791
  ["model", "Model call"],
@@ -8780,13 +8794,19 @@ function KindLegend({ rows }) {
8780
8794
  ["handoff", "Handoff"]
8781
8795
  ].filter(([kind]) => kind === "waiting" ? rows.some((row) => row.kind === "pipeline") : kind === "handoff" ? rows.some((row) => row.span.handoffTo || row.hasAgents) : has(kind));
8782
8796
  if (shown.length === 0) return null;
8783
- return /* @__PURE__ */ jsx("ul", {
8797
+ const example = rows.find((row) => toneKey(row.tone) > 0 && row.own.some((mark) => mark.kind === "model")) ?? rows.find((row) => row.own.length);
8798
+ return /* @__PURE__ */ jsxs("ul", {
8784
8799
  className: "fw-legend fw-kinds",
8785
8800
  "aria-label": "Key",
8786
- children: shown.map(([kind, name]) => /* @__PURE__ */ jsxs("li", { children: [/* @__PURE__ */ jsx("span", {
8801
+ "data-tone": fill === "agent" ? toneKey(example?.tone ?? 0) : void 0,
8802
+ "data-example": example?.id,
8803
+ children: [shown.map(([kind, name]) => /* @__PURE__ */ jsxs("li", { children: [/* @__PURE__ */ jsx("span", {
8787
8804
  className: `fw-swatch-${kind === "waiting" ? "wait" : kind}`,
8788
8805
  "aria-hidden": true
8789
- }), name] }, kind))
8806
+ }), name] }, kind)), /* @__PURE__ */ jsx("li", {
8807
+ className: "fw-kind-rule",
8808
+ children: fill === "agent" ? "Bar color identifies family; shade identifies work." : "Shade identifies work."
8809
+ })]
8790
8810
  });
8791
8811
  }
8792
8812
  /** The axis dots and the dashed lines they drop: one key naming what they mark (a release, or the kinds the host gave). */
@@ -8886,17 +8906,37 @@ function OutcomeLegend({ agents, selected, onFilter, neutral }) {
8886
8906
  ]
8887
8907
  });
8888
8908
  }
8909
+ function ViewControl({ view, graph, onChange }) {
8910
+ return /* @__PURE__ */ jsx("div", {
8911
+ className: "fw-views",
8912
+ role: "radiogroup",
8913
+ "aria-label": "Trace view",
8914
+ children: [
8915
+ "agents",
8916
+ "spans",
8917
+ ...graph ? ["graph"] : []
8918
+ ].map((option) => /* @__PURE__ */ jsx("button", {
8919
+ type: "button",
8920
+ role: "radio",
8921
+ "aria-checked": view === option,
8922
+ disabled: view === option,
8923
+ className: "fw-view-button",
8924
+ onClick: () => onChange(option),
8925
+ children: option === "agents" ? "Agents" : option === "spans" ? "Steps" : "Graph"
8926
+ }, option))
8927
+ });
8928
+ }
8889
8929
  /** Shared trace inspector: the agent timeline and the span waterfall over one clock.
8890
8930
  * Data remains producer-owned; playback, zoom and folding change presentation only. */
8891
- function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defaultCollapsed = false, defaultView = "spans", renderIcon, maxHeight, onSelect, markers, onTimeChange, agentFill: fillProp, selectedId, onDeselect, description, summary: showSummary = true, criticalPath = false, query: queryProp, onQueryChange, collapsed: collapsedProp, onCollapsedChange, zoom: zoomProp, onZoomChange, matchSpan }) {
8931
+ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defaultCollapsed = false, defaultView = "spans", graph, latestEvents, renderIcon, maxHeight, onSelect, markers, onTimeChange, agentFill: fillProp, selectedId, onDeselect, description, summary: showSummary = true, criticalPath = false, query: queryProp, onQueryChange, collapsed: collapsedProp, onCollapsedChange, zoom: zoomProp, onZoomChange, matchSpan }) {
8892
8932
  const root = useRef(null);
8893
8933
  const search = useRef(null);
8894
8934
  const slider = useRef(null);
8895
8935
  const timeOutput = useRef(null);
8896
8936
  const hideTimer = useRef(void 0);
8897
- const searchId = useId();
8898
- const cardId = useId();
8899
- const [view, setView] = useState(defaultView);
8937
+ const searchId = useId(), cardId = useId();
8938
+ const [requestedView, setView] = useState(defaultView);
8939
+ const view = requestedView === "graph" && graph == null ? "agents" : requestedView;
8900
8940
  const [query, setQuery] = useHostable(queryProp, onQueryChange, "");
8901
8941
  const [collapsed, setCollapsed] = useHostable(collapsedProp, onCollapsedChange, () => new Set(defaultCollapsed ? trace.spans.flatMap((span) => span.parentId ? [span.parentId] : []) : []));
8902
8942
  const [speed, setSpeed] = useState(1);
@@ -9012,6 +9052,7 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
9012
9052
  }, [view, agents]);
9013
9053
  useEffect(() => {
9014
9054
  const keydown = (event) => {
9055
+ if (view === "graph") return;
9015
9056
  const active = document.activeElement;
9016
9057
  const ownsFocus = root.current?.contains(active);
9017
9058
  if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === "f") {
@@ -9326,7 +9367,7 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
9326
9367
  done && speedup >= 1.05 ? ` · ${speedup.toFixed(1)}x parallel` : ""
9327
9368
  ]
9328
9369
  }),
9329
- failedAgents > 0 && /* @__PURE__ */ jsxs("button", {
9370
+ view !== "graph" && failedAgents > 0 && /* @__PURE__ */ jsxs("button", {
9330
9371
  type: "button",
9331
9372
  className: "fw-text-button fw-alert",
9332
9373
  "aria-pressed": filter === "__errors",
@@ -9338,7 +9379,7 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
9338
9379
  " failed"
9339
9380
  ]
9340
9381
  }),
9341
- quietAgents > 0 && /* @__PURE__ */ jsxs("button", {
9382
+ view !== "graph" && quietAgents > 0 && /* @__PURE__ */ jsxs("button", {
9342
9383
  type: "button",
9343
9384
  className: "fw-text-button fw-quiet",
9344
9385
  "aria-pressed": filter === "__quiet",
@@ -9351,7 +9392,7 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
9351
9392
  children: "Live"
9352
9393
  })
9353
9394
  ]
9354
- }), /* @__PURE__ */ jsxs("details", {
9395
+ }), view !== "graph" && /* @__PURE__ */ jsxs("details", {
9355
9396
  className: "fw-key",
9356
9397
  open: root.current ? root.current.clientWidth >= 600 : true,
9357
9398
  children: [/* @__PURE__ */ jsxs("summary", { children: ["Key", /* @__PURE__ */ jsx(ChevronGlyph, {})] }), /* @__PURE__ */ jsxs("div", {
@@ -9363,30 +9404,24 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
9363
9404
  selected: filter,
9364
9405
  onFilter: applyFilter
9365
9406
  }),
9366
- view === "agents" && /* @__PURE__ */ jsx(KindLegend, { rows }),
9407
+ view === "agents" && /* @__PURE__ */ jsx(KindLegend, {
9408
+ rows: visible,
9409
+ fill: agentFill
9410
+ }),
9367
9411
  /* @__PURE__ */ jsx(MarkerLegend, { markers })
9368
9412
  ]
9369
9413
  })]
9370
9414
  })]
9371
9415
  }), /* @__PURE__ */ jsxs("div", {
9372
9416
  className: "fw-tools",
9373
- children: [/* @__PURE__ */ jsx("div", {
9374
- className: "fw-views",
9375
- role: "radiogroup",
9376
- "aria-label": "Trace view",
9377
- children: ["agents", "spans"].map((option) => /* @__PURE__ */ jsx("button", {
9378
- type: "button",
9379
- role: "radio",
9380
- "aria-checked": view === option,
9381
- disabled: view === option,
9382
- className: "fw-view-button",
9383
- onClick: () => {
9384
- setView(option);
9385
- setNameWidth(NAME_WIDTH[option]);
9386
- setCard(null);
9387
- },
9388
- children: option === "agents" ? "Agents" : "Steps"
9389
- }, option))
9417
+ children: [/* @__PURE__ */ jsx(ViewControl, {
9418
+ view,
9419
+ graph: graph != null,
9420
+ onChange: (option) => {
9421
+ setView(option);
9422
+ setNameWidth(NAME_WIDTH[option]);
9423
+ setCard(null);
9424
+ }
9390
9425
  }), view === "spans" && /* @__PURE__ */ jsxs("div", {
9391
9426
  className: "fw-search",
9392
9427
  children: [
@@ -9420,91 +9455,94 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
9420
9455
  })]
9421
9456
  })]
9422
9457
  }),
9423
- showSummary && /* @__PURE__ */ jsx("dl", {
9424
- className: "fw-summary",
9425
- children: summary.map(([name, value, title]) => /* @__PURE__ */ jsxs("div", {
9426
- className: name === "Errors" && errors !== void 0 && errors > 0 ? "fw-has-errors" : void 0,
9427
- children: [/* @__PURE__ */ jsx("dt", { children: name }), /* @__PURE__ */ jsx("dd", {
9428
- className: "fw-value",
9429
- title,
9430
- children: name === "Errors" ? /* @__PURE__ */ jsx("button", {
9431
- type: "button",
9432
- "aria-pressed": filter === "__errors",
9433
- onClick: () => applyFilter("__errors"),
9434
- children: value
9435
- }) : value
9436
- })]
9437
- }, name))
9438
- }),
9439
- view === "agents" && playback,
9440
- /* @__PURE__ */ jsx(FlowBody, {
9441
- rows: visible,
9442
- byId,
9443
- totalRows: rows.length,
9444
- view,
9445
- maxHeight: rowsHeight,
9446
- collapsed,
9447
- searching: Boolean(query.trim()),
9448
- query,
9449
- matchSpan,
9450
- pauses: trace.pauses,
9451
- errorTimes,
9452
- cursorLine: isLive || position !== null || playing,
9453
- sharedDetail,
9454
- sharedOutcome,
9455
- expandedIds: view === "agents" ? /* @__PURE__ */ new Set([...openFamilies, ...openSteps]) : void 0,
9456
- holdHeight: isLive,
9457
- slowestId: slowest?.id ?? callouts.slowestId,
9458
- costliestId: callouts.costliestId,
9459
- criticalIds: criticalPath && view === "spans" ? critical : void 0,
9460
- activeId,
9461
- selectedId: selection,
9462
- focusId: cardAgent?.id ?? (view === "agents" ? activeId : void 0),
9463
- clockMs,
9464
- window: win,
9465
- trackPx,
9466
- showCursor,
9467
- markers,
9468
- fillByAgent: agentFill !== "outcome",
9469
- zoomed: zoom !== null,
9470
- renderIcon,
9471
- emptyText: rows.length === 0 ? "No spans yet. Waiting for trace events." : `No matching steps for “${query}”.`,
9472
- onToggle: toggle,
9473
- onInspect: inspect,
9474
- onLeave: leave,
9475
- onPin: pin,
9476
- onActivate: (row) => setActiveId(row.id),
9477
- onActiveChange: setActiveId,
9478
- onScrub: scrubTo,
9479
- onZoom: zoomBy,
9480
- onPan: pan,
9481
- nameWidth,
9482
- onNameWidth: (px) => {
9483
- resized.current = true;
9484
- setNameWidth(Math.max(140, Math.min(560, px)));
9485
- },
9486
- onTrack: setLaneElement,
9487
- onExpandAll: rows.some((row) => row.hasChildren) ? () => setCollapsed(/* @__PURE__ */ new Set()) : void 0,
9488
- onCollapseAll: () => setCollapsed(new Set(rows.filter((row) => row.hasChildren).map((row) => row.id)))
9489
- }),
9490
- card && cardRow && /* @__PURE__ */ jsx("div", {
9491
- className: `fw-card-anchor${card.above && card.top === void 0 ? " fw-card-above" : ""}`,
9492
- onPointerEnter: () => clearTimeout(hideTimer.current),
9493
- onPointerLeave: card.pinned ? void 0 : leave,
9494
- children: /* @__PURE__ */ jsx(SpanCard, {
9495
- id: cardId,
9496
- row: cardRow,
9497
- agent: cardAgent,
9458
+ latestEvents,
9459
+ view === "graph" ? graph : /* @__PURE__ */ jsxs(Fragment, { children: [
9460
+ showSummary && /* @__PURE__ */ jsx("dl", {
9461
+ className: "fw-summary",
9462
+ children: summary.map(([name, value, title]) => /* @__PURE__ */ jsxs("div", {
9463
+ className: name === "Errors" && errors !== void 0 && errors > 0 ? "fw-has-errors" : void 0,
9464
+ children: [/* @__PURE__ */ jsx("dt", { children: name }), /* @__PURE__ */ jsx("dd", {
9465
+ className: "fw-value",
9466
+ title,
9467
+ children: name === "Errors" ? /* @__PURE__ */ jsx("button", {
9468
+ type: "button",
9469
+ "aria-pressed": filter === "__errors",
9470
+ onClick: () => applyFilter("__errors"),
9471
+ children: value
9472
+ }) : value
9473
+ })]
9474
+ }, name))
9475
+ }),
9476
+ view === "agents" && playback,
9477
+ /* @__PURE__ */ jsx(FlowBody, {
9478
+ rows: visible,
9479
+ byId,
9480
+ totalRows: rows.length,
9481
+ view,
9482
+ maxHeight: rowsHeight,
9483
+ collapsed,
9484
+ searching: Boolean(query.trim()),
9485
+ query,
9486
+ matchSpan,
9487
+ pauses: trace.pauses,
9488
+ errorTimes,
9489
+ cursorLine: isLive || position !== null || playing,
9490
+ sharedDetail,
9491
+ sharedOutcome,
9492
+ expandedIds: view === "agents" ? /* @__PURE__ */ new Set([...openFamilies, ...openSteps]) : void 0,
9493
+ holdHeight: isLive,
9494
+ slowestId: slowest?.id ?? callouts.slowestId,
9495
+ costliestId: callouts.costliestId,
9496
+ criticalIds: criticalPath && view === "spans" ? critical : void 0,
9497
+ activeId,
9498
+ selectedId: selection,
9499
+ focusId: cardAgent?.id ?? (view === "agents" ? activeId : void 0),
9498
9500
  clockMs,
9499
- left: card.left,
9500
- top: card.top ?? (card.above ? card.anchorTop - 8 : card.anchorBottom + 8),
9501
- pinned: card.pinned,
9502
- onClose: closeCard,
9501
+ window: win,
9502
+ trackPx,
9503
+ showCursor,
9504
+ markers,
9505
+ fillByAgent: agentFill !== "outcome",
9506
+ zoomed: zoom !== null,
9503
9507
  renderIcon,
9504
- compact: view === "agents"
9505
- })
9506
- }),
9507
- view === "spans" && playback
9508
+ emptyText: rows.length === 0 ? "No spans yet. Waiting for trace events." : `No matching steps for “${query}”.`,
9509
+ onToggle: toggle,
9510
+ onInspect: inspect,
9511
+ onLeave: leave,
9512
+ onPin: pin,
9513
+ onActivate: (row) => setActiveId(row.id),
9514
+ onActiveChange: setActiveId,
9515
+ onScrub: scrubTo,
9516
+ onZoom: zoomBy,
9517
+ onPan: pan,
9518
+ nameWidth,
9519
+ onNameWidth: (px) => {
9520
+ resized.current = true;
9521
+ setNameWidth(Math.max(140, Math.min(560, px)));
9522
+ },
9523
+ onTrack: setLaneElement,
9524
+ onExpandAll: rows.some((row) => row.hasChildren) ? () => setCollapsed(/* @__PURE__ */ new Set()) : void 0,
9525
+ onCollapseAll: () => setCollapsed(new Set(rows.filter((row) => row.hasChildren).map((row) => row.id)))
9526
+ }),
9527
+ card && cardRow && /* @__PURE__ */ jsx("div", {
9528
+ className: `fw-card-anchor${card.above && card.top === void 0 ? " fw-card-above" : ""}`,
9529
+ onPointerEnter: () => clearTimeout(hideTimer.current),
9530
+ onPointerLeave: card.pinned ? void 0 : leave,
9531
+ children: /* @__PURE__ */ jsx(SpanCard, {
9532
+ id: cardId,
9533
+ row: cardRow,
9534
+ agent: cardAgent,
9535
+ clockMs,
9536
+ left: card.left,
9537
+ top: card.top ?? (card.above ? card.anchorTop - 8 : card.anchorBottom + 8),
9538
+ pinned: card.pinned,
9539
+ onClose: closeCard,
9540
+ renderIcon,
9541
+ compact: view === "agents"
9542
+ })
9543
+ }),
9544
+ view === "spans" && playback
9545
+ ] })
9508
9546
  ]
9509
9547
  });
9510
9548
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/charts",
3
- "version": "2.0.0",
3
+ "version": "2.0.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",