@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,5 +1,3 @@
1
- import { applyDagreLayout } from '@/layout/dagre';
2
- import { applyElkLayout } from '@/layout/elk';
3
1
  import type { System } from '@/system';
4
2
  import { create } from 'zustand';
5
3
 
@@ -9,284 +7,317 @@ export type HotkeyStore = {
9
7
  keymap: Record<string, string | string[]>;
10
8
  handlers: Record<string, Handler>;
11
9
  descriptions: Record<string, string>;
10
+ /** Reverse map: command id -> keymap action, so any UI surface that runs a
11
+ * command can look up its live shortcut without knowing the action name. */
12
+ commandToAction: Record<string, string>;
12
13
  register(val: {
13
14
  action: string;
14
15
  trigger: string | string[];
15
16
  description?: string;
16
17
  handler?: Handler;
18
+ /** Command id this binding invokes; enables shortcut hints in menus. */
19
+ command?: string;
17
20
  }): void;
18
21
  registerDescription(action: string, description: string): void;
19
22
  registerHandler(action: string, handler: Handler): void;
23
+ /**
24
+ * Formatted shortcut hint for a command id (e.g. `'Ctrl+Shift+←'`), derived
25
+ * live from the keymap, or undefined if the command has no bound key.
26
+ */
27
+ getCommandKeybinding(commandId: string): string | undefined;
20
28
  };
21
29
 
