@kiberon-labs/behave-graph-flow 1.0.0 → 2.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 (314) hide show
  1. package/.fallowrc.json +16 -0
  2. package/.storybook/main.ts +32 -0
  3. package/.storybook/preview.ts +16 -0
  4. package/.storybook/styles.css +10 -0
  5. package/.storybook/vscode.css +814 -0
  6. package/.turbo/turbo-build.log +7 -0
  7. package/LICENSE +6 -0
  8. package/README.md +2 -2
  9. package/data/Polynomial.json +510 -0
  10. package/data/sequence.json +337 -0
  11. package/data/trigger-event.json +241 -0
  12. package/data/variable-change.json +210 -0
  13. package/dist/entry.css +4 -0
  14. package/dist/index.css +39 -0
  15. package/dist/index.css.map +1 -0
  16. package/dist/index.d.ts +2282 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +14873 -0
  19. package/dist/index.js.map +1 -0
  20. package/docs/notifications.md +246 -0
  21. package/docs/protocol.md +679 -0
  22. package/docs/specifics.md +191 -0
  23. package/package.json +85 -21
  24. package/postcss.config.ts +3 -4
  25. package/src/annotations/index.ts +32 -0
  26. package/src/components/FloatingToolbar/index.module.css +45 -0
  27. package/src/components/FloatingToolbar/index.tsx +256 -0
  28. package/src/components/Flow.tsx +276 -75
  29. package/src/components/contextMenus/NodePicker.module.css +274 -0
  30. package/src/components/contextMenus/NodePicker.tsx +481 -0
  31. package/src/components/contextMenus/edge.tsx +108 -0
  32. package/src/components/contextMenus/node.tsx +155 -0
  33. package/src/components/contextMenus/selection.tsx +77 -0
  34. package/src/components/controls/any/index.tsx +8 -0
  35. package/src/components/controls/boolean/index.tsx +13 -0
  36. package/src/components/controls/colorPicker/InputPopover.module.css +100 -0
  37. package/src/components/controls/colorPicker/InputPopover.tsx +31 -0
  38. package/src/components/controls/colorPicker/index.module.css +18 -0
  39. package/src/components/controls/colorPicker/index.tsx +61 -0
  40. package/src/components/controls/number/index.tsx +35 -0
  41. package/src/components/controls/string/index.tsx +16 -0
  42. package/src/components/edges/index.tsx +469 -0
  43. package/src/components/edges/offsetBezier.ts +134 -0
  44. package/src/components/hotKeys.tsx +20 -0
  45. package/src/components/layoutController/index.module.css +10 -0
  46. package/src/components/layoutController/index.tsx +117 -0
  47. package/src/components/layoutController/utils.ts +205 -0
  48. package/src/components/menubar/defaults.tsx +480 -0
  49. package/src/components/menubar/index.tsx +49 -0
  50. package/src/components/menubar/menuItem.module.css +16 -0
  51. package/src/components/menubar/menuItem.tsx +32 -0
  52. package/src/components/nodes/behave/Node.module.css +23 -0
  53. package/src/components/nodes/behave/Node.tsx +176 -0
  54. package/src/components/nodes/behave/NodeContainer.module.css +87 -0
  55. package/src/components/nodes/behave/NodeContainer.tsx +46 -0
  56. package/src/components/nodes/behave/index.tsx +14 -0
  57. package/src/components/nodes/comment/FormatToolbar.tsx +118 -0
  58. package/src/components/nodes/comment/comment.tsx +103 -0
  59. package/src/components/nodes/comment/styles.module.css +150 -0
  60. package/src/components/nodes/group/index.tsx +109 -0
  61. package/src/components/nodes/wrapper/index.tsx +73 -0
  62. package/src/components/nodes/wrapper/styles.module.css +113 -0
  63. package/src/components/notifications/NotificationProvider.tsx +81 -0
  64. package/src/components/notifications/index.ts +2 -0
  65. package/src/components/notifications/utils.ts +71 -0
  66. package/src/components/panels/alignment/index.module.css +20 -0
  67. package/src/components/panels/alignment/index.tsx +244 -0
  68. package/src/components/panels/base/index.tsx +5 -0
  69. package/src/components/panels/base/styles.module.css +12 -0
  70. package/src/components/panels/conversation/index.module.css +151 -0
  71. package/src/components/panels/conversation/index.tsx +162 -0
  72. package/src/components/panels/events/CustomEventsEditor.tsx +384 -0
  73. package/src/components/panels/events/EditEventPanel.tsx +315 -0
  74. package/src/components/panels/events/ManageEventsPanel.tsx +98 -0
  75. package/src/components/panels/events/index.tsx +23 -0
  76. package/src/components/panels/events/styles.module.css +236 -0
  77. package/src/components/panels/history/index.tsx +92 -0
  78. package/src/components/panels/history/styles.module.css +106 -0
  79. package/src/components/panels/keymaps/index.module.css +78 -0
  80. package/src/components/panels/keymaps/index.tsx +167 -0
  81. package/src/components/panels/layers/index.tsx +240 -0
  82. package/src/components/panels/layers/styles.module.css +110 -0
  83. package/src/components/panels/legend/index.module.css +6 -0
  84. package/src/components/panels/legend/index.tsx +76 -0
  85. package/src/components/panels/logs/index.module.css +212 -0
  86. package/src/components/panels/logs/index.tsx +288 -0
  87. package/src/components/panels/nodeInputs/InputControl.tsx +63 -0
  88. package/src/components/panels/nodeInputs/InputsGroup.tsx +64 -0
  89. package/src/components/panels/nodeInputs/MultipleNodesView.tsx +37 -0
  90. package/src/components/panels/nodeInputs/NodeSettings.tsx +92 -0
  91. package/src/components/panels/nodeInputs/NodeTitleEditor.tsx +125 -0
  92. package/src/components/panels/nodeInputs/OutputsGroup.tsx +65 -0
  93. package/src/components/panels/nodeInputs/SocketGenerators.tsx +32 -0
  94. package/src/components/panels/nodeInputs/index.module.css +284 -0
  95. package/src/components/panels/nodeInputs/index.tsx +339 -0
  96. package/src/components/panels/nodeInputs/useNodeHandlers.ts +76 -0
  97. package/src/components/panels/nodeInputs/useNodeInputsData.ts +173 -0
  98. package/src/components/panels/nodePicker/index.tsx +115 -0
  99. package/src/components/panels/panel/index.module.css +66 -0
  100. package/src/components/panels/panel/index.tsx +88 -0
  101. package/src/components/panels/search/index.module.css +66 -0
  102. package/src/components/panels/search/index.tsx +215 -0
  103. package/src/components/panels/systemSettings/index.tsx +206 -0
  104. package/src/components/panels/systemSettings/styles.module.css +11 -0
  105. package/src/components/panels/traces/GridLines.tsx +38 -0
  106. package/src/components/panels/traces/TimeGrid.tsx +48 -0
  107. package/src/components/panels/traces/TraceLane.tsx +62 -0
  108. package/src/components/panels/traces/TraceTooltip.tsx +22 -0
  109. package/src/components/panels/traces/TracesHeader.tsx +56 -0
  110. package/src/components/panels/traces/index.module.css +166 -0
  111. package/src/components/panels/traces/index.tsx +294 -0
  112. package/src/components/panels/traces/types.ts +48 -0
  113. package/src/components/panels/traces/useDerivedSpans.ts +212 -0
  114. package/src/components/panels/traces/utils.ts +25 -0
  115. package/src/components/panels/variables/CreateVariableScreen.tsx +162 -0
  116. package/src/components/panels/variables/ManageVariablesScreen.tsx +144 -0
  117. package/src/components/panels/variables/index.tsx +125 -0
  118. package/src/components/panels/variables/styles.module.css +236 -0
  119. package/src/components/primitives/icon.module.css +45 -0
  120. package/src/components/primitives/icon.tsx +38 -0
  121. package/src/components/sockets/input/index.tsx +76 -0
  122. package/src/components/sockets/input/styles.module.css +27 -0
  123. package/src/components/sockets/output/index.tsx +61 -0
  124. package/src/components/sockets/output/styles.module.css +27 -0
  125. package/src/css/prosemirror.css +57 -0
  126. package/src/css/rc-dock.css +112 -0
  127. package/src/css/rc-menu.css +100 -0
  128. package/src/css/vars.css +14 -0
  129. package/src/css/vscode.css +13 -0
  130. package/src/entry.css +4 -0
  131. package/src/generators/CustomEventOnTriggeredGenerator.tsx +85 -0
  132. package/src/generators/SequenceGenerator.tsx +104 -0
  133. package/src/generators/SwitchOnIntegerGenerator.tsx +256 -0
  134. package/src/generators/SwitchOnStringGenerator.tsx +263 -0
  135. package/src/generators/registerDefaultGenerators.ts +34 -0
  136. package/src/hooks/useBehaveGraphFlow.ts +17 -16
  137. package/src/hooks/useDetachNodes.ts +39 -0
  138. package/src/hooks/useFlowHandlers.ts +115 -29
  139. package/src/hooks/useWasdPan.ts +188 -0
  140. package/src/index.css +146 -0
  141. package/src/index.ts +36 -18
  142. package/src/layout/dagre.tsx +119 -0
  143. package/src/layout/elk.ts +200 -0
  144. package/src/plugin/alignment/index.ts +81 -0
  145. package/src/plugin/docs/index.tsx +299 -0
  146. package/src/plugin/docs/panel/index.tsx +200 -0
  147. package/src/plugin/docs/panel/styles.module.css +174 -0
  148. package/src/plugin/graphrunner/actions.ts +253 -0
  149. package/src/plugin/graphrunner/buttons.tsx +87 -0
  150. package/src/plugin/graphrunner/client.ts +704 -0
  151. package/src/plugin/graphrunner/index.tsx +255 -0
  152. package/src/plugin/graphrunner/panel.tsx +386 -0
  153. package/src/plugin/graphrunner/runner.ts +358 -0
  154. package/src/plugin/graphrunner/session.ts +243 -0
  155. package/src/plugin/graphrunner/store.ts +206 -0
  156. package/src/plugin/graphrunner/styles.module.css +211 -0
  157. package/src/plugin/graphrunner/transport.ts +224 -0
  158. package/src/plugin/graphrunner/types.ts +672 -0
  159. package/src/plugin/graphrunner-local/execution-utils.ts +457 -0
  160. package/src/plugin/graphrunner-local/index.tsx +166 -0
  161. package/src/plugin/graphrunner-local/panel.tsx +231 -0
  162. package/src/plugin/graphrunner-local/store.ts +41 -0
  163. package/src/plugin/graphrunner-local/styles.module.css +101 -0
  164. package/src/plugin/graphrunner-local/transport.ts +1372 -0
  165. package/src/plugin/graphrunner-local/types.ts +10 -0
  166. package/src/plugin/graphrunner-webworker/graph-executor.worker.ts +633 -0
  167. package/src/plugin/graphrunner-webworker/index.tsx +146 -0
  168. package/src/plugin/graphrunner-webworker/panel.tsx +173 -0
  169. package/src/plugin/graphrunner-webworker/store.ts +89 -0
  170. package/src/plugin/graphrunner-webworker/types.ts +17 -0
  171. package/src/plugin/graphrunner-webworker/worker-transport.ts +123 -0
  172. package/src/plugin/realtime/realtimeRunner.ts +570 -0
  173. package/src/specifics/CustomEventOnTriggeredSpecific.tsx +92 -0
  174. package/src/specifics/CustomEventTriggerSpecific.tsx +141 -0
  175. package/src/specifics/VariableGetSpecific.tsx +110 -0
  176. package/src/specifics/VariableSetSpecific.tsx +110 -0
  177. package/src/specifics/registerDefaultSpecifics.ts +5 -0
  178. package/src/store/actions.tsx +698 -0
  179. package/src/store/chat.ts +73 -0
  180. package/src/store/controls.tsx +62 -0
  181. package/src/store/documentation.tsx +69 -0
  182. package/src/store/events.tsx +116 -0
  183. package/src/store/flow.tsx +245 -0
  184. package/src/store/graphRunnerClient.ts +110 -0
  185. package/src/store/hotKeys.tsx +323 -0
  186. package/src/store/layers.ts +259 -0
  187. package/src/store/legend.tsx +76 -0
  188. package/src/store/logs.ts +28 -0
  189. package/src/store/menubar.ts +41 -0
  190. package/src/store/refs.ts +84 -0
  191. package/src/store/registry.ts +43 -0
  192. package/src/store/selection.ts +22 -0
  193. package/src/store/settings.ts +99 -0
  194. package/src/store/socketGenerator.tsx +54 -0
  195. package/src/store/specific.tsx +75 -0
  196. package/src/store/specs.tsx +35 -0
  197. package/src/store/tabs.ts +278 -0
  198. package/src/store/toolbar.tsx +45 -0
  199. package/src/store/traces.ts +240 -0
  200. package/src/store/variables.ts +37 -0
  201. package/src/system/graph.ts +134 -0
  202. package/src/system/index.ts +3 -0
  203. package/src/system/notifications.ts +98 -0
  204. package/src/system/plugin.ts +27 -0
  205. package/src/system/provider.tsx +22 -0
  206. package/src/system/pubsub.ts +323 -0
  207. package/src/system/system.ts +223 -0
  208. package/src/system/tabLoader.tsx +265 -0
  209. package/src/system/undoRedo.ts +103 -0
  210. package/src/transformers/Uigraph.ts +60 -0
  211. package/src/transformers/behaveToFlow.ts +16 -4
  212. package/src/transformers/flowToBehave.ts +32 -12
  213. package/src/types/NodeMetadata.ts +27 -0
  214. package/src/types/graph.ts +49 -0
  215. package/src/types/nodes.ts +45 -0
  216. package/src/types.ts +16 -0
  217. package/src/util/colors.ts +1 -29
  218. package/src/util/downloadJson.ts +18 -0
  219. package/src/util/extractNodeMetadata.ts +16 -0
  220. package/src/util/getPickerFilters.ts +1 -1
  221. package/src/util/isBehaveNode.ts +6 -0
  222. package/src/util/isValidConnection.ts +28 -15
  223. package/src/util/mergeSockets.ts +29 -0
  224. package/src/util/serializeVariables.ts +66 -0
  225. package/src/util/sockets.ts +43 -0
  226. package/stories/apex/layoutController/example-graph.worker.ts +39 -0
  227. package/stories/apex/layoutController/index.stories.tsx +48 -0
  228. package/stories/apex/layoutController/webworker.stories.tsx +103 -0
  229. package/stories/apex/menubar/menubar.stories.tsx +19 -0
  230. package/stories/components/colorpicker/index.stories.tsx +20 -0
  231. package/stories/components/contextMenus/edge.stories.tsx +32 -0
  232. package/stories/components/contextMenus/node.stories.tsx +26 -0
  233. package/stories/components/contextMenus/nodePicker.stories.tsx +115 -0
  234. package/stories/components/controls/any/index.stories.tsx +19 -0
  235. package/stories/components/controls/boolean/index.stories.tsx +19 -0
  236. package/stories/components/controls/colorPicker/index.stories.tsx +49 -0
  237. package/stories/components/controls/number/index.stories.tsx +19 -0
  238. package/stories/components/controls/string/index.stories.tsx +19 -0
  239. package/stories/components/nodes/behaveNode.stories.tsx +108 -0
  240. package/stories/components/nodes/comment.stories.tsx +106 -0
  241. package/stories/components/panels/alignment.stories.tsx +24 -0
  242. package/stories/components/panels/events.stories.tsx +38 -0
  243. package/stories/components/panels/graphRunner.stories.tsx +317 -0
  244. package/stories/components/panels/history.stories.tsx +37 -0
  245. package/stories/components/panels/keymaps.stories.tsx +21 -0
  246. package/stories/components/panels/legend.stories.tsx +37 -0
  247. package/stories/components/panels/logs.stories.tsx +24 -0
  248. package/stories/components/panels/nodeInputs.stories.tsx +21 -0
  249. package/stories/components/panels/nodePicker.stories.tsx +37 -0
  250. package/stories/components/panels/panel.stories.tsx +39 -0
  251. package/stories/components/panels/search.stories.tsx +24 -0
  252. package/stories/components/panels/systemSettings.stories.tsx +26 -0
  253. package/stories/components/panels/traces.stories.tsx +225 -0
  254. package/stories/components/panels/variables.stories.tsx +24 -0
  255. package/stories/defaults/defaultStoryProvider.tsx +167 -0
  256. package/stories/defaults/systemGenerator.ts +38 -0
  257. package/tests/components/edges/offsetBezier.test.ts +51 -0
  258. package/tests/components/layoutController/utils.test.ts +68 -0
  259. package/tests/components/panels/traces/utils.test.ts +52 -0
  260. package/tests/flowToBehave.test.ts +26 -4
  261. package/tests/notifications.test.ts +87 -0
  262. package/tests/saveLoad.test.ts +372 -0
  263. package/tests/util/calculateNewEdge.test.ts +98 -0
  264. package/tests/util/getSocketsByNodeTypeAndHandleType.test.ts +31 -0
  265. package/tests/util/hasPositionMetaData.test.ts +33 -0
  266. package/tests/util/isBehaveNode.test.ts +22 -0
  267. package/tests/util/isHandleConnected.test.ts +37 -0
  268. package/tests/util/mergeSockets.test.ts +43 -0
  269. package/tests/visual/README.md +64 -0
  270. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-alignment-chromium-win32.png +0 -0
  271. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-conversation-chromium-win32.png +0 -0
  272. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-events-chromium-win32.png +0 -0
  273. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-history-chromium-win32.png +0 -0
  274. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-keymaps-chromium-win32.png +0 -0
  275. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-layers-chromium-win32.png +0 -0
  276. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-legend-chromium-win32.png +0 -0
  277. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-logs-chromium-win32.png +0 -0
  278. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-nodeInputs-chromium-win32.png +0 -0
  279. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-nodePicker-chromium-win32.png +0 -0
  280. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-panel-chromium-win32.png +0 -0
  281. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-search-chromium-win32.png +0 -0
  282. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-systemSettings-chromium-win32.png +0 -0
  283. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-traces-chromium-win32.png +0 -0
  284. package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-variables-chromium-win32.png +0 -0
  285. package/tests/visual/panels.visual.test.tsx +76 -0
  286. package/tsconfig.base.json +39 -0
  287. package/tsconfig.json +18 -59
  288. package/tsconfig.prod.json +23 -0
  289. package/tsdown.config.ts +15 -3
  290. package/typedoc.json +7 -7
  291. package/vite.config.js +7 -0
  292. package/vitest.config.ts +5 -2
  293. package/vitest.visual.config.ts +48 -0
  294. package/src/components/AutoSizeInput.tsx +0 -65
  295. package/src/components/Controls.tsx +0 -87
  296. package/src/components/InputSocket.tsx +0 -142
  297. package/src/components/Node.tsx +0 -68
  298. package/src/components/NodeContainer.tsx +0 -46
  299. package/src/components/NodePicker.tsx +0 -77
  300. package/src/components/OutputSocket.tsx +0 -58
  301. package/src/components/modals/ClearModal.tsx +0 -40
  302. package/src/components/modals/HelpModal.tsx +0 -36
  303. package/src/components/modals/LoadModal.tsx +0 -96
  304. package/src/components/modals/Modal.tsx +0 -64
  305. package/src/components/modals/SaveModal.tsx +0 -60
  306. package/src/hooks/useCustomNodeTypes.tsx +0 -31
  307. package/src/hooks/useGraphRunner.ts +0 -104
  308. package/src/hooks/useMergeMap.ts +0 -14
  309. package/src/hooks/useNodeSpecJson.ts +0 -20
  310. package/src/hooks/useQueriableDefinitions.ts +0 -22
  311. package/src/styles.css +0 -8
  312. package/tailwind.config.ts +0 -19
  313. package/tests/tsconfig.json +0 -10
  314. /package/src/{types.d.ts → types-declarations.d.ts} +0 -0
