footprint-explainable-ui 0.18.0 → 0.19.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.
Files changed (90) hide show
  1. package/dist/adapters/fromRuntimeSnapshot.d.ts +33 -4
  2. package/dist/adapters/fromRuntimeSnapshot.d.ts.map +1 -1
  3. package/dist/adapters/fromRuntimeSnapshot.js +162 -33
  4. package/dist/adapters/fromRuntimeSnapshot.js.map +1 -1
  5. package/dist/components/ExplainableShell/ExplainableShell.d.ts +157 -14
  6. package/dist/components/ExplainableShell/ExplainableShell.d.ts.map +1 -1
  7. package/dist/components/ExplainableShell/ExplainableShell.js +676 -68
  8. package/dist/components/ExplainableShell/ExplainableShell.js.map +1 -1
  9. package/dist/components/ExplainableShell/index.d.ts +1 -1
  10. package/dist/components/ExplainableShell/index.d.ts.map +1 -1
  11. package/dist/components/FlowchartView/SubflowTree.d.ts +7 -14
  12. package/dist/components/FlowchartView/SubflowTree.d.ts.map +1 -1
  13. package/dist/components/FlowchartView/SubflowTree.js +56 -46
  14. package/dist/components/FlowchartView/SubflowTree.js.map +1 -1
  15. package/dist/components/FlowchartView/index.d.ts +32 -4
  16. package/dist/components/FlowchartView/index.d.ts.map +1 -1
  17. package/dist/components/FlowchartView/index.js +22 -2
  18. package/dist/components/FlowchartView/index.js.map +1 -1
  19. package/dist/components/FlowchartView/useSubflowNavigation.d.ts +41 -16
  20. package/dist/components/FlowchartView/useSubflowNavigation.d.ts.map +1 -1
  21. package/dist/components/FlowchartView/useSubflowNavigation.js +69 -50
  22. package/dist/components/FlowchartView/useSubflowNavigation.js.map +1 -1
  23. package/dist/components/GanttTimeline/GanttTimeline.d.ts.map +1 -1
  24. package/dist/components/GanttTimeline/GanttTimeline.js +5 -5
  25. package/dist/components/GanttTimeline/GanttTimeline.js.map +1 -1
  26. package/dist/components/MemoryInspector/MemoryInspector.d.ts.map +1 -1
  27. package/dist/components/MemoryInspector/MemoryInspector.js +36 -13
  28. package/dist/components/MemoryInspector/MemoryInspector.js.map +1 -1
  29. package/dist/components/NarrativeTrace/NarrativeTrace.d.ts.map +1 -1
  30. package/dist/components/NarrativeTrace/NarrativeTrace.js +24 -17
  31. package/dist/components/NarrativeTrace/NarrativeTrace.js.map +1 -1
  32. package/dist/components/ScopeDiff/ScopeDiff.js +3 -3
  33. package/dist/components/ScopeDiff/ScopeDiff.js.map +1 -1
  34. package/dist/components/StageNode/StageNode.d.ts +21 -0
  35. package/dist/components/StageNode/StageNode.d.ts.map +1 -1
  36. package/dist/components/StageNode/StageNode.js +189 -9
  37. package/dist/components/StageNode/StageNode.js.map +1 -1
  38. package/dist/components/TimeTravelControls/TimeTravelControls.d.ts.map +1 -1
  39. package/dist/components/TimeTravelControls/TimeTravelControls.js +19 -3
  40. package/dist/components/TimeTravelControls/TimeTravelControls.js.map +1 -1
  41. package/dist/components/TimeTravelDebugger/TimeTravelDebugger.d.ts +19 -8
  42. package/dist/components/TimeTravelDebugger/TimeTravelDebugger.d.ts.map +1 -1
  43. package/dist/components/TimeTravelDebugger/TimeTravelDebugger.js +23 -8
  44. package/dist/components/TimeTravelDebugger/TimeTravelDebugger.js.map +1 -1
  45. package/dist/flowchart.cjs +3485 -1340
  46. package/dist/flowchart.cjs.map +1 -1
  47. package/dist/flowchart.d.cts +1616 -177
  48. package/dist/flowchart.d.ts +1616 -177
  49. package/dist/flowchart.d.ts.map +1 -1
  50. package/dist/flowchart.js +3507 -1384
  51. package/dist/flowchart.js.map +1 -1
  52. package/dist/index.cjs +749 -557
  53. package/dist/index.cjs.map +1 -1
  54. package/dist/index.d.cts +276 -29
  55. package/dist/index.d.ts +276 -29
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +742 -547
  58. package/dist/index.js.map +1 -1
  59. package/dist/theme/ThemeProvider.d.ts +14 -0
  60. package/dist/theme/ThemeProvider.d.ts.map +1 -1
  61. package/dist/theme/ThemeProvider.js +15 -1
  62. package/dist/theme/ThemeProvider.js.map +1 -1
  63. package/dist/theme/index.d.ts +4 -2
  64. package/dist/theme/index.d.ts.map +1 -1
  65. package/dist/theme/index.js +3 -2
  66. package/dist/theme/index.js.map +1 -1
  67. package/dist/theme/presets.d.ts +3 -0
  68. package/dist/theme/presets.d.ts.map +1 -1
  69. package/dist/theme/presets.js +22 -0
  70. package/dist/theme/presets.js.map +1 -1
  71. package/dist/theme/tokens.d.ts +22 -1
  72. package/dist/theme/tokens.d.ts.map +1 -1
  73. package/dist/theme/tokens.js +23 -2
  74. package/dist/theme/tokens.js.map +1 -1
  75. package/dist/tsconfig.tsbuildinfo +1 -1
  76. package/dist/types.d.ts +25 -0
  77. package/dist/types.d.ts.map +1 -1
  78. package/package.json +4 -4
  79. package/dist/components/FlowchartView/FlowchartView.d.ts +0 -20
  80. package/dist/components/FlowchartView/FlowchartView.d.ts.map +0 -1
  81. package/dist/components/FlowchartView/FlowchartView.js +0 -80
  82. package/dist/components/FlowchartView/FlowchartView.js.map +0 -1
  83. package/dist/components/FlowchartView/TracedFlowchartView.d.ts +0 -20
  84. package/dist/components/FlowchartView/TracedFlowchartView.d.ts.map +0 -1
  85. package/dist/components/FlowchartView/TracedFlowchartView.js +0 -101
  86. package/dist/components/FlowchartView/TracedFlowchartView.js.map +0 -1
  87. package/dist/components/FlowchartView/specToReactFlow.d.ts +0 -56
  88. package/dist/components/FlowchartView/specToReactFlow.d.ts.map +0 -1
  89. package/dist/components/FlowchartView/specToReactFlow.js +0 -202
  90. package/dist/components/FlowchartView/specToReactFlow.js.map +0 -1
@@ -1,75 +1,660 @@
1
- import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState, useCallback, useMemo } from "react";
3
- import { theme, fontSize, padding } from "../../theme";
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ /**
3
+ * ExplainableShell Pure orchestrator for explainable pipeline visualization.
4
+ *
5
+ * Collapsible sections use the **line + centered pill** pattern:
6
+ * - Collapsed = thin divider line with a pill button sitting on it
7
+ * - Expanded = full content with a pill at the closing edge
8
+ *
9
+ * Sub-components are memo'd to minimize re-renders when scrubbing the
10
+ * time-travel slider. Only components that depend on snapshotIdx re-render.
11
+ *
12
+ * Consumer controls theme via --fp-* CSS custom properties.
13
+ */
14
+ import { memo, useState, useCallback, useMemo, useRef, useEffect } from "react";
15
+ import { theme } from "../../theme";
16
+ import { extractSubflowNarrative } from "../../utils/narrativeSync";
17
+ import { toVisualizationSnapshots, subflowResultToSnapshots } from "../../adapters/fromRuntimeSnapshot";
4
18
  import { ResultPanel } from "../ResultPanel";
5
19
  import { GanttTimeline } from "../GanttTimeline";
