@tangle-network/charts 0.21.13 → 0.21.15

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.
@@ -196,8 +196,10 @@
196
196
  .fw-root .fw-view-agents .fw-names { flex-direction: column; align-items: flex-start; gap: 0; }
197
197
  .fw-root .fw-view-agents .fw-span-name { font-weight: 600; line-height: 18px; }
198
198
  /* One 16 px line that wraps its last part out of sight: when the line is full the tool count drops whole, never "7 to…". */
199
- .fw-root .fw-view-agents .fw-subtitle { display: flex; flex-wrap: wrap; align-items: center; column-gap: 4px; height: 16px; overflow: hidden; max-width: 100%; min-width: 0; color: var(--fw-muted); font-size: 13px; line-height: 16px; }
199
+ .fw-root .fw-view-agents .fw-subtitle { display: flex; align-items: center; column-gap: 4px; height: 16px; overflow: hidden; max-width: 100%; min-width: 0; color: var(--fw-muted); font-size: 13px; line-height: 16px; }
200
200
  .fw-root .fw-view-agents .fw-subtitle-extra { flex: none; white-space: nowrap; }
201
+ /* Only the tail wraps, so the text is always first on its line and shrinks with an ellipsis; the tool count wraps out of sight. */
202
+ .fw-root .fw-view-agents .fw-subtitle-tail { display: flex; flex-wrap: wrap; column-gap: 4px; height: 16px; overflow: hidden; min-width: 0; flex: 0 1 auto; }
201
203
  .fw-root .fw-view-agents .fw-subtitle-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
202
204
  .fw-root .fw-view-agents .fw-detail-icon { flex: none; width: 16px; height: 16px; align-items: center; justify-content: center; overflow: hidden; }
203
205
  .fw-root .fw-view-agents .fw-detail { font-size: 12px; line-height: 16px; }
@@ -224,6 +226,9 @@
224
226
  .fw-root .fw-row[data-running] .fw-bar { transition: width 120ms linear; }
225
227
  /* The chain that gates the trace's end: an accent edge under its bars, so cutting latency there is visibly what moves the end. */
226
228
  .fw-root .fw-row[data-critical] .fw-bar { box-shadow: inset 0 -2px 0 var(--fw-accent); }
229
+ /* While searching, ancestors shown only for context recede and the matching names stand out. */
230
+ .fw-root .fw-row[data-search="context"] .fw-label { opacity: 0.55; }
231
+ .fw-root .fw-row[data-search="match"] .fw-name-text { font-weight: 600; }
227
232
  /* A restart: an amber notch through the bar at that moment; the bar keeps its own state. */
228
233
  .fw-root .fw-mark { position: absolute; z-index: 2; left: calc(var(--x0) - 1px); width: 2px; top: calc(50% + var(--fw-bar-shift) - var(--fw-bar-h) / 2 - 4px); height: calc(var(--fw-bar-h) + 8px);
229
234
  border-radius: 2px; background: var(--fw-amber); box-shadow: 0 0 0 1px var(--fw-surface); opacity: var(--on); pointer-events: auto; }
@@ -284,6 +289,12 @@
284
289
  agent's own hue (the rules above), so each agent stays one color. */
285
290
  .fw-root:is(.fw-fill-muted, .fw-fill-outcome) .fw-view-agents .fw-seg-model { background: var(--fw-kind-model); box-shadow: none; }
286
291
  .fw-root:is(.fw-fill-muted, .fw-fill-outcome) .fw-view-agents .fw-seg-tool { background: var(--fw-kind-tool); box-shadow: none; }
