@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.
- package/.storybook/manager.ts +6 -0
- package/.storybook/preview.ts +49 -1
- package/.storybook/styles.css +9 -3
- package/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +368 -0
- package/dist/AnyControlImpl-Ds-CShIB.js +20 -0
- package/dist/AnyControlImpl-Ds-CShIB.js.map +1 -0
- package/dist/DocumentationBrowserPanelImpl-deZNzFX8.js +166 -0
- package/dist/DocumentationBrowserPanelImpl-deZNzFX8.js.map +1 -0
- package/dist/index.css +36 -33
- package/dist/index.css.map +1 -1
- package/dist/index.d.ts +1865 -550
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14357 -11221
- package/dist/index.js.map +1 -1
- package/dist/noteImpl-KkrrWgJd.js +242 -0
- package/dist/noteImpl-KkrrWgJd.js.map +1 -0
- package/dist/styles.module-CvmpDkZj.css +3 -0
- package/dist/styles.module-CvmpDkZj.css.map +1 -0
- package/dist/styles.module-DZxg8aW9.js +271 -0
- package/dist/styles.module-DZxg8aW9.js.map +1 -0
- package/dist/useChangeNodeData-ChQGK7AI.js +23 -0
- package/dist/useChangeNodeData-ChQGK7AI.js.map +1 -0
- package/docs/protocol.md +43 -20
- package/package.json +5 -9
- package/src/components/FloatingToolbar/index.module.css +5 -13
- package/src/components/FloatingToolbar/index.tsx +9 -9
- package/src/components/Flow.tsx +34 -23
- package/src/components/contextMenus/DynamicContextMenu.tsx +85 -0
- package/src/components/contextMenus/NodePicker.module.css +13 -13
- package/src/components/contextMenus/edge.tsx +9 -95
- package/src/components/contextMenus/node.tsx +9 -149
- package/src/components/contextMenus/selection.tsx +5 -71
- package/src/components/controls/any/AnyControlImpl.tsx +14 -0
- package/src/components/controls/any/index.tsx +13 -2
- package/src/components/edges/index.tsx +75 -69
- package/src/components/layoutController/index.module.css +3 -0
- package/src/components/layoutController/index.tsx +24 -1
- package/src/components/layoutController/utils.ts +46 -3
- package/src/components/menubar/defaults.tsx +55 -19
- package/src/components/menubar/menuItem.module.css +18 -3
- package/src/components/menubar/menuItem.tsx +34 -1
- package/src/components/nodes/behave/NodeContainer.module.css +26 -25
- package/src/components/nodes/group/index.tsx +3 -3
- package/src/components/nodes/wrapper/styles.module.css +6 -32
- package/src/components/panels/alignment/index.module.css +0 -10
- package/src/components/panels/alignment/index.tsx +4 -4
- package/src/components/panels/base/styles.module.css +2 -2
- package/src/components/panels/common/PanelHeader.module.css +24 -0
- package/src/components/panels/common/PanelHeader.tsx +22 -0
- package/src/components/panels/common/SectionTitle.module.css +13 -0
- package/src/components/panels/common/SectionTitle.tsx +10 -0
- package/src/components/panels/events/EditEventPanel.tsx +14 -5
- package/src/components/panels/events/ManageEventsPanel.tsx +11 -8
- package/src/components/panels/events/styles.module.css +6 -64
- package/src/components/panels/graphProperties/index.tsx +125 -0
- package/src/components/panels/history/index.tsx +2 -2
- package/src/components/panels/history/styles.module.css +0 -9
- package/src/components/panels/keymaps/index.module.css +3 -13
- package/src/components/panels/keymaps/index.tsx +1 -2
- package/src/components/panels/layers/index.tsx +20 -15
- package/src/components/panels/layers/styles.module.css +9 -12
- package/src/components/panels/legend/index.tsx +1 -1
- package/src/components/panels/logs/index.module.css +25 -19
- package/src/components/panels/logs/index.tsx +7 -7
- package/src/components/panels/nodeInputs/InputsGroup.tsx +1 -0
- package/src/components/panels/nodeInputs/NodeSettings.tsx +2 -2
- package/src/components/panels/nodeInputs/NodeTitleEditor.tsx +1 -1
- package/src/components/panels/nodeInputs/OutputsGroup.tsx +2 -12
- package/src/components/panels/nodeInputs/index.module.css +99 -75
- package/src/components/panels/nodeInputs/index.tsx +21 -11
- package/src/components/panels/nodeInputs/useNodeHandlers.ts +2 -2
- package/src/components/panels/nodeInputs/useNodeInputsData.ts +23 -43
- package/src/components/panels/nodePicker/index.tsx +8 -8
- package/src/components/panels/panel/index.module.css +7 -7
- package/src/components/panels/search/index.module.css +0 -50
- package/src/components/panels/search/index.tsx +2 -2
- package/src/components/panels/systemSettings/ConversionsSettings.tsx +203 -0
- package/src/components/panels/systemSettings/index.tsx +221 -176
- package/src/components/panels/systemSettings/styles.module.css +135 -8
- package/src/components/panels/traces/GridLines.tsx +1 -1
- package/src/components/panels/traces/TimeGrid.tsx +3 -3
- package/src/components/panels/traces/TraceLane.tsx +1 -1
- package/src/components/panels/traces/index.module.css +1 -8
- package/src/components/panels/traces/index.tsx +8 -4
- package/src/components/panels/traces/useDerivedSpans.ts +241 -146
- package/src/components/panels/traces/utils.ts +8 -0
- package/src/components/panels/variables/CreateVariableScreen.tsx +3 -3
- package/src/components/panels/variables/ManageVariablesScreen.tsx +12 -9
- package/src/components/panels/variables/index.tsx +2 -2
- package/src/components/panels/variables/styles.module.css +4 -91
- package/src/components/primitives/icon.module.css +4 -4
- package/src/components/sockets/input/index.tsx +9 -2
- package/src/components/sockets/input/styles.module.css +2 -3
- package/src/components/sockets/output/index.tsx +10 -3
- package/src/components/sockets/output/styles.module.css +1 -6
- package/src/css/notes.css +135 -0
- package/src/css/prosemirror.css +3 -3
- package/src/css/rc-dock.css +143 -43
- package/src/css/rc-menu.css +56 -55
- package/src/css/themes/kiberon.css +127 -0
- package/src/css/vars.css +197 -13
- package/src/css/vscode-elements.css +124 -0
- package/src/generators/CallSubgraphGenerator.tsx +136 -0
- package/src/generators/CustomEventOnTriggeredGenerator.tsx +2 -2
- package/src/generators/GraphBoundaryGenerator.module.css +32 -0
- package/src/generators/GraphBoundaryGenerator.tsx +193 -0
- package/src/generators/SequenceGenerator.tsx +2 -2
- package/src/generators/SwitchOnIntegerGenerator.tsx +2 -2
- package/src/generators/SwitchOnStringGenerator.tsx +2 -2
- package/src/generators/callSubgraphSync.ts +126 -0
- package/src/generators/registerDefaultGenerators.ts +21 -0
- package/src/generators/registerDefaults.ts +26 -0
- package/src/hooks/useBehaveGraphFlow.ts +2 -2
- package/src/hooks/useFlowHandlers.ts +47 -9
- package/src/hooks/useWasdPan.ts +26 -4
- package/src/index.css +4 -16
- package/src/index.ts +17 -0
- package/src/manifest/contributionRegistry.ts +93 -0
- package/src/manifest/index.ts +4 -0
- package/src/manifest/loadManifest.ts +82 -0
- package/src/manifest/manifestPlugin.ts +29 -0
- package/src/manifest/passthroughValueType.ts +40 -0
- package/src/plugin/alignment/index.ts +22 -12
- package/src/plugin/autosave/controller.ts +366 -0
- package/src/plugin/autosave/index.tsx +114 -0
- package/src/plugin/autosave/panel/BackupPanel.tsx +141 -0
- package/src/plugin/autosave/panel/index.tsx +1 -0
- package/src/plugin/autosave/panel/styles.module.css +56 -0
- package/src/plugin/autosave/settings.ts +65 -0
- package/src/plugin/autosave/storage.ts +147 -0
- package/src/plugin/docs/index.tsx +2 -4
- package/src/plugin/docs/panel/DocumentationBrowserPanelImpl.tsx +200 -0
- package/src/plugin/docs/panel/index.tsx +15 -194
- package/src/plugin/docs/panel/styles.module.css +8 -8
- package/src/plugin/graphrunner/actions.ts +258 -185
- package/src/plugin/graphrunner/buttons.tsx +34 -26
- package/src/plugin/graphrunner/client.ts +4 -1
- package/src/plugin/graphrunner/index.tsx +29 -100
- package/src/plugin/graphrunner/panel.tsx +2 -2
- package/src/plugin/graphrunner/runController.ts +283 -0
- package/src/plugin/graphrunner/runner.ts +21 -192
- package/src/plugin/graphrunner/store.ts +14 -24
- package/src/plugin/graphrunner/styles.module.css +17 -57
- package/src/plugin/graphrunner/transport.ts +26 -0
- package/src/plugin/graphrunner/types.ts +21 -0
- package/src/plugin/graphrunner-local/execution-utils.ts +260 -80
- package/src/plugin/graphrunner-local/index.tsx +8 -2
- package/src/plugin/graphrunner-local/panel.tsx +131 -175
- package/src/plugin/graphrunner-local/styles.module.css +57 -76
- package/src/plugin/graphrunner-local/transport.ts +151 -184
- package/src/plugin/graphrunner-webworker/graph-executor.worker.ts +2 -0
- package/src/plugin/graphrunner-webworker/index.tsx +4 -10
- package/src/plugin/graphrunner-webworker/store.ts +9 -0
- package/src/plugin/kitchen-sink/index.ts +38 -0
- package/src/{layout/dagre.tsx → plugin/layout/dagre.ts} +17 -5
- package/src/{layout → plugin/layout}/elk.ts +22 -6
- package/src/plugin/layout/index.ts +80 -0
- package/src/plugin/notes/FormatToolbar.tsx +200 -0
- package/src/plugin/notes/index.tsx +191 -0
- package/src/plugin/notes/nodeActions.ts +100 -0
- package/src/plugin/notes/note.tsx +20 -0
- package/src/plugin/notes/noteImpl.tsx +89 -0
- package/src/plugin/realtime/realtimeRunner.ts +58 -4
- package/src/specifics/CustomEventOnTriggeredSpecific.tsx +2 -2
- package/src/specifics/CustomEventTriggerSpecific.tsx +2 -2
- package/src/specifics/VariableGetSpecific.tsx +2 -2
- package/src/specifics/VariableSetSpecific.tsx +2 -2
- package/src/store/actions.tsx +5 -5
- package/src/store/commands.ts +278 -0
- package/src/store/contextMenu.ts +192 -0
- package/src/store/conversions.ts +47 -0
- package/src/store/flow.tsx +23 -38
- package/src/store/graphMeta.ts +39 -0
- package/src/store/hotKeys.tsx +301 -260
- package/src/store/layers.ts +3 -3
- package/src/store/registry.ts +12 -4
- package/src/store/selection.ts +3 -3
- package/src/store/settings.ts +82 -82
- package/src/store/settingsSchema.ts +210 -0
- package/src/store/tabs.ts +5 -1
- package/src/store/traces.ts +3 -3
- package/src/system/graph.ts +11 -14
- package/src/system/graphSession.ts +172 -0
- package/src/system/index.ts +3 -0
- package/src/system/notifications.ts +13 -0
- package/src/system/persistence.ts +82 -0
- package/src/system/plugin.ts +28 -0
- package/src/system/provider.tsx +64 -0
- package/src/system/system.ts +518 -88
- package/src/system/tabLoader.tsx +70 -32
- package/src/system/undoRedo.ts +1 -1
- package/src/transformers/Uigraph.ts +5 -4
- package/src/transformers/contract.ts +87 -0
- package/src/transformers/flowToBehave.ts +13 -5
- package/src/types/nodes.ts +8 -3
- package/src/types.ts +2 -0
- package/src/util/autoConvert.ts +200 -0
- package/src/util/isValidConnection.ts +23 -2
- package/stories/defaults/defaultStoryProvider.tsx +17 -14
- package/stories/defaults/systemGenerator.ts +6 -1
- package/stories/{components/nodes/comment.stories.tsx → plugins/notes.stories.tsx} +24 -30
- package/tests/autoConvert.test.ts +329 -0
- package/tests/autosavePlugin.test.ts +204 -0
- package/tests/callSubgraphSync.test.ts +148 -0
- package/tests/commandRegistry.test.ts +137 -0
- package/tests/contract.test.ts +51 -0
- package/tests/contractSerialize.test.ts +62 -0
- package/tests/deriveSpans.test.ts +71 -0
- package/tests/flowToBehave.test.ts +2 -1
- package/tests/hotkeys.test.ts +79 -0
- package/tests/keepAliveLifecycle.test.ts +167 -0
- package/tests/loadManifest.test.ts +113 -0
- package/tests/noteMarkdown.test.ts +65 -0
- package/tests/notesPlugin.test.ts +162 -0
- package/tests/persistence.test.ts +51 -0
- package/tests/saveLoad.test.ts +7 -6
- package/tests/settings.test.ts +178 -0
- package/tests/traceStore.test.ts +46 -0
- package/tests/visual/README.md +2 -2
- package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-conversation-chromium-win32.png +0 -0
- package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-events-chromium-win32.png +0 -0
- package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-history-chromium-win32.png +0 -0
- package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-keymaps-chromium-win32.png +0 -0
- package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-layers-chromium-win32.png +0 -0
- package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-legend-chromium-win32.png +0 -0
- package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-localGraphRunner-chromium-win32.png +0 -0
- package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-logs-chromium-win32.png +0 -0
- package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-nodeInputs-chromium-win32.png +0 -0
- package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-nodePicker-chromium-win32.png +0 -0
- package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-panel-chromium-win32.png +0 -0
- package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-search-chromium-win32.png +0 -0
- package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-systemSettings-chromium-win32.png +0 -0
- package/tests/visual/__screenshots__/panels.visual.test.tsx/panel-variables-chromium-win32.png +0 -0
- package/tests/visual/panels.visual.test.tsx +3 -3
- package/tests/wasdPan.test.ts +71 -0
- package/vitest.config.ts +1 -1
- package/vitest.visual.config.ts +7 -0
- package/.storybook/vscode.css +0 -814
- package/src/components/nodes/comment/FormatToolbar.tsx +0 -118
- package/src/components/nodes/comment/comment.tsx +0 -103
- package/src/components/nodes/comment/styles.module.css +0 -150
- package/src/components/panels/conversation/index.module.css +0 -151
- package/src/components/panels/conversation/index.tsx +0 -162
- package/src/components/panels/events/CustomEventsEditor.tsx +0 -384
- package/src/css/vscode.css +0 -13
- package/src/hooks/useDetachNodes.ts +0 -39
- package/src/plugin/graphrunner-webworker/types.ts +0 -17
- package/src/specifics/registerDefaultSpecifics.ts +0 -5
- package/src/store/chat.ts +0 -73
- package/src/store/graphRunnerClient.ts +0 -110
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
Engine,
|
|
4
|
+
ManualLifecycleEventEmitter,
|
|
5
|
+
makeEventNodeDefinition,
|
|
6
|
+
makeFlowNodeDefinition,
|
|
7
|
+
readGraphFromJSON,
|
|
8
|
+
registerCoreProfile,
|
|
9
|
+
NodeCategory,
|
|
10
|
+
type GraphJSON
|
|
11
|
+
} from '@kiberon-labs/behave-graph';
|
|
12
|
+
import {
|
|
13
|
+
executeGraphLifecycle,
|
|
14
|
+
type ActiveRun,
|
|
15
|
+
type MessageContext
|
|
16
|
+
} from '../src/plugin/graphrunner-local/execution-utils.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A minimal external event source standing in for the AI conversation runtime:
|
|
20
|
+
* the event node subscribes on init, and `fire()` commits a flow the way
|
|
21
|
+
* ai/onToolCall does when the model requests a tool.
|
|
22
|
+
*/
|
|
23
|
+
const makeExternalSource = () => {
|
|
24
|
+
const listeners = new Set<() => void>();
|
|
25
|
+
return {
|
|
26
|
+
subscribe(listener: () => void) {
|
|
27
|
+
listeners.add(listener);
|
|
28
|
+
return () => listeners.delete(listener);
|
|
29
|
+
},
|
|
30
|
+
fire() {
|
|
31
|
+
listeners.forEach((listener) => listener());
|
|
32
|
+
},
|
|
33
|
+
get listenerCount() {
|
|
34
|
+
return listeners.size;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const buildRun = (handled: string[]) => {
|
|
40
|
+
const source = makeExternalSource();
|
|
41
|
+
|
|
42
|
+
const onExternal = makeEventNodeDefinition({
|
|
43
|
+
typeName: 'test/onExternal',
|
|
44
|
+
category: NodeCategory.Event,
|
|
45
|
+
in: {},
|
|
46
|
+
out: { flow: 'flow' },
|
|
47
|
+
initialState: { unsubscribe: undefined as undefined | (() => void) },
|
|
48
|
+
init: ({ commit }) => ({
|
|
49
|
+
unsubscribe: source.subscribe(() => commit('flow'))
|
|
50
|
+
}),
|
|
51
|
+
dispose: ({ state }) => {
|
|
52
|
+
state.unsubscribe?.();
|
|
53
|
+
return { unsubscribe: undefined };
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const handle = makeFlowNodeDefinition({
|
|
58
|
+
typeName: 'test/handle',
|
|
59
|
+
category: NodeCategory.Action,
|
|
60
|
+
in: { flow: 'flow' },
|
|
61
|
+
out: {},
|
|
62
|
+
initialState: undefined,
|
|
63
|
+
triggered: () => {
|
|
64
|
+
handled.push('handled');
|
|
65
|
+
}
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
const registry = registerCoreProfile({
|
|
69
|
+
nodes: {},
|
|
70
|
+
values: {},
|
|
71
|
+
dependencies: {
|
|
72
|
+
ILifecycleEventEmitter: new ManualLifecycleEventEmitter()
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
registry.nodes['test/onExternal'] = onExternal;
|
|
76
|
+
registry.nodes['test/handle'] = handle;
|
|
77
|
+
|
|
78
|
+
const graphJson: GraphJSON = {
|
|
79
|
+
name: 'keep-alive test',
|
|
80
|
+
nodes: [
|
|
81
|
+
{
|
|
82
|
+
id: 'listener',
|
|
83
|
+
type: 'test/onExternal',
|
|
84
|
+
flows: { flow: { nodeId: 'handler', socket: 'flow' } }
|
|
85
|
+
},
|
|
86
|
+
{ id: 'handler', type: 'test/handle' }
|
|
87
|
+
],
|
|
88
|
+
variables: [],
|
|
89
|
+
customEvents: []
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const graphInstance = readGraphFromJSON({ graphJson, registry });
|
|
93
|
+
const engine = new Engine(graphInstance, registry);
|
|
94
|
+
|
|
95
|
+
const run: ActiveRun = {
|
|
96
|
+
runId: 'run-1',
|
|
97
|
+
graphId: 'graph-1',
|
|
98
|
+
engine,
|
|
99
|
+
graphInstance,
|
|
100
|
+
registry,
|
|
101
|
+
status: 'running',
|
|
102
|
+
startedAt: Date.now(),
|
|
103
|
+
performance: { nodesExecuted: 0, eventsEmitted: 0, variableChanges: 0 },
|
|
104
|
+
isPaused: false,
|
|
105
|
+
executionPhase: 'start',
|
|
106
|
+
currentTick: 0
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
return { run, source };
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const collectMessages = () => {
|
|
113
|
+
const messages: Array<{ type: string }> = [];
|
|
114
|
+
const ctx: MessageContext = {
|
|
115
|
+
sendMessage: (message) => messages.push(message),
|
|
116
|
+
sendError: () => {}
|
|
117
|
+
};
|
|
118
|
+
return { messages, ctx };
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
describe('graph run lifecycle', () => {
|
|
122
|
+
it('autoEnd: true finalizes when flows drain and unsubscribes event nodes', async () => {
|
|
123
|
+
const handled: string[] = [];
|
|
124
|
+
const { run, source } = buildRun(handled);
|
|
125
|
+
const { messages, ctx } = collectMessages();
|
|
126
|
+
|
|
127
|
+
// The engine constructor fires event-node init without awaiting it; let
|
|
128
|
+
// its state (the unsubscribe handle) settle before the lifecycle disposes.
|
|
129
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
130
|
+
|
|
131
|
+
await executeGraphLifecycle(run, 'graph-1', ctx, { autoEnd: true });
|
|
132
|
+
|
|
133
|
+
expect(run.status).toBe('completed');
|
|
134
|
+
expect(messages.some((m) => m.type === 'completed')).toBe(true);
|
|
135
|
+
// Dispose tore the subscription down; a late event finds no listener.
|
|
136
|
+
expect(source.listenerCount).toBe(0);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('stays alive by default and services events fired after the flows drain', async () => {
|
|
140
|
+
const handled: string[] = [];
|
|
141
|
+
const { run, source } = buildRun(handled);
|
|
142
|
+
const { messages, ctx } = collectMessages();
|
|
143
|
+
|
|
144
|
+
const lifecycle = executeGraphLifecycle(run, 'graph-1', ctx, {
|
|
145
|
+
tickInterval: 1
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Give the lifecycle time to drain the (empty) start flow. With autoEnd
|
|
149
|
+
// it would have completed here.
|
|
150
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
151
|
+
expect(run.status).toBe('running');
|
|
152
|
+
expect(source.listenerCount).toBe(1);
|
|
153
|
+
|
|
154
|
+
// The out-of-band event (the "tool call") arrives after the start flow
|
|
155
|
+
// ended; the idle loop must still drain the fiber it commits.
|
|
156
|
+
source.fire();
|
|
157
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
158
|
+
expect(handled).toEqual(['handled']);
|
|
159
|
+
|
|
160
|
+
// Stopping ends the idle loop; no `completed` message is emitted.
|
|
161
|
+
run.status = 'stopped';
|
|
162
|
+
await lifecycle;
|
|
163
|
+
expect(messages.some((m) => m.type === 'completed')).toBe(false);
|
|
164
|
+
|
|
165
|
+
run.engine.dispose();
|
|
166
|
+
});
|
|
167
|
+
});
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import type {
|
|
3
|
+
ManifestJSON,
|
|
4
|
+
PackageRequirement
|
|
5
|
+
} from '@kiberon-labs/behave-graph';
|
|
6
|
+
import { System } from '../src/system/system.js';
|
|
7
|
+
import { loadManifest } from '../src/manifest/loadManifest.js';
|
|
8
|
+
|
|
9
|
+
const Vec2Control = () => null;
|
|
10
|
+
const conversionRule = {
|
|
11
|
+
from: 'vec2',
|
|
12
|
+
to: 'object',
|
|
13
|
+
nodeType: 'convert/vec2ToObject'
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
const manifest: ManifestJSON = {
|
|
17
|
+
manifestVersion: 1,
|
|
18
|
+
package: { name: '@test/pkg', version: '1.0.0' },
|
|
19
|
+
values: [{ name: 'vec2', defaultJSON: { x: 0, y: 0 } }],
|
|
20
|
+
nodes: [
|
|
21
|
+
{
|
|
22
|
+
type: 'test/node',
|
|
23
|
+
category: 'Logic',
|
|
24
|
+
label: 'Test Node',
|
|
25
|
+
inputs: [],
|
|
26
|
+
outputs: [],
|
|
27
|
+
configuration: []
|
|
28
|
+
}
|
|
29
|
+
],
|
|
30
|
+
contributions: [
|
|
31
|
+
{
|
|
32
|
+
id: 'vec2-ctrl',
|
|
33
|
+
kind: 'control',
|
|
34
|
+
export: './ui.js#Vec2Control',
|
|
35
|
+
bind: { controlName: 'vec2' }
|
|
36
|
+
},
|
|
37
|
+
{ id: 'vec2-conv', kind: 'conversion', export: './ui.js#rule' }
|
|
38
|
+
],
|
|
39
|
+
requirements: [
|
|
40
|
+
{ kind: 'backendService', entry: './server.js', persistent: true }
|
|
41
|
+
]
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const resolve = (c: { id: string }) => {
|
|
45
|
+
if (c.id === 'vec2-ctrl') return Vec2Control;
|
|
46
|
+
if (c.id === 'vec2-conv') return conversionRule;
|
|
47
|
+
return undefined;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
describe('loadManifest', () => {
|
|
51
|
+
it('loads nodes + pass-through value types without trust (no code exec)', async () => {
|
|
52
|
+
const system = new System();
|
|
53
|
+
await loadManifest(system, manifest);
|
|
54
|
+
|
|
55
|
+
const reg = system.registry.getState();
|
|
56
|
+
expect(reg.specs.some((s) => s.type === 'test/node')).toBe(true);
|
|
57
|
+
|
|
58
|
+
const vec2 = reg.values['vec2'];
|
|
59
|
+
expect(vec2).toBeDefined();
|
|
60
|
+
// Pass-through creator returns a *clone* of the declared default.
|
|
61
|
+
const created = vec2.creator();
|
|
62
|
+
expect(created).toEqual({ x: 0, y: 0 });
|
|
63
|
+
expect(created).not.toBe(manifest.values[0].defaultJSON);
|
|
64
|
+
// Identity (de)serialize keeps existing UI call sites working.
|
|
65
|
+
expect(vec2.serialize?.(created)).toEqual({ x: 0, y: 0 });
|
|
66
|
+
|
|
67
|
+
// No contributions applied without trust.
|
|
68
|
+
expect(system.controlStore.getState().controls['vec2']).toBeUndefined();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('surfaces host requirements via onRequirement', async () => {
|
|
72
|
+
const system = new System();
|
|
73
|
+
const seen: PackageRequirement[] = [];
|
|
74
|
+
await loadManifest(system, manifest, {
|
|
75
|
+
onRequirement: (req) => seen.push(req)
|
|
76
|
+
});
|
|
77
|
+
expect(seen).toHaveLength(1);
|
|
78
|
+
expect(seen[0].kind).toBe('backendService');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('applies code contributions only under trust + resolve', async () => {
|
|
82
|
+
const system = new System();
|
|
83
|
+
await loadManifest(system, manifest, { trust: true, resolve });
|
|
84
|
+
|
|
85
|
+
expect(system.controlStore.getState().controls['vec2']).toBe(Vec2Control);
|
|
86
|
+
expect(
|
|
87
|
+
system.conversionStore.getState().findConversion('vec2', 'object')
|
|
88
|
+
).toEqual(conversionRule);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('skips contributions when trusted but no resolver is given', async () => {
|
|
92
|
+
const system = new System();
|
|
93
|
+
await loadManifest(system, manifest, { trust: true });
|
|
94
|
+
expect(system.controlStore.getState().controls['vec2']).toBeUndefined();
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
it('does not let one failing contribution abort the rest', async () => {
|
|
98
|
+
const system = new System();
|
|
99
|
+
const spy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
100
|
+
await loadManifest(system, manifest, {
|
|
101
|
+
trust: true,
|
|
102
|
+
resolve: (c) => {
|
|
103
|
+
if (c.id === 'vec2-ctrl') throw new Error('boom');
|
|
104
|
+
return resolve(c);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
// The conversion still registered despite the control throwing.
|
|
108
|
+
expect(
|
|
109
|
+
system.conversionStore.getState().findConversion('vec2', 'object')
|
|
110
|
+
).toEqual(conversionRule);
|
|
111
|
+
spy.mockRestore();
|
|
112
|
+
});
|
|
113
|
+
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// @vitest-environment happy-dom
|
|
2
|
+
// @vitest-environment-options { "settings": { "disableIframePageLoading": true } }
|
|
3
|
+
// (embedded youtube iframes must not trigger real network requests in tests)
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect } from 'vitest';
|
|
6
|
+
import { Editor } from '@tiptap/react';
|
|
7
|
+
import StarterKit from '@tiptap/starter-kit';
|
|
8
|
+
import Youtube from '@tiptap/extension-youtube';
|
|
9
|
+
import { Markdown } from 'tiptap-markdown';
|
|
10
|
+
|
|
11
|
+
const makeEditor = (content: string) =>
|
|
12
|
+
new Editor({
|
|
13
|
+
element: document.createElement('div'),
|
|
14
|
+
extensions: [StarterKit, Markdown, Youtube.configure({ nocookie: true })],
|
|
15
|
+
content
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const getMarkdown = (editor: Editor): string =>
|
|
19
|
+
(
|
|
20
|
+
editor.storage as unknown as { markdown: { getMarkdown(): string } }
|
|
21
|
+
).markdown.getMarkdown();
|
|
22
|
+
|
|
23
|
+
describe('note markdown serialization', () => {
|
|
24
|
+
it('round-trips formatted text', () => {
|
|
25
|
+
const editor = makeEditor('# Title\n\nSome **bold** and `code`.');
|
|
26
|
+
const md = getMarkdown(editor);
|
|
27
|
+
expect(md).toContain('# Title');
|
|
28
|
+
expect(md).toContain('**bold**');
|
|
29
|
+
expect(md).toContain('`code`');
|
|
30
|
+
editor.destroy();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it('embeds a youtube video and round-trips it through markdown', () => {
|
|
34
|
+
const editor = makeEditor('');
|
|
35
|
+
const inserted = editor
|
|
36
|
+
.chain()
|
|
37
|
+
.setYoutubeVideo({ src: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ' })
|
|
38
|
+
.run();
|
|
39
|
+
expect(inserted).toBe(true);
|
|
40
|
+
|
|
41
|
+
// The video serializes as an HTML block inside the markdown text.
|
|
42
|
+
const md = getMarkdown(editor);
|
|
43
|
+
expect(md).toContain('data-youtube-video');
|
|
44
|
+
expect(md).toContain('dQw4w9WgXcQ');
|
|
45
|
+
editor.destroy();
|
|
46
|
+
|
|
47
|
+
// Loading that markdown back (what NoteNodeImpl does with data.text)
|
|
48
|
+
// restores the embed.
|
|
49
|
+
const reloaded = makeEditor(md);
|
|
50
|
+
expect(reloaded.getHTML()).toContain('data-youtube-video');
|
|
51
|
+
expect(reloaded.getHTML()).toContain('dQw4w9WgXcQ');
|
|
52
|
+
reloaded.destroy();
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('rejects non-youtube urls', () => {
|
|
56
|
+
const editor = makeEditor('');
|
|
57
|
+
const inserted = editor
|
|
58
|
+
.chain()
|
|
59
|
+
.setYoutubeVideo({ src: 'https://example.com/video.mp4' })
|
|
60
|
+
.run();
|
|
61
|
+
expect(inserted).toBe(false);
|
|
62
|
+
expect(getMarkdown(editor)).not.toContain('example.com');
|
|
63
|
+
editor.destroy();
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { System } from '../src/system/system.js';
|
|
3
|
+
import type { GraphSession } from '../src/system/graphSession.js';
|
|
4
|
+
import {
|
|
5
|
+
notesPlugin,
|
|
6
|
+
NOTE_NODE_TYPE,
|
|
7
|
+
LEGACY_COMMENT_NODE_TYPE
|
|
8
|
+
} from '../src/plugin/notes/index.js';
|
|
9
|
+
|
|
10
|
+
describe('notes plugin', () => {
|
|
11
|
+
let system: System;
|
|
12
|
+
let session: GraphSession;
|
|
13
|
+
|
|
14
|
+
const addNote = (position = { x: 10, y: 20 }) =>
|
|
15
|
+
system.commandStore.getState().run('notes.addNote', {
|
|
16
|
+
editor: system,
|
|
17
|
+
session,
|
|
18
|
+
position
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const nodes = () => session.nodeStore.getState().nodes;
|
|
22
|
+
|
|
23
|
+
beforeEach(async () => {
|
|
24
|
+
system = new System();
|
|
25
|
+
await system.registerPlugin(notesPlugin);
|
|
26
|
+
session = system.createSession('graph');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('does not register note node types without the plugin', () => {
|
|
30
|
+
const bare = new System();
|
|
31
|
+
const bareSession = bare.createSession('graph');
|
|
32
|
+
expect(bareSession.flowStore.getState().nodeTypes[NOTE_NODE_TYPE]).toBe(
|
|
33
|
+
undefined
|
|
34
|
+
);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('registers the note component (and legacy alias) on new sessions', () => {
|
|
38
|
+
const { nodeTypes } = session.flowStore.getState();
|
|
39
|
+
expect(nodeTypes[NOTE_NODE_TYPE]).toBeDefined();
|
|
40
|
+
expect(nodeTypes[LEGACY_COMMENT_NODE_TYPE]).toBe(nodeTypes[NOTE_NODE_TYPE]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('adds a toolbar group with the Add Note button', () => {
|
|
44
|
+
const group = system.toolbarStore
|
|
45
|
+
.getState()
|
|
46
|
+
.groups.find((g) => g.id === 'notes');
|
|
47
|
+
expect(group).toBeDefined();
|
|
48
|
+
expect(group?.buttons).toHaveLength(1);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('notes.addNote creates a selected, header-draggable note (undoable)', () => {
|
|
52
|
+
void addNote();
|
|
53
|
+
|
|
54
|
+
expect(nodes()).toHaveLength(1);
|
|
55
|
+
const note = nodes()[0];
|
|
56
|
+
expect(note?.type).toBe(NOTE_NODE_TYPE);
|
|
57
|
+
expect(note?.position).toEqual({ x: 10, y: 20 });
|
|
58
|
+
expect(note?.selected).toBe(true);
|
|
59
|
+
expect(note?.dragHandle).toBe('.notes-node__header');
|
|
60
|
+
expect(note?.data).toEqual({ text: '' });
|
|
61
|
+
|
|
62
|
+
session.undoManager.undo();
|
|
63
|
+
expect(nodes()).toHaveLength(0);
|
|
64
|
+
session.undoManager.redo();
|
|
65
|
+
expect(nodes()).toHaveLength(1);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('note nodes are ignored by the behave graph transform', () => {
|
|
69
|
+
void addNote();
|
|
70
|
+
const graph = session.flowStore.getState().getGraph();
|
|
71
|
+
expect(graph.nodes ?? []).toHaveLength(0);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('note.duplicate clones the note with an offset', () => {
|
|
75
|
+
void addNote();
|
|
76
|
+
const original = nodes()[0]!;
|
|
77
|
+
|
|
78
|
+
void system.commandStore.getState().run('note.duplicate', {
|
|
79
|
+
editor: system,
|
|
80
|
+
session,
|
|
81
|
+
nodeId: original.id
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
expect(nodes()).toHaveLength(2);
|
|
85
|
+
const copy = nodes()[1]!;
|
|
86
|
+
expect(copy.id).not.toBe(original.id);
|
|
87
|
+
expect(copy.position).toEqual({
|
|
88
|
+
x: original.position.x + 24,
|
|
89
|
+
y: original.position.y + 24
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
session.undoManager.undo();
|
|
93
|
+
expect(nodes()).toHaveLength(1);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('note.delete removes the note and undo restores it', () => {
|
|
97
|
+
void addNote();
|
|
98
|
+
const note = nodes()[0]!;
|
|
99
|
+
|
|
100
|
+
void system.commandStore.getState().run('note.delete', {
|
|
101
|
+
editor: system,
|
|
102
|
+
session,
|
|
103
|
+
nodeId: note.id
|
|
104
|
+
});
|
|
105
|
+
expect(nodes()).toHaveLength(0);
|
|
106
|
+
|
|
107
|
+
session.undoManager.undo();
|
|
108
|
+
expect(nodes()).toHaveLength(1);
|
|
109
|
+
expect(nodes()[0]?.id).toBe(note.id);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('bring to front / send to back reorder within the nodes array', () => {
|
|
113
|
+
void addNote({ x: 0, y: 0 });
|
|
114
|
+
void addNote({ x: 5, y: 5 });
|
|
115
|
+
const [first, second] = nodes();
|
|
116
|
+
|
|
117
|
+
void system.commandStore.getState().run('note.bringToFront', {
|
|
118
|
+
editor: system,
|
|
119
|
+
session,
|
|
120
|
+
nodeId: first!.id
|
|
121
|
+
});
|
|
122
|
+
expect(nodes().map((n) => n.id)).toEqual([second!.id, first!.id]);
|
|
123
|
+
|
|
124
|
+
void system.commandStore.getState().run('note.sendToBack', {
|
|
125
|
+
editor: system,
|
|
126
|
+
session,
|
|
127
|
+
nodeId: first!.id
|
|
128
|
+
});
|
|
129
|
+
expect(nodes().map((n) => n.id)).toEqual([first!.id, second!.id]);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
it('behave-only context menu items are hidden on notes, note items shown', () => {
|
|
133
|
+
void addNote();
|
|
134
|
+
const note = nodes()[0]!;
|
|
135
|
+
const ctx = { editor: system, session, nodeId: note.id };
|
|
136
|
+
|
|
137
|
+
const visible = system.contextMenuStore
|
|
138
|
+
.getState()
|
|
139
|
+
.getItems('node')
|
|
140
|
+
.filter((i) => !i.when || i.when(ctx))
|
|
141
|
+
.map((i) => i.id);
|
|
142
|
+
|
|
143
|
+
expect(visible).toContain('note.duplicate');
|
|
144
|
+
expect(visible).toContain('note.delete');
|
|
145
|
+
expect(visible).not.toContain('node.traceUpstream');
|
|
146
|
+
expect(visible).not.toContain('node.togglePinned');
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('note commands no-op for behave nodes', () => {
|
|
150
|
+
session.actionStore
|
|
151
|
+
.getState()
|
|
152
|
+
.actions.addBehaveNode('debug/log', { x: 0, y: 0 });
|
|
153
|
+
const behave = nodes()[0]!;
|
|
154
|
+
|
|
155
|
+
void system.commandStore.getState().run('note.delete', {
|
|
156
|
+
editor: system,
|
|
157
|
+
session,
|
|
158
|
+
nodeId: behave.id
|
|
159
|
+
});
|
|
160
|
+
expect(nodes()).toHaveLength(1);
|
|
161
|
+
});
|
|
162
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { System } from '../src/system/system.js';
|
|
3
|
+
import type { UIGraphJSON } from '../src/types/graph.js';
|
|
4
|
+
|
|
5
|
+
const fakeUIGraph = (): UIGraphJSON =>
|
|
6
|
+
({ flow: { nodes: [] } }) as unknown as UIGraphJSON;
|
|
7
|
+
|
|
8
|
+
describe('default graph/layout persistence', () => {
|
|
9
|
+
let system: System;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
system = new System();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('subscribes save handlers by default (no per-host wiring)', () => {
|
|
16
|
+
// The three save topics each have exactly one default subscriber.
|
|
17
|
+
expect(system.pubsub.countSubscriptions('graph:saved')).toBe(1);
|
|
18
|
+
expect(system.pubsub.countSubscriptions('graph:inner:saved')).toBe(1);
|
|
19
|
+
expect(system.pubsub.countSubscriptions('layout:saved')).toBe(1);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('routes a custom adapter and keeps defaults for omitted topics', () => {
|
|
23
|
+
const saveGraph = vi.fn();
|
|
24
|
+
system.enablePersistence({ saveGraph });
|
|
25
|
+
|
|
26
|
+
// Replacing does not stack subscriptions.
|
|
27
|
+
expect(system.pubsub.countSubscriptions('graph:saved')).toBe(1);
|
|
28
|
+
|
|
29
|
+
const graph = fakeUIGraph();
|
|
30
|
+
system.pubsub.publishSync('graph:saved', graph);
|
|
31
|
+
expect(saveGraph).toHaveBeenCalledWith(graph);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('disablePersistence removes all save handlers', () => {
|
|
35
|
+
system.disablePersistence();
|
|
36
|
+
expect(system.pubsub.countSubscriptions('graph:saved')).toBe(0);
|
|
37
|
+
expect(system.pubsub.countSubscriptions('graph:inner:saved')).toBe(0);
|
|
38
|
+
expect(system.pubsub.countSubscriptions('layout:saved')).toBe(0);
|
|
39
|
+
|
|
40
|
+
// Publishing after disabling is a no-op (no subscriber throws).
|
|
41
|
+
expect(() =>
|
|
42
|
+
system.pubsub.publishSync('graph:saved', fakeUIGraph())
|
|
43
|
+
).not.toThrow();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('does not throw when the default (download) sink runs without a DOM', () => {
|
|
47
|
+
const graph = fakeUIGraph();
|
|
48
|
+
// jsdom provides document/URL, but the guard makes this safe either way.
|
|
49
|
+
expect(() => system.pubsub.publishSync('graph:saved', graph)).not.toThrow();
|
|
50
|
+
});
|
|
51
|
+
});
|
package/tests/saveLoad.test.ts
CHANGED
|
@@ -24,6 +24,7 @@ describe('Save and Load Graph', () => {
|
|
|
24
24
|
};
|
|
25
25
|
|
|
26
26
|
system = new System(registry);
|
|
27
|
+
system.createSession('graph');
|
|
27
28
|
});
|
|
28
29
|
|
|
29
30
|
it('should save and load a graph with node positions', () => {
|
|
@@ -67,7 +68,7 @@ describe('Save and Load Graph', () => {
|
|
|
67
68
|
system.edgeStore.getState().setEdges(initialEdges);
|
|
68
69
|
|
|
69
70
|
// Save the graph
|
|
70
|
-
const savedGraph = buildUIGraphJSON(system);
|
|
71
|
+
const savedGraph = buildUIGraphJSON(system.session!);
|
|
71
72
|
|
|
72
73
|
// Verify saved graph has nodes with positions
|
|
73
74
|
expect(savedGraph.nodes).toHaveLength(2);
|
|
@@ -115,7 +116,7 @@ describe('Save and Load Graph', () => {
|
|
|
115
116
|
system.nodeStore.getState().setNodes(initialNodes);
|
|
116
117
|
|
|
117
118
|
// Save the graph
|
|
118
|
-
const savedGraph = buildUIGraphJSON(system);
|
|
119
|
+
const savedGraph = buildUIGraphJSON(system.session!);
|
|
119
120
|
|
|
120
121
|
// Verify viewport is saved
|
|
121
122
|
expect(savedGraph.user?.viewport).toEqual({ x: 50, y: 100, zoom: 1.5 });
|
|
@@ -151,7 +152,7 @@ describe('Save and Load Graph', () => {
|
|
|
151
152
|
system.nodeStore.getState().setNodes(nodes);
|
|
152
153
|
|
|
153
154
|
// Save the graph
|
|
154
|
-
const savedGraph = buildUIGraphJSON(system);
|
|
155
|
+
const savedGraph = buildUIGraphJSON(system.session!);
|
|
155
156
|
|
|
156
157
|
// Verify variables are in the flow
|
|
157
158
|
expect(savedGraph.flow.variables).toBeDefined();
|
|
@@ -200,7 +201,7 @@ describe('Save and Load Graph', () => {
|
|
|
200
201
|
system.nodeStore.getState().setNodes(nodes);
|
|
201
202
|
|
|
202
203
|
// Save the graph
|
|
203
|
-
const savedGraph = buildUIGraphJSON(system);
|
|
204
|
+
const savedGraph = buildUIGraphJSON(system.session!);
|
|
204
205
|
|
|
205
206
|
// Verify custom events are in the flow
|
|
206
207
|
expect(savedGraph.flow.customEvents).toEqual(customEvents);
|
|
@@ -236,7 +237,7 @@ describe('Save and Load Graph', () => {
|
|
|
236
237
|
system.nodeStore.getState().setNodes(nodesWithPositions);
|
|
237
238
|
|
|
238
239
|
// Save the graph
|
|
239
|
-
const savedGraph = buildUIGraphJSON(system);
|
|
240
|
+
const savedGraph = buildUIGraphJSON(system.session!);
|
|
240
241
|
|
|
241
242
|
// Modify the node position
|
|
242
243
|
system.nodeStore.getState().setNodes([
|
|
@@ -340,7 +341,7 @@ describe('Save and Load Graph', () => {
|
|
|
340
341
|
system.variableStore.getState().setVariables(variables);
|
|
341
342
|
|
|
342
343
|
// Save
|
|
343
|
-
const saved = buildUIGraphJSON(system);
|
|
344
|
+
const saved = buildUIGraphJSON(system.session!);
|
|
344
345
|
|
|
345
346
|
// Clear
|
|
346
347
|
system.nodeStore.getState().setNodes([]);
|