@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
@@ -8,7 +8,8 @@
8
8
  import '@xyflow/svelte/dist/style.css';
9
9
  import UniversalNode from '../components/UniversalNode.svelte';
10
10
  import FlowDropEdge from '../components/FlowDropEdge.svelte';
11
- import { registerBuiltinNodes } from '../registry/builtinNodes.js';
11
+ import { getDefaultInstance } from '../stores/instanceContainer.svelte.js';
12
+ import { provideInstance } from '../stores/getInstance.svelte.js';
12
13
  import { EDGE_MARKER_SIZES } from '../config/constants.js';
13
14
 
14
15
  interface Props {
@@ -33,7 +34,9 @@
33
34
  targetHandleId = 'input'
34
35
  }: Props = $props();
35
36
 
36
- registerBuiltinNodes();
37
+ // Provide an instance so UniversalNode can resolve built-in node components
38
+ // (the registry is seeded with BUILTIN_NODE_COMPONENTS at construction).
39
+ provideInstance(getDefaultInstance());
37
40
 
38
41
  const nodeTypes = {
39
42
  universalNode: UniversalNode
@@ -7,12 +7,14 @@
7
7
  import type { Node, ColorMode } from '@xyflow/svelte';
8
8
  import '@xyflow/svelte/dist/style.css';
9
9
  import UniversalNode from '../components/UniversalNode.svelte';
10
- import { registerBuiltinNodes } from '../registry/builtinNodes.js';
10
+ import { getDefaultInstance } from '../stores/instanceContainer.svelte.js';
11
+ import { provideInstance } from '../stores/getInstance.svelte.js';
11
12
 
12
13
  let { data, selected = false }: { data: Record<string, unknown>; selected?: boolean } = $props();
13
14
 
14
- // Ensure built-in node components are registered
15
- registerBuiltinNodes();
15
+ // Provide an instance so UniversalNode can resolve built-in node components
16
+ // (the registry is seeded with BUILTIN_NODE_COMPONENTS at construction).
17
+ provideInstance(getDefaultInstance());
16
18
 
17
19
  const nodeTypes = {
18
20
  universalNode: UniversalNode
@@ -12,7 +12,8 @@ import type { AuthProvider } from './types/auth.js';
12
12
  import type { FlowDropEventHandlers, FlowDropFeatures } from './types/events.js';
13
13
  import type { FlowDropTheme, FlowDropThemeName } from './types/theme.js';
14
14
  import type { WorkflowFormatAdapter } from './registry/workflowFormatRegistry.js';
15
- import './registry/builtinFormats.js';
15
+ import { type FlowDropInstance } from './stores/instanceContainer.svelte.js';
16
+ import { type DraftStorageOption } from './services/draftStorage.js';
16
17
  import type { PartialSettings, SettingsCategory } from './types/settings.js';
17
18
  import type { NavbarAction } from './types/navbar.js';
18
19
  export type { NavbarAction };
@@ -38,20 +39,25 @@ export interface FlowDropMountOptions {
38
39
  showNavbar?: boolean;
39
40
  /** Disable the node sidebar */
40
41
  disableSidebar?: boolean;
41
- /** Lock the workflow (prevent changes) */
42
- lockWorkflow?: boolean;
43
- /** Read-only mode */
44
- readOnly?: boolean;
42
+ /**
43
+ * Editor interaction mode. Replaces the former `readOnly` + `lockWorkflow`
44
+ * mount options (2.0). `'edit'` (default) allows editing; `'readonly'` and
45
+ * `'locked'` disable all canvas interaction (identical behavior today — see
46
+ * App's `mode` prop JSDoc for the full matrix). Migration: `readOnly: true`
47
+ * → `mode: 'readonly'`; `lockWorkflow: true` → `mode: 'locked'`.
48
+ * @default 'edit'
49
+ */
50
+ mode?: 'edit' | 'readonly' | 'locked';
45
51
  /** Pipeline ID for status display */
46
52
  pipelineId?: string;
47
- /** Node execution statuses */
48
- nodeStatuses?: Record<string, 'pending' | 'running' | 'completed' | 'error'>;
49
53
  /** Custom navbar title */
50
54
  navbarTitle?: string;
51
55
  /** Custom navbar actions */
52
56
  navbarActions?: NavbarAction[];
53
57
  /** Show settings gear icon in navbar */
54
58
  showSettings?: boolean;
59
+ /** Show the "Connected" status indicator in the navbar (default: true) */
60
+ showStatus?: boolean;
55
61
  /** Authentication provider for API requests */
56
62
  authProvider?: AuthProvider;
57
63
  /** Event handlers for workflow lifecycle */
@@ -60,8 +66,28 @@ export interface FlowDropMountOptions {
60
66
  features?: FlowDropFeatures;
61
67
  /** Initial settings overrides (theme, behavior, editor, ui, api) */
62
68
  settings?: PartialSettings;
63
- /** Custom storage key for localStorage drafts */
69
+ /** Custom storage key for workflow drafts */
64
70
  draftStorageKey?: string;
71
+ /**
72
+ * Where workflow drafts are persisted.
73
+ *
74
+ * - `'local'` (default): `localStorage` — drafts survive reloads and remain
75
+ * stored on the device even after the tab or browser is closed, until
76
+ * saved or cleared. Call `clearAllDrafts()` on logout for shared devices.
77
+ * - `'session'`: `sessionStorage` — drafts are scoped to the tab and removed
78
+ * when it closes (they do not survive crash-and-reopen).
79
+ * - A custom `DraftStorageAdapter` — note the adapter interface is
80
+ * synchronous, so it suits in-memory stores and write-through caches;
81
+ * async backends (IndexedDB, network) need a sync cache in front.
82
+ *
83
+ * The resolved adapter is captured per mount, so multiple FlowDrop
84
+ * instances on one page can use different backends.
85
+ *
86
+ * Note: neither built-in backend protects against same-origin script access
87
+ * (XSS). End users can additionally opt out of draft persistence at runtime
88
+ * via the "Store Drafts in Browser" behavior setting.
89
+ */
90
+ draftStorage?: DraftStorageOption;
65
91
  /** Custom workflow format adapters to register */
66
92
  formatAdapters?: WorkflowFormatAdapter[];
67
93
  /** Visual theme — named built-in ('default' | 'minimal') or custom theme object */
@@ -72,11 +98,32 @@ export interface FlowDropMountOptions {
72
98
  showSettingsSyncButton?: boolean;
73
99
  /** Show the reset buttons in the settings modal */
74
100
  showSettingsResetButton?: boolean;
101
+ /**
102
+ * Identifier for this FlowDrop instance.
103
+ *
104
+ * When omitted, the first mount on the page becomes the *default* instance
105
+ * (id `default`). Additional mounts get auto-generated ids (`fd-1`,
106
+ * `fd-2`, …). Every instance scopes its draft/panel storage keys by id
107
+ * (`flowdrop:draft:<id>:…`); the default instance migrates 1.x bare keys
108
+ * on first read.
109
+ *
110
+ * Pass an explicit id whenever you mount more than one editor with drafts
111
+ * enabled, so keys stay stable across page loads.
112
+ */
113
+ instanceId?: string;
75
114
  }
76
115
  /**
77
116
  * Return type for mounted FlowDrop app
78
117
  */
79
118
  export interface MountedFlowDropApp {
119
+ /**
120
+ * This mount's state container — workflow, history, playground, interrupts,
121
+ * categories, and the rest. The 2.0 replacement for the removed
122
+ * module-level store APIs: where 1.x called `workflowActions.addNode(...)`
123
+ * or `historyService.undo()`, call `app.instance.workflow.addNode(...)` /
124
+ * `app.instance.history.undo()` on the mount that owns the editor.
125
+ */
126
+ instance: FlowDropInstance;
80
127
  /**
81
128
  * Destroy the app and clean up resources
82
129
  */
@@ -102,7 +149,8 @@ export interface MountedFlowDropApp {
102
149
  */
103
150
  export: () => void;
104
151
  /**
105
- * Clear all FlowDrop workflow drafts from `localStorage`.
152
+ * Clear all FlowDrop workflow drafts from the configured draft storage
153
+ * (`localStorage` unless changed via the `draftStorage` mount option).
106
154
  *
107
155
  * Removes every key beginning with `flowdrop:draft:` plus the custom
108
156
  * `draftStorageKey` configured at mount time (if any). Call this from
@@ -148,12 +196,14 @@ export declare function mountFlowDropApp(container: HTMLElement, options?: FlowD
148
196
  * @returns Promise resolving to a MountedFlowDropApp instance
149
197
  */
150
198
  export declare function mountWorkflowEditor(container: HTMLElement, options?: {
199
+ /** Initial workflow to load into the editor */
151
200
  workflow?: Workflow;
152
- nodes?: NodeMetadata[];
153
201
  endpointConfig?: EndpointConfig;
154
202
  portConfig?: PortConfig;
155
203
  categories?: CategoryDefinition[];
156
204
  authProvider?: AuthProvider;
205
+ /** Instance identifier — see {@link FlowDropMountOptions.instanceId}. */
206
+ instanceId?: string;
157
207
  }): Promise<MountedFlowDropApp>;
158
208
  /**
159
209
  * Unmount a FlowDrop app
@@ -9,19 +9,52 @@
9
9
  import { mount, unmount } from 'svelte';
10
10
  import WorkflowEditor from './components/WorkflowEditor.svelte';
11
11
  import App from './components/App.svelte';
12
- import { workflowFormatRegistry } from './registry/workflowFormatRegistry.js';
13
- import './registry/builtinFormats.js';
14
- import { initializePortCompatibility } from './utils/connections.js';
15
12
  import { DEFAULT_PORT_CONFIG } from './config/defaultPortConfig.js';
16
13
  import { fetchPortConfig } from './services/portConfigApi.js';
17
14
  import { fetchCategories } from './services/categoriesApi.js';
18
- import { initializeCategories } from './stores/categoriesStore.svelte.js';
19
- import { isDirty, markAsSaved, getWorkflow as getWorkflowFromStore, setOnDirtyStateChange, setOnWorkflowChange } from './stores/workflowStore.svelte.js';
20
- import { DraftAutoSaveManager, getDraftStorageKey, clearAllDrafts as clearAllDraftsFromStorage } from './services/draftStorage.js';
15
+ import { createFlowDropInstance, getDefaultInstance, DEFAULT_DRAFT_PREFIX } from './stores/instanceContainer.svelte.js';
16
+ import { DraftAutoSaveManager, getDraftStorageKey, migrateLegacyDraftKey, setDraftStorage, resolveDraftStorage, clearAllDrafts as clearAllDraftsFromStorage } from './services/draftStorage.js';
21
17
  import { mergeFeatures } from './types/events.js';
22
- import { initializeSettings } from './stores/settingsStore.svelte.js';
18
+ import { initializeSettings, getBehaviorSettings, onSettingsChange } from './stores/settingsStore.svelte.js';
23
19
  import { logger } from './utils/logger.js';
24
20
  import { globalSaveWorkflow, globalExportWorkflow } from './services/globalSave.js';
21
+ // ---------------------------------------------------------------------------
22
+ // Instance acquisition
23
+ // ---------------------------------------------------------------------------
24
+ /** Whether a mount currently owns the page-default instance. */
25
+ let defaultInstanceClaimed = false;
26
+ /**
27
+ * Resolve the FlowDrop instance for a new mount.
28
+ *
29
+ * No `instanceId` + default unclaimed → the page-default instance (legacy
30
+ * storage keys, reachable via the module-level store APIs). Everything else
31
+ * gets its own isolated instance.
32
+ */
33
+ function acquireInstance(instanceId) {
34
+ if (!instanceId && !defaultInstanceClaimed) {
35
+ defaultInstanceClaimed = true;
36
+ return { fd: getDefaultInstance(), isDefault: true };
37
+ }
38
+ return { fd: createFlowDropInstance({ id: instanceId }), isDefault: false };
39
+ }
40
+ /**
41
+ * Release a mount's instance on destroy.
42
+ *
43
+ * Non-default instances are fully destroyed (subscriptions, effect roots,
44
+ * callbacks). The default instance is only *released* — its callbacks are
45
+ * cleared but its reactive wiring stays alive, matching the pre-multi-instance
46
+ * behavior where module stores survived unmount and a later mount reused them.
47
+ */
48
+ function releaseInstance(fd, isDefault) {
49
+ if (isDefault) {
50
+ fd.workflow.setOnDirtyStateChange(null);
51
+ fd.workflow.setOnWorkflowChange(null);
52
+ defaultInstanceClaimed = false;
53
+ }
54
+ else {
55
+ fd.destroy();
56
+ }
57
+ }
25
58
  /**
26
59
  * Mount the full FlowDrop App with navbar, sidebars, and workflow editor
27
60
  *
@@ -47,15 +80,25 @@ import { globalSaveWorkflow, globalExportWorkflow } from './services/globalSave.
47
80
  * ```
48
81
  */
49
82
  export async function mountFlowDropApp(container, options = {}) {
50
- const { workflow, nodes, endpointConfig, portConfig, categories, height = '100vh', width = '100%', showNavbar = false, disableSidebar, lockWorkflow, readOnly, nodeStatuses, pipelineId, navbarTitle, navbarActions, showSettings, authProvider, eventHandlers, features: userFeatures, settings: initialSettings, draftStorageKey: customDraftKey, formatAdapters, theme, settingsCategories, showSettingsSyncButton, showSettingsResetButton } = options;
83
+ const { workflow, nodes, endpointConfig, portConfig, categories, height = '100vh', width = '100%', showNavbar = false, disableSidebar, mode, pipelineId, navbarTitle, navbarActions, showSettings, showStatus, authProvider, eventHandlers, features: userFeatures, settings: initialSettings, draftStorageKey: customDraftKey, draftStorage, formatAdapters, theme, settingsCategories, showSettingsSyncButton, showSettingsResetButton, instanceId } = options;
84
+ // Per-instance state container — this is what allows multiple FlowDrop
85
+ // editors to coexist on one page without sharing workflow/history state.
86
+ const { fd, isDefault } = acquireInstance(instanceId);
51
87
  // Register custom format adapters before mounting
52
88
  if (formatAdapters) {
53
89
  for (const adapter of formatAdapters) {
54
- workflowFormatRegistry.register(adapter);
90
+ fd.formats.register(adapter);
55
91
  }
56
92
  }
57
93
  // Merge features with defaults
58
94
  const features = mergeFeatures(userFeatures);
95
+ // Resolve this instance's draft storage backend. The adapter is captured
96
+ // here and threaded into the draft manager and clearAllDrafts, so a later
97
+ // mount with a different backend cannot retarget this instance. The
98
+ // module-level default is also updated for the standalone helpers
99
+ // (last mount wins there — instance paths do not depend on it).
100
+ const draftStorageAdapter = resolveDraftStorage(draftStorage ?? 'local');
101
+ setDraftStorage(draftStorageAdapter);
59
102
  // Apply initial settings overrides and initialize theme
60
103
  await initializeSettings({
61
104
  defaults: initialSettings
@@ -94,47 +137,64 @@ export async function mountFlowDropApp(container, options = {}) {
94
137
  else if (!finalPortConfig) {
95
138
  finalPortConfig = DEFAULT_PORT_CONFIG;
96
139
  }
97
- initializePortCompatibility(finalPortConfig);
98
- // Initialize categories
140
+ // Configure this instance's API context (endpoints + auth provider) so
141
+ // <App> and services resolve it via getInstance().api.
142
+ if (config) {
143
+ fd.api.configure(config, authProvider);
144
+ }
145
+ // Re-initialize this instance's port compatibility checker with the resolved
146
+ // config (it was seeded with DEFAULT_PORT_CONFIG at construction).
147
+ fd.portCompatibility.reinitialize(finalPortConfig);
148
+ // Initialize this instance's categories
99
149
  if (categories) {
100
- initializeCategories(categories);
150
+ fd.categories.initialize(categories);
101
151
  }
102
152
  else if (config) {
103
153
  try {
104
154
  const fetchedCategories = await fetchCategories(config, authProvider);
105
- initializeCategories(fetchedCategories);
155
+ fd.categories.initialize(fetchedCategories);
106
156
  }
107
157
  catch (error) {
108
158
  logger.warn('Failed to fetch categories from API, using defaults:', error);
109
159
  }
110
160
  }
111
- // Set up event handler callbacks in the store
161
+ // Set up event handler callbacks in this instance's store
112
162
  if (eventHandlers?.onDirtyStateChange) {
113
- setOnDirtyStateChange(eventHandlers.onDirtyStateChange);
163
+ fd.workflow.setOnDirtyStateChange(eventHandlers.onDirtyStateChange);
114
164
  }
115
165
  if (eventHandlers?.onWorkflowChange) {
116
- setOnWorkflowChange(eventHandlers.onWorkflowChange);
166
+ fd.workflow.setOnWorkflowChange(eventHandlers.onWorkflowChange);
117
167
  }
118
168
  // Create the Svelte App component with configuration
119
169
  const svelteApp = mount(App, {
120
170
  target: container,
121
171
  props: {
172
+ instance: fd,
122
173
  workflow,
123
174
  nodes,
124
175
  height,
125
176
  width,
126
177
  showNavbar,
127
178
  disableSidebar,
128
- lockWorkflow,
129
- readOnly,
130
- nodeStatuses,
179
+ mode,
131
180
  pipelineId,
132
181
  navbarTitle,
133
182
  navbarActions,
134
183
  showSettings,
184
+ showStatus,
135
185
  endpointConfig: config,
136
186
  authProvider,
137
- eventHandlers,
187
+ // App's event-handler props are flat (the grouped `eventHandlers` option
188
+ // is a mount-API convenience). onDirtyStateChange / onWorkflowChange are
189
+ // wired into the instance store above; onBeforeUnmount is handled in
190
+ // unmount() below; the remaining handlers map 1:1 to App props.
191
+ onBeforeSave: eventHandlers?.onBeforeSave,
192
+ onAfterSave: eventHandlers?.onAfterSave,
193
+ onSaveError: eventHandlers?.onSaveError,
194
+ onApiError: eventHandlers?.onApiError,
195
+ onWorkflowLoad: eventHandlers?.onWorkflowLoad,
196
+ onBeforeSwap: eventHandlers?.onBeforeSwap,
197
+ onAfterSwap: eventHandlers?.onAfterSwap,
138
198
  features,
139
199
  theme,
140
200
  settingsCategories,
@@ -144,88 +204,119 @@ export async function mountFlowDropApp(container, options = {}) {
144
204
  });
145
205
  // Set up draft auto-save manager
146
206
  let draftManager = null;
207
+ let unsubscribeDraftSettings = null;
147
208
  if (features.autoSaveDraft) {
148
- const storageKey = getDraftStorageKey(workflow?.id, customDraftKey);
209
+ // Instance-scoped prefix: every instance (including the default) gets
210
+ // 'flowdrop:draft:<id>' sub-keys.
211
+ const storageKey = getDraftStorageKey(workflow?.id, customDraftKey, fd.storagePrefix);
212
+ // One-time migration: 1.x stored the page-default instance's drafts under
213
+ // the bare 'flowdrop:draft:<workflowId>' key. Move an existing draft to
214
+ // the scoped key so an upgrade mid-edit doesn't lose work.
215
+ if (fd.isDefault && !customDraftKey) {
216
+ migrateLegacyDraftKey(getDraftStorageKey(workflow?.id, undefined, DEFAULT_DRAFT_PREFIX), storageKey, draftStorageAdapter);
217
+ }
149
218
  draftManager = new DraftAutoSaveManager({
150
219
  storageKey,
151
220
  interval: features.autoSaveDraftInterval,
152
221
  enabled: features.autoSaveDraft,
153
- getWorkflow: getWorkflowFromStore,
154
- isDirty
222
+ getWorkflow: () => fd.workflow.current,
223
+ isDirty: () => fd.workflow.isDirty,
224
+ // User-facing opt-out: the "Store Drafts in Browser" behavior setting.
225
+ // Checked on every save so toggling it takes effect immediately.
226
+ isPersistenceAllowed: () => getBehaviorSettings().storeDraftsInBrowser,
227
+ storage: draftStorageAdapter
155
228
  });
156
229
  draftManager.start();
230
+ // When the user opts out, also remove the draft already in storage.
231
+ unsubscribeDraftSettings = onSettingsChange((event) => {
232
+ if (event.category === 'behavior' &&
233
+ event.key === 'storeDraftsInBrowser' &&
234
+ event.newValue === false) {
235
+ draftManager?.clearDraft();
236
+ }
237
+ });
157
238
  }
158
239
  // Store state for cleanup
159
240
  const state = {
160
241
  svelteApp,
161
242
  draftManager,
162
- eventHandlers: eventHandlers ?? null
243
+ eventHandlers: eventHandlers ?? null,
244
+ unsubscribeDraftSettings
163
245
  };
164
246
  // Create the mounted app interface
165
247
  const mountedApp = {
248
+ instance: fd,
166
249
  destroy: () => {
167
250
  // Call onBeforeUnmount if provided
168
251
  if (state.eventHandlers?.onBeforeUnmount) {
169
- const currentWorkflow = getWorkflowFromStore();
252
+ const currentWorkflow = fd.workflow.current;
170
253
  if (currentWorkflow) {
171
- state.eventHandlers.onBeforeUnmount(currentWorkflow, isDirty());
254
+ state.eventHandlers.onBeforeUnmount(currentWorkflow, fd.workflow.isDirty);
172
255
  }
173
256
  }
174
257
  // Stop draft manager
175
258
  if (state.draftManager) {
176
- // Save one final draft if dirty
177
- if (isDirty()) {
259
+ // Save one final draft if dirty (no-op when the user opted out)
260
+ if (fd.workflow.isDirty) {
178
261
  state.draftManager.forceSave();
179
262
  }
180
263
  state.draftManager.stop();
181
264
  }
182
- // Clear event callbacks
183
- setOnDirtyStateChange(null);
184
- setOnWorkflowChange(null);
265
+ // Stop listening for the draft opt-out setting
266
+ if (state.unsubscribeDraftSettings) {
267
+ state.unsubscribeDraftSettings();
268
+ state.unsubscribeDraftSettings = null;
269
+ }
270
+ // Release this mount's instance: clears its callbacks (and, for
271
+ // non-default instances, all subscriptions/effect roots) without
272
+ // touching sibling instances on the same page.
273
+ releaseInstance(fd, isDefault);
185
274
  // Unmount Svelte app
186
275
  unmount(state.svelteApp);
187
276
  },
188
- isDirty: () => isDirty(),
277
+ isDirty: () => fd.workflow.isDirty,
189
278
  markAsSaved: () => {
190
- markAsSaved();
279
+ fd.workflow.markAsSaved();
191
280
  if (state.draftManager) {
192
281
  // Migrate the draft key when the host confirms a save. New workflows start
193
- // on 'flowdrop:draft:new', a key shared across all tabs. If the host has
282
+ // on '<prefix>:new', a key shared across all tabs. If the host has
194
283
  // written the server-assigned ID back into the store before calling
195
284
  // markAsSaved(), we can move to a unique per-workflow key and stop
196
285
  // competing with other tabs that may also have unsaved new workflows.
197
286
  // Skip when customDraftKey is set — the host manages that key explicitly.
198
287
  if (!customDraftKey) {
199
- const currentWorkflow = getWorkflowFromStore();
288
+ const currentWorkflow = fd.workflow.current;
200
289
  if (currentWorkflow?.id) {
201
- state.draftManager.updateStorageKey(getDraftStorageKey(currentWorkflow.id));
290
+ state.draftManager.updateStorageKey(getDraftStorageKey(currentWorkflow.id, undefined, fd.storagePrefix));
202
291
  }
203
292
  }
204
293
  state.draftManager.markAsSaved();
205
294
  }
206
295
  },
207
- getWorkflow: () => getWorkflowFromStore(),
296
+ getWorkflow: () => fd.workflow.current,
208
297
  save: async () => {
209
298
  await globalSaveWorkflow({
299
+ instance: fd,
210
300
  onSaved: (saved) => {
211
301
  // globalSaveWorkflow does not write the server-assigned ID back to the
212
- // workflow store, so we cannot read it from getWorkflowFromStore() here.
302
+ // workflow store, so we cannot read it from the store here.
213
303
  // Instead we use the savedWorkflow returned by the API directly.
214
- // This migrates 'flowdrop:draft:new' to a unique per-workflow key
304
+ // This migrates '<prefix>:new' to a unique per-workflow key
215
305
  // immediately after the first save, preventing cross-tab collisions
216
306
  // when multiple new workflows are open simultaneously.
217
307
  if (state.draftManager && !customDraftKey && saved.id) {
218
- state.draftManager.updateStorageKey(getDraftStorageKey(saved.id));
308
+ state.draftManager.updateStorageKey(getDraftStorageKey(saved.id, undefined, fd.storagePrefix));
219
309
  }
220
310
  }
221
311
  });
222
312
  },
223
313
  export: () => {
224
- globalExportWorkflow();
314
+ globalExportWorkflow({ instance: fd });
225
315
  },
226
316
  clearAllDrafts: () => {
227
317
  const extras = customDraftKey ? [customDraftKey] : [];
228
- const removed = clearAllDraftsFromStorage(extras);
318
+ // Clear this instance's backend, not whatever the module default is now
319
+ const removed = clearAllDraftsFromStorage(extras, draftStorageAdapter);
229
320
  if (state.draftManager) {
230
321
  state.draftManager.markAsSaved();
231
322
  }
@@ -244,7 +335,9 @@ export async function mountFlowDropApp(container, options = {}) {
244
335
  * @returns Promise resolving to a MountedFlowDropApp instance
245
336
  */
246
337
  export async function mountWorkflowEditor(container, options = {}) {
247
- const { nodes = [], endpointConfig, portConfig, categories, authProvider } = options;
338
+ const { workflow, endpointConfig, portConfig, categories, authProvider, instanceId } = options;
339
+ // Per-instance state container (see mountFlowDropApp)
340
+ const { fd, isDefault } = acquireInstance(instanceId);
248
341
  // Create endpoint configuration
249
342
  let config;
250
343
  if (endpointConfig) {
@@ -279,41 +372,52 @@ export async function mountWorkflowEditor(container, options = {}) {
279
372
  else if (!finalPortConfig) {
280
373
  finalPortConfig = DEFAULT_PORT_CONFIG;
281
374
  }
282
- initializePortCompatibility(finalPortConfig);
283
- // Initialize categories
375
+ // Configure this instance's API context and port compatibility checker.
376
+ if (config) {
377
+ fd.api.configure(config, authProvider);
378
+ }
379
+ fd.portCompatibility.reinitialize(finalPortConfig);
380
+ // Initialize this instance's categories
284
381
  if (categories) {
285
- initializeCategories(categories);
382
+ fd.categories.initialize(categories);
286
383
  }
287
384
  else if (config) {
288
385
  try {
289
386
  const fetchedCategories = await fetchCategories(config, authProvider);
290
- initializeCategories(fetchedCategories);
387
+ fd.categories.initialize(fetchedCategories);
291
388
  }
292
389
  catch (error) {
293
390
  logger.warn('Failed to fetch categories from API, using defaults:', error);
294
391
  }
295
392
  }
393
+ // Seed the instance's workflow before mounting so the editor renders it
394
+ // immediately. (1.x accepted this option but silently ignored it.)
395
+ if (workflow) {
396
+ fd.workflow.initialize(workflow);
397
+ }
296
398
  // Create the Svelte component
297
399
  const svelteApp = mount(WorkflowEditor, {
298
400
  target: container,
299
401
  props: {
300
- nodes,
402
+ instance: fd,
301
403
  endpointConfig: config
302
404
  }
303
405
  });
304
406
  // Create the mounted app interface (simpler version)
305
407
  const mountedApp = {
408
+ instance: fd,
306
409
  destroy: () => {
410
+ releaseInstance(fd, isDefault);
307
411
  unmount(svelteApp);
308
412
  },
309
- isDirty: () => isDirty(),
310
- markAsSaved: () => markAsSaved(),
311
- getWorkflow: () => getWorkflowFromStore(),
413
+ isDirty: () => fd.workflow.isDirty,
414
+ markAsSaved: () => fd.workflow.markAsSaved(),
415
+ getWorkflow: () => fd.workflow.current,
312
416
  save: async () => {
313
- await globalSaveWorkflow();
417
+ await globalSaveWorkflow({ instance: fd });
314
418
  },
315
419
  export: () => {
316
- globalExportWorkflow();
420
+ globalExportWorkflow({ instance: fd });
317
421
  },
318
422
  clearAllDrafts: () => clearAllDraftsFromStorage()
319
423
  };
@@ -166,10 +166,13 @@ export interface FlowDropEventHandlers {
166
166
  */
167
167
  export interface FlowDropFeatures {
168
168
  /**
169
- * Save drafts to localStorage automatically
169
+ * Save drafts to browser storage automatically
170
170
  *
171
171
  * When enabled, FlowDrop will periodically save the current workflow
172
- * to localStorage as a draft. This helps prevent data loss.
172
+ * to draft storage (localStorage by default, configurable via the
173
+ * `draftStorage` mount option). This helps prevent data loss. End users
174
+ * can additionally opt out at runtime via the "Store Drafts in Browser"
175
+ * behavior setting.
173
176
  *
174
177
  * @default true
175
178
  */
@@ -177,7 +180,7 @@ export interface FlowDropFeatures {
177
180
  /**
178
181
  * Auto-save interval in milliseconds
179
182
  *
180
- * How often to save drafts to localStorage when autoSaveDraft is enabled.
183
+ * How often to save drafts to storage when autoSaveDraft is enabled.
181
184
  *
182
185
  * @default 30000 (30 seconds)
183
186
  */