@flowdrop/flowdrop 1.14.0 → 2.0.0-beta.1

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 (218) hide show
  1. package/CHANGELOG.md +475 -0
  2. package/MIGRATION-2.0.md +472 -0
  3. package/README.md +23 -23
  4. package/dist/adapters/WorkflowAdapter.d.ts +1 -1
  5. package/dist/adapters/WorkflowAdapter.js +14 -8
  6. package/dist/adapters/agentspec/AgentSpecAdapter.js +7 -7
  7. package/dist/chat/batchFeedback.d.ts +39 -0
  8. package/dist/chat/batchFeedback.js +51 -0
  9. package/dist/commands/executor.js +15 -1
  10. package/dist/commands/storeIntegration.svelte.d.ts +4 -1
  11. package/dist/commands/storeIntegration.svelte.js +26 -21
  12. package/dist/commands/types.d.ts +2 -0
  13. package/dist/components/App.svelte +162 -192
  14. package/dist/components/App.svelte.d.ts +47 -8
  15. package/dist/components/ConfigForm.svelte +110 -66
  16. package/dist/components/ConfigModal.svelte +7 -2
  17. package/dist/components/ConnectionLine.svelte +4 -2
  18. package/dist/components/Navbar.svelte +61 -1
  19. package/dist/components/NodeSidebar.svelte +27 -45
  20. package/dist/components/NodeStatusOverlay.svelte +94 -6
  21. package/dist/components/NodeSwapPicker.svelte +10 -8
  22. package/dist/components/PipelineStatus.svelte +16 -67
  23. package/dist/components/PortCoordinateTracker.svelte +5 -6
  24. package/dist/components/SchemaForm.stories.svelte +1 -3
  25. package/dist/components/SchemaForm.svelte +45 -40
  26. package/dist/components/SchemaForm.svelte.d.ts +0 -8
  27. package/dist/components/SettingsModal.svelte +8 -3
  28. package/dist/components/SettingsPanel.svelte +20 -4
  29. package/dist/components/SwapMappingEditor.svelte +67 -49
  30. package/dist/components/SwapMappingEditor.svelte.d.ts +0 -2
  31. package/dist/components/UniversalNode.svelte +9 -7
  32. package/dist/components/WorkflowEditor.svelte +118 -111
  33. package/dist/components/WorkflowEditor.svelte.d.ts +18 -10
  34. package/dist/components/chat/AIChatPanel.svelte +93 -89
  35. package/dist/components/chat/AIChatPanel.svelte.d.ts +0 -4
  36. package/dist/components/chat/CommandPreview.svelte +2 -1
  37. package/dist/components/console/CommandConsole.svelte +7 -5
  38. package/dist/components/console/ConsoleAutocomplete.svelte +10 -11
  39. package/dist/components/console/ConsoleAutocomplete.svelte.d.ts +6 -0
  40. package/dist/components/console/ConsoleInput.svelte +15 -6
  41. package/dist/components/console/ConsoleOutput.svelte +2 -1
  42. package/dist/components/form/FormArray.svelte +5 -9
  43. package/dist/components/form/FormArray.svelte.d.ts +2 -1
  44. package/dist/components/form/FormAutocomplete.svelte +29 -13
  45. package/dist/components/form/FormField.svelte +4 -2
  46. package/dist/components/form/FormFieldLight.svelte +4 -2
  47. package/dist/components/form/FormMarkdownEditor.svelte +9 -4
  48. package/dist/components/form/FormRangeField.svelte +1 -0
  49. package/dist/components/form/FormTemplateEditor.svelte +11 -3
  50. package/dist/components/form/FormToggle.svelte +5 -12
  51. package/dist/components/form/FormToggle.svelte.d.ts +4 -2
  52. package/dist/components/form/templateAutocomplete.js +1 -5
  53. package/dist/components/form/types.d.ts +1 -14
  54. package/dist/components/interrupt/FormPrompt.svelte +3 -2
  55. package/dist/components/interrupt/InterruptBubble.svelte +16 -17
  56. package/dist/components/interrupt/ReviewPrompt.svelte +10 -3
  57. package/dist/components/interrupt/TextInputPrompt.svelte +2 -1
  58. package/dist/components/layouts/MainLayout.svelte +20 -13
  59. package/dist/components/layouts/MainLayout.svelte.d.ts +4 -0
  60. package/dist/components/nodes/AtomNode.svelte +292 -0
  61. package/dist/components/nodes/AtomNode.svelte.d.ts +26 -0
  62. package/dist/components/nodes/GatewayNode.svelte +19 -10
  63. package/dist/components/nodes/IdeaNode.svelte +7 -0
  64. package/dist/components/nodes/SimpleNode.svelte +11 -6
  65. package/dist/components/nodes/SquareNode.svelte +15 -8
  66. package/dist/components/nodes/TerminalNode.svelte +9 -4
  67. package/dist/components/nodes/ToolNode.svelte +7 -1
  68. package/dist/components/nodes/WorkflowNode.svelte +16 -7
  69. package/dist/components/playground/ChatInput.svelte +11 -14
  70. package/dist/components/playground/ChatPanel.svelte +6 -49
  71. package/dist/components/playground/ChatPanel.svelte.d.ts +0 -14
  72. package/dist/components/playground/ControlPanel.svelte +134 -123
  73. package/dist/components/playground/ControlPanel.svelte.d.ts +3 -0
  74. package/dist/components/playground/ExecutionLogs.svelte +11 -9
  75. package/dist/components/playground/InputCollector.svelte +11 -9
  76. package/dist/components/playground/MessageStream.svelte +17 -23
  77. package/dist/components/playground/PipelineKanbanView.svelte +65 -6
  78. package/dist/components/playground/PipelinePanel.svelte +11 -5
  79. package/dist/components/playground/PipelineTableView.svelte +186 -44
  80. package/dist/components/playground/Playground.svelte +95 -92
  81. package/dist/components/playground/Playground.svelte.d.ts +2 -0
  82. package/dist/components/playground/PlaygroundApp.svelte +6 -1
  83. package/dist/components/playground/PlaygroundApp.svelte.d.ts +3 -0
  84. package/dist/components/playground/PlaygroundModal.svelte +13 -3
  85. package/dist/components/playground/PlaygroundModal.svelte.d.ts +3 -0
  86. package/dist/components/playground/PlaygroundStudio.svelte +34 -32
  87. package/dist/components/playground/PlaygroundStudio.svelte.d.ts +3 -0
  88. package/dist/components/playground/SessionManager.svelte +9 -12
  89. package/dist/components/playground/pipelineViewUtils.svelte.d.ts +28 -0
  90. package/dist/components/playground/pipelineViewUtils.svelte.js +38 -1
  91. package/dist/config/endpoints.d.ts +0 -7
  92. package/dist/config/endpoints.js +2 -10
  93. package/dist/core/index.d.ts +4 -4
  94. package/dist/core/index.js +6 -6
  95. package/dist/display/index.d.ts +0 -2
  96. package/dist/display/index.js +0 -6
  97. package/dist/editor/index.d.ts +19 -20
  98. package/dist/editor/index.js +25 -35
  99. package/dist/form/code.d.ts +25 -15
  100. package/dist/form/code.js +44 -41
  101. package/dist/form/fieldRegistry.d.ts +17 -13
  102. package/dist/form/fieldRegistry.js +32 -12
  103. package/dist/form/full.d.ts +17 -13
  104. package/dist/form/full.js +22 -27
  105. package/dist/form/index.d.ts +3 -3
  106. package/dist/form/index.js +3 -3
  107. package/dist/form/markdown.d.ts +13 -8
  108. package/dist/form/markdown.js +22 -23
  109. package/dist/helpers/proximityConnect.d.ts +7 -3
  110. package/dist/helpers/proximityConnect.js +19 -6
  111. package/dist/helpers/workflowEditorHelper.d.ts +12 -5
  112. package/dist/helpers/workflowEditorHelper.js +27 -25
  113. package/dist/index.d.ts +28 -24
  114. package/dist/index.js +27 -50
  115. package/dist/messages/defaults.d.ts +2 -5
  116. package/dist/messages/defaults.js +3 -6
  117. package/dist/messages/index.d.ts +0 -1
  118. package/dist/messages/index.js +0 -1
  119. package/dist/mocks/app-forms.d.ts +6 -2
  120. package/dist/mocks/app-forms.js +11 -4
  121. package/dist/openapi/v1/openapi.yaml +227 -164
  122. package/dist/playground/index.d.ts +2 -3
  123. package/dist/playground/index.js +2 -30
  124. package/dist/playground/mount.d.ts +15 -0
  125. package/dist/playground/mount.js +46 -20
  126. package/dist/registry/{BaseRegistry.d.ts → BaseRegistry.svelte.d.ts} +22 -1
  127. package/dist/registry/{BaseRegistry.js → BaseRegistry.svelte.js} +37 -1
  128. package/dist/registry/builtinFormats.d.ts +9 -18
  129. package/dist/registry/builtinFormats.js +9 -39
  130. package/dist/registry/builtinNodes.d.ts +1 -26
  131. package/dist/registry/builtinNodes.js +14 -50
  132. package/dist/registry/index.d.ts +3 -4
  133. package/dist/registry/index.js +4 -6
  134. package/dist/registry/nodeComponentRegistry.d.ts +182 -15
  135. package/dist/registry/nodeComponentRegistry.js +235 -17
  136. package/dist/registry/workflowFormatRegistry.d.ts +14 -9
  137. package/dist/registry/workflowFormatRegistry.js +24 -8
  138. package/dist/{schema → schemas}/index.d.ts +2 -2
  139. package/dist/{schema → schemas}/index.js +2 -2
  140. package/dist/schemas/v1/workflow.schema.json +53 -6
  141. package/dist/services/agentSpecExecutionService.js +0 -1
  142. package/dist/services/apiVariableService.d.ts +2 -1
  143. package/dist/services/apiVariableService.js +5 -22
  144. package/dist/services/autoSaveService.d.ts +7 -0
  145. package/dist/services/autoSaveService.js +6 -4
  146. package/dist/services/chatService.d.ts +8 -4
  147. package/dist/services/chatService.js +15 -15
  148. package/dist/services/draftStorage.d.ts +129 -13
  149. package/dist/services/draftStorage.js +185 -37
  150. package/dist/services/dynamicSchemaService.d.ts +2 -1
  151. package/dist/services/dynamicSchemaService.js +5 -22
  152. package/dist/services/globalSave.d.ts +13 -12
  153. package/dist/services/globalSave.js +29 -51
  154. package/dist/services/historyService.d.ts +9 -3
  155. package/dist/services/historyService.js +9 -3
  156. package/dist/services/interruptService.d.ts +14 -9
  157. package/dist/services/interruptService.js +27 -27
  158. package/dist/services/nodeExecutionService.d.ts +18 -3
  159. package/dist/services/nodeExecutionService.js +71 -45
  160. package/dist/services/playgroundService.d.ts +14 -9
  161. package/dist/services/playgroundService.js +31 -30
  162. package/dist/services/variableService.d.ts +2 -1
  163. package/dist/services/variableService.js +2 -2
  164. package/dist/services/workflowStorage.js +6 -6
  165. package/dist/stores/apiContext.d.ts +45 -0
  166. package/dist/stores/apiContext.js +65 -0
  167. package/dist/stores/categoriesStore.svelte.d.ts +28 -23
  168. package/dist/stores/categoriesStore.svelte.js +70 -64
  169. package/dist/stores/getInstance.svelte.d.ts +39 -0
  170. package/dist/stores/getInstance.svelte.js +65 -0
  171. package/dist/stores/historyStore.svelte.d.ts +77 -93
  172. package/dist/stores/historyStore.svelte.js +134 -160
  173. package/dist/stores/instanceContainer.svelte.d.ts +111 -0
  174. package/dist/stores/instanceContainer.svelte.js +114 -0
  175. package/dist/stores/interruptStore.svelte.d.ts +112 -82
  176. package/dist/stores/interruptStore.svelte.js +253 -226
  177. package/dist/stores/pipelinePanelStore.svelte.d.ts +27 -3
  178. package/dist/stores/pipelinePanelStore.svelte.js +61 -14
  179. package/dist/stores/playgroundStore.svelte.d.ts +169 -216
  180. package/dist/stores/playgroundStore.svelte.js +515 -572
  181. package/dist/stores/portCoordinateStore.svelte.d.ts +57 -51
  182. package/dist/stores/portCoordinateStore.svelte.js +109 -98
  183. package/dist/stores/settingsStore.svelte.d.ts +4 -1
  184. package/dist/stores/settingsStore.svelte.js +47 -12
  185. package/dist/stores/workflowStore.svelte.d.ts +178 -213
  186. package/dist/stores/workflowStore.svelte.js +449 -501
  187. package/dist/stories/EdgeDecorator.svelte +5 -2
  188. package/dist/stories/NodeDecorator.svelte +5 -3
  189. package/dist/svelte-app.d.ts +60 -10
  190. package/dist/svelte-app.js +157 -53
  191. package/dist/types/events.d.ts +6 -3
  192. package/dist/types/index.d.ts +71 -6
  193. package/dist/types/navbar.d.ts +7 -0
  194. package/dist/types/playground.d.ts +18 -3
  195. package/dist/types/settings.d.ts +13 -0
  196. package/dist/types/settings.js +1 -0
  197. package/dist/utils/colors.d.ts +47 -21
  198. package/dist/utils/colors.js +69 -68
  199. package/dist/utils/connections.d.ts +9 -15
  200. package/dist/utils/connections.js +13 -32
  201. package/dist/utils/duration.d.ts +13 -0
  202. package/dist/utils/duration.js +45 -0
  203. package/dist/utils/formMerge.d.ts +36 -0
  204. package/dist/utils/formMerge.js +70 -0
  205. package/dist/utils/icons.d.ts +5 -2
  206. package/dist/utils/icons.js +6 -5
  207. package/dist/utils/nodeSwap.d.ts +6 -2
  208. package/dist/utils/nodeSwap.js +62 -126
  209. package/dist/utils/nodeTypes.d.ts +17 -8
  210. package/dist/utils/nodeTypes.js +27 -19
  211. package/dist/utils/performanceUtils.js +7 -0
  212. package/package.json +6 -5
  213. package/dist/messages/deprecation.d.ts +0 -20
  214. package/dist/messages/deprecation.js +0 -33
  215. package/dist/registry/plugin.d.ts +0 -215
  216. package/dist/registry/plugin.js +0 -249
  217. package/dist/services/api.d.ts +0 -129
  218. package/dist/services/api.js +0 -217
