@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
@@ -16,29 +16,34 @@ import { HistoryPanel } from '@/components/panels/history';
16
16
  import { HotKeys } from '@/components/hotKeys';
17
17
  import { NodePickerPanel } from '@/components/panels/nodePicker';
18
18
  import { PanelPanel } from '@/components/panels/panel';
19
- import { ConversationPanel } from '@/components/panels/conversation';
20
19
  import { LayersPanel } from '@/components/panels/layers';
20
+ import { GraphPropertiesPanel } from '@/components/panels/graphProperties';
21
+ import { GraphProvider } from './provider';
22
+ import { useStore } from 'zustand';
23
+ import type { GraphSession } from './graphSession';
24
+ import {
25
+ DEFAULT_GRAPH_ID,
26
+ isGraphTabId,
27
+ sessionIdFromTabId,
28
+ tabIdForSession
29
+ } from '@/components/layoutController/utils';
30
+
31
+ /** Live graph tab title , re-renders when the graph is renamed. */
32
+ const GraphTabTitle = ({ session }: { session: GraphSession }) => {
33
+ const name = useStore(session.metaStore, (s) => s.name);
34
+ return <>{name}</>;
35
+ };
21
36
 
22
37
  export class TabLoader {
23
38
  public readonly tabs: Record<string, () => TabData> = {};
39
+ private readonly system: System;
24
40
 
25
- constructor(_system: System) {
26
- this.register('graph', () => {
27
- return {
28
- id: 'graph',
29
- closable: true,
30
- cached: true,
31
- group: 'graph',
32
- title: 'Graph',
33
- content: () => (
34
- <ErrorBoundary fallback={'whoops'}>
35
- <HotKeys>
36
- <Flow />
37
- </HotKeys>
38
- </ErrorBoundary>
39
- )
40
- };
41
- });
41
+ constructor(system: System) {
42
+ this.system = system;
43
+
44
+ // The default graph tab. Other graphs are loaded dynamically by id
45
+ // (see `loadGraphTab`).
46
+ this.register(DEFAULT_GRAPH_ID, () => this.buildGraphTab(DEFAULT_GRAPH_ID));
42
47
 
43
48
  this.register('system:settings', () => {
44
49
  return {
@@ -95,6 +100,20 @@ export class TabLoader {
95
100
  )
96
101
  };
97
102
  });
103
+
104
+ this.register('graphProperties', () => {
105
+ return {
106
+ id: 'graphProperties',
107
+ closable: true,
108
+ title: 'Graph Properties',
109
+ group: 'default',
110
+ content: () => (
111
+ <ErrorBoundary fallback={'whoops'}>
112
+ <GraphPropertiesPanel />
113
+ </ErrorBoundary>
114
+ )
115
+ };
116
+ });
98
117
  this.register('find', () => {
99
118
  return {
100
119
  id: 'find',
@@ -236,30 +255,49 @@ export class TabLoader {
236
255
  };
237
256
  });
238
257
 
239
- this.register('conversation', () => {
240
- return {
241
- id: 'conversation',
242
- closable: true,
243
- cached: true,
244
- title: 'Conversation',
245
- group: 'default',
246
- content: () => (
247
- <ErrorBoundary fallback={'whoops'}>
248
- <ConversationPanel />
249
- </ErrorBoundary>
250
- )
251
- };
252
- });
258
+ // The 'conversation' tab is provided by the AI nodes package's editor
259
+ // plugin (`@kiberon-labs/behave-graph-nodes-ai/ui`), which owns the chat
260
+ // store and panel.
253
261
  }
254
262
 
255
263
  load(tab: TabBase): TabData | undefined {
256
264
  if (!tab.id) {
257
265
  return;
258
266
  }
267
+ // Dynamic per-graph tabs (`graph:<sessionId>`) are not in the registry.
268
+ if (isGraphTabId(tab.id) && tab.id !== DEFAULT_GRAPH_ID) {
269
+ return this.buildGraphTab(tab.id);
270
+ }
259
271
  return this.tabs[tab.id]?.();
260
272
  }
261
273
 
262
274
  register(id: string, loader: () => TabData) {
263
275
  this.tabs[id] = loader;
264
276
  }
277
+
278
+ /**
279
+ * Build the TabData for a graph tab, resolving (or lazily creating) its
280
+ * session and wrapping the canvas in a {@link GraphProvider} so it stays bound
281
+ * to its own graph regardless of which tab is focused.
282
+ */
283
+ private buildGraphTab(tabId: string): TabData {
284
+ const sessionId = sessionIdFromTabId(tabId);
285
+ const session = this.system.getOrCreateSession(sessionId);
286
+ return {
287
+ id: tabIdForSession(sessionId),
288
+ closable: true,
289
+ cached: true,
290
+ group: 'graph',
291
+ title: <GraphTabTitle session={session} />,
292
+ content: () => (
293
+ <ErrorBoundary fallback={'whoops'}>
294
+ <GraphProvider value={session}>
295
+ <HotKeys>
296
+ <Flow />
297
+ </HotKeys>
298
+ </GraphProvider>
299
+ </ErrorBoundary>
300
+ )
301
+ };
302
+ }
265
303
  }
@@ -27,7 +27,7 @@ export type UndoStore = {
27
27
  }) => void;
28
28
  };
