@kiberon-labs/behave-graph-flow 2.0.0 → 3.0.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 (251) hide show
  1. package/.storybook/manager.ts +6 -0
  2. package/.storybook/preview.ts +49 -1
  3. package/.storybook/styles.css +9 -3
  4. package/.turbo/turbo-build.log +1 -1
  5. package/CHANGELOG.md +368 -0
  6. package/dist/AnyControlImpl-Ds-CShIB.js +20 -0
  7. package/dist/AnyControlImpl-Ds-CShIB.js.map +1 -0
  8. package/dist/DocumentationBrowserPanelImpl-deZNzFX8.js +166 -0
  9. package/dist/DocumentationBrowserPanelImpl-deZNzFX8.js.map +1 -0
  10. package/dist/index.css +36 -33
  11. package/dist/index.css.map +1 -1
  12. package/dist/index.d.ts +1865 -550
  13. package/dist/index.d.ts.map +1 -1
  14. package/dist/index.js +14357 -11221
  15. package/dist/index.js.map +1 -1
  16. package/dist/noteImpl-KkrrWgJd.js +242 -0
  17. package/dist/noteImpl-KkrrWgJd.js.map +1 -0
  18. package/dist/styles.module-CvmpDkZj.css +3 -0
  19. package/dist/styles.module-CvmpDkZj.css.map +1 -0
  20. package/dist/styles.module-DZxg8aW9.js +271 -0
  21. package/dist/styles.module-DZxg8aW9.js.map +1 -0
  22. package/dist/useChangeNodeData-ChQGK7AI.js +23 -0
  23. package/dist/useChangeNodeData-ChQGK7AI.js.map +1 -0
  24. package/docs/protocol.md +43 -20
  25. package/package.json +5 -9
  26. package/src/components/FloatingToolbar/index.module.css +5 -13
  27. package/src/components/FloatingToolbar/index.tsx +9 -9
  28. package/src/components/Flow.tsx +34 -23
  29. package/src/components/contextMenus/DynamicContextMenu.tsx +85 -0
  30. package/src/components/contextMenus/NodePicker.module.css +13 -13
  31. package/src/components/contextMenus/edge.tsx +9 -95
  32. package/src/components/contextMenus/node.tsx +9 -149
  33. package/src/components/contextMenus/selection.tsx +5 -71
  34. package/src/components/controls/any/AnyControlImpl.tsx +14 -0
  35. package/src/components/controls/any/index.tsx +13 -2
  36. package/src/components/edges/index.tsx +75 -69
  37. package/src/components/layoutController/index.module.css +3 -0
  38. package/src/components/layoutController/index.tsx +24 -1
  39. package/src/components/layoutController/utils.ts +46 -3
  40. package/src/components/menubar/defaults.tsx +55 -19
  41. package/src/components/menubar/menuItem.module.css +18 -3
  42. package/src/components/menubar/menuItem.tsx +34 -1
  43. package/src/components/nodes/behave/NodeContainer.module.css +26 -25
  44. package/src/components/nodes/group/index.tsx +3 -3
  45. package/src/components/nodes/wrapper/styles.module.css +6 -32
  46. package/src/components/panels/alignment/index.module.css +0 -10
  47. package/src/components/panels/alignment/index.tsx +4 -4
  48. package/src/components/panels/base/styles.module.css +2 -2
  49. package/src/components/panels/common/PanelHeader.module.css +24 -0
  50. package/src/components/panels/common/PanelHeader.tsx +22 -0
  51. package/src/components/panels/common/SectionTitle.module.css +13 -0
  52. package/src/components/panels/common/SectionTitle.tsx +10 -0
  53. package/src/components/panels/events/EditEventPanel.tsx +14 -5
  54. package/src/components/panels/events/ManageEventsPanel.tsx +11 -8
  55. package/src/components/panels/events/styles.module.css +6 -64
  56. package/src/components/panels/graphProperties/index.tsx +125 -0
  57. package/src/components/panels/history/index.tsx +2 -2
  58. package/src/components/panels/history/styles.module.css +0 -9
  59. package/src/components/panels/keymaps/index.module.css +3 -13
  60. package/src/components/panels/keymaps/index.tsx +1 -2
  61. package/src/components/panels/layers/index.tsx +20 -15
  62. package/src/components/panels/layers/styles.module.css +9 -12
  63. package/src/components/panels/legend/index.tsx +1 -1
  64. package/src/components/panels/logs/index.module.css +25 -19
  65. package/src/components/panels/logs/index.tsx +7 -7
  66. package/src/components/panels/nodeInputs/InputsGroup.tsx +1 -0
  67. package/src/components/panels/nodeInputs/NodeSettings.tsx +2 -2
  68. package/src/components/panels/nodeInputs/NodeTitleEditor.tsx +1 -1
  69. package/src/components/panels/nodeInputs/OutputsGroup.tsx +2 -12
  70. package/src/components/panels/nodeInputs/index.module.css +99 -75
  71. package/src/components/panels/nodeInputs/index.tsx +21 -11
  72. package/src/components/panels/nodeInputs/useNodeHandlers.ts +2 -2
  73. package/src/components/panels/nodeInputs/useNodeInputsData.ts +23 -43
  74. package/src/components/panels/nodePicker/index.tsx +8 -8
  75. package/src/components/panels/panel/index.module.css +7 -7
  76. package/src/components/panels/search/index.module.css +0 -50
  77. package/src/components/panels/search/index.tsx +2 -2
  78. package/src/components/panels/systemSettings/ConversionsSettings.tsx +203 -0
  79. package/src/components/panels/systemSettings/index.tsx +221 -176
  80. package/src/components/panels/systemSettings/styles.module.css +135 -8
  81. package/src/components/panels/traces/GridLines.tsx +1 -1
  82. package/src/components/panels/traces/TimeGrid.tsx +3 -3
  83. package/src/components/panels/traces/TraceLane.tsx +1 -1
  84. package/src/components/panels/traces/index.module.css +1 -8
  85. package/src/components/panels/traces/index.tsx +8 -4
  86. package/src/components/panels/traces/useDerivedSpans.ts +241 -146
  87. package/src/components/panels/traces/utils.ts +8 -0
  88. package/src/components/panels/variables/CreateVariableScreen.tsx +3 -3
  89. package/src/components/panels/variables/ManageVariablesScreen.tsx +12 -9
  90. package/src/components/panels/variables/index.tsx +2 -2
  91. package/src/components/panels/variables/styles.module.css +4 -91
  92. package/src/components/primitives/icon.module.css +4 -4
  93. package/src/components/sockets/input/index.tsx +9 -2
  94. package/src/components/sockets/input/styles.module.css +2 -3
  95. package/src/components/sockets/output/index.tsx +10 -3
  96. package/src/components/sockets/output/styles.module.css +1 -6
  97. package/src/css/notes.css +135 -0
  98. package/src/css/prosemirror.css +3 -3
  99. package/src/css/rc-dock.css +143 -43
  100. package/src/css/rc-menu.css +56 -55
  101. package/src/css/themes/kiberon.css +127 -0
  102. package/src/css/vars.css +197 -13
  103. package/src/css/vscode-elements.css +124 -0
  104. package/src/generators/CallSubgraphGenerator.tsx +136 -0
  105. package/src/generators/CustomEventOnTriggeredGenerator.tsx +2 -2
  106. package/src/generators/GraphBoundaryGenerator.module.css +32 -0
  107. package/src/generators/GraphBoundaryGenerator.tsx +193 -0
  108. package/src/generators/SequenceGenerator.tsx +2 -2
  109. package/src/generators/SwitchOnIntegerGenerator.tsx +2 -2
  110. package/src/generators/SwitchOnStringGenerator.tsx +2 -2
  111. package/src/generators/callSubgraphSync.ts +126 -0
  112. package/src/generators/registerDefaultGenerators.ts +21 -0
  113. package/src/generators/registerDefaults.ts +26 -0
  114. package/src/hooks/useBehaveGraphFlow.ts +2 -2
  115. package/src/hooks/useFlowHandlers.ts +47 -9
  116. package/src/hooks/useWasdPan.ts +26 -4
  117. package/src/index.css +4 -16
  118. package/src/index.ts +17 -0
  119. package/src/manifest/contributionRegistry.ts +93 -0
  120. package/src/manifest/index.ts +4 -0
  121. package/src/manifest/loadManifest.ts +82 -0
  122. package/src/manifest/manifestPlugin.ts +29 -0
  123. package/src/manifest/passthroughValueType.ts +40 -0
  124. package/src/plugin/alignment/index.ts +22 -12
  125. package/src/plugin/autosave/controller.ts +366 -0
  126. package/src/plugin/autosave/index.tsx +114 -0
  127. package/src/plugin/autosave/panel/BackupPanel.tsx +141 -0
  128. package/src/plugin/autosave/panel/index.tsx +1 -0
  129. package/src/plugin/autosave/panel/styles.module.css +56 -0
  130. package/src/plugin/autosave/settings.ts +65 -0
  131. package/src/plugin/autosave/storage.ts +147 -0
  132. package/src/plugin/docs/index.tsx +2 -4
  133. package/src/plugin/docs/panel/DocumentationBrowserPanelImpl.tsx +200 -0
  134. package/src/plugin/docs/panel/index.tsx +15 -194
  135. package/src/plugin/docs/panel/styles.module.css +8 -8
  136. package/src/plugin/graphrunner/actions.ts +258 -185
  137. package/src/plugin/graphrunner/buttons.tsx +34 -26
  138. package/src/plugin/graphrunner/client.ts +4 -1
  139. package/src/plugin/graphrunner/index.tsx +29 -100
  140. package/src/plugin/graphrunner/panel.tsx +2 -2
  141. package/src/plugin/graphrunner/runController.ts +283 -0
  142. package/src/plugin/graphrunner/runner.ts +21 -192
  143. package/src/plugin/graphrunner/store.ts +14 -24
  144. package/src/plugin/graphrunner/styles.module.css +17 -57
  145. package/src/plugin/graphrunner/transport.ts +26 -0
  146. package/src/plugin/graphrunner/types.ts +21 -0
  147. package/src/plugin/graphrunner-local/execution-utils.ts +260 -80
  148. package/src/plugin/graphrunner-local/index.tsx +8 -2
  149. package/src/plugin/graphrunner-local/panel.tsx +131 -175
  150. package/src/plugin/graphrunner-local/styles.module.css +57 -76
  151. package/src/plugin/graphrunner-local/transport.ts +151 -184
  152. package/src/plugin/graphrunner-webworker/graph-executor.worker.ts +2 -0
  153. package/src/plugin/graphrunner-webworker/index.tsx +4 -10
  154. package/src/plugin/graphrunner-webworker/store.ts +9 -0
  155. package/src/plugin/kitchen-sink/index.ts +38 -0
  156. package/src/{layout/dagre.tsx → plugin/layout/dagre.ts} +17 -5
  157. package/src/{layout → plugin/layout}/elk.ts +22 -6
  158. package/src/plugin/layout/index.ts +80 -0
  159. package/src/plugin/notes/FormatToolbar.tsx +200 -0
  160. package/src/plugin/notes/index.tsx +191 -0
  161. package/src/plugin/notes/nodeActions.ts +100 -0
  162. package/src/plugin/notes/note.tsx +20 -0
  163. package/src/plugin/notes/noteImpl.tsx +89 -0
  164. package/src/plugin/realtime/realtimeRunner.ts +58 -4
  165. package/src/specifics/CustomEventOnTriggeredSpecific.tsx +2 -2
  166. package/src/specifics/CustomEventTriggerSpecific.tsx +2 -2
  167. package/src/specifics/VariableGetSpecific.tsx +2 -2
  168. package/src/specifics/VariableSetSpecific.tsx +2 -2
  169. package/src/store/actions.tsx +5 -5
  170. package/src/store/commands.ts +278 -0
  171. package/src/store/contextMenu.ts +192 -0
  172. package/src/store/conversions.ts +47 -0
  173. package/src/store/flow.tsx +23 -38
  174. package/src/store/graphMeta.ts +39 -0
  175. package/src/store/hotKeys.tsx +301 -260
  176. package/src/store/layers.ts +3 -3
  177. package/src/store/registry.ts +12 -4
  178. package/src/store/selection.ts +3 -3
  179. package/src/store/settings.ts +82 -82
  180. package/src/store/settingsSchema.ts +210 -0
  181. package/src/store/tabs.ts +5 -1
  182. package/src/store/traces.ts +3 -3
  183. package/src/system/graph.ts +11 -14
  184. package/src/system/graphSession.ts +172 -0
  185. package/src/system/index.ts +3 -0
  186. package/src/system/notifications.ts +13 -0
  187. package/src/system/persistence.ts +82 -0
  188. package/src/system/plugin.ts +28 -0
  189. package/src/system/provider.tsx +64 -0
  190. package/src/system/system.ts +518 -88
  191. package/src/system/tabLoader.tsx +70 -32
  192. package/src/system/undoRedo.ts +1 -1
  193. package/src/transformers/Uigraph.ts +5 -4
  194. package/src/transformers/contract.ts +87 -0
  195. package/src/transformers/flowToBehave.ts +13 -5
  196. package/src/types/nodes.ts +8 -3
  197. package/src/types.ts +2 -0
  198. package/src/util/autoConvert.ts +200 -0
  199. package/src/util/isValidConnection.ts +23 -2
  200. package/stories/defaults/defaultStoryProvider.tsx +17 -14
  201. package/stories/defaults/systemGenerator.ts +6 -1
  202. package/stories/{components/nodes/comment.stories.tsx → plugins/notes.stories.tsx} +24 -30
  203. package/tests/autoConvert.test.ts +329 -0
  204. package/tests/autosavePlugin.test.ts +204 -0
  205. package/tests/callSubgraphSync.test.ts +148 -0
  206. package/tests/commandRegistry.test.ts +137 -0
  207. package/tests/contract.test.ts +51 -0
  208. package/tests/contractSerialize.test.ts +62 -0
  209. package/tests/deriveSpans.test.ts +71 -0
  210. package/tests/flowToBehave.test.ts +2 -1
  211. package/tests/hotkeys.test.ts +79 -0
  212. package/tests/keepAliveLifecycle.test.ts +167 -0
  213. package/tests/loadManifest.test.ts +113 -0
  214. package/tests/noteMarkdown.test.ts +65 -0
  215. package/tests/notesPlugin.test.ts +162 -0
  216. package/tests/persistence.test.ts +51 -0
  217. package/tests/saveLoad.test.ts +7 -6
  218. package/tests/settings.test.ts +178 -0
  219. package/tests/traceStore.test.ts +46 -0
  220. package/tests/visual/README.md +2 -2
  221. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-conversation-chromium-win32.png +0 -0
  222. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-events-chromium-win32.png +0 -0
  223. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-history-chromium-win32.png +0 -0
  224. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-keymaps-chromium-win32.png +0 -0
  225. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-layers-chromium-win32.png +0 -0
  226. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-legend-chromium-win32.png +0 -0
  227. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-localGraphRunner-chromium-win32.png +0 -0
  228. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-logs-chromium-win32.png +0 -0
  229. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-nodeInputs-chromium-win32.png +0 -0
  230. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-nodePicker-chromium-win32.png +0 -0
  231. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-panel-chromium-win32.png +0 -0
  232. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-search-chromium-win32.png +0 -0
  233. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-systemSettings-chromium-win32.png +0 -0
  234. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-variables-chromium-win32.png +0 -0
  235. package/tests/visual/panels.visual.test.tsx +3 -3
  236. package/tests/wasdPan.test.ts +71 -0
  237. package/vitest.config.ts +1 -1
  238. package/vitest.visual.config.ts +7 -0
  239. package/.storybook/vscode.css +0 -814
  240. package/src/components/nodes/comment/FormatToolbar.tsx +0 -118
  241. package/src/components/nodes/comment/comment.tsx +0 -103
  242. package/src/components/nodes/comment/styles.module.css +0 -150
  243. package/src/components/panels/conversation/index.module.css +0 -151
  244. package/src/components/panels/conversation/index.tsx +0 -162
  245. package/src/components/panels/events/CustomEventsEditor.tsx +0 -384
  246. package/src/css/vscode.css +0 -13
  247. package/src/hooks/useDetachNodes.ts +0 -39
  248. package/src/plugin/graphrunner-webworker/types.ts +0 -17
  249. package/src/specifics/registerDefaultSpecifics.ts +0 -5
  250. package/src/store/chat.ts +0 -73
  251. package/src/store/graphRunnerClient.ts +0 -110