@@ -18,15 +18,7 @@
18
18
  import NodeSwapPicker from './NodeSwapPicker.svelte';
19
19
  import SwapMappingEditor from './SwapMappingEditor.svelte';
20
20
  import Navbar from './Navbar.svelte';
21
- import { api, setEndpointConfig } from '../services/api.js';
22
- import { EnhancedFlowDropApiClient } from '../api/enhanced-client.js';
23
- import type {
24
- NodeMetadata,
25
- Workflow,
26
- WorkflowNode,
27
- ConfigSchema,
28
- NodeUIExtensions
29
- } from '../types/index.js';
21
+ import type { NodeMetadata, Workflow, WorkflowNode, ConfigSchema } from '../types/index.js';
30
22
  import type { InteractiveSwapState, SwapEventContext } from '../utils/nodeSwap.js';
31
23
  import {
32
24
  computeInteractiveState,
@@ -44,24 +36,16 @@
44
36
  import type { FlowDropTheme, FlowDropThemeName } from '../types/theme.js';
45
37
  import type { FlowDropSkinTokens } from '../types/skin.js';
46
38
  import { resolveTheme } from '../themes/index.js';
47
- import {
48
- getWorkflowStore,
49
- workflowActions,
50
- getWorkflowName,
51
- getWorkflowFormat,
52
- markAsSaved
53
- } from '../stores/workflowStore.svelte.js';
39
+ import { provideInstance } from '../stores/getInstance.svelte.js';
40
+ import type { FlowDropInstance } from '../stores/instanceContainer.svelte.js';
54
41
  import { globalSaveWorkflow, globalExportWorkflow } from '../services/globalSave.js';
55
42
  import { apiToasts, dismissToast } from '../services/toastService.js';
56
43
  import { initAutoSave } from '../services/autoSaveService.js';
57
- import { getUiSettings, updateSettings } from '../stores/settingsStore.svelte.js';
58
44
  import {
59
- initializePortCompatibility,
60
- getPortCompatibilityChecker,
61
- isPortCompatibilityInitialized
62
- } from '../utils/connections.js';
63
- import { DEFAULT_PORT_CONFIG } from '../config/defaultPortConfig.js';
64
- import { workflowFormatRegistry } from '../registry/workflowFormatRegistry.js';
45
+ getUiSettings,
46
+ updateSettings,
47
+ initializeTheme
48
+ } from '../stores/settingsStore.svelte.js';
65
49
  import { logger } from '../utils/logger.js';
