footprint-explainable-ui 0.25.5 → 0.26.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/README.md CHANGED
@@ -10,6 +10,8 @@
10
10
 
11
11
  Themeable React components for visualizing [footprintjs](https://github.com/footprintjs/footPrint) pipeline execution — time-travel debugging, flowchart overlays, subflow drill-down, and collapsible detail panels.
12
12
 
13
+ > Part of the **[footprintjs ecosystem](https://footprintjs.github.io/)** — the self-explaining stack.
14
+
13
15
  ## Install
14
16
 
15
17
  ```bash
@@ -52,25 +54,16 @@ const snapshots = toVisualizationSnapshots(
52
54
 
53
55
  ```tsx
54
56
  import { ExplainableShell } from "footprint-explainable-ui";
55
- import { TracedFlowchartView } from "footprint-explainable-ui/flowchart";
56
57
 
57
- function DebugView({ snapshots, spec, narrative, narrativeEntries }) {
58
+ function DebugView({ snapshots, narrativeEntries, traceGraph, runtimeOverlay }) {
58
59
  return (
59
60
  <ExplainableShell
60
61
  snapshots={snapshots}
61
- spec={spec}
62
- narrative={narrative}
63
62
  narrativeEntries={narrativeEntries}
63
+ traceGraph={traceGraph}
64
+ runtimeOverlay={runtimeOverlay}
64
65
  title="My Pipeline"
65
66
  panelLabels={{ topology: "What Ran", details: "What Happened", timeline: "How Long" }}
66
- renderFlowchart={({ spec, snapshots, selectedIndex, onNodeClick }) => (
67
- <TracedFlowchartView
68
- spec={spec}
69
- snapshots={snapshots}
70
- snapshotIndex={selectedIndex}
71
- onNodeClick={onNodeClick}
72
- />
73
- )}
74
67
  />
75
68
  );
76
69
  }
@@ -132,7 +125,8 @@ The all-in-one orchestrator. Handles time-travel, subflow drill-down, memory/nar
132
125
  | Prop | Type | Default | Description |
133
126
  |---|---|---|---|
134
127
  | `snapshots` | `StageSnapshot[]` | required | Visualization snapshots |
135
- | `spec` | `SpecNode \| null` | — | Pipeline spec (enables flowchart + subflow tree) |
128
+ | `traceGraph` | `TraceGraph \| null` | — | Build-time graph drives the flowchart + subflow drill-down |
129
+ | `runtimeOverlay` | `RuntimeOverlay` | — | Per-step execution overlay — lights the executed path |
136
130
  | `title` | `string` | `"Flowchart"` | Breadcrumb root label |
137
131
  | `narrative` | `string[]` | — | Flat narrative lines |
138
132
  | `narrativeEntries` | `NarrativeEntry[]` | — | Structured narrative (rich rendering) |
@@ -164,7 +158,7 @@ Control which panels start open. Desktop default: details panel open (flowchart
164
158
 
165
159
  ```tsx
166
160
  // Desktop (default) — memory panel open
167
- <ExplainableShell snapshots={...} spec={...} />
161
+ <ExplainableShell snapshots={...} traceGraph={...} runtimeOverlay={...} />
168
162
 
169
163
  // Mobile — all collapsed, flowchart fills screen
170
164
  <ExplainableShell
@@ -378,6 +372,20 @@ pipeline and snapshot-asserts the outputs in `test/golden/__snapshots__/`.
378
372
  `footprintjs` is a devDependency used ONLY by the generator — the published
379
373
  library still has zero footprintjs dependency (it consumes plain JSON shapes).
380
374
 
375
+ ## The footprintjs ecosystem
376
+
377
+ The self-explaining stack — from backend pipelines to AI agents. → **[overview](https://footprintjs.github.io/)**
378
+
379
+ | Project | Role |
380
+ |---|---|
381
+ | [footprintjs](https://footprintjs.github.io/footPrint/) | the flowchart pattern (core engine) |
382
+ | [agentfootprint](https://footprintjs.github.io/agentfootprint/) | build self-explaining AI agents |
383
+ | **Explainable UI** ← you are here | visualize a footprintjs run |
384
+ | [Lens](https://github.com/footprintjs/agentfootprint-lens) | debug an agentfootprint run |
385
+ | [Thinking UI](https://footprintjs.github.io/agentThinkingUI/) | replay an agent run for non-devs |
386
+
387
+ ---
388
+
381
389
  ## License
382
390
 
383
391
  MIT
package/dist/index.cjs CHANGED
@@ -5994,42 +5994,19 @@ var DetailsContent = (0, import_react31.memo)(function DetailsContent2({
5994
5994
  /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { style: { flex: 1, overflow: "auto" }, children: activeView?.render({ snapshots, selectedIndex }) })
5995
5995
  ] });
5996
5996
  });
5997
- function resolveSubflowLevel(parentSpec, parentSnapshots, subflowNodeName, narrativeEntries) {
5998
- const specNode = findSubflowSpecNode(parentSpec, subflowNodeName);
5999
- if (!specNode?.subflowStructure) return null;
6000
- const parentSnap = parentSnapshots.find(
6001
- (s) => s.stageName === subflowNodeName || s.stageLabel === subflowNodeName
6002
- );
5997
+ function resolveSubflowFromRuntime(parentSnapshots, subflowId, narrativeEntries) {
5998
+ const localId = subflowId.split("/").pop() ?? subflowId;
5999
+ const parentSnap = parentSnapshots.find((s) => {
6000
+ if (!s.subflowResult) return false;
6001
+ const sfStageId = s.runtimeStageId?.split("#")[0]?.split("/").pop();
6002
+ return s.subflowId === subflowId || s.subflowId === localId || s.stageName === subflowId || s.stageLabel === subflowId || sfStageId === subflowId || sfStageId === localId;
6003
+ });
6003
6004
  if (!parentSnap?.subflowResult) return null;
6004
- const sfId = specNode.subflowId ?? subflowNodeName;
6005
- const sfDisplayName = specNode.subflowName ?? specNode.name;
6006
- const sfNarrative = narrativeEntries ? extractSubflowNarrative(narrativeEntries, sfId, sfDisplayName) : void 0;
6005
+ const label = parentSnap.stageLabel ?? parentSnap.stageName ?? localId;
6006
+ const sfNarrative = narrativeEntries ? extractSubflowNarrative(narrativeEntries, subflowId, label) : void 0;
6007
6007
  const sfSnapshots = subflowResultToSnapshots(parentSnap.subflowResult, sfNarrative);
6008
6008
  if (sfSnapshots.length === 0) return null;
6009
- return {
6010
- subflowId: specNode.subflowId ?? subflowNodeName,
6011
- label: specNode.subflowName ?? specNode.name,
6012
- spec: specNode.subflowStructure,
6013
- snapshots: sfSnapshots
6014
- };
6015
- }
6016
- function findSubflowSpecNode(node, name) {
6017
- if ((node.name === name || node.id === name) && node.isSubflowRoot) return node;
6018
- if (node.children) {
6019
- for (const child of node.children) {
6020
- const f = findSubflowSpecNode(child, name);
6021
- if (f) return f;
6022
- }
6023
- }
6024
- if (node.next) return findSubflowSpecNode(node.next, name);
6025
- return null;
6026
- }
6027
- function hasSubflowNodes(node) {
6028
- if (!node) return false;
6029
- if (node.isSubflowRoot) return true;
6030
- if (node.children?.some((c) => c && hasSubflowNodes(c))) return true;
6031
- if (node.next && hasSubflowNodes(node.next)) return true;
6032
- return false;
6009
+ return { subflowId, label, spec: null, snapshots: sfSnapshots, narrative: sfNarrative };
6033
6010
  }
6034
6011
  function buildDataTrace(commitLog, targetRuntimeStageId, maxDepth = 10) {
6035
6012
  const log = commitLog;
@@ -6071,7 +6048,6 @@ var RightPanel = (0, import_react31.memo)(function RightPanel2({
6071
6048
  snapshots,
6072
6049
  selectedIndex,
6073
6050
  runtimeSnapshot,
6074
- spec,
6075
6051
  activeTab,
6076
6052
  allTabs,
6077
6053
  activeNarrativeEntries,
@@ -6117,7 +6093,7 @@ var RightPanel = (0, import_react31.memo)(function RightPanel2({
6117
6093
  id: tab.id,
6118
6094
  name: insightName(tab.name),
6119
6095
  render: () => {
6120
- if (tab.id === "narrative") return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(NarrativePanel, { snapshots, selectedIndex, narrativeEntries: activeNarrativeEntries, runtimeSnapshot, spec, size, style: { height: "100%" } });
6096
+ if (tab.id === "narrative") return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(NarrativePanel, { snapshots, selectedIndex, narrativeEntries: activeNarrativeEntries, runtimeSnapshot, size, style: { height: "100%" } });
6121
6097
  const customView = recorderViews?.find((v2) => v2.id === tab.id);
6122
6098
  if (customView?.render) return customView.render({ snapshots, selectedIndex });
6123
6099
  const autoView = autoRecorderViews.find((v2) => v2.id === tab.id);
@@ -6151,7 +6127,6 @@ function insightName(name) {
6151
6127
  function ExplainableShell({
6152
6128
  snapshots: snapshotsProp,
6153
6129
  runtimeSnapshot,
6154
- spec,
6155
6130
  title,
6156
6131
  resultData: resultDataProp,
6157
6132
  logs = [],
@@ -6167,6 +6142,7 @@ function ExplainableShell({
6167
6142
  showStageId = false,
6168
6143
  traceGraph,
6169
6144
  runtimeOverlay,
6145
+ traceTheme,
6170
6146
  size = "default",
6171
6147
  unstyled = false,
6172
6148
  className,
@@ -6189,16 +6165,27 @@ function ExplainableShell({
6189
6165
  const activeRsid = snapshots2[selectedIndex]?.runtimeStageId;
6190
6166
  let overlayIdx = selectedIndex;
6191
6167
  if (activeRsid && runtimeOverlay) {
6192
- const i = runtimeOverlay.executionOrder.findIndex(
6168
+ let i = runtimeOverlay.executionOrder.findIndex(
6193
6169
  (s) => s.runtimeStageId === activeRsid
6194
6170
  );
6171
+ if (i < 0) {
6172
+ i = runtimeOverlay.executionOrder.findIndex(
6173
+ (s) => s.runtimeStageId?.endsWith("/" + activeRsid)
6174
+ );
6175
+ }
6195
6176
  if (i >= 0) overlayIdx = i;
6196
6177
  }
6178
+ const traceColors = traceTheme && {
6179
+ ...traceTheme.visited !== void 0 && { done: traceTheme.visited },
6180
+ ...traceTheme.current !== void 0 && { active: traceTheme.current },
6181
+ ...traceTheme.mode !== void 0 && { default: traceTheme.mode === "dark" ? "#94a3b8" : "#64748b" }
6182
+ };
6197
6183
  return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(
6198
6184
  TracedFlow,
6199
6185
  {
6200
6186
  graph: traceGraph,
6201
6187
  overlay: runtimeOverlay ?? void 0,
6188
+ colors: traceColors || void 0,
6202
6189
  scrubIndex: overlayIdx,
6203
6190
  onNodeClick: (stageId) => onNodeClick?.(stageId),
6204
6191
  onSubflowChange: (mountId) => {
@@ -6207,7 +6194,7 @@ function ExplainableShell({
6207
6194
  }
6208
6195
  );
6209
6196
  };
6210
- }, [traceGraph, runtimeOverlay]);
6197
+ }, [traceGraph, runtimeOverlay, traceTheme]);
6211
6198
  const effectiveRenderFlowchart = renderFlowchart ?? tracedFlowRenderer;
6212
6199
  const leftLabel = panelLabels?.topology ?? "Topology";
6213
6200
  const rightLabel = panelLabels?.details ?? "Details";
@@ -6287,24 +6274,23 @@ function ExplainableShell({
6287
6274
  const currentLevel = (0, import_react31.useMemo)(() => {
6288
6275
  if (drillDownStack.length > 0) {
6289
6276
  const top = drillDownStack[drillDownStack.length - 1];
6290
- return { spec: top.spec, snapshots: top.snapshots };
6277
+ return { spec: top.spec, snapshots: top.snapshots, narrative: top.narrative };
6291
6278
  }
6292
- return { spec: spec ?? null, snapshots };
6293
- }, [drillDownStack, spec, snapshots]);
6279
+ return { spec: null, snapshots, narrative: void 0 };
6280
+ }, [drillDownStack, snapshots]);
6294
6281
  const activeSnapshots = currentLevel.snapshots;
6295
- const activeSpec = currentLevel.spec;
6296
6282
  const safeIdx = activeSnapshots.length > 0 ? Math.max(0, Math.min(snapshotIdx, activeSnapshots.length - 1)) : 0;
6297
- const activeNarrativeEntries = isInSubflow ? void 0 : narrativeEntries;
6283
+ const activeNarrativeEntries = isInSubflow ? currentLevel.narrative : narrativeEntries;
6298
6284
  const breadcrumbs = (0, import_react31.useMemo)(() => {
6299
- const root = { label: title || "Flowchart", spec, description: spec?.description };
6285
+ const root = { label: title || "Flowchart", spec: null, description: void 0 };
6300
6286
  return [root, ...drillDownStack.map((e) => ({ label: e.label, spec: e.spec, description: void 0 }))];
6301
- }, [spec, title, drillDownStack]);
6287
+ }, [title, drillDownStack]);
6302
6288
  const showTreeSidebar = (0, import_react31.useMemo)(() => {
6303
6289
  if (traceGraph?.nodes?.length) {
6304
6290
  return traceGraph.nodes.some((n) => n.data?.isSubflow === true);
6305
6291
  }
6306
- return !!spec && hasSubflowNodes(spec);
6307
- }, [traceGraph, spec]);
6292
+ return false;
6293
+ }, [traceGraph]);
6308
6294
  const rootOverlay = (0, import_react31.useMemo)(() => {
6309
6295
  if (isInSubflow || !snapshots.length) return { activeStage: void 0, doneStages: void 0 };
6310
6296
  const doneStages = new Set(snapshots.slice(0, safeIdx).map((s) => s.stageLabel));
@@ -6320,14 +6306,13 @@ function ExplainableShell({
6320
6306
  }, []);
6321
6307
  const handleDrillDown = (0, import_react31.useCallback)(
6322
6308
  (nodeName) => {
6323
- if (!activeSpec) return;
6324
- const entry = resolveSubflowLevel(activeSpec, activeSnapshots, nodeName, narrativeEntries);
6309
+ const entry = resolveSubflowFromRuntime(activeSnapshots, nodeName, narrativeEntries);
6325
6310
  if (entry) {
6326
6311
  setDrillDownStack((prev) => [...prev, { ...entry, parentSnapshotIdx: snapshotIdx }]);
6327
6312
  setSnapshotIdx(0);
6328
6313
  }
6329
6314
  },
6330
- [activeSpec, activeSnapshots, narrativeEntries, snapshotIdx]
6315
+ [activeSnapshots, narrativeEntries, snapshotIdx]
6331
6316
  );
6332
6317
  const handleBreadcrumbNavigate = (0, import_react31.useCallback)((level) => {
6333
6318
  setDrillDownStack((prev) => {
@@ -6342,23 +6327,21 @@ function ExplainableShell({
6342
6327
  setSnapshotIdx(indexOrId);
6343
6328
  return;
6344
6329
  }
6345
- if (activeSpec) {
6346
- const sfNode = findSubflowSpecNode(activeSpec, indexOrId);
6347
- if (sfNode?.subflowStructure) {
6348
- handleDrillDown(indexOrId);
6349
- return;
6350
- }
6330
+ const drillable = resolveSubflowFromRuntime(activeSnapshots, indexOrId, narrativeEntries);
6331
+ if (drillable) {
6332
+ handleDrillDown(indexOrId);
6333
+ return;
6351
6334
  }
6352
6335
  const idx = activeSnapshots.findIndex((s) => s.stageLabel === indexOrId);
6353
6336
  if (idx >= 0) setSnapshotIdx(idx);
6354
6337
  },
6355
- [activeSpec, activeSnapshots, handleDrillDown]
6338
+ [activeSnapshots, narrativeEntries, handleDrillDown]
6356
6339
  );
6357
6340
  const handleTreeNodeSelect = (0, import_react31.useCallback)(
6358
6341
  (name, isSubflow) => {
6359
- if (isSubflow && spec) {
6342
+ if (isSubflow) {
6360
6343
  setDrillDownStack([]);
6361
- const entry = resolveSubflowLevel(spec, snapshots, name, narrativeEntries);
6344
+ const entry = resolveSubflowFromRuntime(snapshots, name, narrativeEntries);
6362
6345
  if (entry) {
6363
6346
  setDrillDownStack([{ ...entry, parentSnapshotIdx: snapshotIdx }]);
6364
6347
  setSnapshotIdx(0);
@@ -6369,7 +6352,7 @@ function ExplainableShell({
6369
6352
  if (idx >= 0) setSnapshotIdx(idx);
6370
6353
  }
6371
6354
  },
6372
- [spec, snapshots, narrativeEntries, snapshotIdx]
6355
+ [snapshots, narrativeEntries, snapshotIdx]
6373
6356
  );
6374
6357
  const tabLabels = new Map(allTabs.map((t) => [t.id, t.name]));
6375
6358
  if (unstyled) {
@@ -6380,7 +6363,7 @@ function ExplainableShell({
6380
6363
  (activeTab === "explainable" || activeTab === "ai-compatible") && /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(import_jsx_runtime26.Fragment, { children: [
6381
6364
  /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(TimeTravelControls, { snapshots: activeSnapshots, selectedIndex: safeIdx, onIndexChange: handleSnapshotChange, unstyled: true }),
6382
6365
  isInSubflow && /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(SubflowBreadcrumb, { breadcrumbs, onNavigate: handleBreadcrumbNavigate }),
6383
- activeSpec && effectiveRenderFlowchart?.({ spec: activeSpec, snapshots: activeSnapshots, selectedIndex: safeIdx, onNodeClick: handleNodeClick, showStageId }),
6366
+ traceGraph && effectiveRenderFlowchart?.({ spec: null, snapshots: activeSnapshots, selectedIndex: safeIdx, onNodeClick: handleNodeClick, showStageId }),
6384
6367
  /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(MemoryPanel, { snapshots: activeSnapshots, selectedIndex: safeIdx, unstyled: true }),
6385
6368
  /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(NarrativePanel, { snapshots: activeSnapshots, selectedIndex: safeIdx, narrativeEntries: activeNarrativeEntries, unstyled: true }),
6386
6369
  /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(GanttTimeline, { snapshots: activeSnapshots, selectedIndex: safeIdx, onSelect: handleSnapshotChange, unstyled: true })
@@ -6388,7 +6371,7 @@ function ExplainableShell({
6388
6371
  ] })
6389
6372
  ] });
6390
6373
  }
6391
- const showTopology = !!effectiveRenderFlowchart && !!activeSpec;
6374
+ const showTopology = !!effectiveRenderFlowchart && !!traceGraph;
6392
6375
  const detailsContent = (0, import_react31.useMemo)(() => {
6393
6376
  if (activeTab === "result") {
6394
6377
  return /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(ResultPanel, { data: resultData ?? null, logs, hideConsole, size });
@@ -6485,7 +6468,7 @@ function ExplainableShell({
6485
6468
  /* ── Mobile: stacked vertical ── */
6486
6469
  /* @__PURE__ */ (0, import_jsx_runtime26.jsxs)(import_jsx_runtime26.Fragment, { children: [
6487
6470
  showTopology && /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { style: { height: 350, flexShrink: 0, overflow: "hidden" }, children: effectiveRenderFlowchart({
6488
- spec: activeSpec,
6471
+ spec: null,
6489
6472
  snapshots: activeSnapshots,
6490
6473
  selectedIndex: safeIdx,
6491
6474
  onNodeClick: handleNodeClick,
@@ -6525,7 +6508,7 @@ function ExplainableShell({
6525
6508
  /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(VLinePill, { label: "Topology", expanded: true, side: "left", onClick: () => toggleLeft(false) })
6526
6509
  ] }) : /* @__PURE__ */ (0, import_jsx_runtime26.jsx)(VLinePill, { label: "Topology", expanded: false, side: "left", onClick: () => toggleLeft(true) })),
6527
6510
  showTopology ? /* @__PURE__ */ (0, import_jsx_runtime26.jsx)("div", { style: { flex: 1, overflow: "hidden", minWidth: 0 }, children: effectiveRenderFlowchart({
6528
- spec: activeSpec,
6511
+ spec: null,
6529
6512
  snapshots: activeSnapshots,
6530
6513
  selectedIndex: safeIdx,
6531
6514
  onNodeClick: handleNodeClick,
@@ -6540,7 +6523,6 @@ function ExplainableShell({
6540
6523
  snapshots: activeSnapshots,
6541
6524
  selectedIndex: safeIdx,
6542
6525
  runtimeSnapshot,
6543
- spec,
6544
6526
  activeTab,
6545
6527
  allTabs,
6546
6528
  activeNarrativeEntries,
@@ -6657,7 +6639,6 @@ function TraceViewer({
6657
6639
  ExplainableShell,
6658
6640
  {
6659
6641
  snapshots,
6660
- spec: parsed.trace.spec,
6661
6642
  narrativeEntries: parsed.trace.narrativeEntries,
6662
6643
  tabs,
6663
6644
  defaultTab,