@@ -0,0 +1,384 @@
1
+ import React, { useEffect, useMemo, useState } from 'react';
2
+ import { useStore } from 'zustand';
3
+ import {
4
+ VscodeButton,
5
+ VscodeCollapsible,
6
+ VscodeDivider,
7
+ VscodeOption,
8
+ VscodeSingleSelect,
9
+ VscodeTextfield
10
+ } from '@vscode-elements/react-elements';
11
+ import { useSystem } from '@/system';
12
+ import type { GraphJSON } from '@kiberon-labs/behave-graph';
13
+ import { Download, Plus, Trash } from 'iconoir-react';
14
+
15
+ type CustomEventJSON = NonNullable<GraphJSON['customEvents']>[number];
16
+
17
+ type CustomEventParameterJSON = NonNullable<
18
+ CustomEventJSON['parameters']
19
+ >[number];
20
+
21
+ const nextNumericId = (events: Array<{ id: string }>) => {
22
+ const used = new Set(events.map((e) => String(e.id)));
23
+ let i = 0;
24
+ while (used.has(String(i))) i += 1;
25
+ return String(i);
26
+ };
27
+
28
+ export const CustomEventsEditor: React.FC = () => {
29
+ const system = useSystem();
30
+
31
+ const eventsStoreApi = system.eventsStore;
32
+ const persistedEvents = useStore(system.eventsStore, (s) =>
33
+ Object.values(s.customEvents)
34
+ );
35
+
36
+ const controls = useStore(system.controlStore, (s) => s.controls);
37
+ const defaultControl = useStore(system.controlStore, (s) => s.defaultControl);
38
+
39
+ const valueTypeOptions = useMemo(
40
+ () => Object.keys(controls).sort((a, b) => a.localeCompare(b)),
41
+ [controls]
42
+ );
43
+
44
+ const [newEventName, setNewEventName] = useState('');
45
+
46
+ const [dirtyEventUiIds, setDirtyEventUiIds] = useState<string[]>([]);
47
+ const [draftEvents, setDraftEvents] = useState<CustomEventJSON[]>([]);
48
+
49
+ const isClean = dirtyEventUiIds.length === 0;
50
+
51
+ const markDirty = (uiId: string) => {
52
+ setDirtyEventUiIds((prev) =>
53
+ prev.includes(uiId) ? prev : [...prev, uiId]
54
+ );
55
+ };
56
+
57
+ const markClean = (uiId: string) => {
58
+ setDirtyEventUiIds((prev) => prev.filter((id) => id !== uiId));
59
+ };
60
+
61
+ useEffect(() => {
62
+ if (!isClean) return;
63
+ setDraftEvents(persistedEvents);
64
+ }, [isClean, persistedEvents]);
65
+
66
+ const commitEvent = (uiId: string) => {
67
+ const draft = draftEvents.find((e) => String(e.id) === uiId);
68
+ if (!draft) return;
69
+
70
+ const persisted = persistedEvents;
71
+ const existing = persisted.find((e) => String(e.id) === uiId);
72
+ const oldId = existing ? String(existing.id ?? '') : undefined;
73
+
74
+ const usedIds = new Set(
75
+ persisted.filter((e) => String(e.id) !== uiId).map((e) => String(e.id))
76
+ );
77
+
78
+ const desiredId = String(draft.id);
79
+ let finalId = desiredId;
80
+ if (usedIds.has(desiredId)) {
81
+ finalId = oldId ?? nextNumericId(persisted);
82
+ }
83
+
84
+ const normalized = { ...draft, id: finalId };
85
+
86
+ const nextPersisted = existing
87
+ ? persisted.map((e) => (String(e.id) === uiId ? normalized : e))
88
+ : [...persisted, normalized];
89
+
90
+ // Convert array to Record for store
91
+ const nextPersistedRecord: Record<string, CustomEventJSON> = {};
92
+ nextPersisted.forEach((evt) => {
93
+ nextPersistedRecord[String(evt.id)] = evt;
94
+ });
95
+
96
+ eventsStoreApi.getState().setCustomEvents(nextPersistedRecord);
97
+
98
+ const stored = eventsStoreApi.getState().getCustomEvents();
99
+ const storedEvent = stored.find((e) => String(e.id) === uiId);
100
+ if (storedEvent) {
101
+ setDraftEvents((prev) =>
102
+ prev.map((e) => (String(e.id) === uiId ? storedEvent : e))
103
+ );
104
+ }
105
+
106
+ markClean(uiId);
107
+ };
108
+
109
+ const updateDraftEvent = (id: string, patch: Partial<CustomEventJSON>) => {
110
+ setDraftEvents((prev) =>
111
+ prev.map((e) => {
112
+ if (String(e.id) !== String(id)) return e;
113
+ return { ...e, ...patch };
114
+ })
115
+ );
116
+ markDirty(id);
117
+ };
118
+
119
+ const removeDraftEvent = (uiId: string) => {
120
+ const persisted = persistedEvents;
121
+ const persistedMatch = persisted.find((e) => String(e.id) === uiId);
122
+
123
+ setDraftEvents((prev) => prev.filter((e) => String(e.id) !== String(uiId)));
124
+
125
+ // If it exists in the store, delete it immediately.
126
+ if (persistedMatch) {
127
+ const idToRemove = String(persistedMatch.id);
128
+ if (idToRemove) eventsStoreApi.getState().removeCustomEvent(idToRemove);
129
+ }
130
+
131
+ markClean(uiId);
132
+ };
133
+
134
+ const addDraftEvent = () => {
135
+ const nextId = nextNumericId(draftEvents);
136
+ const created: CustomEventJSON = {
137
+ id: nextId,
138
+ name: newEventName.trim() || 'NewCustomEvent',
139
+ parameters: []
140
+ };
141
+ setDraftEvents((prev) => [...prev, created]);
142
+ setNewEventName('');
143
+ markDirty(String(created.id));
144
+ };
145
+
146
+ const addDraftParameter = (uiId: string) => {
147
+ const vt = valueTypeOptions[0] ?? 'string';
148
+ const defaultValue =
149
+ vt === 'boolean'
150
+ ? false
151
+ : vt === 'number' || vt === 'float' || vt === 'integer'
152
+ ? 0
153
+ : '';
154
+
155
+ setDraftEvents((prev) =>
156
+ prev.map((e) => {
157
+ if (String(e.id) !== String(uiId)) return e;
158
+ const parameters = [
159
+ ...((e.parameters ?? []) as CustomEventParameterJSON[]),
160
+ {
161
+ name: 'param',
162
+ valueTypeName: vt,
163
+ defaultValue
164
+ }
165
+ ];
166
+ return { ...e, parameters };
167
+ })
168
+ );
169
+ markDirty(uiId);
170
+ };
171
+
172
+ const updateDraftParameter = (
173
+ uiId: string,
174
+ index: number,
175
+ patch: Partial<CustomEventParameterJSON>
176
+ ) => {
177
+ setDraftEvents((prev) =>
178
+ prev.map((e) => {
179
+ if (String(e.id) !== String(uiId)) return e;
180
+ const parameters = [
181
+ ...((e.parameters ?? []) as CustomEventParameterJSON[])
182
+ ];
183
+ if (!parameters[index]) return e;
184
+ parameters[index] = { ...parameters[index], ...patch };
185
+ return { ...e, parameters };
186
+ })
187
+ );
188
+ markDirty(uiId);
189
+ };
190
+
191
+ const removeDraftParameter = (uiId: string, index: number) => {
192
+ setDraftEvents((prev) =>
193
+ prev.map((e) => {
194
+ if (String(e.id) !== String(uiId)) return e;
195
+ const parameters = [
196
+ ...((e.parameters ?? []) as CustomEventParameterJSON[])
197
+ ];
198
+ parameters.splice(index, 1);
199
+ return { ...e, parameters };
200
+ })
201
+ );
202
+ markDirty(uiId);
203
+ };
204
+
205
+ return (
206
+ <div className="h-full w-full flex flex-col p-2 gap-2">
207
+ <div className="flex items-center gap-2 pb-2 border-b border-gray-700">
208
+ <h2 className="text-lg font-semibold">Custom Events</h2>
209
+ </div>
210
+
211
+ <div className="flex items-center gap-2">
212
+ <VscodeTextfield
213
+ placeholder="New event name"
214
+ value={newEventName}
215
+ onInput={(e: Event) => {
216
+ const target = e.target as HTMLInputElement;
217
+ setNewEventName(target.value);
218
+ }}
219
+ />
220
+ <VscodeButton onClick={addDraftEvent}>Add</VscodeButton>
221
+ </div>
222
+
223
+ <VscodeDivider />
224
+
225
+ {draftEvents.length === 0 ? (
226
+ <div className="text-gray-500 text-center py-4">
227
+ No custom events yet.
228
+ </div>
229
+ ) : (
230
+ <div className="flex flex-col gap-2">
231
+ {draftEvents.map((evt) => {
232
+ const parameters = (evt.parameters ??
233
+ []) as CustomEventParameterJSON[];
234
+ const eventId = String(evt.id);
235
+ const uiKey = String(evt.id);
236
+ const isEventDirty = dirtyEventUiIds.includes(uiKey);
237
+
238
+ return (
239
+ <VscodeCollapsible key={uiKey} title={`${evt.name} (${eventId})`}>
240
+ <div className="flex flex-col gap-2 p-2">
241
+ <div className="grid grid-cols-12 gap-2 items-end">
242
+ <div className="col-span-4 flex flex-col gap-1">
243
+ <div className="text-xs text-gray-300">id</div>
244
+ <VscodeTextfield
245
+ value={String(evt.id)}
246
+ onInput={(e: Event) => {
247
+ const target = e.target as HTMLInputElement;
248
+ updateDraftEvent(uiKey, {
249
+ id: String(target.value)
250
+ });
251
+ }}
252
+ />
253
+ </div>
254
+
255
+ <div className="col-span-6 flex flex-col gap-1">
256
+ <div className="text-xs text-gray-300">name</div>
257
+ <VscodeTextfield
258
+ value={String(evt.name)}
259
+ onInput={(e: Event) => {
260
+ const target = e.target as HTMLInputElement;
261
+ updateDraftEvent(uiKey, {
262
+ name: String(target.value)
263
+ });
264
+ }}
265
+ />
266
+ </div>
267
+
268
+ <div className="col-span-2 flex justify-end gap-1">
269
+ <VscodeButton
270
+ iconOnly
271
+ title="Save"
272
+ disabled={!isEventDirty}
273
+ onClick={() => commitEvent(uiKey)}
274
+ >
275
+ <Download />
276
+ </VscodeButton>
277
+ <VscodeButton
278
+ secondary
279
+ iconOnly
280
+ title="Delete event"
281
+ onClick={() => removeDraftEvent(uiKey)}
282
+ >
283
+ <Trash />
284
+ </VscodeButton>
285
+ </div>
286
+ </div>
287
+
288
+ <VscodeDivider />
289
+
290
+ <div className="flex items-center justify-between">
291
+ <div className="text-sm font-semibold">Parameters</div>
292
+ <VscodeButton
293
+ secondary
294
+ iconOnly
295
+ title="Add parameter"
296
+ onClick={() => addDraftParameter(uiKey)}
297
+ >
298
+ <Plus />
299
+ </VscodeButton>
300
+ </div>
301
+
302
+ {parameters.length === 0 ? (
303
+ <div className="text-gray-500 text-sm">No parameters.</div>
304
+ ) : (
305
+ <div className="flex flex-col gap-2">
306
+ <div className="grid grid-cols-12 gap-2 text-xs text-gray-300">
307
+ <div className="col-span-4">name</div>
308
+ <div className="col-span-3">type</div>
309
+ <div className="col-span-4">default</div>
310
+ <div className="col-span-1" />
311
+ </div>
312
+
313
+ {parameters.map((param, index) => {
314
+ const Control =
315
+ controls[String(param.valueTypeName)] ??
316
+ defaultControl;
317
+
318
+ return (
319
+ <div
320
+ key={`${eventId}:${index}`}
321
+ className="grid grid-cols-12 gap-2 items-center"
322
+ >
323
+ <VscodeTextfield
324
+ className="col-span-4"
325
+ value={String(param.name)}
326
+ onInput={(e: Event) => {
327
+ const target = e.target as HTMLInputElement;
328
+ updateDraftParameter(uiKey, index, {
329
+ name: String(target.value)
330
+ });
331
+ }}
332
+ />
333
+
334
+ <VscodeSingleSelect
335
+ className="col-span-3"
336
+ value={String(param.valueTypeName)}
337
+ onChange={(e: Event) => {
338
+ const target = e.target as HTMLSelectElement;
339
+ updateDraftParameter(uiKey, index, {
340
+ valueTypeName: String(target.value)
341
+ });
342
+ }}
343
+ >
344
+ {valueTypeOptions.map((vt) => (
345
+ <VscodeOption key={vt} value={vt}>
346
+ {vt}
347
+ </VscodeOption>
348
+ ))}
349
+ </VscodeSingleSelect>
350
+
351
+ <div className="col-span-4 min-w-0">
352
+ <Control
353
+ value={param.defaultValue}
354
+ onChange={(v) =>
355
+ updateDraftParameter(uiKey, index, {
356
+ defaultValue: v
357
+ })
358
+ }
359
+ valueType={String(param.valueTypeName ?? '')}
360
+ />
361
+ </div>
362
+
363
+ <VscodeButton
364
+ secondary
365
+ iconOnly
366
+ title="Delete parameter"
367
+ onClick={() => removeDraftParameter(uiKey, index)}
368
+ >
369
+ <Trash />
370
+ </VscodeButton>
371
+ </div>
372
+ );
373
+ })}
374
+ </div>
375
+ )}
376
+ </div>
377
+ </VscodeCollapsible>
378
+ );
379
+ })}
380
+ </div>
381
+ )}
382
+ </div>
383
+ );
384
+ };
@@ -0,0 +1,315 @@
1
+ import React, { useEffect, useMemo, useState } from 'react';
2
+ import { useStore } from 'zustand';
3
+ import {
4
+ VscodeBadge,
5
+ VscodeButton,
6
+ VscodeOption,
7
+ VscodeSingleSelect,
8
+ VscodeTextfield
9
+ } from '@vscode-elements/react-elements';
10
+
11
+ import { useSystem } from '@/system';
12
+ import { BasePanel } from '../base';
13
+ import styles from './styles.module.css';
14
+ import type { ExtendedCustomEventJSON } from '@/store/events';
15
+ import { Plus, Trash, ArrowLeft, FloppyDisk, TrashSolid } from 'iconoir-react';
16
+ import { Icon } from '@/components/primitives/icon';
17
+
18
+ type CustomEventParameterJSON = NonNullable<
19
+ ExtendedCustomEventJSON['parameters']
20
+ >[number];
21
+
22
+ interface EditEventPanelProps {
23
+ eventUiId: string | null;
24
+ onBack: () => void;
25
+ }
26
+
27
+ export const EditEventPanel: React.FC<EditEventPanelProps> = ({
28
+ eventUiId,
29
+ onBack
30
+ }) => {
31
+ const system = useSystem();
32
+
33
+ const rawEvents = useStore(system.eventsStore, (s) => s.customEvents);
34
+
35
+ const controls = useStore(system.controlStore, (s) => s.controls);
36
+ const defaultControl = useStore(system.controlStore, (s) => s.defaultControl);
37
+ const registry = useStore(system.registry);
38
+
39
+ const valueTypeOptions = useMemo(
40
+ () => Object.keys(controls).sort((a, b) => a.localeCompare(b)),
41
+ [controls]
42
+ );
43
+
44
+ const [draftEvent, setDraftEvent] = useState<ExtendedCustomEventJSON | null>(
45
+ null
46
+ );
47
+ const [isDirty, setIsDirty] = useState(true);
48
+ const isReadonly = draftEvent?.readonly === true;
49
+ // Load event when eventUiId changes
50
+ useEffect(() => {
51
+ if (!eventUiId) {
52
+ setDraftEvent(null);
53
+ setIsDirty(true);
54
+ return;
55
+ }
56
+
57
+ const event = rawEvents[eventUiId];
58
+ if (event) {
59
+ setDraftEvent({ ...event });
60
+ setIsDirty(true);
61
+ }
62
+ }, [eventUiId, rawEvents]);
63
+
64
+ const updateDraft = (patch: Partial<ExtendedCustomEventJSON>) => {
65
+ if (!draftEvent || isReadonly) return;
66
+ setDraftEvent({ ...draftEvent, ...patch });
67
+ setIsDirty(true);
68
+ };
69
+
70
+ const addParameter = () => {
71
+ if (!draftEvent || isReadonly) return;
72
+
73
+ const vt = valueTypeOptions[0] ?? 'string';
74
+ const valueType = registry.values[vt];
75
+ const defaultValue = valueType ? valueType.creator() : '';
76
+
77
+ const parameters = [
78
+ ...(draftEvent.parameters ?? []),
79
+ {
80
+ name: 'param',
81
+ valueTypeName: vt,
82
+ defaultValue
83
+ }
84
+ ];
85
+
86
+ updateDraft({ parameters });
87
+ };
88
+
89
+ const updateParameter = (
90
+ index: number,
91
+ patch: Partial<CustomEventParameterJSON>
92
+ ) => {
93
+ if (!draftEvent || isReadonly) return;
94
+
95
+ const parameters = [...(draftEvent.parameters ?? [])];
96
+ if (!parameters[index]) return;
97
+
98
+ // If valueTypeName is being changed, reset defaultValue to the new type's default
99
+ if (
100
+ patch.valueTypeName !== undefined &&
101
+ patch.valueTypeName !== parameters[index]?.valueTypeName
102
+ ) {
103
+ const valueType = registry.values[patch.valueTypeName];
104
+ if (valueType) {
105
+ patch.defaultValue = valueType.creator();
106
+ }
107
+ }
108
+
109
+ parameters[index] = {
110
+ ...parameters[index],
111
+ ...patch
112
+ } as CustomEventParameterJSON;
113
+ updateDraft({ parameters });
114
+ };
115
+
116
+ const removeParameter = (index: number) => {
117
+ if (!draftEvent || isReadonly) return;
118
+
119
+ const parameters = [...(draftEvent.parameters ?? [])];
120
+ parameters.splice(index, 1);
121
+ updateDraft({ parameters });
122
+ };
123
+
124
+ const saveEvent = () => {
125
+ if (!draftEvent || !eventUiId || isReadonly) return;
126
+ system.eventsStore.getState().addCustomEvent(draftEvent);
127
+ setIsDirty(false);
128
+ onBack();
129
+ };
130
+
131
+ const deleteEvent = () => {
132
+ if (!draftEvent || isReadonly) return;
133
+
134
+ system.eventsStore.getState().removeCustomEvent(String(draftEvent.id));
135
+ onBack();
136
+ };
137
+
138
+ if (!draftEvent) {
139
+ return (
140
+ <BasePanel>
141
+ <div className="h-full w-full flex items-center justify-center p-4">
142
+ <div className="text-gray-500 text-center">
143
+ Select an event to edit
144
+ </div>
145
+ </div>
146
+ </BasePanel>
147
+ );
148
+ }
149
+
150
+ const parameters = draftEvent.parameters ?? [];
151
+
152
+ return (
153
+ <BasePanel>
154
+ <div className={styles.editRoot}>
155
+ <div className={styles.editorHeader}>
156
+ <VscodeButton
157
+ iconOnly
158
+ secondary
159
+ onClick={onBack}
160
+ title="Back to list"
161
+ >
162
+ <ArrowLeft width={16} height={16} />
163
+ </VscodeButton>
164
+ <h2 className={styles.headerTitle}>
165
+ Edit Event
166
+ {isReadonly && <VscodeBadge>Read-only</VscodeBadge>}
167
+ </h2>
168
+ </div>
169
+
170
+ <div className={styles.editorFields}>
171
+ <div className={styles.formGrid}>
172
+ <div className={`${styles.formField}`}>
173
+ <div className={styles.formLabel}>id</div>
174
+ <VscodeTextfield
175
+ value={String(draftEvent.id)}
176
+ onInput={(e: Event) => {
177
+ const target = e.target as HTMLInputElement;
178
+ updateDraft({ id: String(target.value) });
179
+ }}
180
+ disabled={isReadonly}
181
+ />
182
+ </div>
183
+
184
+ <div className={`${styles.formField}`}>
185
+ <div className={styles.formLabel}>name</div>
186
+ <VscodeTextfield
187
+ value={String(draftEvent.name)}
188
+ onInput={(e: Event) => {
189
+ const target = e.target as HTMLInputElement;
190
+ updateDraft({ name: String(target.value) });
191
+ }}
192
+ disabled={isReadonly}
193
+ />
194
+ </div>
195
+ </div>
196
+ </div>
197
+
198
+ <div className={styles.parametersSection}>
199
+ <div className={styles.parametersHeader}>
200
+ <div className={styles.parametersTitle}>Parameters</div>
201
+ {!isReadonly && (
202
+ <Icon title="Add parameter" onClick={addParameter}>
203
+ <Plus />
204
+ </Icon>
205
+ )}
206
+ </div>
207
+ <div className={styles.parametersContent}>
208
+ {parameters.length === 0 ? (
209
+ <div className={styles.parametersEmpty}>No parameters.</div>
210
+ ) : (
211
+ <div className={styles.parametersGrid}>
212
+ {parameters.map((param, index) => {
213
+ const Control =
214
+ controls[String(param.valueTypeName)] ?? defaultControl;
215
+
216
+ return (
217
+ <div key={index} className={styles.parameterRow}>
218
+ <div className={styles.parameterField}>
219
+ <div>
220
+ <div className={styles.parameterFieldLabel}>name</div>
221
+ {!isReadonly && (
222
+ <Icon
223
+ className={styles.parameterDelete}
224
+ title="Delete parameter"
225
+ onClick={() => removeParameter(index)}
226
+ >
227
+ <TrashSolid />
228
+ </Icon>
229
+ )}
230
+ </div>
231
+ <VscodeTextfield
232
+ style={{ width: '100%' }}
233
+ value={String(param.name)}
234
+ onInput={(e: Event) => {
235
+ const target = e.target as HTMLInputElement;
236
+ updateParameter(index, {
237
+ name: String(target.value)
238
+ });
239
+ }}
240
+ disabled={isReadonly}
241
+ />
242
+ </div>
243
+
244
+ <div className={styles.parameterField}>
245
+ <div className={styles.parameterFieldLabel}>type</div>
246
+ <VscodeSingleSelect
247
+ value={String(param.valueTypeName)}
248
+ onChange={(e: Event) => {
249
+ const target = e.target as HTMLSelectElement;
250
+ updateParameter(index, {
251
+ valueTypeName: String(target.value)
252
+ });
253
+ }}
254
+ disabled={isReadonly}
255
+ >
256
+ {valueTypeOptions.map((vt) => (
257
+ <VscodeOption key={vt} value={vt}>
258
+ {vt}
259
+ </VscodeOption>
260
+ ))}
261
+ </VscodeSingleSelect>
262
+ </div>
263
+
264
+ <div className={styles.parameterField}>
265
+ <div className={styles.parameterFieldLabel}>
266
+ default
267
+ </div>
268
+ <div
269
+ className={styles.controlWrapper}
270
+ style={{
271
+ opacity: isReadonly ? 0.5 : 1,
272
+ pointerEvents: isReadonly ? 'none' : 'auto'
273
+ }}
274
+ >
275
+ <Control
276
+ value={param.defaultValue}
277
+ onChange={(v) =>
278
+ updateParameter(index, { defaultValue: v })
279
+ }
280
+ valueType={String(param.valueTypeName)}
281
+ />
282
+ </div>
283
+ </div>
284
+ </div>
285
+ );
286
+ })}
287
+ </div>
288
+ )}
289
+ </div>
290
+ </div>
291
+
292
+ <div className={styles.editorActions}>
293
+ <div className={styles.actions}>
294
+ <VscodeButton
295
+ iconOnly
296
+ onClick={saveEvent}
297
+ disabled={!isDirty || isReadonly}
298
+ >
299
+ <FloppyDisk width={16} height={16} />
300
+ Save
301
+ </VscodeButton>
302
+ <VscodeButton
303
+ iconOnly
304
+ secondary
305
+ onClick={deleteEvent}
306
+ disabled={isReadonly}
307
+ >
308
+ <Trash width={16} height={16} />
309
+ </VscodeButton>
310
+ </div>
311
+ </div>
312
+ </div>
313
+ </BasePanel>
314
+ );
315
+ };