66
50
  import { validateWorkflowData } from '../utils/validation.js';
67
51
  import type { SettingsCategory } from '../types/settings.js';
@@ -84,14 +68,33 @@
84
68
  showNavbar?: boolean;
85
69
  /** Disable the node sidebar */
86
70
  disableSidebar?: boolean;
87
- /** Lock the workflow (prevent changes) */
88
- lockWorkflow?: boolean;
89
- /** Read-only mode */
90
- readOnly?: boolean;
91
- /** Node execution statuses */
92
- nodeStatuses?: Record<string, 'pending' | 'running' | 'completed' | 'error'>;
71
+ /**
72
+ * Editor interaction mode. Replaces the former `readOnly` + `lockWorkflow`
73
+ * boolean pair (2.0 breaking change).
74
+ *
75
+ * | mode | node drag / connect / select | proximity-connect | node swap | bottom console panel + toggle |
76
+ * |--------------|------------------------------|-------------------|-----------|-------------------------------|
77
+ * | `'edit'` | enabled | enabled | enabled | available |
78
+ * | `'readonly'` | disabled | disabled | disabled | hidden |
79
+ * | `'locked'` | disabled | disabled | disabled | hidden |
80
+ *
81
+ * In 1.x `readOnly` and `lockWorkflow` gated the exact same set of
82
+ * interactions and were always combined as `!readOnly && !lockWorkflow`,
83
+ * so any combination of the two booleans collapsed to "edit" (both false)
84
+ * or "disabled" (either true). `'readonly'` and `'locked'` therefore behave
85
+ * identically today; the two names are kept as distinct intents so future
86
+ * versions can differentiate them without another breaking change.
87
+ *
88
+ * Migration: `readOnly` → `mode="readonly"`; `lockWorkflow` →
89
+ * `mode="locked"`; both `false` (or unset) → `mode="edit"` (the default).
90
+ *
91
+ * @default 'edit'
92
+ */
93
+ mode?: 'edit' | 'readonly' | 'locked';
93
94
  /** Pipeline ID for fetching node execution info */
