@d34dman/flowdrop 0.0.61 → 0.0.63

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 (204) hide show
  1. package/README.md +6 -0
  2. package/dist/adapters/WorkflowAdapter.d.ts +1 -1
  3. package/dist/adapters/agentspec/AgentSpecAdapter.js +3 -1
  4. package/dist/api/client.d.ts +4 -0
  5. package/dist/api/client.js +6 -1
  6. package/dist/api/enhanced-client.js +7 -6
  7. package/dist/components/App.svelte +143 -219
  8. package/dist/components/CanvasBanner.stories.svelte +25 -0
  9. package/dist/components/CanvasBanner.stories.svelte.d.ts +27 -0
  10. package/dist/components/CanvasBanner.svelte +2 -2
  11. package/dist/components/ConfigForm.svelte +37 -36
  12. package/dist/components/ConfigPanel.stories.svelte +38 -0
  13. package/dist/components/ConfigPanel.stories.svelte.d.ts +27 -0
  14. package/dist/components/ConfigPanel.svelte +2 -2
  15. package/dist/components/ConnectionLine.svelte +2 -2
  16. package/dist/components/FlowDropZone.svelte +18 -2
  17. package/dist/components/FlowDropZone.svelte.d.ts +2 -0
  18. package/dist/components/LoadingSpinner.stories.svelte +30 -0
  19. package/dist/components/LoadingSpinner.stories.svelte.d.ts +27 -0
  20. package/dist/components/Logo.stories.svelte +22 -0
  21. package/dist/components/Logo.stories.svelte.d.ts +27 -0
  22. package/dist/components/Logo.svelte +33 -13
  23. package/dist/components/Logo.svelte.d.ts +1 -1
  24. package/dist/components/MarkdownDisplay.stories.svelte +21 -0
  25. package/dist/components/MarkdownDisplay.stories.svelte.d.ts +27 -0
  26. package/dist/components/MarkdownDisplay.svelte +4 -3
  27. package/dist/components/Navbar.stories.svelte +41 -0
  28. package/dist/components/Navbar.stories.svelte.d.ts +27 -0
  29. package/dist/components/Navbar.svelte +4 -4
  30. package/dist/components/NodeSidebar.svelte +12 -12
  31. package/dist/components/NodeStatusOverlay.stories.svelte +74 -0
  32. package/dist/components/NodeStatusOverlay.stories.svelte.d.ts +27 -0
  33. package/dist/components/PipelineStatus.svelte +11 -4
  34. package/dist/components/PortCoordinateTracker.svelte +1 -1
  35. package/dist/components/SchemaForm.stories.svelte +101 -0
  36. package/dist/components/SchemaForm.stories.svelte.d.ts +27 -0
  37. package/dist/components/SchemaForm.svelte +17 -12
  38. package/dist/components/SettingsModal.svelte +3 -3
  39. package/dist/components/SettingsPanel.svelte +23 -22
  40. package/dist/components/StatusIcon.stories.svelte +60 -0
  41. package/dist/components/StatusIcon.stories.svelte.d.ts +27 -0
  42. package/dist/components/StatusIcon.svelte +7 -0
  43. package/dist/components/StatusLabel.stories.svelte +17 -0
  44. package/dist/components/StatusLabel.stories.svelte.d.ts +27 -0
  45. package/dist/components/ThemeToggle.stories.svelte +25 -0
  46. package/dist/components/ThemeToggle.stories.svelte.d.ts +27 -0
  47. package/dist/components/ThemeToggle.svelte +8 -8
  48. package/dist/components/UniversalNode.svelte +1 -1
  49. package/dist/components/WorkflowEditor.svelte +298 -298
  50. package/dist/components/form/FormAutocomplete.svelte +20 -19
  51. package/dist/components/form/FormCheckboxGroup.stories.svelte +28 -0
  52. package/dist/components/form/FormCheckboxGroup.stories.svelte.d.ts +27 -0
  53. package/dist/components/form/FormField.svelte +3 -3
  54. package/dist/components/form/FormFieldLight.svelte +2 -2
  55. package/dist/components/form/FormFieldWrapper.stories.svelte +31 -0
  56. package/dist/components/form/FormFieldWrapper.stories.svelte.d.ts +27 -0
  57. package/dist/components/form/FormFieldset.svelte +7 -7
  58. package/dist/components/form/FormNumberField.stories.svelte +33 -0
  59. package/dist/components/form/FormNumberField.stories.svelte.d.ts +27 -0
  60. package/dist/components/form/FormRangeField.stories.svelte +31 -0
  61. package/dist/components/form/FormRangeField.stories.svelte.d.ts +27 -0
  62. package/dist/components/form/FormSelect.stories.svelte +50 -0
  63. package/dist/components/form/FormSelect.stories.svelte.d.ts +27 -0
  64. package/dist/components/form/FormTemplateEditor.svelte +2 -1
  65. package/dist/components/form/FormTextField.stories.svelte +30 -0
  66. package/dist/components/form/FormTextField.stories.svelte.d.ts +27 -0
  67. package/dist/components/form/FormTextarea.stories.svelte +31 -0
  68. package/dist/components/form/FormTextarea.stories.svelte.d.ts +27 -0
  69. package/dist/components/form/FormToggle.stories.svelte +30 -0
  70. package/dist/components/form/FormToggle.stories.svelte.d.ts +27 -0
  71. package/dist/components/form/FormUISchemaRenderer.svelte +1 -1
  72. package/dist/components/form/types.d.ts +15 -47
  73. package/dist/components/interrupt/ChoicePrompt.stories.svelte +43 -0
  74. package/dist/components/interrupt/ChoicePrompt.stories.svelte.d.ts +27 -0
  75. package/dist/components/interrupt/ChoicePrompt.svelte +24 -24
  76. package/dist/components/interrupt/ConfirmationPrompt.stories.svelte +49 -0
  77. package/dist/components/interrupt/ConfirmationPrompt.stories.svelte.d.ts +27 -0
  78. package/dist/components/interrupt/ConfirmationPrompt.svelte +19 -19
  79. package/dist/components/interrupt/FormPrompt.svelte +15 -15
  80. package/dist/components/interrupt/InterruptBubble.svelte +202 -236
  81. package/dist/components/interrupt/InterruptBubble.svelte.d.ts +1 -1
  82. package/dist/components/interrupt/ReviewPrompt.stories.svelte +46 -0
  83. package/dist/components/interrupt/ReviewPrompt.stories.svelte.d.ts +27 -0
  84. package/dist/components/interrupt/ReviewPrompt.svelte +842 -0
  85. package/dist/components/interrupt/ReviewPrompt.svelte.d.ts +23 -0
  86. package/dist/components/interrupt/TextInputPrompt.stories.svelte +34 -0
  87. package/dist/components/interrupt/TextInputPrompt.stories.svelte.d.ts +27 -0
  88. package/dist/components/interrupt/TextInputPrompt.svelte +21 -21
  89. package/dist/components/nodes/GatewayNode.stories.svelte +76 -0
  90. package/dist/components/nodes/GatewayNode.stories.svelte.d.ts +26 -0
  91. package/dist/components/nodes/GatewayNode.svelte +19 -17
  92. package/dist/components/nodes/IdeaNode.stories.svelte +48 -0
  93. package/dist/components/nodes/IdeaNode.stories.svelte.d.ts +26 -0
  94. package/dist/components/nodes/IdeaNode.svelte +10 -26
  95. package/dist/components/nodes/NotesNode.stories.svelte +69 -0
  96. package/dist/components/nodes/NotesNode.stories.svelte.d.ts +26 -0
  97. package/dist/components/nodes/NotesNode.svelte +8 -8
  98. package/dist/components/nodes/SimpleNode.stories.svelte +101 -0
  99. package/dist/components/nodes/SimpleNode.stories.svelte.d.ts +26 -0
  100. package/dist/components/nodes/SimpleNode.svelte +16 -24
  101. package/dist/components/nodes/SquareNode.stories.svelte +56 -0
  102. package/dist/components/nodes/SquareNode.stories.svelte.d.ts +26 -0
  103. package/dist/components/nodes/SquareNode.svelte +13 -21
  104. package/dist/components/nodes/TerminalNode.stories.svelte +25 -0
  105. package/dist/components/nodes/TerminalNode.stories.svelte.d.ts +26 -0
  106. package/dist/components/nodes/TerminalNode.svelte +7 -7
  107. package/dist/components/nodes/ToolNode.stories.svelte +71 -0
  108. package/dist/components/nodes/ToolNode.stories.svelte.d.ts +26 -0
  109. package/dist/components/nodes/ToolNode.svelte +7 -15
  110. package/dist/components/nodes/WorkflowNode.stories.svelte +50 -0
  111. package/dist/components/nodes/WorkflowNode.stories.svelte.d.ts +26 -0
  112. package/dist/components/nodes/WorkflowNode.svelte +13 -13
  113. package/dist/components/playground/ChatPanel.svelte +48 -48
  114. package/dist/components/playground/ExecutionLogs.svelte +23 -23
  115. package/dist/components/playground/InputCollector.svelte +24 -24
  116. package/dist/components/playground/MessageBubble.stories.svelte +49 -0
  117. package/dist/components/playground/MessageBubble.stories.svelte.d.ts +27 -0
  118. package/dist/components/playground/MessageBubble.svelte +49 -46
  119. package/dist/components/playground/Playground.svelte +194 -129
  120. package/dist/components/playground/PlaygroundModal.svelte +5 -5
  121. package/dist/components/playground/SessionManager.svelte +26 -26
  122. package/dist/config/constants.d.ts +22 -0
  123. package/dist/config/constants.js +22 -0
  124. package/dist/config/endpoints.d.ts +19 -0
  125. package/dist/config/runtimeConfig.js +2 -1
  126. package/dist/core/index.d.ts +5 -2
  127. package/dist/core/index.js +9 -1
  128. package/dist/editor/index.d.ts +13 -9
  129. package/dist/editor/index.js +15 -11
  130. package/dist/form/code.d.ts +2 -1
  131. package/dist/form/code.js +1 -3
  132. package/dist/form/markdown.d.ts +2 -1
  133. package/dist/form/markdown.js +1 -3
  134. package/dist/helpers/workflowEditorHelper.js +18 -33
  135. package/dist/mocks/app-forms.js +1 -0
  136. package/dist/mocks/app-navigation.js +3 -1
  137. package/dist/mocks/app-stores.d.ts +4 -4
  138. package/dist/playground/index.d.ts +4 -3
  139. package/dist/playground/index.js +12 -10
  140. package/dist/playground/mount.js +6 -13
  141. package/dist/services/agentSpecExecutionService.js +2 -1
  142. package/dist/services/api.js +10 -18
  143. package/dist/services/apiVariableService.js +2 -1
  144. package/dist/services/autoSaveService.d.ts +3 -3
  145. package/dist/services/autoSaveService.js +21 -17
  146. package/dist/services/categoriesApi.js +13 -5
  147. package/dist/services/draftStorage.js +5 -4
  148. package/dist/services/dynamicSchemaService.js +4 -4
  149. package/dist/services/globalSave.d.ts +60 -11
  150. package/dist/services/globalSave.js +160 -83
  151. package/dist/services/historyService.d.ts +2 -1
  152. package/dist/services/historyService.js +7 -3
  153. package/dist/services/interruptService.js +9 -8
  154. package/dist/services/nodeExecutionService.js +14 -6
  155. package/dist/services/playgroundService.js +2 -1
  156. package/dist/services/portConfigApi.js +11 -7
  157. package/dist/services/toastService.d.ts +1 -1
  158. package/dist/services/toastService.js +6 -5
  159. package/dist/services/variableService.js +3 -2
  160. package/dist/settings/index.d.ts +1 -1
  161. package/dist/settings/index.js +1 -1
  162. package/dist/stores/{categoriesStore.d.ts → categoriesStore.svelte.d.ts} +3 -3
  163. package/dist/stores/{categoriesStore.js → categoriesStore.svelte.js} +15 -18
  164. package/dist/stores/editorStateMachine.svelte.d.ts +42 -0
  165. package/dist/stores/editorStateMachine.svelte.js +132 -0
  166. package/dist/stores/{historyStore.d.ts → historyStore.svelte.d.ts} +18 -15
  167. package/dist/stores/{historyStore.js → historyStore.svelte.js} +40 -21
  168. package/dist/stores/{interruptStore.d.ts → interruptStore.svelte.d.ts} +16 -15
  169. package/dist/stores/{interruptStore.js → interruptStore.svelte.js} +85 -94
  170. package/dist/stores/{playgroundStore.d.ts → playgroundStore.svelte.d.ts} +41 -33
  171. package/dist/stores/{playgroundStore.js → playgroundStore.svelte.js} +164 -84
  172. package/dist/stores/{portCoordinateStore.d.ts → portCoordinateStore.svelte.d.ts} +10 -4
  173. package/dist/stores/{portCoordinateStore.js → portCoordinateStore.svelte.js} +38 -35
  174. package/dist/stores/{settingsStore.d.ts → settingsStore.svelte.d.ts} +45 -28
  175. package/dist/stores/{settingsStore.js → settingsStore.svelte.js} +169 -128
  176. package/dist/stores/{workflowStore.d.ts → workflowStore.svelte.d.ts} +101 -65
  177. package/dist/stores/{workflowStore.js → workflowStore.svelte.js} +285 -239
  178. package/dist/stories/CanvasDecorator.svelte +50 -0
  179. package/dist/stories/CanvasDecorator.svelte.d.ts +8 -0
  180. package/dist/stories/NodeDecorator.svelte +74 -0
  181. package/dist/stories/NodeDecorator.svelte.d.ts +8 -0
  182. package/dist/stories/utils.d.ts +93 -0
  183. package/dist/stories/utils.js +122 -0
  184. package/dist/styles/base.css +114 -61
  185. package/dist/styles/toast.css +2 -2
  186. package/dist/styles/tokens.css +250 -185
  187. package/dist/svelte-app.d.ts +0 -6
  188. package/dist/svelte-app.js +13 -31
  189. package/dist/types/index.d.ts +2 -0
  190. package/dist/types/interrupt.d.ts +89 -5
  191. package/dist/types/interrupt.js +13 -1
  192. package/dist/types/playground.d.ts +5 -0
  193. package/dist/types/settings.js +1 -1
  194. package/dist/utils/colors.js +4 -4
  195. package/dist/utils/connections.js +33 -8
  196. package/dist/utils/icons.js +1 -1
  197. package/dist/utils/logger.d.ts +47 -0
  198. package/dist/utils/logger.js +72 -0
  199. package/dist/utils/nodeWrapper.js +1 -1
  200. package/dist/utils/sanitize.d.ts +19 -0
  201. package/dist/utils/sanitize.js +31 -0
  202. package/dist/utils/validation.d.ts +29 -0
  203. package/dist/utils/validation.js +39 -0
  204. package/package.json +243 -232
