@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
@@ -1,73 +1,95 @@
1
- import { UndoManager } from './undoRedo';
2
1
  import { type StoreApi } from 'zustand';
2
+ import { createStore } from 'zustand/vanilla';
3
3
  import type { Edge, Node, Viewport } from 'reactflow';
4
4
  import { tabStoreFactory, type TabStore } from '@/store/tabs';
5
5
  import { TabLoader } from './tabLoader';
6
6
  import {
7
7
  systemSettingsFactory,
8
+ PERSISTED_SETTING_KEYS,
8
9
  type SystemSettingsStore
9
10
  } from '../store/settings.js';
10
- import { logStoreFactory, type LogStore } from '@/store/logs';
11
+ import {
12
+ settingsSchemaStoreFactory,
13
+ type SettingsSchemaStore,
14
+ type SettingDescriptor
15
+ } from '../store/settingsSchema.js';
11
16
  import { legendStoreFactory, type LegendStore } from '@/store/legend';
12
17
  import { menubarStoreFactory, type MenuBarStore } from '@/store/menubar';
13
18
  import { hotKeyStoreFactory, type HotkeyStore } from '@/store/hotKeys';
14
19
  import { PubSub } from './pubsub';
15
- import {
16
- edgeStoreFactory,
17
- flowStoreFactory,
18
- nodeStoreFactory,
19
- type EdgeStore,
20
- type FlowStore,
21
- type NodeStore
22
- } from '@/store/flow';
23
20
  import { controlsStoreFactory, type ControlsStore } from '@/store/controls';
24
- import { variableStoreFactory, type VariableStore } from '@/store/variables';
25
21
  import { selectionStoreFactory, type SelectionStore } from '@/store/selection';
26
- import { refStoreFactory, type RefStore } from '@/store/refs';
27
- import { Graph } from './graph';
28
- import { actionStoreFactory, type ActionStore } from '@/store/actions';
29
22
  import { registryStoreFactory, type RegistryStore } from '@/store/registry';
30
23
  import { specsStoreFactory, type SpecsStore } from '@/store/specs';
31
- import { traceStoreFactory, type TraceStore } from '@/store/traces';
32
24
  import { specificStoreFactory, type SpecificStore } from '@/store/specific';
33
25
  import {
34
26
  socketGeneratorStoreFactory,
35
27
  type SocketGeneratorStore
36
28
  } from '@/store/socketGenerator';
37
- import { eventsStoreFactory, type EventsStore } from '@/store/events';
38
29
  import {
39
30
  documentationStoreFactory,
40
31
  type DocumentationStore
41
32
  } from '@/store/documentation';
42
33
  import { toolbarStoreFactory, type ToolbarStore } from '@/store/toolbar';
43
- import { layerStoreFactory, type LayerStore } from '@/store/layers';
44
- import { setupSystemActions } from '@/plugin/alignment';
45
- import { Notifications } from './notifications';
46
- import { chatStoreFactory, type ChatStore } from '@/store/chat';
34
+ import { Notifications, type NotificationData } from './notifications';
35
+ import {
36
+ conversionStoreFactory,
37
+ type ConversionStore,
38
+ type ConversionRule
39
+ } from '@/store/conversions';
40
+ import {
41
+ commandStoreFactory,
42
+ registerDefaultCommands,
43
+ type CommandStore,
44
+ type CommandContext
45
+ } from '@/store/commands';
46
+ import {
47
+ contextMenuStoreFactory,
48
+ registerDefaultContextMenu,
49
+ type ContextMenuStore
50
+ } from '@/store/contextMenu';
51
+ import { GraphSession } from './graphSession';
52
+ import { installPersistence, type PersistenceAdapter } from './persistence';
53
+ import { v4 as uuidv4 } from 'uuid';
54
+ import { tabIdForSession } from '@/components/layoutController/utils';
47
55
  import type { Renderable } from 'react-hot-toast';
48
56
  import type { INodeRegistry } from '@/types/NodeMetadata';