94
95
  pipelineId?: string;
96
+ /** Increments to force a refresh of pipeline node status from the server */
97
+ refreshTrigger?: number;
95
98
  /** Custom navbar title */
96
99
  navbarTitle?: string;
97
100
  /** Custom navbar actions */
@@ -104,14 +107,31 @@
104
107
  }>;
105
108
  /** Show settings gear icon in navbar */
106
109
  showSettings?: boolean;
110
+ /** Show the "Connected" status indicator in the navbar (default: true) */
111
+ showStatus?: boolean;
107
112
  /** API base URL */
108
113
  apiBaseUrl?: string;
109
114
  /** Endpoint configuration */
110
115
  endpointConfig?: EndpointConfig;
111
116
  /** Authentication provider */
112
117
  authProvider?: AuthProvider;
113
- /** Event handlers */
114
- eventHandlers?: FlowDropEventHandlers;
118
+ /**
119
+ * Called before save — return false to cancel. Forwarded to the save
120
+ * pipeline. (Flattened from the former `eventHandlers` object in 2.0.)
121
+ */
122
+ onBeforeSave?: FlowDropEventHandlers['onBeforeSave'];
123
+ /** Called after a successful save. */
124
+ onAfterSave?: FlowDropEventHandlers['onAfterSave'];
125
+ /** Called when a save fails. */
126
+ onSaveError?: FlowDropEventHandlers['onSaveError'];
127
+ /** Called on any API error — return true to suppress the default toast. */
128
+ onApiError?: FlowDropEventHandlers['onApiError'];
129
+ /** Called after a workflow is loaded/imported. */
130
+ onWorkflowLoad?: FlowDropEventHandlers['onWorkflowLoad'];
131
+ /** Called before a node swap — return false to cancel. */
132
+ onBeforeSwap?: FlowDropEventHandlers['onBeforeSwap'];
133
+ /** Called after a node swap is applied. */
134
+ onAfterSwap?: FlowDropEventHandlers['onAfterSwap'];
115
135
  /** Feature configuration */
116
136
  features?: FlowDropFeatures;
117
137
  /** Visual theme — named built-in ('default' | 'minimal') or custom theme object */
@@ -138,6 +158,8 @@
138
158
  * function call you'd rather not invoke unless the prop is actually read.
139
159
  */
140
160
  messages?: MessagesOverride | (() => MessagesOverride);
161
+ /** Per-instance state container (created by mount functions). Defaults to the page-default instance. */
162
+ instance?: FlowDropInstance;
141
163
  }
142
164
 
143
165
  let {
@@ -147,17 +169,23 @@
147
169
  width = '100%',
148
170
  showNavbar = false,
149
171
  disableSidebar = false,
150
- lockWorkflow = false,
151
- readOnly = false,
152
- nodeStatuses = {},
172
+ mode = 'edit',
153
173
  pipelineId,
174
+ refreshTrigger = 0,
154
175
  navbarTitle,
155
176
  navbarActions = [],
156
177
  showSettings = true,
178
+ showStatus = true,
157
179
  apiBaseUrl,
158
180
  endpointConfig: propEndpointConfig,
159
181
  authProvider,
160
- eventHandlers,
182
+ onBeforeSave,
183
+ onAfterSave,
184
+ onSaveError,
185
+ onApiError,
186
+ onWorkflowLoad,
187
+ onBeforeSwap,
188
+ onAfterSwap,
161
189
  features: propFeatures,
162
190
  theme: themeProp,
163
191
  settingsCategories,
@@ -165,12 +193,25 @@
165
193
  showSettingsResetButton,
166
194
  swapStrategies,
167
195
  workflowSettingsSchema,
168
- messages: messagesOverride
196
+ messages: messagesOverride,
197
+ instance
169
198
  }: Props = $props();
170
199
 
171
- // svelte-ignore state_referenced_locally feature flags don't change at runtime
200
+ // Resolve (and provide to children) the per-instance state container.
201
+ // Must run during component init — provideInstance reads/sets Svelte context.
202
+ // The instance never changes for a mounted component, so capturing it once is correct.
203
+ // svelte-ignore state_referenced_locally
204
+ const fd = provideInstance(instance);
205
+
206
+ // feature flags don't change at runtime
207
+ // svelte-ignore state_referenced_locally
172
208
  const features = mergeFeatures(propFeatures);
173
209
 
210
+ // `mode` is the public API; internally the canvas only cares whether editing
211
+ // is disabled. 'readonly' and 'locked' both disable the same interactions
212
+ // (see the `mode` prop JSDoc for the full matrix).
213
+ const canvasEditable = $derived(mode === 'edit');
214
+
174
215
  // Messages: merge consumer overrides over defaults; expose via context as a
175
216
  // getter so consumer-side reactivity (e.g. paraglide-js locale switches)
176
217
  // propagates into every child without a subscription. Accepts either a
@@ -271,7 +312,7 @@
271
312
  return navbarTitle;
272
313
  }
273
314
  // Default workflow title logic
274
- const wfName = getWorkflowName();
315
+ const wfName = fd.workflow.name;
275
316
  if (!wfName || wfName === 'Untitled Workflow') {
276
317
  return 'Workflow / New Workflow';
277
318
  }
@@ -285,12 +326,6 @@
285
326
  let error = $state<string | null>(null);
286
327
  let endpointConfig = $state<EndpointConfig | null>(null);
287
328
 
288
- /**
289
- * Enhanced API client with authProvider support
290
- * Used when authProvider is provided; otherwise falls back to legacy api service
291
- */
292
- let apiClient = $state<EnhancedFlowDropApiClient | null>(null);
293
-
294
329
  // ConfigSidebar state
295
330
  let isConfigSidebarOpen = $state(false);
296
331
  let selectedNodeId = $state<string | null>(null);
@@ -300,7 +335,6 @@
300
335
 
301
336
  // Node swap state
302
337
  let swapMode = $state<'idle' | 'picking' | 'mapping'>('idle');
303
- let swapTargetMetadata = $state<NodeMetadata | null>(null);
304
338
  let swapInteractiveState = $state<InteractiveSwapState | null>(null);
305
339
 
