@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
@@ -0,0 +1,80 @@
1
+ import { plugin } from '@/system/plugin';
2
+ import type { System } from '@/system/system';
3
+ import { applyDagreLayout } from './dagre';
4
+ import { applyElkLayout } from './elk';
5
+
6
+ export * from './dagre';
7
+ export * from './elk';
8
+
9
+ /**
10
+ * Available auto-layout engines. `Dagre` is a small, synchronous layered layout;
11
+ * the `Elk - *` options use elkjs (~1.4 MB, loaded lazily on first use) for
12
+ * higher-quality layouts.
13
+ */
14
+ export const LAYOUT_TYPE = {
15
+ dagre: 'Dagre',
16
+ elkForce: 'Elk - Force',
17
+ elkRect: 'Elk - Rect',
18
+ elkLayered: 'Elk - Layered'
19
+ } as const;
20
+
21
+ export type LayoutType = (typeof LAYOUT_TYPE)[keyof typeof LAYOUT_TYPE];
22
+
23
+ /**
24
+ * Run the currently-selected auto-layout engine against the focused graph.
25
+ * Reads the `layoutType` setting (registered by this plugin) to pick the engine.
26
+ */
27
+ export const applyAutoLayout = (system: System): void => {
28
+ switch (system.getSetting<LayoutType>('layoutType')) {
29
+ case LAYOUT_TYPE.dagre:
30
+ void applyDagreLayout(system);
31
+ break;
32
+ case LAYOUT_TYPE.elkLayered:
33
+ void applyElkLayout(system, 'org.eclipse.elk.layered');
34
+ break;
35
+ case LAYOUT_TYPE.elkForce:
36
+ void applyElkLayout(system, 'org.eclipse.elk.force');
37
+ break;
38
+ case LAYOUT_TYPE.elkRect:
39
+ void applyElkLayout(system, 'org.eclipse.elk.rectpacking');
40
+ break;
41
+ }
42
+ };
43
+
44
+ /**
45
+ * Adds graph auto-layout (Dagre + ELK) to the editor. elkjs and dagre are heavy
46
+ * dependencies that not every host needs, so they live here rather than in the
47
+ * core editor — register this plugin (directly or via the kitchen-sink plugin)
48
+ * to opt in.
49
+ *
50
+ * The plugin:
51
+ * - registers the `layoutType` setting (the engine picker in the Settings panel);
52
+ * - registers the `editor.autoLayout` command that the "Auto Layout" hotkey and
53
+ * menu dispatch to.
54
+ *
55
+ * Without it, `editor.autoLayout` is simply unregistered (the hotkey no-ops).
56
+ */
57
+ export const layoutPlugin = plugin(
58
+ (system: System) => {
59
+ system.registerSetting({
60
+ key: 'layoutType',
61
+ section: 'Layout',
62
+ type: 'enum',
63
+ default: LAYOUT_TYPE.dagre,
64
+ options: Object.values(LAYOUT_TYPE).map((value) => ({
65
+ value,
66
+ label: value
67
+ })),
68
+ title: 'Layout Type',
69
+ description:
70
+ 'Select the type of layout engine to use in the graph editor.'
71
+ });
72
+
73
+ system.commandStore.getState().register({
74
+ id: 'editor.autoLayout',
75
+ title: 'Auto Layout',
76
+ run: (ctx) => applyAutoLayout(ctx.editor)
77
+ });
78
+ },
79
+ { name: 'layout' }
80
+ );
@@ -0,0 +1,200 @@
1
+ import { useState } from 'react';
2
+ import { useEditorState, type Editor } from '@tiptap/react';
3
+ import { VscodeButton, VscodeTextfield } from '@vscode-elements/react-elements';
4
+ import {
5
+ Bold,
6
+ Code,
7
+ Italic,
8
+ List,
9
+ NumberedListLeft,
10
+ Quote,
11
+ Strikethrough,
12
+ Youtube
13
+ } from 'iconoir-react';
14
+
15
+ interface FormatToolbarProps {
16
+ editor: Editor;
17
+ }
18
+
19
+ /**
20
+ * Formatting actions for the note editor. Buttons are the same VscodeButton
21
+ * component the FloatingToolbar uses, so the note toolbar stays visually
22
+ * consistent with the rest of the editor chrome; the active format renders as
23
+ * a primary (accent) button.
24
+ *
25
+ * The button row swallows mousedown so clicking a formatting button keeps the
26
+ * editor's text selection instead of blurring it. The video row must NOT: its
27
+ * text field needs focus. Video embedding uses an inline URL field rather than
28
+ * `window.prompt`, which is blocked in VS Code webviews.
29
+ */
30
+ export const FormatToolbar = ({ editor }: FormatToolbarProps) => {
31
+ // null = embed row closed; a string = the URL being typed.
32
+ const [videoUrl, setVideoUrl] = useState<string | null>(null);
33
+
34
+ const active = useEditorState({
35
+ editor,
36
+ selector: ({ editor }) => ({
37
+ bold: editor.isActive('bold'),
38
+ italic: editor.isActive('italic'),
39
+ strike: editor.isActive('strike'),
40
+ code: editor.isActive('code'),
41
+ h1: editor.isActive('heading', { level: 1 }),
42
+ h2: editor.isActive('heading', { level: 2 }),
43
+ h3: editor.isActive('heading', { level: 3 }),
44
+ bulletList: editor.isActive('bulletList'),
45
+ orderedList: editor.isActive('orderedList'),
46
+ codeBlock: editor.isActive('codeBlock'),
47
+ blockquote: editor.isActive('blockquote')
48
+ })
49
+ });
50
+
51
+ const embedVideo = () => {
52
+ const src = videoUrl?.trim();
53
+ if (src) {
54
+ editor.chain().focus().setYoutubeVideo({ src }).run();
55
+ }
56
+ setVideoUrl(null);
57
+ };
58
+
59
+ return (
60
+ <div className="notes-toolbar nodrag nopan">
61
+ <div
62
+ className="notes-toolbar__row"
63
+ onMouseDown={(e) => e.preventDefault()}
64
+ >
65
+ <VscodeButton
66
+ secondary={!active.bold}
67
+ iconOnly
68
+ title="Bold"
69
+ onClick={() => editor.chain().focus().toggleBold().run()}
70
+ >
71
+ <Bold />
72
+ </VscodeButton>
73
+ <VscodeButton
74
+ secondary={!active.italic}
75
+ iconOnly
76
+ title="Italic"
77
+ onClick={() => editor.chain().focus().toggleItalic().run()}
78
+ >
79
+ <Italic />
80
+ </VscodeButton>
81
+ <VscodeButton
82
+ secondary={!active.strike}
83
+ iconOnly
84
+ title="Strikethrough"
85
+ onClick={() => editor.chain().focus().toggleStrike().run()}
86
+ >
87
+ <Strikethrough />
88
+ </VscodeButton>
89
+ <VscodeButton
90
+ secondary={!active.code}
91
+ iconOnly
92
+ title="Code"
93
+ onClick={() => editor.chain().focus().toggleCode().run()}
94
+ >
95
+ <Code />
96
+ </VscodeButton>
97
+ <div className="notes-toolbar__separator" />
98
+ <VscodeButton
99
+ secondary={!active.h1}
100
+ title="Heading 1"
101
+ onClick={() =>
102
+ editor.chain().focus().toggleHeading({ level: 1 }).run()
103
+ }
104
+ >
105
+ H1
106
+ </VscodeButton>
107
+ <VscodeButton
108
+ secondary={!active.h2}
109
+ title="Heading 2"
110
+ onClick={() =>
111
+ editor.chain().focus().toggleHeading({ level: 2 }).run()
112
+ }
113
+ >
114
+ H2
115
+ </VscodeButton>
116
+ <VscodeButton
117
+ secondary={!active.h3}
118
+ title="Heading 3"
119
+ onClick={() =>
120
+ editor.chain().focus().toggleHeading({ level: 3 }).run()
121
+ }
122
+ >
123
+ H3
124
+ </VscodeButton>
125
+ <div className="notes-toolbar__separator" />
126
+ <VscodeButton
127
+ secondary={!active.bulletList}
128
+ iconOnly
129
+ title="Bullet List"
130
+ onClick={() => editor.chain().focus().toggleBulletList().run()}
131
+ >
132
+ <List />
133
+ </VscodeButton>
134
+ <VscodeButton
135
+ secondary={!active.orderedList}
136
+ iconOnly
137
+ title="Numbered List"
138
+ onClick={() => editor.chain().focus().toggleOrderedList().run()}
139
+ >
140
+ <NumberedListLeft />
141
+ </VscodeButton>
142
+ <VscodeButton
143
+ secondary={!active.codeBlock}
144
+ iconOnly
145
+ title="Code Block"
146
+ onClick={() => editor.chain().focus().toggleCodeBlock().run()}
147
+ >
148
+ <Code />
149
+ </VscodeButton>
150
+ <VscodeButton
151
+ secondary={!active.blockquote}
152
+ iconOnly
153
+ title="Quote"
154
+ onClick={() => editor.chain().focus().toggleBlockquote().run()}
155
+ >
156
+ <Quote />
157
+ </VscodeButton>
158
+ <div className="notes-toolbar__separator" />
159
+ <VscodeButton
160
+ secondary={videoUrl === null}
161
+ iconOnly
162
+ title="Embed YouTube Video"
163
+ onClick={() => setVideoUrl(videoUrl === null ? '' : null)}
164
+ >
165
+ <Youtube />
166
+ </VscodeButton>
167
+ </div>
168
+
169
+ {videoUrl !== null && (
170
+ <div
171
+ className="notes-toolbar__embed"
172
+ // Keep keystrokes (e.g. Backspace) from reaching React Flow, which
173
+ // would delete the selected note while the URL is being typed.
174
+ onKeyDown={(e) => {
175
+ e.stopPropagation();
176
+ if (e.key === 'Enter') embedVideo();
177
+ if (e.key === 'Escape') setVideoUrl(null);
178
+ }}
179
+ >
180
+ <VscodeTextfield
181
+ type="text"
182
+ placeholder="YouTube video URL"
183
+ value={videoUrl}
184
+ onInput={(e: any) => setVideoUrl(e.target.value)}
185
+ />
186
+ <VscodeButton title="Embed" onClick={embedVideo}>
187
+ Embed
188
+ </VscodeButton>
189
+ <VscodeButton
190
+ secondary
191
+ title="Cancel"
192
+ onClick={() => setVideoUrl(null)}
193
+ >
194
+ Cancel
195
+ </VscodeButton>
196
+ </div>
197
+ )}
198
+ </div>
199
+ );
200
+ };
@@ -0,0 +1,191 @@
1
+ import { Notes } from 'iconoir-react';
2
+ import { v4 as uuidv4 } from 'uuid';
3
+ import { plugin } from '@/system/plugin';
4
+ import type { System } from '@/system/system';
5
+ import type { CommandContext } from '@/store/commands';
6
+ import type { INoteNode } from '@/types/nodes';
7
+ import { NoteNode } from './note';
8
+ import {
9
+ NOTE_NODE_TYPE,
10
+ LEGACY_COMMENT_NODE_TYPE,
11
+ noteAt,
12
+ duplicateNote,
13
+ deleteNote,
14
+ reorderNote
15
+ } from './nodeActions';
16
+
17
+ export { NoteNode } from './note';
18
+ export * from './nodeActions';
19
+
20
+ /**
21
+ * Create a note node on the graph the command targets. Placed at the given
22
+ * position (e.g. from a context menu) or, from surfaces without one (toolbar,
23
+ * hotkey), at the centre of the current viewport. Undo-aware.
24
+ */
25
+ const addNote = (ctx: CommandContext): void => {
26
+ const reactflow = ctx.session.refStore.getState().getRef('reactflow');
27
+ const position = ctx.position ??
28
+ reactflow?.screenToFlowPosition({
29
+ x: window.innerWidth / 2,
30
+ y: window.innerHeight / 2
31
+ }) ?? { x: 0, y: 0 };
32
+
33
+ const note: INoteNode = {
34
+ id: uuidv4(),
35
+ type: NOTE_NODE_TYPE,
36
+ position,
37
+ selected: true,
38
+ // Only the header drags the node; the body is a text surface.
39
+ dragHandle: '.notes-node__header',
40
+ style: { width: 280, height: 180 },
41
+ data: { text: '' }
42
+ };
43
+
44
+ ctx.session.undoManager.execute({
45
+ name: 'Add note',
46
+ execute: () => {
47
+ ctx.session.nodeStore.getState().addNode(note);
48
+ },
49
+ undo: () => {
50
+ ctx.session.nodeStore
51
+ .getState()
52
+ .setNodes((existing) => existing.filter((n) => n.id !== note.id));
53
+ }
54
+ });
55
+ };
56
+
57
+ /**
58
+ * Adds markdown note nodes to the editor. Notes are purely presentational:
59
+ * they never appear in the compiled behave graph (only behave nodes do), but
60
+ * they persist with the UI graph JSON like any other canvas node.
61
+ *
62
+ * The note editor embeds tiptap/prosemirror, a heavy dependency most hosts do
63
+ * not need, so notes live here rather than in the core editor — register this
64
+ * plugin (directly or via the kitchen-sink plugin) to opt in.
65
+ *
66
+ * The plugin:
67
+ * - registers the `noteNode` component on every graph session (plus the legacy
68
+ * `commentNode` alias for graphs saved before notes moved here);
69
+ * - registers the `notes.addNote` command, an "Add Note" button on the
70
+ * floating toolbar, and a `Shift+N` hotkey that dispatch it;
71
+ * - registers note-specific node commands + context-menu items
72
+ * (duplicate / delete / bring to front / send to back).
73
+ */
74
+ export const notesPlugin = plugin(
75
+ (system: System) => {
76
+ system.registerSessionExtension((session) => {
77
+ const { registerNodeType } = session.flowStore.getState();
78
+ registerNodeType(NOTE_NODE_TYPE, NoteNode);
79
+ registerNodeType(LEGACY_COMMENT_NODE_TYPE, NoteNode);
80
+ });
81
+
82
+ const commands = system.commandStore.getState();
83
+ commands.register({
84
+ id: 'notes.addNote',
85
+ title: 'Add Note',
86
+ run: addNote
87
+ });
88
+ commands.register({
89
+ id: 'note.duplicate',
90
+ title: 'Duplicate Note',
91
+ run: (ctx) => {
92
+ const note = noteAt(ctx);
93
+ if (note) duplicateNote(ctx.session, note);
94
+ }
95
+ });
96
+ commands.register({
97
+ id: 'note.delete',
98
+ title: 'Delete Note',
99
+ run: (ctx) => {
100
+ const note = noteAt(ctx);
101
+ if (note) deleteNote(ctx.session, note);
102
+ }
103
+ });
104
+ commands.register({
105
+ id: 'note.bringToFront',
106
+ title: 'Bring Note to Front',
107
+ run: (ctx) => {
108
+ const note = noteAt(ctx);
109
+ if (note) reorderNote(ctx.session, note, 'front');
110
+ }
111
+ });
112
+ commands.register({
113
+ id: 'note.sendToBack',
114
+ title: 'Send Note to Back',
115
+ run: (ctx) => {
116
+ const note = noteAt(ctx);
117
+ if (note) reorderNote(ctx.session, note, 'back');
118
+ }
119
+ });
120
+
121
+ // Notes get their own node context menu; the behave items (trace, pin,
122
+ // ...) are guarded by `when` in the core defaults and stay hidden here.
123
+ const menu = system.contextMenuStore.getState();
124
+ const noteOnly = (ctx: CommandContext) => Boolean(noteAt(ctx));
125
+ menu.register({
126
+ id: 'note.duplicate',
127
+ target: 'node',
128
+ label: 'Duplicate',
129
+ order: 10,
130
+ group: 'note',
131
+ when: noteOnly,
132
+ commandId: 'note.duplicate'
133
+ });
134
+ menu.register({
135
+ id: 'note.bringToFront',
136
+ target: 'node',
137
+ label: 'Bring to Front',
138
+ order: 20,
139
+ group: 'note-order',
140
+ when: noteOnly,
141
+ commandId: 'note.bringToFront'
142
+ });
143
+ menu.register({
144
+ id: 'note.sendToBack',
145
+ target: 'node',
146
+ label: 'Send to Back',
147
+ order: 21,
148
+ group: 'note-order',
149
+ when: noteOnly,
150
+ commandId: 'note.sendToBack'
151
+ });
152
+ menu.register({
153
+ id: 'note.delete',
154
+ target: 'node',
155
+ label: 'Delete',
156
+ order: 30,
157
+ group: 'note-danger',
158
+ when: noteOnly,
159
+ commandId: 'note.delete'
160
+ });
161
+
162
+ const dispatchAddNote = () => {
163
+ const session = system.session;
164
+ if (!session) return;
165
+ void system.commandStore
166
+ .getState()
167
+ .run('notes.addNote', { editor: system, session });
168
+ };
169
+
170
+ system.toolbarStore.getState().addGroup({
171
+ id: 'notes',
172
+ label: 'Notes',
173
+ buttons: [
174
+ {
175
+ id: 'notes.addNote',
176
+ icon: <Notes />,
177
+ label: 'Add Note',
178
+ onClick: dispatchAddNote
179
+ }
180
+ ]
181
+ });
182
+
183
+ system.hotKeyStore.getState().register({
184
+ action: 'ADD_NOTE',
185
+ description: 'Add a markdown note to the graph',
186
+ trigger: 'shift+n',
187
+ handler: dispatchAddNote
188
+ });
189
+ },
190
+ { name: 'notes' }
191
+ );
@@ -0,0 +1,100 @@
1
+ import { v4 as uuidv4 } from 'uuid';
2
+ import type { Node } from 'reactflow';
3
+ import type { CommandContext } from '@/store/commands';
4
+ import type { GraphSession } from '@/system/graphSession';
5
+
6
+ /** Node type registered for notes created by this plugin. */
7
+ export const NOTE_NODE_TYPE = 'noteNode';
8
+
9
+ /**
10
+ * Type string notes used while they lived in the core editor as "comment"
11
+ * nodes. Registered as an alias so graphs saved before the move still render.
12
+ */
13
+ export const LEGACY_COMMENT_NODE_TYPE = 'commentNode';
14
+
15
+ export const isNoteNode = (node: Node | undefined): node is Node =>
16
+ node?.type === NOTE_NODE_TYPE || node?.type === LEGACY_COMMENT_NODE_TYPE;
17
+
18
+ export const noteAt = (ctx: CommandContext): Node | undefined => {
19
+ const node = ctx.session.nodeStore
20
+ .getState()
21
+ .nodes.find((n) => n.id === ctx.nodeId);
22
+ return isNoteNode(node) ? node : undefined;
23
+ };
24
+
25
+ export const duplicateNote = (session: GraphSession, note: Node): void => {
26
+ const copy: Node = {
27
+ ...note,
28
+ id: uuidv4(),
29
+ selected: false,
30
+ position: { x: note.position.x + 24, y: note.position.y + 24 },
31
+ data: { ...note.data }
32
+ };
33
+ session.undoManager.execute({
34
+ name: 'Duplicate note',
35
+ execute: () => {
36
+ session.nodeStore.getState().addNode(copy);
37
+ },
38
+ undo: () => {
39
+ session.nodeStore
40
+ .getState()
41
+ .setNodes((existing) => existing.filter((n) => n.id !== copy.id));
42
+ }
43
+ });
44
+ };
45
+
46
+ export const deleteNote = (session: GraphSession, note: Node): void => {
47
+ const index = session.nodeStore
48
+ .getState()
49
+ .nodes.findIndex((n) => n.id === note.id);
50
+ session.undoManager.execute({
51
+ name: 'Delete note',
52
+ execute: () => {
53
+ session.nodeStore
54
+ .getState()
55
+ .setNodes((existing) => existing.filter((n) => n.id !== note.id));
56
+ },
57
+ undo: () => {
58
+ session.nodeStore.getState().setNodes((existing) => {
59
+ const next = [...existing];
60
+ next.splice(Math.min(index, next.length), 0, note);
61
+ return next;
62
+ });
63
+ }
64
+ });
65
+ };
66
+
67
+ /**
68
+ * Move a note within the nodes array: React Flow paints later nodes on top, so
69
+ * array order is z-order.
70
+ */
71
+ export const reorderNote = (
72
+ session: GraphSession,
73
+ note: Node,
74
+ to: 'front' | 'back'
75
+ ): void => {
76
+ const from = session.nodeStore
77
+ .getState()
78
+ .nodes.findIndex((n) => n.id === note.id);
79
+ if (from < 0) return;
80
+
81
+ const move = (position: 'front' | 'back' | number) => {
82
+ session.nodeStore.getState().setNodes((existing) => {
83
+ const index = existing.findIndex((n) => n.id === note.id);
84
+ if (index < 0) return existing;
85
+ const next = [...existing];
86
+ const [moved] = next.splice(index, 1);
87
+ if (moved === undefined) return existing;
88
+ if (position === 'front') next.push(moved);
89
+ else if (position === 'back') next.unshift(moved);
90
+ else next.splice(Math.min(position, next.length), 0, moved);
91
+ return next;
92
+ });
93
+ };
94
+
95
+ session.undoManager.execute({
96
+ name: to === 'front' ? 'Bring note to front' : 'Send note to back',
97
+ execute: () => move(to),
98
+ undo: () => move(from)
99
+ });
100
+ };
@@ -0,0 +1,20 @@
1
+ import { lazy, memo, Suspense } from 'react';
2
+ import type { NodeProps } from 'reactflow';
3
+ import type { INoteNode } from '@/types/nodes';
4
+
5
+ /**
6
+ * Note nodes embed a tiptap/prosemirror rich-text editor (~320 KB). Most
7
+ * graphs have no note nodes, so load the implementation lazily — the editor
8
+ * weight only enters the bundle when a note node is actually rendered.
9
+ */
10
+ const LazyNoteNode = lazy(() =>
11
+ import('./noteImpl').then((m) => ({ default: m.NoteNodeImpl }))
12
+ );
13
+
14
+ const NoteNodeRaw = (props: NodeProps<INoteNode['data']>) => (
15
+ <Suspense fallback={null}>
16
+ <LazyNoteNode {...props} />
17
+ </Suspense>
18
+ );
19
+
20
+ export const NoteNode = memo(NoteNodeRaw);
@@ -0,0 +1,89 @@
1
+ import { NodeResizer } from 'reactflow';
2
+ import { ErrorBoundary } from 'react-error-boundary';
3
+ import { useEditor, EditorContent } from '@tiptap/react';
4
+ import StarterKit from '@tiptap/starter-kit';
5
+ import Youtube from '@tiptap/extension-youtube';
6
+ import { useChangeNodeData } from '@/hooks/useChangeNodeData';
7
+ import { Markdown } from 'tiptap-markdown';
8
+ import type { INoteNode } from '@/types/nodes';
9
+ import { FormatToolbar } from './FormatToolbar';
10
+ import type { NodeProps } from 'reactflow';
11
+
12
+ const NOTE_MIN_WIDTH = 160;
13
+ const NOTE_MIN_HEIGHT = 80;
14
+
15
+ /**
16
+ * The actual note node, pulling in tiptap/prosemirror (~320 KB). It is loaded
17
+ * lazily by `note.tsx` so that weight only lands in the bundle when a note
18
+ * node is actually rendered.
19
+ *
20
+ * The note draws its own frame directly against the React Flow node bounds.
21
+ * It must NOT use BaseNodeWrapper: that adds behave-node chrome (a second
22
+ * bordered box) and breaks the height chain, so the frame sizes to content
23
+ * instead of the node and drifts out of the NodeResizer bounds.
24
+ *
25
+ * Layout: the root div only positions the NodeResizer and floating toolbar (it
26
+ * must not clip, or the toolbar disappears); the frame inside it clips. Only
27
+ * the header drags the node (`dragHandle` set by `addNote`) — the body is
28
+ * `nodrag nopan` so selecting text neither moves the node nor pans the canvas.
29
+ * The editor is always editable: a single click gives a caret; the format
30
+ * toolbar shows while the node is selected.
31
+ *
32
+ * Font size cascades from a React `style` on the root — never write to
33
+ * `editor.view.dom` in an effect; tiptap v3 throws if the view has not mounted.
34
+ */
35
+ export const NoteNodeImpl = ({
36
+ data,
37
+ selected,
38
+ id
39
+ }: NodeProps<INoteNode['data']>) => {
40
+ const handleNodeChange = useChangeNodeData(id);
41
+
42
+ const editor = useEditor({
43
+ extensions: [StarterKit, Markdown, Youtube.configure({ nocookie: true })],
44
+ content: data.text || 'New note',
45
+ editorProps: {
46
+ attributes: {
47
+ class: 'nodrag nopan'
48
+ }
49
+ },
50
+ onUpdate: ({ editor }) => {
51
+ const markdown = (
52
+ editor.storage as { markdown?: { getMarkdown(): string } }
53
+ ).markdown;
54
+ if (markdown) {
55
+ handleNodeChange('text', markdown.getMarkdown());
56
+ }
57
+ }
58
+ });
59
+
60
+ return (
61
+ <ErrorBoundary fallback={<div>Error rendering note</div>}>
62
+ <div
63
+ className="notes-node-root"
64
+ style={{ fontSize: data.fontSize || 'medium' }}
65
+ >
66
+ <NodeResizer
67
+ minWidth={NOTE_MIN_WIDTH}
68
+ minHeight={NOTE_MIN_HEIGHT}
69
+ color="#ff0071"
70
+ isVisible={selected || false}
71
+ />
72
+ {editor && selected && <FormatToolbar editor={editor} />}
73
+
74
+ <div className="notes-node">
75
+ <div className="notes-node__header">
76
+ <span className="notes-node__grip" aria-hidden>
77
+
78
+ </span>
79
+ <span>Note</span>
80
+ </div>
81
+ <EditorContent
82
+ editor={editor}
83
+ className="notes-node__editor nodrag nopan"
84
+ />
85
+ </div>
86
+ </div>
87
+ </ErrorBoundary>
88
+ );
89
+ };