6
- import { MemoryInspector } from "../MemoryInspector";
7
- import { NarrativeTrace } from "../NarrativeTrace";
8
- import { ScopeDiff } from "../ScopeDiff";
9
20
  import { TimeTravelControls } from "../TimeTravelControls";
21
+ import { MemoryPanel } from "../MemoryPanel";
22
+ import { NarrativePanel } from "../NarrativePanel";
23
+ import { SubflowTree } from "../FlowchartView/SubflowTree";
24
+ import { SubflowBreadcrumb } from "../FlowchartView/SubflowBreadcrumb";
25
+ import { TracedFlow } from "../FlowchartView/TracedFlow";
26
+ import { InspectorPanel } from "../InspectorPanel/InspectorPanel";
27
+ import { InsightPanel } from "../InsightPanel/InsightPanel";
28
+ import { CompactTimeline } from "../CompactTimeline/CompactTimeline";
10
29
  // ---------------------------------------------------------------------------
11
- // Component
30
+ // Line + Pill — collapsed state is just a line with a pill centered on it
12
31
  // ---------------------------------------------------------------------------
13
- export function ExplainableShell({ snapshots, resultData, logs = [], narrative = [], tabs = ["result", "explainable", "ai-compatible"], defaultTab, hideConsole = false, renderFlowchart, size = "default", unstyled = false, className, style, }) {
14
- const [activeTab, setActiveTab] = useState(defaultTab ?? tabs[0]);
32
+ /** Horizontal line with centered pill (for top/bottom edges) */
33
+ const HLinePill = memo(function HLinePill({ label, detail, expanded, onClick, }) {
34
+ return (_jsxs("div", { style: {
35
+ display: "flex",
36
+ alignItems: "center",
37
+ gap: 0,
38
+ padding: "0",
39
+ }, children: [_jsx("div", { style: { flex: 1, height: 1, background: theme.border } }), _jsxs("button", { onClick: onClick, style: {
40
+ display: "flex",
41
+ alignItems: "center",
42
+ gap: 5,
43
+ padding: "3px 12px",
44
+ margin: "4px 0",
45
+ fontSize: 10,
46
+ fontWeight: 600,
47
+ fontFamily: "inherit",
48
+ color: theme.textMuted,
49
+ background: theme.bgSecondary,
50
+ border: `1px solid ${theme.border}`,
51
+ borderRadius: 10,
52
+ cursor: "pointer",
53
+ whiteSpace: "nowrap",
54
+ letterSpacing: "0.04em",
55
+ textTransform: "uppercase",
56
+ transition: "color 0.15s ease",
57
+ }, children: [_jsx("span", { style: { fontSize: 7 }, children: expanded ? "▼" : "▶" }), label, detail && _jsx("span", { style: { fontWeight: 400, opacity: 0.5, fontSize: 9 }, children: detail })] }), _jsx("div", { style: { flex: 1, height: 1, background: theme.border } })] }));
58
+ });
59
+ /** Vertical line with centered pill (for left/right edges).
60
+ * `side` controls arrow direction:
61
+ * - "right": expanded=▶ collapsed=◀ (panel is on right, collapses right)
62
+ * - "left": expanded=◀ collapsed=▶ (panel is on left, collapses left)
63
+ */
64
+ const VLinePill = memo(function VLinePill({ label, expanded, side = "right", onClick, }) {
65
+ const arrow = side === "right"
66
+ ? (expanded ? "▶" : "◀")
67
+ : (expanded ? "◀" : "▶");
68
+ return (_jsxs("div", { style: {
69
+ display: "flex",
70
+ flexDirection: "column",
71
+ alignItems: "center",
72
+ gap: 0,
73
+ padding: "0",
74
+ }, children: [_jsx("div", { style: { flex: 1, width: 1, background: theme.border } }), _jsxs("button", { onClick: onClick, style: {
75
+ display: "flex",
76
+ alignItems: "center",
77
+ gap: 4,
78
+ padding: "10px 4px",
79
+ margin: "0 3px",
80
+ fontSize: 10,
81
+ fontWeight: 600,
82
+ fontFamily: "inherit",
83
+ color: theme.textMuted,
84
+ background: theme.bgSecondary,
85
+ border: `1px solid ${theme.border}`,
86
+ borderRadius: 10,
87
+ cursor: "pointer",
88
+ whiteSpace: "nowrap",
89
+ letterSpacing: "0.04em",
90
+ textTransform: "uppercase",
91
+ writingMode: "vertical-lr",
92
+ transition: "color 0.15s ease",
93
+ }, children: [_jsx("span", { style: { fontSize: 7, writingMode: "horizontal-tb" }, children: arrow }), label] }), _jsx("div", { style: { flex: 1, width: 1, background: theme.border } })] }));
94
+ });
95
+ // ---------------------------------------------------------------------------
96
+ // KeyedRecorderView — Time-travel aware renderer for auto-detected recorders
97
+ // ---------------------------------------------------------------------------
98
+ /**
99
+ * Detects if data has a keyed-recorder shape: an object property whose values
100
+ * are objects with at least one numeric field. Returns { steps, keyType }.
101
+ * keyType: 'runtimeStageId' (keys contain '#') or 'stageName' (plain names).
102
+ */
103
+ function detectKeyedSteps(data) {
104
+ if (!data || typeof data !== "object")
105
+ return null;
106
+ const obj = data;
107
+ for (const val of Object.values(obj)) {
108
+ if (val && typeof val === "object" && !Array.isArray(val)) {
109
+ const entries = Object.entries(val);
110
+ if (entries.length === 0)
111
+ continue;
112
+ // Check: values must be objects with at least one numeric field
113
+ const allObjectsWithNumbers = entries.every(([, v]) => {
114
+ if (!v || typeof v !== "object" || Array.isArray(v))
115
+ return false;
116
+ return Object.values(v).some((f) => typeof f === "number");
117
+ });
118
+ if (allObjectsWithNumbers) {
119
+ const keyType = entries.some(([k]) => k.includes("#")) ? "runtimeStageId" : "stageName";
120
+ return { steps: val, keyType };
121
+ }
122
+ }
123
+ }
124
+ return null;
125
+ }
126
+ /** Extract render hints from recorder data: numericField name + grandTotal. */
127
+ function extractRenderHints(data) {
128
+ if (!data || typeof data !== "object")
129
+ return null;
130
+ const obj = data;
131
+ if (typeof obj.numericField === "string" && typeof obj.grandTotal === "number") {
132
+ return { numericField: obj.numericField, grandTotal: obj.grandTotal };
133
+ }
134
+ return null;
135
+ }
136
+ function KeyedRecorderView({ data, description, preferredOperation = "accumulate", snapshots, selectedIndex, }) {
137
+ const [showAggregate, setShowAggregate] = useState(false);
138
+ const detected = useMemo(() => detectKeyedSteps(data), [data]);
139
+ // Visible keys up to slider position — match by runtimeStageId or stageName
140
+ const visibleKeys = useMemo(() => {
141
+ const keys = new Set();
142
+ for (let i = 0; i <= selectedIndex && i < snapshots.length; i++) {
143
+ const snap = snapshots[i];
144
+ if (detected?.keyType === "runtimeStageId") {
145
+ if (snap.runtimeStageId)
146
+ keys.add(snap.runtimeStageId);
147
+ }
148
+ else {
149
+ // Match by stageName or stageLabel
150
+ if (snap.stageName)
151
+ keys.add(snap.stageName);
152
+ if (snap.stageLabel)
153
+ keys.add(snap.stageLabel);
154
+ }
155
+ }
156
+ return keys;
157
+ }, [snapshots, selectedIndex, detected?.keyType]);
158
+ const isAtEnd = selectedIndex >= snapshots.length - 1;
159
+ if (!detected) {
160
+ // Fallback: raw JSON for non-keyed data
161
+ return (_jsx("div", { style: { padding: 12, fontFamily: theme.fontMono, fontSize: 11, whiteSpace: "pre-wrap", overflow: "auto", height: "100%" }, children: typeof data === "string" ? data : JSON.stringify(data, null, 2) }));
162
+ }
163
+ const steps = detected.steps;
164
+ const hints = extractRenderHints(data);
165
+ const numFieldKey = hints?.numericField ?? "";
166
+ // Progressive entries (accumulate)
167
+ const allKeys = Object.keys(steps);
168
+ const visibleEntries = allKeys.filter((k) => visibleKeys.has(k));
169
+ // Running total — computed from visible entries using the declared numeric field
170
+ let runningTotal = 0;
171
+ if (numFieldKey) {
172
+ for (const k of visibleEntries) {
173
+ runningTotal += steps[k][numFieldKey] ?? 0;
174
+ }
175
+ }
176
+ // Grand total — provided by the recorder, not recomputed
177
+ const grandTotal = hints?.grandTotal ?? 0;
178
+ return (_jsxs("div", { style: { overflow: "auto", height: "100%", display: "flex", flexDirection: "column" }, children: [description && (_jsx("div", { style: { padding: "6px 12px", fontSize: 11, color: theme.textMuted, fontStyle: "italic", borderBottom: `1px solid ${theme.border}`, flexShrink: 0 }, children: description })), _jsxs("div", { style: { padding: 12, flex: 1, overflow: "auto" }, children: [preferredOperation === "aggregate" ? (
179
+ /* AGGREGATE: collect silently during scrub, button at end to reveal total */
180
+ _jsxs(_Fragment, { children: [isAtEnd ? (_jsx("div", { style: { marginBottom: 16 }, children: !showAggregate ? (_jsx("button", { onClick: () => setShowAggregate(true), style: {
181
+ background: theme.primary, color: "#fff", border: "none", borderRadius: 8,
182
+ padding: "12px 20px", fontSize: 13, fontWeight: 600, cursor: "pointer",
183
+ fontFamily: "inherit", width: "100%",
184
+ }, children: "Aggregate \u2014 Show Grand Total" })) : (_jsxs("div", { style: { padding: "14px 16px", background: `color-mix(in srgb, ${theme.success} 12%, transparent)`, borderRadius: 8, border: `1px solid ${theme.success}44` }, children: [_jsx("div", { style: { fontSize: 10, color: theme.textMuted, textTransform: "uppercase", letterSpacing: "0.08em", marginBottom: 6, fontWeight: 600 }, children: "Aggregate \u2014 grand total" }), numFieldKey && (_jsxs("div", { style: { fontSize: 26, fontWeight: 700, color: theme.success }, children: [grandTotal < 1 ? grandTotal.toFixed(3) : grandTotal.toFixed(1), _jsxs("span", { style: { fontSize: 11, color: theme.textMuted, fontWeight: 400, marginLeft: 8 }, children: [numFieldKey, " \u00B7 ", allKeys.length, " steps"] })] }))] })) })) : (_jsxs("div", { style: { padding: "10px 14px", background: `color-mix(in srgb, ${theme.textMuted} 6%, transparent)`, borderRadius: 6, marginBottom: 16, border: `1px dashed ${theme.border}` }, children: [_jsx("div", { style: { fontSize: 10, color: theme.textMuted, textTransform: "uppercase", letterSpacing: "0.08em", fontWeight: 600 }, children: "Collecting data..." }), _jsxs("div", { style: { fontSize: 11, color: theme.textMuted, marginTop: 4 }, children: [visibleEntries.length, " of ", allKeys.length, " steps collected. Scrub to end to aggregate."] })] })), _jsx("div", { style: { fontSize: 10, color: theme.textMuted, textTransform: "uppercase", letterSpacing: "0.08em", marginBottom: 6, fontWeight: 600 }, children: "Per-step detail" })] })) : preferredOperation === "accumulate" ? (
185
+ /* ACCUMULATE: running total grows with slider — IS the total at end, no button */
186
+ _jsxs(_Fragment, { children: [numFieldKey && visibleEntries.length > 0 && (_jsxs("div", { style: { padding: "10px 14px", background: `color-mix(in srgb, ${theme.primary} 8%, transparent)`, borderRadius: 6, marginBottom: 16 }, children: [_jsx("div", { style: { fontSize: 10, color: theme.textMuted, textTransform: "uppercase", letterSpacing: "0.08em", marginBottom: 4, fontWeight: 600 }, children: "Accumulate \u2014 running total up to this step" }), _jsx("span", { style: { fontWeight: 700, fontSize: 18, color: theme.primary }, children: runningTotal < 1 ? runningTotal.toFixed(3) : runningTotal.toFixed(1) }), _jsxs("span", { style: { color: theme.textMuted, marginLeft: 8, fontSize: 10 }, children: [numFieldKey, " \u00B7 ", visibleEntries.length, " of ", allKeys.length, " steps"] })] })), _jsx("div", { style: { fontSize: 10, color: theme.textMuted, textTransform: "uppercase", letterSpacing: "0.08em", marginBottom: 6, fontWeight: 600 }, children: "Per-step detail" })] })) : (
187
+ /* TRANSLATE: per-step entries prominent, no totals */
188
+ _jsx("div", { style: { fontSize: 10, color: theme.textMuted, textTransform: "uppercase", letterSpacing: "0.08em", marginBottom: 6, fontWeight: 600 }, children: "Translate \u2014 per-step detail" })), visibleEntries.map((key) => {
189
+ const entry = steps[key];
190
+ const label = entry.stageName ?? key;
191
+ const numVal = numFieldKey ? entry[numFieldKey] : undefined;
192
+ return (_jsxs("div", { style: { display: "flex", alignItems: "center", padding: "4px 0", fontSize: 12, fontFamily: theme.fontMono, borderBottom: `1px solid ${theme.border}22` }, children: [_jsx("span", { style: { color: theme.textMuted, width: 140, flexShrink: 0, fontSize: 10 }, children: key }), _jsx("span", { style: { fontWeight: 600, flex: 1 }, children: label }), numVal !== undefined && (_jsx("span", { style: { color: theme.primary, fontWeight: 700, marginLeft: 8 }, children: numVal < 1 ? numVal.toFixed(3) : numVal.toFixed(1) }))] }, key));
193
+ }), visibleEntries.length === 0 && (_jsx("div", { style: { color: theme.textMuted, fontSize: 11, fontStyle: "italic", padding: "8px 0" }, children: "Scrub the slider to reveal entries..." }))] })] }));
194
+ }
195
+ // ---------------------------------------------------------------------------
196
+ // DetailsContent — Recorder-driven tab switcher (Memory + Narrative are defaults)
197
+ // ---------------------------------------------------------------------------
198
+ const DetailsContent = memo(function DetailsContent({ snapshots, selectedIndex, narrativeEntries, size, fillHeight, extraViews, }) {
199
+ // Built-in views (always available)
200
+ const builtInViews = [
201
+ {
202
+ id: "memory",
203
+ name: "Memory",
204
+ render: ({ snapshots: snaps, selectedIndex: idx }) => (_jsx(MemoryPanel, { snapshots: snaps, selectedIndex: idx, size: size, style: fillHeight ? { height: "100%" } : undefined })),
205
+ },
206
+ {
207
+ id: "narrative",
208
+ name: "Narrative",
209
+ render: ({ snapshots: snaps, selectedIndex: idx }) => (_jsx(NarrativePanel, { snapshots: snaps, selectedIndex: idx, narrativeEntries: narrativeEntries, size: size, style: fillHeight ? { height: "100%" } : undefined })),
210
+ },
211
+ ];
212
+ const allViews = [...builtInViews, ...(extraViews ?? [])];
213
+ const [activeViewId, setActiveViewId] = useState(allViews[0]?.id ?? "memory");
214
+ // Reset tab when available views change (e.g., recorder toggled on/off)
215
+ const viewIds = allViews.map((v) => v.id).join(",");
216
+ useEffect(() => {
217
+ if (!allViews.find((v) => v.id === activeViewId)) {
218
+ setActiveViewId(allViews[0]?.id ?? "memory");
219
+ }
220
+ }, [viewIds]); // eslint-disable-line react-hooks/exhaustive-deps
221
+ const activeView = allViews.find((v) => v.id === activeViewId) ?? allViews[0];
222
+ return (_jsxs("div", { style: { flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" }, children: [_jsx("div", { style: { display: "flex", borderBottom: `1px solid ${theme.border}`, flexShrink: 0, overflowX: "auto" }, children: allViews.map((view) => {
223
+ const active = view.id === activeViewId;
224
+ return (_jsx("button", { onClick: () => setActiveViewId(view.id), style: {
225
+ flex: allViews.length <= 3 ? 1 : undefined,
226
+ padding: "6px 8px", fontSize: 11,
227
+ fontWeight: active ? 600 : 400,
228
+ color: active ? theme.primary : theme.textMuted,
229
+ background: active ? `color-mix(in srgb, ${theme.primary} 8%, transparent)` : "transparent",
230
+ border: "none",
231
+ borderBottom: active ? `2px solid ${theme.primary}` : "2px solid transparent",
232
+ cursor: "pointer", textTransform: "uppercase", letterSpacing: "0.06em", fontFamily: "inherit",
233
+ whiteSpace: "nowrap",
234
+ }, children: view.name }, view.id));
235
+ }) }), _jsx("div", { style: { flex: 1, overflow: "auto" }, children: activeView?.render({ snapshots, selectedIndex }) })] }));
236
+ });
237
+ // ---------------------------------------------------------------------------
238
+ // Subflow resolution helpers
239
+ // ---------------------------------------------------------------------------
240
+ function resolveSubflowLevel(parentSpec, parentSnapshots, subflowNodeName, narrativeEntries) {
241
+ const specNode = findSubflowSpecNode(parentSpec, subflowNodeName);
242
+ if (!specNode?.subflowStructure)
243
+ return null;
244
+ const parentSnap = parentSnapshots.find((s) => s.stageName === subflowNodeName || s.stageLabel === subflowNodeName);
245
+ if (!parentSnap?.subflowResult)
246
+ return null;
247
+ // Extract subflow narrative: prefer subflowId (structured), fall back to display name (text scan)
248
+ const sfId = specNode.subflowId ?? subflowNodeName;
249
+ const sfDisplayName = specNode.subflowName ?? specNode.name;
250
+ const sfNarrative = narrativeEntries
251
+ ? extractSubflowNarrative(narrativeEntries, sfId, sfDisplayName)
252
+ : undefined;
253
+ const sfSnapshots = subflowResultToSnapshots(parentSnap.subflowResult, sfNarrative);
254
+ if (sfSnapshots.length === 0)
255
+ return null;
256
+ return {
257
+ subflowId: specNode.subflowId ?? subflowNodeName,
258
+ label: specNode.subflowName ?? specNode.name,
259
+ spec: specNode.subflowStructure,
260
+ snapshots: sfSnapshots,
261
+ };
262
+ }
263
+ function findSubflowSpecNode(node, name) {
264
+ if ((node.name === name || node.id === name) && node.isSubflowRoot)
265
+ return node;
266
+ if (node.children) {
267
+ for (const child of node.children) {
268
+ const f = findSubflowSpecNode(child, name);
269
+ if (f)
270
+ return f;
271
+ }
272
+ }
273
+ if (node.next)
274
+ return findSubflowSpecNode(node.next, name);
275
+ return null;
276
+ }
277
+ function hasSubflowNodes(node) {
278
+ if (!node)
279
+ return false;
280
+ if (node.isSubflowRoot)
281
+ return true;
282
+ if (node.children?.some((c) => c && hasSubflowNodes(c)))
283
+ return true;
284
+ if (node.next && hasSubflowNodes(node.next))
285
+ return true;
286
+ return false;
287
+ }
288
+ function buildDataTrace(commitLog, targetRuntimeStageId, maxDepth = 10) {
289
+ const log = commitLog;
290
+ if (!log?.length)
291
+ return [];
292
+ const idxMap = new Map();
293
+ for (let i = 0; i < log.length; i++)
294
+ idxMap.set(log[i].runtimeStageId, i);
295
+ const startIdx = idxMap.get(targetRuntimeStageId);
296
+ if (startIdx === undefined)
297
+ return [];
298
+ const startCommit = log[startIdx];
299
+ const frames = [];
300
+ const visited = new Set();
301
+ // BFS backward: for each commit, find what keys existed before it that it might have read
302
+ // Simplified: trace the write chain backward — each commit's written keys link to whoever wrote the keys it implicitly depends on
303
+ let current = startCommit;
304
+ let currentIdx = startIdx;
305
+ let depth = 0;
306
+ while (current && depth <= maxDepth) {
307
+ if (visited.has(current.runtimeStageId))
308
+ break;
309
+ visited.add(current.runtimeStageId);
310
+ frames.push({
311
+ runtimeStageId: current.runtimeStageId,
312
+ stageId: current.stageId,
313
+ stageName: current.stage,
314
+ keysWritten: current.trace.map((t) => t.path),
315
+ linkedBy: depth === 0 ? "" : current.trace[0]?.path ?? "",
316
+ depth,
317
+ });
318
+ // Find the previous commit (the one right before this one)
319
+ if (currentIdx > 0) {
320
+ currentIdx--;
321
+ current = log[currentIdx];
322
+ depth++;
323
+ }
324
+ else {
325
+ break;
326
+ }
327
+ }
328
+ return frames;
329
+ }
330
+ const RightPanel = memo(function RightPanel({ mode, onModeChange, snapshots, selectedIndex, runtimeSnapshot, spec, activeTab, allTabs, activeNarrativeEntries, recorderViews, autoRecorderViews, size, onNavigateToStage, }) {
331
+ return (_jsxs(_Fragment, { children: [_jsx("div", { style: {
332
+ display: "flex",
333
+ borderBottom: `1px solid ${theme.border}`,
334
+ flexShrink: 0,
335
+ background: theme.bgSecondary,
336
+ }, children: ["insights", "what"].map((m) => (_jsx("button", { onClick: () => onModeChange(m), style: {
337
+ flex: 1,
338
+ padding: "7px 12px",
339
+ fontSize: 11,
340
+ fontWeight: mode === m ? 700 : 500,
341
+ textTransform: "uppercase",
342
+ letterSpacing: "0.06em",
343
+ color: mode === m ? theme.primary : theme.textMuted,
344
+ background: "transparent",
345
+ border: "none",
346
+ borderBottom: mode === m ? `2px solid ${theme.primary}` : "2px solid transparent",
347
+ cursor: "pointer",
348
+ fontFamily: "inherit",
349
+ }, children: m === "insights" ? "Insights" : "Inspector" }, m))) }), _jsx("div", { style: { flex: 1, overflow: "hidden" }, children: mode === "insights" ? (_jsx(InsightPanel, { mode: "tabs", expandedId: activeTab, insights: allTabs.filter((t) => t.id !== "result" && t.id !== "memory").map((tab) => ({
350
+ id: tab.id,
351
+ name: insightName(tab.name),
352
+ render: () => {
353
+ if (tab.id === "narrative")
354
+ return _jsx(NarrativePanel, { snapshots: snapshots, selectedIndex: selectedIndex, narrativeEntries: activeNarrativeEntries, runtimeSnapshot: runtimeSnapshot, spec: spec, size: size, style: { height: "100%" } });
355
+ const customView = recorderViews?.find((v) => v.id === tab.id);
356
+ if (customView?.render)
357
+ return customView.render({ snapshots, selectedIndex });
358
+ const autoView = autoRecorderViews.find((v) => v.id === tab.id);
359
+ if (autoView)
360
+ return _jsx(KeyedRecorderView, { data: autoView.data, description: autoView.description, preferredOperation: autoView.preferredOperation, snapshots: snapshots, selectedIndex: selectedIndex });
361
+ return null;
362
+ },
363
+ })) })) : (_jsx(InspectorPanel, { snapshots: snapshots, selectedIndex: selectedIndex, dataTraceFrames: runtimeSnapshot?.commitLog ? buildDataTrace(runtimeSnapshot.commitLog, snapshots[selectedIndex]?.runtimeStageId ?? '') : [], selectedStageId: snapshots[selectedIndex]?.runtimeStageId, onNavigateToStage: onNavigateToStage })) })] }));
364
+ });
365
+ /** Map internal recorder names to user-facing Insight names. */
366
+ function insightName(name) {
367
+ const map = {
368
+ "Narrative": "Story",
369
+ "Memory": "State",
370
+ "Metrics": "Performance",
371
+ "Quality": "Quality",
372
+ "Cost": "Cost",
373
+ };
374
+ return map[name] ?? name;
375
+ }
376
+ export function ExplainableShell({ snapshots: snapshotsProp, runtimeSnapshot, spec, title, resultData: resultDataProp, logs = [], narrativeEntries, tabs = ["result", "explainable"], defaultTab, hideConsole = false, hideTabs: hideTabsProp, panelLabels, defaultExpanded, recorderViews, renderFlowchart, showStageId = false, traceGraph, runtimeOverlay, size = "default", unstyled = false, className, style, }) {
377
+ // Convert runtimeSnapshot → visualization snapshots (zero-boilerplate mode)
378
+ const derivedFromRuntime = useMemo(() => {
379
+ if (!runtimeSnapshot)
380
+ return null;
381
+ try {
382
+ const snaps = toVisualizationSnapshots(runtimeSnapshot, narrativeEntries);
383
+ return { snapshots: snaps, resultData: runtimeSnapshot.sharedState };
384
+ }
385
+ catch {
386
+ return null;
387
+ }
388
+ }, [runtimeSnapshot, narrativeEntries]);
389
+ // Use derived data when runtimeSnapshot is provided, otherwise use explicit props
390
+ const snapshots = snapshotsProp ?? derivedFromRuntime?.snapshots ?? [];
391
+ const resultData = resultDataProp ?? derivedFromRuntime?.resultData ?? null;
392
+ // Flowchart renderer selection (v6+ — recorder-driven only):
393
+ // - explicit `renderFlowchart` always wins (consumer override)
394
+ // - `traceGraph` → render via `<TracedFlow>` (event-driven graph
395
+ // + optional runtime overlay, no spec-tree post-walk)
396
+ //
397
+ // Consumers MUST pass `traceGraph` for chart visualization. The
398
+ // legacy `spec={...}` → post-walk fallback was removed when the
399
+ // recorder gained convergence-edge expansion (post-fork `next`
400
+ // fires N edges, one per branch child) — the recorder graph is now
401
+ // the single source of truth.
402
+ const tracedFlowRenderer = useMemo(() => {
403
+ if (!traceGraph)
404
+ return undefined;
405
+ return ({ selectedIndex, snapshots, onNodeClick }) => {
406
+ // The shell's `selectedIndex` indexes into `snapshots[]` (which
407
+ // may be filtered to a drill-down subset). The overlay's
408
+ // `executionOrder` is the FULL execution timeline (all stages
409
+ // including subflow internals). When the two arrays have
410
+ // different lengths, passing selectedIndex straight through
411
+ // misaligns the chart's active highlight.
412
+ //
413
+ // Translate: take the runtimeStageId at snapshots[selectedIndex]
414
+ // and find the matching position in overlay.executionOrder.
415
+ // Fall back to selectedIndex when no overlay or no match
416
+ // (charts without subflows have aligned indexes anyway).
417
+ const activeRsid = snapshots[selectedIndex]?.runtimeStageId;
418
+ let overlayIdx = selectedIndex;
419
+ if (activeRsid && runtimeOverlay) {
420
+ const i = runtimeOverlay.executionOrder.findIndex((s) => s.runtimeStageId === activeRsid);
421
+ if (i >= 0)
422
+ overlayIdx = i;
423
+ }
424
+ return (_jsx(TracedFlow, { graph: traceGraph, overlay: runtimeOverlay ?? undefined, scrubIndex: overlayIdx, onNodeClick: (stageId) => onNodeClick?.(stageId), onSubflowChange: (mountId) => {
425
+ // Forward chart's drill state to the shell's drill-down
426
+ // stack so memory/narrative/timeline panels follow the
427
+ // chart into/out of subflows. We route through the same
428
+ // onNodeClick channel — it already triggers drill-down
429
+ // for subflow mount nodes via the shell's handleNodeClick
430
+ // → handleDrillDown path.
431
+ //
432
+ // The `mountId === null` case (popping back to top) is
433
+ // intentionally NOT auto-triggered here: the shell's
434
+ // breadcrumb-back button is the right user gesture for
435
+ // navigating UP from a subflow. Auto-popping on scrub
436
+ // would surprise users who manually drilled in.
437
+ if (mountId !== null)
438
+ onNodeClick?.(mountId);
439
+ } }));
440
+ };
441
+ }, [traceGraph, runtimeOverlay]);
442
+ const effectiveRenderFlowchart = renderFlowchart ?? tracedFlowRenderer;
443
+ const leftLabel = panelLabels?.topology ?? "Topology";
444
+ const rightLabel = panelLabels?.details ?? "Details";
445
+ const bottomLabel = panelLabels?.timeline ?? "Timeline";
446
+ // Responsive: detect narrow container + notify children of size changes
447
+ const shellRef = useRef(null);
448
+ const [isNarrow, setIsNarrow] = useState(false);
449
+ const [isMedium, setIsMedium] = useState(false);
450
+ useEffect(() => {
451
+ const el = shellRef.current;
452
+ if (!el)
453
+ return;
454
+ const ro = new ResizeObserver(([entry]) => {
455
+ const w = entry.contentRect.width;
456
+ setIsNarrow(w < 640);
457
+ setIsMedium(w >= 640 && w < 960);
458
+ // Notify ReactFlow (and other layout-sensitive children) that our container resized
459
+ window.dispatchEvent(new Event("resize"));
460
+ });
461
+ ro.observe(el);
462
+ return () => ro.disconnect();
463
+ }, []);
464
+ // Auto-detect recorder views from runtimeSnapshot.recorders
465
+ const autoRecorderViews = useMemo(() => {
466
+ const recorders = runtimeSnapshot?.recorders;
467
+ if (!recorders?.length)
468
+ return [];
469
+ // Don't auto-generate for IDs that have explicit recorderViews
470
+ const explicitIds = new Set((recorderViews ?? []).map((v) => v.id));
471
+ return recorders
472
+ .filter((r) => !explicitIds.has(r.id))
473
+ .map((r) => ({ id: r.id, name: r.name, description: r.description, preferredOperation: r.preferredOperation, data: r.data }));
474
+ }, [runtimeSnapshot, recorderViews]);
475
+ // Build tab list: Result + Memory (always), Narrative (when data exists),
476
+ // explicit recorder views, auto-detected recorder views
477
+ const hasNarrative = !!narrativeEntries?.length;
478
+ const allTabs = useMemo(() => {
479
+ const tabs = [
480
+ { id: "result", name: "Result", description: "Final output and console logs" },
481
+ { id: "memory", name: "Memory", description: "Accumulator — progressive shared state at each stage" },
482
+ ];
483
+ if (hasNarrative) {
484
+ tabs.push({ id: "narrative", name: "Narrative", description: "Translator (SequenceRecorder) — interleaved flow + data narrative per execution step" });
485
+ }
486
+ for (const v of recorderViews ?? []) {
487
+ tabs.push({ id: v.id, name: v.name, description: v.description });
488
+ }
489
+ for (const v of autoRecorderViews) {
490
+ tabs.push({ id: v.id, name: v.name, description: v.description });
491
+ }
492
+ // Filter hidden tabs
493
+ const hideSet = new Set(hideTabsProp ?? []);
494
+ return hideSet.size > 0 ? tabs.filter((t) => !hideSet.has(t.id)) : tabs;
495
+ }, [hasNarrative, recorderViews, autoRecorderViews, hideTabsProp]);
496
+ const validTabIds = new Set(allTabs.map((t) => t.id));
497
+ const resolvedDefault = defaultTab && validTabIds.has(defaultTab) ? defaultTab : allTabs[0]?.id ?? "result";
498
+ const [activeTab, setActiveTab] = useState(resolvedDefault);
15
499
  const [snapshotIdx, setSnapshotIdx] = useState(0);
16
- const fs = fontSize[size];
17
- const pad = padding[size];
500
+ const [drillDownStack, setDrillDownStack] = useState([]);
501
+ const [rightExpanded, setRightExpanded] = useState(defaultExpanded?.details ?? true);
502
+ const [rightPanelMode, setRightPanelMode] = useState("insights");
503
+ const [leftExpanded, setLeftExpanded] = useState(defaultExpanded?.topology ?? false);
504
+ const [timelineExpanded, setTimelineExpanded] = useState(defaultExpanded?.timeline ?? false);
505
+ // Auto-collapse all panels when switching to narrow (mobile)
506
+ useEffect(() => {
507
+ if (isNarrow) {
508
+ setLeftExpanded(false);
509
+ setRightExpanded(false);
510
+ setTimelineExpanded(false);
511
+ }
512
+ }, [isNarrow]);
513
+ // Notify ReactFlow (and any ResizeObserver-based children) when panels toggle
514
+ const triggerReflow = useCallback(() => {
515
+ // Fire twice: once immediately for fast response, once after CSS transition ends
516
+ requestAnimationFrame(() => window.dispatchEvent(new Event("resize")));
517
+ setTimeout(() => window.dispatchEvent(new Event("resize")), 320);
518
+ }, []);
519
+ const toggleLeft = useCallback((v) => { setLeftExpanded(v); triggerReflow(); }, [triggerReflow]);
520
+ const toggleRight = useCallback((v) => { setRightExpanded(v); triggerReflow(); }, [triggerReflow]);
521
+ const toggleTimeline = useCallback(() => { setTimelineExpanded((p) => !p); triggerReflow(); }, [triggerReflow]);
522
+ const isInSubflow = drillDownStack.length > 0;
523
+ const currentLevel = useMemo(() => {
524
+ if (drillDownStack.length > 0) {
525
+ const top = drillDownStack[drillDownStack.length - 1];
526
+ return { spec: top.spec, snapshots: top.snapshots };
527
+ }
528
+ return { spec: spec ?? null, snapshots };
529
+ }, [drillDownStack, spec, snapshots]);
530
+ const activeSnapshots = currentLevel.snapshots;
531
+ const activeSpec = currentLevel.spec;
532
+ const safeIdx = activeSnapshots.length > 0
533
+ ? Math.max(0, Math.min(snapshotIdx, activeSnapshots.length - 1))
534
+ : 0;
535
+ const activeNarrativeEntries = isInSubflow ? undefined : narrativeEntries;
536
+ const breadcrumbs = useMemo(() => {
537
+ const root = { label: title || "Flowchart", spec: spec, description: spec?.description };
538
+ return [root, ...drillDownStack.map((e) => ({ label: e.label, spec: e.spec, description: undefined }))];
539
+ }, [spec, title, drillDownStack]);
540
+ // Recorder-driven: derive subflow presence from the build-time graph.
541
+ // Falls back to the legacy spec walk only when traceGraph is absent
542
+ // (e.g., a consumer still threading raw spec). When both are absent,
543
+ // the tree sidebar is hidden.
544
+ const showTreeSidebar = useMemo(() => {
545
+ if (traceGraph?.nodes?.length) {
546
+ return traceGraph.nodes.some((n) => n.data?.isSubflow === true);
547
+ }
548
+ return !!spec && hasSubflowNodes(spec);
549
+ }, [traceGraph, spec]);
550
+ const rootOverlay = useMemo(() => {
551
+ if (isInSubflow || !snapshots.length)
552
+ return { activeStage: undefined, doneStages: undefined };
553
+ const doneStages = new Set(snapshots.slice(0, safeIdx).map((s) => s.stageLabel));
554
+ const activeStage = snapshots[safeIdx]?.stageLabel ?? null;
555
+ return { activeStage, doneStages };
556
+ }, [isInSubflow, snapshots, safeIdx]);
557
+ // ── Handlers ──
558
+ const handleTabChange = useCallback((tab) => {
559
+ setActiveTab(tab);
560
+ setDrillDownStack([]);
561
+ }, []);
18
562
  const handleSnapshotChange = useCallback((idx) => {
19
- setSnapshotIdx(Math.max(0, Math.min(idx, snapshots.length - 1)));
20
- }, [snapshots.length]);
21
- // Progressive narrative reveal
22
- const revealedCount = useMemo(() => {
23
- if (snapshots.length === 0 || narrative.length === 0)
24
- return narrative.length;
25
- const boundaries = [];
26
- for (let i = 0; i < narrative.length; i++) {
27
- const trimmed = narrative[i].trimStart();
28
- if ((trimmed.startsWith("Stage ") && !trimmed.match(/^Stage\s+\d+:\s*Step\s/)) ||
29
- trimmed.startsWith("[")) {
30
- boundaries.push(i);
563
+ if (typeof idx === "number")
564
+ setSnapshotIdx(idx);
565
+ }, []);
566
+ const handleDrillDown = useCallback((nodeName) => {
567
+ if (!activeSpec)
568
+ return;
569
+ const entry = resolveSubflowLevel(activeSpec, activeSnapshots, nodeName, narrativeEntries);
570
+ if (entry) {
571
+ setDrillDownStack((prev) => [...prev, { ...entry, parentSnapshotIdx: snapshotIdx }]);
572
+ setSnapshotIdx(0);
573
+ }
574
+ }, [activeSpec, activeSnapshots, narrativeEntries, snapshotIdx]);
575
+ const handleBreadcrumbNavigate = useCallback((level) => {
576
+ setDrillDownStack((prev) => {
577
+ const popped = level === 0 ? prev[0] : prev[level];
578
+ if (popped)
579
+ setSnapshotIdx(popped.parentSnapshotIdx);
580
+ return level === 0 ? [] : prev.slice(0, level);
581
+ });
582
+ }, []);
583
+ const handleNodeClick = useCallback((indexOrId) => {
584
+ if (typeof indexOrId === "number") {
585
+ setSnapshotIdx(indexOrId);
586
+ return;
587
+ }
588
+ if (activeSpec) {
589
+ const sfNode = findSubflowSpecNode(activeSpec, indexOrId);
590
+ if (sfNode?.subflowStructure) {
591
+ handleDrillDown(indexOrId);
592
+ return;
31
593
  }
32
594
  }
33
- if (boundaries.length === 0) {
34
- const ratio = (snapshotIdx + 1) / snapshots.length;
35
- return Math.max(1, Math.ceil(narrative.length * ratio));
36
- }
37
- const groupsToShow = Math.max(1, Math.min(Math.floor(((snapshotIdx + 1) / snapshots.length) * boundaries.length) || 1, boundaries.length));
38
- const endIdx = groupsToShow < boundaries.length ? boundaries[groupsToShow] : narrative.length;
39
- return Math.max(1, endIdx);
40
- }, [snapshots.length, snapshotIdx, narrative]);
41
- // Scope diff data
42
- const prevMemory = snapshotIdx > 0 ? snapshots[snapshotIdx - 1]?.memory : null;
43
- const currMemory = snapshots[snapshotIdx]?.memory ?? {};
44
- const tabLabels = {
45
- result: "Result",
46
- explainable: "Explainable",
47
- "ai-compatible": "AI-Compatible",
48
- };
595
+ const idx = activeSnapshots.findIndex((s) => s.stageLabel === indexOrId);
596
+ if (idx >= 0)
597
+ setSnapshotIdx(idx);
598
+ }, [activeSpec, activeSnapshots, handleDrillDown]);
599
+ const handleTreeNodeSelect = useCallback((name, isSubflow) => {
600
+ if (isSubflow && spec) {
601
+ setDrillDownStack([]);
602
+ const entry = resolveSubflowLevel(spec, snapshots, name, narrativeEntries);
603
+ if (entry) {
604
+ setDrillDownStack([{ ...entry, parentSnapshotIdx: snapshotIdx }]);
605
+ setSnapshotIdx(0);
606
+ }
607
+ }
608
+ else {
609
+ setDrillDownStack([]);
610
+ const idx = snapshots.findIndex((s) => s.stageLabel === name);
611
+ if (idx >= 0)
612
+ setSnapshotIdx(idx);
613
+ }
614
+ }, [spec, snapshots, narrativeEntries, snapshotIdx]);
615
+ // Map tab id → label for rendering
616
+ const tabLabels = new Map(allTabs.map((t) => [t.id, t.name]));
617
+ // ── Unstyled mode ──
49
618
  if (unstyled) {
50
- return (_jsxs("div", { className: className, style: style, "data-fp": "explainable-shell", children: [_jsx("div", { "data-fp": "shell-tabs", children: tabs.map((tab) => (_jsx("button", { "data-fp": "shell-tab", "data-active": tab === activeTab, onClick: () => setActiveTab(tab), children: tabLabels[tab] }, tab))) }), _jsxs("div", { "data-fp": "shell-content", "data-tab": activeTab, children: [activeTab === "result" && (_jsx(ResultPanel, { data: resultData ?? null, logs: logs, hideConsole: hideConsole, unstyled: true })), activeTab === "explainable" && (_jsxs(_Fragment, { children: [_jsx(TimeTravelControls, { snapshots: snapshots, selectedIndex: snapshotIdx, onIndexChange: handleSnapshotChange, unstyled: true }), renderFlowchart?.({ snapshots, selectedIndex: snapshotIdx, onNodeClick: handleSnapshotChange }), _jsx(MemoryInspector, { snapshots: snapshots, selectedIndex: snapshotIdx, unstyled: true }), _jsx(ScopeDiff, { previous: prevMemory, current: currMemory, unstyled: true }), _jsx(GanttTimeline, { snapshots: snapshots, selectedIndex: snapshotIdx, onSelect: handleSnapshotChange, unstyled: true })] })), activeTab === "ai-compatible" && (_jsxs(_Fragment, { children: [_jsx(TimeTravelControls, { snapshots: snapshots, selectedIndex: snapshotIdx, onIndexChange: handleSnapshotChange, unstyled: true }), renderFlowchart?.({ snapshots, selectedIndex: snapshotIdx, onNodeClick: handleSnapshotChange }), _jsx(NarrativeTrace, { narrative: narrative, revealedCount: revealedCount, unstyled: true })] }))] })] }));
619
+ return (_jsxs("div", { className: className, style: style, "data-fp": "explainable-shell", children: [_jsx("div", { "data-fp": "shell-tabs", children: allTabs.map((tab) => (_jsx("button", { "data-fp": "shell-tab", "data-active": tab.id === activeTab, onClick: () => handleTabChange(tab.id), children: tab.name }, tab.id))) }), _jsxs("div", { "data-fp": "shell-content", "data-tab": activeTab, children: [activeTab === "result" && _jsx(ResultPanel, { data: resultData ?? null, logs: logs, hideConsole: hideConsole, unstyled: true }), (activeTab === "explainable" || activeTab === "ai-compatible") && (_jsxs(_Fragment, { children: [_jsx(TimeTravelControls, { snapshots: activeSnapshots, selectedIndex: safeIdx, onIndexChange: handleSnapshotChange, unstyled: true }), isInSubflow && _jsx(SubflowBreadcrumb, { breadcrumbs: breadcrumbs, onNavigate: handleBreadcrumbNavigate }), activeSpec && effectiveRenderFlowchart?.({ spec: activeSpec, snapshots: activeSnapshots, selectedIndex: safeIdx, onNodeClick: handleNodeClick, showStageId }), _jsx(MemoryPanel, { snapshots: activeSnapshots, selectedIndex: safeIdx, unstyled: true }), _jsx(NarrativePanel, { snapshots: activeSnapshots, selectedIndex: safeIdx, narrativeEntries: activeNarrativeEntries, unstyled: true }), _jsx(GanttTimeline, { snapshots: activeSnapshots, selectedIndex: safeIdx, onSelect: handleSnapshotChange, unstyled: true })] }))] })] }));
51
620
  }
52
621
  // ── Styled mode ──
53
- return (_jsxs("div", { className: className, style: {
54
- height: "100%",
55
- display: "flex",
56
- flexDirection: "column",
57
- overflow: "hidden",
58
- background: theme.bgPrimary,
59
- color: theme.textPrimary,
60
- fontFamily: theme.fontSans,
61
- ...style,
62
- }, "data-fp": "explainable-shell", children: [_jsx("div", { style: {
622
+ // Show topology when spec has subflows
623
+ const showTopology = !!effectiveRenderFlowchart && !!activeSpec;
624
+ // Render the active details tab content
625
+ const detailsContent = useMemo(() => {
626
+ if (activeTab === "result") {
627
+ return _jsx(ResultPanel, { data: resultData ?? null, logs: logs, hideConsole: hideConsole, size: size });
628
+ }
629
+ if (activeTab === "memory") {
630
+ return _jsx(MemoryPanel, { snapshots: activeSnapshots, selectedIndex: safeIdx, size: size, style: { height: "100%" } });
631
+ }
632
+ if (activeTab === "narrative") {
633
+ return _jsx(NarrativePanel, { snapshots: activeSnapshots, selectedIndex: safeIdx, narrativeEntries: activeNarrativeEntries, size: size, style: { height: "100%" } });
634
+ }
635
+ const customView = recorderViews?.find((v) => v.id === activeTab);
636
+ if (customView?.render) {
637
+ return customView.render({ snapshots: activeSnapshots, selectedIndex: safeIdx });
638
+ }
639
+ // Auto-detected recorder view — time-travel aware for keyed recorders, JSON fallback
640
+ const autoView = autoRecorderViews.find((v) => v.id === activeTab);
641
+ if (autoView) {
642
+ return (_jsx(KeyedRecorderView, { data: autoView.data, description: autoView.description, preferredOperation: autoView.preferredOperation, snapshots: activeSnapshots, selectedIndex: safeIdx }));
643
+ }
644
+ return null;
645
+ }, [activeTab, resultData, logs, hideConsole, size, activeSnapshots, safeIdx, activeNarrativeEntries, recorderViews, autoRecorderViews]);
646
+ // Details panel with internal tabs
647
+ const detailsPanel = (_jsxs("div", { style: { display: "flex", flexDirection: "column", height: "100%", overflow: "hidden" }, children: [_jsx("div", { style: {
63
648
  display: "flex",
64
- gap: 0,
65
649
  borderBottom: `1px solid ${theme.border}`,
66
650
  background: theme.bgSecondary,
67
651
  flexShrink: 0,
68
- }, children: tabs.map((tab) => {
69
- const active = tab === activeTab;
70
- return (_jsx("button", { onClick: () => setActiveTab(tab), style: {
71
- padding: `${pad - 4}px ${pad}px`,
72
- fontSize: fs.label,
652
+ overflowX: "auto",
653
+ }, children: allTabs.map((tab) => {
654
+ const active = tab.id === activeTab;
655
+ return (_jsx("button", { onClick: () => handleTabChange(tab.id), title: tab.description, style: {
656
+ padding: "6px 14px",
657
+ fontSize: 11,
73
658
  fontWeight: active ? 700 : 500,
74
659
  textTransform: "uppercase",
75
660
  letterSpacing: "0.08em",
@@ -78,17 +663,40 @@ export function ExplainableShell({ snapshots, resultData, logs = [], narrative =
78
663
  border: "none",
79
664
  borderBottom: active ? `2px solid ${theme.primary}` : "2px solid transparent",
80
665
  cursor: "pointer",
81
- transition: "all 0.15s ease",
82
- }, children: tabLabels[tab] }, tab));
83
- }) }), _jsxs("div", { style: { flex: 1, overflow: "hidden", display: "flex", flexDirection: "column" }, children: [activeTab === "result" && (_jsx(ResultPanel, { data: resultData ?? null, logs: logs, hideConsole: hideConsole, size: size })), activeTab === "explainable" && (_jsxs(_Fragment, { children: [_jsx(TimeTravelControls, { snapshots: snapshots, selectedIndex: snapshotIdx, onIndexChange: handleSnapshotChange, size: size }), _jsxs("div", { style: { flex: 1, display: "flex", overflow: "hidden" }, children: [renderFlowchart && (_jsx("div", { style: { flex: 1, overflow: "hidden", borderRight: `1px solid ${theme.border}` }, children: renderFlowchart({ snapshots, selectedIndex: snapshotIdx, onNodeClick: handleSnapshotChange }) })), _jsxs("div", { style: {
84
- width: renderFlowchart ? "40%" : "100%",
85
- minWidth: 280,
86
- overflow: "auto",
87
- display: "flex",
88
- flexDirection: "column",
89
- }, children: [_jsx(MemoryInspector, { snapshots: snapshots, selectedIndex: snapshotIdx, size: size }), _jsx("div", { style: { borderTop: `1px solid ${theme.border}` }, children: _jsx(ScopeDiff, { previous: prevMemory, current: currMemory, hideUnchanged: true, size: size }) })] })] }), _jsx("div", { style: { borderTop: `1px solid ${theme.border}`, flexShrink: 0 }, children: _jsx(GanttTimeline, { snapshots: snapshots, selectedIndex: snapshotIdx, onSelect: handleSnapshotChange, size: size }) })] })), activeTab === "ai-compatible" && (_jsxs(_Fragment, { children: [_jsx(TimeTravelControls, { snapshots: snapshots, selectedIndex: snapshotIdx, onIndexChange: handleSnapshotChange, size: size }), _jsxs("div", { style: { flex: 1, display: "flex", overflow: "hidden" }, children: [renderFlowchart && (_jsx("div", { style: { flex: 1, overflow: "hidden", borderRight: `1px solid ${theme.border}` }, children: renderFlowchart({ snapshots, selectedIndex: snapshotIdx, onNodeClick: handleSnapshotChange }) })), _jsx(NarrativeTrace, { narrative: narrative, revealedCount: revealedCount, size: size, style: {
90
- width: renderFlowchart ? "40%" : "100%",
91
- minWidth: 280,
92
- } })] })] }))] })] }));
666
+ fontFamily: "inherit",
667
+ whiteSpace: "nowrap",
668
+ }, children: tab.name }, tab.id));
669
+ }) }), _jsx("div", { style: { flex: 1, overflow: "auto" }, children: detailsContent })] }));
670
+ return (_jsxs("div", { ref: shellRef, className: className, style: {
671
+ height: "100%",
672
+ display: "flex",
673
+ flexDirection: "column",
674
+ overflow: "hidden",
675
+ background: theme.bgPrimary,
676
+ color: theme.textPrimary,
677
+ fontFamily: theme.fontSans,
678
+ fontSize: 12,
679
+ ...style,
680
+ }, "data-fp": "explainable-shell", children: [_jsx(TimeTravelControls, { snapshots: activeSnapshots, selectedIndex: safeIdx, onIndexChange: handleSnapshotChange, size: size }), isInSubflow && (_jsx(SubflowBreadcrumb, { breadcrumbs: breadcrumbs, onNavigate: handleBreadcrumbNavigate })), _jsx("div", { style: { flex: 1, overflow: isNarrow ? "auto" : "hidden", display: "flex", flexDirection: "column" }, children: isNarrow ? (
681
+ /* ── Mobile: stacked vertical ── */
682
+ _jsxs(_Fragment, { children: [showTopology && (_jsx("div", { style: { height: 350, flexShrink: 0, overflow: "hidden" }, children: effectiveRenderFlowchart({
683
+ spec: activeSpec,
684
+ snapshots: activeSnapshots,
685
+ selectedIndex: safeIdx,
686
+ onNodeClick: handleNodeClick,
687
+ showStageId,
688
+ }) })), showTreeSidebar && (_jsxs(_Fragment, { children: [_jsx(HLinePill, { label: leftLabel, expanded: leftExpanded, onClick: () => toggleLeft(!leftExpanded) }), leftExpanded && (_jsx("div", { style: { maxHeight: 180, overflow: "auto", flexShrink: 0 }, children: _jsx(SubflowTree, { graph: traceGraph ?? { nodes: [], edges: [] }, activeStage: rootOverlay.activeStage, doneStages: rootOverlay.doneStages, onNodeSelect: handleTreeNodeSelect }) }))] })), _jsx(HLinePill, { label: rightLabel, expanded: rightExpanded, onClick: () => toggleRight(!rightExpanded) }), rightExpanded && (_jsx("div", { style: { maxHeight: 350, flexShrink: 0, overflow: "hidden" }, children: detailsPanel })), _jsx(HLinePill, { label: bottomLabel, detail: `${activeSnapshots.length} stages`, expanded: timelineExpanded, onClick: toggleTimeline }), timelineExpanded && (_jsx("div", { style: { flexShrink: 0, overflow: "hidden" }, children: _jsx(GanttTimeline, { snapshots: activeSnapshots, selectedIndex: safeIdx, onSelect: handleSnapshotChange, size: size }) }))] })) : (
689
+ /* ── Desktop: two-column — Flowchart | Right Panel ── */
690
+ _jsxs(_Fragment, { children: [_jsxs("div", { style: { flex: 1, display: "flex", overflow: "hidden" }, children: [showTreeSidebar && (leftExpanded ? (_jsxs("div", { style: { width: 180, flexShrink: 0, display: "flex", flexDirection: "row", overflow: "hidden" }, children: [_jsx("div", { style: { flex: 1, overflow: "auto" }, children: _jsx(SubflowTree, { graph: traceGraph ?? { nodes: [], edges: [] }, activeStage: rootOverlay.activeStage, doneStages: rootOverlay.doneStages, onNodeSelect: handleTreeNodeSelect }) }), _jsx(VLinePill, { label: "Topology", expanded: true, side: "left", onClick: () => toggleLeft(false) })] })) : (_jsx(VLinePill, { label: "Topology", expanded: false, side: "left", onClick: () => toggleLeft(true) }))), showTopology ? (_jsx("div", { style: { flex: 1, overflow: "hidden", minWidth: 0 }, children: effectiveRenderFlowchart({
691
+ spec: activeSpec,
692
+ snapshots: activeSnapshots,
693
+ selectedIndex: safeIdx,
694
+ onNodeClick: handleNodeClick,
695
+ showStageId,
696
+ }) })) : (_jsx("div", { style: { flex: 1 } })), _jsx(VLinePill, { label: "Details", expanded: rightExpanded, onClick: () => toggleRight(!rightExpanded) }), rightExpanded && (_jsx("div", { style: { width: "42%", minWidth: 320, maxWidth: 550, display: "flex", flexDirection: "column", overflow: "hidden" }, children: _jsx(RightPanel, { mode: rightPanelMode, onModeChange: setRightPanelMode, snapshots: activeSnapshots, selectedIndex: safeIdx, runtimeSnapshot: runtimeSnapshot, spec: spec, activeTab: activeTab, allTabs: allTabs, activeNarrativeEntries: activeNarrativeEntries, recorderViews: recorderViews, autoRecorderViews: autoRecorderViews, size: size, onNavigateToStage: (id) => {
697
+ const idx = activeSnapshots.findIndex((s) => s.runtimeStageId === id);
698
+ if (idx >= 0)
699
+ setSnapshotIdx(idx);
700
+ } }) }))] }), _jsx(CompactTimeline, { snapshots: activeSnapshots, selectedIndex: safeIdx, defaultExpanded: timelineExpanded })] })) })] }));
93
701
  }
94
702
  //# sourceMappingURL=ExplainableShell.js.map