@@ -16,7 +16,7 @@
16
16
  type ColorMode
17
17
  } from '@xyflow/svelte';
18
18
  import '@xyflow/svelte/dist/style.css';
19
- import { resolvedTheme, editorSettings, behaviorSettings } from '../stores/settingsStore.js';
19
+ import { getResolvedTheme, getEditorSettings, getBehaviorSettings } from '../stores/settingsStore.svelte.js';
20
20
  import type {
21
21
  WorkflowNode as WorkflowNodeType,
22
22
  NodeMetadata,
@@ -26,11 +26,11 @@
26
26
  import CanvasBanner from './CanvasBanner.svelte';
27
27
  import FlowDropZone from './FlowDropZone.svelte';
28
28
  import EdgeRefresher from './EdgeRefresher.svelte';
29
- import { tick } from 'svelte';
29
+ import { tick, untrack } from 'svelte';
30
30
  import type { EndpointConfig } from '../config/endpoints.js';
31
31
  import ConnectionLine from './ConnectionLine.svelte';
32
- import { workflowStore, workflowActions } from '../stores/workflowStore.js';
33
- import { historyActions, setOnRestoreCallback } from '../stores/historyStore.js';
32
+ import { getWorkflowStore, workflowActions } from '../stores/workflowStore.svelte.js';
33
+ import { historyActions, setOnRestoreCallback } from '../stores/historyStore.svelte.js';
34
34
  import UniversalNode from './UniversalNode.svelte';
35
35
  import {
36
36
  EdgeStylingHelper,
@@ -39,15 +39,17 @@
39
39
  ConfigurationHelper
40
40
  } from '../helpers/workflowEditorHelper.js';
41
41
  import type { NodeExecutionInfo } from '../types/index.js';
42
- import { areNodeArraysEqual, areEdgeArraysEqual, throttle } from '../utils/performanceUtils.js';
43
42
  import { Toaster } from 'svelte-5-french-toast';
44
- import { flowdropToastOptions, FLOWDROP_TOASTER_CLASS } from '../services/toastService.js';
43
+ import { flowdropToastOptions, FLOWDROP_TOASTER_CLASS, apiToasts } from '../services/toastService.js';
45
44
  import {
46
45
  ProximityConnectHelper,
47
46
  type ProximityEdgeCandidate
48
47
  } from '../helpers/proximityConnect.js';
49
48
  import PortCoordinateTracker from './PortCoordinateTracker.svelte';
50
- import { getPortCoordinateSnapshot } from '../stores/portCoordinateStore.js';
49
+ import { getPortCoordinateSnapshot } from '../stores/portCoordinateStore.svelte.js';
50
+ import { logger } from '../utils/logger.js';
51
+ import { validateWorkflowData } from '../utils/validation.js';
52
+ import { createEditorStateMachine } from '../stores/editorStateMachine.svelte.js';
51
53
 
52
54
  interface Props {
53
55
  nodes?: NodeMetadata[];
@@ -68,11 +70,19 @@
68
70
 
69
71
  let props: Props = $props();
70
72
 
71
- // Create a local currentWorkflow variable that we can control directly
72
- let currentWorkflow = $state<Workflow | null>(null);
73
-
74
- // Track if we're currently dragging a node (for history debouncing)
75
- let isDraggingNode = $state(false);
73
+ // ---------------------------------------------------------------------------
74
+ // Editor State Machine
75
+ // Centralizes reactive guards — replaces scattered boolean flags
76
+ // (isDraggingNode, lastEditorStoreValue identity checks, etc.)
77
+ // ---------------------------------------------------------------------------
78
+ const machine = createEditorStateMachine();
79
+
80
+ // Dev-mode transition logging
81
+ if (import.meta.env?.DEV) {
82
+ machine.onTransition((from, event, to) => {
83
+ logger.debug(`[EditorFSM] ${from} --${event}--> ${to}`);
84
+ });
85
+ }
76
86
 
77
87
  // Proximity connect state
78
88
  let currentProximityCandidates = $state<ProximityEdgeCandidate[]>([]);
@@ -81,259 +91,228 @@
81
91
  let portCoordNodeToUpdate = $state<WorkflowNodeType | null>(null);
82
92
  let portCoordRebuildTrigger = $state(0);
83
93
 
84
- // Track the workflow ID we're currently editing to detect workflow switches
85
- let currentWorkflowId: string | null = null;
86
-
87
- // Track the last store value written by this editor to distinguish
88
- // external programmatic changes from our own echoed writes
89
- let lastEditorStoreValue: Workflow | null = null;
90
-
91
- // Initialize currentWorkflow from global store
92
- // Sync on workflow ID change (new workflow loaded) or external programmatic changes
93
- $effect(() => {
94
- if ($workflowStore) {
95
- const storeWorkflowId = $workflowStore.id;
96
-
97
- if (currentWorkflowId !== storeWorkflowId) {
98
- // New workflow loaded
99
- currentWorkflow = $workflowStore;
100
- currentWorkflowId = storeWorkflowId;
101
- lastEditorStoreValue = null;
102
- } else if ($workflowStore !== lastEditorStoreValue) {
103
- // External programmatic change (e.g. addEdge, updateNode, updateEdges)
104
- // The store value differs from what this editor last wrote, so sync it
105
- currentWorkflow = $workflowStore;
106
- }
107
- } else if (currentWorkflow !== null) {
108
- // Store was cleared
109
- currentWorkflow = null;
110
- currentWorkflowId = null;
111
- }
112
- });
113
-
114
- // Set up the history restore callback to update workflow when undo/redo is triggered
115
- $effect(() => {
116
- setOnRestoreCallback((restoredWorkflow: Workflow) => {
117
- // Directly update local state (bypass store sync effect)
118
- currentWorkflow = restoredWorkflow;
119
- // Mark as our own write so sync effect doesn't re-process it
120
- lastEditorStoreValue = restoredWorkflow;
121
- // Also update the store without triggering history
122
- workflowActions.restoreFromHistory(restoredWorkflow);
123
- });
94
+ // ---------------------------------------------------------------------------
95
+ // Flow state bound to SvelteFlow via bind:nodes / bind:edges
96
+ // These are $state.raw to prevent deep proxy leaking (SvelteFlow mutates
97
+ // node internals during drag which would cause infinite loops with $state).
98
+ // ---------------------------------------------------------------------------
99
+ let flowNodes = $state.raw<WorkflowNodeType[]>([]);
100
+ let flowEdges = $state.raw<WorkflowEdge[]>([]);
124
101
 
125
- // Cleanup on unmount
126
- return () => {
127
- setOnRestoreCallback(null);
128
- };
129
- });
130
-
131
- // Create local reactive variables that sync with currentWorkflow
132
- let flowNodes = $state<WorkflowNodeType[]>([]);
133
- let flowEdges = $state<WorkflowEdge[]>([]);
134
-
135
- // Sync local state with currentWorkflow
102
+ // Execution info loading state
136
103
  let loadExecutionInfoTimeout: number | null = null;
137
104
  let executionInfoAbortController: AbortController | null = null;
138
- // Track previous workflow ID to detect when we need to reload execution info
139
- let previousWorkflowId: string | null = null;
140
- let previousPipelineId: string | undefined = undefined;
141
105
 
142
106
  /**
143
- * Key for SvelteFlow component - changes when workflow ID changes
144
- * This forces SvelteFlow to remount with fresh state, allowing fitView to work correctly
107
+ * Key for SvelteFlow component changes when workflow ID changes.
108
+ * Forces SvelteFlow to remount with fresh state, allowing fitView to work correctly.
145
109
  */
146
- let svelteFlowKey = $derived(currentWorkflow?.id ?? 'default');
110
+ let svelteFlowKey = $derived(getWorkflowStore()?.id ?? 'default');
147
111
 
148
112
  /**
149
113
  * Derive snap grid configuration from editor settings
150
- * Returns [gridSize, gridSize] tuple when snapToGrid is enabled, undefined otherwise
151
114
  */
152
115
  let snapGrid = $derived(
153
- $editorSettings.snapToGrid
154
- ? ([$editorSettings.gridSize, $editorSettings.gridSize] as [number, number])
116
+ getEditorSettings().snapToGrid
117
+ ? ([getEditorSettings().gridSize, getEditorSettings().gridSize] as [number, number])
155
118
  : undefined
156
119
  );
157
120
 
158
121
  /**
159
122
  * Derive initial viewport configuration from editor settings
160
- * Sets initial zoom level based on user preferences
161
123
  */
162
124
  let initialViewport = $derived({
163
- zoom: $editorSettings.defaultZoom,
125
+ zoom: getEditorSettings().defaultZoom,
164
126
  x: 0,
165
127
  y: 0
166
128
  });
167
129
 
130
+ // ---------------------------------------------------------------------------
131
+ // Helper: derive flowNodes/flowEdges from a Workflow object
132
+ // ---------------------------------------------------------------------------
133
+ function buildFlowNodesFromStore(workflow: Workflow): {
134
+ nodes: WorkflowNodeType[];
135
+ edges: WorkflowEdge[];
136
+ } {
137
+ const nodesWithCallbacks = workflow.nodes.map((node) => ({
138
+ ...node,
139
+ data: {
140
+ ...node.data,
141
+ onConfigOpen: props.openConfigSidebar
142
+ }
143
+ }));
144
+ const styledEdges = EdgeStylingHelper.updateEdgeStyles(workflow.edges, nodesWithCallbacks);
145
+ return { nodes: nodesWithCallbacks, edges: styledEdges };
146
+ }
147
+
148
+ // ---------------------------------------------------------------------------
149
+ // Helper: sync current flowNodes/flowEdges back to the global store
150
+ // ---------------------------------------------------------------------------
151
+ function syncFlowToStore(): void {
152
+ const storeValue = untrack(() => getWorkflowStore());
153
+ if (!storeValue) return;
154
+ const updatedWorkflow = WorkflowOperationsHelper.updateWorkflow(
155
+ storeValue,
156
+ flowNodes,
157
+ flowEdges
158
+ );
159
+ workflowActions.updateWorkflow(updatedWorkflow);
160
+ }
161
+
162
+ // ---------------------------------------------------------------------------
163
+ // Single sync effect: workflowStore → flowNodes / flowEdges
164
+ // Replaces the old Effect A (store→currentWorkflow) + Effect B (currentWorkflow→flow).
165
+ // Suppressed during operations via state machine; handlers update flowNodes directly.
166
+ // ---------------------------------------------------------------------------
167
+ let previousSyncedWorkflowId: string | null = null;
168
+
168
169
  $effect(() => {
169
- if (currentWorkflow) {
170
- const nodesWithCallbacks = currentWorkflow.nodes.map((node) => ({
171
- ...node,
172
- data: {
173
- ...node.data,
174
- onConfigOpen: props.openConfigSidebar
175
- }
176
- }));
177
- flowNodes = nodesWithCallbacks;
170
+ const storeValue = getWorkflowStore();
178
171
 
179
- // Apply edge styling based on source port data type when loading workflow
180
- const styledEdges = EdgeStylingHelper.updateEdgeStyles(
181
- currentWorkflow.edges,
182
- nodesWithCallbacks
183
- );
184
- flowEdges = styledEdges;
185
-
186
- // Trigger port coordinate rebuild after workflow load
187
- // (PortCoordinateTracker will wait for SvelteFlow to render before reading handleBounds)
188
- // Note: Using Date.now() instead of ++ to avoid reading the old value,
189
- // which would make this effect depend on portCoordRebuildTrigger and loop.
190
- if ($editorSettings.proximityConnect) {
191
- portCoordRebuildTrigger = Date.now();
172
+ // Suppressed during operations handlers write to flowNodes directly
173
+ if (untrack(() => machine.permissions.suppressEffect)) return;
174
+
175
+ if (!storeValue) {
176
+ if (flowNodes.length > 0 || flowEdges.length > 0) {
177
+ flowNodes = [];
178
+ flowEdges = [];
179
+ previousSyncedWorkflowId = null;
180
+ untrack(() => machine.send('WORKFLOW_CLEARED'));
192
181
  }
182
+ return;
183
+ }
193
184
 
194
- // Only load execution info if we have a pipelineId (pipeline status mode)
195
- // and if the workflow or pipeline has changed
196
- const workflowChanged = currentWorkflow.id !== previousWorkflowId;
197
- const pipelineChanged = props.pipelineId !== previousPipelineId;
185
+ const isNewWorkflow = storeValue.id !== previousSyncedWorkflowId;
198
186
 
199
- if (props.pipelineId && (workflowChanged || pipelineChanged)) {
200
- // Cancel any pending timeout
201
- if (loadExecutionInfoTimeout) {
202
- clearTimeout(loadExecutionInfoTimeout);
203
- loadExecutionInfoTimeout = null;
204
- }
187
+ if (isNewWorkflow) {
188
+ untrack(() =>
189
+ machine.send(previousSyncedWorkflowId ? 'WORKFLOW_SWITCHED' : 'WORKFLOW_LOADED')
190
+ );
191
+ }
205
192
 
206
- // Cancel any in-flight request
207
- if (executionInfoAbortController) {
208
- executionInfoAbortController.abort();
209
- executionInfoAbortController = null;
210
- }
193
+ // Derive flowNodes/flowEdges from store
194
+ const derived = buildFlowNodesFromStore(storeValue);
195
+ flowNodes = derived.nodes;
196
+ flowEdges = derived.edges;
197
+ previousSyncedWorkflowId = storeValue.id;
211
198
 
212
- // Update tracking variables
213
- previousWorkflowId = currentWorkflow.id;
214
- previousPipelineId = props.pipelineId;
215
-
216
- // Use requestIdleCallback for non-critical updates (falls back to setTimeout)
217
- if (typeof requestIdleCallback !== 'undefined') {
218
- loadExecutionInfoTimeout = requestIdleCallback(
219
- () => {
220
- loadNodeExecutionInfo();
221
- },
222
- { timeout: 500 }
223
- ) as unknown as number;
224
- } else {
225
- // Fallback to setTimeout with longer delay for better performance
226
- loadExecutionInfoTimeout = setTimeout(() => {
227
- loadNodeExecutionInfo();
228
- }, 300) as unknown as number;
229
- }
230
- }
199
+ // Trigger port coordinate rebuild after workflow load
200
+ if (getEditorSettings().proximityConnect) {
201
+ portCoordRebuildTrigger = Date.now();
202
+ }
203
+
204
+ if (isNewWorkflow) {
205
+ untrack(() => machine.send('LOAD_COMPLETE'));
231
206
  }
232
207
  });
233
208
 
234
- /**
235
- * Throttled function to update the global store
236
- * Reduces update frequency during rapid changes (e.g., node dragging)
237
- * Uses 16ms throttle (~60fps) for smooth performance
238
- */
239
- const updateGlobalStore = throttle((): void => {
240
- if (currentWorkflow) {
241
- lastEditorStoreValue = currentWorkflow;
242
- workflowActions.updateWorkflow(currentWorkflow);
209
+ // ---------------------------------------------------------------------------
210
+ // Execution info effect (separate async, depends on workflow + pipeline ID)
211
+ // ---------------------------------------------------------------------------
212
+ let previousExecWorkflowId: string | null = null;
213
+ let previousExecPipelineId: string | undefined = undefined;
214
+
215
+ $effect(() => {
216
+ const storeValue = getWorkflowStore();
217
+ const pipelineId = props.pipelineId;
218
+
219
+ if (!storeValue || !pipelineId) return;
220
+
221
+ const workflowChanged = storeValue.id !== previousExecWorkflowId;
222
+ const pipelineChanged = pipelineId !== previousExecPipelineId;
223
+
224
+ if (!workflowChanged && !pipelineChanged) return;
225
+
226
+ previousExecWorkflowId = storeValue.id;
227
+ previousExecPipelineId = pipelineId;
228
+
229
+ // Cancel any pending timeout / in-flight request
230
+ if (loadExecutionInfoTimeout) {
231
+ clearTimeout(loadExecutionInfoTimeout);
232
+ loadExecutionInfoTimeout = null;
243
233
  }
244
- }, 16);
234
+ if (executionInfoAbortController) {
235
+ executionInfoAbortController.abort();
236
+ executionInfoAbortController = null;
237
+ }
238
+
239
+ // Schedule loading with requestIdleCallback (falls back to setTimeout)
240
+ if (typeof requestIdleCallback !== 'undefined') {
241
+ loadExecutionInfoTimeout = requestIdleCallback(
242
+ () => {
243
+ loadNodeExecutionInfo();
244
+ },
245
+ { timeout: 500 }
246
+ ) as unknown as number;
247
+ } else {
248
+ loadExecutionInfoTimeout = setTimeout(() => {
249
+ loadNodeExecutionInfo();
250
+ }, 300) as unknown as number;
251
+ }
252
+ });
253
+
254
+ // ---------------------------------------------------------------------------
255
+ // History restore callback
256
+ // ---------------------------------------------------------------------------
257
+ $effect(() => {
258
+ setOnRestoreCallback((restoredWorkflow: Workflow) => {
259
+ machine.send('START_RESTORE');
260
+ // Update the store (effect is suppressed during 'restoring')
261
+ workflowActions.restoreFromHistory(restoredWorkflow);
262
+ // Derive flowNodes/flowEdges directly for immediate visual update
263
+ const derived = buildFlowNodesFromStore(restoredWorkflow);
264
+ flowNodes = derived.nodes;
265
+ flowEdges = derived.edges;
266
+ machine.send('RESTORE_COMPLETE');
267
+ // After RESTORE_COMPLETE → idle, the sync effect runs but produces
268
+ // the same data (no-op re-derive).
269
+ });
270
+
271
+ return () => {
272
+ setOnRestoreCallback(null);
273
+ };
274
+ });
245
275
 
246
276
  /**
247
277
  * Load node execution information for all nodes in the workflow
248
- * Optimized to reduce processing time and prevent blocking the main thread
249
278
  */
250
279
  async function loadNodeExecutionInfo(): Promise<void> {
251
- if (!currentWorkflow?.nodes || !props.pipelineId) return;
280
+ const workflow = untrack(() => getWorkflowStore());
281
+ if (!workflow?.nodes || !props.pipelineId) return;
252
282
 
253
283
  try {
254
- // Create abort controller for this request
255
284
  executionInfoAbortController = new AbortController();
256
285
 
257
- // Fetch execution info with abort signal
258
286
  const executionInfo = await NodeOperationsHelper.loadNodeExecutionInfo(
259
- currentWorkflow,
287
+ workflow,
260
288
  props.pipelineId
261
289
  );
262
290
 
263
- // Check if request was aborted
264
- if (executionInfoAbortController?.signal.aborted) {
265
- return;
266
- }
291
+ if (executionInfoAbortController?.signal.aborted) return;
267
292
 
268
- // Default execution info for nodes without data
269
293
  const defaultExecutionInfo: NodeExecutionInfo = {
270
294
  status: 'idle' as const,
271
295
  executionCount: 0,
272
296
  isExecuting: false
273
297
  };
274
298
 
275
- // Optimize: Single pass through nodes instead of multiple maps
276
- // This reduces processing time from ~100ms to ~10-20ms for large workflows
277
- const updatedNodes = currentWorkflow.nodes.map((node) => ({
299
+ // Update flowNodes with execution info (visual-only, no store sync needed)
300
+ flowNodes = flowNodes.map((node) => ({
278
301
  ...node,
279
302
  data: {
280
303
  ...node.data,
281
- executionInfo: executionInfo[node.id] || defaultExecutionInfo,
282
- onConfigOpen: props.openConfigSidebar
304
+ executionInfo: executionInfo[node.id] || defaultExecutionInfo
283
305
  }
284
306
  }));
285
307
 
286
- // Update state in a single operation
287
- flowNodes = updatedNodes;
288
- currentWorkflow.nodes = updatedNodes;
289
-
290
- // Clear abort controller
291
308
  executionInfoAbortController = null;
292
309
  } catch (error) {
293
- // Only log if it's not an abort error
294
310
  if (error instanceof Error && error.name !== 'AbortError') {
295
- console.error('Failed to load node execution info:', error);
311
+ logger.error('Failed to load node execution info:', error);
296
312
  }
297
313
  }
298
314
  }
299
315
 
300
- // Function to update currentWorkflow when SvelteFlow changes nodes/edges
301
- function updateCurrentWorkflowFromSvelteFlow(): void {
302
- if (currentWorkflow) {
303
- currentWorkflow = WorkflowOperationsHelper.updateWorkflow(
304
- currentWorkflow,
305
- flowNodes,
306
- flowEdges
307
- );
308
-
309
- // Update the global store
310
- updateGlobalStore();
311
- }
312
- }
313
-
314
- // Track previous values to detect changes from SvelteFlow
315
- let previousNodes = $state<WorkflowNodeType[]>([]);
316
- let previousEdges = $state<WorkflowEdge[]>([]);
317
-
318
- /**
319
- * Watch for changes from SvelteFlow and update currentWorkflow
320
- * Uses efficient comparison instead of expensive JSON.stringify
321
- * This reduces event handler time from 290-310ms to <50ms
322
- */
323
- $effect(() => {
324
- // Check if nodes have changed from SvelteFlow using fast comparison
325
- const nodesChanged = !areNodeArraysEqual(flowNodes, previousNodes);
326
- const edgesChanged = !areEdgeArraysEqual(flowEdges, previousEdges);
327
-
328
- if ((nodesChanged || edgesChanged) && currentWorkflow) {
329
- updateCurrentWorkflowFromSvelteFlow();
330
-
331
- // Update previous values with shallow copies
332
- previousNodes = [...flowNodes];
333
- previousEdges = [...flowEdges];
334
- }
335
- });
336
-
337
316
  // The global store should be initialized by the parent App component
338
317
 
339
318
  // Sidebar is now always visible - removed toggle functionality
@@ -350,10 +329,12 @@
350
329
  /**
351
330
  * Handle node drag start
352
331
  *
353
- * Marks the beginning of a drag operation.
332
+ * Transitions the state machine to 'dragging', which suppresses
333
+ * the sync effect to prevent reactive loops during high-frequency
334
+ * position updates. SvelteFlow mutates flowNodes directly via bind:nodes.
354
335
  */
355
336
  function handleNodeDragStart(): void {
356
- isDraggingNode = true;
337
+ machine.send('START_DRAG');
357
338
  // Clear any leftover proximity previews
358
339
  currentProximityCandidates = [];
359
340
  }
@@ -370,7 +351,7 @@
370
351
  nodes: WorkflowNodeType[];
371
352
  event: MouseEvent | TouchEvent;
372
353
  }): void {
373
- if (!$editorSettings.proximityConnect || !targetNode || props.readOnly || props.lockWorkflow) {
354
+ if (!getEditorSettings().proximityConnect || !targetNode || props.readOnly || props.lockWorkflow) {
374
355
  if (currentProximityCandidates.length > 0) {
375
356
  flowEdges = ProximityConnectHelper.removePreviewEdges(flowEdges);
376
357
  currentProximityCandidates = [];
@@ -393,13 +374,13 @@
393
374
  targetNode.id,
394
375
  portCoordinates,
395
376
  baseEdges,
396
- $editorSettings.proximityConnectDistance
377
+ getEditorSettings().proximityConnectDistance
397
378
  )
398
379
  : ProximityConnectHelper.findCompatibleEdges(
399
380
  targetNode,
400
381
  flowNodes,
401
382
  baseEdges,
402
- $editorSettings.proximityConnectDistance
383
+ getEditorSettings().proximityConnectDistance
403
384
  );
404
385
 
405
386
  // Create preview edges
@@ -413,24 +394,19 @@
413
394
  /**
414
395
  * Handle node drag stop
415
396
  *
416
- * Push the NEW state (after drag) to history.
417
- * Undo will then restore to the previous state (before drag).
397
+ * Still in 'dragging' state sync effect suppressed.
398
+ * Syncs final positions to store, pushes history, then transitions to idle.
418
399
  */
419
400
  function handleNodeDragStop(): void {
420
- isDraggingNode = false;
421
401
  portCoordNodeToUpdate = null;
422
402
 
423
403
  // Finalize proximity connect if there are candidates
424
- if ($editorSettings.proximityConnect && currentProximityCandidates.length > 0) {
425
- // Remove all preview edges
404
+ if (getEditorSettings().proximityConnect && currentProximityCandidates.length > 0) {
426
405
  const baseEdges = ProximityConnectHelper.removePreviewEdges(flowEdges);
427
-
428
- // Create permanent edges from candidates
429
406
  const permanentEdges = ProximityConnectHelper.createPermanentEdges(
430
407
  currentProximityCandidates
431
408
  );
432
409
 
433
- // Apply proper styling to each new permanent edge
434
410
  for (const edge of permanentEdges) {
435
411
  const sourceNode = flowNodes.find((n) => n.id === edge.source);
436
412
  const targetNode = flowNodes.find((n) => n.id === edge.target);
@@ -439,27 +415,25 @@
439
415
  }
440
416
  }
441
417
 
442
- // Set final edges
443
418
  flowEdges = [...baseEdges, ...permanentEdges];
444
-
445
- // Clear proximity state
446
419
  currentProximityCandidates = [];
447
-
448
- // Update workflow
449
- if (currentWorkflow) {
450
- updateCurrentWorkflowFromSvelteFlow();
451
- }
452
420
  }
453
421
 
454
- // Push the current state AFTER the drag completed
455
- if (currentWorkflow) {
456
- workflowActions.pushHistory('Move node', currentWorkflow);
422
+ // Sync flowNodes/flowEdges store
423
+ syncFlowToStore();
424
+
425
+ // Push history AFTER the drag completed
426
+ const storeValue = getWorkflowStore();
427
+ if (storeValue) {
428
+ workflowActions.pushHistory('Move node', storeValue);
457
429
  }
430
+
431
+ // Transition to idle — sync effect is now unblocked
432
+ machine.send('STOP_DRAG');
458
433
  }
459
434
 
460
435
  /**
461
436
  * Handle new connections between nodes
462
- * Let SvelteFlow handle edge creation, styling will be applied via reactive effects
463
437
  */
464
438
  async function handleConnect(connection: {
465
439
  source: string;
@@ -467,23 +441,23 @@
467
441
  sourceHandle?: string;
468
442
  targetHandle?: string;
469
443
  }): Promise<void> {
470
- // SvelteFlow will automatically create the edge due to bind:edges
471
- // Wait for DOM update before applying styling
444
+ machine.send('START_CONNECT');
445
+
446
+ // SvelteFlow auto-creates the edge via bind:edges — wait for DOM update
472
447
  await tick();
473
448
 
474
- // Apply styling to the new edge (including arrows)
475
- updateExistingEdgeStyles();
449
+ // Apply styling to all edges (including the new one)
450
+ flowEdges = EdgeStylingHelper.updateEdgeStyles(flowEdges, flowNodes);
476
451
 
477
- // Update currentWorkflow with the new edge
478
- if (currentWorkflow) {
479
- updateCurrentWorkflowFromSvelteFlow();
480
- }
452
+ // Sync to store
453
+ syncFlowToStore();
481
454
 
482
- // Push to history AFTER the connection is made
483
- // This way undo will restore to the state before the connection
484
- if (currentWorkflow) {
485
- workflowActions.pushHistory('Add connection', currentWorkflow);
455
+ const storeValue = getWorkflowStore();
456
+ if (storeValue) {
457
+ workflowActions.pushHistory('Add connection', storeValue);
486
458
  }
459
+
460
+ machine.send('CONNECTION_MADE');
487
461
  }
488
462
 
489
463
  /**
@@ -500,7 +474,7 @@
500
474
  edges: WorkflowEdge[];
501
475
  }): Promise<boolean> {
502
476
  // If confirmDelete setting is enabled, show confirmation dialog
503
- if ($behaviorSettings.confirmDelete) {
477
+ if (getBehaviorSettings().confirmDelete) {
504
478
  const nodeCount = params.nodes.length;
505
479
  const edgeCount = params.edges.length;
506
480
 
@@ -533,6 +507,8 @@
533
507
  * Handle node deletion - automatically remove connected edges and push to history
534
508
  */
535
509
  function handleNodesDelete(params: { nodes: WorkflowNodeType[]; edges: WorkflowEdge[] }): void {
510
+ machine.send('START_DELETE');
511
+
536
512
  const deletedNodeIds = new Set(params.nodes.map((node) => node.id));
537
513
 
538
514
  // Filter out edges connected to deleted nodes
@@ -540,10 +516,8 @@
540
516
  (edge) => !deletedNodeIds.has(edge.source) && !deletedNodeIds.has(edge.target)
541
517
  );
542
518
 
543
- // Update currentWorkflow
544
- if (currentWorkflow) {
545
- updateCurrentWorkflowFromSvelteFlow();
546
- }
519
+ // Sync to store
520
+ syncFlowToStore();
547
521
 
548
522
  // Push to history AFTER the deletion so undo restores the previous state
549
523
  const nodeCount = params.nodes.length;
@@ -556,32 +530,12 @@
556
530
  } else if (edgeCount > 0) {
557
531
  description = `Delete ${edgeCount} connection${edgeCount > 1 ? 's' : ''}`;
558
532
  }
559
- if (currentWorkflow) {
560
- workflowActions.pushHistory(description, currentWorkflow);
533
+ const storeValue = getWorkflowStore();
534
+ if (storeValue) {
535
+ workflowActions.pushHistory(description, storeValue);
561
536
  }
562
- }
563
-
564
- /**
565
- * Update existing edges with our custom styling rules
566
- * This ensures all edges (including existing ones) follow our rules
567
- */
568
- async function updateExistingEdgeStyles(): Promise<void> {
569
- // Wait for any pending DOM updates
570
- await tick();
571
537
 
572
- const updatedEdges = EdgeStylingHelper.updateEdgeStyles(flowEdges, flowNodes);
573
-
574
- // Update currentWorkflow with the styled edges
575
- if (currentWorkflow) {
576
- currentWorkflow = WorkflowOperationsHelper.updateWorkflow(
577
- currentWorkflow,
578
- flowNodes,
579
- updatedEdges
580
- );
581
-
582
- // Update the global store
583
- updateGlobalStore();
584
- }
538
+ machine.send('DELETE_COMPLETE');
585
539
  }
586
540
 
587
541
  // Edge styling will be handled when edges are first created or manually updated
@@ -602,31 +556,73 @@
602
556
 
603
557
  /**
604
558
  * Handle drop event and add new node to canvas
605
- * This will be called from the inner DropZone component
606
559
  */
607
560
  async function handleNodeDrop(
608
561
  nodeTypeData: string,
609
562
  position: { x: number; y: number }
610
563
  ): Promise<void> {
611
- // Create the node using the helper, passing existing nodes for ID generation
564
+ machine.send('START_DROP');
565
+
612
566
  const newNode = NodeOperationsHelper.createNodeFromDrop(nodeTypeData, position, flowNodes);
613
567
 
614
- if (newNode && currentWorkflow) {
615
- // Add the node first
616
- currentWorkflow = WorkflowOperationsHelper.addNode(currentWorkflow, newNode);
568
+ if (newNode) {
569
+ // Add onConfigOpen callback and append to flowNodes for immediate visual feedback
570
+ const nodeWithCallback = {
571
+ ...newNode,
572
+ data: { ...newNode.data, onConfigOpen: props.openConfigSidebar }
573
+ };
574
+ flowNodes = [...flowNodes, nodeWithCallback];
617
575
 
618
- // Update the global store
619
- updateGlobalStore();
576
+ // Sync to store
577
+ syncFlowToStore();
620
578
 
621
- // Wait for DOM update to ensure SvelteFlow updates
622
579
  await tick();
623
580
 
624
- // Push to history AFTER adding the node
625
- // This way undo will restore to the state before the add
626
- workflowActions.pushHistory('Add node', currentWorkflow);
627
- } else if (!currentWorkflow) {
628
- console.warn('No currentWorkflow available for new node');
581
+ const storeValue = getWorkflowStore();
582
+ if (storeValue) {
583
+ workflowActions.pushHistory('Add node', storeValue);
584
+ }
585
+ } else {
586
+ logger.warn('Failed to create node from drop data');
629
587
  }
588
+
589
+ machine.send('DROP_COMPLETE');
590
+ }
591
+
592
+ /**
593
+ * Handle a workflow JSON file dropped directly onto the canvas.
594
+ *
595
+ * Validates the JSON against the minimum required Workflow fields and, if valid,
596
+ * loads it into the workflow store. Shows a toast on validation failure or read error.
597
+ */
598
+ function handleWorkflowFileDrop(file: File): void {
599
+ const reader = new FileReader();
600
+ reader.onload = (event) => {
601
+ try {
602
+ const text = event.target?.result;
603
+ if (typeof text !== 'string') {
604
+ throw new Error('Could not read file contents.');
605
+ }
606
+ const data = JSON.parse(text);
607
+ const validation = validateWorkflowData(data);
608
+ if (!validation.valid) {
609
+ apiToasts.error('Import workflow', validation.error ?? 'Invalid workflow JSON');
610
+ logger.warn('Workflow file drop validation failed:', validation.error);
611
+ return;
612
+ }
613
+ workflowActions.initialize(data as Workflow);
614
+ } catch (error) {
615
+ const errorObj = error instanceof Error ? error : new Error('Unknown error occurred');
616
+ logger.error('Workflow file drop import failed:', errorObj);
617
+ apiToasts.error('Import workflow', errorObj.message);
618
+ }
619
+ };
620
+ reader.onerror = () => {
621
+ const message = 'Failed to read the dropped file.';
622
+ logger.error(message);
623
+ apiToasts.error('Import workflow', message);
624
+ };
625
+ reader.readAsText(file);
630
626
  }
631
627
 
632
628
  /**
@@ -636,8 +632,9 @@
636
632
 
637
633
  /**
638
634
  * Update a node's data in the local editor state.
639
- * This should be called after updating the node in the global store to ensure
640
- * the visual representation is updated immediately (e.g., for nodeType changes).
635
+ * Called by App.svelte AFTER it has already updated the global store via
636
+ * workflowActions.updateNode(). We only need to update flowNodes for
637
+ * immediate visual feedback — no store sync needed.
641
638
  *
642
639
  * @param nodeId - The ID of the node to update
643
640
  * @param dataUpdates - Partial data updates to merge into the node's data
@@ -646,6 +643,8 @@
646
643
  nodeId: string,
647
644
  dataUpdates: Partial<WorkflowNodeType['data']>
648
645
  ): void {
646
+ machine.send('START_NODE_UPDATE');
647
+
649
648
  flowNodes = flowNodes.map((node) => {
650
649
  if (node.id === nodeId) {
651
650
  return {
@@ -658,6 +657,8 @@
658
657
  }
659
658
  return node;
660
659
  });
660
+
661
+ machine.send('UPDATE_COMPLETE');
661
662
  }
662
663
 
663
664
  /**
@@ -739,14 +740,14 @@
739
740
  <div class="flowdrop-workflow-editor__main">
740
741
  <!-- Flow Canvas -->
741
742
  <div class="flowdrop-canvas">
742
- <FlowDropZone ondrop={handleNodeDrop}>
743
+ <FlowDropZone ondrop={handleNodeDrop} onfiledrop={handleWorkflowFileDrop}>
743
744
  {#key svelteFlowKey}
744
745
  <SvelteFlow
745
746
  bind:nodes={flowNodes}
746
747
  bind:edges={flowEdges}
747
748
  {nodeTypes}
748
749
  {defaultEdgeOptions}
749
- onconnect={handleConnect}
750
+ onconnect={(connection) => void handleConnect({ source: connection.source, target: connection.target, sourceHandle: connection.sourceHandle ?? undefined, targetHandle: connection.targetHandle ?? undefined })}
750
751
  onbeforedelete={handleBeforeDelete}
751
752
  ondelete={handleNodesDelete}
752
753
  onnodedragstart={handleNodeDragStart}
@@ -760,18 +761,18 @@
760
761
  connectionLineComponent={ConnectionLine}
761
762
  {snapGrid}
762
763
  {initialViewport}
763
- colorMode={$resolvedTheme as ColorMode}
764
- fitView={$editorSettings.fitViewOnLoad}
764
+ colorMode={getResolvedTheme() as ColorMode}
765
+ fitView={getEditorSettings().fitViewOnLoad}
765
766
  >
766
767
  <Controls />
767
768
  <!-- Always render Background for consistent bg color in dark/light mode -->
768
769
  <Background
769
- gap={$editorSettings.gridSize}
770
+ gap={getEditorSettings().gridSize}
770
771
  bgColor="var(--fd-background)"
771
772
  variant={BackgroundVariant.Dots}
772
- patternColor={$editorSettings.showGrid ? undefined : 'transparent'}
773
+ patternColor={getEditorSettings().showGrid ? undefined : 'transparent'}
773
774
  />
774
- {#if $editorSettings.showMinimap}
775
+ {#if getEditorSettings().showMinimap}
775
776
  <MiniMap />
776
777
  {/if}
777
778
  </SvelteFlow>
@@ -787,8 +788,8 @@
787
788
  </FlowDropZone>
788
789
  </div>
789
790
 
790
- <!-- Status Bar -->
791
- <div class="flowdrop-status-bar">
791
+ <!-- Status Bar: aria-live announces dynamic changes (node/edge counts, cycle warnings) -->
792
+ <div class="flowdrop-status-bar" aria-live="polite" aria-atomic="true">
792
793
  <div class="flowdrop-status-bar__content">
793
794
  <div class="flowdrop-flex flowdrop-gap--4">
794
795
  <span class="flowdrop-text--xs flowdrop-text--gray">{flowNodes.length} nodes</span>
@@ -810,11 +811,14 @@
810
811
  </SvelteFlowProvider>
811
812
 
812
813
  <!-- Toast notifications container -->
813
- <Toaster
814
- position="bottom-center"
815
- containerClassName={FLOWDROP_TOASTER_CLASS}
816
- toastOptions={flowdropToastOptions}
817
- />
814
+ <!-- aria-live="polite" ensures screen readers announce toast messages without interrupting -->
815
+ <div aria-live="polite" aria-atomic="true">
816
+ <Toaster
817
+ position="bottom-center"
818
+ containerClassName={FLOWDROP_TOASTER_CLASS}
819
+ toastOptions={flowdropToastOptions}
820
+ />
821
+ </div>
818
822
 
819
823
  <style>
820
824
  .flowdrop-workflow-editor {
@@ -901,10 +905,6 @@
901
905
  cursor: pointer;
902
906
  }
903
907
 
904
- /* Enhanced arrow markers for input ports */
905
- :global(.flowdrop-workflow-editor .svelte-flow__edge-marker) {
906
- fill: currentColor;
907
- }
908
908
 
909
909
  /* Handle size/position only; colors come from inline --fd-handle-fill and base.css ::before */
910
910
  :global(.flowdrop-workflow-editor .svelte-flow__handle) {