306
340
  // Built-in workflow settings field names — consumer schemas must not reuse these.
@@ -342,7 +376,7 @@
342
376
  type: 'string',
343
377
  title: 'Workflow Format',
344
378
  description: 'The specification format for this workflow',
345
- oneOf: workflowFormatRegistry.getOneOfOptions(),
379
+ oneOf: fd.formats.getOneOfOptions(),
346
380
  default: 'flowdrop'
347
381
  },
348
382
  ...extraProps
@@ -353,15 +387,15 @@
353
387
 
354
388
  // Workflow configuration values
355
389
  let workflowConfigValues = $derived({
356
- name: getWorkflowName() || '',
357
- description: getWorkflowStore()?.description || '',
358
- format: getWorkflowStore()?.metadata?.format || 'flowdrop',
359
- ...(getWorkflowStore()?.config ?? {})
390
+ name: fd.workflow.name || '',
391
+ description: fd.workflow.current?.description || '',
392
+ format: fd.workflow.current?.metadata?.format || 'flowdrop',
393
+ ...(fd.workflow.current?.config ?? {})
360
394
  });
361
395
 
362
396
  // Get the current node from the workflow store
363
397
  let selectedNodeForConfig = $derived.by(() => {
364
- const wf = getWorkflowStore();
398
+ const wf = fd.workflow.current;
365
399
  if (!selectedNodeId || !wf) return null;
366
400
  return wf.nodes.find((node) => node.id === selectedNodeId) || null;
367
401
  });
@@ -379,7 +413,7 @@
379
413
  // If nodes were provided as props, use them directly (skip API fetch)
