@ra3orblade/swarm 0.11.3 → 0.12.1

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/web/viz.js CHANGED
@@ -72,7 +72,7 @@ function heatmap(cells, { fmt = fmtUsd, label = "cost" } = {}) {
72
72
  }
73
73
 
74
74
  /** Tiny line for a table cell. pts: number[]; 88×20. */
75
- function sparkline(pts, color = "var(--acc)") {
75
+ function sparkline(pts, color = "var(--acc-fill)") {
76
76
  if (!pts || pts.length < 2) return `<svg viewBox="0 0 88 20" class="spark"></svg>`;
77
77
  const max = Math.max(1e-9, ...pts);
78
78
  const step = 88 / (pts.length - 1);
@@ -95,7 +95,7 @@ function compositionBar(parts, { fmt = fmtTok } = {}) {
95
95
  /** Horizontal bars for a ranked list (tool mix). rows: [[label, n]] */
96
96
  // rows: [label, value] or [label, value, displayValue] — a caller with large magnitudes passes its
97
97
  // own formatted string rather than printing 8518611 at the reader.
98
- function hbars(rows, color = "var(--acc)") {
98
+ function hbars(rows, color = "var(--acc-fill)") {
99
99
  const max = Math.max(1, ...rows.map((r) => r[1]));
100
100
  return `<div class="hbars">${rows
101
101
  .map(([k, v, d]) => `<div class="hb" title="${attr(k)}${d === undefined ? "" : ` — ${attr(d)}`}"><span class="k">${attr(k)}</span><span class="t"><i style="width:${(100 * v) / max}%;background:${color}"></i></span><span class="n">${attr(d ?? v)}</span></div>`)
@@ -123,7 +123,7 @@ function turnStripRender(turns, height) {
123
123
  const h = ((t.costUsd ?? 0) / max) * (H - 8);
124
124
  const x = i * slot + (slot - bw) / 2;
125
125
  const tip = `<b>turn ${i + 1}</b> · ${hm(t.ts)}<br>${fmtUsd(t.costUsd)} · ${fmtTok(t.output)} out${t.thinking ? ` · ${fmtTok(t.thinking)} thinking` : ""}<br><span>${attr(t.model ?? "")}</span>`;
126
- return `<g data-tip="${attr(tip)}"><rect x="${i * slot}" y="0" width="${slot}" height="${H}" fill="transparent"/><path d="${roundTop(x, H - 2 - h, bw, h, 2)}" fill="var(--acc)"/></g>`;
126
+ return `<g data-tip="${attr(tip)}"><rect x="${i * slot}" y="0" width="${slot}" height="${H}" fill="transparent"/><path d="${roundTop(x, H - 2 - h, bw, h, 2)}" fill="var(--acc-fill)"/></g>`;
127
127
  })
128
128
  .join("");
129
129
  return `<svg viewBox="0 0 ${W} ${H}" preserveAspectRatio="none" class="chart" style="height:${H}px"><line x1="0" x2="${W}" y1="${H - 2}" y2="${H - 2}" class="base"/>${bars}</svg>`;
@@ -182,7 +182,7 @@ function timeline(sessions, { from, to, projName, now = Date.now(), detail = nul
182
182
  * Single-series line with area fill (cumulative spend). days: string[]; values: number[] aligned.
183
183
  * Hover column per point, tooltip shows the value and its delta from the previous point.
184
184
  */
185
- function line(days, values, { height = 150, fmt = fmtUsd, color = "var(--acc)", label = (d) => d.slice(5) } = {}) {
185
+ function line(days, values, { height = 150, fmt = fmtUsd, color = "var(--acc-fill)", label = (d) => d.slice(5) } = {}) {
186
186
  const n = days.length;
187
187
  if (!n) return "";
188
188
  const max = Math.max(1e-9, ...values);
@@ -335,6 +335,43 @@ function bipartite(sessions, files, { rowH = 30, maxFiles = 40 } = {}) {
335
335
  }
336
336
 
337
337
 
338
+ // M9.15: a transition digraph as an adjacency matrix — rows are what ran, columns what ran next.
339
+ // A layered graph drawing is the wrong tool at this density: the graph is cyclic and near-complete,
340
+ // so almost every edge becomes a back-edge and the picture is a hairball. A matrix has no crossings,
341
+ // puts self-loops on the diagonal where they read at a glance, and shows asymmetry (A→B far heavier
342
+ // than B→A) as a visibly lopsided pair of cells.
343
+ function matrix(tools, edges, { label = (t) => t, cell = 26, maxWeight = 0 } = {}) {
344
+ if (!tools.length) return "";
345
+ const at = new Map(edges.map((e) => [`${e.from} ${e.to}`, e]));
346
+ const top = maxWeight || edges.reduce((m, e) => Math.max(m, e.weight), 0);
347
+ const rowLabel = 150, colLabel = 96;
348
+ const W = rowLabel + tools.length * cell + 8;
349
+ const H = colLabel + tools.length * cell + 8;
350
+ // Column headers are rotated rather than truncated: tool names are long and a column is 26px.
351
+ const cols = tools.map((t, i) => {
352
+ const x = rowLabel + i * cell + cell / 2;
353
+ return `<text transform="translate(${x} ${colLabel - 6}) rotate(-55)" font-size="10.5" fill="var(--dim)" text-anchor="start">${attr(label(t))}</text>`;
354
+ }).join("");
355
+ const rows = tools.map((from, r) => {
356
+ const y = colLabel + r * cell;
357
+ const name = `<text x="${rowLabel - 8}" y="${y + cell / 2 + 3.5}" font-size="11" fill="var(--fg-2)" text-anchor="end">${attr(label(from))}</text>`;
358
+ const cells = tools.map((to, c) => {
359
+ const e = at.get(`${from} ${to}`);
360
+ const x = rowLabel + c * cell;
361
+ const diag = from === to;
362
+ if (!e) return `<rect x="${x}" y="${y}" width="${cell - 2}" height="${cell - 2}" rx="2" fill="var(--panel-2)" opacity="${diag ? 0.9 : 0.45}"/>`;
363
+ // Log scale: one hot pair (Bash→Bash, thousands) would flatten everything else on a linear one.
364
+ const t = Math.min(1, Math.log1p(e.weight) / Math.log1p(top || 1));
365
+ const step = t > 0.8 ? 5 : t > 0.6 ? 4 : t > 0.4 ? 3 : t > 0.2 ? 2 : 1;
366
+ const tip = `${attr(label(from))} → ${attr(label(to))} · ${e.weight.toLocaleString()}× · ${e.sessions} session${e.sessions === 1 ? "" : "s"}`;
367
+ return `<rect x="${x}" y="${y}" width="${cell - 2}" height="${cell - 2}" rx="2" fill="var(--acc-${step})"
368
+ ${diag ? 'stroke="var(--acc-fill)" stroke-width="1"' : ""} data-tip="${tip}"/>`;
369
+ }).join("");
370
+ return name + cells;
371
+ }).join("");
372
+ return `<div style="overflow:auto"><svg viewBox="0 0 ${W} ${H}" width="${W}" height="${H}" role="img" aria-label="tool transition matrix">${cols}${rows}</svg></div>`;
373
+ }
374
+
338
375
  // M9.11/M9.13: render a graph the layout engine has already positioned. All the maths (layering,
339
376
  // crossing reduction, cycle breaking) lives in core/dag.ts and is unit-tested; this only draws.
340
377
  // Edge colour carries the *relationship*, node colour the agent, so the two never compete.
@@ -406,4 +443,4 @@ function dag(graph, { nodeW = 190, rowH = 34, pad = 20 } = {}) {
406
443
  return `<svg viewBox="0 0 ${W} ${H}" width="${W}" height="${H}" role="img" aria-label="Session lineage graph">${edges.join("")}${nodes.join("")}</svg>`;
407
444
  }
408
445
 
409
- window.viz = { stackedColumns, line, calendar, streaks, localDay, heatmap, sparkline, compositionBar, hbars, turnStrip, timeline, legend, agentColor, agentName, AGENT_ORDER, agentSort, bipartite, dag, EDGE_STYLE };
446
+ window.viz = { matrix, stackedColumns, line, calendar, streaks, localDay, heatmap, sparkline, compositionBar, hbars, turnStrip, timeline, legend, agentColor, agentName, AGENT_ORDER, agentSort, bipartite, dag, EDGE_STYLE };