@@ -10,7 +10,7 @@
10
10
 
11
11
  .header {
12
12
  padding: 1rem;
13
- border-bottom: 1px solid var(--vscode-input-border, var(--colors-borderSubtle));
13
+ border-bottom: 1px solid var(--ds-input-border, var(--colors-borderSubtle));
14
14
  background: var(--colors-bgPanel);
15
15
  }
16
16
 
@@ -28,7 +28,7 @@
28
28
  align-items: center;
29
29
  justify-content: center;
30
30
  border-radius: 0.5rem;
31
- border: 1px solid var(--vscode-input-border, var(--colors-borderSubtle));
31
+ border: 1px solid var(--ds-input-border, var(--colors-borderSubtle));
32
32
  background: var(--colors-bgCanvas);
33
33
  flex-shrink: 0;
34
34
  }
@@ -62,8 +62,8 @@
62
62
  font-size: 0.75rem;
63
63
  padding: 0.25rem 0.5rem;
64
64
  border-radius: 0.25rem;
65
- background: var(--vscode-badge-background, #4d4d4d);
66
- color: var(--vscode-badge-foreground, #ffffff);
65
+ background: var(--ds-badge-bg, #4d4d4d);
66
+ color: var(--ds-badge-fg, #ffffff);
67
67
  }
68
68
 
69
69
  .content {
@@ -77,8 +77,8 @@
77
77
  line-height: 1.5;
78
78
  margin-bottom: 1.5rem;
79
79
  padding: 0.75rem;
80
- background: var(--vscode-textBlockQuote-background, rgba(127, 127, 127, 0.1));
81
- border-left: 4px solid var(--vscode-textBlockQuote-border, #007acc);
80
+ background: var(--ds-blockquote-bg, rgba(127, 127, 127, 0.1));
81
+ border-left: 4px solid var(--ds-blockquote-border, #007acc);
82
82
  border-radius: 0.25rem;
83
83
  }
84
84
 
@@ -98,7 +98,7 @@
98
98
  font-weight: 600;
99
99
  margin-bottom: 0.75rem;
100
100
  padding-bottom: 0.25rem;
101
- border-bottom: 1px solid var(--vscode-input-border, var(--colors-borderSubtle));
101
+ border-bottom: 1px solid var(--ds-input-border, var(--colors-borderSubtle));
102
102
  }
103
103
 
104
104
  .socketList {
@@ -114,7 +114,7 @@
114
114
  padding: 0.5rem;
115
115
  background: var(--colors-bgPanel);
116
116
  border-radius: 0.25rem;
117
- border: 1px solid var(--vscode-input-border, var(--colors-borderSubtle));
117
+ border: 1px solid var(--ds-input-border, var(--colors-borderSubtle));
118
118
  }
119
119
 
120
120
  .socketIcon {
@@ -1,17 +1,88 @@
1
- import type { System } from '../../system/system';
2
- import type { StoreApi } from 'zustand';
3
- import type { GraphRunnerClientStore } from './store';
1
+ import type { GraphSession } from '@/system/graphSession';
4
2
  import type { GraphRunnerClient } from './client';
3
+ import type { GraphRunner } from './runner';
5
4
  import { executing } from '@/annotations';
6
- import { sleep } from '@/util/sleep';
7
5
  import { type ValueJSON } from '@kiberon-labs/behave-graph';
6
+ import type { TraceBatchEvent } from './types';
8
7
 
9
- // Helper to clear executing state from all nodes
10
- async function clearAllExecutingStates(system: System) {
11
- await sleep(1); // Delay to allow any final traces to process
12
- system.nodeStore.getState().setNodes((nodes) =>
13
- nodes.map((node) => {
8
+ /**
9
+ * Per-session batcher for the `executing` node annotation.
10
+ *
11
+ * Trace events arrive per node execution (start + end), which at display-rate
12
+ * ticking means hundreds of events per frame. Writing the annotation straight
13
+ * through did a full O(nodes) copy-map of the node array per event; instead,
14
+ * accumulate the net executing state here and apply it once per animation
15
+ * frame with a single identity-preserving setNodes pass.
16
+ */
17
+ type ExecutingBatch = {
18
+ pending: Map<string, boolean>;
19
+ scheduled: boolean;
20
+ };
21
+
22
+ const executingBatches = new WeakMap<GraphSession, ExecutingBatch>();
23
+
24
+ const scheduleFrame = (cb: () => void): void => {
25
+ if (typeof requestAnimationFrame === 'function') {
26
+ requestAnimationFrame(cb);
27
+ } else {
28
+ setTimeout(cb, 16);
29
+ }
30
+ };
31
+
32
+ function flushExecutingState(session: GraphSession, batch: ExecutingBatch) {
33
+ batch.scheduled = false;
34
+ if (batch.pending.size === 0) return;
35
+ const updates = batch.pending;
36
+ batch.pending = new Map();
37
+
38
+ session.nodeStore.getState().setNodes((nodes) => {
39
+ let changed = false;
40
+ const next = nodes.map((node) => {
41
+ if (!('data' in node)) return node;
42
+ const target = updates.get(node.id);
43
+ if (target === undefined) return node;
44
+ if (Boolean(node.data.annotations?.[executing]) === target) return node;
45
+ changed = true;
46
+ return {
47
+ ...node,
48
+ data: {
49
+ ...node.data,
50
+ annotations: {
51
+ ...node.data.annotations,
52
+ [executing]: target
53
+ }
54
+ }
55
+ };
56
+ });
57
+ // Keep the original array identity when nothing changed so selector-based
58
+ // subscribers (the React Flow canvas) skip the re-render entirely.
59
+ return changed ? next : nodes;
60
+ });
61
+ }
62
+
63
+ function markExecuting(session: GraphSession, nodeId: string, state: boolean) {
64
+ let batch = executingBatches.get(session);
65
+ if (!batch) {
66
+ batch = { pending: new Map(), scheduled: false };
67
+ executingBatches.set(session, batch);
68
+ }
69
+ batch.pending.set(nodeId, state);
70
+ if (!batch.scheduled) {
71
+ batch.scheduled = true;
72
+ scheduleFrame(() => flushExecutingState(session, batch));
73
+ }
74
+ }
75
+
76
+ // Clear executing state from all nodes of a session, dropping any queued
77
+ // per-node updates so a pending flush can't re-highlight after the run ended.
78
+ function clearAllExecutingStates(session: GraphSession) {
79
+ const batch = executingBatches.get(session);
80
+ if (batch) batch.pending.clear();
81
+ session.nodeStore.getState().setNodes((nodes) => {
82
+ let changed = false;
83
+ const next = nodes.map((node) => {
14
84
  if ('data' in node && node.data.annotations?.[executing]) {
85
+ changed = true;
15
86
  return {
16
87
  ...node,
17
88
  data: {
@@ -24,95 +95,92 @@ async function clearAllExecutingStates(system: System) {
24
95
  };
25
96
  }
26
97
  return node;
27
- })
28
- );
98
+ });
99
+ return changed ? next : nodes;
100
+ });
29
101
  }
30
102
 
103
+ /** Apply one trace event to the session's trace spans + executing annotation. */
104
+ function processTraceEvent(session: GraphSession, ev: TraceBatchEvent) {
105
+ const traceStore = session.traceStore.getState();
106
+ if (ev.event === 'start') {
107
+ let name = ev.nodeId;
108
+ if (ev.data && typeof ev.data === 'object' && 'typeName' in ev.data) {
109
+ const typeName = (ev.data as { typeName?: unknown }).typeName;
110
+ if (typeof typeName === 'string') {
111
+ name = typeName;
112
+ }
113
+ }
114
+ traceStore.addSpan({
115
+ nodeId: ev.nodeId,
116
+ name,
117
+ // `??` not `||`: the worker sends run-relative ms, so 0 is a valid (and
118
+ // common, for the first node) start , `||` fell back to the main thread's
119
+ // performance.now(), producing huge, wrong-clock timestamps.
120
+ start: ev.timestamp ?? performance.now(),
121
+ // Open span: NaN until the matching `end` event arrives. The store/render
122
+ // treat NaN as "still running"; a literal end let it render mis-sized.
123
+ end: Number.NaN
124
+ // lane omitted: let the store allocate/free lanes so concurrent spans
125
+ // stack instead of all piling into lane 0.
126
+ });
127
+ markExecuting(session, ev.nodeId, true);
128
+ } else if (ev.event === 'end') {
129
+ traceStore.updateSpan(ev.nodeId, {
130
+ end: ev.timestamp ?? performance.now()
131
+ });
132
+ markExecuting(session, ev.nodeId, false);
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Clients that already have listeners attached. Guards against double-wiring when
138
+ * both the plugin's `runner.connect()` and a host (e.g. the webworker runner)
139
+ * call {@link setupClientEventListeners} on the same client , which would record
140
+ * every trace span, log and event twice.
141
+ */
142
+ const wiredClients = new WeakSet<GraphRunnerClient>();
143
+
31
144
  /**
32
- * Setup persistent event listeners on the client for trace, logs, and run lifecycle
33
- * This should be called once when the client is connected
145
+ * Setup persistent event listeners on the shared client. Registered once when
146
+ * the client connects; every message is routed to the session that started its
147
+ * run (via {@link GraphRunner.runIndex}), so concurrent graphs stay isolated.
148
+ *
149
+ * Idempotent per client: calling it again with the same client is a no-op.
34
150
  */
35
151
  export function setupClientEventListeners(
36
152
  client: GraphRunnerClient,
37
- system: System,
38
- store: StoreApi<GraphRunnerClientStore>
153
+ runner: GraphRunner
39
154
  ) {
40
- const { setIsExecuting, setCurrentRunId, setCurrentGraphId, setIsPaused } =
41
- store.getState();
42
-
43
- // Listen for trace events - these apply to all runs
44
- client.on('trace', async (message) => {
45
- const traceStore = system.traceStore.getState();
46
- if (message.event === 'start') {
47
- let name = message.nodeId;
48
- if (
49
- message.data &&
50
- typeof message.data === 'object' &&
51
- 'typeName' in message.data
52
- ) {
53
- const typeName = (message.data as { typeName?: unknown }).typeName;
54
- if (typeof typeName === 'string') {
55
- name = typeName;
56
- }
57
- }
58
- traceStore.addSpan({
59
- nodeId: message.nodeId,
60
- name,
61
- start: message.timestamp || performance.now(),
62
- end: 1,
63
- lane: 0
64
- });
65
-
66
- // Mark node as executing
67
- system.nodeStore.getState().setNodes((nodes) =>
68
- nodes.map((node) =>
69
- node.id === message.nodeId && 'data' in node
70
- ? {
71
- ...node,
72
- data: {
73
- ...node.data,
74
- annotations: {
75
- ...node.data.annotations,
76
- [executing]: true
77
- }
78
- }
79
- }
80
- : node
81
- )
82
- );
83
- } else if (message.event === 'end') {
84
- traceStore.updateSpan(message.nodeId, {
85
- end: message.timestamp || performance.now()
86
- });
87
-
88
- //Delay to allow UI to show executing state
89
- await sleep(1);
90
-
91
- // Mark node as no longer executing
92
- system.nodeStore.getState().setNodes((nodes) =>
93
- nodes.map((node) =>
94
- node.id === message.nodeId && 'data' in node
95
- ? {
96
- ...node,
97
- data: {
98
- ...node.data,
99
- annotations: {
100
- ...node.data.annotations,
101
- [executing]: false
102
- }
103
- }
104
- }
105
- : node
106
- )
107
- );
155
+ if (wiredClients.has(client)) return;
156
+ wiredClients.add(client);
157
+
158
+ // Resolve the session that owns a given run id, or null if unknown.
159
+ const sessionFor = (runId: string): GraphSession | null =>
160
+ runner.runIndex.get(runId)?.session ?? null;
161
+
162
+ // Batched trace events (one message per flush window, many events inside).
163
+ client.on('traceBatch', (message) => {
164
+ const session = sessionFor(message.runId);
165
+ if (!session) return;
166
+ for (const ev of message.events) {
167
+ processTraceEvent(session, ev);
108
168
  }
109
169
  });
110
170
 
171
+ // Single trace events , kept for remote servers that predate `traceBatch`.
172
+ client.on('trace', (message) => {
173
+ const session = sessionFor(message.runId);
174
+ if (!session) return;
175
+ processTraceEvent(session, message);
176
+ });
177
+
111
178
  // Listen for log messages
112
179
  client.on('log', (message) => {
113
- const logStore = system.logsStore.getState();
180
+ const session = sessionFor(message.runId);
181
+ if (!session) return;
114
182
  const formattedMessage = `[${message.runId}/${message.graphId}] ${message.message}${message.data !== undefined ? ` ${JSON.stringify(message.data)}` : ''}`;
115
- logStore.append({
183
+ session.logsStore.getState().append({
116
184
  time: new Date(),
117
185
  data: {
118
186
  message: formattedMessage
@@ -121,133 +189,138 @@ export function setupClientEventListeners(
121
189
  });
122
190
  });
123
191
 
124
- // Listen for variable change events from server
192
+ // Listen for variable change events from server. A tick-driven graph writes
193
+ // variables every frame, so coalesce to the latest value per variable and
194
+ // apply once per animation frame instead of one store write per change.
195
+ const pendingVariableUpdates = new Map<GraphSession, Map<string, unknown>>();
196
+ let variableFlushScheduled = false;
197
+
198
+ const flushVariableUpdates = () => {
199
+ variableFlushScheduled = false;
200
+ for (const [session, updates] of pendingVariableUpdates) {
201
+ const variableStore = session.variableStore.getState();
202
+ for (const [id, newValue] of updates) {
203
+ const existingVariable = variableStore.variables[id];
204
+ if (existingVariable) {
205
+ variableStore.setVariable(id, {
206
+ ...existingVariable,
207
+ initialValue: newValue as ValueJSON
208
+ });
209
+ } else {
210
+ const inferredType = typeof newValue;
211
+ variableStore.setVariable(id, {
212
+ id,
213
+ name: id,
214
+ valueTypeName: inferredType === 'object' ? 'string' : inferredType,
215
+ initialValue: newValue as ValueJSON
216
+ });
217
+ }
218
+ }
219
+ }
220
+ pendingVariableUpdates.clear();
221
+ };
222
+
125
223
  client.on('variableChanged', (message) => {
126
- const variableStore = system.variableStore.getState();
127
- const id = message.variableName;
128
-
129
- // Get existing variable or create new one
130
- const existingVariable = variableStore.variables[id];
131
-
132
- if (existingVariable) {
133
- // Update existing variable
134
- system.variableStore.getState().setVariable(id, {
135
- ...existingVariable,
136
- initialValue: message.newValue as ValueJSON
137
- });
138
- } else {
139
- // Create new variable if it doesn't exist
140
- const inferredType = typeof message.newValue;
141
- const newVariable = {
142
- id,
143
- name: message.variableName,
144
- valueTypeName: inferredType === 'object' ? 'string' : inferredType,
145
- initialValue: message.newValue as ValueJSON
146
- };
147
- variableStore.setVariable(id, newVariable);
224
+ const session = sessionFor(message.runId);
225
+ if (!session) return;
226
+ let updates = pendingVariableUpdates.get(session);
227
+ if (!updates) {
228
+ updates = new Map();
229
+ pendingVariableUpdates.set(session, updates);
230
+ }
231
+ updates.set(message.variableName, message.newValue);
232
+ if (!variableFlushScheduled) {
233
+ variableFlushScheduled = true;
234
+ scheduleFrame(flushVariableUpdates);
148
235
  }
149
236
  });
150
237
 
151
- // Listen for run completion events
238
+ // Run lifecycle events
152
239
  client.on('completed', (message) => {
153
- const currentRunId = store.getState().currentRunId;
154
- if (message.runId === currentRunId) {
155
- system.notifications.success(`Graph completed: ${message.graphId}`);
156
- setIsExecuting(false);
157
- setCurrentRunId(null);
158
- setCurrentGraphId(null);
159
- setIsPaused(false);
160
- clearAllExecutingStates(system);
161
- }
240
+ const controller = runner.runIndex.get(message.runId);
241
+ if (!controller) return;
242
+ controller.session.editor.notifications.success(
243
+ `Graph completed: ${message.graphId}`
244
+ );
245
+ controller.finishRun();
246
+ clearAllExecutingStates(controller.session);
162
247
  });
163
248
 
164
249
  client.on('error', (message) => {
165
- const currentRunId = store.getState().currentRunId;
166
- if (message.runId === currentRunId) {
167
- system.notifications.error(`Graph failed: ${message.graphId}`);
168
- setIsExecuting(false);
169
- setCurrentRunId(null);
170
- setCurrentGraphId(null);
171
- setIsPaused(false);
172
- clearAllExecutingStates(system);
173
- }
250
+ if (!message.runId) return;
251
+ const controller = runner.runIndex.get(message.runId);
252
+ if (!controller) return;
253
+ controller.session.editor.notifications.error(
254
+ `Graph failed: ${message.graphId}`
255
+ );
256
+ controller.finishRun();
257
+ clearAllExecutingStates(controller.session);
174
258
  });
175
259
 
176
260
  client.on('stopped', (message) => {
177
- const currentRunId = store.getState().currentRunId;
178
- if (message.runId === currentRunId) {
179
- system.notifications.info(`Graph stopped: ${message.graphId}`);
180
- setIsExecuting(false);
181
- setCurrentRunId(null);
182
- setCurrentGraphId(null);
183
- setIsPaused(false);
184
- clearAllExecutingStates(system);
185
- }
261
+ const controller = runner.runIndex.get(message.runId);
262
+ if (!controller) return;
263
+ controller.session.editor.notifications.info(
264
+ `Graph stopped: ${message.graphId}`
265
+ );
266
+ controller.finishRun();
267
+ clearAllExecutingStates(controller.session);
186
268
  });
187
269
 
188
270
  // Realtime state change listeners
189
271
  client.on('nodeRemoved', (message) => {
190
- const currentRunId = store.getState().currentRunId;
191
- if (message.runId === currentRunId) {
192
- // Update node store to reflect removal
193
- system.nodeStore
194
- .getState()
195
- .setNodes((nodes) =>
196
- nodes.filter((node) => node.id !== message.nodeId)
197
- );
198
- system.notifications.info(`Node removed: ${message.nodeId}`);
199
- }
272
+ const session = sessionFor(message.runId);
273
+ if (!session) return;
274
+ session.nodeStore
275
+ .getState()
276
+ .setNodes((nodes) => nodes.filter((node) => node.id !== message.nodeId));
277
+ session.editor.notifications.info(`Node removed: ${message.nodeId}`);
200
278
  });
201
279
 
202
280
  client.on('linkCreated', (message) => {
203
- const currentRunId = store.getState().currentRunId;
204
- if (message.runId === currentRunId) {
205
- system.notifications.info(
206
- `Link created: ${message.fromNodeId}/${message.fromSocket} -> ${message.toNodeId}/${message.toSocket}`
207
- );
208
- }
281
+ const session = sessionFor(message.runId);
282
+ if (!session) return;
283
+ session.editor.notifications.info(
284
+ `Link created: ${message.fromNodeId}/${message.fromSocket} -> ${message.toNodeId}/${message.toSocket}`
285
+ );
209
286
  });
210
287
 
211
288
  client.on('linkRemoved', (message) => {
212
- const currentRunId = store.getState().currentRunId;
213
- if (message.runId === currentRunId) {
214
- system.notifications.info(
215
- `Link removed: ${message.fromNodeId}/${message.fromSocket} -> ${message.toNodeId}/${message.toSocket}`
216
- );
217
- }
289
+ const session = sessionFor(message.runId);
290
+ if (!session) return;
291
+ session.editor.notifications.info(
292
+ `Link removed: ${message.fromNodeId}/${message.fromSocket} -> ${message.toNodeId}/${message.toSocket}`
293
+ );
218
294
  });
219
295
 
220
296
  client.on('nodeParamUpdated', (message) => {
221
- const currentRunId = store.getState().currentRunId;
222
- if (message.runId === currentRunId) {
223
- system.notifications.info(`Parameter updated on ${message.nodeId}`);
224
- }
297
+ const session = sessionFor(message.runId);
298
+ if (!session) return;
299
+ session.editor.notifications.info(`Parameter updated on ${message.nodeId}`);
225
300
  });
226
301
 
227
302
  client.on('affectedNodes', (message) => {
228
- const currentRunId = store.getState().currentRunId;
229
- if (message.runId === currentRunId) {
230
- // Highlight affected nodes
231
- system.nodeStore.getState().setNodes((nodes) =>
232
- nodes.map((node) => {
233
- if (message.nodeIds.includes(node.id)) {
234
- return {
235
- ...node,
236
- data: {
237
- ...node.data,
238
- annotations: {
239
- ...node.data?.annotations,
240
- [executing]: true
241
- }
303
+ const session = sessionFor(message.runId);
304
+ if (!session) return;
305
+ session.nodeStore.getState().setNodes((nodes) =>
306
+ nodes.map((node) => {
307
+ if (message.nodeIds.includes(node.id)) {
308
+ return {
309
+ ...node,
310
+ data: {
311
+ ...node.data,
312
+ annotations: {
313
+ ...node.data?.annotations,
314
+ [executing]: true
242
315
  }
243
- };
244
- }
245
- return node;
246
- })
247
- );
248
- system.notifications.info(
249
- `Executing ${message.reason}: ${message.nodeIds.length} node(s)`
250
- );
251
- }
316
+ }
317
+ };
318
+ }
319
+ return node;
320
+ })
321
+ );
322
+ session.editor.notifications.info(
323
+ `Executing ${message.reason}: ${message.nodeIds.length} node(s)`
324
+ );
252
325
  });
253
326
  }
@@ -1,29 +1,37 @@
1
1
  import { VscodeButton } from '@vscode-elements/react-elements';
2
- import { useStore } from 'zustand';
2
+ import { useStore, type StoreApi } from 'zustand';
3
3
  import { Play, PauseWindow, Square, ArrowRight } from 'iconoir-react';
4
- import type { StoreApi } from 'zustand';
4
+ import { useGraph } from '@/system/provider';
5
5
  import type { GraphRunnerClientStore } from './store';
6
+ import type { GraphRunController } from './runController';
6
7
 
7
- interface GraphRunnerButtonsProps {
8
- store: StoreApi<GraphRunnerClientStore>;
9
- onPlay: () => void;
10
- onPause: () => void;
11
- onResume: () => void;
12
- onStep: () => void;
13
- onStop: () => void;
14
- }
8
+ /**
9
+ * Execution controls for the graph in the surrounding tab. Connection state
10
+ * comes from the shared runner connection; run state comes from this graph's own
11
+ * run controller, so each open graph has independent controls.
12
+ */
13
+ export const GraphRunnerButtons = () => {
14
+ const session = useGraph();
15
+ const controller = session.runController;
16
+ if (!controller) return null;
17
+ return (
18
+ <Buttons
19
+ controller={controller}
20
+ connectionStore={session.editor.runner.store}
21
+ />
22
+ );
23
+ };
15
24
 
16
- export const GraphRunnerButtons = ({
17
- store,
18
- onPlay,
19
- onPause,
20
- onResume,
21
- onStep,
22
- onStop
23
- }: GraphRunnerButtonsProps) => {
24
- const connectionState = useStore(store, (state) => state.connectionState);
25
- const isExecuting = useStore(store, (state) => state.isExecuting);
26
- const isPaused = useStore(store, (state) => state.isPaused);
25
+ const Buttons = ({
26
+ controller,
27
+ connectionStore
28
+ }: {
29
+ controller: GraphRunController;
30
+ connectionStore: StoreApi<GraphRunnerClientStore>;
31
+ }) => {
32
+ const connectionState = useStore(connectionStore, (s) => s.connectionState);
33
+ const isExecuting = useStore(controller.store, (s) => s.isExecuting);
34
+ const isPaused = useStore(controller.store, (s) => s.isPaused);
27
35
 
28
36
  const isConnected = connectionState === 'connected';
29
37
 
@@ -34,7 +42,7 @@ export const GraphRunnerButtons = ({
34
42
  secondary
35
43
  iconOnly
36
44
  title="Play Graph"
37
- onClick={onPlay}
45
+ onClick={() => controller.play()}
38
46
  disabled={!isConnected}
39
47
  >
40
48
  <Play />
@@ -45,7 +53,7 @@ export const GraphRunnerButtons = ({
45
53
  secondary
46
54
  iconOnly
47
55
  title="Pause Graph"
48
- onClick={onPause}
56
+ onClick={() => controller.pause()}
49
57
  disabled={!isConnected}
50
58
  >
51
59
  <PauseWindow />
@@ -57,7 +65,7 @@ export const GraphRunnerButtons = ({
57
65
  secondary
58
66
  iconOnly
59
67
  title="Resume Graph"
60
- onClick={onResume}
68
+ onClick={() => controller.resume()}
61
69
  disabled={!isConnected}
62
70
  >
63
71
  <Play />
@@ -66,7 +74,7 @@ export const GraphRunnerButtons = ({
66
74
  secondary
67
75
  iconOnly
68
76
  title="Step Forward"
69
- onClick={onStep}
77
+ onClick={() => controller.step()}
70
78
  disabled={!isConnected}
71
79
  >
72
80
  <ArrowRight />
@@ -77,7 +85,7 @@ export const GraphRunnerButtons = ({
77
85
  secondary
78
86
  iconOnly
79
87
  title="Stop Graph"
80
- onClick={onStop}
88
+ onClick={() => controller.stop()}
81
89
  disabled={!isExecuting}
82
90
  >
83
91
  <Square />
@@ -301,7 +301,10 @@ export class GraphRunnerClient {
301
301
  graph: options?.graph as GraphJSON,
302
302
  inputs: options?.inputs,
303
303
  options: {
304
- trace: options?.trace ?? true
304
+ // Default off , tracing is a debugging aid with a real per-node cost;
305
+ // callers opt in explicitly (the run controller passes the panel's
306
+ // "Enable execution tracing" preference).
307
+ trace: options?.trace ?? false
305
308
  }
306
309
  });
307
310