footprint-explainable-ui 0.6.0 → 0.7.2

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/dist/index.js CHANGED
@@ -23,7 +23,7 @@ function tokensToCSSVars(tokens) {
23
23
  if (tokens.fontFamily?.mono) vars["--fp-font-mono"] = tokens.fontFamily.mono;
24
24
  return vars;
25
25
  }
26
- var defaultTokens = {
26
+ var rawDefaults = {
27
27
  colors: {
28
28
  primary: "#6366f1",
29
29
  success: "#22c55e",
@@ -43,6 +43,26 @@ var defaultTokens = {
43
43
  mono: "'JetBrains Mono', 'Fira Code', monospace"
44
44
  }
45
45
  };
46
+ var defaultTokens = {
47
+ colors: {
48
+ primary: `var(--fp-color-primary, ${rawDefaults.colors.primary})`,
49
+ success: `var(--fp-color-success, ${rawDefaults.colors.success})`,
50
+ error: `var(--fp-color-error, ${rawDefaults.colors.error})`,
51
+ warning: `var(--fp-color-warning, ${rawDefaults.colors.warning})`,
52
+ bgPrimary: `var(--fp-bg-primary, ${rawDefaults.colors.bgPrimary})`,
53
+ bgSecondary: `var(--fp-bg-secondary, ${rawDefaults.colors.bgSecondary})`,
54
+ bgTertiary: `var(--fp-bg-tertiary, ${rawDefaults.colors.bgTertiary})`,
55
+ textPrimary: `var(--fp-text-primary, ${rawDefaults.colors.textPrimary})`,
56
+ textSecondary: `var(--fp-text-secondary, ${rawDefaults.colors.textSecondary})`,
57
+ textMuted: `var(--fp-text-muted, ${rawDefaults.colors.textMuted})`,
58
+ border: `var(--fp-border, ${rawDefaults.colors.border})`
59
+ },
60
+ radius: `var(--fp-radius, ${rawDefaults.radius})`,
61
+ fontFamily: {
62
+ sans: `var(--fp-font-sans, ${rawDefaults.fontFamily.sans})`,
63
+ mono: `var(--fp-font-mono, ${rawDefaults.fontFamily.mono})`
64
+ }
65
+ };
46
66
 
47
67
  // src/theme/ThemeProvider.tsx
48
68
  import { jsx } from "react/jsx-runtime";
@@ -197,7 +217,7 @@ function useDarkModeTokens(options) {
197
217
  }
198
218
 
199
219
  // src/components/MemoryInspector/MemoryInspector.tsx
200
- import { useMemo } from "react";
220
+ import { useMemo, useRef } from "react";
201
221
  import { jsx as jsx2, jsxs } from "react/jsx-runtime";
202
222
  function MemoryInspector({
203
223
  data,
@@ -210,6 +230,7 @@ function MemoryInspector({
210
230
  className,
211
231
  style
212
232
  }) {
233
+ const cacheRef = useRef(null);
213
234
  const { memory, newKeys } = useMemo(() => {
214
235
  if (data) {
215
236
  return { memory: data, newKeys: /* @__PURE__ */ new Set() };
@@ -217,21 +238,37 @@ function MemoryInspector({
217
238
  if (!snapshots || snapshots.length === 0) {
218
239
  return { memory: {}, newKeys: /* @__PURE__ */ new Set() };
219
240
  }
220
- const merged = {};
221
- for (let i = 0; i <= Math.min(selectedIndex, snapshots.length - 1); i++) {
222
- Object.assign(merged, snapshots[i]?.memory);
241
+ const safeIdx = Math.min(selectedIndex, snapshots.length - 1);
242
+ let merged;
243
+ const cache = cacheRef.current;
244
+ if (cache && cache.snapshots === snapshots && cache.index <= safeIdx) {
245
+ merged = { ...cache.accumulated };
246
+ for (let i = cache.index + 1; i <= safeIdx; i++) {
247
+ Object.assign(merged, snapshots[i]?.memory);
248
+ }
249
+ } else {
250
+ merged = {};
251
+ for (let i = 0; i <= safeIdx; i++) {
252
+ Object.assign(merged, snapshots[i]?.memory);
253
+ }
223
254
  }
255
+ cacheRef.current = { snapshots, index: safeIdx, accumulated: merged };
224
256
  const nk = /* @__PURE__ */ new Set();
225
- if (highlightNew && selectedIndex > 0) {
226
- const prev = {};
227
- for (let i = 0; i < selectedIndex; i++) {
228
- Object.assign(prev, snapshots[i]?.memory);
257
+ if (highlightNew && safeIdx > 0) {
258
+ let prev;
259
+ if (cache && cache.snapshots === snapshots && cache.index === safeIdx - 1) {
260
+ prev = cache.accumulated;
261
+ } else {
262
+ prev = {};
263
+ for (let i = 0; i < safeIdx; i++) {
264
+ Object.assign(prev, snapshots[i]?.memory);
265
+ }
229
266
  }
230
- const current = snapshots[selectedIndex]?.memory ?? {};
267
+ const current = snapshots[safeIdx]?.memory ?? {};
231
268
  for (const k of Object.keys(current)) {
232
269
  if (!(k in prev)) nk.add(k);
233
270
  }
234
- } else if (highlightNew && selectedIndex === 0 && snapshots[0]) {
271
+ } else if (highlightNew && safeIdx === 0 && snapshots[0]) {
235
272
  for (const k of Object.keys(snapshots[0].memory)) nk.add(k);
236
273
  }
237
274
  return { memory: merged, newKeys: nk };
@@ -240,9 +277,9 @@ function MemoryInspector({
240
277
  const fs = fontSize[size];
241
278
  const pad = padding[size];
242
279
  if (unstyled) {
243
- return /* @__PURE__ */ jsxs("div", { className, style, "data-fp": "memory-inspector", children: [
280
+ return /* @__PURE__ */ jsxs("div", { className, style, "data-fp": "memory-inspector", role: "region", "aria-label": "Memory state", children: [
244
281
  /* @__PURE__ */ jsx2("div", { "data-fp": "memory-label", children: "Memory State" }),
245
- /* @__PURE__ */ jsx2("pre", { "data-fp": "memory-json", children: JSON.stringify(memory, null, 2) })
282
+ /* @__PURE__ */ jsx2("pre", { "data-fp": "memory-json", children: /* @__PURE__ */ jsx2("code", { children: JSON.stringify(memory, null, 2) }) })
246
283
  ] });
247
284
  }
248
285
  return /* @__PURE__ */ jsxs(
@@ -255,6 +292,8 @@ function MemoryInspector({
255
292
  ...style
256
293
  },
257
294
  "data-fp": "memory-inspector",
295
+ role: "region",
296
+ "aria-label": "Memory state",
258
297
  children: [
259
298
  /* @__PURE__ */ jsx2(
260
299
  "span",
@@ -487,7 +526,7 @@ function NarrativeLog({
487
526
  }
488
527
 
489
528
  // src/components/NarrativeTrace/NarrativeTrace.tsx
490
- import { useState as useState2, useCallback, useMemo as useMemo3, useEffect as useEffect2, useRef } from "react";
529
+ import { useState as useState2, useCallback, useMemo as useMemo3, useEffect as useEffect2, useRef as useRef2 } from "react";
491
530
  import { jsx as jsx4, jsxs as jsxs3 } from "react/jsx-runtime";
492
531
  function parseGroups(lines) {
493
532
  const groups = [];
@@ -523,7 +562,7 @@ function NarrativeTrace({
523
562
  if (!defaultCollapsed) return /* @__PURE__ */ new Set();
524
563
  return new Set(parseGroups(narrative).map((g) => g.headerIdx));
525
564
  });
526
- const latestRef = useRef(null);
565
+ const latestRef = useRef2(null);
527
566
  useEffect2(() => {
528
567
  latestRef.current?.scrollIntoView({ behavior: "smooth", block: "nearest" });
529
568
  }, [revealedGroups.length]);
@@ -547,19 +586,32 @@ function NarrativeTrace({
547
586
  "data-fp": "narrative-header",
548
587
  "data-collapsible": group.steps.length > 0,
549
588
  "data-collapsed": collapsedSet.has(group.headerIdx),
589
+ role: group.steps.length > 0 ? "button" : void 0,
590
+ tabIndex: group.steps.length > 0 ? 0 : void 0,
591
+ "aria-expanded": group.steps.length > 0 ? !collapsedSet.has(group.headerIdx) : void 0,
592
+ "aria-label": `Stage ${gi + 1}, ${group.steps.length} steps${gi === lastIdx ? ", current" : ""}`,
550
593
  onClick: () => {
551
594
  if (group.steps.length > 0) toggle(group.headerIdx);
552
595
  onStageClick?.(group.headerIdx);
553
596
  },
597
+ onKeyDown: (e) => {
598
+ if (e.key === "Enter" || e.key === " ") {
599
+ e.preventDefault();
600
+ if (group.steps.length > 0) toggle(group.headerIdx);
601
+ onStageClick?.(group.headerIdx);
602
+ }
603
+ },
554
604
  children: group.header
555
605
  }
556
606
  ),
557
607
  !collapsedSet.has(group.headerIdx) && group.steps.map((step) => /* @__PURE__ */ jsx4("div", { "data-fp": "narrative-step", children: step.text }, step.idx))
558
608
  ] }, group.headerIdx)),
559
- futureGroups.map((group) => /* @__PURE__ */ jsxs3("div", { "data-fp": "narrative-group", "data-future": true, children: [
560
- /* @__PURE__ */ jsx4("div", { "data-fp": "narrative-header", children: group.header }),
561
- group.steps.map((step) => /* @__PURE__ */ jsx4("div", { "data-fp": "narrative-step", children: step.text }, `f-${step.idx}`))
562
- ] }, `f-${group.headerIdx}`))
609
+ futureGroups.length > 0 && /* @__PURE__ */ jsxs3("div", { "data-fp": "narrative-future-hint", children: [
610
+ futureGroups.length,
611
+ " more ",
612
+ futureGroups.length === 1 ? "stage" : "stages",
613
+ " ahead..."
614
+ ] })
563
615
  ] });
564
616
  }
565
617
  return /* @__PURE__ */ jsxs3(
@@ -589,10 +641,21 @@ function NarrativeTrace({
589
641
  /* @__PURE__ */ jsxs3(
590
642
  "div",
591
643
  {
644
+ role: hasSteps ? "button" : void 0,
645
+ tabIndex: hasSteps ? 0 : void 0,
646
+ "aria-expanded": hasSteps ? !isCollapsed : void 0,
647
+ "aria-label": `Stage ${gi + 1}, ${group.steps.length} steps${isLatest ? ", current" : ", completed"}`,
592
648
  onClick: () => {
593
649
  if (hasSteps) toggle(group.headerIdx);
594
650
  onStageClick?.(group.headerIdx);
595
651
  },
652
+ onKeyDown: (e) => {
653
+ if (e.key === "Enter" || e.key === " ") {
654
+ e.preventDefault();
655
+ if (hasSteps) toggle(group.headerIdx);
656
+ onStageClick?.(group.headerIdx);
657
+ }
658
+ },
596
659
  style: {
597
660
  fontSize: fs.body,
598
661
  lineHeight: 1.7,
@@ -651,43 +714,25 @@ function NarrativeTrace({
651
714
  group.headerIdx
652
715
  );
653
716
  }),
654
- futureGroups.length > 0 && /* @__PURE__ */ jsx4("div", { style: { opacity: 0.2 }, children: futureGroups.map((group) => /* @__PURE__ */ jsxs3("div", { style: { marginBottom: 2 }, children: [
655
- /* @__PURE__ */ jsx4(
656
- "div",
657
- {
658
- style: {
659
- fontSize: fs.body,
660
- lineHeight: 1.7,
661
- color: theme.textMuted,
662
- padding: `4px ${pad - 4}px`,
663
- borderLeft: `3px solid ${theme.border}`,
664
- fontWeight: 600,
665
- paddingLeft: pad + 12
666
- },
667
- children: group.header
668
- }
669
- ),
670
- group.steps.map((step) => /* @__PURE__ */ jsx4(
671
- "div",
672
- {
673
- style: {
674
- fontSize: fs.small,
675
- lineHeight: 1.6,
676
- color: theme.textMuted,
677
- padding: `2px ${pad - 4}px 2px ${pad + 20}px`
678
- },
679
- children: step.text
680
- },
681
- `f-${step.idx}`
682
- ))
683
- ] }, `f-${group.headerIdx}`)) })
717
+ futureGroups.length > 0 && /* @__PURE__ */ jsxs3("div", { style: {
718
+ opacity: 0.3,
719
+ fontSize: fs.small,
720
+ color: theme.textMuted,
721
+ padding: `8px ${pad}px`,
722
+ fontStyle: "italic"
723
+ }, children: [
724
+ futureGroups.length,
725
+ " more ",
726
+ futureGroups.length === 1 ? "stage" : "stages",
727
+ " ahead..."
728
+ ] })
684
729
  ]
685
730
  }
686
731
  );
687
732
  }
688
733
 
689
734
  // src/components/GanttTimeline/GanttTimeline.tsx
690
- import { useState as useState3, useMemo as useMemo4, useRef as useRef2, useEffect as useEffect3 } from "react";
735
+ import { useState as useState3, useMemo as useMemo4, useRef as useRef3, useEffect as useEffect3 } from "react";
691
736
  import { jsx as jsx5, jsxs as jsxs4 } from "react/jsx-runtime";
692
737
  function GanttTimeline({
693
738
  snapshots,
@@ -700,8 +745,8 @@ function GanttTimeline({
700
745
  maxVisibleRows = 5
701
746
  }) {
702
747
  const [expanded, setExpanded] = useState3(false);
703
- const activeRowRef = useRef2(null);
704
- const scrollContainerRef = useRef2(null);
748
+ const activeRowRef = useRef3(null);
749
+ const scrollContainerRef = useRef3(null);
705
750
  const totalWallTime = useMemo4(
706
751
  () => Math.max(...snapshots.map((s) => s.startMs + s.durationMs), 1),
707
752
  [snapshots]
@@ -722,12 +767,15 @@ function GanttTimeline({
722
767
  }
723
768
  }, [selectedIndex, showAll]);
724
769
  if (unstyled) {
725
- return /* @__PURE__ */ jsx5("div", { className, style, "data-fp": "gantt-timeline", children: snapshots.map((snap, idx) => /* @__PURE__ */ jsxs4(
770
+ return /* @__PURE__ */ jsx5("div", { className, style, "data-fp": "gantt-timeline", role: "listbox", "aria-label": "Execution timeline", children: snapshots.map((snap, idx) => /* @__PURE__ */ jsxs4(
726
771
  "div",
727
772
  {
728
773
  "data-fp": "gantt-bar",
729
774
  "data-selected": idx === selectedIndex,
730
775
  "data-visible": idx <= selectedIndex,
776
+ role: "option",
777
+ "aria-selected": idx === selectedIndex,
778
+ "aria-label": `${snap.stageLabel}, ${snap.durationMs}ms`,
731
779
  onClick: () => onSelect?.(idx),
732
780
  children: [
733
781
  /* @__PURE__ */ jsx5("span", { "data-fp": "gantt-label", children: snap.stageLabel }),
@@ -737,7 +785,7 @@ function GanttTimeline({
737
785
  ] })
738
786
  ]
739
787
  },
740
- snap.stageName
788
+ `${snap.stageName}-${idx}`
741
789
  )) });
742
790
  }
743
791
  return /* @__PURE__ */ jsxs4(
@@ -793,6 +841,8 @@ function GanttTimeline({
793
841
  "div",
794
842
  {
795
843
  ref: scrollContainerRef,
844
+ role: "listbox",
845
+ "aria-label": "Execution timeline",
796
846
  style: {
797
847
  marginTop: 8,
798
848
  display: "flex",
@@ -813,6 +863,9 @@ function GanttTimeline({
813
863
  "div",
814
864
  {
815
865
  ref: isSelected ? activeRowRef : void 0,
866
+ role: "option",
867
+ "aria-selected": isSelected,
868
+ "aria-label": `${snap.stageLabel}, ${snap.durationMs}ms`,
816
869
  onClick: () => onSelect?.(idx),
817
870
  style: {
818
871
  display: "flex",
@@ -828,6 +881,7 @@ function GanttTimeline({
828
881
  /* @__PURE__ */ jsx5(
829
882
  "span",
830
883
  {
884
+ title: snap.stageLabel,
831
885
  style: {
832
886
  width: labelWidth,
833
887
  fontSize: fs.small,
@@ -887,7 +941,7 @@ function GanttTimeline({
887
941
  )
888
942
  ]
889
943
  },
890
- snap.stageName
944
+ `${snap.stageName}-${idx}`
891
945
  );
892
946
  })
893
947
  }
@@ -1209,9 +1263,9 @@ function fmt(v2) {
1209
1263
  return String(v2);
1210
1264
  }
1211
1265
  var diffColors = {
1212
- added: { bg: "rgba(34,197,94,0.10)", fg: "#22c55e", icon: "+" },
1213
- removed: { bg: "rgba(239,68,68,0.10)", fg: "#ef4444", icon: "-" },
1214
- changed: { bg: "rgba(245,158,11,0.10)", fg: "#f59e0b", icon: "~" },
1266
+ added: { bg: `color-mix(in srgb, ${theme.success} 10%, transparent)`, fg: theme.success, icon: "+" },
1267
+ removed: { bg: `color-mix(in srgb, ${theme.error} 10%, transparent)`, fg: theme.error, icon: "-" },
1268
+ changed: { bg: `color-mix(in srgb, ${theme.warning} 10%, transparent)`, fg: theme.warning, icon: "~" },
1215
1269
  unchanged: { bg: "transparent", fg: "", icon: " " }
1216
1270
  };
1217
1271
  function ScopeDiff({
@@ -1905,7 +1959,7 @@ function StageDetailPanel({
1905
1959
  }
1906
1960
 
1907
1961
  // src/components/TimeTravelControls/TimeTravelControls.tsx
1908
- import { useState as useState6, useEffect as useEffect4, useRef as useRef3, useCallback as useCallback3 } from "react";
1962
+ import { useState as useState6, useEffect as useEffect4, useRef as useRef4, useCallback as useCallback3 } from "react";
1909
1963
  import { jsx as jsx10, jsxs as jsxs9 } from "react/jsx-runtime";
1910
1964
  function TimeTravelControls({
1911
1965
  snapshots,
@@ -1918,7 +1972,7 @@ function TimeTravelControls({
1918
1972
  style
1919
1973
  }) {
1920
1974
  const [playing, setPlaying] = useState6(false);
1921
- const playRef = useRef3(null);
1975
+ const playRef = useRef4(null);
1922
1976
  const total = snapshots.length;
1923
1977
  const canPrev = selectedIndex > 0;
1924
1978
  const canNext = selectedIndex < total - 1;
@@ -1948,49 +2002,80 @@ function TimeTravelControls({
1948
2002
  setPlaying(true);
1949
2003
  }
1950
2004
  }, [playing, selectedIndex, total, onIndexChange]);
2005
+ const handleKeyDown = useCallback3(
2006
+ (e) => {
2007
+ if (e.key === "ArrowLeft" && canPrev && !playing) {
2008
+ e.preventDefault();
2009
+ setPlaying(false);
2010
+ onIndexChange(selectedIndex - 1);
2011
+ } else if (e.key === "ArrowRight" && canNext && !playing) {
2012
+ e.preventDefault();
2013
+ setPlaying(false);
2014
+ onIndexChange(selectedIndex + 1);
2015
+ } else if (e.key === " " && autoPlayable) {
2016
+ e.preventDefault();
2017
+ togglePlay();
2018
+ }
2019
+ },
2020
+ [canPrev, canNext, playing, selectedIndex, onIndexChange, autoPlayable, togglePlay]
2021
+ );
1951
2022
  const fs = fontSize[size];
1952
2023
  if (unstyled) {
1953
- return /* @__PURE__ */ jsxs9("div", { className, style, "data-fp": "time-travel-controls", children: [
1954
- /* @__PURE__ */ jsx10(
1955
- "button",
1956
- {
1957
- "data-fp": "tt-prev",
1958
- disabled: !canPrev || playing,
1959
- onClick: () => {
1960
- setPlaying(false);
1961
- onIndexChange(selectedIndex - 1);
1962
- },
1963
- children: "Prev"
1964
- }
1965
- ),
1966
- autoPlayable && /* @__PURE__ */ jsx10("button", { "data-fp": "tt-play", onClick: togglePlay, children: playing ? "Pause" : "Play" }),
1967
- /* @__PURE__ */ jsx10(
1968
- "button",
1969
- {
1970
- "data-fp": "tt-next",
1971
- disabled: !canNext || playing,
1972
- onClick: () => {
1973
- setPlaying(false);
1974
- onIndexChange(selectedIndex + 1);
1975
- },
1976
- children: "Next"
1977
- }
1978
- ),
1979
- /* @__PURE__ */ jsx10("div", { "data-fp": "tt-ticks", children: snapshots.map((snap, i) => /* @__PURE__ */ jsx10(
1980
- "button",
1981
- {
1982
- "data-fp": "tt-tick",
1983
- "data-active": i === selectedIndex,
1984
- "data-done": i < selectedIndex,
1985
- onClick: () => {
1986
- setPlaying(false);
1987
- onIndexChange(i);
1988
- },
1989
- title: snap.stageLabel
1990
- },
1991
- i
1992
- )) })
1993
- ] });
2024
+ return /* @__PURE__ */ jsxs9(
2025
+ "div",
2026
+ {
2027
+ className,
2028
+ style,
2029
+ "data-fp": "time-travel-controls",
2030
+ role: "toolbar",
2031
+ "aria-label": "Time travel controls",
2032
+ tabIndex: 0,
2033
+ onKeyDown: handleKeyDown,
2034
+ children: [
2035
+ /* @__PURE__ */ jsx10(
2036
+ "button",
2037
+ {
2038
+ "data-fp": "tt-prev",
2039
+ disabled: !canPrev || playing,
2040
+ onClick: () => {
2041
+ setPlaying(false);
2042
+ onIndexChange(selectedIndex - 1);
2043
+ },
2044
+ "aria-label": "Previous stage",
2045
+ children: "Prev"
2046
+ }
2047
+ ),
2048
+ autoPlayable && /* @__PURE__ */ jsx10("button", { "data-fp": "tt-play", onClick: togglePlay, "aria-label": playing ? "Pause" : "Play", children: playing ? "Pause" : "Play" }),
2049
+ /* @__PURE__ */ jsx10(
2050
+ "button",
2051
+ {
2052
+ "data-fp": "tt-next",
2053
+ disabled: !canNext || playing,
2054
+ onClick: () => {
2055
+ setPlaying(false);
2056
+ onIndexChange(selectedIndex + 1);
2057
+ },
2058
+ "aria-label": "Next stage",
2059
+ children: "Next"
2060
+ }
2061
+ ),
2062
+ /* @__PURE__ */ jsx10("div", { "data-fp": "tt-ticks", children: snapshots.map((snap, i) => /* @__PURE__ */ jsx10(
2063
+ "button",
2064
+ {
2065
+ "data-fp": "tt-tick",
2066
+ "data-active": i === selectedIndex,
2067
+ "data-done": i < selectedIndex,
2068
+ onClick: () => {
2069
+ setPlaying(false);
2070
+ onIndexChange(i);
2071
+ },
2072
+ title: snap.stageLabel
2073
+ },
2074
+ i
2075
+ )) })
2076
+ ]
2077
+ }
2078
+ );
1994
2079
  }
1995
2080
  const btnStyle = (disabled) => ({
1996
2081
  background: theme.bgTertiary,
@@ -2019,6 +2104,10 @@ function TimeTravelControls({
2019
2104
  ...style
2020
2105
  },
2021
2106
  "data-fp": "time-travel-controls",
2107
+ role: "toolbar",
2108
+ "aria-label": "Time travel controls",
2109
+ tabIndex: 0,
2110
+ onKeyDown: handleKeyDown,
2022
2111
  children: [
2023
2112
  /* @__PURE__ */ jsx10(
2024
2113
  "button",
@@ -2029,6 +2118,7 @@ function TimeTravelControls({
2029
2118
  setPlaying(false);
2030
2119
  onIndexChange(selectedIndex - 1);
2031
2120
  },
2121
+ "aria-label": "Previous stage",
2032
2122
  children: "\u25C0"
2033
2123
  }
2034
2124
  ),
@@ -2051,6 +2141,7 @@ function TimeTravelControls({
2051
2141
  flexShrink: 0
2052
2142
  },
2053
2143
  title: playing ? "Pause" : "Play",
2144
+ "aria-label": playing ? "Pause" : "Play",
2054
2145
  children: playing ? "\u23F8" : "\u25B6"
2055
2146
  }
2056
2147
  ),
@@ -2063,6 +2154,7 @@ function TimeTravelControls({
2063
2154
  setPlaying(false);
2064
2155
  onIndexChange(selectedIndex + 1);
2065
2156
  },
2157
+ "aria-label": "Next stage",
2066
2158
  children: "\u25B6"
2067
2159
  }
2068
2160
  ),
@@ -2109,125 +2201,146 @@ function TimeTravelControls({
2109
2201
  }
2110
2202
 
2111
2203
  // src/components/ExplainableShell/ExplainableShell.tsx
2112
- import { useState as useState7, useCallback as useCallback4, useMemo as useMemo7 } from "react";
2113
- import { Fragment as Fragment3, jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
2114
- function ExplainableShell({
2204
+ import { memo as memo3, useState as useState8, useCallback as useCallback5, useMemo as useMemo10, useRef as useRef6, useEffect as useEffect6 } from "react";
2205
+
2206
+ // src/adapters/fromRuntimeSnapshot.ts
2207
+ function toVisualizationSnapshots(runtime, narrativeEntries) {
2208
+ const stageNarrativeMap = narrativeEntries?.length ? buildStageNarrativeMap(narrativeEntries) : /* @__PURE__ */ new Map();
2209
+ const snapshots = [];
2210
+ flattenTree(runtime.executionTree, snapshots, runtime.sharedState, 0, runtime.subflowResults, {}, stageNarrativeMap);
2211
+ return snapshots;
2212
+ }
2213
+ function buildStageNarrativeMap(entries) {
2214
+ const map = /* @__PURE__ */ new Map();
2215
+ let currentStageName;
2216
+ for (const entry of entries) {
2217
+ if (entry.stageName) {
2218
+ currentStageName = entry.stageName;
2219
+ }
2220
+ if (currentStageName) {
2221
+ if (!map.has(currentStageName)) {
2222
+ map.set(currentStageName, []);
2223
+ }
2224
+ const indent = " ".repeat(entry.depth);
2225
+ map.get(currentStageName).push(`${indent}${entry.text}`);
2226
+ }
2227
+ }
2228
+ return map;
2229
+ }
2230
+ function flattenTree(node, out, sharedState, accumulatedMs = 0, subflowResults, cumulativeMemory = {}, stageNarrativeMap = /* @__PURE__ */ new Map()) {
2231
+ const durationMs = typeof node.metrics?.durationMs === "number" ? node.metrics.durationMs : 1;
2232
+ const startMs = accumulatedMs;
2233
+ const stageId = node.id || node.name || "unknown";
2234
+ const displayName = node.name || node.id || "unknown";
2235
+ const stageLines = stageNarrativeMap.get(stageId);
2236
+ let narrative;
2237
+ if (stageLines) {
2238
+ narrative = stageLines.join("\n");
2239
+ } else {
2240
+ const parts = [`${displayName} executed.`];
2241
+ if (node.description) parts.push(node.description);
2242
+ if (node.stageWrites) {
2243
+ const keys = Object.keys(node.stageWrites);
2244
+ if (keys.length > 0) parts.push(`Wrote: ${keys.join(", ")}`);
2245
+ }
2246
+ narrative = parts.join("\n");
2247
+ }
2248
+ const memory = { ...cumulativeMemory };
2249
+ if (node.stageWrites) {
2250
+ for (const [key, value] of Object.entries(node.stageWrites)) {
2251
+ if (value === void 0) {
2252
+ delete memory[key];
2253
+ } else {
2254
+ memory[key] = value;
2255
+ }
2256
+ }
2257
+ }
2258
+ const sfResult = subflowResults?.[node.subflowId ?? stageId];
2259
+ out.push({
2260
+ stageName: displayName,
2261
+ stageLabel: stageId,
2262
+ memory,
2263
+ narrative,
2264
+ startMs,
2265
+ durationMs,
2266
+ status: "done",
2267
+ ...node.description ? { description: node.description } : void 0,
2268
+ ...node.subflowId ? { subflowId: node.subflowId } : void 0,
2269
+ ...sfResult ? { subflowResult: sfResult } : void 0
2270
+ });
2271
+ let nextMs = startMs + durationMs;
2272
+ if (node.children && node.children.length > 0) {
2273
+ let maxChildEnd = nextMs;
2274
+ for (const child of node.children) {
2275
+ const childEnd = flattenTree(child, out, sharedState, nextMs, subflowResults, memory, stageNarrativeMap);
2276
+ maxChildEnd = Math.max(maxChildEnd, childEnd);
2277
+ }
2278
+ nextMs = maxChildEnd;
2279
+ }
2280
+ if (node.next) {
2281
+ nextMs = flattenTree(node.next, out, sharedState, nextMs, subflowResults, memory, stageNarrativeMap);
2282
+ }
2283
+ return nextMs;
2284
+ }
2285
+ function subflowResultToSnapshots(subflowResult, narrativeEntries) {
2286
+ if (!subflowResult || typeof subflowResult !== "object") return [];
2287
+ const sf = subflowResult;
2288
+ if (!sf.treeContext?.stageContexts) return [];
2289
+ const runtime = {
2290
+ sharedState: sf.treeContext.globalContext ?? {},
2291
+ executionTree: sf.treeContext.stageContexts,
2292
+ commitLog: sf.treeContext.history ?? []
2293
+ };
2294
+ const snapshots = toVisualizationSnapshots(runtime, narrativeEntries);
2295
+ const prefix = sf.subflowId ? `${sf.subflowId}/` : "";
2296
+ if (prefix) {
2297
+ for (const snap of snapshots) {
2298
+ if (snap.stageName.startsWith(prefix)) {
2299
+ snap.stageName = snap.stageName.slice(prefix.length);
2300
+ }
2301
+ if (snap.stageLabel.startsWith(prefix)) {
2302
+ snap.stageLabel = snap.stageLabel.slice(prefix.length);
2303
+ }
2304
+ }
2305
+ }
2306
+ return snapshots;
2307
+ }
2308
+ function createSnapshots(stages) {
2309
+ let accMs = 0;
2310
+ return stages.map((s) => {
2311
+ const duration = s.durationMs ?? 1;
2312
+ const snap = {
2313
+ stageName: s.name,
2314
+ stageLabel: s.label ?? s.name,
2315
+ memory: s.memory ?? {},
2316
+ narrative: s.narrative ?? `${s.label ?? s.name} completed.`,
2317
+ startMs: accMs,
2318
+ durationMs: duration,
2319
+ status: "done",
2320
+ ...s.description ? { description: s.description } : void 0,
2321
+ ...s.subflowId ? { subflowId: s.subflowId } : void 0
2322
+ };
2323
+ accMs += duration;
2324
+ return snap;
2325
+ });
2326
+ }
2327
+
2328
+ // src/components/MemoryPanel/MemoryPanel.tsx
2329
+ import { jsx as jsx11, jsxs as jsxs10 } from "react/jsx-runtime";
2330
+ function MemoryPanel({
2115
2331
  snapshots,
2116
- resultData,
2117
- logs = [],
2118
- narrative = [],
2119
- tabs = ["result", "explainable", "ai-compatible"],
2120
- defaultTab,
2121
- hideConsole = false,
2122
- renderFlowchart,
2332
+ selectedIndex,
2123
2333
  size = "default",
2124
2334
  unstyled = false,
2125
2335
  className,
2126
2336
  style
2127
2337
  }) {
2128
- const [activeTab, setActiveTab] = useState7(defaultTab ?? tabs[0]);
2129
- const [snapshotIdx, setSnapshotIdx] = useState7(0);
2130
- const fs = fontSize[size];
2131
- const pad = padding[size];
2132
- const handleSnapshotChange = useCallback4((idx) => {
2133
- setSnapshotIdx(Math.max(0, Math.min(idx, snapshots.length - 1)));
2134
- }, [snapshots.length]);
2135
- const revealedCount = useMemo7(() => {
2136
- if (snapshots.length === 0 || narrative.length === 0) return narrative.length;
2137
- const boundaries = [];
2138
- for (let i = 0; i < narrative.length; i++) {
2139
- const trimmed = narrative[i].trimStart();
2140
- if (trimmed.startsWith("Stage ") && !trimmed.match(/^Stage\s+\d+:\s*Step\s/) || trimmed.startsWith("[")) {
2141
- boundaries.push(i);
2142
- }
2143
- }
2144
- if (boundaries.length === 0) {
2145
- const ratio = (snapshotIdx + 1) / snapshots.length;
2146
- return Math.max(1, Math.ceil(narrative.length * ratio));
2147
- }
2148
- const groupsToShow = Math.max(
2149
- 1,
2150
- Math.min(
2151
- Math.floor((snapshotIdx + 1) / snapshots.length * boundaries.length) || 1,
2152
- boundaries.length
2153
- )
2154
- );
2155
- const endIdx = groupsToShow < boundaries.length ? boundaries[groupsToShow] : narrative.length;
2156
- return Math.max(1, endIdx);
2157
- }, [snapshots.length, snapshotIdx, narrative]);
2158
- const prevMemory = snapshotIdx > 0 ? snapshots[snapshotIdx - 1]?.memory : null;
2159
- const currMemory = snapshots[snapshotIdx]?.memory ?? {};
2160
- const tabLabels = {
2161
- result: "Result",
2162
- explainable: "Explainable",
2163
- "ai-compatible": "AI-Compatible"
2164
- };
2338
+ const prevMemory = selectedIndex > 0 ? snapshots[selectedIndex - 1]?.memory ?? null : null;
2339
+ const currMemory = snapshots[selectedIndex]?.memory ?? {};
2165
2340
  if (unstyled) {
2166
- return /* @__PURE__ */ jsxs10("div", { className, style, "data-fp": "explainable-shell", children: [
2167
- /* @__PURE__ */ jsx11("div", { "data-fp": "shell-tabs", children: tabs.map((tab) => /* @__PURE__ */ jsx11(
2168
- "button",
2169
- {
2170
- "data-fp": "shell-tab",
2171
- "data-active": tab === activeTab,
2172
- onClick: () => setActiveTab(tab),
2173
- children: tabLabels[tab]
2174
- },
2175
- tab
2176
- )) }),
2177
- /* @__PURE__ */ jsxs10("div", { "data-fp": "shell-content", "data-tab": activeTab, children: [
2178
- activeTab === "result" && /* @__PURE__ */ jsx11(
2179
- ResultPanel,
2180
- {
2181
- data: resultData ?? null,
2182
- logs,
2183
- hideConsole,
2184
- unstyled: true
2185
- }
2186
- ),
2187
- activeTab === "explainable" && /* @__PURE__ */ jsxs10(Fragment3, { children: [
2188
- /* @__PURE__ */ jsx11(
2189
- TimeTravelControls,
2190
- {
2191
- snapshots,
2192
- selectedIndex: snapshotIdx,
2193
- onIndexChange: handleSnapshotChange,
2194
- unstyled: true
2195
- }
2196
- ),
2197
- renderFlowchart?.({ snapshots, selectedIndex: snapshotIdx, onNodeClick: handleSnapshotChange }),
2198
- /* @__PURE__ */ jsx11(MemoryInspector, { snapshots, selectedIndex: snapshotIdx, unstyled: true }),
2199
- /* @__PURE__ */ jsx11(ScopeDiff, { previous: prevMemory, current: currMemory, unstyled: true }),
2200
- /* @__PURE__ */ jsx11(
2201
- GanttTimeline,
2202
- {
2203
- snapshots,
2204
- selectedIndex: snapshotIdx,
2205
- onSelect: handleSnapshotChange,
2206
- unstyled: true
2207
- }
2208
- )
2209
- ] }),
2210
- activeTab === "ai-compatible" && /* @__PURE__ */ jsxs10(Fragment3, { children: [
2211
- /* @__PURE__ */ jsx11(
2212
- TimeTravelControls,
2213
- {
2214
- snapshots,
2215
- selectedIndex: snapshotIdx,
2216
- onIndexChange: handleSnapshotChange,
2217
- unstyled: true
2218
- }
2219
- ),
2220
- renderFlowchart?.({ snapshots, selectedIndex: snapshotIdx, onNodeClick: handleSnapshotChange }),
2221
- /* @__PURE__ */ jsx11(
2222
- NarrativeTrace,
2223
- {
2224
- narrative,
2225
- revealedCount,
2226
- unstyled: true
2227
- }
2228
- )
2229
- ] })
2230
- ] })
2341
+ return /* @__PURE__ */ jsxs10("div", { className, style, "data-fp": "memory-panel", children: [
2342
+ /* @__PURE__ */ jsx11(MemoryInspector, { snapshots, selectedIndex, unstyled: true }),
2343
+ /* @__PURE__ */ jsx11(ScopeDiff, { previous: prevMemory, current: currMemory, unstyled: true })
2231
2344
  ] });
2232
2345
  }
2233
2346
  return /* @__PURE__ */ jsxs10(
@@ -2235,156 +2348,251 @@ function ExplainableShell({
2235
2348
  {
2236
2349
  className,
2237
2350
  style: {
2238
- height: "100%",
2351
+ overflow: "auto",
2239
2352
  display: "flex",
2240
2353
  flexDirection: "column",
2241
- overflow: "hidden",
2242
- background: theme.bgPrimary,
2243
- color: theme.textPrimary,
2244
- fontFamily: theme.fontSans,
2245
2354
  ...style
2246
2355
  },
2247
- "data-fp": "explainable-shell",
2356
+ "data-fp": "memory-panel",
2248
2357
  children: [
2249
- /* @__PURE__ */ jsx11(
2250
- "div",
2251
- {
2252
- style: {
2253
- display: "flex",
2254
- gap: 0,
2255
- borderBottom: `1px solid ${theme.border}`,
2256
- background: theme.bgSecondary,
2257
- flexShrink: 0
2258
- },
2259
- children: tabs.map((tab) => {
2260
- const active = tab === activeTab;
2261
- return /* @__PURE__ */ jsx11(
2262
- "button",
2263
- {
2264
- onClick: () => setActiveTab(tab),
2265
- style: {
2266
- padding: `${pad - 4}px ${pad}px`,
2267
- fontSize: fs.label,
2268
- fontWeight: active ? 700 : 500,
2269
- textTransform: "uppercase",
2270
- letterSpacing: "0.08em",
2271
- color: active ? theme.primary : theme.textMuted,
2272
- background: "transparent",
2273
- border: "none",
2274
- borderBottom: active ? `2px solid ${theme.primary}` : "2px solid transparent",
2275
- cursor: "pointer",
2276
- transition: "all 0.15s ease"
2277
- },
2278
- children: tabLabels[tab]
2279
- },
2280
- tab
2281
- );
2282
- })
2283
- }
2284
- ),
2285
- /* @__PURE__ */ jsxs10("div", { style: { flex: 1, overflow: "hidden", display: "flex", flexDirection: "column" }, children: [
2286
- activeTab === "result" && /* @__PURE__ */ jsx11(
2287
- ResultPanel,
2288
- {
2289
- data: resultData ?? null,
2290
- logs,
2291
- hideConsole,
2292
- size
2293
- }
2294
- ),
2295
- activeTab === "explainable" && /* @__PURE__ */ jsxs10(Fragment3, { children: [
2296
- /* @__PURE__ */ jsx11(
2297
- TimeTravelControls,
2298
- {
2299
- snapshots,
2300
- selectedIndex: snapshotIdx,
2301
- onIndexChange: handleSnapshotChange,
2302
- size
2303
- }
2304
- ),
2305
- /* @__PURE__ */ jsxs10("div", { style: { flex: 1, display: "flex", overflow: "hidden" }, children: [
2306
- renderFlowchart && /* @__PURE__ */ jsx11("div", { style: { flex: 1, overflow: "hidden", borderRight: `1px solid ${theme.border}` }, children: renderFlowchart({ snapshots, selectedIndex: snapshotIdx, onNodeClick: handleSnapshotChange }) }),
2307
- /* @__PURE__ */ jsxs10(
2308
- "div",
2309
- {
2310
- style: {
2311
- width: renderFlowchart ? "40%" : "100%",
2312
- minWidth: 280,
2313
- overflow: "auto",
2314
- display: "flex",
2315
- flexDirection: "column"
2316
- },
2317
- children: [
2318
- /* @__PURE__ */ jsx11(
2319
- MemoryInspector,
2320
- {
2321
- snapshots,
2322
- selectedIndex: snapshotIdx,
2323
- size
2324
- }
2325
- ),
2326
- /* @__PURE__ */ jsx11("div", { style: { borderTop: `1px solid ${theme.border}` }, children: /* @__PURE__ */ jsx11(
2327
- ScopeDiff,
2328
- {
2329
- previous: prevMemory,
2330
- current: currMemory,
2331
- hideUnchanged: true,
2332
- size
2333
- }
2334
- ) })
2335
- ]
2336
- }
2337
- )
2338
- ] }),
2339
- /* @__PURE__ */ jsx11("div", { style: { borderTop: `1px solid ${theme.border}`, flexShrink: 0 }, children: /* @__PURE__ */ jsx11(
2340
- GanttTimeline,
2341
- {
2342
- snapshots,
2343
- selectedIndex: snapshotIdx,
2344
- onSelect: handleSnapshotChange,
2345
- size
2346
- }
2347
- ) })
2348
- ] }),
2349
- activeTab === "ai-compatible" && /* @__PURE__ */ jsxs10(Fragment3, { children: [
2350
- /* @__PURE__ */ jsx11(
2351
- TimeTravelControls,
2352
- {
2353
- snapshots,
2354
- selectedIndex: snapshotIdx,
2355
- onIndexChange: handleSnapshotChange,
2356
- size
2357
- }
2358
- ),
2359
- /* @__PURE__ */ jsxs10("div", { style: { flex: 1, display: "flex", overflow: "hidden" }, children: [
2360
- renderFlowchart && /* @__PURE__ */ jsx11("div", { style: { flex: 1, overflow: "hidden", borderRight: `1px solid ${theme.border}` }, children: renderFlowchart({ snapshots, selectedIndex: snapshotIdx, onNodeClick: handleSnapshotChange }) }),
2361
- /* @__PURE__ */ jsx11(
2362
- NarrativeTrace,
2363
- {
2364
- narrative,
2365
- revealedCount,
2366
- size,
2367
- style: {
2368
- width: renderFlowchart ? "40%" : "100%",
2369
- minWidth: 280
2358
+ /* @__PURE__ */ jsx11(MemoryInspector, { snapshots, selectedIndex, size }),
2359
+ /* @__PURE__ */ jsx11("div", { style: { borderTop: `1px solid ${theme.border}` }, children: /* @__PURE__ */ jsx11(ScopeDiff, { previous: prevMemory, current: currMemory, hideUnchanged: true, size }) })
2360
+ ]
2361
+ }
2362
+ );
2363
+ }
2364
+
2365
+ // src/components/NarrativePanel/NarrativePanel.tsx
2366
+ import { useMemo as useMemo8 } from "react";
2367
+
2368
+ // src/components/StoryNarrative/StoryNarrative.tsx
2369
+ import { useMemo as useMemo7, useRef as useRef5, useEffect as useEffect5 } from "react";
2370
+ import { jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
2371
+ var ENTRY_ICONS = {
2372
+ stage: { icon: "\u25B8", color: theme.primary, label: "Stage" },
2373
+ step: { icon: "\xB7", color: theme.textMuted, label: "Data operation" },
2374
+ condition: { icon: "\u25C7", color: theme.warning, label: "Decision" },
2375
+ fork: { icon: "\u2443", color: theme.primary, label: "Parallel" },
2376
+ subflow: { icon: "\u21B3", color: theme.textSecondary, label: "Subflow" },
2377
+ loop: { icon: "\u21BB", color: theme.warning, label: "Loop" },
2378
+ break: { icon: "\u25A0", color: theme.error, label: "Break" },
2379
+ error: { icon: "\u2717", color: theme.error, label: "Error" }
2380
+ };
2381
+ function StoryNarrative({
2382
+ entries,
2383
+ stageCount,
2384
+ size = "default",
2385
+ unstyled = false,
2386
+ className,
2387
+ style: outerStyle
2388
+ }) {
2389
+ const fs = fontSize[size];
2390
+ const pad = padding[size];
2391
+ const revealedCount = useMemo7(() => {
2392
+ let stagesSeen = 0;
2393
+ for (let i = 0; i < entries.length; i++) {
2394
+ const e = entries[i];
2395
+ const isBoundary = e.type === "stage" || e.type === "subflow" && e.text.startsWith("Entering");
2396
+ if (isBoundary) stagesSeen++;
2397
+ if (stagesSeen > stageCount) return i;
2398
+ }
2399
+ return entries.length;
2400
+ }, [entries, stageCount]);
2401
+ const revealed = entries.slice(0, revealedCount);
2402
+ const future = entries.slice(revealedCount);
2403
+ const latestRef = useRef5(null);
2404
+ useEffect5(() => {
2405
+ latestRef.current?.scrollIntoView({ behavior: "smooth", block: "nearest" });
2406
+ }, [revealed.length]);
2407
+ if (unstyled) {
2408
+ return /* @__PURE__ */ jsx12("div", { className, style: outerStyle, "data-fp": "story-narrative", role: "log", children: revealed.map((entry, i) => /* @__PURE__ */ jsx12("div", { "data-fp": "narrative-entry", "data-type": entry.type, children: entry.text }, i)) });
2409
+ }
2410
+ return /* @__PURE__ */ jsxs11(
2411
+ "div",
2412
+ {
2413
+ className,
2414
+ style: {
2415
+ flex: 1,
2416
+ overflow: "auto",
2417
+ padding: pad,
2418
+ fontFamily: theme.fontSans,
2419
+ ...outerStyle
2420
+ },
2421
+ "data-fp": "story-narrative",
2422
+ role: "log",
2423
+ "aria-label": "Execution narrative",
2424
+ children: [
2425
+ revealed.map((entry, i) => {
2426
+ const meta = ENTRY_ICONS[entry.type] ?? ENTRY_ICONS.step;
2427
+ const isStage = entry.type === "stage";
2428
+ const isDecision = entry.type === "condition";
2429
+ const isError = entry.type === "error";
2430
+ const isLast = i === revealed.length - 1;
2431
+ return /* @__PURE__ */ jsxs11(
2432
+ "div",
2433
+ {
2434
+ ref: isLast ? latestRef : void 0,
2435
+ style: {
2436
+ display: "flex",
2437
+ gap: 8,
2438
+ padding: isStage ? `${pad - 4}px 0` : `2px 0`,
2439
+ marginLeft: entry.depth * 16,
2440
+ borderBottom: isStage ? `1px solid ${theme.border}` : void 0,
2441
+ marginTop: isStage && i > 0 ? 8 : 0
2442
+ },
2443
+ children: [
2444
+ /* @__PURE__ */ jsx12(
2445
+ "span",
2446
+ {
2447
+ style: {
2448
+ color: meta.color,
2449
+ fontWeight: 700,
2450
+ fontSize: isStage ? fs.body : fs.small,
2451
+ width: 16,
2452
+ textAlign: "center",
2453
+ flexShrink: 0
2454
+ },
2455
+ title: meta.label,
2456
+ "aria-label": meta.label,
2457
+ children: meta.icon
2370
2458
  }
2371
- }
2372
- )
2373
- ] })
2374
- ] })
2459
+ ),
2460
+ /* @__PURE__ */ jsx12(
2461
+ "span",
2462
+ {
2463
+ style: {
2464
+ fontSize: isStage ? fs.body : fs.small,
2465
+ fontWeight: isStage ? 600 : 400,
2466
+ color: isError ? theme.error : isDecision ? theme.warning : isStage ? theme.textPrimary : theme.textSecondary,
2467
+ lineHeight: 1.6,
2468
+ fontFamily: entry.type === "step" ? theme.fontMono : theme.fontSans
2469
+ },
2470
+ children: entry.text
2471
+ }
2472
+ )
2473
+ ]
2474
+ },
2475
+ i
2476
+ );
2477
+ }),
2478
+ future.length > 0 && /* @__PURE__ */ jsxs11("div", { style: {
2479
+ opacity: 0.3,
2480
+ fontSize: fs.small,
2481
+ color: theme.textMuted,
2482
+ padding: `8px 0`,
2483
+ fontStyle: "italic"
2484
+ }, children: [
2485
+ future.length,
2486
+ " more ",
2487
+ future.length === 1 ? "entry" : "entries",
2488
+ " ahead..."
2375
2489
  ] })
2376
2490
  ]
2377
2491
  }
2378
2492
  );
2379
2493
  }
2380
2494
 
2495
+ // src/components/NarrativePanel/NarrativePanel.tsx
2496
+ import { jsx as jsx13, jsxs as jsxs12 } from "react/jsx-runtime";
2497
+ function NarrativePanel({
2498
+ snapshots,
2499
+ selectedIndex,
2500
+ narrativeEntries,
2501
+ narrative: narrativeProp,
2502
+ size = "default",
2503
+ unstyled = false,
2504
+ className,
2505
+ style
2506
+ }) {
2507
+ const fs = fontSize[size];
2508
+ const pad = padding[size];
2509
+ const narrative = useMemo8(() => {
2510
+ if (narrativeProp && narrativeProp.length > 0) return narrativeProp;
2511
+ const lines = [];
2512
+ for (const snap of snapshots) {
2513
+ const stageLines = (snap.narrative ?? "").split("\n").filter(Boolean);
2514
+ lines.push(...stageLines);
2515
+ }
2516
+ return lines;
2517
+ }, [narrativeProp, snapshots]);
2518
+ const revealedCount = useMemo8(() => {
2519
+ if (snapshots.length === 0 || narrative.length === 0) return narrative.length;
2520
+ const stageBoundaries = [];
2521
+ for (let i = 0; i < narrative.length; i++) {
2522
+ const trimmed = narrative[i].trimStart();
2523
+ if (trimmed.startsWith("Stage ") && !trimmed.match(/^Stage\s+\d+:\s*Step\s/)) {
2524
+ stageBoundaries.push(i);
2525
+ }
2526
+ }
2527
+ if (stageBoundaries.length === 0) {
2528
+ const ratio = (selectedIndex + 1) / snapshots.length;
2529
+ return Math.max(1, Math.ceil(narrative.length * ratio));
2530
+ }
2531
+ const groupsToShow = Math.min(selectedIndex + 1, stageBoundaries.length);
2532
+ const endIdx = groupsToShow < stageBoundaries.length ? stageBoundaries[groupsToShow] : narrative.length;
2533
+ return Math.max(1, endIdx);
2534
+ }, [snapshots.length, selectedIndex, narrative]);
2535
+ const hasStructured = narrativeEntries && narrativeEntries.length > 0;
2536
+ if (unstyled) {
2537
+ return /* @__PURE__ */ jsx13("div", { className, style, "data-fp": "narrative-panel", children: hasStructured ? /* @__PURE__ */ jsx13(StoryNarrative, { entries: narrativeEntries, stageCount: selectedIndex + 1, unstyled: true }) : /* @__PURE__ */ jsx13(NarrativeTrace, { narrative, revealedCount, unstyled: true }) });
2538
+ }
2539
+ return /* @__PURE__ */ jsxs12(
2540
+ "div",
2541
+ {
2542
+ className,
2543
+ style: {
2544
+ overflow: "auto",
2545
+ display: "flex",
2546
+ flexDirection: "column",
2547
+ ...style
2548
+ },
2549
+ "data-fp": "narrative-panel",
2550
+ children: [
2551
+ /* @__PURE__ */ jsx13(
2552
+ "div",
2553
+ {
2554
+ style: {
2555
+ padding: `${pad - 4}px ${pad}px`,
2556
+ fontSize: fs.small,
2557
+ color: theme.textMuted,
2558
+ fontStyle: "italic",
2559
+ borderBottom: `1px solid ${theme.border}`,
2560
+ flexShrink: 0
2561
+ },
2562
+ children: "What happened at each stage, what data flowed, what decisions were made, and why."
2563
+ }
2564
+ ),
2565
+ hasStructured ? /* @__PURE__ */ jsx13(
2566
+ StoryNarrative,
2567
+ {
2568
+ entries: narrativeEntries,
2569
+ stageCount: selectedIndex + 1,
2570
+ size,
2571
+ style: { flex: 1 }
2572
+ }
2573
+ ) : /* @__PURE__ */ jsx13(
2574
+ NarrativeTrace,
2575
+ {
2576
+ narrative,
2577
+ revealedCount,
2578
+ size,
2579
+ style: { flex: 1 }
2580
+ }
2581
+ )
2582
+ ]
2583
+ }
2584
+ );
2585
+ }
2586
+
2381
2587
  // src/components/FlowchartView/SubflowTree.tsx
2382
- import { memo, useState as useState8, useCallback as useCallback5, useMemo as useMemo8 } from "react";
2383
- import { Fragment as Fragment4, jsx as jsx12, jsxs as jsxs11 } from "react/jsx-runtime";
2588
+ import { memo, useState as useState7, useCallback as useCallback4, useMemo as useMemo9 } from "react";
2589
+ import { Fragment as Fragment3, jsx as jsx14, jsxs as jsxs13 } from "react/jsx-runtime";
2384
2590
  function specToTree(node) {
2591
+ if (!node) return [];
2385
2592
  const entries = [];
2386
2593
  const seen = /* @__PURE__ */ new Set();
2387
2594
  function walk(n) {
2595
+ if (!n) return;
2388
2596
  const id = n.name || n.id || "";
2389
2597
  if (seen.has(id)) return;
2390
2598
  seen.add(id);
@@ -2400,7 +2608,7 @@ function specToTree(node) {
2400
2608
  entries.push(entry);
2401
2609
  if (n.children) {
2402
2610
  for (const child of n.children) {
2403
- walk(child);
2611
+ if (child) walk(child);
2404
2612
  }
2405
2613
  }
2406
2614
  if (n.next) {
@@ -2417,18 +2625,18 @@ var TreeNode = memo(function TreeNode2({
2417
2625
  doneStages,
2418
2626
  onNodeSelect
2419
2627
  }) {
2420
- const [expanded, setExpanded] = useState8(true);
2628
+ const [expanded, setExpanded] = useState7(true);
2421
2629
  const hasChildren = entry.children && entry.children.length > 0;
2422
2630
  const isActive = activeStage === entry.name;
2423
2631
  const isDone = doneStages?.has(entry.name);
2424
- const handleClick = useCallback5(() => {
2632
+ const handleClick = useCallback4(() => {
2425
2633
  if (hasChildren) {
2426
2634
  setExpanded((prev) => !prev);
2427
2635
  }
2428
2636
  onNodeSelect?.(entry.name, !!entry.isSubflow);
2429
2637
  }, [hasChildren, onNodeSelect, entry.name, entry.isSubflow]);
2430
- return /* @__PURE__ */ jsxs11(Fragment4, { children: [
2431
- /* @__PURE__ */ jsxs11(
2638
+ return /* @__PURE__ */ jsxs13(Fragment3, { children: [
2639
+ /* @__PURE__ */ jsxs13(
2432
2640
  "button",
2433
2641
  {
2434
2642
  onClick: handleClick,
@@ -2459,7 +2667,7 @@ var TreeNode = memo(function TreeNode2({
2459
2667
  }
2460
2668
  },
2461
2669
  children: [
2462
- hasChildren ? /* @__PURE__ */ jsx12(
2670
+ hasChildren ? /* @__PURE__ */ jsx14(
2463
2671
  "span",
2464
2672
  {
2465
2673
  style: {
@@ -2474,8 +2682,8 @@ var TreeNode = memo(function TreeNode2({
2474
2682
  },
2475
2683
  children: "\u25B6"
2476
2684
  }
2477
- ) : /* @__PURE__ */ jsx12("span", { style: { width: 12, flexShrink: 0 } }),
2478
- /* @__PURE__ */ jsx12(
2685
+ ) : /* @__PURE__ */ jsx14("span", { style: { width: 12, flexShrink: 0 } }),
2686
+ /* @__PURE__ */ jsx14(
2479
2687
  "span",
2480
2688
  {
2481
2689
  style: {
@@ -2487,8 +2695,8 @@ var TreeNode = memo(function TreeNode2({
2487
2695
  }
2488
2696
  }
2489
2697
  ),
2490
- /* @__PURE__ */ jsxs11("span", { style: { display: "flex", flexDirection: "column", minWidth: 0 }, children: [
2491
- /* @__PURE__ */ jsxs11(
2698
+ /* @__PURE__ */ jsxs13("span", { style: { display: "flex", flexDirection: "column", minWidth: 0 }, children: [
2699
+ /* @__PURE__ */ jsxs13(
2492
2700
  "span",
2493
2701
  {
2494
2702
  style: {
@@ -2500,11 +2708,11 @@ var TreeNode = memo(function TreeNode2({
2500
2708
  },
2501
2709
  children: [
2502
2710
  entry.name,
2503
- entry.isSubflow && /* @__PURE__ */ jsx12("span", { style: { opacity: 0.5, marginLeft: 4, fontSize: 10 }, children: "\u229E" })
2711
+ entry.isSubflow && /* @__PURE__ */ jsx14("span", { style: { opacity: 0.5, marginLeft: 4, fontSize: 10 }, children: "\u229E" })
2504
2712
  ]
2505
2713
  }
2506
2714
  ),
2507
- entry.description && /* @__PURE__ */ jsx12(
2715
+ entry.description && /* @__PURE__ */ jsx14(
2508
2716
  "span",
2509
2717
  {
2510
2718
  style: {
@@ -2521,7 +2729,7 @@ var TreeNode = memo(function TreeNode2({
2521
2729
  ]
2522
2730
  }
2523
2731
  ),
2524
- hasChildren && expanded && /* @__PURE__ */ jsx12("div", { children: entry.children.map((child, i) => /* @__PURE__ */ jsx12(
2732
+ hasChildren && expanded && /* @__PURE__ */ jsx14("div", { children: entry.children.map((child, i) => /* @__PURE__ */ jsx14(
2525
2733
  TreeNode2,
2526
2734
  {
2527
2735
  entry: child,
@@ -2530,12 +2738,12 @@ var TreeNode = memo(function TreeNode2({
2530
2738
  doneStages,
2531
2739
  onNodeSelect
2532
2740
  },
2533
- `${child.name}-${i}`
2741
+ child.subflowId ?? `${child.name}-${i}`
2534
2742
  )) })
2535
2743
  ] });
2536
2744
  });
2537
2745
  var SectionLabel = memo(function SectionLabel2({ children }) {
2538
- return /* @__PURE__ */ jsx12(
2746
+ return /* @__PURE__ */ jsx14(
2539
2747
  "div",
2540
2748
  {
2541
2749
  style: {
@@ -2559,10 +2767,10 @@ var SubflowTree = memo(function SubflowTree2({
2559
2767
  className,
2560
2768
  style
2561
2769
  }) {
2562
- const tree = useMemo8(() => specToTree(spec), [spec]);
2563
- const subflowStages = useMemo8(() => tree.filter((e) => e.isSubflow), [tree]);
2770
+ const tree = useMemo9(() => specToTree(spec), [spec]);
2771
+ const subflowStages = useMemo9(() => tree.filter((e) => e.isSubflow), [tree]);
2564
2772
  if (subflowStages.length === 0) return null;
2565
- return /* @__PURE__ */ jsxs11(
2773
+ return /* @__PURE__ */ jsxs13(
2566
2774
  "div",
2567
2775
  {
2568
2776
  className,
@@ -2580,8 +2788,8 @@ var SubflowTree = memo(function SubflowTree2({
2580
2788
  ...style
2581
2789
  },
2582
2790
  children: [
2583
- !unstyled && /* @__PURE__ */ jsx12(SectionLabel, { children: "Subflows" }),
2584
- subflowStages.map((entry, i) => /* @__PURE__ */ jsx12(
2791
+ !unstyled && /* @__PURE__ */ jsx14(SectionLabel, { children: "Subflows" }),
2792
+ subflowStages.map((entry, i) => /* @__PURE__ */ jsx14(
2585
2793
  TreeNode,
2586
2794
  {
2587
2795
  entry,
@@ -2590,116 +2798,625 @@ var SubflowTree = memo(function SubflowTree2({
2590
2798
  doneStages,
2591
2799
  onNodeSelect
2592
2800
  },
2593
- `${entry.name}-${i}`
2801
+ entry.subflowId ?? `${entry.name}-${i}`
2594
2802
  ))
2595
2803
  ]
2596
2804
  }
2597
2805
  );
2598
2806
  });
2599
2807
 
2600
- // src/adapters/fromRuntimeSnapshot.ts
2601
- function toVisualizationSnapshots(runtime, narrativeEntries) {
2602
- const stageNarrativeMap = narrativeEntries?.length ? buildStageNarrativeMap(narrativeEntries) : /* @__PURE__ */ new Map();
2603
- const snapshots = [];
2604
- flattenTree(runtime.executionTree, snapshots, runtime.sharedState, 0, runtime.subflowResults, {}, stageNarrativeMap);
2605
- return snapshots;
2808
+ // src/components/FlowchartView/SubflowBreadcrumb.tsx
2809
+ import { memo as memo2 } from "react";
2810
+ import { jsx as jsx15, jsxs as jsxs14 } from "react/jsx-runtime";
2811
+ var SubflowBreadcrumb = memo2(function SubflowBreadcrumb2({
2812
+ breadcrumbs,
2813
+ onNavigate
2814
+ }) {
2815
+ if (breadcrumbs.length <= 1) return null;
2816
+ return /* @__PURE__ */ jsx15(
2817
+ "div",
2818
+ {
2819
+ style: {
2820
+ display: "flex",
2821
+ alignItems: "center",
2822
+ gap: 4,
2823
+ padding: "6px 12px",
2824
+ background: theme.bgSecondary,
2825
+ borderBottom: `1px solid ${theme.border}`,
2826
+ fontSize: 12,
2827
+ fontFamily: theme.fontSans,
2828
+ flexShrink: 0,
2829
+ overflowX: "auto"
2830
+ },
2831
+ children: breadcrumbs.map((crumb, i) => {
2832
+ const isLast = i === breadcrumbs.length - 1;
2833
+ return /* @__PURE__ */ jsxs14("span", { style: { display: "flex", alignItems: "center", gap: 4 }, children: [
2834
+ i > 0 && /* @__PURE__ */ jsx15("span", { style: { color: theme.textMuted, fontSize: 10 }, children: "\u203A" }),
2835
+ isLast ? /* @__PURE__ */ jsxs14("span", { style: { display: "flex", alignItems: "center", gap: 6 }, children: [
2836
+ /* @__PURE__ */ jsx15(
2837
+ "span",
2838
+ {
2839
+ style: {
2840
+ color: theme.primary,
2841
+ fontWeight: 600
2842
+ },
2843
+ children: crumb.label
2844
+ }
2845
+ ),
2846
+ crumb.description && /* @__PURE__ */ jsxs14(
2847
+ "span",
2848
+ {
2849
+ style: {
2850
+ color: theme.textMuted,
2851
+ fontWeight: 400,
2852
+ fontSize: 11
2853
+ },
2854
+ children: [
2855
+ "\u2014 ",
2856
+ crumb.description
2857
+ ]
2858
+ }
2859
+ )
2860
+ ] }) : /* @__PURE__ */ jsx15(
2861
+ "button",
2862
+ {
2863
+ onClick: () => onNavigate(i),
2864
+ style: {
2865
+ background: "none",
2866
+ border: "none",
2867
+ color: theme.textSecondary,
2868
+ cursor: "pointer",
2869
+ padding: "2px 4px",
2870
+ borderRadius: 4,
2871
+ fontSize: 12,
2872
+ fontFamily: "inherit",
2873
+ fontWeight: 500,
2874
+ transition: "color 0.15s"
2875
+ },
2876
+ onMouseEnter: (e) => {
2877
+ e.currentTarget.style.color = `${theme.primary}`;
2878
+ },
2879
+ onMouseLeave: (e) => {
2880
+ e.currentTarget.style.color = `${theme.textSecondary}`;
2881
+ },
2882
+ children: crumb.label
2883
+ }
2884
+ )
2885
+ ] }, `${crumb.label}-${i}`);
2886
+ })
2887
+ }
2888
+ );
2889
+ });
2890
+
2891
+ // src/components/ExplainableShell/ExplainableShell.tsx
2892
+ import { Fragment as Fragment4, jsx as jsx16, jsxs as jsxs15 } from "react/jsx-runtime";
2893
+ var HLinePill = memo3(function HLinePill2({
2894
+ label,
2895
+ detail,
2896
+ expanded,
2897
+ onClick
2898
+ }) {
2899
+ return /* @__PURE__ */ jsxs15("div", { style: {
2900
+ display: "flex",
2901
+ alignItems: "center",
2902
+ gap: 0,
2903
+ padding: "0"
2904
+ }, children: [
2905
+ /* @__PURE__ */ jsx16("div", { style: { flex: 1, height: 1, background: theme.border } }),
2906
+ /* @__PURE__ */ jsxs15(
2907
+ "button",
2908
+ {
2909
+ onClick,
2910
+ style: {
2911
+ display: "flex",
2912
+ alignItems: "center",
2913
+ gap: 5,
2914
+ padding: "3px 12px",
2915
+ margin: "4px 0",
2916
+ fontSize: 10,
2917
+ fontWeight: 600,
2918
+ fontFamily: "inherit",
2919
+ color: theme.textMuted,
2920
+ background: theme.bgSecondary,
2921
+ border: `1px solid ${theme.border}`,
2922
+ borderRadius: 10,
2923
+ cursor: "pointer",
2924
+ whiteSpace: "nowrap",
2925
+ letterSpacing: "0.04em",
2926
+ textTransform: "uppercase",
2927
+ transition: "color 0.15s ease"
2928
+ },
2929
+ children: [
2930
+ /* @__PURE__ */ jsx16("span", { style: { fontSize: 7 }, children: expanded ? "\u25BC" : "\u25B6" }),
2931
+ label,
2932
+ detail && /* @__PURE__ */ jsx16("span", { style: { fontWeight: 400, opacity: 0.5, fontSize: 9 }, children: detail })
2933
+ ]
2934
+ }
2935
+ ),
2936
+ /* @__PURE__ */ jsx16("div", { style: { flex: 1, height: 1, background: theme.border } })
2937
+ ] });
2938
+ });
2939
+ var VLinePill = memo3(function VLinePill2({
2940
+ label,
2941
+ expanded,
2942
+ side = "right",
2943
+ onClick
2944
+ }) {
2945
+ const arrow = side === "right" ? expanded ? "\u25B6" : "\u25C0" : expanded ? "\u25C0" : "\u25B6";
2946
+ return /* @__PURE__ */ jsxs15("div", { style: {
2947
+ display: "flex",
2948
+ flexDirection: "column",
2949
+ alignItems: "center",
2950
+ gap: 0,
2951
+ padding: "0"
2952
+ }, children: [
2953
+ /* @__PURE__ */ jsx16("div", { style: { flex: 1, width: 1, background: theme.border } }),
2954
+ /* @__PURE__ */ jsxs15(
2955
+ "button",
2956
+ {
2957
+ onClick,
2958
+ style: {
2959
+ display: "flex",
2960
+ alignItems: "center",
2961
+ gap: 4,
2962
+ padding: "10px 4px",
2963
+ margin: "0 3px",
2964
+ fontSize: 10,
2965
+ fontWeight: 600,
2966
+ fontFamily: "inherit",
2967
+ color: theme.textMuted,
2968
+ background: theme.bgSecondary,
2969
+ border: `1px solid ${theme.border}`,
2970
+ borderRadius: 10,
2971
+ cursor: "pointer",
2972
+ whiteSpace: "nowrap",
2973
+ letterSpacing: "0.04em",
2974
+ textTransform: "uppercase",
2975
+ writingMode: "vertical-lr",
2976
+ transition: "color 0.15s ease"
2977
+ },
2978
+ children: [
2979
+ /* @__PURE__ */ jsx16("span", { style: { fontSize: 7, writingMode: "horizontal-tb" }, children: arrow }),
2980
+ label
2981
+ ]
2982
+ }
2983
+ ),
2984
+ /* @__PURE__ */ jsx16("div", { style: { flex: 1, width: 1, background: theme.border } })
2985
+ ] });
2986
+ });
2987
+ var RIGHT_PANEL_LABELS = {
2988
+ memory: "Memory",
2989
+ narrative: "Narrative"
2990
+ };
2991
+ var DetailsContent = memo3(function DetailsContent2({
2992
+ snapshots,
2993
+ selectedIndex,
2994
+ narrativeEntries,
2995
+ narrative,
2996
+ size,
2997
+ fillHeight
2998
+ }) {
2999
+ const [rightPanel, setRightPanel] = useState8("memory");
3000
+ return /* @__PURE__ */ jsxs15("div", { style: { flex: 1, display: "flex", flexDirection: "column", overflow: "hidden" }, children: [
3001
+ /* @__PURE__ */ jsx16("div", { style: { display: "flex", borderBottom: `1px solid ${theme.border}`, flexShrink: 0 }, children: ["memory", "narrative"].map((panel) => {
3002
+ const active = rightPanel === panel;
3003
+ return /* @__PURE__ */ jsx16(
3004
+ "button",
3005
+ {
3006
+ onClick: () => setRightPanel(panel),
3007
+ style: {
3008
+ flex: 1,
3009
+ padding: "6px 8px",
3010
+ fontSize: 11,
3011
+ fontWeight: active ? 600 : 400,
3012
+ color: active ? theme.primary : theme.textMuted,
3013
+ background: active ? `color-mix(in srgb, ${theme.primary} 8%, transparent)` : "transparent",
3014
+ border: "none",
3015
+ borderBottom: active ? `2px solid ${theme.primary}` : "2px solid transparent",
3016
+ cursor: "pointer",
3017
+ textTransform: "uppercase",
3018
+ letterSpacing: "0.06em",
3019
+ fontFamily: "inherit"
3020
+ },
3021
+ children: RIGHT_PANEL_LABELS[panel]
3022
+ },
3023
+ panel
3024
+ );
3025
+ }) }),
3026
+ /* @__PURE__ */ jsxs15("div", { style: { flex: 1, overflow: "auto" }, children: [
3027
+ rightPanel === "memory" && /* @__PURE__ */ jsx16(MemoryPanel, { snapshots, selectedIndex, size, style: fillHeight ? { height: "100%" } : void 0 }),
3028
+ rightPanel === "narrative" && /* @__PURE__ */ jsx16(NarrativePanel, { snapshots, selectedIndex, narrativeEntries, narrative, size, style: fillHeight ? { height: "100%" } : void 0 })
3029
+ ] })
3030
+ ] });
3031
+ });
3032
+ function resolveSubflowLevel(parentSpec, parentSnapshots, subflowNodeName, narrativeEntries) {
3033
+ const specNode = findSubflowSpecNode(parentSpec, subflowNodeName);
3034
+ if (!specNode?.subflowStructure) return null;
3035
+ const parentSnap = parentSnapshots.find(
3036
+ (s) => s.stageName === subflowNodeName || s.stageLabel === subflowNodeName
3037
+ );
3038
+ if (!parentSnap?.subflowResult) return null;
3039
+ const sfNarrative = narrativeEntries ? extractSubflowNarrative(narrativeEntries, subflowNodeName) : void 0;
3040
+ const sfSnapshots = subflowResultToSnapshots(parentSnap.subflowResult, sfNarrative);
3041
+ if (sfSnapshots.length === 0) return null;
3042
+ return {
3043
+ subflowId: specNode.subflowId ?? subflowNodeName,
3044
+ label: specNode.subflowName ?? specNode.name,
3045
+ spec: specNode.subflowStructure,
3046
+ snapshots: sfSnapshots
3047
+ };
2606
3048
  }
2607
- function buildStageNarrativeMap(entries) {
2608
- const map = /* @__PURE__ */ new Map();
2609
- let currentStageName;
3049
+ function extractSubflowNarrative(entries, subflowName) {
3050
+ const result = [];
3051
+ let inside = false;
2610
3052
  for (const entry of entries) {
2611
- if (entry.stageName) {
2612
- currentStageName = entry.stageName;
3053
+ if (entry.type === "subflow" && entry.text.includes(subflowName) && entry.text.startsWith("Entering")) {
3054
+ inside = true;
3055
+ continue;
2613
3056
  }
2614
- if (currentStageName) {
2615
- if (!map.has(currentStageName)) {
2616
- map.set(currentStageName, []);
2617
- }
2618
- const indent = " ".repeat(entry.depth);
2619
- map.get(currentStageName).push(`${indent}${entry.text}`);
3057
+ if (inside && entry.type === "subflow" && entry.text.includes(subflowName) && entry.text.startsWith("Exiting")) break;
3058
+ if (inside) result.push(entry);
3059
+ }
3060
+ return result;
3061
+ }
3062
+ function findSubflowSpecNode(node, name) {
3063
+ if ((node.name === name || node.id === name) && node.isSubflowRoot) return node;
3064
+ if (node.children) {
3065
+ for (const child of node.children) {
3066
+ const f = findSubflowSpecNode(child, name);
3067
+ if (f) return f;
2620
3068
  }
2621
3069
  }
2622
- return map;
3070
+ if (node.next) return findSubflowSpecNode(node.next, name);
3071
+ return null;
2623
3072
  }
2624
- function flattenTree(node, out, sharedState, accumulatedMs = 0, subflowResults, cumulativeMemory = {}, stageNarrativeMap = /* @__PURE__ */ new Map()) {
2625
- const durationMs = typeof node.metrics?.durationMs === "number" ? node.metrics.durationMs : 1;
2626
- const startMs = accumulatedMs;
2627
- const stageId = node.name || node.id;
2628
- const stageLines = stageNarrativeMap.get(stageId);
2629
- const narrative = stageLines ? stageLines.join("\n") : "Narrative not part of this run.";
2630
- const memory = { ...cumulativeMemory };
2631
- if (node.stageWrites) {
2632
- for (const [key, value] of Object.entries(node.stageWrites)) {
2633
- if (value === void 0) {
2634
- delete memory[key];
3073
+ function hasSubflowNodes(node) {
3074
+ if (!node) return false;
3075
+ if (node.isSubflowRoot) return true;
3076
+ if (node.children?.some((c) => c && hasSubflowNodes(c))) return true;
3077
+ if (node.next && hasSubflowNodes(node.next)) return true;
3078
+ return false;
3079
+ }
3080
+ function ExplainableShell({
3081
+ snapshots,
3082
+ spec,
3083
+ title,
3084
+ resultData,
3085
+ logs = [],
3086
+ narrative,
3087
+ narrativeEntries,
3088
+ tabs = ["result", "explainable"],
3089
+ defaultTab,
3090
+ hideConsole = false,
3091
+ panelLabels,
3092
+ defaultExpanded,
3093
+ renderFlowchart,
3094
+ size = "default",
3095
+ unstyled = false,
3096
+ className,
3097
+ style
3098
+ }) {
3099
+ const leftLabel = panelLabels?.topology ?? "Topology";
3100
+ const rightLabel = panelLabels?.details ?? "Details";
3101
+ const bottomLabel = panelLabels?.timeline ?? "Timeline";
3102
+ const shellRef = useRef6(null);
3103
+ const [isNarrow, setIsNarrow] = useState8(false);
3104
+ useEffect6(() => {
3105
+ const el = shellRef.current;
3106
+ if (!el) return;
3107
+ const ro = new ResizeObserver(([entry]) => {
3108
+ setIsNarrow(entry.contentRect.width < 640);
3109
+ });
3110
+ ro.observe(el);
3111
+ return () => ro.disconnect();
3112
+ }, []);
3113
+ const [activeTab, setActiveTab] = useState8(defaultTab ?? tabs[0]);
3114
+ const [snapshotIdx, setSnapshotIdx] = useState8(0);
3115
+ const [drillDownStack, setDrillDownStack] = useState8([]);
3116
+ const [rightExpanded, setRightExpanded] = useState8(defaultExpanded?.details ?? true);
3117
+ const [leftExpanded, setLeftExpanded] = useState8(defaultExpanded?.topology ?? false);
3118
+ const [timelineExpanded, setTimelineExpanded] = useState8(defaultExpanded?.timeline ?? false);
3119
+ useEffect6(() => {
3120
+ if (isNarrow) {
3121
+ setLeftExpanded(false);
3122
+ setRightExpanded(false);
3123
+ setTimelineExpanded(false);
3124
+ }
3125
+ }, [isNarrow]);
3126
+ const triggerReflow = useCallback5(() => {
3127
+ requestAnimationFrame(() => window.dispatchEvent(new Event("resize")));
3128
+ }, []);
3129
+ const toggleLeft = useCallback5((v2) => {
3130
+ setLeftExpanded(v2);
3131
+ triggerReflow();
3132
+ }, [triggerReflow]);
3133
+ const toggleRight = useCallback5((v2) => {
3134
+ setRightExpanded(v2);
3135
+ triggerReflow();
3136
+ }, [triggerReflow]);
3137
+ const toggleTimeline = useCallback5(() => {
3138
+ setTimelineExpanded((p) => !p);
3139
+ triggerReflow();
3140
+ }, [triggerReflow]);
3141
+ const isInSubflow = drillDownStack.length > 0;
3142
+ const currentLevel = useMemo10(() => {
3143
+ if (drillDownStack.length > 0) {
3144
+ const top = drillDownStack[drillDownStack.length - 1];
3145
+ return { spec: top.spec, snapshots: top.snapshots };
3146
+ }
3147
+ return { spec: spec ?? null, snapshots };
3148
+ }, [drillDownStack, spec, snapshots]);
3149
+ const activeSnapshots = currentLevel.snapshots;
3150
+ const activeSpec = currentLevel.spec;
3151
+ const safeIdx = activeSnapshots.length > 0 ? Math.max(0, Math.min(snapshotIdx, activeSnapshots.length - 1)) : 0;
3152
+ const activeNarrative = useMemo10(() => {
3153
+ if (!isInSubflow) return narrative;
3154
+ const lines = [];
3155
+ for (const snap of activeSnapshots) {
3156
+ const stageLines = (snap.narrative ?? "").split("\n").filter(Boolean);
3157
+ lines.push(...stageLines);
3158
+ }
3159
+ return lines.length > 0 ? lines : void 0;
3160
+ }, [isInSubflow, narrative, activeSnapshots]);
3161
+ const activeNarrativeEntries = isInSubflow ? void 0 : narrativeEntries;
3162
+ const breadcrumbs = useMemo10(() => {
3163
+ const root = { label: title || "Flowchart", spec, description: spec?.description };
3164
+ return [root, ...drillDownStack.map((e) => ({ label: e.label, spec: e.spec, description: void 0 }))];
3165
+ }, [spec, title, drillDownStack]);
3166
+ const showTreeSidebar = useMemo10(() => !!spec && hasSubflowNodes(spec), [spec]);
3167
+ const rootOverlay = useMemo10(() => {
3168
+ if (isInSubflow || !snapshots.length) return { activeStage: void 0, doneStages: void 0 };
3169
+ const doneStages = new Set(snapshots.slice(0, safeIdx).map((s) => s.stageLabel));
3170
+ const activeStage = snapshots[safeIdx]?.stageLabel ?? null;
3171
+ return { activeStage, doneStages };
3172
+ }, [isInSubflow, snapshots, safeIdx]);
3173
+ const handleTabChange = useCallback5((tab) => {
3174
+ setActiveTab(tab);
3175
+ setDrillDownStack([]);
3176
+ setSnapshotIdx(999);
3177
+ }, []);
3178
+ const handleSnapshotChange = useCallback5((idx) => {
3179
+ if (typeof idx === "number") setSnapshotIdx(idx);
3180
+ }, []);
3181
+ const handleDrillDown = useCallback5(
3182
+ (nodeName) => {
3183
+ if (!activeSpec) return;
3184
+ const entry = resolveSubflowLevel(activeSpec, activeSnapshots, nodeName, narrativeEntries);
3185
+ if (entry) {
3186
+ setDrillDownStack((prev) => [...prev, { ...entry, parentSnapshotIdx: snapshotIdx }]);
3187
+ setSnapshotIdx(0);
3188
+ }
3189
+ },
3190
+ [activeSpec, activeSnapshots, narrativeEntries, snapshotIdx]
3191
+ );
3192
+ const handleBreadcrumbNavigate = useCallback5((level) => {
3193
+ setDrillDownStack((prev) => {
3194
+ const popped = level === 0 ? prev[0] : prev[level];
3195
+ if (popped) setSnapshotIdx(popped.parentSnapshotIdx);
3196
+ return level === 0 ? [] : prev.slice(0, level);
3197
+ });
3198
+ }, []);
3199
+ const handleNodeClick = useCallback5(
3200
+ (indexOrId) => {
3201
+ if (typeof indexOrId === "number") {
3202
+ setSnapshotIdx(indexOrId);
3203
+ return;
3204
+ }
3205
+ if (activeSpec) {
3206
+ const sfNode = findSubflowSpecNode(activeSpec, indexOrId);
3207
+ if (sfNode?.subflowStructure) {
3208
+ handleDrillDown(indexOrId);
3209
+ return;
3210
+ }
3211
+ }
3212
+ const idx = activeSnapshots.findIndex((s) => s.stageLabel === indexOrId);
3213
+ if (idx >= 0) setSnapshotIdx(idx);
3214
+ },
3215
+ [activeSpec, activeSnapshots, handleDrillDown]
3216
+ );
3217
+ const handleTreeNodeSelect = useCallback5(
3218
+ (name, isSubflow) => {
3219
+ if (isSubflow && spec) {
3220
+ setDrillDownStack([]);
3221
+ const entry = resolveSubflowLevel(spec, snapshots, name, narrativeEntries);
3222
+ if (entry) {
3223
+ setDrillDownStack([{ ...entry, parentSnapshotIdx: snapshotIdx }]);
3224
+ setSnapshotIdx(0);
3225
+ }
2635
3226
  } else {
2636
- memory[key] = value;
3227
+ setDrillDownStack([]);
3228
+ const idx = snapshots.findIndex((s) => s.stageLabel === name);
3229
+ if (idx >= 0) setSnapshotIdx(idx);
2637
3230
  }
2638
- }
3231
+ },
3232
+ [spec, snapshots, narrativeEntries, snapshotIdx]
3233
+ );
3234
+ const tabLabels = {
3235
+ result: "Result",
3236
+ explainable: "Explainable",
3237
+ "ai-compatible": "AI-Compatible"
3238
+ };
3239
+ if (unstyled) {
3240
+ return /* @__PURE__ */ jsxs15("div", { className, style, "data-fp": "explainable-shell", children: [
3241
+ /* @__PURE__ */ jsx16("div", { "data-fp": "shell-tabs", children: tabs.map((tab) => /* @__PURE__ */ jsx16("button", { "data-fp": "shell-tab", "data-active": tab === activeTab, onClick: () => handleTabChange(tab), children: tabLabels[tab] }, tab)) }),
3242
+ /* @__PURE__ */ jsxs15("div", { "data-fp": "shell-content", "data-tab": activeTab, children: [
3243
+ activeTab === "result" && /* @__PURE__ */ jsx16(ResultPanel, { data: resultData ?? null, logs, hideConsole, unstyled: true }),
3244
+ (activeTab === "explainable" || activeTab === "ai-compatible") && /* @__PURE__ */ jsxs15(Fragment4, { children: [
3245
+ /* @__PURE__ */ jsx16(TimeTravelControls, { snapshots: activeSnapshots, selectedIndex: safeIdx, onIndexChange: handleSnapshotChange, unstyled: true }),
3246
+ isInSubflow && /* @__PURE__ */ jsx16(SubflowBreadcrumb, { breadcrumbs, onNavigate: handleBreadcrumbNavigate }),
3247
+ activeSpec && renderFlowchart?.({ spec: activeSpec, snapshots: activeSnapshots, selectedIndex: safeIdx, onNodeClick: handleNodeClick }),
3248
+ /* @__PURE__ */ jsx16(MemoryPanel, { snapshots: activeSnapshots, selectedIndex: safeIdx, unstyled: true }),
3249
+ /* @__PURE__ */ jsx16(NarrativePanel, { snapshots: activeSnapshots, selectedIndex: safeIdx, narrativeEntries: activeNarrativeEntries, narrative: activeNarrative, unstyled: true }),
3250
+ /* @__PURE__ */ jsx16(GanttTimeline, { snapshots: activeSnapshots, selectedIndex: safeIdx, onSelect: handleSnapshotChange, unstyled: true })
3251
+ ] })
3252
+ ] })
3253
+ ] });
2639
3254
  }
2640
- const sfResult = subflowResults?.[node.subflowId ?? stageId];
2641
- out.push({
2642
- stageName: stageId,
2643
- stageLabel: stageId,
2644
- memory,
2645
- narrative,
2646
- startMs,
2647
- durationMs,
2648
- status: "done",
2649
- ...node.description ? { description: node.description } : void 0,
2650
- ...node.subflowId ? { subflowId: node.subflowId } : void 0,
2651
- ...sfResult ? { subflowResult: sfResult } : void 0
2652
- });
2653
- let nextMs = startMs + durationMs;
2654
- if (node.children && node.children.length > 0) {
2655
- let maxChildEnd = nextMs;
2656
- for (const child of node.children) {
2657
- const childEnd = flattenTree(child, out, sharedState, nextMs, subflowResults, memory, stageNarrativeMap);
2658
- maxChildEnd = Math.max(maxChildEnd, childEnd);
3255
+ const isVisualizationTab = activeTab === "explainable" || activeTab === "ai-compatible";
3256
+ return /* @__PURE__ */ jsxs15(
3257
+ "div",
3258
+ {
3259
+ ref: shellRef,
3260
+ className,
3261
+ style: {
3262
+ height: "100%",
3263
+ display: "flex",
3264
+ flexDirection: "column",
3265
+ overflow: "hidden",
3266
+ background: theme.bgPrimary,
3267
+ color: theme.textPrimary,
3268
+ fontFamily: theme.fontSans,
3269
+ fontSize: 12,
3270
+ ...style
3271
+ },
3272
+ "data-fp": "explainable-shell",
3273
+ children: [
3274
+ tabs.length > 1 && /* @__PURE__ */ jsx16("div", { style: {
3275
+ display: "flex",
3276
+ borderBottom: `1px solid ${theme.border}`,
3277
+ background: theme.bgSecondary,
3278
+ flexShrink: 0
3279
+ }, children: tabs.map((tab) => {
3280
+ const active = tab === activeTab;
3281
+ return /* @__PURE__ */ jsx16(
3282
+ "button",
3283
+ {
3284
+ onClick: () => handleTabChange(tab),
3285
+ style: {
3286
+ padding: "6px 14px",
3287
+ fontSize: 11,
3288
+ fontWeight: active ? 700 : 500,
3289
+ textTransform: "uppercase",
3290
+ letterSpacing: "0.08em",
3291
+ color: active ? theme.primary : theme.textMuted,
3292
+ background: "transparent",
3293
+ border: "none",
3294
+ borderBottom: active ? `2px solid ${theme.primary}` : "2px solid transparent",
3295
+ cursor: "pointer",
3296
+ fontFamily: "inherit"
3297
+ },
3298
+ children: tabLabels[tab]
3299
+ },
3300
+ tab
3301
+ );
3302
+ }) }),
3303
+ /* @__PURE__ */ jsxs15("div", { style: { flex: 1, overflow: isNarrow ? "auto" : "hidden", display: "flex", flexDirection: "column" }, children: [
3304
+ activeTab === "result" && /* @__PURE__ */ jsx16(ResultPanel, { data: resultData ?? null, logs, hideConsole, size }),
3305
+ isVisualizationTab && /* @__PURE__ */ jsxs15(Fragment4, { children: [
3306
+ /* @__PURE__ */ jsx16(
3307
+ TimeTravelControls,
3308
+ {
3309
+ snapshots: activeSnapshots,
3310
+ selectedIndex: safeIdx,
3311
+ onIndexChange: handleSnapshotChange,
3312
+ size
3313
+ }
3314
+ ),
3315
+ isInSubflow && /* @__PURE__ */ jsx16(SubflowBreadcrumb, { breadcrumbs, onNavigate: handleBreadcrumbNavigate }),
3316
+ isNarrow ? (
3317
+ /* ── Mobile: stacked vertical ── */
3318
+ /* @__PURE__ */ jsxs15(Fragment4, { children: [
3319
+ /* @__PURE__ */ jsx16("div", { style: { height: 350, flexShrink: 0, overflow: "hidden" }, children: renderFlowchart && activeSpec && renderFlowchart({
3320
+ spec: activeSpec,
3321
+ snapshots: activeSnapshots,
3322
+ selectedIndex: safeIdx,
3323
+ onNodeClick: handleNodeClick
3324
+ }) }),
3325
+ showTreeSidebar && /* @__PURE__ */ jsxs15(Fragment4, { children: [
3326
+ /* @__PURE__ */ jsx16(HLinePill, { label: leftLabel, expanded: leftExpanded, onClick: () => toggleLeft(!leftExpanded) }),
3327
+ leftExpanded && /* @__PURE__ */ jsx16("div", { style: { maxHeight: 180, overflow: "auto", flexShrink: 0 }, children: /* @__PURE__ */ jsx16(
3328
+ SubflowTree,
3329
+ {
3330
+ spec,
3331
+ activeStage: rootOverlay.activeStage,
3332
+ doneStages: rootOverlay.doneStages,
3333
+ onNodeSelect: handleTreeNodeSelect
3334
+ }
3335
+ ) })
3336
+ ] }),
3337
+ /* @__PURE__ */ jsx16(HLinePill, { label: rightLabel, expanded: rightExpanded, onClick: () => toggleRight(!rightExpanded) }),
3338
+ rightExpanded && /* @__PURE__ */ jsx16("div", { style: { maxHeight: 250, flexShrink: 0, display: "flex", flexDirection: "column", overflow: "hidden" }, children: /* @__PURE__ */ jsx16(
3339
+ DetailsContent,
3340
+ {
3341
+ snapshots: activeSnapshots,
3342
+ selectedIndex: safeIdx,
3343
+ narrativeEntries: activeNarrativeEntries,
3344
+ narrative: activeNarrative,
3345
+ size
3346
+ }
3347
+ ) }),
3348
+ /* @__PURE__ */ jsx16(HLinePill, { label: bottomLabel, detail: `${activeSnapshots.length} stages`, expanded: timelineExpanded, onClick: toggleTimeline }),
3349
+ timelineExpanded && /* @__PURE__ */ jsx16("div", { style: { flexShrink: 0, overflow: "hidden" }, children: /* @__PURE__ */ jsx16(GanttTimeline, { snapshots: activeSnapshots, selectedIndex: safeIdx, onSelect: handleSnapshotChange, size }) })
3350
+ ] })
3351
+ ) : (
3352
+ /* ── Desktop: side-by-side ── */
3353
+ /* @__PURE__ */ jsxs15(Fragment4, { children: [
3354
+ /* @__PURE__ */ jsxs15("div", { style: { flex: 1, display: "flex", overflow: "hidden" }, children: [
3355
+ showTreeSidebar && (leftExpanded ? /* @__PURE__ */ jsxs15("div", { style: { width: 220, flexShrink: 0, display: "flex", flexDirection: "row", overflow: "hidden" }, children: [
3356
+ /* @__PURE__ */ jsx16("div", { style: { flex: 1, overflow: "auto" }, children: /* @__PURE__ */ jsx16(
3357
+ SubflowTree,
3358
+ {
3359
+ spec,
3360
+ activeStage: rootOverlay.activeStage,
3361
+ doneStages: rootOverlay.doneStages,
3362
+ onNodeSelect: handleTreeNodeSelect
3363
+ }
3364
+ ) }),
3365
+ /* @__PURE__ */ jsx16(VLinePill, { label: leftLabel, expanded: true, side: "left", onClick: () => toggleLeft(false) })
3366
+ ] }) : /* @__PURE__ */ jsx16(VLinePill, { label: leftLabel, expanded: false, side: "left", onClick: () => toggleLeft(true) })),
3367
+ /* @__PURE__ */ jsx16("div", { style: { flex: 1, overflow: "hidden", minWidth: 0 }, children: renderFlowchart && activeSpec && renderFlowchart({
3368
+ spec: activeSpec,
3369
+ snapshots: activeSnapshots,
3370
+ selectedIndex: safeIdx,
3371
+ onNodeClick: handleNodeClick
3372
+ }) }),
3373
+ rightExpanded ? /* @__PURE__ */ jsxs15("div", { style: { width: "38%", minWidth: 300, maxWidth: 500, display: "flex", flexDirection: "row", overflow: "hidden" }, children: [
3374
+ /* @__PURE__ */ jsx16(VLinePill, { label: rightLabel, expanded: true, onClick: () => toggleRight(false) }),
3375
+ /* @__PURE__ */ jsx16(
3376
+ DetailsContent,
3377
+ {
3378
+ snapshots: activeSnapshots,
3379
+ selectedIndex: safeIdx,
3380
+ narrativeEntries: activeNarrativeEntries,
3381
+ narrative: activeNarrative,
3382
+ size,
3383
+ fillHeight: true
3384
+ }
3385
+ )
3386
+ ] }) : /* @__PURE__ */ jsx16(VLinePill, { label: rightLabel, expanded: false, onClick: () => toggleRight(true) })
3387
+ ] }),
3388
+ /* @__PURE__ */ jsx16(HLinePill, { label: bottomLabel, detail: `${activeSnapshots.length} stages`, expanded: timelineExpanded, onClick: toggleTimeline }),
3389
+ timelineExpanded && /* @__PURE__ */ jsx16("div", { style: { flexShrink: 0, overflow: "hidden" }, children: /* @__PURE__ */ jsx16(GanttTimeline, { snapshots: activeSnapshots, selectedIndex: safeIdx, onSelect: handleSnapshotChange, size }) })
3390
+ ] })
3391
+ )
3392
+ ] })
3393
+ ] })
3394
+ ]
2659
3395
  }
2660
- nextMs = maxChildEnd;
2661
- }
2662
- if (node.next) {
2663
- nextMs = flattenTree(node.next, out, sharedState, nextMs, subflowResults, memory, stageNarrativeMap);
2664
- }
2665
- return nextMs;
2666
- }
2667
- function createSnapshots(stages) {
2668
- let accMs = 0;
2669
- return stages.map((s) => {
2670
- const duration = s.durationMs ?? 1;
2671
- const snap = {
2672
- stageName: s.name,
2673
- stageLabel: s.label ?? s.name,
2674
- memory: s.memory ?? {},
2675
- narrative: s.narrative ?? `${s.label ?? s.name} completed.`,
2676
- startMs: accMs,
2677
- durationMs: duration,
2678
- status: "done",
2679
- ...s.description ? { description: s.description } : void 0,
2680
- ...s.subflowId ? { subflowId: s.subflowId } : void 0
2681
- };
2682
- accMs += duration;
2683
- return snap;
2684
- });
3396
+ );
2685
3397
  }
2686
3398
  export {
2687
3399
  ExplainableShell,
2688
3400
  FootprintTheme,
2689
3401
  GanttTimeline,
2690
3402
  MemoryInspector,
3403
+ MemoryPanel,
2691
3404
  NarrativeLog,
3405
+ NarrativePanel,
2692
3406
  NarrativeTrace,
2693
3407
  ResultPanel,
2694
3408
  ScopeDiff,
2695
3409
  SnapshotPanel,
2696
3410
  StageDetailPanel,
3411
+ StoryNarrative,
2697
3412
  SubflowTree,
2698
3413
  TimeTravelControls,
2699
3414
  coolDark,
2700
3415
  coolLight,
2701
3416
  createSnapshots,
2702
3417
  defaultTokens,
3418
+ rawDefaults,
3419
+ subflowResultToSnapshots,
2703
3420
  themePresets,
2704
3421
  toVisualizationSnapshots,
2705
3422
  tokensToCSSVars,