29
29
 
30
- export const undoStoreFactory = () =>
30
+ const undoStoreFactory = () =>
31
31
  create<UndoStore>((set) => ({
32
32
  canUndo: false,
33
33
  canRedo: false,
@@ -1,4 +1,4 @@
1
- import type { System } from '@/system/system';
1
+ import type { GraphSession } from '@/system/graphSession';
2
2
  import { annotatedTitle, uiVersion } from '@/annotations';
3
3
  import type { UIGraphJSON } from '@/types/graph';
4
4
 
@@ -18,7 +18,7 @@ function getStringAnnotation(
18
18
  /**
19
19
  * Assembles the full UI graph definition for saving.
20
20
  */
21
- export function buildUIGraphJSON(system: System): UIGraphJSON {
21
+ export function buildUIGraphJSON(system: GraphSession): UIGraphJSON {
22
22
  system.flowStore.getState().invalidateCache();
23
23
 
24
24
  const flow = system.flowStore.getState().getGraph();
@@ -34,8 +34,9 @@ export function buildUIGraphJSON(system: System): UIGraphJSON {
34
34
  const graphVersion =
35
35
  getStringAnnotation(annotations, uiVersion) ?? DEFAULT_UI_GRAPH_VERSION;
36
36
  const graphName =
37
- getStringAnnotation(annotations, annotatedTitle) ??
38
- flow.name ??
37
+ system.metaStore.getState().name ||
38
+ getStringAnnotation(annotations, annotatedTitle) ||
39
+ flow.name ||
39
40
  DEFAULT_UI_GRAPH_NAME;
40
41
 
41
42
  const viewports = system.graph.viewports.length
@@ -0,0 +1,87 @@
1
+ import type { Node } from 'reactflow';
2
+ import type { GraphSocketJSON } from '@kiberon-labs/behave-graph';
3
+ import type { Socket } from '@/types';
4
+
5
+ export const GRAPH_INPUT_TYPE = 'graph/input';
6
+ export const GRAPH_OUTPUT_TYPE = 'graph/output';
7
+ export const CALL_SUBGRAPH_TYPE = 'flow/callSubgraph';
8
+
9
+ export type ContractParam = {
10
+ /** Stable identifier (socket key / contract key). Independent of the name. */
11
+ id?: string;
12
+ /** User-facing display name; editable without breaking wiring. */
13
+ name: string;
14
+ valueTypeName: string;
15
+ defaultValue?: any;
16
+ };
17
+
18
+ export type GraphContract = {
19
+ graphInputs: GraphSocketJSON[];
20
+ graphOutputs: GraphSocketJSON[];
21
+ };
22
+
23
+ /** Stable identity of a param , its id, falling back to name for legacy data. */
24
+ export const paramId = (param: ContractParam): string => param.id ?? param.name;
25
+
26
+ const readParams = (node: Node): ContractParam[] => {
27
+ const params = (node.data as any)?.configuration?.parameters;
28
+ return Array.isArray(params) ? (params as ContractParam[]) : [];
29
+ };
30
+
31
+ const toSocket = (param: ContractParam): GraphSocketJSON => ({
32
+ key: paramId(param),
33
+ valueType: param.valueTypeName,
34
+ ...(param.defaultValue !== undefined
35
+ ? { defaultValue: param.defaultValue }
36
+ : {}),
37
+ label: param.name
38
+ });
39
+
40
+ /**
41
+ * Derive a graph's contract from its boundary nodes. The `graph/input` /
42
+ * `graph/output` nodes are the source of truth; their configured parameters
43
+ * become the graph's `graphInputs` / `graphOutputs`.
44
+ */
45
+ export const deriveContract = (nodes: Node[]): GraphContract => {
46
+ const graphInputs: GraphSocketJSON[] = [];
47
+ const graphOutputs: GraphSocketJSON[] = [];
48
+
49
+ for (const node of nodes) {
50
+ const type = (node.data as any)?.type;
51
+ if (type === GRAPH_INPUT_TYPE) {
52
+ graphInputs.push(...readParams(node).map(toSocket));
53
+ } else if (type === GRAPH_OUTPUT_TYPE) {
54
+ graphOutputs.push(...readParams(node).map(toSocket));
55
+ }
56
+ }
57
+
58
+ return { graphInputs, graphOutputs };
59
+ };
60
+
61
+ /**
62
+ * Convert a derived contract's sockets back into editable {@link ContractParam}s
63
+ * , the form stored in a Call Subgraph node's configuration. The socket `key`
64
+ * is the stable param id; its `label` is the display name.
65
+ */
66
+ export const contractToParams = (sockets: GraphSocketJSON[]): ContractParam[] =>
67
+ sockets.map((s) => ({
68
+ id: s.key,
69
+ name: s.label ?? s.key,
70
+ valueTypeName: s.valueType,
71
+ ...(s.defaultValue !== undefined ? { defaultValue: s.defaultValue } : {})
72
+ }));
73
+
74
+ /**
75
+ * Convert contract params into dynamic-port sockets. The socket identity
76
+ * (name/key/handle id) is the stable param id; the display label is the name.
77
+ */
78
+ export const paramsToSockets = (params: ContractParam[]): Socket[] =>
79
+ params.map((p) => {
80
+ const id = paramId(p);
81
+ return {
82
+ name: id,
83
+ key: id,
84
+ label: p.name || id,
85
+ valueType: p.valueTypeName || 'string'
86
+ };
87
+ });
@@ -4,15 +4,16 @@ import type {
4
4
  NodeSpecJSON
5
5
  } from '@kiberon-labs/behave-graph';
6
6
  import type { Edge, Node } from 'reactflow';
7
- import { System } from '../system/system';
7
+ import type { GraphSession } from '../system/graphSession';
8
8
  import { writeVariablesToJSON } from '../util/serializeVariables';
9
9
  import { isBehaveNode } from '@/util/isBehaveNode';
10
+ import { deriveContract } from './contract';
10
11
 
11
12
  const isNullish = (value: any): value is null | undefined =>
12
13
  value === undefined || value === null;
13
14
 
14
15
  export const flowToBehave = (
15
- system: System,
16
+ session: GraphSession,
16
17
  nodes: Node[],
17
18
  edges: Edge[],
18
19
  nodeSpecJSON: NodeSpecJSON[]
@@ -20,11 +21,11 @@ export const flowToBehave = (
20
21
  const graph: GraphJSON = {
21
22
  nodes: [],
22
23
  variables: [],
23
- customEvents: system.eventsStore.getState().getCustomEvents()
24
+ customEvents: session.eventsStore.getState().getCustomEvents()
24
25
  };
25
26
 
26
- const registry = system.registry.getState();
27
- const varStore = system.variableStore.getState().variables;
27
+ const registry = session.editor.registry.getState();
28
+ const varStore = session.variableStore.getState().variables;
28
29
 
29
30
  nodes.forEach((node) => {
30
31
  if (!isBehaveNode(node)) return;
@@ -109,5 +110,12 @@ export const flowToBehave = (
109
110
  if (Object.keys(varStore).length > 0) {
110
111
  graph.variables = writeVariablesToJSON(registry, varStore);
111
112
  }
113
+
114
+ // Derive the graph's contract (inputs/outputs) from its boundary nodes so
115
+ // callers and the runtime can read it.
116
+ const { graphInputs, graphOutputs } = deriveContract(nodes);
117
+ if (graphInputs.length > 0) graph.graphInputs = graphInputs;
118
+ if (graphOutputs.length > 0) graph.graphOutputs = graphOutputs;
119
+
112
120
  return graph;
113
121
  };
@@ -15,8 +15,13 @@ export type IBehaveNode = Omit<Node, 'data' | 'type'> & {
15
15
  };
16
16
  };
17
17
 
18
- export type ICommentNode = Omit<Node, 'data' | 'type'> & {
19
- type: 'commentNode';
18
+ /**
19
+ * Presentational markdown note, contributed by the notes plugin
20
+ * (`@/plugin/notes`). `commentNode` is the legacy type string notes carried
21
+ * when they lived in the core editor.
22
+ */
23
+ export type INoteNode = Omit<Node, 'data' | 'type'> & {
24
+ type: 'noteNode' | 'commentNode';
20
25
  data: {
21
26
  annotations?: Record<string, any>;
22
27
  text: string;
@@ -41,5 +46,5 @@ export type IAINode = Omit<Node, 'data' | 'type'> & {
41
46
  };
42
47
  };
43
48
 
44
- export type AnyNode = IBehaveNode | ICommentNode | IAINode | IGroupNode;
49
+ export type AnyNode = IBehaveNode | INoteNode | IAINode | IGroupNode;
45
50
  export type AnyNodeType = AnyNode['type'];
package/src/types.ts CHANGED
@@ -3,6 +3,8 @@ import type { ChoiceJSON } from '@kiberon-labs/behave-graph';
3
3
  export interface SocketBase {
4
4
  name: string;
5
5
  key: string;
6
+ /** Optional display label; falls back to `name` when absent. */
7
+ label?: string;
6
8
  choices?: ChoiceJSON;
7
9
  valueType: string;
8
10
  defaultValue?: any;
@@ -0,0 +1,200 @@
1
+ import type { NodeSpecJSON } from '@kiberon-labs/behave-graph';
2
+ import type { Connection, Edge, Node, XYPosition } from 'reactflow';
3
+ import { v4 as uuidv4 } from 'uuid';
4
+ import { getSocketsByNodeTypeAndHandleType } from './getSocketsByNodeTypeAndHandleType.js';
5
+ import { mergeSockets } from './mergeSockets.js';
6
+ import type { ConversionRule } from '@/store/conversions';
7
+ import type { IBehaveNode } from '@/types/nodes.js';
8
+
9
+ /**
10
+ * Find a node spec that converts `sourceType` to `targetType` , a pure value
11
+ * function with exactly one value input of the source type and one value output
12
+ * of the target type, and no flow sockets (e.g. `math/toString/integer`).
13
+ */
14
+ export const findConverterSpec = (
15
+ specs: NodeSpecJSON[],
16
+ sourceType: string,
17
+ targetType: string
18
+ ): NodeSpecJSON | undefined =>
19
+ specs.find((spec) => {
20
+ const valueIns = spec.inputs.filter((i) => i.valueType !== 'flow');
21
+ const valueOuts = spec.outputs.filter((o) => o.valueType !== 'flow');
22
+ const hasFlow =
23
+ spec.inputs.some((i) => i.valueType === 'flow') ||
24
+ spec.outputs.some((o) => o.valueType === 'flow');
25
+ return (
26
+ !hasFlow &&
27
+ valueIns.length === 1 &&
28
+ valueOuts.length === 1 &&
29
+ valueIns[0]!.valueType === sourceType &&
30
+ valueOuts[0]!.valueType === targetType
31
+ );
32
+ });
33
+
34
+ export type ResolvedConverter = {
35
+ nodeType: string;
36
+ inputName: string;
37
+ outputName: string;
38
+ };
39
+
40
+ const firstValueSocketName = (
41
+ sockets: { name: string; valueType: string }[]
42
+ ): string | undefined => sockets.find((s) => s.valueType !== 'flow')?.name;
43
+
44
+ /**
45
+ * Pick the socket that should carry a value of `valueType`: prefer one whose type
46
+ * matches exactly (so multi-port converter nodes resolve the *right* port), then
47
+ * fall back to the first non-flow socket for single-port nodes.
48
+ */
49
+ const socketNameForType = (
50
+ sockets: { name: string; valueType: string }[],
51
+ valueType: string
52
+ ): string | undefined =>
53
+ sockets.find((s) => s.valueType === valueType)?.name ??
54
+ firstValueSocketName(sockets);
55
+
56
+ /**
57
+ * Resolve which converter to use for `from`→`to`. A registered
58
+ * {@link ConversionRule} (e.g. from a custom profile) takes precedence; otherwise
59
+ * fall back to scanning the specs for a generic single-in/single-out converter.
60
+ *
61
+ * A rule's `inputKey`/`outputKey` pin the exact ports to wire (required for
62
+ * converter nodes with more than one input or output). When omitted they are
63
+ * resolved by matching the port's value type to `from`/`to`.
64
+ */
65
+ export const resolveConverter = (
66
+ specs: NodeSpecJSON[],
67
+ from: string,
68
+ to: string,
69
+ conversions?: ConversionRule[]
70
+ ): ResolvedConverter | undefined => {
71
+ const rule = conversions?.find((c) => c.from === from && c.to === to);
72
+ if (rule) {
73
+ const spec = specs.find((s) => s.type === rule.nodeType);
74
+ const inputName =
75
+ rule.inputKey ?? socketNameForType(spec?.inputs ?? [], from);
76
+ const outputName =
77
+ rule.outputKey ?? socketNameForType(spec?.outputs ?? [], to);
78
+ if (inputName && outputName) {
79
+ return { nodeType: rule.nodeType, inputName, outputName };
80
+ }
81
+ return undefined;
82
+ }
83
+
84
+ const spec = findConverterSpec(specs, from, to);
85
+ if (!spec) return undefined;
86
+ const inputName = firstValueSocketName(spec.inputs);
87
+ const outputName = firstValueSocketName(spec.outputs);
88
+ if (!inputName || !outputName) return undefined;
89
+ return { nodeType: spec.type, inputName, outputName };
90
+ };
91
+
92
+ /** Resolve a node socket's value type (spec sockets merged with dynamic ports). */
93
+ const getSocketValueType = (
94
+ node: IBehaveNode,
95
+ handleId: string | null | undefined,
96
+ handleType: 'source' | 'target',
97
+ specs: NodeSpecJSON[]
98
+ ): string | undefined => {
99
+ if (!handleId) return undefined;
100
+ const specSockets = getSocketsByNodeTypeAndHandleType(
101
+ specs,
102
+ node.data?.type,
103
+ handleType
104
+ );
105
+ if (!specSockets) return undefined;
106
+ const dynamic =
107
+ handleType === 'source'
108
+ ? node.data.dynamicPorts?.outputs
109
+ : node.data.dynamicPorts?.inputs;
110
+ const sockets = mergeSockets(specSockets, dynamic);
111
+ return sockets.find((s) => s.name === handleId)?.valueType;
112
+ };
113
+
114
+ export type ConverterInsertion = { node: IBehaveNode; edges: Edge[] };
115
+
116
+ /**
117
+ * If a connection joins different-but-convertible value types, build the
118
+ * converter node and the two edges needed to splice it in between source and
119
+ * target. Returns null when the types match, can't be resolved, or no converter
120
+ * is registered.
121
+ */
122
+ export const buildConverterInsertion = (
123
+ connection: Connection,
124
+ nodes: Node[],
125
+ specs: NodeSpecJSON[],
126
+ conversions?: ConversionRule[]
127
+ ): ConverterInsertion | null => {
128
+ if (!connection.source || !connection.target) return null;
129
+
130
+ const sourceNode = nodes.find((n) => n.id === connection.source) as
131
+ | IBehaveNode
132
+ | undefined;
133
+ const targetNode = nodes.find((n) => n.id === connection.target) as
134
+ | IBehaveNode
135
+ | undefined;
136
+ if (!sourceNode || !targetNode) return null;
137
+
138
+ const sourceType = getSocketValueType(
139
+ sourceNode,
140
+ connection.sourceHandle,
141
+ 'source',
142
+ specs
143
+ );
144
+ const targetType = getSocketValueType(
145
+ targetNode,
146
+ connection.targetHandle,
147
+ 'target',
148
+ specs
149
+ );
150
+ if (!sourceType || !targetType) return null;
151
+ if (sourceType === targetType) return null;
152
+ if (sourceType === 'flow' || targetType === 'flow') return null;
153
+
154
+ const converter = resolveConverter(
155
+ specs,
156
+ sourceType,
157
+ targetType,
158
+ conversions
159
+ );
160
+ if (!converter) return null;
161
+
162
+ const { nodeType, inputName: inName, outputName: outName } = converter;
163
+
164
+ const position: XYPosition = {
165
+ x: (sourceNode.position.x + targetNode.position.x) / 2,
166
+ y: (sourceNode.position.y + targetNode.position.y) / 2
167
+ };
168
+
169
+ const convId = uuidv4();
170
+ const node: IBehaveNode = {
171
+ id: convId,
172
+ type: 'behaveNode',
173
+ position,
174
+ data: {
175
+ type: nodeType,
176
+ configuration: {},
177
+ ports: {},
178
+ dynamicPorts: {}
179
+ }
180
+ };
181
+
182
+ const edges: Edge[] = [
183
+ {
184
+ id: uuidv4(),
185
+ source: connection.source,
186
+ sourceHandle: connection.sourceHandle ?? undefined,
187
+ target: convId,
188
+ targetHandle: inName
189
+ },
190
+ {
191
+ id: uuidv4(),
192
+ source: convId,
193
+ sourceHandle: outName,
194
+ target: connection.target,
195
+ targetHandle: connection.targetHandle ?? undefined
196
+ }
197
+ ];
198
+
199
+ return { node, edges };
200
+ };
@@ -3,12 +3,15 @@ import type { Connection, ReactFlowInstance } from 'reactflow';
3
3
  import { getSocketsByNodeTypeAndHandleType } from './getSocketsByNodeTypeAndHandleType.js';
4
4
  import { isHandleConnected } from './isHandleConnected.js';
5
5
  import { mergeSockets } from './mergeSockets.js';
6
+ import { resolveConverter } from './autoConvert.js';
7
+ import type { ConversionRule } from '@/store/conversions';
6
8
  import type { IBehaveNode } from '@/types/nodes.js';
7
9
 
8
10
  export const isValidConnection = (
9
11
  connection: Connection,
10
12
  instance: ReactFlowInstance,
11
- specJSON: NodeSpecJSON[]
13
+ specJSON: NodeSpecJSON[],
14
+ options?: { autoConvert?: boolean; conversions?: ConversionRule[] }
12
15
  ) => {
13
16
  if (connection.source === null || connection.target === null) return false;
14
17
 
@@ -60,5 +63,23 @@ export const isValidConnection = (
60
63
  return false;
61
64
  }
62
65
 
63
- return sourceSocket.valueType === targetSocket.valueType;
66
+ if (sourceSocket.valueType === targetSocket.valueType) return true;
67
+
68
+ // Different value types are allowed when auto-convert can splice in a
69
+ // converter node (handled on connect).
70
+ if (
71
+ options?.autoConvert &&
72
+ sourceSocket.valueType !== 'flow' &&
73
+ targetSocket.valueType !== 'flow' &&
74
+ resolveConverter(
75
+ specJSON,
76
+ sourceSocket.valueType,
77
+ targetSocket.valueType,
78
+ options.conversions
79
+ )
80
+ ) {
81
+ return true;
82
+ }
83
+
84
+ return false;
64
85
  };
@@ -1,16 +1,14 @@
1
- import { docsPlugin } from '@/plugin/docs';
2
-
3
- import { SystemProvider } from '@/system/provider';
1
+ import { SystemProvider, GraphProvider } from '@/system/provider';
4
2
  import { System } from '@/system/system';
5
3
  import {
6
4
  registerCoreProfile,
7
5
  type ValueType
8
6
  } from '@kiberon-labs/behave-graph';
9
7
 
10
- import { alignmentPlugin } from '@/plugin/alignment';
11
- import { downloadJson, localGraphRunnerPlugin } from '@/index';
8
+ import { kitchenSinkPlugin } from '@/plugin/kitchen-sink';
9
+ import { localGraphRunnerPlugin } from '@/index';
12
10
 
13
- export const ColorValue: ValueType = {
11
+ const ColorValue: ValueType = {
14
12
  name: 'color',
15
13
  creator: () => '#000000',
16
14
  deserialize: (value: string) => value,
@@ -37,13 +35,14 @@ const nodeRegistry = {
37
35
  //Basic system generator for tests and stories
38
36
  export const systemGenerator = () => {
39
37
  const defaultSys = new System(nodeRegistry);
38
+ defaultSys.registerPlugin(kitchenSinkPlugin);
40
39
  return defaultSys;
41
40
  };
42
41
 
43
42
  const defaultSys = new System(nodeRegistry);
43
+ const defaultSession = defaultSys.createSession('graph');
44
44
 
45
- defaultSys.registerPlugin(alignmentPlugin);
46
- defaultSys.registerPlugin(docsPlugin);
45
+ defaultSys.registerPlugin(kitchenSinkPlugin);
47
46
  defaultSys.registerPlugin(localGraphRunnerPlugin, {
48
47
  registry: coreRegistry,
49
48
  events: [
@@ -73,11 +72,11 @@ defaultSys.registerPlugin(localGraphRunnerPlugin, {
73
72
  ]
74
73
  });
75
74
 
76
- defaultSys.pubsub.subscribe('graph:saved', (_, graph) => {
77
- downloadJson('graph.json', graph);
78
- });
75
+ // Saving is wired by default on the System (download-to-file); no per-story
76
+ // subscription is needed. Override with defaultSys.enablePersistence(...) only
77
+ // when a story needs to demonstrate a custom sink.
79
78
 
80
- defaultSys.flowStore.getState().setGraph({
79
+ defaultSession.flowStore.getState().setGraph({
81
80
  nodes: [
82
81
  {
83
82
  type: 'lifecycle/onStart',
@@ -151,7 +150,7 @@ const exampleLogTime = (h: number, m: number, s: number, ms: number) =>
151
150
  message: 'debug/log: failed to resolve socket "value" on node 3.'
152
151
  }
153
152
  ].forEach((entry, index) => {
154
- defaultSys.logsStore.getState().append({
153
+ defaultSession.logsStore.getState().append({
155
154
  type: entry.type,
156
155
  data: { message: entry.message },
157
156
  time: exampleLogTime(13, 45, 30 + index, 120 + index * 37)
@@ -163,5 +162,9 @@ export const DefaultSystemProvider = ({
163
162
  }: {
164
163
  children: React.ReactElement;
165
164
  }) => {
166
- return <SystemProvider value={defaultSys}>{children}</SystemProvider>;
165
+ return (
166
+ <SystemProvider value={defaultSys}>
167
+ <GraphProvider value={defaultSession}>{children}</GraphProvider>
168
+ </SystemProvider>
169
+ );
167
170
  };
@@ -1,11 +1,12 @@
1
1
  import { System } from '@/system/system';
2
+ import { kitchenSinkPlugin } from '@/plugin/kitchen-sink';
2
3
  import {
3
4
  registerCoreProfile,
4
5
  writeNodeSpecsToJSON,
5
6
  type ValueType
6
7
  } from '@kiberon-labs/behave-graph';
7
8
 
8
- export const ColorValue: ValueType = {
9
+ const ColorValue: ValueType = {
9
10
  name: 'color',
10
11
  creator: () => '#000000',
11
12
  deserialize: (value: string) => value,
@@ -34,5 +35,9 @@ const nodeRegistry = {
34
35
  //Basic system generator for tests and stories
35
36
  export const systemGenerator = () => {
36
37
  const defaultSys = new System(nodeRegistry);
38
+ defaultSys.createSession('graph');
39
+ // Standard editor UI bundle (docs, alignment, layout, notes) so stories built
40
+ // on this generator get the full editor without wiring plugins per story.
41
+ defaultSys.registerPlugin(kitchenSinkPlugin);
37
42
  return defaultSys;
38
43
  };