@tangle-network/charts 0.13.0 → 0.15.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
@@ -4,15 +4,9 @@ Benchmark figures as static SVG and HTML tables, plus optional React timing prim
4
4
 
5
5
  The React entry also exports `DayBars`, `HBars`, `Histogram`, `Lines`, `Meter`, `Spark`, and `Gantt` for live operational pages. Import `@tangle-network/charts/react/live.css` for their layout and set `--chart-live-*` theme tokens on an ancestor. Data, state colors, destination links, labels, and interpretation remain with the caller. `DayBars` accepts an `onOpen(index)` action with keyboard activation.
6
6
 
7
- `WaterfallHeader`, `WaterfallTimeAxis`, `WaterfallBar` and `WaterfallRow` from
8
- `@tangle-network/charts/react` own the timing presentation extracted from
9
- Intelligence's trace viewer. Supply actual span start/end milliseconds and the
10
- same `{ startMs, endMs }` window to every row. Hierarchy, selection, attribution,
11
- formatting and failure interpretation stay with the consumer. The original
12
- minimum/clipped marker remains visible; its tooltip reports the actual duration.
13
- Set `--waterfall-timing-width`, `--waterfall-duration-width` and the existing
14
- `--intel-waterfall-*` colors for the consumer's layout and theme. No stylesheet,
15
- router or React DOM dependency is required.
7
+ `FlowWaterfall` from `@tangle-network/charts/react` is the shared live trace inspector. It owns hierarchy layout, folding, search, playback 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. The React-free root exports the `FlowSpan` and `FlowTrace` types.
8
+
9
+ `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.
16
10
 
17
11
  Five figures cover the decisions a benchmark reader makes:
18
12
 
@@ -44,7 +38,7 @@ phaseWaterfall([{ id: "route", label: "Route", ms: 5 }, { id: "restore", label:
44
38
 
45
39
  The producer maps its own blocked or absent states to `no-data`; Charts never assigns a status from a value. `latencyRange` refuses any row below `minN`; when the producer omits those rows, pass their reasons in `excluded` so the note names them. `phaseWaterfall` refuses phases whose sum exceeds the reported total because overlapping spans cannot be drawn as a cumulative waterfall. Its input order is retained, including zero-duration phases. The producer marks known unaccounted leaves with `kind: "unaccounted"`; Charts also marks any difference between their sum and `totalMs`.
46
40
 
47
- The library draws and never decides.
41
+ Charts owns presentation and interaction; producers own interpretation and status. The library never derives failure or running state from metadata values.
48
42
  Ranks, intervals, frontier membership, estimate labels and pass counts come from the producer: a board record, a VerticalBench report or an agent-eval lens.
49
43
  A figure function returns a `Refusal` in place of a figure it cannot draw honestly, for example a bar over too few units, an interval that names no method, or a frontier the producer did not compute.
50
44
 
@@ -319,8 +313,31 @@ npm run build
319
313
 
320
314
  The package-boundary test builds and packs Charts, installs the tarball in an OS-temporary consumer with a normal offline npm install (no peer-omission flags), asserts React/React DOM/React types cannot resolve, and executes the static root without DOM globals. It compares root exports, static SVG/tables, Refusals, report/chart/plot HTML and CSS with the untouched source implementation, then compiles a static-only consumer with `lib: ["ES2022"]`, `types: []` and `skipLibCheck: false`. Only afterward does it supply the installed React peer/types to verify the packed React runtime and declarations. It runs under `npm test`.
321
315
 
322
- The component tests cover gap/zero/one/equal/negative cases, controlled identity, keyboard/touch selection, nonvisual values and resize contracts. The jsdom resize check verifies fluid/minimum-width attributes and preserved selection, **not browser-computed layout**; The retained packed-browser fixture checks Chromium at 1100px and 390px, keyboard selection, emulated touch, light/dark Brand tokens, reduced motion, and top-layer positioning inside a transformed, paint-contained panel. It passes with React 18 and React 19; the React 18 consumer also compiles the packed declarations with React 18 types. Physical touch hardware and screen-reader audio remain unchecked.
316
+ The component tests cover gap/zero/one/equal/negative cases, controlled identity, keyboard/touch selection, nonvisual values and resize contracts. The jsdom resize check verifies fluid/minimum-width attributes and preserved selection, **not browser-computed layout**; The retained [packed-browser fixture](scripts/charts-browser-smoke.mjs), ported from brand, and its [browser proof](scripts/charts-browser-proof.mjs) check Chromium at 1100px and 390px, keyboard selection, emulated touch, light/dark Brand tokens, reduced motion, and top-layer positioning inside a transformed, paint-contained panel. It passes with React 18 and React 19; the React 18 consumer also compiles the packed declarations with React 18 types. Physical touch hardware and screen-reader audio remain unchecked.
323
317
 
324
318
  ### Figure browser proof
325
319
 
326
320
  `node scripts/render-figures.mjs` writes a comparison page from the retained fixtures and approved speed reference. `node scripts/capture-figures.mjs` uses Playwright Chromium to capture all three figures at phone and desktop widths in light and dark themes. The reviewed images are in [`docs/screenshots`](docs/screenshots).
321
+
322
+ ## Live trace waterfall
323
+
324
+ ```tsx
325
+ import { FlowWaterfall } from '@tangle-network/charts/react';
326
+ import type { FlowTrace } from '@tangle-network/charts';
327
+ import '@tangle-network/charts/react/flow-waterfall.css';
328
+
329
+ // The producer supplies stable identities, explicit status and measured totals.
330
+ <FlowWaterfall trace={trace satisfies FlowTrace} live={running} />;
331
+ ```
332
+
333
+ Search supports Cmd/Ctrl+F and keeps ancestor context. Folding, five playback speeds, scrubbing and Follow live affect presentation only. Running spans grow between snapshots. Missing cost and authoritative LLM counts stay Unknown. Invalid intervals retain a per-row `—` timing mark, preserving the rest of the trace. The static phase waterfall remains a separate chart for disjoint phases.
334
+
335
+ **Provenance.** FlowWaterfall and its recorded Discovery fixture move from agent-app #1267/#1273 at `78a3e8f7fd9bf93b23afa8290db6d721db544237`; the independent implementation copied no paid reference assets. [Fixture provenance](docs/fixtures/discovery-waterfall-provenance.md) records measured timing, observed token totals and unknowns. The historical [baseline](docs/fixtures/live-playback-rates-baseline.json) and [fixed](docs/fixtures/live-playback-rates-fixed.json) rate measurements are retained. The packed public consumer proof measures all five rates against actual browser wall time during live growth, with ±15% acceptance, using React 18 and 19 at 1100px and 390px.
336
+
337
+ ```sh
338
+ npm run build
339
+ CHARTS_REACT_VERSION=18 CHARTS_PROOF_DIR=/tmp/charts-react18 node scripts/charts-browser-proof.mjs --record
340
+ CHARTS_REACT_VERSION=19 CHARTS_PROOF_DIR=/tmp/charts-react19 node scripts/charts-browser-proof.mjs --record
341
+ ```
342
+
343
+ 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.
@@ -304,4 +304,33 @@ interface TimelineOptions {
304
304
  }
305
305
  declare function timeline(lanes: TimelineLane[], o: TimelineOptions): Figure | Refusal;
306
306
  //#endregion
307
- export { Exclusion as A, isRefusal as B, YScale as C, ComparisonRow as D, BreakdownInput as E, RateRow as F, Refusal as I, Setup as L, Interval as M, IntervalMethod as N, CostBasis as O, MatrixCell as P, Tier as R, XCategory as S, YTick as T, ChartBand as _, timeline as a, MarkVerdict as b, ENDING_ORDER as c, RunEnding as d, TickOutcome as f, BandKind as g, PointGlyph as h, TimelineTick as i, Figure as j, Estimate as k, OUTCOME_COLORS as l, PointFields as m, TimelineOptions as n, CHECKER_HUES as o, checkerColor as p, TimelineSpan as r, ENDING_COLORS as s, TimelineLane as t, OUTCOME_ORDER as u, ChartMark as v, YTarget as w, MarkerShape as x, MarkKind as y, TierCounts as z };
307
+ //#region src/flow-trace.d.ts
308
+ /** Producer-owned identities, explicit status and observed totals; no React dependency. */
309
+ interface FlowSpan {
310
+ kind: 'pipeline' | 'model' | 'tool' | 'waiting' | 'handoff';
311
+ /** Stable identity and parent identity for nested, incrementally updated traces. */
312
+ id?: string;
313
+ parentId?: string;
314
+ /** Running spans extend from endMs to the viewer's observation time. */
315
+ status?: 'running' | 'done' | 'failed';
316
+ name: string;
317
+ startMs: number;
318
+ endMs: number;
319
+ approx?: boolean;
320
+ meta?: Record<string, unknown>;
321
+ }
322
+ /** Describe the structure of a flow trace including spans, timing, tokens, cost, and tool calls */
323
+ interface FlowTrace {
324
+ spans: FlowSpan[];
325
+ totalMs: number;
326
+ promptTokens: number;
327
+ completionTokens: number;
328
+ /** Computed when per-token pricing is supplied. */
329
+ costUsd?: number;
330
+ toolCalls: number;
331
+ /** Authoritative LLM request count when supplied; absent means unknown. */
332
+ modelCalls?: number;
333
+ errors?: number;
334
+ }
335
+ //#endregion
336
+ export { CostBasis as A, Tier as B, MarkerShape as C, YTick as D, YTarget as E, IntervalMethod as F, isRefusal as H, MatrixCell as I, RateRow as L, Exclusion as M, Figure as N, BreakdownInput as O, Interval as P, Refusal as R, MarkVerdict as S, YScale as T, TierCounts as V, PointGlyph as _, TimelineSpan as a, ChartMark as b, CHECKER_HUES as c, OUTCOME_COLORS as d, OUTCOME_ORDER as f, PointFields as g, checkerColor as h, TimelineOptions as i, Estimate as j, ComparisonRow as k, ENDING_COLORS as l, TickOutcome as m, FlowTrace as n, TimelineTick as o, RunEnding as p, TimelineLane as r, timeline as s, FlowSpan as t, ENDING_ORDER as u, BandKind as v, XCategory as w, MarkKind as x, ChartBand as y, Setup as z };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { A as Exclusion, B as isRefusal, C as YScale, D as ComparisonRow, E as BreakdownInput, F as RateRow, I as Refusal, L as Setup, M as Interval, N as IntervalMethod, O as CostBasis, P as MatrixCell, R as Tier, S as XCategory, T as YTick, _ as ChartBand, a as timeline, b as MarkVerdict, c as ENDING_ORDER, d as RunEnding, f as TickOutcome, g as BandKind, h as PointGlyph, i as TimelineTick, j as Figure, k as Estimate, l as OUTCOME_COLORS, m as PointFields, n as TimelineOptions, o as CHECKER_HUES, p as checkerColor, r as TimelineSpan, s as ENDING_COLORS, t as TimelineLane, u as OUTCOME_ORDER, v as ChartMark, w as YTarget, x as MarkerShape, y as MarkKind, z as TierCounts } from "./timeline-BvVxPTGp.js";
1
+ import { A as CostBasis, B as Tier, C as MarkerShape, D as YTick, E as YTarget, F as IntervalMethod, H as isRefusal, I as MatrixCell, L as RateRow, M as Exclusion, N as Figure, O as BreakdownInput, P as Interval, R as Refusal, S as MarkVerdict, T as YScale, V as TierCounts, _ as PointGlyph, a as TimelineSpan, b as ChartMark, c as CHECKER_HUES, d as OUTCOME_COLORS, f as OUTCOME_ORDER, g as PointFields, h as checkerColor, i as TimelineOptions, j as Estimate, k as ComparisonRow, l as ENDING_COLORS, m as TickOutcome, n as FlowTrace, o as TimelineTick, p as RunEnding, r as TimelineLane, s as timeline, t as FlowSpan, u as ENDING_ORDER, v as BandKind, w as XCategory, x as MarkKind, y as ChartBand, z as Setup } from "./flow-trace-B5dlYNsD.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. */
@@ -344,4 +344,4 @@ interface UnitDotOptions {
344
344
  */
345
345
  export declare function unitDotLayout(groups: DotGroup[], width: number, height: number, o?: UnitDotOptions): UnitDotLayout;
346
346
  //#endregion
347
- export { type BandKind, type BreakdownInput, type BreakdownOptions, CHECKER_HUES, type ChartBand, type ChartMark, type ComparisonIntervalsOptions, type ComparisonRow, type CostBasis, type CostFrontierOptions, type DotGroup, type DotGroupRect, ENDING_COLORS, ENDING_ORDER, type Estimate, type Exclusion, type Figure, type HeatmapCell, type HeatmapColumn, type HeatmapOptions, type HeatmapRow, type HeatmapStatus, type Interval, type IntervalMethod, type LatencyRangeOptions, type LatencyRangeRow, type MarkKind, type MarkVerdict, type MatrixCell, type MetricBarsOptions, type MetricRow, type MultiplePanel, OUTCOME_COLORS, OUTCOME_ORDER, type Observations, type PhaseWaterfallOptions, type PointFields, type PointGlyph, type RankedRatesOptions, type RateIdentity, type RateRow, type Refusal, type RenderFigureOptions, type RunEnding, type Setup, type SmallMultiplesOptions, type TaskMatrixOptions, type TickOutcome, type Tier, type TierCounts, type TimePoint, type TimeSeriesOptions, type TimeSeriesRow, type TimelineLane, type TimelineOptions, type TimelineSpan, type TimelineTick, type UnitDotLayout, type UnitDotOptions, type WaterfallPhase, type XCategory, type YScale, type YTarget, type YTick, checkerColor, isRefusal, timeline };
347
+ export { type BandKind, type BreakdownInput, type BreakdownOptions, CHECKER_HUES, type ChartBand, type ChartMark, type ComparisonIntervalsOptions, type ComparisonRow, type CostBasis, type CostFrontierOptions, type DotGroup, type DotGroupRect, ENDING_COLORS, ENDING_ORDER, type Estimate, type Exclusion, type Figure, type FlowSpan, type FlowTrace, type HeatmapCell, type HeatmapColumn, type HeatmapOptions, type HeatmapRow, type HeatmapStatus, type Interval, type IntervalMethod, type LatencyRangeOptions, type LatencyRangeRow, type MarkKind, type MarkVerdict, type MatrixCell, type MetricBarsOptions, type MetricRow, type MultiplePanel, OUTCOME_COLORS, OUTCOME_ORDER, type Observations, type PhaseWaterfallOptions, type PointFields, type PointGlyph, type RankedRatesOptions, type RateIdentity, type RateRow, type Refusal, type RenderFigureOptions, type RunEnding, type Setup, type SmallMultiplesOptions, type TaskMatrixOptions, type TickOutcome, type Tier, type TierCounts, type TimePoint, type TimeSeriesOptions, type TimeSeriesRow, type TimelineLane, type TimelineOptions, type TimelineSpan, type TimelineTick, type UnitDotLayout, type UnitDotOptions, type WaterfallPhase, type XCategory, type YScale, type YTarget, type YTick, checkerColor, isRefusal, timeline };
@@ -0,0 +1,76 @@
1
+ /* Standalone surface: consumes Brand/Agent App tokens; no Tailwind build required. */
2
+ .fw-root {
3
+ --waterfall-bar-height: 1rem;
4
+ --waterfall-bar-radius: .125rem;
5
+ --waterfall-duration-width: 100%;
6
+ --intel-waterfall-track: var(--fw-well);
7
+ --fw-text: var(--text-primary, hsl(var(--foreground)));
8
+ --fw-muted: var(--text-muted, hsl(var(--muted-foreground)));
9
+ --fw-surface: var(--bg-card, hsl(var(--background)));
10
+ --fw-border: var(--border-default, hsl(var(--border)));
11
+ --fw-well: var(--md3-surface-container, hsl(var(--secondary)));
12
+ --fw-accent: var(--accent-text, hsl(var(--primary)));
13
+ color: var(--fw-text);
14
+ background: var(--fw-surface);
15
+ border: 1px solid var(--fw-border);
16
+ border-radius: var(--radius-lg, .75rem);
17
+ min-width: 0;
18
+ font: inherit;
19
+ font-size: .8125rem;
20
+ line-height: 1.5;
21
+ }
22
+ .fw-root *, .fw-root *::before, .fw-root *::after { box-sizing: border-box; }
23
+ .fw-root :is(p, dl, dd) { margin: 0; }
24
+ .fw-root .fw-heading { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: .5rem; padding: .75rem; border-bottom: 1px solid var(--fw-border); }
25
+ .fw-root .fw-title { display: flex; align-items: center; gap: .5rem; font-weight: 600; font-size: .875rem; }
26
+ .fw-root .fw-search { display: flex; min-width: 0; gap: .25rem; align-items: center; }
27
+ .fw-root .fw-control { appearance: auto; font: inherit; font-size: .75rem; line-height: 1.4; min-height: 2rem; padding: .25rem .5rem; border: 1px solid var(--fw-border); border-radius: var(--radius-sm, .375rem); color: var(--fw-text); background: var(--fw-surface); cursor: pointer; }
28
+ .fw-root .fw-control:hover, .fw-root .fw-toggle:hover { background: var(--fw-well); }
29
+ .fw-root :is(button, input, select, [tabindex]):focus-visible { outline: 2px solid var(--fw-accent); outline-offset: 2px; }
30
+ .fw-root :is(button, input):disabled { opacity: .5; cursor: default; }
31
+ .fw-root .fw-search-input { width: 10rem; min-width: 0; cursor: text; }
32
+ .fw-root .fw-summary { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: .75rem 1rem; padding: .75rem; border-bottom: 1px solid var(--fw-border); }
33
+ .fw-root .fw-muted { color: var(--fw-muted); font-size: .75rem; }
34
+ .fw-root .fw-value { margin-top: .125rem; font-family: var(--font-mono, monospace); font-size: .875rem; font-variant-numeric: tabular-nums; overflow-wrap: anywhere; }
35
+ .fw-root .fw-legend { display: flex; flex-wrap: wrap; gap: .25rem .75rem; padding: .5rem .75rem; border-bottom: 1px solid var(--fw-border); color: var(--fw-muted); font-size: .75rem; }
36
+ .fw-root .fw-legend-item { display: inline-flex; align-items: center; gap: .25rem; }
37
+ .fw-root .fw-dot { width: .375rem; height: .375rem; border-radius: 50%; }
38
+ .fw-root .fw-orchestrator { --fw-bar-color: var(--fw-muted); background: var(--fw-bar-color); }
39
+ .fw-root .fw-model { --fw-bar-color: var(--chart-accent, hsl(var(--primary))); background: var(--fw-bar-color); }
40
+ .fw-root .fw-tool { --fw-bar-color: var(--status-cold, hsl(var(--accent))); background: var(--fw-bar-color); }
41
+ .fw-root .fw-waiting { --fw-bar-color: var(--status-stopped, hsl(var(--warning-strong))); background: var(--fw-bar-color); }
42
+ .fw-root .fw-handoff { --fw-bar-color: var(--status-running, hsl(var(--success))); background: var(--fw-bar-color); }
43
+ .fw-root .fw-failed { --fw-bar-color: var(--status-error, hsl(var(--destructive))); background: var(--fw-bar-color); }
44
+ .fw-root .fw-branches { display: flex; gap: .5rem; padding: .5rem .75rem 0; }
45
+ .fw-root .fw-timeline { overflow-x: auto; padding: .75rem; }
46
+ .fw-root .fw-track { min-width: 480px; }
47
+ .fw-root .fw-grid { display: grid; grid-template-columns: minmax(0, 2fr) minmax(0, 3fr) 70px; align-items: center; gap: .5rem; }
48
+ .fw-root .fw-axis { padding-bottom: .5rem; color: var(--fw-muted); font-size: .75rem; }
49
+ .fw-root .fw-ticks { display: flex; justify-content: space-between; font-family: var(--font-mono, monospace); font-variant-numeric: tabular-nums; }
50
+ .fw-root .fw-align-right { text-align: right; }
51
+ .fw-root .fw-empty { padding: 2rem .5rem; text-align: center; color: var(--fw-muted); font-size: .875rem; }
52
+ .fw-root .fw-row { border-radius: .25rem; padding: .375rem 0; }
53
+ .fw-root .fw-slowest { background: var(--fw-well); outline: 1px solid var(--fw-border); }
54
+ .fw-root .fw-pending { opacity: .4; }
55
+ .fw-root .fw-name { display: flex; align-items: center; min-width: 0; }
56
+ .fw-root .fw-toggle { appearance: none; width: 1.5rem; height: 1.5rem; margin-right: .25rem; flex-shrink: 0; border: 0; border-radius: .25rem; background: transparent; color: var(--fw-text); font: inherit; cursor: pointer; }
57
+ .fw-root .fw-leaf { margin-right: .25rem; width: 1.5rem; flex-shrink: 0; text-align: center; color: var(--fw-muted); }
58
+ .fw-root .fw-span-name { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: .75rem; }
59
+ .fw-root .fw-error-text { color: var(--text-danger, var(--status-error)); }
60
+ .fw-root .fw-bar-track { position: relative; height: 1rem; overflow: hidden; border-radius: .125rem; background: var(--fw-well); }
61
+ .fw-root .fw-bar { position: absolute; top: 0; bottom: 0; border-radius: .125rem; }
62
+ .fw-root .fw-approx { opacity: .7; }
63
+ .fw-root .fw-playhead { position: absolute; top: 0; bottom: 0; width: 1px; background: var(--fw-text); }
64
+ .fw-root .fw-duration { padding-right: .25rem; text-align: right; font-family: var(--font-mono, monospace); font-size: .75rem; font-variant-numeric: tabular-nums; }
65
+ .fw-root .fw-slowest-caption { padding: 0 .75rem .5rem; color: var(--fw-muted); font-size: .75rem; overflow-wrap: anywhere; }
66
+ .fw-root .fw-text { color: var(--fw-text); }
67
+ .fw-root .fw-playback { display: flex; align-items: center; flex-wrap: wrap; gap: .5rem; border-top: 1px solid var(--fw-border); padding: .75rem; }
68
+ .fw-root .fw-speed, .fw-root .fw-position { display: flex; align-items: center; gap: .25rem; color: var(--fw-muted); font-size: .75rem; }
69
+ .fw-root .fw-position { flex: 1; min-width: 6rem; gap: .5rem; }
70
+ .fw-root .fw-slider { min-width: 0; flex: 1; accent-color: var(--fw-accent); }
71
+ .fw-root .fw-time { font-family: var(--font-mono, monospace); font-size: .75rem; font-variant-numeric: tabular-nums; color: var(--fw-muted); }
72
+ .fw-root .fw-sr-only { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; border: 0; }
73
+ @media (max-width: 480px) {
74
+ .fw-root .fw-summary { grid-template-columns: repeat(2, minmax(0, 1fr)); }
75
+ .fw-root .fw-position { flex-basis: 100%; }
76
+ }
@@ -1,5 +1,5 @@
1
- import { C as YScale, I as Refusal, S as XCategory, T as YTick, _ as ChartBand, b as MarkVerdict, f as TickOutcome, g as BandKind, h as PointGlyph, i as TimelineTick, j as Figure, m as PointFields, n as TimelineOptions, r as TimelineSpan, t as TimelineLane, v as ChartMark, w as YTarget, x as MarkerShape, y as MarkKind } from "../timeline-BvVxPTGp.js";
2
- import { ReactElement, ReactNode } from "react";
1
+ import { C as MarkerShape, D as YTick, E as YTarget, N as Figure, R as Refusal, S as MarkVerdict, T as YScale, _ as PointGlyph, a as TimelineSpan, b as ChartMark, g as PointFields, i as TimelineOptions, m as TickOutcome, n as FlowTrace, o as TimelineTick, r as TimelineLane, v as BandKind, w as XCategory, x as MarkKind, y as ChartBand } from "../flow-trace-B5dlYNsD.js";
2
+ import { CSSProperties, ReactElement, ReactNode } from "react";
3
3
  //#region src/react/sparkline.d.ts
4
4
  type SparklineDirection = "rising" | "falling" | "flat";
5
5
  type SparklineValues = readonly (number | null)[];
@@ -111,8 +111,10 @@ interface WaterfallTimeAxisProps {
111
111
  widthPx?: number;
112
112
  formatDuration?: (ms: number) => string;
113
113
  className?: string;
114
+ /** Fill the embedding grid track; the default retains Intelligence's fixed cell. */
115
+ fluid?: boolean;
114
116
  }
115
- export declare function WaterfallTimeAxis({ windowMs, originMs, widthPx, formatDuration, className }: WaterfallTimeAxisProps): import("react").JSX.Element | null;
117
+ export declare function WaterfallTimeAxis({ windowMs, originMs, widthPx, formatDuration, className, fluid }: WaterfallTimeAxisProps): import("react").JSX.Element | null;
116
118
  interface WaterfallBarProps {
117
119
  startMs: number;
118
120
  endMs: number;
@@ -120,17 +122,26 @@ interface WaterfallBarProps {
120
122
  tone?: "default" | "error" | "critical";
121
123
  formatDuration?: (ms: number) => string;
122
124
  className?: string;
125
+ /** Presentation only; the producer continues to own status and tone. */
126
+ color?: string;
127
+ barClassName?: string;
128
+ overlay?: ReactNode;
129
+ fluid?: boolean;
130
+ /** Clip playback without changing the recorded duration or its tooltip. */
131
+ clipAtMs?: number;
123
132
  }
124
- export declare function WaterfallBar({ startMs, endMs, window, tone, formatDuration, className }: WaterfallBarProps): import("react").JSX.Element;
133
+ export declare function WaterfallBar({ startMs, endMs, window, tone, formatDuration, className, color: suppliedColor, barClassName, overlay, fluid, clipAtMs }: WaterfallBarProps): import("react").JSX.Element;
125
134
  interface WaterfallRowProps extends Omit<WaterfallBarProps, "className"> {
126
135
  label: ReactNode;
127
136
  attribution?: ReactNode;
128
137
  timingClassName?: string;
129
138
  durationClassName?: string;
130
139
  className?: string;
140
+ style?: CSSProperties;
141
+ durationLabel?: ReactNode;
131
142
  }
132
143
  /** Non-interactive row body; its caller owns treeitems, buttons and selection. */
133
- export declare function WaterfallRow({ label, attribution, startMs, endMs, window, tone, formatDuration, className, timingClassName, durationClassName }: WaterfallRowProps): import("react").JSX.Element;
144
+ export declare function WaterfallRow({ label, attribution, startMs, endMs, window, tone, formatDuration, className, timingClassName, durationClassName, style, durationLabel, ...bar }: WaterfallRowProps): import("react").JSX.Element;
134
145
  interface WaterfallHeaderProps extends WaterfallTimeAxisProps {
135
146
  label?: ReactNode;
136
147
  attribution?: ReactNode;
@@ -231,6 +242,19 @@ export declare function Spark({ points, max, color, label, format, time }: {
231
242
  format?: (v: number) => string;
232
243
  time?: (t: number) => string;
233
244
  }): import("react").JSX.Element;
245
+ /** Recorded instants on one time axis, such as process restarts or deploys: a tick per event in `[from, to]`, with
246
+ * `ticks` evenly spaced axis labels under it. Hovering a tick names its time through `time`. The strip keeps its type
247
+ * size at any width, and `label` names it for assistive tech. */
248
+ export declare function Events({ at, from, to, label, color, ticks, axis, time }: {
249
+ at: number[];
250
+ from: number;
251
+ to: number;
252
+ label: string;
253
+ color?: string;
254
+ ticks?: number;
255
+ axis?: (t: number) => string;
256
+ time?: (t: number) => string;
257
+ }): import("react").JSX.Element;
234
258
  /** One bar per row on a shared time axis (who worked when): each row's bar spans its start to its end, coloured by its
235
259
  * state; hovering a span names it, its state and its span. Times are epoch ms; `axis` labels the time axis.
236
260
  * `lanes` give a row several spans (the same shape as the static `timeline`); a span with `end: null` is open and runs
@@ -323,4 +347,19 @@ interface DotMapProps {
323
347
  export declare const DOT_MAP_CATEGORIES: readonly string[];
324
348
  export declare function DotMap({ positions, radii, colors, palette, extent, rects, labels, dimmed, selected, minPixelRadius, onHover, onSelect, describe, height, ariaLabel }: DotMapProps): import("react").JSX.Element;
325
349
  //#endregion
326
- export type { BandKind, ChartBand, ChartMark, DotMapLabel, DotMapProps, DotMapRect, Figure, FigureAtWidth, FigureViewProps, 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 };
350
+ //#region src/react/flow-waterfall.d.ts
351
+ interface FlowWaterfallProps {
352
+ trace: FlowTrace;
353
+ /** Extend running spans between incoming snapshots. Inferred from span status by default. */
354
+ live?: boolean;
355
+ /** Epoch-ms origin when the producer supplies one; otherwise advance from the latest snapshot. */
356
+ startedAt?: number;
357
+ /** Accessible name also distinguishes multiple waterfalls on one page. */
358
+ label?: string;
359
+ /** Fold all branches on first mount for large traces; search still reveals matches. */
360
+ defaultCollapsed?: boolean;
361
+ }
362
+ /** Shared trace inspector. Data remains producer-owned; playback only clips its presentation. */
363
+ export declare function FlowWaterfall({ trace, live, startedAt, label, defaultCollapsed }: FlowWaterfallProps): import("react").JSX.Element;
364
+ //#endregion
365
+ export type { BandKind, ChartBand, ChartMark, DotMapLabel, DotMapProps, DotMapRect, Figure, FigureAtWidth, FigureViewProps, FlowWaterfallProps, LineSeries, MarkKind, MarkVerdict, MarkerShape, MeterSegment, PointFields, PointGlyph, Refusal, SparklineDirection, SparklineGeometry, SparklineGeometryOptions, SparklineLabelOptions, SparklinePoint, SparklineProps, SparklineValues, StackedBarBucket, StackedBarChartProps, StackedBarSegment, StackedBarSeries, TickOutcome, TimelineLane, TimelineOptions, TimelineSpan, TimelineTick, TimelineViewProps, WaterfallBarProps, WaterfallHeaderProps, WaterfallRowProps, WaterfallSpanGeometry, WaterfallTimeAxisProps, WaterfallWindow, XCategory, YScale, YTarget, YTick };
@@ -653,7 +653,7 @@ const durationStyle = {
653
653
  fontVariantNumeric: "tabular-nums"
654
654
  };
655
655
  const ordered = (start, end) => Number.isFinite(start) && Number.isFinite(end) && end >= start;
656
- function WaterfallTimeAxis({ windowMs, originMs = 0, widthPx = 208, formatDuration = formatWaterfallDuration, className }) {
656
+ function WaterfallTimeAxis({ windowMs, originMs = 0, widthPx = 208, formatDuration = formatWaterfallDuration, className, fluid = false }) {
657
657
  if (!ordered(0, windowMs) || !Number.isFinite(originMs)) return null;
658
658
  const ticks = waterfallTicks(windowMs, widthPx);
659
659
  if (windowMs === 0) return null;
@@ -662,7 +662,10 @@ function WaterfallTimeAxis({ windowMs, originMs = 0, widthPx = 208, formatDurati
662
662
  "data-waterfall-axis": "",
663
663
  "aria-hidden": "true",
664
664
  style: {
665
- ...timingCell,
665
+ ...fluid ? {
666
+ minWidth: 0,
667
+ flex: "1 1 0%"
668
+ } : timingCell,
666
669
  position: "relative",
667
670
  height: "var(--waterfall-axis-height, 1.5rem)"
668
671
  },
@@ -698,34 +701,40 @@ function WaterfallTimeAxis({ windowMs, originMs = 0, widthPx = 208, formatDurati
698
701
  })
699
702
  });
700
703
  }
701
- function WaterfallBar({ startMs, endMs, window, tone = "default", formatDuration = formatWaterfallDuration, className }) {
704
+ function WaterfallBar({ startMs, endMs, window, tone = "default", formatDuration = formatWaterfallDuration, className, color: suppliedColor, barClassName, overlay, fluid = false, clipAtMs }) {
705
+ const cell = fluid ? {
706
+ minWidth: 0,
707
+ flex: "1 1 0%"
708
+ } : timingCell;
702
709
  if (!ordered(startMs, endMs) || !ordered(window.startMs, window.endMs)) return /* @__PURE__ */ jsx("span", {
703
710
  className,
704
711
  "data-waterfall-track": "",
705
712
  "data-unavailable": "true",
706
713
  title: "Timing unavailable",
707
714
  style: {
708
- ...timingCell,
715
+ ...cell,
709
716
  textAlign: "center"
710
717
  },
711
718
  children: "—"
712
719
  });
713
720
  const geometry = waterfallSpanGeometry(startMs, endMs, window);
714
- const color = tone === "error" ? "var(--intel-waterfall-error, #dc2626)" : tone === "critical" ? "var(--intel-waterfall-critical, #0891b2)" : "var(--intel-waterfall-bar, #2563eb)";
715
- return /* @__PURE__ */ jsx("span", {
721
+ const color = suppliedColor ?? (tone === "error" ? "var(--intel-waterfall-error, #dc2626)" : tone === "critical" ? "var(--intel-waterfall-critical, #0891b2)" : "var(--intel-waterfall-bar, #2563eb)");
722
+ const visible = clipAtMs === void 0 ? geometry : waterfallSpanGeometry(startMs, Math.max(startMs, Math.min(endMs, clipAtMs)), window);
723
+ return /* @__PURE__ */ jsxs("span", {
716
724
  className,
717
725
  "data-waterfall-track": "",
718
726
  "data-tone": tone,
719
727
  title: `+${formatDuration(geometry.startOffsetMs)} · ${formatDuration(geometry.durationMs)}${tone === "critical" ? " · critical path" : ""}`,
720
728
  style: {
721
- ...timingCell,
729
+ ...cell,
722
730
  position: "relative",
723
731
  height: "var(--waterfall-bar-height, 0.875rem)",
724
732
  overflow: "hidden",
725
733
  borderRadius: "var(--waterfall-bar-radius, 999px)",
726
734
  background: "var(--intel-waterfall-track, rgb(148 163 184 / 22%))"
727
735
  },
728
- children: /* @__PURE__ */ jsx("span", {
736
+ children: [/* @__PURE__ */ jsx("span", {
737
+ className: barClassName,
729
738
  "data-waterfall-bar": "",
730
739
  "data-clipped": geometry.clipped || void 0,
731
740
  style: {
@@ -735,18 +744,21 @@ function WaterfallBar({ startMs, endMs, window, tone = "default", formatDuration
735
744
  borderRadius: "inherit",
736
745
  background: color,
737
746
  left: `${geometry.offsetPct}%`,
738
- width: `${geometry.widthPct}%`,
747
+ width: `${clipAtMs !== void 0 && clipAtMs <= startMs ? 0 : visible.widthPct}%`,
739
748
  boxShadow: tone === "default" ? void 0 : `0 0 0 1px ${color}`
740
749
  }
741
- })
750
+ }), overlay]
742
751
  });
743
752
  }
744
753
  /** Non-interactive row body; its caller owns treeitems, buttons and selection. */
745
- function WaterfallRow({ label, attribution, startMs, endMs, window, tone, formatDuration = formatWaterfallDuration, className, timingClassName, durationClassName }) {
754
+ function WaterfallRow({ label, attribution, startMs, endMs, window, tone, formatDuration = formatWaterfallDuration, className, timingClassName, durationClassName, style, durationLabel, ...bar }) {
746
755
  return /* @__PURE__ */ jsxs("span", {
747
756
  "data-waterfall-row": "",
748
757
  className,
749
- style: rowStyle,
758
+ style: style ? {
759
+ ...rowStyle,
760
+ ...style
761
+ } : rowStyle,
750
762
  children: [
751
763
  /* @__PURE__ */ jsx("span", {
752
764
  "data-waterfall-label": "",
@@ -760,13 +772,14 @@ function WaterfallRow({ label, attribution, startMs, endMs, window, tone, format
760
772
  window,
761
773
  tone,
762
774
  formatDuration,
763
- className: timingClassName
775
+ className: timingClassName,
776
+ ...bar
764
777
  }),
765
778
  /* @__PURE__ */ jsx("span", {
766
779
  "data-waterfall-duration": "",
767
780
  className: durationClassName,
768
781
  style: durationStyle,
769
- children: ordered(startMs, endMs) ? formatDuration(endMs - startMs) : "—"
782
+ children: durationLabel ?? (ordered(startMs, endMs) ? formatDuration(endMs - startMs) : "—")
770
783
  })
771
784
  ]
772
785
  });
@@ -1501,6 +1514,70 @@ function Spark({ points, max = 1, color = LIVE_SERIES[0], label, format = format
1501
1514
  ]
1502
1515
  });
1503
1516
  }
1517
+ /** Recorded instants on one time axis, such as process restarts or deploys: a tick per event in `[from, to]`, with
1518
+ * `ticks` evenly spaced axis labels under it. Hovering a tick names its time through `time`. The strip keeps its type
1519
+ * size at any width, and `label` names it for assistive tech. */
1520
+ function Events({ at, from, to, label, color = LIVE_SERIES[0], ticks = 7, axis = (t) => new Date(t).toLocaleDateString(void 0, { weekday: "short" }), time = (t) => new Date(t).toLocaleString() }) {
1521
+ const [ref, width, em] = useWidth();
1522
+ if (!Number.isFinite(from) || !Number.isFinite(to) || to <= from) throw new RangeError("Events needs a window with to after from.");
1523
+ const shown = at.filter((t) => Number.isFinite(t) && t >= from && t <= to).sort((a, b) => a - b);
1524
+ const strip = Math.round(2.2 * em);
1525
+ const height = strip + Math.round(1.5 * em);
1526
+ const pad = 6;
1527
+ const x = (t) => pad + (t - from) / (to - from) * Math.max(0, width - 12);
1528
+ const steps = Math.max(1, Math.round(ticks));
1529
+ return /* @__PURE__ */ jsx("div", {
1530
+ ref,
1531
+ className: "ov-plot",
1532
+ children: width > 0 && /* @__PURE__ */ jsxs("svg", {
1533
+ width,
1534
+ height,
1535
+ role: "img",
1536
+ "aria-label": `${label}: ${shown.length} in the window`,
1537
+ children: [
1538
+ /* @__PURE__ */ jsx("line", {
1539
+ x1: pad,
1540
+ x2: width - pad,
1541
+ y1: strip / 2,
1542
+ y2: strip / 2,
1543
+ className: "ov-gridline"
1544
+ }),
1545
+ shown.map((t, i) => /* @__PURE__ */ jsxs("g", {
1546
+ "data-event": t,
1547
+ children: [
1548
+ /* @__PURE__ */ jsx("title", { children: time(t) }),
1549
+ /* @__PURE__ */ jsx("rect", {
1550
+ x: x(t) - 6,
1551
+ y: 0,
1552
+ width: 12,
1553
+ height: strip,
1554
+ fill: "transparent"
1555
+ }),
1556
+ /* @__PURE__ */ jsx("line", {
1557
+ x1: x(t),
1558
+ x2: x(t),
1559
+ y1: 4,
1560
+ y2: strip - 4,
1561
+ stroke: color,
1562
+ strokeWidth: 2,
1563
+ strokeLinecap: "round"
1564
+ })
1565
+ ]
1566
+ }, `${t}-${i}`)),
1567
+ Array.from({ length: steps + 1 }, (_, i) => {
1568
+ const t = from + i / steps * (to - from);
1569
+ return /* @__PURE__ */ jsx("text", {
1570
+ x: x(t),
1571
+ y: height - em * .35,
1572
+ className: "ov-axis",
1573
+ textAnchor: i === 0 ? "start" : i === steps ? "end" : "middle",
1574
+ children: axis(t)
1575
+ }, i);
1576
+ })
1577
+ ]
1578
+ })
1579
+ });
1580
+ }
1504
1581
  /** One bar per row on a shared time axis (who worked when): each row's bar spans its start to its end, coloured by its
1505
1582
  * state; hovering a span names it, its state and its span. Times are epoch ms; `axis` labels the time axis.
1506
1583
  * `lanes` give a row several spans (the same shape as the static `timeline`); a span with `end: null` is open and runs
@@ -2228,4 +2305,494 @@ function DotMap({ positions, radii, colors, palette, extent, rects, labels, dimm
2228
2305
  });
2229
2306
  }
2230
2307
  //#endregion
2231
- export { DEFAULT_SPARKLINE_EMPTY_LABEL, DEFAULT_SPARKLINE_HEIGHT, DEFAULT_SPARKLINE_LABEL, DEFAULT_SPARKLINE_UNAVAILABLE_LABEL, DEFAULT_SPARKLINE_WIDTH, DOT_MAP_CATEGORIES, DayBars, DotMap, FigureView, Gantt, HBars, Histogram, LIVE_SERIES, Lines, Meter, Spark, Sparkline, StackedBarChart, TimelineView, WaterfallBar, WaterfallHeader, WaterfallRow, WaterfallTimeAxis, formatCompact, formatSparklineValue, formatWaterfallDuration, sparklineGeometry, sparklineLabel, sparklinePointsAttribute, sparklineReadings, waterfallSpanGeometry, waterfallTicks };
2308
+ //#region src/react/flow-waterfall-model.ts
2309
+ const spanFailed = (span) => span.status === "failed";
2310
+ const spanRunning = (span) => span.status === "running";
2311
+ /** Hierarchy only; the shared timing primitives own all bar geometry.
2312
+ * Orphaned parents become roots; cycles are broken without dropping any span. */
2313
+ function flowWaterfallRows(trace) {
2314
+ const ids = /* @__PURE__ */ new Set();
2315
+ const rows = [...trace.spans].map((span, i) => ({
2316
+ span,
2317
+ key: span.id ?? `span-${i}`
2318
+ })).sort((a, b) => a.span.startMs - b.span.startMs).map(({ span, key }, index) => {
2319
+ let id = key;
2320
+ let suffix = index;
2321
+ while (ids.has(id)) id = `${key}:${suffix++}`;
2322
+ ids.add(id);
2323
+ return {
2324
+ id,
2325
+ parentId: span.parentId,
2326
+ name: span.name,
2327
+ kind: span.kind,
2328
+ durationLabel: Number.isFinite(span.startMs) && Number.isFinite(span.endMs) && span.endMs >= span.startMs ? `${((span.endMs - span.startMs) / 1e3).toFixed(1)}s${span.approx ? "~" : ""}` : "—",
2329
+ approx: span.approx === true,
2330
+ ok: !spanFailed(span),
2331
+ depth: 0,
2332
+ hasChildren: false,
2333
+ span
2334
+ };
2335
+ });
2336
+ const byId = new Map(rows.map((row) => [row.id, row]));
2337
+ const children = /* @__PURE__ */ new Map();
2338
+ for (const row of rows) {
2339
+ const seen = /* @__PURE__ */ new Set([row.id]);
2340
+ let parent = row.parentId;
2341
+ while (parent && byId.has(parent)) {
2342
+ if (seen.has(parent)) {
2343
+ row.parentId = void 0;
2344
+ break;
2345
+ }
2346
+ seen.add(parent);
2347
+ parent = byId.get(parent)?.parentId;
2348
+ }
2349
+ if (row.parentId && !byId.has(row.parentId)) row.parentId = void 0;
2350
+ if (row.parentId) {
2351
+ const siblings = children.get(row.parentId) ?? [];
2352
+ siblings.push(row);
2353
+ children.set(row.parentId, siblings);
2354
+ }
2355
+ }
2356
+ const ordered = [];
2357
+ const visit = (row, depth) => {
2358
+ row.depth = depth;
2359
+ row.hasChildren = children.has(row.id);
2360
+ ordered.push(row);
2361
+ for (const child of children.get(row.id) ?? []) visit(child, depth + 1);
2362
+ };
2363
+ for (const row of rows) if (!row.parentId) visit(row, 0);
2364
+ return ordered;
2365
+ }
2366
+ /** Search keeps ancestor context and deliberately reveals matches inside collapsed branches. */
2367
+ function visibleFlowWaterfallRows(rows, query, collapsed) {
2368
+ const byId = new Map(rows.map((row) => [row.id, row]));
2369
+ const term = query.trim().toLowerCase();
2370
+ if (term) {
2371
+ const matching = /* @__PURE__ */ new Set();
2372
+ for (const row of rows) {
2373
+ if (!`${row.name} ${row.kind} ${row.ok ? "" : "failed"} ${row.span.status ?? ""}`.toLowerCase().includes(term)) continue;
2374
+ let cursor = row;
2375
+ while (cursor && !matching.has(cursor.id)) {
2376
+ matching.add(cursor.id);
2377
+ cursor = cursor.parentId ? byId.get(cursor.parentId) : void 0;
2378
+ }
2379
+ }
2380
+ return rows.filter((row) => matching.has(row.id));
2381
+ }
2382
+ const hidden = /* @__PURE__ */ new Set();
2383
+ return rows.filter((row) => {
2384
+ const isHidden = row.parentId !== void 0 && (collapsed.has(row.parentId) || hidden.has(row.parentId));
2385
+ if (isHidden) hidden.add(row.id);
2386
+ return !isHidden;
2387
+ });
2388
+ }
2389
+ //#endregion
2390
+ //#region src/react/flow-waterfall.tsx
2391
+ const TONES = {
2392
+ pipeline: {
2393
+ label: "Orchestrator",
2394
+ color: "fw-orchestrator"
2395
+ },
2396
+ model: {
2397
+ label: "Model call",
2398
+ color: "fw-model"
2399
+ },
2400
+ tool: {
2401
+ label: "Tool call",
2402
+ color: "fw-tool"
2403
+ },
2404
+ waiting: {
2405
+ label: "Waiting",
2406
+ color: "fw-waiting"
2407
+ },
2408
+ handoff: {
2409
+ label: "Handoff",
2410
+ color: "fw-handoff"
2411
+ },
2412
+ failed: {
2413
+ label: "Failed",
2414
+ color: "fw-failed"
2415
+ }
2416
+ };
2417
+ const SPEEDS = [
2418
+ .5,
2419
+ 1,
2420
+ 1.5,
2421
+ 2,
2422
+ 4
2423
+ ];
2424
+ const controlClass = "fw-control";
2425
+ const seconds = (ms) => `${(ms / 1e3).toFixed(1)}s`;
2426
+ /** Shared trace inspector. Data remains producer-owned; playback only clips its presentation. */
2427
+ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defaultCollapsed = false }) {
2428
+ const root = useRef(null);
2429
+ const search = useRef(null);
2430
+ const searchId = useId();
2431
+ const [query, setQuery] = useState("");
2432
+ const [collapsed, setCollapsed] = useState(() => new Set(defaultCollapsed ? trace.spans.flatMap((span) => span.parentId ? [span.parentId] : []) : []));
2433
+ const [speed, setSpeed] = useState(1);
2434
+ const [playing, setPlaying] = useState(false);
2435
+ const [position, setPosition] = useState(null);
2436
+ const observedTotal = Number.isFinite(trace.totalMs) && trace.totalMs >= 0 ? trace.totalMs : 0;
2437
+ const [elapsed, setElapsed] = useState(observedTotal);
2438
+ const isLive = live ?? trace.spans.some(spanRunning);
2439
+ useEffect(() => {
2440
+ setElapsed(observedTotal);
2441
+ if (!isLive) return;
2442
+ const receivedAt = Date.now();
2443
+ const tick = () => setElapsed(Math.max(observedTotal, startedAt === void 0 ? observedTotal + Date.now() - receivedAt : Date.now() - startedAt));
2444
+ tick();
2445
+ const timer = setInterval(tick, 100);
2446
+ return () => clearInterval(timer);
2447
+ }, [
2448
+ trace,
2449
+ observedTotal,
2450
+ isLive,
2451
+ startedAt
2452
+ ]);
2453
+ const currentTrace = useMemo(() => {
2454
+ const totalMs = Math.max(observedTotal, isLive ? elapsed : 0, ...trace.spans.map((span) => Number.isFinite(span.endMs) ? span.endMs : 0));
2455
+ return {
2456
+ ...trace,
2457
+ totalMs,
2458
+ spans: trace.spans.map((span) => isLive && spanRunning(span) && Number.isFinite(span.endMs) && span.endMs >= span.startMs ? {
2459
+ ...span,
2460
+ endMs: Math.max(span.endMs, totalMs)
2461
+ } : span)
2462
+ };
2463
+ }, [
2464
+ trace,
2465
+ observedTotal,
2466
+ elapsed,
2467
+ isLive
2468
+ ]);
2469
+ const totalMs = currentTrace.totalMs;
2470
+ const playhead = Math.min(position ?? totalMs, totalMs);
2471
+ const playbackLimit = useRef(totalMs);
2472
+ useEffect(() => {
2473
+ playbackLimit.current = totalMs;
2474
+ }, [totalMs]);
2475
+ useEffect(() => {
2476
+ if (!playing) return;
2477
+ let last = performance.now();
2478
+ const timer = setInterval(() => {
2479
+ const now = performance.now();
2480
+ const delta = (now - last) * speed;
2481
+ last = now;
2482
+ setPosition((previous) => Math.min(playbackLimit.current, (previous ?? 0) + delta));
2483
+ }, 50);
2484
+ return () => clearInterval(timer);
2485
+ }, [playing, speed]);
2486
+ useEffect(() => {
2487
+ if (playing && position !== null && position >= totalMs) setPlaying(false);
2488
+ }, [
2489
+ position,
2490
+ playing,
2491
+ totalMs
2492
+ ]);
2493
+ useEffect(() => {
2494
+ const keydown = (event) => {
2495
+ if (!(event.metaKey || event.ctrlKey) || event.key.toLowerCase() !== "f") return;
2496
+ const active = document.activeElement;
2497
+ const ownsFocus = root.current?.contains(active);
2498
+ const first = Array.from(document.querySelectorAll("[data-flow-waterfall]")).find((element) => element.getClientRects().length > 0);
2499
+ if (!ownsFocus && (active !== document.body || first !== root.current)) return;
2500
+ event.preventDefault();
2501
+ search.current?.focus();
2502
+ search.current?.select();
2503
+ };
2504
+ document.addEventListener("keydown", keydown);
2505
+ return () => document.removeEventListener("keydown", keydown);
2506
+ }, []);
2507
+ const rows = useMemo(() => flowWaterfallRows(currentTrace), [currentTrace]);
2508
+ const visible = visibleFlowWaterfallRows(rows, query, collapsed);
2509
+ const slowest = rows.filter((row) => !row.hasChildren && row.kind !== "pipeline" && row.durationLabel !== "—").reduce((best, row) => !best || row.span.endMs - row.span.startMs > best.span.endMs - best.span.startMs ? row : best, void 0);
2510
+ const summary = [
2511
+ ["Duration", seconds(totalMs)],
2512
+ ["Cost", trace.costUsd === void 0 ? "Unknown" : `$${trace.costUsd.toFixed(trace.costUsd < .01 ? 6 : 4)}`],
2513
+ ["Tokens", (trace.promptTokens + trace.completionTokens).toLocaleString()],
2514
+ ["LLM calls", trace.modelCalls === void 0 ? "Unknown" : String(trace.modelCalls)],
2515
+ ["Tool calls", String(trace.toolCalls)],
2516
+ ["Errors", String(trace.errors ?? trace.spans.filter(spanFailed).length)]
2517
+ ];
2518
+ const toggle = (id) => setCollapsed((previous) => {
2519
+ const next = new Set(previous);
2520
+ if (next.has(id)) next.delete(id);
2521
+ else next.add(id);
2522
+ return next;
2523
+ });
2524
+ return /* @__PURE__ */ jsxs("section", {
2525
+ ref: root,
2526
+ "data-flow-waterfall": true,
2527
+ "aria-label": label,
2528
+ className: "fw-root",
2529
+ children: [
2530
+ /* @__PURE__ */ jsxs("div", {
2531
+ className: "fw-heading",
2532
+ children: [/* @__PURE__ */ jsxs("div", {
2533
+ className: "fw-title",
2534
+ children: [label, isLive && /* @__PURE__ */ jsx("span", {
2535
+ className: "fw-muted",
2536
+ children: "Live"
2537
+ })]
2538
+ }), /* @__PURE__ */ jsxs("div", {
2539
+ className: "fw-search",
2540
+ children: [
2541
+ /* @__PURE__ */ jsx("label", {
2542
+ htmlFor: searchId,
2543
+ className: "fw-sr-only",
2544
+ children: "Search trace"
2545
+ }),
2546
+ /* @__PURE__ */ jsx("input", {
2547
+ ref: search,
2548
+ id: searchId,
2549
+ type: "search",
2550
+ value: query,
2551
+ placeholder: "Search trace…",
2552
+ "aria-keyshortcuts": "Meta+f Control+f",
2553
+ onChange: (event) => setQuery(event.target.value),
2554
+ onKeyDown: (event) => {
2555
+ if (event.key === "Escape") {
2556
+ setQuery("");
2557
+ event.stopPropagation();
2558
+ }
2559
+ },
2560
+ className: `${controlClass} fw-search-input`
2561
+ }),
2562
+ /* @__PURE__ */ jsx("button", {
2563
+ type: "button",
2564
+ "aria-label": "Focus trace search",
2565
+ title: "Search trace (⌘F / Ctrl+F)",
2566
+ onClick: () => search.current?.focus(),
2567
+ className: controlClass,
2568
+ children: "⌘F"
2569
+ })
2570
+ ]
2571
+ })]
2572
+ }),
2573
+ /* @__PURE__ */ jsx("dl", {
2574
+ className: "fw-summary",
2575
+ children: summary.map(([name, value]) => /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", {
2576
+ className: "fw-muted",
2577
+ children: name
2578
+ }), /* @__PURE__ */ jsx("dd", {
2579
+ className: "fw-value",
2580
+ title: name === "LLM calls" && trace.modelCalls === void 0 ? "Model output windows do not establish the number of LLM requests" : void 0,
2581
+ children: value
2582
+ })] }, name))
2583
+ }),
2584
+ /* @__PURE__ */ jsx("div", {
2585
+ className: "fw-legend",
2586
+ "aria-label": "Span types",
2587
+ children: Object.entries(TONES).map(([kind, tone]) => /* @__PURE__ */ jsxs("span", {
2588
+ className: "fw-legend-item",
2589
+ children: [/* @__PURE__ */ jsx("span", {
2590
+ "aria-hidden": true,
2591
+ className: `fw-dot ${tone.color}`
2592
+ }), tone.label]
2593
+ }, kind))
2594
+ }),
2595
+ rows.some((row) => row.hasChildren) && /* @__PURE__ */ jsxs("div", {
2596
+ className: "fw-branches",
2597
+ children: [/* @__PURE__ */ jsx("button", {
2598
+ type: "button",
2599
+ className: controlClass,
2600
+ onClick: () => setCollapsed(/* @__PURE__ */ new Set()),
2601
+ children: "Expand all"
2602
+ }), /* @__PURE__ */ jsx("button", {
2603
+ type: "button",
2604
+ className: controlClass,
2605
+ onClick: () => setCollapsed(new Set(rows.filter((row) => row.hasChildren).map((row) => row.id))),
2606
+ children: "Collapse all"
2607
+ })]
2608
+ }),
2609
+ /* @__PURE__ */ jsx("div", {
2610
+ className: "fw-timeline",
2611
+ role: "region",
2612
+ "aria-label": "Trace timeline",
2613
+ tabIndex: 0,
2614
+ children: /* @__PURE__ */ jsxs("div", {
2615
+ className: "fw-track",
2616
+ children: [
2617
+ /* @__PURE__ */ jsxs("div", {
2618
+ className: "fw-axis fw-grid",
2619
+ children: [
2620
+ /* @__PURE__ */ jsxs("span", { children: [
2621
+ "Steps · ",
2622
+ visible.length,
2623
+ "/",
2624
+ rows.length
2625
+ ] }),
2626
+ /* @__PURE__ */ jsx("div", { children: /* @__PURE__ */ jsx(WaterfallTimeAxis, {
2627
+ fluid: true,
2628
+ windowMs: totalMs,
2629
+ widthPx: 240,
2630
+ formatDuration: seconds
2631
+ }) }),
2632
+ /* @__PURE__ */ jsx("span", {
2633
+ className: "fw-align-right",
2634
+ children: "Duration"
2635
+ })
2636
+ ]
2637
+ }),
2638
+ visible.length === 0 && /* @__PURE__ */ jsx("p", {
2639
+ role: "status",
2640
+ className: "fw-empty",
2641
+ children: rows.length === 0 ? "No spans yet. Waiting for trace events." : `No matching steps for “${query}”.`
2642
+ }),
2643
+ visible.map((row) => {
2644
+ const tone = !row.ok ? TONES.failed : TONES[row.kind];
2645
+ const pending = row.span.startMs > playhead;
2646
+ const running = spanRunning(row.span);
2647
+ return /* @__PURE__ */ jsx("div", {
2648
+ "data-span-id": row.id,
2649
+ "data-span-kind": !row.ok ? "failed" : row.kind,
2650
+ "data-slowest": row.id === slowest?.id || void 0,
2651
+ className: `fw-row ${row.id === slowest?.id ? "fw-slowest" : ""} ${pending ? "fw-pending" : ""}`,
2652
+ children: /* @__PURE__ */ jsx(WaterfallRow, {
2653
+ className: "fw-grid",
2654
+ style: { display: "grid" },
2655
+ startMs: row.span.startMs,
2656
+ endMs: row.span.endMs,
2657
+ window: {
2658
+ startMs: 0,
2659
+ endMs: totalMs
2660
+ },
2661
+ fluid: true,
2662
+ color: "var(--fw-bar-color)",
2663
+ barClassName: `fw-bar ${tone.color} ${row.approx ? "fw-approx" : ""}`,
2664
+ timingClassName: "fw-bar-track",
2665
+ durationClassName: "fw-duration",
2666
+ durationLabel: row.durationLabel,
2667
+ formatDuration: seconds,
2668
+ clipAtMs: position === null ? void 0 : playhead,
2669
+ overlay: position !== null && /* @__PURE__ */ jsx("span", {
2670
+ className: "fw-playhead",
2671
+ style: { left: `${playhead / (totalMs || 1) * 100}%` }
2672
+ }),
2673
+ label: /* @__PURE__ */ jsxs("div", {
2674
+ className: "fw-name",
2675
+ style: { paddingLeft: Math.min(row.depth, 8) * 12 },
2676
+ children: [row.hasChildren ? /* @__PURE__ */ jsx("button", {
2677
+ type: "button",
2678
+ "aria-label": `${collapsed.has(row.id) && !query ? "Expand" : "Collapse"} ${row.name}`,
2679
+ "aria-expanded": query ? true : !collapsed.has(row.id),
2680
+ onClick: () => toggle(row.id),
2681
+ disabled: Boolean(query.trim()),
2682
+ className: "fw-toggle",
2683
+ children: collapsed.has(row.id) && !query ? "›" : "⌄"
2684
+ }) : /* @__PURE__ */ jsx("span", {
2685
+ className: "fw-leaf",
2686
+ "aria-hidden": true,
2687
+ children: "·"
2688
+ }), /* @__PURE__ */ jsxs("span", {
2689
+ title: `${row.name} · ${tone.label}${running ? " · running" : ""}`,
2690
+ className: "fw-span-name",
2691
+ children: [
2692
+ row.name,
2693
+ !row.ok && /* @__PURE__ */ jsx("span", {
2694
+ className: "fw-error-text",
2695
+ children: " · failed"
2696
+ }),
2697
+ running && /* @__PURE__ */ jsx("span", {
2698
+ className: "fw-muted",
2699
+ children: " · running"
2700
+ })
2701
+ ]
2702
+ })]
2703
+ })
2704
+ })
2705
+ }, row.id);
2706
+ })
2707
+ ]
2708
+ })
2709
+ }),
2710
+ slowest && /* @__PURE__ */ jsxs("p", {
2711
+ className: "fw-slowest-caption",
2712
+ children: [
2713
+ "Slowest step: ",
2714
+ /* @__PURE__ */ jsx("span", {
2715
+ className: "fw-text",
2716
+ children: slowest.name
2717
+ }),
2718
+ " · ",
2719
+ slowest.durationLabel
2720
+ ]
2721
+ }),
2722
+ /* @__PURE__ */ jsxs("div", {
2723
+ className: "fw-playback",
2724
+ children: [
2725
+ /* @__PURE__ */ jsx("button", {
2726
+ type: "button",
2727
+ className: controlClass,
2728
+ disabled: totalMs === 0,
2729
+ onClick: () => {
2730
+ if (playing) setPlaying(false);
2731
+ else {
2732
+ if (position === null || position >= totalMs) setPosition(0);
2733
+ setPlaying(true);
2734
+ }
2735
+ },
2736
+ children: playing ? "Pause" : "Play"
2737
+ }),
2738
+ /* @__PURE__ */ jsx("button", {
2739
+ type: "button",
2740
+ className: controlClass,
2741
+ disabled: totalMs === 0,
2742
+ onClick: () => {
2743
+ setPosition(0);
2744
+ setPlaying(false);
2745
+ },
2746
+ children: "Restart"
2747
+ }),
2748
+ /* @__PURE__ */ jsxs("label", {
2749
+ className: "fw-speed",
2750
+ children: ["Speed", /* @__PURE__ */ jsx("select", {
2751
+ "aria-label": "Playback speed",
2752
+ className: controlClass,
2753
+ value: speed,
2754
+ onChange: (event) => setSpeed(Number(event.target.value)),
2755
+ children: SPEEDS.map((value) => /* @__PURE__ */ jsxs("option", {
2756
+ value,
2757
+ children: [value, "x"]
2758
+ }, value))
2759
+ })]
2760
+ }),
2761
+ /* @__PURE__ */ jsxs("label", {
2762
+ className: "fw-position",
2763
+ children: ["Position", /* @__PURE__ */ jsx("input", {
2764
+ "aria-label": "Playback position",
2765
+ type: "range",
2766
+ min: 0,
2767
+ max: totalMs || 1,
2768
+ step: "any",
2769
+ value: playhead,
2770
+ disabled: totalMs === 0,
2771
+ onChange: (event) => {
2772
+ setPosition(Number(event.target.value));
2773
+ setPlaying(false);
2774
+ },
2775
+ className: "fw-slider"
2776
+ })]
2777
+ }),
2778
+ /* @__PURE__ */ jsx("output", {
2779
+ className: "fw-time",
2780
+ "aria-label": "Playback time",
2781
+ children: seconds(playhead)
2782
+ }),
2783
+ position !== null && /* @__PURE__ */ jsx("button", {
2784
+ type: "button",
2785
+ className: controlClass,
2786
+ onClick: () => {
2787
+ setPosition(null);
2788
+ setPlaying(false);
2789
+ },
2790
+ children: isLive ? "Follow live" : "Show full trace"
2791
+ })
2792
+ ]
2793
+ })
2794
+ ]
2795
+ });
2796
+ }
2797
+ //#endregion
2798
+ 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, HBars, Histogram, LIVE_SERIES, Lines, Meter, Spark, Sparkline, StackedBarChart, TimelineView, WaterfallBar, WaterfallHeader, WaterfallRow, WaterfallTimeAxis, 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.13.0",
3
+ "version": "0.15.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",
@@ -14,7 +14,8 @@
14
14
  "homepage": "https://github.com/tangle-network/charts",
15
15
  "sideEffects": [
16
16
  "./dist/figure.css",
17
- "./dist/react/live.css"
17
+ "./dist/react/live.css",
18
+ "./dist/react/flow-waterfall.css"
18
19
  ],
19
20
  "files": [
20
21
  "dist",
@@ -32,7 +33,8 @@
32
33
  "types": "./dist/react/index.d.ts",
33
34
  "import": "./dist/react/index.js",
34
35
  "default": "./dist/react/index.js"
35
- }
36
+ },
37
+ "./react/flow-waterfall.css": "./dist/react/flow-waterfall.css"
36
38
  },
37
39
  "peerDependencies": {
38
40
  "react": "^18.0.0 || ^19.0.0"
@@ -44,7 +46,7 @@
44
46
  },
45
47
  "scripts": {
46
48
  "gen:css": "node scripts/gen-css.mjs",
47
- "build": "node scripts/gen-css.mjs --check && tsdown && cp src/figure.css dist/figure.css && cp src/react/live.css dist/react/live.css",
49
+ "build": "node scripts/gen-css.mjs --check && tsdown && cp src/figure.css dist/figure.css && cp src/react/live.css dist/react/live.css && cp src/react/flow-waterfall.css dist/react/flow-waterfall.css",
48
50
  "typecheck": "tsc --noEmit",
49
51
  "test": "vitest run"
50
52
  },
@@ -55,6 +57,7 @@
55
57
  "@types/node": "^26.6.3",
56
58
  "@types/react": "^19.3.0",
57
59
  "@types/react-dom": "^19.2.3",
60
+ "esbuild": "^0.28.2",
58
61
  "jsdom": "^30.1.1",
59
62
  "playwright": "^1.63.0",
60
63
  "react": "^19.3.0",