@flowdrop/flowdrop 1.4.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/README.md +68 -24
  2. package/dist/adapters/WorkflowAdapter.js +2 -22
  3. package/dist/adapters/agentspec/autoLayout.d.ts +51 -5
  4. package/dist/adapters/agentspec/autoLayout.js +120 -23
  5. package/dist/chat/commandClassifier.d.ts +19 -0
  6. package/dist/chat/commandClassifier.js +30 -0
  7. package/dist/chat/index.d.ts +27 -0
  8. package/dist/chat/index.js +32 -0
  9. package/dist/chat/responseParser.d.ts +21 -0
  10. package/dist/chat/responseParser.js +87 -0
  11. package/dist/commands/batch.d.ts +18 -0
  12. package/dist/commands/batch.js +56 -0
  13. package/dist/commands/executor.d.ts +37 -0
  14. package/dist/commands/executor.js +1044 -0
  15. package/dist/commands/index.d.ts +14 -0
  16. package/dist/commands/index.js +17 -0
  17. package/dist/commands/parser.d.ts +16 -0
  18. package/dist/commands/parser.js +278 -0
  19. package/dist/commands/positioner.d.ts +19 -0
  20. package/dist/commands/positioner.js +33 -0
  21. package/dist/commands/storeIntegration.svelte.d.ts +16 -0
  22. package/dist/commands/storeIntegration.svelte.js +67 -0
  23. package/dist/commands/types.d.ts +343 -0
  24. package/dist/commands/types.js +45 -0
  25. package/dist/components/App.svelte +351 -12
  26. package/dist/components/App.svelte.d.ts +3 -0
  27. package/dist/components/CanvasController.svelte +38 -0
  28. package/dist/components/CanvasController.svelte.d.ts +32 -0
  29. package/dist/components/ConfigMappingRow.svelte +130 -0
  30. package/dist/components/ConfigMappingRow.svelte.d.ts +8 -0
  31. package/dist/components/ConfigPanel.svelte +56 -7
  32. package/dist/components/ConfigPanel.svelte.d.ts +2 -0
  33. package/dist/components/FlowDropEdge.svelte +2 -10
  34. package/dist/components/LogsSidebar.svelte +5 -5
  35. package/dist/components/NodeSidebar.svelte +15 -49
  36. package/dist/components/NodeSwapPicker.svelte +537 -0
  37. package/dist/components/NodeSwapPicker.svelte.d.ts +16 -0
  38. package/dist/components/PortMappingRow.svelte +209 -0
  39. package/dist/components/PortMappingRow.svelte.d.ts +12 -0
  40. package/dist/components/SwapMappingEditor.svelte +550 -0
  41. package/dist/components/SwapMappingEditor.svelte.d.ts +12 -0
  42. package/dist/components/WorkflowEditor.svelte +99 -4
  43. package/dist/components/WorkflowEditor.svelte.d.ts +8 -0
  44. package/dist/components/chat/AIChatPanel.svelte +658 -0
  45. package/dist/components/chat/AIChatPanel.svelte.d.ts +13 -0
  46. package/dist/components/chat/CommandPreview.svelte +184 -0
  47. package/dist/components/chat/CommandPreview.svelte.d.ts +9 -0
  48. package/dist/components/console/CommandConsole.stories.svelte +93 -0
  49. package/dist/components/console/CommandConsole.stories.svelte.d.ts +27 -0
  50. package/dist/components/console/CommandConsole.svelte +259 -0
  51. package/dist/components/console/CommandConsole.svelte.d.ts +11 -0
  52. package/dist/components/console/ConsoleAutocomplete.svelte +139 -0
  53. package/dist/components/console/ConsoleAutocomplete.svelte.d.ts +21 -0
  54. package/dist/components/console/ConsoleInput.svelte +712 -0
  55. package/dist/components/console/ConsoleInput.svelte.d.ts +16 -0
  56. package/dist/components/console/ConsoleOutput.svelte +121 -0
  57. package/dist/components/console/ConsoleOutput.svelte.d.ts +11 -0
  58. package/dist/components/console/formatters.d.ts +26 -0
  59. package/dist/components/console/formatters.js +118 -0
  60. package/dist/components/interrupt/index.d.ts +1 -0
  61. package/dist/components/interrupt/index.js +1 -0
  62. package/dist/config/endpoints.d.ts +8 -0
  63. package/dist/config/endpoints.js +5 -0
  64. package/dist/core/index.d.ts +5 -0
  65. package/dist/core/index.js +9 -0
  66. package/dist/editor/index.d.ts +3 -1
  67. package/dist/editor/index.js +4 -2
  68. package/dist/helpers/proximityConnect.js +8 -1
  69. package/dist/helpers/workflowEditorHelper.d.ts +3 -53
  70. package/dist/helpers/workflowEditorHelper.js +13 -228
  71. package/dist/playground/index.d.ts +1 -1
  72. package/dist/playground/index.js +1 -1
  73. package/dist/schemas/v1/workflow.schema.json +107 -22
  74. package/dist/services/chatService.d.ts +65 -0
  75. package/dist/services/chatService.js +131 -0
  76. package/dist/services/historyService.d.ts +6 -4
  77. package/dist/services/historyService.js +21 -6
  78. package/dist/stores/interruptStore.svelte.js +6 -1
  79. package/dist/stores/playgroundStore.svelte.d.ts +1 -1
  80. package/dist/stores/playgroundStore.svelte.js +11 -2
  81. package/dist/stores/portCoordinateStore.svelte.d.ts +4 -0
  82. package/dist/stores/portCoordinateStore.svelte.js +20 -26
  83. package/dist/stores/workflowStore.svelte.d.ts +31 -2
  84. package/dist/stores/workflowStore.svelte.js +84 -64
  85. package/dist/types/chat.d.ts +63 -0
  86. package/dist/types/chat.js +9 -0
  87. package/dist/types/events.d.ts +28 -2
  88. package/dist/types/events.js +1 -0
  89. package/dist/types/index.d.ts +8 -0
  90. package/dist/types/settings.d.ts +6 -0
  91. package/dist/types/settings.js +3 -0
  92. package/dist/utils/edgeStyling.d.ts +42 -0
  93. package/dist/utils/edgeStyling.js +176 -0
  94. package/dist/utils/nodeIds.d.ts +31 -0
  95. package/dist/utils/nodeIds.js +42 -0
  96. package/dist/utils/nodeSwap.d.ts +221 -0
  97. package/dist/utils/nodeSwap.js +686 -0
  98. package/package.json +6 -1
  99. package/dist/helpers/nodeLayoutHelper.d.ts +0 -14
  100. package/dist/helpers/nodeLayoutHelper.js +0 -19
