@flowdrop/flowdrop 1.8.1 → 1.9.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.
@@ -19,6 +19,12 @@ interface Props {
19
19
  config?: PlaygroundConfig;
20
20
  /** Callback when playground is closed (for embedded mode) */
21
21
  onClose?: () => void;
22
+ /** Callback to toggle the pipeline panel (if undefined, toggle button is hidden) */
23
+ onTogglePanel?: () => void;
24
+ /** Whether the pipeline panel is currently open (for toggle button active state) */
25
+ isPipelinePanelOpen?: boolean;
26
+ /** When provided, session switches and creation navigate to a URL instead of mutating store state */
27
+ onSessionNavigate?: (sessionId: string) => void;
22
28
  }
23
29
  declare const Playground: import("svelte").Component<Props, {}, "">;
24
30
  type Playground = ReturnType<typeof Playground>;
@@ -34,6 +34,13 @@ export interface GlobalSaveOptions {
34
34
  * Pass workflowStore's markAsSaved here when calling from App.svelte.
35
35
  */
36
36
  onMarkAsSaved?: () => void;
37
+ /**
38
+ * Callback invoked after a successful save with the persisted workflow.
39
+ * Receives the server-returned workflow (which may have a different ID than
40
+ * the one sent, e.g. server-assigned integer vs client UUID).
41
+ * Use this to update draft storage keys or other ID-dependent state.
42
+ */
43
+ onSaved?: (savedWorkflow: Workflow) => void;
37
44
  }
38
45
  /**
39
46
  * Options accepted by globalExportWorkflow().
@@ -85,7 +85,7 @@ async function flushPendingFormChanges() {
85
85
  * 7. Show toast notifications (respecting features.showToasts)
86
86
  */
87
87
  export async function globalSaveWorkflow(options = {}) {
88
- const { apiClient, eventHandlers, onMarkAsSaved } = options;
88
+ const { apiClient, eventHandlers, onMarkAsSaved, onSaved } = options;
89
89
  const features = { ...DEFAULT_FEATURES, ...options.features };
90
90
  // Step 1 — Flush pending form changes (single location for this logic)
91
91
  await flushPendingFormChanges();
@@ -170,6 +170,10 @@ export async function globalSaveWorkflow(options = {}) {
170
170
  // Fallback: call the store's own markAsSaved if no callback was provided
171
171
  storeMarkAsSaved();
172
172
  }
173
+ // Notify caller with the definitive saved workflow (server-assigned ID)
174
+ if (onSaved) {
175
+ onSaved(savedWorkflow);
176
+ }
173
177
  // Show success toast
174
178
  if (loadingToast)
175
179
  dismissToast(loadingToast);
@@ -0,0 +1,6 @@
1
+ export declare function getPipelinePanelOpen(): boolean;
2
+ export declare const pipelinePanelActions: {
3
+ init(): void;
4
+ toggle(): void;
5
+ setOpen(value: boolean): void;
6
+ };
@@ -0,0 +1,24 @@
1
+ const STORAGE_KEY = 'fd-pipeline-panel-open';
2
+ let _isOpen = $state(false);
3
+ export function getPipelinePanelOpen() {
4
+ return _isOpen;
5
+ }
6
+ export const pipelinePanelActions = {
7
+ init() {
8
+ if (typeof localStorage !== 'undefined') {
9
+ _isOpen = localStorage.getItem(STORAGE_KEY) === 'true';
10
+ }
11
+ },
12
+ toggle() {
13
+ _isOpen = !_isOpen;
14
+ if (typeof localStorage !== 'undefined') {
15
+ localStorage.setItem(STORAGE_KEY, String(_isOpen));
16
+ }
17
+ },
18
+ setOpen(value) {
19
+ _isOpen = value;
20
+ if (typeof localStorage !== 'undefined') {
21
+ localStorage.setItem(STORAGE_KEY, String(value));
22
+ }
23
+ }
24
+ };
@@ -75,6 +75,9 @@ export declare function getHasChatInput(): boolean;
75
75
  * Get session count
76
76
  */
77
77
  export declare function getSessionCount(): number;
78
+ export declare function getPinnedExecutionId(): string | null;
79
+ export declare function getLatestExecutionId(): string | null;
80
+ export declare function getActiveExecutionId(): string | null;
78
81
  /**
79
82
  * Playground store actions for modifying state
80
83
  */
@@ -174,6 +177,7 @@ export declare const playgroundActions: {
174
177
  * @param sessionId - The session ID to switch to
175
178
  */
176
179
  switchSession: (sessionId: string) => void;
180
+ pinExecution(executionId: string | null): void;
177
181
  };
178
182
  /**
179
183
  * Create a polling callback that processes poll responses.
@@ -43,6 +43,12 @@ let _currentWorkflow = $state(null);
43
43
  * Last polling timestamp for incremental message fetching
44
44
  */
45
45
  let _lastPollTimestamp = $state(null);
46
+ /** Execution ID explicitly pinned by the user (null = follow latest) */
47
+ let _pinnedExecutionId = $state(null);
48
+ /** Latest execution ID derived from current session's executions list */
49
+ const _latestExecutionId = $derived(_currentSession?.executions?.at(-1)?.id ?? null);
50
+ /** Active execution: pinned if set, otherwise latest */
51
+ const _activeExecutionId = $derived(_pinnedExecutionId ?? _latestExecutionId);
46
52
  // =========================================================================
47
53
  // Getter Functions (for reactive access in components)
48
54
  // =========================================================================
@@ -204,6 +210,15 @@ export function getHasChatInput() {
204
210
  export function getSessionCount() {
205
211
  return _sessions.length;
206
212
  }
213
+ export function getPinnedExecutionId() {
214
+ return _pinnedExecutionId;
215
+ }
216
+ export function getLatestExecutionId() {
217
+ return _latestExecutionId;
218
+ }
219
+ export function getActiveExecutionId() {
220
+ return _activeExecutionId;
221
+ }
207
222
  // =========================================================================
208
223
  // Helper Functions
209
224
  // =========================================================================
@@ -238,6 +253,32 @@ function sortMessagesChronologically(messageList) {
238
253
  return a.id.localeCompare(b.id);
239
254
  });
240
255
  }