292
+ /* Per-agent hues at full saturation (blind round 26: muted per-agent bars "blur together", "wash out on white"): kind is a darker or
293
+ lighter step of the agent's own saturated hue, every solid segment at least 3:1 against the card; waiting stays the only pale fill. */
294
+ .fw-root.fw-fill-agent .fw-view-agents .fw-seg-model { background: light-dark(color-mix(in oklab, var(--tone) 78%, #000), var(--tone)); box-shadow: none; }
295
+ .fw-root.fw-fill-agent .fw-view-agents .fw-seg-tool { background: light-dark(var(--tone), color-mix(in oklab, var(--tone) 72%, #fff)); box-shadow: none; }
296
+ /* A failed turn is still the agent's work: it keeps the agent's saturated hue, and the red cross on the ending turn says it failed. */
297
+ .fw-root.fw-fill-agent .fw-view-agents .fw-seg-failed { background: light-dark(var(--tone), color-mix(in oklab, var(--tone) 72%, #fff)); box-shadow: inset 0 0 0 1px color-mix(in oklab, var(--fw-failed) 55%, transparent); }
287
298
  .fw-root .fw-view-agents .fw-seg-tool { container-type: inline-size; justify-content: flex-start; }
288
299
  .fw-root .fw-seg-name { padding: 0 6px; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; font-size: 11px; line-height: 16px; font-weight: 600; color: light-dark(#fff, #06222b); pointer-events: none; }
289
300
  @container (max-width: 56px) { .fw-root .fw-seg-name { display: none; } }
@@ -2422,6 +2422,7 @@ function DotMap({ positions, radii, colors, palette, extent, rects, labels, dimm
2422
2422
  }
2423
2423
  //#endregion
2424
2424
  //#region src/react/flow-waterfall-model.ts
2425
+ const toolOf = (name) => name?.split(" · ")[0];
2425
2426
  /** The CSS hue slot: 0 is the root agent's neutral, then five agent hues cycle. */
2426
2427
  const toneKey = (tone) => tone <= 0 ? 0 : (tone - 1) % 5 + 1;
2427
2428
  const spanFailed = (span) => span.status === "failed";
@@ -2474,6 +2475,7 @@ function mergeSegments(marks, gapMs, cap = 240) {
2474
2475
  covered.set(last, share);
2475
2476
  last.endMs = Math.max(last.endMs, mark.endMs);
2476
2477
  last.count += mark.count;
2478
+ if (last.tool !== mark.tool) last.tool = void 0;
2477
2479
  last.kind = share.model >= share.tool ? "model" : "tool";
2478
2480
  } else out.push({ ...mark });
2479
2481
  }
@@ -2587,18 +2589,23 @@ function segmentOf(row) {
2587
2589
  id: row.id,
2588
2590
  count: 1,
2589
2591
  retry: row.retry,
2590
- name: row.name
2592
+ name: row.name,
2593
+ tool: toolOf(row.name)
2591
2594
  };
2592
2595
  }
2596
+ /** The rows a search term matches by themselves, before ancestor context is added. */
2597
+ function searchMatchIds(rows, query, matchSpan) {
2598
+ const term = query.trim();
2599
+ const lower = term.toLowerCase();
2600
+ return new Set(rows.filter((row) => matchSpan ? matchSpan(row.span, term) : `${row.name} ${row.span.detail ?? ""} ${row.kind} ${row.ok ? "" : "failed"} ${row.span.status ?? ""}`.toLowerCase().includes(lower)).map((row) => row.id));
2601
+ }
2593
2602
  /** Search keeps ancestor context and deliberately reveals matches inside collapsed branches. */
2594
2603
  function visibleFlowWaterfallRows(rows, query, collapsed, matchSpan) {
2595
2604
  const byId = new Map(rows.map((row) => [row.id, row]));
2596
- const term = query.trim().toLowerCase();
2597
- if (term) {
2605
+ if (query.trim()) {
2598
2606
  const matching = /* @__PURE__ */ new Set();
2599
- for (const row of rows) {
2600
- if (matchSpan ? !matchSpan(row.span, query.trim()) : !`${row.name} ${row.span.detail ?? ""} ${row.kind} ${row.ok ? "" : "failed"} ${row.span.status ?? ""}`.toLowerCase().includes(term)) continue;
2601
- let cursor = row;
2607
+ for (const id of searchMatchIds(rows, query, matchSpan)) {
2608
+ let cursor = byId.get(id);
2602
2609
  while (cursor && !matching.has(cursor.id)) {
2603
2610
  matching.add(cursor.id);
2604
2611
  cursor = cursor.parentId ? byId.get(cursor.parentId) : void 0;
@@ -2632,7 +2639,12 @@ function slowestRow(rows, clockMs) {
2632
2639
  * chain ends last wins. Cutting latency anywhere else leaves the trace's end unchanged. */
2633
2640
  function criticalPathIds(rows) {
2634
2641
  const children = /* @__PURE__ */ new Map();
2635
- for (const row of rows) if (row.parentId) children.set(row.parentId, [...children.get(row.parentId) ?? [], row]);
2642
+ for (const row of rows) {
2643
+ if (!row.parentId) continue;
2644
+ const siblings = children.get(row.parentId);
2645
+ if (siblings) siblings.push(row);
2646
+ else children.set(row.parentId, [row]);
2647
+ }
2636
2648
  let best;
2637
2649
  for (const root of rows) {
2638
2650
  if (root.parentId) continue;
@@ -2777,7 +2789,7 @@ function agentFamilies(agents, open) {
2777
2789
  const endMs = Math.max(...members.map((row) => row.span.endMs));
2778
2790
  const running = members.some((row) => spanRunning(row.span));
2779
2791
  const known = members.map((row) => row.span.costUsd ?? row.rollupCostUsd).filter((cost) => cost !== void 0);
2780
- const failed = members.filter((row) => !row.ok).length;
2792
+ const failed = members.filter((row) => row.span.outcome ? row.span.outcome.tone === "failed" : !row.ok).length;
2781
2793
  const outcome = members.every((row) => row.span.outcome && row.span.outcome.tone === members[0].span.outcome?.tone) ? members[0].span.outcome : void 0;
2782
2794
  out.push({
2783
2795
  costFloor: known.length < members.length || members.some((row) => row.costFloor),
@@ -2848,7 +2860,7 @@ function agentSummary(row) {
2848
2860
  };
2849
2861
  const turns = counts.model + counts.failed;
2850
2862
  const tools = counts.tool;
2851
- const state = row.span.outcome?.label ?? (spanRunning(row.span) ? "running" : row.ok ? "done" : "failed");
2863
+ const state = row.span.outcome?.label ?? (spanRunning(row.span) ? "running" : !row.ok ? "failed" : row.kind === "pipeline" ? "" : "done");
2852
2864
  const unit = row.kind === "pipeline" ? ["turn", "turns"] : ["model call", "model calls"];
2853
2865
  return [
2854
2866
  state,
@@ -2919,7 +2931,8 @@ function stepRuns(steps) {
2919
2931
  id: step.id,
2920
2932
  count: 1,
2921
2933
  retry: step.retry,
2922
- name: step.name
2934
+ name: step.name,
2935
+ tool: toolOf(step.name)
2923
2936
  })),
2924
2937
  ...costs.length ? { rollupCostUsd: costs.reduce((sum, cost) => sum + cost, 0) } : {},
2925
2938
  span: {
@@ -3313,7 +3326,7 @@ const at = (startMs, endMs) => ({
3313
3326
  const endLabelWidth = (label, slowest, costliest) => label.length * 7 + 8 + (slowest ? 74 : 0) + (costliest ? 84 : 0);
3314
3327
  function Segment({ mark, events, agentView, ending }) {
3315
3328
  const glyph = mark.kind === "failed" ? agentView && !ending ? null : /* @__PURE__ */ jsx(FailGlyph, {}) : mark.retry ? /* @__PURE__ */ jsx(RetryGlyph, {}) : null;
3316
- const named = agentView && mark.kind === "tool" && mark.count === 1 && mark.name;
3329
+ const named = agentView && mark.kind === "tool" && (mark.count === 1 ? mark.name : mark.tool && `${mark.tool} ×${mark.count}`);
3317
3330
  return /* @__PURE__ */ jsx("span", {
3318
3331
  className: `fw-seg fw-seg-${mark.kind}${mark.alt ? " fw-seg-alt" : ""}`,
3319
3332
  "data-tone": toneKey(mark.tone),
@@ -3329,7 +3342,7 @@ function Segment({ mark, events, agentView, ending }) {
3329
3342
  children: glyph
3330
3343
  }) : named && /* @__PURE__ */ jsx("span", {
3331
3344
  className: "fw-seg-name",
3332
- children: mark.name
3345
+ children: named
3333
3346
  })
3334
3347
  });
3335
3348
  }
@@ -3346,7 +3359,7 @@ function endLabel(row, clockMs) {
3346
3359
  }
3347
3360
  /** One trace row. Geometry comes from CSS variables, so a row re-renders only when its own state changes. */
3348
3361
  const FlowRow = memo(function FlowRow(props) {
3349
- const { row, view, top, collapsed, searching, slowest, costliest, critical, active, focusable, selected, sticky, clockMs, renderIcon, fillByAgent, motion } = props;
3362
+ const { row, view, top, collapsed, searching, slowest, costliest, critical, matched, active, focusable, selected, sticky, clockMs, renderIcon, fillByAgent, motion } = props;
3350
3363
  const span = row.span;
3351
3364
  const running = spanRunning(span);
3352
3365
  const agentView = view === "agents";
@@ -3368,6 +3381,7 @@ const FlowRow = memo(function FlowRow(props) {
3368
3381
  "data-span-kind": kind,
3369
3382
  "data-slowest": slowest || void 0,
3370
3383
  "data-critical": critical || void 0,
3384
+ "data-search": searching ? matched ? "match" : "context" : void 0,
3371
3385
  "data-running": running || void 0,
3372
3386
  "aria-level": agentView ? void 0 : row.depth + 1,
3373
3387
  "aria-expanded": row.hasChildren && !agentView ? !folded : void 0,
@@ -3469,21 +3483,20 @@ const FlowRow = memo(function FlowRow(props) {
3469
3483
  ]
3470
3484
  }), agentView && /* @__PURE__ */ jsxs("span", {
3471
3485
  className: "fw-subtitle",
3472
- children: [
3473
- renderIcon && /* @__PURE__ */ jsx("span", {
3474
- className: "fw-detail-icon",
3475
- "aria-hidden": true,
3476
- children: renderIcon(span)
3477
- }),
3478
- /* @__PURE__ */ jsx("span", {
3486
+ children: [renderIcon && /* @__PURE__ */ jsx("span", {
3487
+ className: "fw-detail-icon",
3488
+ "aria-hidden": true,
3489
+ children: renderIcon(span)
3490
+ }), /* @__PURE__ */ jsxs("span", {
3491
+ className: "fw-subtitle-tail",
3492
+ children: [/* @__PURE__ */ jsx("span", {
3479
3493
  className: "fw-subtitle-text",
3480
3494
  children: span.detail && span.detail !== props.sharedDetail ? `${span.detail} · ${summary[0]}` : summary[0]
3481
- }),
3482
- summary[1] && /* @__PURE__ */ jsxs("span", {
3495
+ }), summary[1] && /* @__PURE__ */ jsxs("span", {
3483
3496
  className: "fw-subtitle-extra",
3484
3497
  children: ["· ", summary[1]]
3485
- })
3486
- ]
3498
+ })]
3499
+ })]
3487
3500
  })]
3488
3501
  }),
3489
3502
  folded && /* @__PURE__ */ jsxs("span", {
@@ -3562,7 +3575,7 @@ const FlowRow = memo(function FlowRow(props) {
3562
3575
  ] })
3563
3576
  })]
3564
3577
  });
3565
- }, (a, b) => a.row === b.row && a.view === b.view && a.top === b.top && a.collapsed === b.collapsed && a.searching === b.searching && a.slowest === b.slowest && a.costliest === b.costliest && a.critical === b.critical && a.active === b.active && a.focusable === b.focusable && a.selected === b.selected && a.sticky === b.sticky && a.motion === b.motion && a.renderIcon === b.renderIcon && a.fillByAgent === b.fillByAgent && a.sharedDetail === b.sharedDetail && a.expanded === b.expanded && (a.clockMs === b.clockMs || !open(a.row, a.clockMs) && !open(b.row, b.clockMs)));
3578
+ }, (a, b) => a.row === b.row && a.view === b.view && a.top === b.top && a.collapsed === b.collapsed && a.searching === b.searching && a.slowest === b.slowest && a.costliest === b.costliest && a.critical === b.critical && a.matched === b.matched && a.active === b.active && a.focusable === b.focusable && a.selected === b.selected && a.sticky === b.sticky && a.motion === b.motion && a.renderIcon === b.renderIcon && a.fillByAgent === b.fillByAgent && a.sharedDetail === b.sharedDetail && a.expanded === b.expanded && (a.clockMs === b.clockMs || !open(a.row, a.clockMs) && !open(b.row, b.clockMs)));
3566
3579
  /** An elbow connector between two rows at one moment, drawn in the row's tone; appears when the clock reaches it. */
3567
3580
  function LinkMark({ link, rowHeight, hot }) {
3568
3581
  const upper = Math.min(link.from, link.to), lower = Math.max(link.from, link.to);
@@ -3774,6 +3787,12 @@ function FlowBody(props) {
3774
3787
  props.byId,
3775
3788
  view
3776
3789
  ]);
3790
+ const matched = useMemo(() => searching ? searchMatchIds(rows, props.query, props.matchSpan) : void 0, [
3791
+ searching,
3792
+ rows,
3793
+ props.query,
3794
+ props.matchSpan
3795
+ ]);
3777
3796
  const index = useMemo(() => new Map(rows.map((row, i) => [row.id, i])), [rows]);
3778
3797
  const motion = useRowMotion(rows);
3779
3798
  const [jumped, setJumped] = useState();
@@ -3847,13 +3866,24 @@ function FlowBody(props) {
3847
3866
  }, []);
3848
3867
  const first = Math.max(0, chunk * CHUNK - OVERSCAN);
3849
3868
  const last = Math.min(rows.length, chunk * CHUNK + Math.ceil(viewport / rowHeight) + CHUNK + OVERSCAN);
3869
+ const scrollToRow = (i, element) => {
3870
+ const offset = (view === "spans" ? Math.min(STICKY_LEVELS, rows[i].depth) : 0) * rowHeight;
3871
+ if (i * rowHeight - offset < element.scrollTop) element.scrollTop = Math.max(0, i * rowHeight - offset);
3872
+ else if ((i + 1) * rowHeight > element.scrollTop + element.clientHeight) element.scrollTop = (i + 1) * rowHeight - element.clientHeight;
3873
+ };
3874
+ const shownSelection = useRef(void 0);
3875
+ useEffect(() => {
3876
+ if (!selectedId || selectedId === shownSelection.current) return;
3877
+ const i = index.get(selectedId);
3878
+ if (i === void 0 || !scroller.current) return;
3879
+ shownSelection.current = selectedId;
3880
+ scrollToRow(i, scroller.current);
3881
+ });
3850
3882
  const focusRow = (id) => {
3851
3883
  const i = index.get(id);
3852
3884
  const element = scroller.current;
3853
3885
  if (i === void 0 || !element) return;
3854
- const offset = (view === "spans" ? Math.min(STICKY_LEVELS, rows[i].depth) : 0) * rowHeight;
3855
- if (i * rowHeight - offset < element.scrollTop) element.scrollTop = Math.max(0, i * rowHeight - offset);
3856
- else if ((i + 1) * rowHeight > element.scrollTop + element.clientHeight) element.scrollTop = (i + 1) * rowHeight - element.clientHeight;
3886
+ scrollToRow(i, element);
3857
3887
  props.onActiveChange(id);
3858
3888
  requestAnimationFrame(() => element.querySelector(`.fw-canvas [data-span-id="${CSS.escape(id)}"]`)?.focus({ preventScroll: true }));
3859
3889
  };
@@ -4021,6 +4051,7 @@ function FlowBody(props) {
4021
4051
  slowest: item.id === slowestId,
4022
4052
  costliest: item.id === props.costliestId,
4023
4053
  critical: props.criticalIds?.has(item.id),
4054
+ matched: matched?.has(item.id),
4024
4055
  active: item.id === activeId,
4025
4056
  focusable: !isSticky && (item.id === activeId || (activeId === void 0 || !index.has(activeId)) && i === 0),
4026
4057
  selected: item.id === selectedId,
@@ -4975,6 +5006,8 @@ function FlowWaterfall({ trace, live, startedAt, label = "Trace waterfall", defa
4975
5006
  maxHeight: rowsHeight,
4976
5007
  collapsed,
4977
5008
  searching: Boolean(query.trim()),
5009
+ query,
5010
+ matchSpan,
4978
5011
  pauses: trace.pauses,
4979
5012
  cursorLine: position !== null || playing,
4980
5013
  sharedDetail,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tangle-network/charts",
3
- "version": "0.21.13",
3
+ "version": "0.21.15",
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",