22
- export const hotKeyStoreFactory = (sys: System) =>
23
- create<HotkeyStore>((set) => ({
24
- keymap: {
25
- SELECT_ALL: 'ctrl+a',
26
- DUPLICATE: ['command+d', 'ctrl+d'],
27
- GROUP: 'ctrl+g',
28
- SAVE: 'ctrl+s',
29
- FIND: ['ctrl+f', 'command+f'],
30
- AUTO_LAYOUT: 'shift+alt+f',
31
- COPY: ['command+c', 'ctrl+c'],
32
- PASTE: ['command+v', 'ctrl+v'],
33
- DELETE: ['delete', 'del', 'backspace'],
34
- UNDO: ['command+z', 'ctrl+z'],
35
- REDO: ['command+shift+z', 'ctrl+shift+z'],
36
- SAVE_VIEWPORT: [
37
- 'command+1',
38
- 'command+2',
39
- 'command+3',
40
- 'command+4',
41
- 'command+5',
42
- 'command+6',
43
- 'command+7',
44
- 'command+8',
45
- 'command+9',
46
- 'ctrl+1',
47
- 'ctrl+2',
48
- 'ctrl+3',
49
- 'ctrl+4',
50
- 'ctrl+5',
51
- 'ctrl+6',
52
- 'ctrl+7',
53
- 'ctrl+8',
54
- 'ctrl+9'
55
- ],
56
- RECALL_VIEWPORT: ['1', '2', '3', '4', '5', '6', '7', '8', '9'],
57
- ZOOM_IN: ['ctrl+plus', 'command+plus'],
58
- ZOOM_OUT: ['command+-', 'ctrl+-'],
59
- ZOOM_RESET: 'ctrl+0',
60
- TOGGLE_GRID: ['command+shift+g', 'ctrl+shift+g'],
61
- TOGGLE_MINIMAP: ['command+shift+m', 'ctrl+shift+m'],
62
- TOGGLE_SNAP_GRID: ['command+shift+s', 'ctrl+shift+s'],
63
- FIT_VIEW: ['f'],
64
- TRACE_DOWNSTREAM: 'ctrl+shift+right',
65
- TRACE_UPSTREAM: 'ctrl+shift+left'
66
- },
67
- descriptions: {
68
- TRACE_DOWNSTREAM: 'Trace Downstream',
69
- TRACE_UPSTREAM: 'Trace Upstream',
70
- AUTO_LAYOUT: 'Auto Layout',
71
- COPY: 'Copy',
72
- PASTE: 'Paste',
73
- DELETE: 'Delete',
74
- UNDO: 'Undo',
75
- REDO: 'Redo',
76
- SELECT_ALL: 'Select All',
77
- DUPLICATE: 'Duplicate',
78
- GROUP: 'Group',
79
- SAVE: 'Save',
80
- FIND: 'Find',
81
- FIT_VIEW: 'Fit View',
82
- ZOOM_IN: 'Zoom In',
83
- ZOOM_OUT: 'Zoom Out',
84
- ZOOM_RESET: 'Zoom Reset',
85
- TOGGLE_GRID: 'Toggle Grid',
86
- TOGGLE_MINIMAP: 'Toggle Minimap',
87
- TOGGLE_SNAP_GRID: 'Toggle Snap Grid',
88
- SAVE_VIEWPORT: 'Save Viewport',
89
- RECALL_VIEWPORT: 'Recall Viewport'
90
- },
91
- handlers: {
92
- SAVE: (event) => {
93
- if (event) {
94
- event.preventDefault();
95
- event.stopPropagation();
96
- }
97
- sys.actionStore.getState().actions.save();
98
- },
30
+ const MODIFIER_LABELS: Record<string, string> = {
31
+ ctrl: 'Ctrl',
32
+ control: 'Ctrl',
33
+ cmd: 'Cmd',
34
+ command: 'Cmd',
35
+ meta: 'Cmd',
36
+ shift: 'Shift',
37
+ alt: 'Alt',
38
+ option: 'Alt'
39
+ };
99
40
 
100
- GROUP: () => {
101
- sys.actionStore.getState().actions.groupNodes();
102
- },
41
+ const KEY_LABELS: Record<string, string> = {
42
+ left: '←',
43
+ right: '→',
44
+ up: '↑',
45
+ down: '↓',
46
+ plus: '+',
47
+ minus: '-',
48
+ esc: 'Esc',
49
+ escape: 'Esc',
50
+ del: 'Del',
51
+ delete: 'Del',
52
+ backspace: 'Backspace',
53
+ enter: 'Enter',
54
+ return: 'Enter',
55
+ space: 'Space',
56
+ tab: 'Tab'
57
+ };
103
58
 
104
- COPY: (event) => {
105
- if (event) {
106
- event.preventDefault();
107
- event.stopPropagation();
108
- }
109
- sys.actionStore.getState().actions.copySelectionToClipboard();
110
- },
111
- PASTE: async (event) => {
112
- if (event) {
113
- event.preventDefault();
114
- event.stopPropagation();
115
- }
59
+ const formatKeyPart = (part: string): string => {
60
+ const lower = part.toLowerCase();
61
+ if (MODIFIER_LABELS[lower]) return MODIFIER_LABELS[lower];
62
+ if (KEY_LABELS[lower]) return KEY_LABELS[lower];
63
+ if (part.length === 1) return part.toUpperCase();
64
+ return part.charAt(0).toUpperCase() + part.slice(1);
65
+ };
116
66
 
117
- await sys.actionStore.getState().actions.pasteFromClipboard();
118
- },
119
- TRACE_UPSTREAM: (event) => {
120
- if (event) {
121
- event.preventDefault();
122
- event.stopPropagation();
123
- const selection = sys.selectionStore.getState().selectedNodeId;
124
- if (!selection) {
125
- return;
126
- }
127
- sys.actionStore.getState().actions.traceUpstream(selection);
128
- }
129
- },
130
- TRACE_DOWNSTREAM: (event) => {
131
- if (event) {
132
- event.preventDefault();
133
- event.stopPropagation();
134
- const selection = sys.selectionStore.getState().selectedNodeId;
135
- if (!selection) {
136
- return;
137
- }
138
- sys.actionStore.getState().actions.traceDownstream(selection);
139
- }
140
- },
141
- FIT_VIEW: (event) => {
142
- if (event) {
143
- event.preventDefault();
144
- const reactFlowInstance = sys.refStore.getState().getRef('reactflow');
145
- reactFlowInstance?.fitView({
146
- padding: 0.2,
147
- includeHiddenNodes: true
148
- });
149
- }
150
- },
67
+ /**
68
+ * Format a keymap trigger into a human-readable hint, e.g. `'ctrl+shift+left'`
69
+ * -> `'Ctrl+Shift+←'`. When several triggers are bound (cross-platform), prefer
70
+ * the `ctrl` variant so menus show one stable shortcut. Returns undefined for an
71
+ * empty/unset trigger.
72
+ */
73
+ export const formatTrigger = (
74
+ trigger: string | string[] | undefined
75
+ ): string | undefined => {
76
+ let chosen: string | undefined;
77
+ if (Array.isArray(trigger)) {
78
+ chosen =
79
+ trigger.find((t) => t.toLowerCase().includes('ctrl')) ?? trigger[0];
80
+ } else {
81
+ chosen = trigger;
82
+ }
83
+ if (!chosen) return undefined;
84
+ return chosen
85
+ .split('+')
86
+ .map((p) => formatKeyPart(p.trim()))
87
+ .join('+');
88
+ };
151
89
 
152
- TOGGLE_SNAP_GRID: (event) => {
153
- if (event) {
154
- event.preventDefault();
155
- const settings = sys.systemSettings.getState();
156
- settings.setSnapGrid(!settings.snapGrid);
157
- }
158
- },
159
- TOGGLE_GRID: (event) => {
160
- if (event) {
161
- event.preventDefault();
162
- const settings = sys.systemSettings.getState();
163
- settings.setShowGrid(!settings.showGrid);
164
- }
165
- },
166
- SAVE_VIEWPORT: (event) => {
167
- if (event) {
168
- event?.preventDefault();
169
- const reactFlowInstance = sys.refStore.getState().getRef('reactflow');
170
- const graph = sys.graph;
171
- if (reactFlowInstance) {
172
- const currentViewport = reactFlowInstance.getViewport();
173
- const key = event.key; // Get pressed key (e.g., '1', '2', etc.)
174
- const viewportIndex = parseInt(key) - 1; // Calculate 0-based index
90
+ /**
91
+ * Declarative default hotkey bindings. One table is the single source of truth
92
+ * for the keymap, descriptions, and handlers (previously three objects that had
93
+ * to be kept in sync). Most bindings just dispatch a registered command by id;
94
+ * the few that need key-specific context (the pressed node / number key) use a
95
+ * custom `handler`.
96
+ */
97
+ type HotkeyBinding = {
98
+ action: string;
99
+ trigger: string | string[];
100
+ description: string;
101
+ /** Dispatch this command id (with preventDefault/stopPropagation). */
102
+ command?: string;
103
+ /**
104
+ * For handler-based bindings, the command id this key maps to. Used only to
105
+ * surface the shortcut hint in menus; does not affect what the key does.
106
+ */
107
+ hintCommand?: string;
108
+ /** Or run custom key-specific logic. */
109
+ handler?: (sys: System, e?: KeyboardEvent) => void;
110
+ };
175
111
 
176
- if (viewportIndex >= 0 && viewportIndex < 9) {
177
- graph.setViewport(viewportIndex, currentViewport);
178
- sys.notifications.info(`Saved viewport ${viewportIndex + 1}`);
179
- }
180
- }
181
- }
182
- },
183
- RECALL_VIEWPORT: (event) => {
184
- if (event) {
185
- event.preventDefault();
186
- const key = event.key;
187
- const graph = sys.graph;
188
- const reactFlowInstance = sys.refStore.getState().getRef('reactflow');
112
+ const NUMBER_TRIGGERS = ['1', '2', '3', '4', '5', '6', '7', '8', '9'];
189
113
 
190
- const viewportIndex = parseInt(key) - 1;
114
+ const traceFromSelection =
115
+ (commandId: string) => (sys: System, e?: KeyboardEvent) => {
116
+ e?.preventDefault();
117
+ e?.stopPropagation();
118
+ const selection = sys.selectionStore.getState().selectedNodeId;
119
+ if (selection) sys.runCommand(commandId, { nodeId: selection });
120
+ };
191
121
 
192
- if (viewportIndex >= 0 && viewportIndex < 9 && reactFlowInstance) {
193
- const viewport = graph.viewports[viewportIndex];
194
- if (!viewport) {
195
- sys.pubsub.publish('missingViewPort', {
196
- index: viewportIndex
197
- });
198
- return;
199
- }
200
- reactFlowInstance.setViewport(viewport);
201
- }
202
- }
203
- },
204
- SELECT_ALL: (event) => {
205
- if (event) {
206
- event.stopPropagation();
207
- event.preventDefault();
208
- sys.nodeStore.getState().setNodes((nodes) => {
209
- return nodes.map((x) => {
210
- return {
211
- ...x,
212
- selected: true
213
- };
214
- });
215
- });
216
- }
217
- },
218
- ZOOM_IN: (event) => {
219
- if (event) {
220
- event.stopPropagation();
221
- event.preventDefault();
222
- sys.refStore
223
- .getState()
224
- .getRef('reactflow')
225
- ?.zoomIn({ duration: 300 });
226
- }
227
- },
228
- ZOOM_OUT: (event) => {
229
- if (event) {
230
- event.preventDefault();
231
- event.stopPropagation();
232
- sys.refStore
233
- .getState()
234
- .getRef('reactflow')
235
- ?.zoomOut({ duration: 300 });
236
- }
237
- },
238
- RESET_ZOOM: (event) => {
239
- if (event) {
240
- event.preventDefault();
241
- event.stopPropagation();
242
- const reactflow = sys.refStore.getState().getRef('reactflow');
122
+ const defaultBindings: HotkeyBinding[] = [
123
+ {
124
+ action: 'SAVE',
125
+ trigger: 'ctrl+s',
126
+ description: 'Save',
127
+ command: 'editor.save'
128
+ },
129
+ {
130
+ action: 'GROUP',
131
+ trigger: 'ctrl+g',
132
+ description: 'Group',
133
+ command: 'selection.group'
134
+ },
135
+ {
136
+ action: 'COPY',
137
+ trigger: ['command+c', 'ctrl+c'],
138
+ description: 'Copy',
139
+ command: 'selection.copy'
140
+ },
141
+ {
142
+ action: 'PASTE',
143
+ trigger: ['command+v', 'ctrl+v'],
144
+ description: 'Paste',
145
+ command: 'selection.paste'
146
+ },
147
+ {
148
+ action: 'SELECT_ALL',
149
+ trigger: 'ctrl+a',
150
+ description: 'Select All',
151
+ command: 'selection.selectAll'
152
+ },
153
+ {
154
+ action: 'UNDO',
155
+ trigger: ['command+z', 'ctrl+z'],
156
+ description: 'Undo',
157
+ command: 'editor.undo'
158
+ },
159
+ {
160
+ action: 'REDO',
161
+ trigger: ['command+shift+z', 'ctrl+shift+z'],
162
+ description: 'Redo',
163
+ command: 'editor.redo'
164
+ },
165
+ {
166
+ action: 'FIND',
167
+ trigger: ['ctrl+f', 'command+f'],
168
+ description: 'Find',
169
+ command: 'editor.find'
170
+ },
171
+ {
172
+ action: 'AUTO_LAYOUT',
173
+ trigger: 'shift+alt+f',
174
+ description: 'Auto Layout',
175
+ command: 'editor.autoLayout'
176
+ },
177
+ {
178
+ action: 'FIT_VIEW',
179
+ trigger: ['f'],
180
+ description: 'Fit View',
181
+ command: 'view.fit'
182
+ },
183
+ {
184
+ action: 'ZOOM_IN',
185
+ trigger: ['ctrl+plus', 'command+plus'],
186
+ description: 'Zoom In',
187
+ command: 'view.zoomIn'
188
+ },
189
+ {
190
+ action: 'ZOOM_OUT',
191
+ trigger: ['command+-', 'ctrl+-'],
192
+ description: 'Zoom Out',
193
+ command: 'view.zoomOut'
194
+ },
195
+ {
196
+ action: 'ZOOM_RESET',
197
+ trigger: 'ctrl+0',
198
+ description: 'Zoom Reset',
199
+ command: 'view.zoomReset'
200
+ },
201
+ {
202
+ action: 'TOGGLE_GRID',
203
+ trigger: ['command+shift+g', 'ctrl+shift+g'],
204
+ description: 'Toggle Grid',
205
+ command: 'view.toggleGrid'
206
+ },
207
+ {
208
+ action: 'TOGGLE_MINIMAP',
209
+ trigger: ['command+shift+m', 'ctrl+shift+m'],
210
+ description: 'Toggle Minimap',
211
+ command: 'view.toggleMinimap'
212
+ },
213
+ {
214
+ action: 'TOGGLE_SNAP_GRID',
215
+ trigger: ['command+shift+s', 'ctrl+shift+s'],
216
+ description: 'Toggle Snap Grid',
217
+ command: 'view.toggleSnapGrid'
218
+ },
219
+ // Bound for the keymap UI; behaviour handled elsewhere or not yet implemented.
220
+ {
221
+ action: 'DUPLICATE',
222
+ trigger: ['command+d', 'ctrl+d'],
223
+ description: 'Duplicate'
224
+ },
225
+ // Deletion is handled natively by reactflow; listed here for the keymap UI.
226
+ {
227
+ action: 'DELETE',
228
+ trigger: ['delete', 'del', 'backspace'],
229
+ description: 'Delete'
230
+ },
231
+ // Context-aware handlers.
232
+ {
233
+ action: 'TRACE_UPSTREAM',
234
+ trigger: 'ctrl+shift+left',
235
+ description: 'Trace Upstream',
236
+ hintCommand: 'node.traceUpstream',
237
+ handler: traceFromSelection('node.traceUpstream')
238
+ },
239
+ {
240
+ action: 'TRACE_DOWNSTREAM',
241
+ trigger: 'ctrl+shift+right',
242
+ description: 'Trace Downstream',
243
+ hintCommand: 'node.traceDownstream',
244
+ handler: traceFromSelection('node.traceDownstream')
245
+ },
246
+ {
247
+ action: 'SAVE_VIEWPORT',
248
+ trigger: [
249
+ ...NUMBER_TRIGGERS.map((n) => `command+${n}`),
250
+ ...NUMBER_TRIGGERS.map((n) => `ctrl+${n}`)
251
+ ],
252
+ description: 'Save Viewport',
253
+ handler: (sys, event) => {
254
+ event?.preventDefault();
255
+ const reactFlowInstance = sys.refStore.getState().getRef('reactflow');
256
+ if (!event || !reactFlowInstance) return;
257
+ const viewportIndex = parseInt(event.key) - 1;
258
+ if (viewportIndex >= 0 && viewportIndex < 9) {
259
+ sys.graph.setViewport(viewportIndex, reactFlowInstance.getViewport());
260
+ sys.notifications.info(`Saved viewport ${viewportIndex + 1}`);
261
+ }
262
+ }
263
+ },
264
+ {
265
+ action: 'RECALL_VIEWPORT',
266
+ trigger: NUMBER_TRIGGERS,
267
+ description: 'Recall Viewport',
268
+ handler: (sys, event) => {
269
+ event?.preventDefault();
270
+ const reactFlowInstance = sys.refStore.getState().getRef('reactflow');
271
+ if (!event || !reactFlowInstance) return;
272
+ const viewportIndex = parseInt(event.key) - 1;
273
+ if (viewportIndex < 0 || viewportIndex >= 9) return;
274
+ const viewport = sys.graph.viewports[viewportIndex];
275
+ if (!viewport) {
276
+ return;
277
+ }
278
+ reactFlowInstance.setViewport(viewport);
279
+ }
280
+ }
281
+ ];
243
282
 
244
- if (!reactflow) return;
283
+ const buildDefaults = (sys: System) => {
284
+ const keymap: Record<string, string | string[]> = {};
285
+ const descriptions: Record<string, string> = {};
286
+ const handlers: Record<string, Handler> = {};
287
+ const commandToAction: Record<string, string> = {};
245
288
 
246
- const existing = reactflow.getViewport();
247
- reactflow.setViewport({ ...existing, zoom: 1 });
248
- }
249
- },
250
- AUTO_LAYOUT: (event) => {
251
- if (event) {
252
- event.preventDefault();
253
- event.stopPropagation();
289
+ for (const binding of defaultBindings) {
290
+ keymap[binding.action] = binding.trigger;
291
+ descriptions[binding.action] = binding.description;
254
292
 
255
- switch (sys.systemSettings.getState().layoutType) {
256
- case 'Dagre':
257
- applyDagreLayout(sys);
258
- break;
259
- case 'Elk - Layered':
260
- applyElkLayout(sys, 'org.eclipse.elk.layered');
261
- break;
262
- case 'Elk - Force':
263
- applyElkLayout(sys, 'org.eclipse.elk.force');
264
- break;
265
- case 'Elk - Rect':
266
- applyElkLayout(sys, 'org.eclipse.elk.rectpacking');
267
- break;
268
- }
269
- }
270
- },
271
- UNDO: (event) => {
272
- if (event) {
273
- event.preventDefault();
274
- sys.undoManager.undo();
275
- }
276
- },
277
- REDO: (event) => {
278
- if (event) {
279
- event.preventDefault();
280
- sys.undoManager.redo();
281
- }
282
- },
283
- FIND: (event) => {
284
- if (event) {
285
- event.preventDefault();
286
- sys.tabStore.getState().openTab('find');
287
- }
288
- }
289
- },
293
+ const hintTarget = binding.command ?? binding.hintCommand;
294
+ if (hintTarget) commandToAction[hintTarget] = binding.action;
295
+
296
+ if (binding.command) {
297
+ const commandId = binding.command;
298
+ handlers[binding.action] = (event) => {
299
+ event?.preventDefault();
300
+ event?.stopPropagation();
301
+ void sys.runCommand(commandId);
302
+ };
303
+ } else if (binding.handler) {
304
+ const handler = binding.handler;
305
+ handlers[binding.action] = (event) => handler(sys, event);
306
+ }
307
+ }
308
+
309
+ return { keymap, descriptions, handlers, commandToAction };
310
+ };
311
+
312
+ export const hotKeyStoreFactory = (sys: System) => {
313
+ const { keymap, descriptions, handlers, commandToAction } =
314
+ buildDefaults(sys);
315
+
316
+ return create<HotkeyStore>((set, get) => ({
317
+ keymap,
318
+ descriptions,
319
+ handlers,
320
+ commandToAction,
290
321
 
291
322
  register(val) {
292
323
  set((s) => ({
@@ -301,6 +332,10 @@ export const hotKeyStoreFactory = (sys: System) =>
301
332
  descriptions: {
302
333
  ...s.descriptions,
303
334
  ...(val.description ? { [val.action]: val.description } : {})
335
+ },
336
+ commandToAction: {
337
+ ...s.commandToAction,
338
+ ...(val.command ? { [val.command]: val.action } : {})
304
339
  }
305
340
  }));
306
341
  },
@@ -319,5 +354,11 @@ export const hotKeyStoreFactory = (sys: System) =>
319
354
  [name]: desc
320
355
  }
321
356
  }));
357
+ },
358
+ getCommandKeybinding(commandId) {
359
+ const action = get().commandToAction[commandId];
360
+ if (!action) return undefined;
361
+ return formatTrigger(get().keymap[action]);
322
362
  }
323
363
  }));