380
414
  if (propNodes && propNodes.length > 0) {
381
415
  // Merge format-provided nodes with prop nodes (deduplicate by ID, props take priority)
382
- const formatNodes = workflowFormatRegistry.getAllFormatNodes();
416
+ const formatNodes = fd.formats.getAllFormatNodes();
383
417
  const existingIds = new Set(propNodes.map((n) => n.id));
384
418
  const uniqueFormatNodes = formatNodes.filter((n) => !existingIds.has(n.id));
385
419
  nodes = [...propNodes, ...uniqueFormatNodes];
@@ -392,16 +426,11 @@
392
426
  try {
393
427
  error = null;
394
428
 
395
- // Use enhanced client with authProvider if available, otherwise fall back to legacy api
396
- let fetchedNodes: NodeMetadata[];
397
- if (apiClient) {
398
- fetchedNodes = await apiClient.getAvailableNodes();
399
- } else {
400
- fetchedNodes = await api.nodes.getNodes();
401
- }
429
+ // Fetch via this instance's API client (configured in initializeApiEndpoints).
430
+ const fetchedNodes: NodeMetadata[] = await fd.api.client.getAvailableNodes();
402
431
 
403
432
  // Merge format-provided nodes with API nodes (deduplicate by ID, API takes priority)
404
- const formatNodes = workflowFormatRegistry.getAllFormatNodes();
433
+ const formatNodes = fd.formats.getAllFormatNodes();
405
434
  const existingIds = new Set(fetchedNodes.map((n) => n.id));
406
435
  const uniqueFormatNodes = formatNodes.filter((n) => !existingIds.has(n.id));
407
436
  nodes = [...fetchedNodes, ...uniqueFormatNodes];
@@ -421,8 +450,8 @@
421
450
  const errorMessage = err instanceof Error ? err.message : 'Unknown error';
422
451
 
423
452
  // Notify parent via event handler
424
- if (eventHandlers?.onApiError) {
425
- const suppressToast = eventHandlers.onApiError(
453
+ if (onApiError) {
454
+ const suppressToast = onApiError(
426
455
  err instanceof Error ? err : new Error(errorMessage),
427
456
  'fetchNodes'
428
457
  );
@@ -481,28 +510,15 @@
481
510
  async function initializeApiEndpoints(): Promise<void> {
482
511
  // First priority: Use endpointConfig prop if provided (from mountFlowDropApp)
483
512
  if (propEndpointConfig) {
484
- setEndpointConfig(propEndpointConfig);
485
- endpointConfig = propEndpointConfig;
486
-
487
- // Create enhanced API client with authProvider support if provided
488
- if (authProvider) {
489
- apiClient = new EnhancedFlowDropApiClient(propEndpointConfig, authProvider);
490
- }
513
+ configureApi(propEndpointConfig);
491
514
  return;
492
515
  }
493
516
 
494
- // Second priority: Check if endpoint config is already set (e.g., by parent layout)
495
- const { getEndpointConfig } = await import('../services/api.js');
496
- const existingConfig = getEndpointConfig();
497
-
498
- // If config already exists and no override provided, use existing
517
+ // Second priority: Reuse this instance's existing config (e.g. set by a
518
+ // parent layout that already configured fd.api) when no override is given.
519
+ const existingConfig = fd.api.config;
499
520
  if (existingConfig && !apiBaseUrl) {
500
- endpointConfig = existingConfig;
501
-
502
- // Create enhanced API client with authProvider support if provided
503
- if (authProvider) {
504
- apiClient = new EnhancedFlowDropApiClient(existingConfig, authProvider);
505
- }
521
+ configureApi(existingConfig);
506
522
  return;
507
523
  }
508
524
 
@@ -510,9 +526,6 @@
510
526
  const baseUrl = apiBaseUrl || '/api/flowdrop';
511
527
 
512
528
  const config = createEndpointConfig(baseUrl, {
513
- auth: {
514
- type: 'none' // No authentication for now
515
- },
516
529
  timeout: 10000, // 10 second timeout
517
530
  retry: {
518
531
  enabled: true,
@@ -522,14 +535,16 @@
522
535
  }
523
536
  });
524
537
 
525
- setEndpointConfig(config);
526
- // Store the configuration for passing to WorkflowEditor
527
- endpointConfig = config;
538
+ configureApi(config);
539
+ }
528
540
 
529
- // Create enhanced API client with authProvider support if provided
530
- if (authProvider) {
531
- apiClient = new EnhancedFlowDropApiClient(config, authProvider);
532
- }
541
+ /**
542
+ * Configure this instance's ApiContext and mirror its config/client into the
543
+ * local state used by child components and the save service.
544
+ */
545
+ function configureApi(config: EndpointConfig): void {
546
+ fd.api.configure(config, authProvider);
547
+ endpointConfig = config;
533
548
  }
534
549
 
535
550
  /**
@@ -545,7 +560,6 @@
545
560
  isConfigSidebarOpen = true;
546
561
  // Reset swap state when switching nodes
547
562
  swapMode = 'idle';
548
- swapTargetMetadata = null;
549
563
  swapInteractiveState = null;
550
564
  }
551
565
 
@@ -554,7 +568,6 @@
554
568
  selectedNodeId = null;
555
569
  // Reset swap state when closing
556
570
  swapMode = 'idle';
557
- swapTargetMetadata = null;
558
571
  swapInteractiveState = null;
559
572
  }
560
573
 
@@ -574,7 +587,6 @@
574
587
  */
575
588
  function startSwap(): void {
576
589
  swapMode = 'picking';
577
- swapTargetMetadata = null;
578
590
  swapInteractiveState = null;
579
591
  }
580
592
 
@@ -585,29 +597,21 @@
585
597
  const node = selectedNodeForConfig;
586
598
  if (!node) return;
587
599
 
588
- const wf = getWorkflowStore();
600
+ const wf = fd.workflow.current;
589
601
  if (!wf) return;
590
602
 
591
603
  // Format compatibility guard — defence-in-depth behind picker's own filter
592
- const currentFormat = getWorkflowFormat();
604
+ const currentFormat = fd.workflow.format;
593
605
  if (metadata.formats?.length && !metadata.formats.includes(currentFormat)) {
594
606
  return;
595
607
  }
596
608
 
597
- // Get port compatibility checker (may be null if not initialized)
598
- let checker: import('../utils/connections.js').PortCompatibilityChecker | null = null;
599
- try {
600
- checker = getPortCompatibilityChecker();
601
- } catch {
602
- // Checker not initialized — computeSwapPreview will use exact dataType matching
603
- }
604
-
609
+ // Port compatibility comes from this instance's checker.
605
610
  const interactive = computeInteractiveState(node, metadata, wf.edges, wf.nodes, {
606
- checker,
611
+ checker: fd.portCompatibility,
607
612
  strategies: swapStrategies
608
613
  });
609
614
 
610
- swapTargetMetadata = metadata;
611
615
  swapInteractiveState = interactive;
612
616
  swapMode = 'mapping';
613
617
  }
@@ -619,7 +623,7 @@
619
623
  const state = finalState ?? swapInteractiveState;
620
624
  if (!state) return;
621
625
 
622
- const wf = getWorkflowStore();
626
+ const wf = fd.workflow.current;
623
627
  if (!wf) return;
624
628
 
625
629
  const oldLabel = state.oldNode.data.label;
@@ -639,7 +643,7 @@
639
643
  }
640
644
 
641
645
  // onBeforeSwap hook — abort if returns false
642
- if (eventHandlers?.onBeforeSwap) {
646
+ if (onBeforeSwap) {
643
647
  const swapEventCtx: SwapEventContext = {
644
648
  oldNode: state.oldNode,
645
649
  newMetadata: state.newMetadata,
@@ -647,21 +651,21 @@
647
651
  portOverrides: [],
648
652
  configOverrides: []
649
653
  };
650
- const shouldProceed = await eventHandlers.onBeforeSwap(swapEventCtx);
654
+ const shouldProceed = await onBeforeSwap(swapEventCtx);
651
655
  if (shouldProceed === false) return;
652
656
  }
653
657
 
654
658
  // Apply as a single atomic swap with descriptive history entry
655
- workflowActions.swapNode({
659
+ fd.workflow.swapNode({
656
660
  nodes: result.updatedNodes,
657
661
  edges: result.updatedEdges,
658
662
  description: `Swap node: ${oldLabel} → ${newLabel}`
659
663
  });
660
664
 
661
665
  // onAfterSwap hook (fire-and-forget — swap is already applied)
662
- if (eventHandlers?.onAfterSwap) {
666
+ if (onAfterSwap) {
663
667
  try {
664
- eventHandlers.onAfterSwap(result, state.oldNode, state.newNodeId);
668
+ onAfterSwap(result, state.oldNode, state.newNodeId);
665
669
  } catch (err) {
666
670
  logger.error('onAfterSwap hook error:', err);
667
671
  }
@@ -673,7 +677,6 @@
673
677
 
674
678
  // Reset swap state
675
679
  swapMode = 'idle';
676
- swapTargetMetadata = null;
677
680
  swapInteractiveState = null;
678
681
 
679
682
  // Wait for SvelteFlow to process the new node before updating visual state
@@ -694,35 +697,9 @@
694
697
  */
695
698
  function cancelSwap(): void {
696
699
  swapMode = 'idle';
697
- swapTargetMetadata = null;
698
700
  swapInteractiveState = null;
699
701
  }
700
702
 
701
- /**
702
- * Handle workflow configuration save
703
- */
704
- async function handleWorkflowSave(config: Record<string, unknown>): Promise<void> {
705
- // Update the workflow store
706
- if (getWorkflowStore()) {
707
- workflowActions.batchUpdate({
708
- name: config.name as string | undefined,
709
- description: config.description as string | undefined
710
- });
711
- }
712
-
713
- // Close the sidebar
714
- isWorkflowSettingsOpen = false;
715
-
716
- // Also save the workflow to the backend
717
- try {
718
- await saveWorkflow();
719
- } catch (error) {
720
- logger.error('Failed to save workflow to backend:', error);
721
- // Note: We don't throw the error here to avoid breaking the UI flow
722
- // The user can still manually save via the main Save button if needed
723
- }
724
- }
725
-
726
703
  /**
727
704
  * Save workflow - thin wrapper that delegates to globalSaveWorkflow().
728
705
  *
@@ -731,10 +708,10 @@
731
708
  */
732
709
  async function saveWorkflow(): Promise<void> {
733
710
  await globalSaveWorkflow({
734
- apiClient: apiClient ?? undefined,
735
- eventHandlers,
711
+ eventHandlers: { onBeforeSave, onAfterSave, onSaveError, onApiError },
736
712
  features,
737
- onMarkAsSaved: markAsSaved
713
+ instance: fd,
714
+ onMarkAsSaved: () => fd.workflow.markAsSaved()
738
715
  });
739
716
  }
740
717
 
@@ -745,7 +722,7 @@
745
722
  * in globalSave.ts — the single source of truth.
746
723
  */
747
724
  async function exportWorkflow(): Promise<void> {
748
- await globalExportWorkflow({ features });
725
+ await globalExportWorkflow({ features, instance: fd });
749
726
  }
750
727
 
751
728
  /**
@@ -770,12 +747,12 @@
770
747
  logger.warn('Workflow import validation failed:', validation.error);
771
748
  return;
772
749
  }
773
- workflowActions.initialize(data as Workflow);
750
+ fd.workflow.initialize(data as Workflow);
774
751
  if (features.showToasts) {
775
752
  apiToasts.success('Import workflow', 'Workflow imported successfully');
776
753
  }
777
- if (eventHandlers?.onWorkflowLoad) {
778
- eventHandlers.onWorkflowLoad(data as Workflow);
754
+ if (onWorkflowLoad) {
755
+ onWorkflowLoad(data as Workflow);
779
756
  }
780
757
  } catch (error) {
781
758
  const errorObj = error instanceof Error ? error : new Error('Unknown error occurred');
@@ -822,26 +799,31 @@
822
799
 
823
800
  // Load node types on mount
824
801
  onMount(() => {
802
+ // Apply the persisted theme preference to the document and wire its
803
+ // reactivity. Idempotent — a no-op when mountFlowDropApp already
804
+ // initialized it; load-bearing for hosts rendering <App> directly,
805
+ // where nothing else applies data-theme (the persisted light/dark
806
+ // choice was otherwise never restored on reload).
807
+ initializeTheme();
808
+
825
809
  (async () => {
826
810
  try {
827
811
  await initializeApiEndpoints();
828
812
 
829
- // Ensure port compatibility checker is initialized (needed for proximity connect, etc.)
830
- // mountFlowDropApp initializes this before mounting, but SvelteKit routes need it here.
831
- // Only initialize with defaults if not already set preserves custom port configs.
832
- if (!isPortCompatibilityInitialized()) {
833
- initializePortCompatibility(DEFAULT_PORT_CONFIG);
834
- }
813
+ // The instance's port compatibility checker is seeded with
814
+ // DEFAULT_PORT_CONFIG at construction; mountFlowDropApp re-initializes
815
+ // it from the backend's port config. SvelteKit routes that render
816
+ // <App> directly keep the defaults (no separate fetch here).
835
817
 
836
818
  await fetchNodeTypes();
837
819
 
838
820
  // Initialize the workflow store
839
821
  if (initialWorkflow) {
840
- workflowActions.initialize(initialWorkflow);
822
+ fd.workflow.initialize(initialWorkflow);
841
823
 
842
824
  // Emit onWorkflowLoad event
843
- if (eventHandlers?.onWorkflowLoad) {
844
- eventHandlers.onWorkflowLoad(initialWorkflow);
825
+ if (onWorkflowLoad) {
826
+ onWorkflowLoad(initialWorkflow);
845
827
  }
846
828
  } else {
847
829
  // Initialize with a default empty workflow so the editor is functional
@@ -852,13 +834,13 @@
852
834
  nodes: [],
853
835
  edges: [],
854
836
  metadata: {
855
- version: '1.0.0',
837
+ schemaVersion: '1.0.0',
856
838
  format: DEFAULT_WORKFLOW_FORMAT,
857
839
  createdAt: new Date().toISOString(),
858
840
  updatedAt: new Date().toISOString()
859
841
  }
860
842
  };
861
- workflowActions.initialize(defaultWorkflow);
843
+ fd.workflow.initialize(defaultWorkflow);
862
844
  }
863
845
  } catch (error) {
864
846
  logger.error('Failed to initialize editor:', error);
@@ -874,6 +856,7 @@
874
856
 
875
857
  // Initialize auto-save based on user settings
876
858
  const cleanupAutoSave = initAutoSave({
859
+ isDirty: () => fd.workflow.isDirty,
877
860
  onSave: async () => {
878
861
  await saveWorkflow();
879
862
  },
@@ -945,7 +928,7 @@
945
928
 
946
929
  function handleConsoleUIAction(action: UIAction): void {
947
930
  if (action.type === 'open_config') {
948
- const wf = getWorkflowStore();
931
+ const wf = fd.workflow.current;
949
932
  if (!wf) return;
950
933
  const node = wf.nodes.find((n) => n.id === action.nodeId);
951
934
  if (node) openConfigSidebar(node);
@@ -1005,10 +988,12 @@
1005
988
  <!-- MainLayout wrapper for workflow editor -->
1006
989
  <div class="flowdrop-root">
1007
990
  <MainLayout
991
+ {height}
992
+ {width}
1008
993
  showHeader={showNavbar}
1009
994
  showLeftSidebar={!disableSidebar}
1010
995
  showRightSidebar={showRightPanel}
1011
- showBottomPanel={getUiSettings().consoleOpen && !readOnly && !lockWorkflow}
996
+ showBottomPanel={getUiSettings().consoleOpen && canvasEditable}
1012
997
  bottomPanelHeight={getUiSettings().consoleHeight}
1013
998
  showFooter={false}
1014
999
  headerHeight={60}
@@ -1027,7 +1012,7 @@
1027
1012
  <Navbar
1028
1013
  title={breadcrumbTitle}
1029
1014
  primaryActions={navbarActions.length > 0 ? navbarActions : defaultPrimaryActions}
1030
- showStatus={true}
1015
+ {showStatus}
1031
1016
  {showSettings}
1032
1017
  {settingsCategories}
1033
1018
  {showSettingsSyncButton}
@@ -1040,7 +1025,7 @@
1040
1025
  <NodeSidebar
1041
1026
  {nodes}
1042
1027
  loading={nodeTypesLoading}
1043
- activeFormat={getWorkflowFormat()}
1028
+ activeFormat={fd.workflow.format}
1044
1029
  categoriesDefaultOpen={themeConfig?.sidebar?.categoriesDefaultOpen ?? false}
1045
1030
  />
1046
1031
  {/snippet}
@@ -1048,16 +1033,8 @@
1048
1033
  <!-- Right Sidebar: Configuration, Swap, or Workflow Settings -->
1049
1034
  {#snippet rightSidebar()}
1050
1035
  {#if swapMode === 'mapping' && swapInteractiveState && selectedNodeForConfig}
1051
- {@const swapChecker = (() => {
1052
- try {
1053
- return getPortCompatibilityChecker();
1054
- } catch {
1055
- return null;
1056
- }
1057
- })()}
1058
1036
  <SwapMappingEditor
1059
1037
  interactiveState={swapInteractiveState}
1060
- checker={swapChecker}
1061
1038
  onConfirm={executeNodeSwap}
1062
1039
  onCancel={cancelSwap}
1063
1040
  onBack={() => {
@@ -1069,22 +1046,22 @@
1069
1046
  <NodeSwapPicker
1070
1047
  currentNode={selectedNodeForConfig}
1071
1048
  availableNodes={nodes}
1072
- activeFormat={getWorkflowFormat()}
1049
+ activeFormat={fd.workflow.format}
1073
1050
  onSelect={handleSwapSelect}
1074
1051
  onCancel={cancelSwap}
1075
1052
  />
1076
1053
  {:else if isWorkflowSettingsOpen}
1077
1054
  <ConfigPanel
1078
1055
  title={mergedMessages.navigation.workflowSettingsPanelTitle}
1079
- id={getWorkflowStore()?.id}
1056
+ id={fd.workflow.current?.id}
1080
1057
  details={[
1081
1058
  {
1082
1059
  label: 'Nodes',
1083
- value: String(getWorkflowStore()?.nodes?.length ?? 0)
1060
+ value: String(fd.workflow.current?.nodes?.length ?? 0)
1084
1061
  },
1085
1062
  {
1086
1063
  label: 'Connections',
1087
- value: String(getWorkflowStore()?.edges?.length ?? 0)
1064
+ value: String(fd.workflow.current?.edges?.length ?? 0)
1088
1065
  }
1089
1066
  ]}
1090
1067
  configTitle={mergedMessages.navigation.workflowSettingsPanelSubtitle}
@@ -1097,7 +1074,7 @@
1097
1074
  showUIExtensions={false}
1098
1075
  onChange={(config) => {
1099
1076
  // Sync workflow settings changes immediately on field blur
1100
- const wf = getWorkflowStore();
1077
+ const wf = fd.workflow.current;
1101
1078
  if (wf) {
1102
1079
  const newFormat = (config.format as string) || DEFAULT_WORKFLOW_FORMAT;
1103
1080
  const currentFormat = wf.metadata?.format || DEFAULT_WORKFLOW_FORMAT;
@@ -1118,7 +1095,7 @@
1118
1095
 
1119
1096
  // Extract built-in fields; everything else belongs in workflow.config
1120
1097
  const { name, description, format: _format, ...customConfig } = config;
1121
- workflowActions.batchUpdate({
1098
+ fd.workflow.batchUpdate({
1122
1099
  name: name as string,
1123
1100
  description: description as string | undefined,
1124
1101
  metadata: {
@@ -1149,14 +1126,14 @@
1149
1126
  }
1150
1127
  ]}
1151
1128
  onClose={closeConfigSidebar}
1152
- onSwap={!readOnly && !lockWorkflow && features.enableNodeSwap ? startSwap : undefined}
1129
+ onSwap={canvasEditable && features.enableNodeSwap ? startSwap : undefined}
1153
1130
  >
1154
1131
  <ConfigForm
1155
1132
  {authProvider}
1156
1133
  node={currentNode}
1157
- workflowId={getWorkflowStore()?.id}
1158
- workflowNodes={getWorkflowStore()?.nodes}
1159
- workflowEdges={getWorkflowStore()?.edges}
1134
+ workflowId={fd.workflow.current?.id}
1135
+ workflowNodes={fd.workflow.current?.nodes}
1136
+ workflowEdges={fd.workflow.current?.edges}
1160
1137
  onChange={async (updatedConfig, uiExtensions) => {
1161
1138
  // Sync config changes to workflow immediately on field blur
1162
1139
  if (selectedNodeId && currentNode) {
@@ -1179,7 +1156,7 @@
1179
1156
  data: updatedData
1180
1157
  };
1181
1158
 
1182
- workflowActions.updateNode(selectedNodeId, nodeUpdates);
1159
+ fd.workflow.updateNode(selectedNodeId, nodeUpdates);
1183
1160
 
1184
1161
  // Update the local editor state to reflect config changes immediately
1185
1162
  // This is needed for nodeType changes to take effect visually
@@ -1228,7 +1205,7 @@
1228
1205
  >
1229
1206
  <AIChatPanel
1230
1207
  nodeTypes={nodes}
1231
- workflowId={getWorkflowStore()?.id}
1208
+ workflowId={fd.workflow.current?.id}
1232
1209
  onUIAction={handleConsoleUIAction}
1233
1210
  {endpointConfig}
1234
1211
  />
@@ -1260,8 +1237,7 @@
1260
1237
  const defaultUrl = '/api/flowdrop';
1261
1238
  const newUrl = prompt('Enter Backend API URL:', defaultUrl);
1262
1239
  if (newUrl) {
1263
- const endpointConfig = createEndpointConfig(newUrl);
1264
- setEndpointConfig(endpointConfig);
1240
+ configureApi(createEndpointConfig(newUrl));
1265
1241
  fetchNodeTypes();
1266
1242
  }
1267
1243
  }}
@@ -1289,7 +1265,8 @@
1289
1265
  {/if}
1290
1266
 
1291
1267
  <!-- Main Editor Area -->
1292
- <!-- svelte-ignore a11y_no_noninteractive_element_interactions — interactive workflow canvas region with keyboard support -->
1268
+ <!-- interactive workflow canvas region with keyboard support -->
1269
+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
1293
1270
  <div
1294
1271
  class="flowdrop-editor-main"
1295
1272
  class:pipeline-view={!!pipelineId}
@@ -1317,18 +1294,11 @@
1317
1294
 
1318
1295
  <WorkflowEditor
1319
1296
  bind:this={workflowEditorRef}
1320
- {nodes}
1321
- {height}
1322
- {width}
1323
1297
  endpointConfig={endpointConfig ?? undefined}
1324
- {isConfigSidebarOpen}
1325
- {selectedNodeForConfig}
1326
1298
  {openConfigSidebar}
1327
- {closeConfigSidebar}
1328
- {lockWorkflow}
1329
- {readOnly}
1330
- {nodeStatuses}
1299
+ {mode}
1331
1300
  {pipelineId}
1301
+ {refreshTrigger}
1332
1302
  consoleOpen={getUiSettings().consoleOpen}
1333
1303
  onToggleConsole={toggleConsole}
1334
1304
  />