@@ -0,0 +1,131 @@
1
+ /**
2
+ * Chat Service
3
+ *
4
+ * Handles API interactions for the LLM Chat feature including
5
+ * sending messages, retrieving history, and clearing history.
6
+ *
7
+ * @module services/chatService
8
+ */
9
+ import { buildEndpointUrl, getEndpointHeaders } from "../config/endpoints.js";
10
+ import { getEndpointConfig } from "./api.js";
11
+ import { logger } from "../utils/logger.js";
12
+ /**
13
+ * Chat Service class
14
+ *
15
+ * Provides methods to interact with the chat API endpoints
16
+ * for LLM-powered workflow building assistance.
17
+ */
18
+ export class ChatService {
19
+ static instance;
20
+ constructor() { }
21
+ /**
22
+ * Get the singleton instance of ChatService
23
+ *
24
+ * @returns The ChatService singleton instance
25
+ */
26
+ static getInstance() {
27
+ if (!ChatService.instance) {
28
+ ChatService.instance = new ChatService();
29
+ }
30
+ return ChatService.instance;
31
+ }
32
+ /**
33
+ * Get the endpoint configuration
34
+ *
35
+ * @throws Error if endpoint configuration is not set
36
+ * @returns The endpoint configuration
37
+ */
38
+ getConfig() {
39
+ const config = getEndpointConfig();
40
+ if (!config) {
41
+ throw new Error("Endpoint configuration not set. Call setEndpointConfig() first.");
42
+ }
43
+ return config;
44
+ }
45
+ /**
46
+ * Generic API request helper
47
+ *
48
+ * @param url - The URL to fetch
49
+ * @param options - Fetch options
50
+ * @returns The parsed JSON response
51
+ */
52
+ async request(url, options = {}) {
53
+ const config = this.getConfig();
54
+ const headers = getEndpointHeaders(config, "chat");
55
+ const response = await fetch(url, {
56
+ ...options,
57
+ headers: {
58
+ ...headers,
59
+ ...options.headers,
60
+ },
61
+ });
62
+ if (!response.ok) {
63
+ const errorData = await response.json().catch(() => ({}));
64
+ const errorMessage = errorData.error ||
65
+ errorData.message ||
66
+ `HTTP ${response.status}: ${response.statusText}`;
67
+ throw new Error(errorMessage);
68
+ }
69
+ const json = await response.json();
70
+ // Unwrap the { success, data } envelope used by the Drupal backend.
71
+ if (json && typeof json === "object" && "data" in json) {
72
+ return json.data;
73
+ }
74
+ return json;
75
+ }
76
+ // =========================================================================
77
+ // Chat Operations
78
+ // =========================================================================
79
+ /**
80
+ * Send a message to the chat endpoint
81
+ *
82
+ * @param workflowId - The workflow ID
83
+ * @param request - The chat request payload
84
+ * @returns The chat response from the LLM
85
+ */
86
+ async sendMessage(workflowId, request) {
87
+ const config = this.getConfig();
88
+ const url = buildEndpointUrl(config, config.endpoints.chat.sendMessage, {
89
+ id: workflowId,
90
+ });
91
+ logger.debug("[ChatService] Sending message to", url);
92
+ return this.request(url, {
93
+ method: "POST",
94
+ body: JSON.stringify(request),
95
+ });
96
+ }
97
+ /**
98
+ * Get conversation history for a workflow
99
+ *
100
+ * @param workflowId - The workflow ID
101
+ * @returns Array of chat history messages
102
+ */
103
+ async getHistory(workflowId) {
104
+ const config = this.getConfig();
105
+ const url = buildEndpointUrl(config, config.endpoints.chat.getHistory, {
106
+ id: workflowId,
107
+ });
108
+ logger.debug("[ChatService] Getting history from", url);
109
+ return this.request(url);
110
+ }
111
+ /**
112
+ * Clear conversation history for a workflow
113
+ *
114
+ * @param workflowId - The workflow ID
115
+ */
116
+ async clearHistory(workflowId) {
117
+ const config = this.getConfig();
118
+ const url = buildEndpointUrl(config, config.endpoints.chat.clearHistory, {
119
+ id: workflowId,
120
+ });
121
+ logger.debug("[ChatService] Clearing history at", url);
122
+ await fetch(url, {
123
+ method: "DELETE",
124
+ headers: getEndpointHeaders(config, "chat"),
125
+ });
126
+ }
127
+ }
128
+ /**
129
+ * Pre-instantiated ChatService singleton
130
+ */
131
+ export const chatService = ChatService.getInstance();
@@ -140,8 +140,10 @@ export declare class HistoryService {
140
140
  * Cancel the current transaction without committing
141
141
  *
142
142
  * Discards the transaction without adding to history.
143
+ * Returns the snapshot captured at transaction start so the caller
144
+ * can restore the store to its pre-transaction state.
143
145
  */
144
- cancelTransaction(): void;
146
+ cancelTransaction(): Workflow | null;
145
147
  /**
146
148
  * Clear all history
147
149
  *
@@ -189,9 +191,9 @@ export declare class HistoryService {
189
191
  /**
190
192
  * Create a deep clone of a workflow
191
193
  *
192
- * Uses JSON parse/stringify to properly strip non-serializable values
193
- * like functions (e.g., onConfigOpen callbacks) that are added to nodes
194
- * at runtime but shouldn't be persisted in history.
194
+ * Strips non-serializable values (e.g., onConfigOpen callbacks) before cloning
195
+ * so structuredClone doesn't throw on function references.
196
+ * Falls back to JSON round-trip if structuredClone still fails (e.g., Svelte proxies).
195
197
  */
196
198
  private cloneWorkflow;
197
199
  /**
@@ -182,11 +182,15 @@ export class HistoryService {
182
182
  * Cancel the current transaction without committing
183
183
  *
184
184
  * Discards the transaction without adding to history.
185
+ * Returns the snapshot captured at transaction start so the caller
186
+ * can restore the store to its pre-transaction state.
185
187
  */
186
188
  cancelTransaction() {
189
+ const snapshot = this.transactionSnapshot;
187
190
  this.inTransaction = false;
188
191
  this.transactionSnapshot = null;
189
192
  this.transactionDescription = null;
193
+ return snapshot;
190
194
  }
191
195
  /**
192
196
  * Clear all history
@@ -292,14 +296,25 @@ export class HistoryService {
292
296
  /**
293
297
  * Create a deep clone of a workflow
294
298
  *
295
- * Uses JSON parse/stringify to properly strip non-serializable values
296
- * like functions (e.g., onConfigOpen callbacks) that are added to nodes
297
- * at runtime but shouldn't be persisted in history.
299
+ * Strips non-serializable values (e.g., onConfigOpen callbacks) before cloning
300
+ * so structuredClone doesn't throw on function references.
301
+ * Falls back to JSON round-trip if structuredClone still fails (e.g., Svelte proxies).
298
302
  */
299
303
  cloneWorkflow(workflow) {
300
- // Always use JSON parse/stringify to strip functions and other non-serializable values
301
- // structuredClone would fail on workflows containing callback functions
302
- return JSON.parse(JSON.stringify(workflow));
304
+ const cleaned = {
305
+ ...workflow,
306
+ nodes: workflow.nodes.map((n) => ({
307
+ ...n,
308
+ data: { ...n.data, onConfigOpen: undefined },
309
+ })),
310
+ };
311
+ try {
312
+ return structuredClone(cleaned);
313
+ }
314
+ catch {
315
+ // Fallback for environments where structuredClone can't handle proxied objects
316
+ return JSON.parse(JSON.stringify(cleaned));
317
+ }
303
318
  }
304
319
  /**
305
320
  * Notify all subscribers of state changes
@@ -56,7 +56,12 @@ export function getPendingInterrupts() {
56
56
  * Get count of pending interrupts
57
57
  */
58
58
  export function getPendingInterruptCount() {
59
- return getPendingInterruptIds().length;
59
+ let count = 0;
60
+ for (const interrupt of interrupts.values()) {
61
+ if (!isTerminalState(interrupt.machineState))
62
+ count++;
63
+ }
64
+ return count;
60
65
  }
61
66
  /**
62
67
  * Get resolved interrupts array
@@ -124,7 +124,7 @@ export declare const playgroundActions: {
124
124
  setMessages: (messageList: PlaygroundMessage[]) => void;
125
125
  /**
126
126
  * Add a message to the current session
127
- * Messages are automatically sorted chronologically after adding
127
+ * Uses binary search insertion for O(log n) instead of full sort.
128
128
  *
129
129
  * @param message - The message to add
130
130
  */
@@ -324,12 +324,21 @@ export const playgroundActions = {
324
324
  },
325
325
  /**
326
326
  * Add a message to the current session
327
- * Messages are automatically sorted chronologically after adding
327
+ * Uses binary search insertion for O(log n) instead of full sort.
328
328
  *
329
329
  * @param message - The message to add
330
330
  */
331
331
  addMessage: (message) => {
332
- _messages = sortMessagesChronologically([..._messages, message]);
332
+ const seq = message.sequenceNumber ?? 0;
333
+ let lo = 0, hi = _messages.length;
334
+ while (lo < hi) {
335
+ const mid = (lo + hi) >>> 1;
336
+ if ((_messages[mid].sequenceNumber ?? 0) <= seq)
337
+ lo = mid + 1;
338
+ else
339
+ hi = mid;
340
+ }
341
+ _messages = [..._messages.slice(0, lo), message, ..._messages.slice(lo)];
333
342
  },
334
343
  /**
335
344
  * Add multiple messages to the current session
@@ -36,6 +36,10 @@ export declare function updateNodePortCoordinates(node: WorkflowNodeType, getInt
36
36
  * @param nodeId - ID of the node to remove
37
37
  */
38
38
  export declare function removeNodePortCoordinates(nodeId: string): void;
39
+ /**
40
+ * Clear all port coordinates (lifecycle cleanup).
41
+ */
42
+ export declare function clearPortCoordinates(): void;
39
43
  /**
40
44
  * Get coordinates for a specific handle.
41
45
  *
@@ -126,27 +126,19 @@ export function updateNodePortCoordinates(node, getInternalNode) {
126
126
  const internalNode = getInternalNode(node.id);
127
127
  if (!internalNode)
128
128
  return;
129
- // Remove old entries for this node.
130
- // untrack prevents this read from creating a reactive dependency on `coordinates`
131
- // inside any $effect that calls this function — otherwise the effect would re-run
132
- // every time we mutate `coordinates`, creating an infinite reactive loop during drag.
133
- const keysToDelete = untrack(() => {
134
- const keys = [];
129
+ // Build a new map with all entries except this node's, then add recomputed entries.
130
+ // Single assignment fires one reactive notification instead of N deletes + M sets.
131
+ const newMap = new SvelteMap();
132
+ untrack(() => {
135
133
  for (const [key, coord] of coordinates) {
136
- if (coord.nodeId === node.id) {
137
- keys.push(key);
138
- }
134
+ if (coord.nodeId !== node.id)
135
+ newMap.set(key, coord);
139
136
  }
140
- return keys;
141
137
  });
142
- for (const key of keysToDelete) {
143
- coordinates.delete(key);
144
- }
145
- // Add new entries
146
- const coords = computeNodePortCoordinates(node, internalNode);
147
- for (const coord of coords) {
148
- coordinates.set(coord.handleId, coord);
138
+ for (const coord of computeNodePortCoordinates(node, internalNode)) {
139
+ newMap.set(coord.handleId, coord);
149
140
  }
141
+ coordinates = newMap;
150
142
  }
151
143
  /**
152
144
  * Remove all coordinates for a node (on node delete).
@@ -154,18 +146,20 @@ export function updateNodePortCoordinates(node, getInternalNode) {
154
146
  * @param nodeId - ID of the node to remove
155
147
  */
156
148
  export function removeNodePortCoordinates(nodeId) {
157
- const keysToDelete = untrack(() => {
158
- const keys = [];
149
+ const newMap = new SvelteMap();
150
+ untrack(() => {
159
151
  for (const [key, coord] of coordinates) {
160
- if (coord.nodeId === nodeId) {
161
- keys.push(key);
162
- }
152
+ if (coord.nodeId !== nodeId)
153
+ newMap.set(key, coord);
163
154
  }
164
- return keys;
165
155
  });
166
- for (const key of keysToDelete) {
167
- coordinates.delete(key);
168
- }
156
+ coordinates = newMap;
157
+ }
158
+ /**
159
+ * Clear all port coordinates (lifecycle cleanup).
160
+ */
161
+ export function clearPortCoordinates() {
162
+ coordinates = new SvelteMap();
169
163
  }
170
164
  /**
171
165
  * Get coordinates for a specific handle.
@@ -22,6 +22,10 @@ export declare function getWorkflowStore(): Workflow | null;
22
22
  /**
23
23
  * Get the current dirty state reactively
24
24
  *
25
+ * Reads both _editVersion and _savedVersion, so Svelte tracks them
26
+ * as reactive dependencies — any component using this will re-render
27
+ * when dirty state changes.
28
+ *
25
29
  * @returns true if there are unsaved changes
26
30
  */
27
31
  export declare function getIsDirty(): boolean;
@@ -121,9 +125,10 @@ export declare function setOnDirtyStateChange(callback: ((isDirty: boolean) => v
121
125
  */
122
126
  export declare function setOnWorkflowChange(callback: ((workflow: Workflow, changeType: WorkflowChangeType) => void) | null): void;
123
127
  /**
124
- * Mark the current workflow state as saved
128
+ * Mark the current workflow state as saved.
125
129
  *
126
- * Clears the dirty state by updating the saved snapshot.
130
+ * Captures the current edit version so isDirty becomes false.
131
+ * Call this after a successful backend save.
127
132
  */
128
133
  export declare function markAsSaved(): void;
129
134
  /**
@@ -132,6 +137,19 @@ export declare function markAsSaved(): void;
132
137
  * @returns true if there are unsaved changes
133
138
  */
134
139
  export declare function isDirty(): boolean;
140
+ /**
141
+ * Get the current edit version.
142
+ *
143
+ * Use this for the save verification protocol:
144
+ * 1. Before save: `const v = getEditVersion()`
145
+ * 2. Include `v` in the save request payload
146
+ * 3. Backend echoes `v` in the response
147
+ * 4. If echoed version matches: `markAsSaved()`
148
+ * 5. If not: the save didn't persist the version you submitted — reset from backend response
149
+ *
150
+ * @returns The current monotonic edit version
151
+ */
152
+ export declare function getEditVersion(): number;
135
153
  /**
136
154
  * Enable or disable history recording
137
155
  *
@@ -246,6 +264,17 @@ export declare const workflowActions: {
246
264
  description?: string;
247
265
  metadata?: Partial<Workflow["metadata"]>;
248
266
  }) => void;
267
+ /**
268
+ * Swap a node — atomically replaces nodes and edges with a descriptive history entry.
269
+ *
270
+ * Unlike batchUpdate, this uses `"node_swap"` as the change type and
271
+ * records a meaningful description for the undo history.
272
+ */
273
+ swapNode: (updates: {
274
+ nodes: WorkflowNode[];
275
+ edges: WorkflowEdge[];
276
+ description?: string;
277
+ }) => void;
249
278
  /**
250
279
  * Push current state to history manually
251
280
  *