364
+ };
@@ -1,4 +1,4 @@
1
- import type { System } from '@/system';
1
+ import type { GraphSession } from '@/system/graphSession';
2
2
  import { create } from 'zustand';
3
3
 
4
4
  const DEFAULT_LAYER_ID = 'default';
@@ -51,7 +51,7 @@ const createLayerId = (): string => {
51
51
  return `layer-${Date.now()}-${suffix}`;
52
52
  };
53
53
 
54
- export const layerStoreFactory = (system: System) => {
54
+ export const layerStoreFactory = (session: GraphSession) => {
55
55
  const store = create<LayerStore>((set, get) => ({
56
56
  defaultLayerId: DEFAULT_LAYER_ID,
57
57
  layers: {
@@ -250,7 +250,7 @@ export const layerStoreFactory = (system: System) => {
250
250
  }
251
251
  }));
252
252
 
253
- system.nodeStore.subscribe((nodeState) => {
253
+ session.nodeStore.subscribe((nodeState) => {
254
254
  const nodeIds = nodeState.nodes.map((node) => node.id);
255
255
  store.getState().pruneNodeAssignments(nodeIds);
256
256
  });
@@ -26,10 +26,18 @@ export const registryStoreFactory = () =>
26
26
  },
27
27
 
28
28
  updateRegistry: (registry: INodeRegistry) =>
29
- set((x) => ({
30
- values: { ...x.values, ...registry.values },
31
- specs: [...registry.specs]
32
- })),
29
+ set((x) => {
30
+ // Merge specs by type (additive) so layering multiple profiles/plugins
31
+ // (core + image + ai + ...) doesn't clobber previously-registered node
32
+ // types. Later registrations override an existing type. Use updateSpecs
33
+ // for a full replacement.
34
+ const byType = new Map(x.specs.map((spec) => [spec.type, spec]));
35
+ for (const spec of registry.specs) byType.set(spec.type, spec);
36
+ return {
37
+ values: { ...x.values, ...registry.values },
38
+ specs: Array.from(byType.values())
39
+ };
40
+ }),
33
41
 
34
42
  updateValues: (values: Record<string, ValueTypeMetadata>) =>
35
43
  set((x) => ({
@@ -1,4 +1,4 @@
1
- import type { System } from '@/system';
1
+ import type { GraphSession } from '@/system/graphSession';
2
2
  import { create } from 'zustand';
3
3
 
4
4
  export type SelectionStore = {
@@ -6,14 +6,14 @@ export type SelectionStore = {
6
6
  setSelectedNodeId: (nodeId: string | null) => void;
7
7
  };
8
8
 
9
- export const selectionStoreFactory = (system: System) => {
9
+ export const selectionStoreFactory = (session: GraphSession) => {
10
10
  const store = create<SelectionStore>((set) => ({
11
11
  selectedNodeId: null,
12
12
  setSelectedNodeId: (selectedNodeId: string | null) =>
13
13
  set(() => ({ selectedNodeId }))
14
14
  }));
15
15
  //Track side effect of selected node in the node store
16
- system.nodeStore.subscribe((state) => {
16
+ session.nodeStore.subscribe((state) => {
17
17
  const selectedNode = state.nodes.find((n) => n.selected);
18
18
  store.getState().setSelectedNodeId(selectedNode?.id ?? null);
19
19
  });