49
- import type { LoadablePlugin } from './plugin';
57
+ import type { LoadablePlugin, SessionExtension } from './plugin';
50
58
  import type { UIGraphJSON } from '@/types/graph';
51
59
  import type { GraphJSON } from '@kiberon-labs/behave-graph';
52
60
  import type { LayoutBase } from 'rc-dock';
61
+ import type { FlowStore, NodeStore, EdgeStore } from '@/store/flow';
62
+ import type { VariableStore } from '@/store/variables';
63
+ import type { RefStore } from '@/store/refs';
64
+ import type { ActionStore } from '@/store/actions';
65
+ import type { TraceStore } from '@/store/traces';
66
+ import type { EventsStore } from '@/store/events';
67
+ import type { LogStore } from '@/store/logs';
68
+ import type { LayerStore } from '@/store/layers';
69
+ import type { UndoManager } from './undoRedo';
70
+ import type { Graph } from './graph';
53
71
 
54
- export type NotificationType = 'info' | 'success' | 'error' | 'loading';
55
-
56
- export interface NotificationData {
57
- type: NotificationType;
58
- message: string;
59
- options?: {
60
- id?: string;
61
- duration?: number;
62
- position?: any;
63
- icon?: Renderable;
64
- style?: React.CSSProperties;
65
- className?: string;
66
- ariaLive?: any;
72
+ /**
73
+ * Editor-level pubsub topics. These are global to the editor and shared across
74
+ * every open graph. Augment this interface (not {@link GraphPubSys}) for events
75
+ * that are not tied to a specific graph.
76
+ */
77
+ export interface EditorPubSys {
78
+ notification: NotificationData;
79
+ 'notification:dismiss': {
80
+ toastId?: string;
67
81
  };
82
+ 'layout:saved': LayoutBase;
83
+ 'graph:saved': UIGraphJSON;
84
+ 'graph:inner:saved': GraphJSON;
68
85
  }
69
86
 
70
- export interface PubSys {
87
+ /**
88
+ * Per-graph pubsub topics. Each {@link GraphSession} owns its own bus typed with
89
+ * this interface, so events stay isolated to the graph that produced them.
90
+ * Augment this interface for events that belong to a single graph.
91
+ */
92
+ export interface GraphPubSys {
71
93
  'edge:added': Edge;
72
94
  'node:added': Node;
73
95
  'edge:removed': Edge;
@@ -75,95 +97,162 @@ export interface PubSys {
75
97
  graphAnnotationsChanged: {
76
98
  [key: string]: any;
77
99
  };
78
- 'aiNode:trigger': {
79
- nodeId: string;
80
- };
81
100
  saveViewport: {
82
101
  index: number;
83
102
  viewport: Viewport;
84
103
  };
85
- missingViewPort: {
86
- index: number;
87
- };
88
-
89
- notification: NotificationData;
90
- 'notification:dismiss': {
91
- toastId?: string;
92
- };
93
- 'layout:saved': LayoutBase;
94
- 'graph:saved': UIGraphJSON;
95
- 'graph:inner:saved': GraphJSON;
96
- 'chat:userMessage': {
97
- content: string;
98
- };
99
104
  }
100
105
 
106
+ /**
107
+ * Combined pubsub surface kept for backwards compatibility. Prefer the split
108
+ * {@link EditorPubSys} / {@link GraphPubSys} interfaces.
109
+ */
110
+ export interface PubSys extends EditorPubSys, GraphPubSys {}
111
+
101
112
  /**
102
113
  * Use this to extend the System interface when adding plugins
103
114
  */
104
115
  export interface ISystem {}
105
116
 
117
+ /** Minimal storage adapter for persisting editor settings. */
118
+ export type SettingsStorage = {
119
+ getItem: (key: string) => string | null;
120
+ setItem: (key: string, value: string) => void;
121
+ };
122
+
123
+ /** Serialized editor-level settings (UI toggles + custom type conversions). */
124
+ export type EditorSettingsJSON = {
125
+ settings?: Record<string, any>;
126
+ conversions?: ConversionRule[];
127
+ };
128
+
129
+ const SETTINGS_STORAGE_KEY = 'behave-graph:editor-settings';
130
+
131
+ const settingSetterName = (key: string): string =>
132
+ `set${key.charAt(0).toUpperCase()}${key.slice(1)}`;
133
+
134
+ const defaultSettingsStorage = (): SettingsStorage | undefined => {
135
+ try {
136
+ if (typeof localStorage !== 'undefined') return localStorage;
137
+ } catch {
138
+ // localStorage access can throw in sandboxed contexts
139
+ }
140
+ return undefined;
141
+ };
142
+
143
+ /**
144
+ * Observable registry of open graph sessions plus the currently focused one.
145
+ * Backed by a zustand store so panels rendered outside of a graph tab can
146
+ * subscribe and re-render when the active graph changes.
147
+ */
148
+ export type ActiveGraphStore = {
149
+ activeGraphId: string | null;
150
+ sessions: Record<string, GraphSession>;
151
+ setActiveGraph: (id: string | null) => void;
152
+ addSession: (session: GraphSession) => void;
153
+ removeSession: (id: string) => void;
154
+ getActive: () => GraphSession | undefined;
155
+ };
156
+
157
+ const activeGraphStoreFactory = () =>
158
+ createStore<ActiveGraphStore>((set, get) => ({
159
+ activeGraphId: null,
160
+ sessions: {},
161
+ setActiveGraph: (activeGraphId) => set(() => ({ activeGraphId })),
162
+ addSession: (session) =>
163
+ set((state) => ({
164
+ sessions: { ...state.sessions, [session.id]: session }
165
+ })),
166
+ removeSession: (id) =>
167
+ set((state) => {
168
+ const next = { ...state.sessions };
169
+ delete next[id];
170
+ const activeGraphId =
171
+ state.activeGraphId === id ? null : state.activeGraphId;
172
+ return { sessions: next, activeGraphId };
173
+ }),
174
+ getActive: () => {
175
+ const state = get();
176
+ return state.activeGraphId
177
+ ? state.sessions[state.activeGraphId]
178
+ : undefined;
179
+ }
180
+ }));
181
+
182
+ /**
183
+ * The editor-level system. Holds state that is shared across every open graph
184
+ * (settings, registry, specs, menubar, tabs, ...) plus an observable registry of
185
+ * per-graph {@link GraphSession} instances. Per-graph state itself lives on the
186
+ * sessions, not here.
187
+ *
188
+ * The class is intentionally still named `System` so existing `declare module`
189
+ * augmentations (`interface System { ... }`) keep merging and the public API is
190
+ * stable; `EditorSystem` is exported as an alias for new code.
191
+ */
106
192
  export class System implements ISystem {
107
- public readonly actionStore: StoreApi<ActionStore>;
108
- public readonly pubsub = new PubSub<PubSys>();
109
- public readonly undoManager = new UndoManager();
110
- public readonly flowStore: StoreApi<FlowStore>;
111
- public readonly controlStore: StoreApi<ControlsStore>;
112
- public readonly variableStore: StoreApi<VariableStore>;
113
- public readonly selectionStore: StoreApi<SelectionStore>;
114
- public readonly refStore: StoreApi<RefStore>;
193
+ public readonly pubsub = new PubSub<EditorPubSys>();
115
194
  public readonly tabStore: StoreApi<TabStore>;
116
- protected deps: Record<string, unknown> = {};
117
- public readonly registry: StoreApi<RegistryStore>;
118
195
  public readonly tabLoader: TabLoader;
119
196
  public readonly systemSettings: StoreApi<SystemSettingsStore>;
120
- public readonly logsStore: StoreApi<LogStore>;
197
+ /** Registry of setting descriptors driving the auto-generated Settings panel. */
198
+ public readonly settingsSchema: StoreApi<SettingsSchemaStore>;
121
199
  public readonly legendStore: StoreApi<LegendStore>;
200
+ public readonly menubarStore: StoreApi<MenuBarStore>;
122
201
  public readonly hotKeyStore: StoreApi<HotkeyStore>;
123
- public readonly edgeStore: StoreApi<EdgeStore>;
124
- public readonly nodeStore: StoreApi<NodeStore>;
202
+ public readonly registry: StoreApi<RegistryStore>;
125
203
  public readonly specStore: StoreApi<SpecsStore>;
126
204
  public readonly specificStore: StoreApi<SpecificStore>;
127
205
  public readonly socketGeneratorStore: StoreApi<SocketGeneratorStore>;
128
- public readonly eventsStore: StoreApi<EventsStore>;
129
206
  public readonly documentationStore: StoreApi<DocumentationStore>;
130
207
  public readonly toolbarStore: StoreApi<ToolbarStore>;
131
- public readonly layerStore: StoreApi<LayerStore>;
132
- public readonly menubarStore: StoreApi<MenuBarStore>;
133
- public readonly graph: Graph;
134
- public readonly traceStore: StoreApi<TraceStore>;
135
- public readonly chatStore: StoreApi<ChatStore>;
208
+ public readonly controlStore: StoreApi<ControlsStore>;
209
+ /** User/plugin-defined automatic type conversions for auto-convert. */
210
+ public readonly conversionStore: StoreApi<ConversionStore>;
211
+ /** Named, dispatchable commands shared across hotkeys/menus/toolbar. */
212
+ public readonly commandStore: StoreApi<CommandStore>;
213
+ /** Per-target context-menu item registry (node/edge/selection/pane). */
214
+ public readonly contextMenuStore: StoreApi<ContextMenuStore>;
136
215
  public readonly notifications: Notifications = new Notifications(this);
137
216
 
217
+ /** Observable registry of open graph sessions + the focused one. */
218
+ public readonly activeGraph: StoreApi<ActiveGraphStore>;
219
+
220
+ /** Editor-level extensions applied to every graph session on creation. */
221
+ private readonly sessionExtensions = new Set<SessionExtension>();
222
+
223
+ /** Disposer for the currently installed graph/layout save handlers. */
224
+ private persistenceDisposer: () => void = () => {};
225
+
226
+ protected deps: Record<string, unknown> = {};
227
+
138
228
  /**
139
- * Create a new System instance
229
+ * Create a new editor System instance
140
230
  * @param registry - INodeRegistry containing nodes and values metadata
141
231
  */
142
232
  constructor(registry?: INodeRegistry) {
233
+ this.activeGraph = activeGraphStoreFactory();
143
234
  this.tabStore = tabStoreFactory();
144
235
  this.controlStore = controlsStoreFactory();
145
- this.variableStore = variableStoreFactory();
146
- this.refStore = refStoreFactory();
147
236
  this.systemSettings = systemSettingsFactory();
148
- this.logsStore = logStoreFactory();
237
+ // Seeded with the built-in setting descriptors (DEFAULT_SETTINGS). Plugins
238
+ // append their own via registerSetting(...).
239
+ this.settingsSchema = settingsSchemaStoreFactory();
149
240
  this.legendStore = legendStoreFactory();
150
- this.eventsStore = eventsStoreFactory();
151
- this.nodeStore = nodeStoreFactory(this);
152
- this.edgeStore = edgeStoreFactory(this);
153
- this.flowStore = flowStoreFactory(this);
154
- this.selectionStore = selectionStoreFactory(this);
155
- this.hotKeyStore = hotKeyStoreFactory(this);
156
- this.actionStore = actionStoreFactory(this);
157
241
  this.registry = registryStoreFactory();
158
242
  this.specStore = specsStoreFactory(this);
159
243
  this.socketGeneratorStore = socketGeneratorStoreFactory();
160
244
  this.specificStore = specificStoreFactory();
161
245
  this.documentationStore = documentationStoreFactory();
162
246
  this.toolbarStore = toolbarStoreFactory();
163
- this.layerStore = layerStoreFactory(this);
247
+ this.conversionStore = conversionStoreFactory();
248
+ this.commandStore = commandStoreFactory();
249
+ this.contextMenuStore = contextMenuStoreFactory();
250
+ this.hotKeyStore = hotKeyStoreFactory(this);
164
251
 
165
- this.chatStore = chatStoreFactory();
166
- this.graph = new Graph(this);
252
+ // Seed the built-in commands + context-menu items. Hosts can override by id
253
+ // or add their own via these registries.
254
+ registerDefaultCommands(this.commandStore);
255
+ registerDefaultContextMenu(this.contextMenuStore);
167
256
 
168
257
  // Handle registry initialization
169
258
  if (registry) {
@@ -174,10 +263,125 @@ export class System implements ISystem {
174
263
  this.menubarStore = menubarStoreFactory();
175
264
  this.tabLoader = new TabLoader(this);
176
265
 
177
- this.traceStore = traceStoreFactory(this);
266
+ // Wire the default graph/layout save handlers (download-to-file) so a fresh
267
+ // editor has working Save actions out of the box. Hosts that persist
268
+ // elsewhere override via enablePersistence(...) or opt out with
269
+ // disablePersistence().
270
+ this.persistenceDisposer = installPersistence(this);
271
+ }
272
+
273
+ /**
274
+ * The currently focused graph session, if any.
275
+ */
276
+ get session(): GraphSession | undefined {
277
+ return this.activeGraph.getState().getActive();
278
+ }
279
+
280
+ /**
281
+ * Create a new graph session, register it and (by default) make it active.
282
+ */
283
+ createSession(
284
+ id = 'graph',
285
+ options?: { activate?: boolean; name?: string }
286
+ ): GraphSession {
287
+ const session = new GraphSession(this, id, options?.name ?? 'Graph');
288
+ this.activeGraph.getState().addSession(session);
289
+ // Let editor plugins extend the fully-constructed session before it becomes
290
+ // active, so panels reacting to the active graph see a complete instance.
291
+ for (const extension of this.sessionExtensions) {
292
+ this.applySessionExtension(session, extension);
293
+ }
294
+ if (options?.activate ?? true) {
295
+ this.activeGraph.getState().setActiveGraph(id);
296
+ }
297
+ return session;
298
+ }
299
+
300
+ /**
301
+ * Look up an existing session by id, creating an empty one if missing.
302
+ */
303
+ getOrCreateSession(id: string): GraphSession {
304
+ return (
305
+ this.activeGraph.getState().sessions[id] ??
306
+ this.createSession(id, { activate: false })
307
+ );
308
+ }
309
+
310
+ /**
311
+ * Create a brand new, empty graph in its own tab and focus it.
312
+ */
313
+ newGraph(name?: string): GraphSession {
314
+ const id = uuidv4();
315
+ const count = Object.keys(this.activeGraph.getState().sessions).length;
316
+ const session = this.createSession(id, {
317
+ activate: true,
318
+ name: name ?? `Untitled ${count}`
319
+ });
320
+ this.tabStore.getState().openTab(tabIdForSession(id));
321
+ return session;
322
+ }
323
+
324
+ /**
325
+ * Dispose a graph session and remove it from the registry. Called when its
326
+ * tab is closed.
327
+ */
328
+ disposeSession(id: string): void {
329
+ const session = this.activeGraph.getState().sessions[id];
330
+ if (!session) return;
331
+ session.dispose();
332
+ this.activeGraph.getState().removeSession(id);
333
+ }
178
334
 
179
- // Setup system-level action subscribers
180
- setupSystemActions(this);
335
+ // ---------------------------------------------------------------------------
336
+ // Focused-graph accessors.
337
+ //
338
+ // All graph-canvas and dock-panel consumers read per-graph state through
339
+ // useGraph()/useActiveGraph(), and execution is per-session via each
340
+ // GraphSession's run controller. These getters remain only for editor-level
341
+ // surfaces that imperatively act on the *focused* graph , the hotkey handlers,
342
+ // layout utilities, the menubar, and server-metadata fan-out. They resolve to
343
+ // the focused session, consistent with the "panels follow focus" model; they
344
+ // are a convenience, not a single-open-graph limit (multiple graphs are open
345
+ // and independently runnable at once).
346
+ // ---------------------------------------------------------------------------
347
+ get flowStore(): StoreApi<FlowStore> {
348
+ return this.session!.flowStore;
349
+ }
350
+ get nodeStore(): StoreApi<NodeStore> {
351
+ return this.session!.nodeStore;
352
+ }
353
+ get edgeStore(): StoreApi<EdgeStore> {
354
+ return this.session!.edgeStore;
355
+ }
356
+ get variableStore(): StoreApi<VariableStore> {
357
+ return this.session!.variableStore;
358
+ }
359
+ get selectionStore(): StoreApi<SelectionStore> {
360
+ return this.session!.selectionStore;
361
+ }
362
+ get refStore(): StoreApi<RefStore> {
363
+ return this.session!.refStore;
364
+ }
365
+ get actionStore(): StoreApi<ActionStore> {
366
+ return this.session!.actionStore;
367
+ }
368
+ get traceStore(): StoreApi<TraceStore> {
369
+ return this.session!.traceStore;
370
+ }
371
+ get eventsStore(): StoreApi<EventsStore> {
372
+ return this.session!.eventsStore;
373
+ }
374
+ get logsStore(): StoreApi<LogStore> {
375
+ return this.session!.logsStore;
376
+ }
377
+ get layerStore(): StoreApi<LayerStore> {
378
+ return this.session!.layerStore;
379
+ }
380
+ get graph(): Graph {
381
+ return this.session!.graph;
382
+ }
383
+ get undoManager(): UndoManager {
384
+ return this.session!.undoManager;
181
385
  }
182
386
 
183
387
  /**
@@ -220,4 +424,230 @@ export class System implements ISystem {
220
424
  await plugin.loader(this, options as TOptions);
221
425
  console.log(`Plugin loaded: ${plugin.opts.name}`);
222
426
  }
427
+
428
+ /**
429
+ * Register an extension applied to every {@link GraphSession}. It runs against
430
+ * each graph already open at registration time and against every graph created
431
+ * afterwards (via {@link createSession}), so a plugin can attach per-graph
432
+ * state to new graph instances from a single editor-level registration.
433
+ *
434
+ * If the extension returns a cleanup it is wired to the session's
435
+ * {@link GraphSession.onDispose} and runs when that graph's tab is closed.
436
+ *
437
+ * @returns an unregister function that stops the extension from applying to
438
+ * sessions created later. It does not retroactively tear down sessions already
439
+ * extended (those clean up on their own dispose).
440
+ */
441
+ registerSessionExtension(extension: SessionExtension): () => void {
442
+ this.sessionExtensions.add(extension);
443
+ // Apply to graphs that already exist so registration order doesn't matter.
444
+ for (const session of Object.values(this.activeGraph.getState().sessions)) {
445
+ this.applySessionExtension(session, extension);
446
+ }
447
+ return () => {
448
+ this.sessionExtensions.delete(extension);
449
+ };
450
+ }
451
+
452
+ /** Run a single session extension, wiring any returned cleanup to dispose. */
453
+ private applySessionExtension(
454
+ session: GraphSession,
455
+ extension: SessionExtension
456
+ ): void {
457
+ try {
458
+ const cleanup = extension(session);
459
+ if (typeof cleanup === 'function') session.onDispose(cleanup);
460
+ } catch (err) {
461
+ console.error('Session extension failed', err);
462
+ }
463
+ }
464
+
465
+ /**
466
+ * Register a custom automatic type conversion (e.g. from a profile plugin) so
467
+ * auto-convert can splice in the given node for that type pair.
468
+ */
469
+ registerConversion(rule: ConversionRule): void {
470
+ this.conversionStore.getState().registerConversion(rule);
471
+ }
472
+
473
+ /**
474
+ * Contribute a setting to the schema-driven Settings panel. The panel
475
+ * auto-generates a row for it (grouped under `descriptor.section`), and its
476
+ * default value is seeded into the settings store if not already present.
477
+ * Built-in settings are registered the same way at construction.
478
+ *
479
+ * @example
480
+ * system.registerSetting({
481
+ * key: 'graphRunner.autoStart', section: 'Graph Runner', type: 'boolean',
482
+ * default: false, title: 'Auto-start runner'
483
+ * });
484
+ */
485
+ registerSetting(descriptor: SettingDescriptor): void {
486
+ if (
487
+ descriptor.type !== 'custom' &&
488
+ !(descriptor.key in this.systemSettings.getState())
489
+ ) {
490
+ this.systemSettings
491
+ .getState()
492
+ .setSetting(descriptor.key, descriptor.default);
493
+ }
494
+ this.settingsSchema.getState().registerSetting(descriptor);
495
+ }
496
+
497
+ /** Contribute several settings at once. */
498
+ registerSettings(descriptors: SettingDescriptor[]): void {
499
+ for (const descriptor of descriptors) this.registerSetting(descriptor);
500
+ }
501
+
502
+ /** Read a setting value by key (built-in or plugin-contributed). */
503
+ getSetting<T = unknown>(key: string): T {
504
+ return this.systemSettings.getState()[key] as T;
505
+ }
506
+
507
+ /** Set a setting value by key. Persists if the descriptor allows it. */
508
+ setSetting(key: string, value: unknown): void {
509
+ this.systemSettings.getState().setSetting(key, value);
510
+ }
511
+
512
+ /**
513
+ * Dispatch a registered command against the focused graph (or a supplied
514
+ * session). Convenience used by hotkeys, the menubar and the toolbar so they
515
+ * share one dispatch path. No-ops if there is no graph to act on.
516
+ */
517
+ runCommand(id: string, ctx?: Partial<CommandContext>): void | Promise<void> {
518
+ const session = ctx?.session ?? this.session;
519
+ if (!session) return;
520
+ return this.commandStore
521
+ .getState()
522
+ .run(id, { editor: this, session, ...ctx });
523
+ }
524
+
525
+ /**
526
+ * Override where the editor's Save actions send their data. The editor
527
+ * publishes `graph:saved`, `graph:inner:saved` and `layout:saved`; by default
528
+ * each triggers a JSON file download. Pass an adapter to redirect any subset
529
+ * of those to your own sink (write to disk, POST to a backend, ...); topics
530
+ * you omit keep the file-download default. Replaces any previously installed
531
+ * handlers and returns a disposer.
532
+ *
533
+ * This governs graph/layout saving only; editor *settings* persistence is
534
+ * separate , see {@link enableSettingsPersistence}.
535
+ *
536
+ * @example
537
+ * // Persist graphs to a backend, keep the default layout download.
538
+ * system.enablePersistence({
539
+ * saveGraph: (graph) => api.put('/graphs/current', graph)
540
+ * });
541
+ */
542
+ enablePersistence(adapter?: Partial<PersistenceAdapter>): () => void {
543
+ this.persistenceDisposer();
544
+ this.persistenceDisposer = installPersistence(this, adapter);
545
+ return this.persistenceDisposer;
546
+ }
547
+
548
+ /**
549
+ * Remove the default (or custom) graph/layout save handlers entirely. Use this
550
+ * when the host handles saving through a channel of its own and the built-in
551
+ * file download would be redundant (e.g. the VS Code extension).
552
+ */
553
+ disablePersistence(): void {
554
+ this.persistenceDisposer();
555
+ this.persistenceDisposer = () => {};
556
+ }
557
+
558
+ /**
559
+ * Serialize the persistable editor settings , the UI toggles plus any custom
560
+ * type conversions , to a plain JSON object.
561
+ */
562
+ serializeSettings(): EditorSettingsJSON {
563
+ const state = this.systemSettings.getState() as Record<string, any>;
564
+ const settings: Record<string, any> = {};
565
+ for (const key of this.persistedSettingKeys()) settings[key] = state[key];
566
+ return {
567
+ settings,
568
+ conversions: this.conversionStore.getState().conversions
569
+ };
570
+ }
571
+
572
+ /**
573
+ * Keys round-tripped to persisted storage: the built-in persisted keys plus
574
+ * every plugin-contributed descriptor that opts in (`persist !== false`).
575
+ * Custom descriptors carry no backing value, so they are excluded.
576
+ */
577
+ private persistedSettingKeys(): string[] {
578
+ const keys = new Set<string>(PERSISTED_SETTING_KEYS as string[]);
579
+ for (const descriptor of this.settingsSchema.getState().settings) {
580
+ if (descriptor.type === 'custom' || descriptor.persist === false)
581
+ continue;
582
+ keys.add(descriptor.key);
583
+ }
584
+ return [...keys];
585
+ }
586
+
587
+ /**
588
+ * Apply previously-serialized editor settings (toggles + conversions).
589
+ * Unknown keys are ignored.
590
+ */
591
+ applySettings(json: EditorSettingsJSON | undefined): void {
592
+ if (!json) return;
593
+ const state = this.systemSettings.getState() as Record<string, any>;
594
+ for (const [key, value] of Object.entries(json.settings ?? {})) {
595
+ if (value === undefined) continue;
596
+ const setter = state[settingSetterName(key)];
597
+ if (typeof setter === 'function') setter(value);
598
+ // Plugin-contributed keys have no typed setter: write generically.
599
+ else if (typeof state.setSetting === 'function')
600
+ state.setSetting(key, value);
601
+ }
602
+ if (Array.isArray(json.conversions)) {
603
+ this.conversionStore.getState().setConversions(json.conversions);
604
+ }
605
+ }
606
+
607
+ /**
608
+ * Persist editor settings + conversions to a storage adapter (localStorage by
609
+ * default). Applies any saved state immediately, then saves (debounced) on
610
+ * change. Returns a disposer. A host can pass its own storage adapter (e.g.
611
+ * one backed by VS Code workspace state) instead of localStorage.
612
+ */
613
+ enableSettingsPersistence(
614
+ storage: SettingsStorage | undefined = defaultSettingsStorage()
615
+ ): () => void {
616
+ if (!storage) return () => {};
617
+
618
+ try {
619
+ const raw = storage.getItem(SETTINGS_STORAGE_KEY);
620
+ if (raw) this.applySettings(JSON.parse(raw));
621
+ } catch {
622
+ // ignore malformed saved state
623
+ }
624
+
625
+ let timer: ReturnType<typeof setTimeout> | undefined;
626
+ const save = () => {
627
+ if (timer) clearTimeout(timer);
628
+ timer = setTimeout(() => {
629
+ try {
630
+ storage.setItem(
631
+ SETTINGS_STORAGE_KEY,
632
+ JSON.stringify(this.serializeSettings())
633
+ );
634
+ } catch {
635
+ // ignore quota / serialization errors
636
+ }
637
+ }, 300);
638
+ };
639
+
640
+ const unsubSettings = this.systemSettings.subscribe(save);
641
+ const unsubConversions = this.conversionStore.subscribe(save);
642
+ return () => {
643
+ if (timer) clearTimeout(timer);
644
+ unsubSettings();
645
+ unsubConversions();
646
+ };
647
+ }
223
648
  }
649
+
650
+ /**
651
+ * Alias for {@link System} expressing its role as the shared editor-level system.
652
+ */
653
+ export { System as EditorSystem };