256
+ /**
257
+ * Syncs the current session's executions list from incoming messages.
258
+ * When a message has a new executionId not yet tracked, adds it as a new execution entry.
259
+ */
260
+ function syncExecutionsFromMessages(messages) {
261
+ if (!_currentSession)
262
+ return;
263
+ const existingIds = new Set((_currentSession.executions ?? []).map((e) => e.id));
264
+ const newExecutions = [];
265
+ for (const msg of messages) {
266
+ if (msg.executionId && !existingIds.has(msg.executionId)) {
267
+ existingIds.add(msg.executionId);
268
+ newExecutions.push({
269
+ id: msg.executionId,
270
+ startedAt: msg.timestamp,
271
+ status: 'running'
272
+ });
273
+ }
274
+ }
275
+ if (newExecutions.length > 0) {
276
+ _currentSession = {
277
+ ..._currentSession,
278
+ executions: [...(_currentSession.executions ?? []), ...newExecutions]
279
+ };
280
+ }
281
+ }
241
282
  // =========================================================================
242
283
  // Actions
243
284
  // =========================================================================
@@ -259,6 +300,7 @@ export const playgroundActions = {
259
300
  * @param session - The session to set as active
260
301
  */
261
302
  setCurrentSession: (session) => {
303
+ _pinnedExecutionId = null;
262
304
  _currentSession = session;
263
305
  if (session) {
264
306
  // Update session in the list
@@ -278,6 +320,17 @@ export const playgroundActions = {
278
320
  updatedAt: new Date().toISOString()
279
321
  };
280
322
  }
323
+ // Update the latest execution status when the session reaches a terminal state.
324
+ // Only the last execution can be running at any time (sessions are single-pipeline),
325
+ // so we only need to check and update the tail entry.
326
+ if ((status === 'completed' || status === 'failed') && _currentSession?.executions?.length) {
327
+ const execs = [..._currentSession.executions];
328
+ const last = execs[execs.length - 1];
329
+ if (last.status === 'running') {
330
+ execs[execs.length - 1] = { ...last, status };
331
+ _currentSession = { ..._currentSession, executions: execs };
332
+ }
333
+ }
281
334
  // Also update in sessions list
282
335
  const session = _currentSession;
283
336
  if (session) {
@@ -354,6 +407,7 @@ export const playgroundActions = {
354
407
  const uniqueNewMessages = newMessages.filter((m) => !existingIds.has(m.id));
355
408
  // Sort the combined messages chronologically
356
409
  _messages = sortMessagesChronologically([..._messages, ...uniqueNewMessages]);
410
+ syncExecutionsFromMessages(newMessages);
357
411
  },
358
412
  /**
359
413
  * Clear all messages
@@ -413,12 +467,16 @@ export const playgroundActions = {
413
467
  * @param sessionId - The session ID to switch to
414
468
  */
415
469
  switchSession: (sessionId) => {
470
+ _pinnedExecutionId = null;
416
471
  const session = _sessions.find((s) => s.id === sessionId);
417
472
  if (session) {
418
473
  _currentSession = session;
419
474
  _messages = [];
420
475
  _lastPollTimestamp = null;
421
476
  }
477
+ },
478
+ pinExecution(executionId) {
479
+ _pinnedExecutionId = executionId;
422
480
  }
423
481
  };
424
482
  // =========================================================================
@@ -188,14 +188,37 @@ export async function mountFlowDropApp(container, options = {}) {
188
188
  isDirty: () => isDirty(),
189
189
  markAsSaved: () => {
190
190
  markAsSaved();
191
- // Also update draft manager
192
191
  if (state.draftManager) {
192
+ // 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
194
+ // written the server-assigned ID back into the store before calling
195
+ // markAsSaved(), we can move to a unique per-workflow key and stop
196
+ // competing with other tabs that may also have unsaved new workflows.
197
+ // Skip when customDraftKey is set — the host manages that key explicitly.
198
+ if (!customDraftKey) {
199
+ const currentWorkflow = getWorkflowFromStore();
200
+ if (currentWorkflow?.id) {
201
+ state.draftManager.updateStorageKey(getDraftStorageKey(currentWorkflow.id));
202
+ }
203
+ }
193
204
  state.draftManager.markAsSaved();
194
205
  }
195
206
  },
196
207
  getWorkflow: () => getWorkflowFromStore(),
197
208
  save: async () => {
198
- await globalSaveWorkflow();
209
+ await globalSaveWorkflow({
210
+ onSaved: (saved) => {
211
+ // globalSaveWorkflow does not write the server-assigned ID back to the
212
+ // workflow store, so we cannot read it from getWorkflowFromStore() here.
213
+ // Instead we use the savedWorkflow returned by the API directly.
214
+ // This migrates 'flowdrop:draft:new' to a unique per-workflow key
215
+ // immediately after the first save, preventing cross-tab collisions
216
+ // when multiple new workflows are open simultaneously.
217
+ if (state.draftManager && !customDraftKey && saved.id) {
218
+ state.draftManager.updateStorageKey(getDraftStorageKey(saved.id));
219
+ }
220
+ }
221
+ });
199
222
  },
200
223
  export: () => {
201
224
  globalExportWorkflow();
@@ -52,6 +52,14 @@ export type PlaygroundMessageLevel = 'info' | 'warning' | 'error' | 'debug';
52
52
  * Status of a playground message
53
53
  */
54
54
  export type PlaygroundMessageStatus = 'pending' | 'processing' | 'completed' | 'failed';
55
+ /**
56
+ * A single pipeline execution associated with a playground session
57
+ */
58
+ export interface PlaygroundExecution {
59
+ id: string;
60
+ startedAt: string;
61
+ status: 'running' | 'completed' | 'failed';
62
+ }
55
63
  /**
56
64
  * Playground session representing a test conversation
57
65
  *
@@ -83,6 +91,8 @@ export interface PlaygroundSession {
83
91
  createdAt: string;
84
92
  /** Last activity timestamp (ISO 8601) */
85
93
  updatedAt: string;
94
+ /** Pipeline executions triggered within this session, ordered oldest-first */
95
+ executions?: PlaygroundExecution[];
86
96
  /** Custom session metadata */
87
97
  metadata?: Record<string, unknown>;
88
98
  }
@@ -144,6 +154,8 @@ export interface PlaygroundMessage {
144
154
  sequenceNumber?: number;
145
155
  /** Parent message ID (for assistant responses linked to user messages) */
146
156
  parentMessageId?: string;
157
+ /** Pipeline/execution ID that generated this message */
158
+ executionId?: string | null;
147
159
  /** Associated node ID (for log/assistant messages) */
148
160
  nodeId?: string | null;
149
161
  /** Additional message metadata */
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "A drop-in visual workflow editor for any web application. You own the backend. You own the data. You own the orchestration.",
4
4
  "license": "MIT",
5
5
  "private": false,
6
- "version": "1.8.1",
6
+ "version": "1.9.0",
7
7
  "author": "Shibin Das (D34dMan)",
8
8
  "bugs": {
9
9
  "url": "https://github.com/flowdrop-io/flowdrop/issues"