@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
@@ -425,10 +425,46 @@ export class RealtimeRunner {
425
425
  if (this.annotatedOutputNodeIds.length === 0) return;
426
426
 
427
427
  for (const nodeId of this.annotatedOutputNodeIds) {
428
- const node = this.engine.nodes?.[nodeId];
429
- if (!node) continue;
430
- if (!isFunctionNode(node)) continue;
428
+ await this.evaluateNodeForPreview(nodeId);
429
+ }
430
+ }
431
+
432
+ /**
433
+ * Evaluate every node that a UI consumer is actively watching.
434
+ *
435
+ * Pure function graphs (e.g. the image nodes) never run during
436
+ * `executeAllSync()` because nothing pushes a flow fiber through them. They
437
+ * are normally pulled lazily when a downstream flow/event node reads their
438
+ * value. In the editor preview there is no such consumer, so the act of a
439
+ * component calling `watchNodeOutput()` is what drives evaluation: we resolve
440
+ * the node's upstream function graph and execute the node itself so its
441
+ * sockets hold a fresh value for `publishWatchedOutputs()` to read.
442
+ *
443
+ * This intentionally does not depend on the `ui.realtime` annotation , any
444
+ * watched output is evaluated.
445
+ */
446
+ private async recalculateWatchedOutputs(): Promise<void> {
447
+ if (!this.engine) return;
448
+ if (this.watched.size === 0) return;
449
+
450
+ for (const nodeId of this.watched.keys()) {
451
+ await this.evaluateNodeForPreview(nodeId);
452
+ }
453
+ }
454
+
455
+ /**
456
+ * Resolve a single function node's inputs (recursively executing its upstream
457
+ * function graph) and execute it. Non-function nodes keep whatever value their
458
+ * last real execution produced. Resilient: a failure on one node is logged and
459
+ * does not abort evaluation of the others.
460
+ */
461
+ private async evaluateNodeForPreview(nodeId: string): Promise<void> {
462
+ if (!this.engine) return;
463
+ const node = this.engine.nodes?.[nodeId];
464
+ if (!node) return;
465
+ if (!isFunctionNode(node)) return;
431
466
 
467
+ try {
432
468
  let executionSteps = 0;
433
469
  for (const inputSocket of node.inputs) {
434
470
  executionSteps += await this.resolveSocketValueForPreview(inputSocket);
@@ -440,6 +476,11 @@ export class RealtimeRunner {
440
476
  this.engine.onNodeExecutionEnd.emit(node);
441
477
 
442
478
  this.engine.executionSteps += executionSteps;
479
+ } catch (err) {
480
+ console.error(
481
+ `RealtimeRunner: failed to evaluate watched node ${nodeId}:`,
482
+ err
483
+ );
443
484
  }
444
485
  }
445
486
 
@@ -450,7 +491,12 @@ export class RealtimeRunner {
450
491
  const nodes = this.system.nodeStore.getState().nodes;
451
492
  const edges = this.system.edgeStore.getState().edges;
452
493
 
453
- const rawGraphJson = flowToBehave(this.system, nodes, edges, specJson);
494
+ const rawGraphJson = flowToBehave(
495
+ this.system.session!,
496
+ nodes,
497
+ edges,
498
+ specJson
499
+ );
454
500
  const graphWithAnnotations = this.mergeNodeAnnotationsIntoMetadata(
455
501
  rawGraphJson,
456
502
  nodes
@@ -540,6 +586,10 @@ export class RealtimeRunner {
540
586
  // Recalculate annotated output nodes to force evaluation of upstream function graphs.
541
587
  await this.recalculateAnnotatedOutputNodes();
542
588
 
589
+ // Pull every watched output (e.g. live image node previews) so pure
590
+ // function graphs are evaluated even without a flow consumer.
591
+ await this.recalculateWatchedOutputs();
592
+
543
593
  this.publishWatchedOutputs();
544
594
  } catch (err) {
545
595
  // Keep preview runner resilient; don't crash the editor.
@@ -562,6 +612,10 @@ export class RealtimeRunner {
562
612
  // Recalculate annotated output nodes to force evaluation of upstream function graphs.
563
613
  await this.recalculateAnnotatedOutputNodes();
564
614
 
615
+ // Pull every watched output (e.g. live image node previews) so pure
616
+ // function graphs are evaluated even without a flow consumer.
617
+ await this.recalculateWatchedOutputs();
618
+
565
619
  this.publishWatchedOutputs();
566
620
  } catch {
567
621
  // ignore
@@ -5,7 +5,7 @@ import {
5
5
  VscodeSingleSelect
6
6
  } from '@vscode-elements/react-elements';
7
7
 
8
- import { useSystem } from '@/system/provider';
8
+ import { useGraph } from '@/system/provider';
9
9
  import type { SpecificRenderProps } from '@/store/specific';
10
10
 
11
11
  const NAME = 'customEvent/onTriggered.customEventId';
@@ -21,7 +21,7 @@ export function getCustomEventOnTriggeredSpecific() {
21
21
  const CustomEventOnTriggeredSpecific: React.FC<SpecificRenderProps> = ({
22
22
  node
23
23
  }) => {
24
- const system = useSystem();
24
+ const system = useGraph();
25
25
  const customEvents = useStore(system.eventsStore, (s) => s.customEvents);
26
26
 
27
27
  const options = useMemo(() => {
@@ -5,7 +5,7 @@ import {
5
5
  VscodeSingleSelect
6
6
  } from '@vscode-elements/react-elements';
7
7
 
8
- import { useSystem } from '@/system/provider';
8
+ import { useGraph } from '@/system/provider';
9
9
  import type { SpecificRenderProps } from '@/store/specific';
10
10
  import type { NodeSpecJSON } from '@kiberon-labs/behave-graph';
11
11
  import type { Socket } from '@/types';
@@ -24,7 +24,7 @@ export function getCustomEventTriggerSpecific() {
24
24
  const CustomEventTriggerSpecific: React.FC<SpecificRenderProps> = ({
25
25
  node
26
26
  }) => {
27
- const system = useSystem();
27
+ const system = useGraph();
28
28
  const customEvents = useStore(system.eventsStore, (s) => s.customEvents);
29
29
 
30
30
  const options = useMemo(() => {
@@ -5,7 +5,7 @@ import {
5
5
  VscodeSingleSelect
6
6
  } from '@vscode-elements/react-elements';
7
7
 
8
- import { useSystem } from '@/system/provider';
8
+ import { useGraph } from '@/system/provider';
9
9
  import type { SpecificRenderProps } from '@/store/specific';
10
10
  import type { NodeSpecJSON } from '@kiberon-labs/behave-graph';
11
11
 
@@ -20,7 +20,7 @@ export function getVariableGetSpecific() {
20
20
  }
21
21
 
22
22
  const VariableGetSpecific: React.FC<SpecificRenderProps> = ({ node }) => {
23
- const system = useSystem();
23
+ const system = useGraph();
24
24
  const variables = useStore(system.variableStore, (s) => s.variables);
25
25
 
26
26
  const options = useMemo(() => {
@@ -5,7 +5,7 @@ import {
5
5
  VscodeSingleSelect
6
6
  } from '@vscode-elements/react-elements';
7
7
 
8
- import { useSystem } from '@/system/provider';
8
+ import { useGraph } from '@/system/provider';
9
9
  import type { SpecificRenderProps } from '@/store/specific';
10
10
  import type { NodeSpecJSON } from '@kiberon-labs/behave-graph';
11
11
 
@@ -20,7 +20,7 @@ export function getVariableSetSpecific() {
20
20
  }
21
21
 
22
22
  const VariableSetSpecific: React.FC<SpecificRenderProps> = ({ node }) => {
23
- const system = useSystem();
23
+ const system = useGraph();
24
24
  const variables = useStore(system.variableStore, (s) => s.variables);
25
25
 
26
26
  const options = useMemo(() => {
@@ -1,4 +1,4 @@
1
- import type { System } from '@/system';
1
+ import type { GraphSession } from '@/system/graphSession';
2
2
  import { Graph } from 'graphlib';
3
3
  import { create } from 'zustand';
4
4
  import copyToClipboard from 'copy-to-clipboard';
@@ -87,7 +87,7 @@ export type ActionStore = {
87
87
  * @param sys
88
88
  * @returns
89
89
  */
90
- const convertToGraph = (sys: System) => {
90
+ const convertToGraph = (sys: GraphSession) => {
91
91
  const nodes = sys.nodeStore.getState().nodes;
92
92
  const edges = sys.edgeStore.getState().edges;
93
93
 
@@ -119,7 +119,7 @@ const createNodeLookup = (nodes: string[]) => {
119
119
  );
120
120
  };
121
121
 
122
- const applyFilters = (sys: System, lookup: Record<string, boolean>) => {
122
+ const applyFilters = (sys: GraphSession, lookup: Record<string, boolean>) => {
123
123
  sys.nodeStore.getState().setNodes((nodes) => {
124
124
  const newNodes = nodes.map((x) => {
125
125
  if (!lookup[x.id]) {
@@ -140,13 +140,13 @@ const applyFilters = (sys: System, lookup: Record<string, boolean>) => {
140
140
  });
141
141
  };
142
142
 
143
- export const actionStoreFactory = (sys: System) =>
143
+ export const actionStoreFactory = (sys: GraphSession) =>
144
144
  create<ActionStore>((set, get) => ({
145
145
  actions: {
146
146
  save: async () => {
147
147
  try {
148
148
  const uiGraph = buildUIGraphJSON(sys);
149
- sys.pubsub.publish('graph:saved', uiGraph);
149
+ sys.editor.pubsub.publish('graph:saved', uiGraph);
150
150
  return uiGraph;
151
151
  } catch (err) {
152
152
  sys.notifications.error('Failed to save graph');
@@ -0,0 +1,278 @@
1
+ import { createStore, type StoreApi } from 'zustand';
2
+ import type { EdgeChange, XYPosition } from 'reactflow';
3
+ import type { System } from '@/system/system';
4
+ import type { GraphSession } from '@/system/graphSession';
5
+
6
+ /**
7
+ * Context handed to a command when it runs. Carries the editor + the graph it
8
+ * acts on, plus optional targets so the same command works from a context menu,
9
+ * a hotkey, or a toolbar button.
10
+ */
11
+ export type CommandContext = {
12
+ editor: System;
13
+ session: GraphSession;
14
+ nodeId?: string;
15
+ edgeId?: string;
16
+ sourceId?: string;
17
+ targetId?: string;
18
+ position?: XYPosition;
19
+ };
20
+
21
+ /**
22
+ * A named, dispatchable action. Decouples *what* (id) from *how* (run), so UI
23
+ * surfaces (context menus, hotkeys, menubar, toolbar) reference commands by id
24
+ * instead of reaching into concrete stores.
25
+ */
26
+ export type Command = {
27
+ id: string;
28
+ title?: string;
29
+ /** When present and false, the command is treated as unavailable. */
30
+ isEnabled?: (ctx: CommandContext) => boolean;
31
+ run: (ctx: CommandContext) => void | Promise<void>;
32
+ };
33
+
34
+ export type CommandStore = {
35
+ commands: Map<string, Command>;
36
+ /** Register (or replace) a command. Returns an unregister disposer. */
37
+ register: (command: Command) => () => void;
38
+ unregister: (id: string) => void;
39
+ get: (id: string) => Command | undefined;
40
+ list: () => Command[];
41
+ /** Run a command by id; no-ops (with a warning) if unknown or disabled. */
42
+ run: (id: string, ctx: CommandContext) => void | Promise<void>;
43
+ };
44
+
45
+ export const commandStoreFactory = (): StoreApi<CommandStore> =>
46
+ createStore<CommandStore>((set, get) => ({
47
+ commands: new Map(),
48
+ register: (command) => {
49
+ set((s) => {
50
+ const next = new Map(s.commands);
51
+ next.set(command.id, command);
52
+ return { commands: next };
53
+ });
54
+ return () => get().unregister(command.id);
55
+ },
56
+ unregister: (id) =>
57
+ set((s) => {
58
+ if (!s.commands.has(id)) return s;
59
+ const next = new Map(s.commands);
60
+ next.delete(id);
61
+ return { commands: next };
62
+ }),
63
+ get: (id) => get().commands.get(id),
64
+ list: () => Array.from(get().commands.values()),
65
+ run: (id, ctx) => {
66
+ const command = get().commands.get(id);
67
+ if (!command) {
68
+ console.warn(`[commands] unknown command: ${id}`);
69
+ return;
70
+ }
71
+ if (command.isEnabled && !command.isEnabled(ctx)) return;
72
+ return command.run(ctx);
73
+ }
74
+ }));
75
+
76
+ // --- Default graph commands --------------------------------------------------
77
+ // Transitional: these delegate to the per-session action store. As actions are
78
+ // decomposed (a later slice), the logic can move into the command handlers and
79
+ // the action store can shrink.
80
+
81
+ const actionsOf = (ctx: CommandContext) =>
82
+ ctx.session.actionStore.getState().actions;
83
+
84
+ const centerOnNode = (session: GraphSession, nodeId: string): void => {
85
+ const node = session.nodeStore.getState().nodes.find((n) => n.id === nodeId);
86
+ if (!node) return;
87
+ const x = node.position.x + (node.width ?? 0) / 2;
88
+ const y = node.position.y + (node.height ?? 0) / 2;
89
+ session.refStore
90
+ .getState()
91
+ .getRef('reactflow')
92
+ ?.setCenter(x, y, { duration: 200, zoom: 1 });
93
+ };
94
+
95
+ const reactFlowOf = (session: GraphSession) =>
96
+ session.refStore.getState().getRef('reactflow');
97
+
98
+ /**
99
+ * Register the built-in editor commands. Hosts may override any of them by
100
+ * re-registering with the same id, or add their own.
101
+ */
102
+ export const registerDefaultCommands = (
103
+ store: StoreApi<CommandStore>
104
+ ): void => {
105
+ const { register } = store.getState();
106
+
107
+ register({
108
+ id: 'node.focus',
109
+ title: 'Focus',
110
+ run: (ctx) => {
111
+ if (ctx.nodeId) actionsOf(ctx).focusNode(ctx.nodeId);
112
+ }
113
+ });
114
+ register({
115
+ id: 'node.traceUpstream',
116
+ title: 'Trace Upstream',
117
+ run: (ctx) => {
118
+ if (ctx.nodeId) actionsOf(ctx).traceUpstream(ctx.nodeId);
119
+ }
120
+ });
121
+ register({
122
+ id: 'node.traceDownstream',
123
+ title: 'Trace Downstream',
124
+ run: (ctx) => {
125
+ if (ctx.nodeId) actionsOf(ctx).traceDownstream(ctx.nodeId);
126
+ }
127
+ });
128
+ register({
129
+ id: 'trace.reset',
130
+ title: 'Reset Trace',
131
+ run: (ctx) => actionsOf(ctx).resetTrace()
132
+ });
133
+ register({
134
+ id: 'node.togglePinned',
135
+ title: 'Pin / Unpin',
136
+ run: (ctx) => {
137
+ if (ctx.nodeId) actionsOf(ctx).toggleNodePinned(ctx.nodeId);
138
+ }
139
+ });
140
+ register({
141
+ id: 'node.toggleHidden',
142
+ title: 'Hide / Show',
143
+ run: (ctx) => {
144
+ if (ctx.nodeId) actionsOf(ctx).toggleNodeHidden(ctx.nodeId);
145
+ }
146
+ });
147
+
148
+ register({
149
+ id: 'edge.findSource',
150
+ title: 'Find Source',
151
+ run: (ctx) => {
152
+ if (ctx.sourceId) centerOnNode(ctx.session, ctx.sourceId);
153
+ }
154
+ });
155
+ register({
156
+ id: 'edge.findTarget',
157
+ title: 'Find Target',
158
+ run: (ctx) => {
159
+ if (ctx.targetId) centerOnNode(ctx.session, ctx.targetId);
160
+ }
161
+ });
162
+ register({
163
+ id: 'edge.delete',
164
+ title: 'Delete',
165
+ run: (ctx) => {
166
+ if (!ctx.edgeId) return;
167
+ const change: EdgeChange = { id: ctx.edgeId, type: 'remove' };
168
+ ctx.session.edgeStore.getState().applyEdgeChanges([change]);
169
+ }
170
+ });
171
+
172
+ register({
173
+ id: 'selection.copy',
174
+ title: 'Copy',
175
+ run: (ctx) => actionsOf(ctx).copySelectionToClipboard()
176
+ });
177
+ register({
178
+ id: 'selection.paste',
179
+ title: 'Paste',
180
+ run: (ctx) => actionsOf(ctx).pasteFromClipboard()
181
+ });
182
+ register({
183
+ id: 'selection.group',
184
+ title: 'Group',
185
+ run: (ctx) => actionsOf(ctx).groupNodes()
186
+ });
187
+ register({
188
+ id: 'selection.selectAll',
189
+ title: 'Select All',
190
+ run: (ctx) =>
191
+ ctx.session.nodeStore
192
+ .getState()
193
+ .setNodes((nodes) => nodes.map((n) => ({ ...n, selected: true })))
194
+ });
195
+
196
+ // Editor-level
197
+ register({
198
+ id: 'editor.save',
199
+ title: 'Save Graph',
200
+ run: (ctx) => {
201
+ void actionsOf(ctx).save();
202
+ }
203
+ });
204
+ register({
205
+ id: 'editor.undo',
206
+ title: 'Undo',
207
+ run: (ctx) => ctx.session.undoManager.undo()
208
+ });
209
+ register({
210
+ id: 'editor.redo',
211
+ title: 'Redo',
212
+ run: (ctx) => ctx.session.undoManager.redo()
213
+ });
214
+ register({
215
+ id: 'editor.find',
216
+ title: 'Find',
217
+ run: (ctx) => ctx.editor.tabStore.getState().openTab('find')
218
+ });
219
+ // `editor.autoLayout` is contributed by the optional layout plugin
220
+ // (`@/plugin/layout`), which owns the heavy elkjs/dagre dependencies. When
221
+ // that plugin is not registered the command is simply unavailable and the
222
+ // bound hotkey no-ops.
223
+
224
+ // View
225
+ register({
226
+ id: 'view.fit',
227
+ title: 'Fit View',
228
+ run: (ctx) => {
229
+ reactFlowOf(ctx.session)?.fitView({
230
+ padding: 0.2,
231
+ includeHiddenNodes: true
232
+ });
233
+ }
234
+ });
235
+ register({
236
+ id: 'view.zoomIn',
237
+ title: 'Zoom In',
238
+ run: (ctx) => reactFlowOf(ctx.session)?.zoomIn({ duration: 300 })
239
+ });
240
+ register({
241
+ id: 'view.zoomOut',
242
+ title: 'Zoom Out',
243
+ run: (ctx) => reactFlowOf(ctx.session)?.zoomOut({ duration: 300 })
244
+ });
245
+ register({
246
+ id: 'view.zoomReset',
247
+ title: 'Reset Zoom',
248
+ run: (ctx) => {
249
+ const rf = reactFlowOf(ctx.session);
250
+ if (!rf) return;
251
+ rf.setViewport({ ...rf.getViewport(), zoom: 1 });
252
+ }
253
+ });
254
+ register({
255
+ id: 'view.toggleGrid',
256
+ title: 'Toggle Grid',
257
+ run: (ctx) => {
258
+ const s = ctx.editor.systemSettings.getState();
259
+ s.setShowGrid(!s.showGrid);
260
+ }
261
+ });
262
+ register({
263
+ id: 'view.toggleMinimap',
264
+ title: 'Toggle Minimap',
265
+ run: (ctx) => {
266
+ const s = ctx.editor.systemSettings.getState();
267
+ s.setShowMinimap(!s.showMinimap);
268
+ }
269
+ });
270
+ register({
271
+ id: 'view.toggleSnapGrid',
272
+ title: 'Toggle Snap to Grid',
273
+ run: (ctx) => {
274
+ const s = ctx.editor.systemSettings.getState();
275
+ s.setSnapGrid(!s.snapGrid);
276
+ }
277
+ });
278
+ };
@@ -0,0 +1,192 @@
1
+ import { createStore, type StoreApi } from 'zustand';
2
+ import { hidden, pinned } from '@/annotations';
3
+ import { isBehaveNode } from '@/util/isBehaveNode';
4
+ import type { CommandContext } from './commands';
5
+
6
+ /** Which canvas target a context-menu item applies to. */
7
+ export type ContextMenuTarget = 'node' | 'edge' | 'selection' | 'pane';
8
+
9
+ /**
10
+ * A registrable context-menu entry. Items dispatch either a registered command
11
+ * (`commandId`) or an inline `onSelect`. `group` controls separator placement;
12
+ * `order` controls position within a target. `when` hides the item dynamically.
13
+ */
14
+ export type ContextMenuItem = {
15
+ id: string;
16
+ target: ContextMenuTarget;
17
+ /** Static text, or a function for state-dependent labels (e.g. Pin/Unpin). */
18
+ label: string | ((ctx: CommandContext) => string);
19
+ keybinding?: string;
20
+ /** Ascending sort within the target. */
21
+ order?: number;
22
+ /** Items with different adjacent groups get a separator between them. */
23
+ group?: string | number;
24
+ when?: (ctx: CommandContext) => boolean;
25
+ /** Dispatch a registered command by id. */
26
+ commandId?: string;
27
+ /** Or run inline (takes precedence over commandId). */
28
+ onSelect?: (ctx: CommandContext) => void;
29
+ };
30
+
31
+ export type ContextMenuStore = {
32
+ items: ContextMenuItem[];
33
+ /** Register (or replace by id) an item. Returns an unregister disposer. */
34
+ register: (item: ContextMenuItem) => () => void;
35
+ unregister: (id: string) => void;
36
+ /** Items for a target, sorted by `order`. Filtering by `when` is the caller's. */
37
+ getItems: (target: ContextMenuTarget) => ContextMenuItem[];
38
+ };
39
+
40
+ export const contextMenuStoreFactory = (): StoreApi<ContextMenuStore> =>
41
+ createStore<ContextMenuStore>((set, get) => ({
42
+ items: [],
43
+ register: (item) => {
44
+ set((s) => ({
45
+ items: [...s.items.filter((i) => i.id !== item.id), item]
46
+ }));
47
+ return () => get().unregister(item.id);
48
+ },
49
+ unregister: (id) =>
50
+ set((s) => ({ items: s.items.filter((i) => i.id !== id) })),
51
+ getItems: (target) =>
52
+ get()
53
+ .items.filter((i) => i.target === target)
54
+ .sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
55
+ }));
56
+
57
+ // --- Default context menus ---------------------------------------------------
58
+
59
+ const nodeAt = (ctx: CommandContext) =>
60
+ ctx.session.nodeStore.getState().nodes.find((n) => n.id === ctx.nodeId);
61
+
62
+ const nodeFlag = (ctx: CommandContext, key: string): boolean => {
63
+ const node = nodeAt(ctx);
64
+ return node && 'data' in node
65
+ ? Boolean((node.data.annotations as Record<string, unknown>)?.[key])
66
+ : false;
67
+ };
68
+
69
+ // The default node items only make sense on behave nodes; presentational node
70
+ // types (notes, groups, ...) register their own `when`-scoped items instead.
71
+ const isBehaveTarget = (ctx: CommandContext): boolean => {
72
+ const node = nodeAt(ctx);
73
+ return node !== undefined && isBehaveNode(node);
74
+ };
75
+
76
+ /**
77
+ * Register the built-in context-menu items, dispatching the default commands.
78
+ * Hosts can add/remove/replace items by id without forking the menu components.
79
+ */
80
+ export const registerDefaultContextMenu = (
81
+ store: StoreApi<ContextMenuStore>
82
+ ): void => {
83
+ const { register } = store.getState();
84
+
85
+ // Node
86
+ register({
87
+ id: 'node.focus',
88
+ target: 'node',
89
+ label: 'Focus',
90
+ order: 10,
91
+ group: 'focus',
92
+ when: isBehaveTarget,
93
+ commandId: 'node.focus'
94
+ });
95
+ register({
96
+ id: 'node.traceUpstream',
97
+ target: 'node',
98
+ label: 'Trace Upstream',
99
+ order: 20,
100
+ group: 'trace',
101
+ when: isBehaveTarget,
102
+ commandId: 'node.traceUpstream'
103
+ });
104
+ register({
105
+ id: 'node.traceDownstream',
106
+ target: 'node',
107
+ label: 'Trace Downstream',
108
+ order: 21,
109
+ group: 'trace',
110
+ when: isBehaveTarget,
111
+ commandId: 'node.traceDownstream'
112
+ });
113
+ register({
114
+ id: 'node.resetTrace',
115
+ target: 'node',
116
+ label: 'Reset Trace',
117
+ order: 30,
118
+ group: 'reset',
119
+ when: isBehaveTarget,
120
+ commandId: 'trace.reset'
121
+ });
122
+ register({
123
+ id: 'node.togglePinned',
124
+ target: 'node',
125
+ label: (ctx) => (nodeFlag(ctx, pinned) ? 'Unpin' : 'Pin'),
126
+ order: 40,
127
+ group: 'visibility',
128
+ when: isBehaveTarget,
129
+ commandId: 'node.togglePinned'
130
+ });
131
+ register({
132
+ id: 'node.toggleHidden',
133
+ target: 'node',
134
+ label: (ctx) => (nodeFlag(ctx, hidden) ? 'Show' : 'Hide'),
135
+ order: 41,
136
+ group: 'visibility',
137
+ when: isBehaveTarget,
138
+ commandId: 'node.toggleHidden'
139
+ });
140
+
141
+ // Edge
142
+ register({
143
+ id: 'edge.findSource',
144
+ target: 'edge',
145
+ label: 'Find Source',
146
+ order: 10,
147
+ group: 'find',
148
+ commandId: 'edge.findSource'
149
+ });
150
+ register({
151
+ id: 'edge.findTarget',
152
+ target: 'edge',
153
+ label: 'Find Target',
154
+ order: 11,
155
+ group: 'find',
156
+ commandId: 'edge.findTarget'
157
+ });
158
+ register({
159
+ id: 'edge.delete',
160
+ target: 'edge',
161
+ label: 'Delete',
162
+ order: 20,
163
+ group: 'delete',
164
+ commandId: 'edge.delete'
165
+ });
166
+
167
+ // Selection
168
+ register({
169
+ id: 'selection.copy',
170
+ target: 'selection',
171
+ label: 'Copy',
172
+ order: 10,
173
+ group: 'clipboard',
174
+ commandId: 'selection.copy'
175
+ });
176
+ register({
177
+ id: 'selection.paste',
178
+ target: 'selection',
179
+ label: 'Paste',
180
+ order: 11,
181
+ group: 'clipboard',
182
+ commandId: 'selection.paste'
183
+ });
184
+ register({
185
+ id: 'selection.group',
186
+ target: 'selection',
187
+ label: 'Group',
188
+ order: 20,
189
+ group: 'group',
190
+ commandId: 'selection.group'
191
+ });
192
+ };