@flowdrop/flowdrop 1.8.1 → 1.10.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 (34) hide show
  1. package/dist/api/enhanced-client.js +5 -1
  2. package/dist/components/PipelineStatus.svelte +31 -8
  3. package/dist/components/PipelineStatus.svelte.d.ts +5 -0
  4. package/dist/components/WorkflowEditor.svelte +26 -0
  5. package/dist/components/chat/AIChatPanel.svelte +16 -5
  6. package/dist/components/playground/ChatPanel.svelte +31 -108
  7. package/dist/components/playground/ChatPanel.svelte.d.ts +3 -1
  8. package/dist/components/playground/ExecutionList.svelte +138 -0
  9. package/dist/components/playground/ExecutionList.svelte.d.ts +10 -0
  10. package/dist/components/playground/MessageBubble.svelte +281 -156
  11. package/dist/components/playground/PipelinePanel.svelte +382 -0
  12. package/dist/components/playground/PipelinePanel.svelte.d.ts +20 -0
  13. package/dist/components/playground/Playground.svelte +707 -174
  14. package/dist/components/playground/Playground.svelte.d.ts +6 -0
  15. package/dist/components/playground/PlaygroundStudio.svelte +404 -0
  16. package/dist/components/playground/PlaygroundStudio.svelte.d.ts +30 -0
  17. package/dist/editor/index.d.ts +1 -1
  18. package/dist/editor/index.js +1 -1
  19. package/dist/playground/index.d.ts +7 -3
  20. package/dist/playground/index.js +14 -5
  21. package/dist/playground/mount.d.ts +7 -0
  22. package/dist/playground/mount.js +78 -81
  23. package/dist/services/globalSave.d.ts +7 -0
  24. package/dist/services/globalSave.js +5 -1
  25. package/dist/services/nodeExecutionService.js +4 -2
  26. package/dist/services/playgroundService.d.ts +11 -4
  27. package/dist/services/playgroundService.js +22 -12
  28. package/dist/stores/pipelinePanelStore.svelte.d.ts +6 -0
  29. package/dist/stores/pipelinePanelStore.svelte.js +24 -0
  30. package/dist/stores/playgroundStore.svelte.d.ts +26 -21
  31. package/dist/stores/playgroundStore.svelte.js +134 -55
  32. package/dist/svelte-app.js +25 -2
  33. package/dist/types/playground.d.ts +15 -5
  34. package/package.json +1 -1
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * @module stores/playgroundStore
8
8
  */
9
- import { isChatInputNode, defaultIsTerminalStatus } from '../types/playground.js';
9
+ import { isChatInputNode } from '../types/playground.js';
10
10
  import { logger } from '../utils/logger.js';
11
11
  // =========================================================================
12
12
  // Core State
@@ -23,10 +23,6 @@ let _sessions = $state([]);
23
23
  * Messages in the current session
24
24
  */
25
25
  let _messages = $state([]);
26
- /**
27
- * Whether an execution is currently running
28
- */
29
- let _isExecuting = $state(false);
30
26
  /**
31
27
  * Whether we are currently loading data
32
28
  */
@@ -42,7 +38,19 @@ let _currentWorkflow = $state(null);
42
38
  /**
43
39
  * Last polling timestamp for incremental message fetching
44
40
  */
45
- let _lastPollTimestamp = $state(null);
41
+ let _lastPollSequenceNumber = $state(null);
42
+ /** Execution ID explicitly pinned by the user (null = follow latest) */
43
+ let _pinnedExecutionId = $state(null);
44
+ /** Incremented on every message batch that should trigger a pipeline re-fetch */
45
+ let _pipelineRefreshTrigger = $state(0);
46
+ /** Latest execution ID derived from current session's executions list */
47
+ const _latestExecutionId = $derived(_currentSession?.executions?.at(-1)?.id ?? null);
48
+ /** Active execution: pinned if set, otherwise latest */
49
+ const _activeExecutionId = $derived(_pinnedExecutionId ?? _latestExecutionId);
50
+ // Derived from server status — never manually set.
51
+ // Exception: updateSessionStatus('running') in handleSendMessage is an
52
+ // acknowledged optimistic write, overwritten by the next server response.
53
+ const _isExecuting = $derived(_currentSession?.status === 'running');
46
54
  // =========================================================================
47
55
  // Getter Functions (for reactive access in components)
48
56
  // =========================================================================
@@ -89,10 +97,10 @@ export function getCurrentWorkflow() {
89
97
  return _currentWorkflow;
90
98
  }
91
99
  /**
92
- * Get the last poll timestamp
100
+ * Get the last poll sequence number cursor
93
101
  */
94
- export function getLastPollTimestamp() {
95
- return _lastPollTimestamp;
102
+ export function getLastPollSequenceNumber() {
103
+ return _lastPollSequenceNumber;
96
104
  }
97
105
  // =========================================================================
98
106
  // Derived Getters
@@ -103,6 +111,14 @@ export function getLastPollTimestamp() {
103
111
  export function getSessionStatus() {
104
112
  return _currentSession?.status ?? 'idle';
105
113
  }
114
+ /**
115
+ * Whether the user can currently send a message.
116
+ * False when executing, when awaiting input, or when no session exists.
117
+ */
118
+ export function getCanSendMessage() {
119
+ const status = _currentSession?.status ?? 'idle';
120
+ return _currentSession !== null && !_isExecuting && status !== 'awaiting_input';
121
+ }
106
122
  /**
107
123
  * Get message count
108
124
  */
@@ -204,6 +220,23 @@ export function getHasChatInput() {
204
220
  export function getSessionCount() {
205
221
  return _sessions.length;
206
222
  }
223
+ export function getPinnedExecutionId() {
224
+ return _pinnedExecutionId;
225
+ }
226
+ export function getLatestExecutionId() {
227
+ return _latestExecutionId;
228
+ }
229
+ export function getActiveExecutionId() {
230
+ return _activeExecutionId;
231
+ }
232
+ /**
233
+ * Counter that increments whenever new messages arrive and the pipeline display
234
+ * should re-fetch — i.e. when following latest or pinned to the latest execution.
235
+ * Pass to PipelinePanel's refreshTrigger prop.
236
+ */
237
+ export function getPipelineRefreshTrigger() {
238
+ return _pipelineRefreshTrigger;
239
+ }
207
240
  // =========================================================================
208
241
  // Helper Functions
209
242
  // =========================================================================
@@ -238,6 +271,34 @@ function sortMessagesChronologically(messageList) {
238
271
  return a.id.localeCompare(b.id);
239
272
  });
240
273
  }
274
+ /**
275
+ * Syncs the current session's executions list from incoming messages.
276
+ * When a message has a new executionId not yet tracked, adds it as a new execution entry.
277
+ */
278
+ function syncExecutionsFromMessages(messages) {
279
+ if (!_currentSession)
280
+ return;
281
+ const existingIds = new Set((_currentSession.executions ?? []).map((e) => e.id));
282
+ const newExecutions = [];
283
+ for (const msg of messages) {
284
+ if (msg.executionId && !existingIds.has(msg.executionId)) {
285
+ existingIds.add(msg.executionId);
286
+ newExecutions.push({
287
+ id: msg.executionId,
288
+ startedAt: msg.timestamp,
289
+ status: 'running'
290
+ });
291
+ }
292
+ }
293
+ if (newExecutions.length > 0) {
294
+ _currentSession = {
295
+ ..._currentSession,
296
+ executions: [...(_currentSession.executions ?? []), ...newExecutions]
297
+ };
298
+ // Clear any manual pin so the panel automatically follows the new run.
299
+ _pinnedExecutionId = null;
300
+ }
301
+ }
241
302
  // =========================================================================
242
303
  // Actions
243
304
  // =========================================================================
@@ -259,6 +320,7 @@ export const playgroundActions = {
259
320
  * @param session - The session to set as active
260
321
  */
261
322
  setCurrentSession: (session) => {
323
+ _pinnedExecutionId = null;
262
324
  _currentSession = session;
263
325
  if (session) {
264
326
  // Update session in the list
@@ -278,6 +340,20 @@ export const playgroundActions = {
278
340
  updatedAt: new Date().toISOString()
279
341
  };
280
342
  }
343
+ // Update the latest execution status when the session reaches a terminal state.
344
+ // Only the last execution can be running at any time (sessions are single-pipeline),
345
+ // so we only need to check and update the tail entry.
346
+ // 'idle' means the run finished normally (server returns 'idle' post-completion,
347
+ // not 'completed'), so map it to 'completed' for the execution entry.
348
+ const terminalExecutionStatus = status === 'failed' ? 'failed' : status === 'completed' || status === 'idle' ? 'completed' : null;
349
+ if (terminalExecutionStatus && _currentSession?.executions?.length) {
350
+ const execs = [..._currentSession.executions];
351
+ const last = execs[execs.length - 1];
352
+ if (last.status === 'running') {
353
+ execs[execs.length - 1] = { ...last, status: terminalExecutionStatus };
354
+ _currentSession = { ..._currentSession, executions: execs };
355
+ }
356
+ }
281
357
  // Also update in sessions list
282
358
  const session = _currentSession;
283
359
  if (session) {
@@ -349,26 +425,27 @@ export const playgroundActions = {
349
425
  addMessages: (newMessages) => {
350
426
  if (newMessages.length === 0)
351
427
  return;
352
- // Deduplicate by message ID
428
+ // Deduplicate against existing messages AND within the incoming batch itself.
429
+ // The latter matters when the backend returns the same page twice (e.g. broken
430
+ // offset pagination), which would otherwise create duplicate IDs in _messages
431
+ // and trigger Svelte's each_key_duplicate error.
353
432
  const existingIds = new Set(_messages.map((m) => m.id));
354
- const uniqueNewMessages = newMessages.filter((m) => !existingIds.has(m.id));
355
- // Sort the combined messages chronologically
433
+ const seenInBatch = new Set();
434
+ const uniqueNewMessages = newMessages.filter((m) => {
435
+ if (existingIds.has(m.id) || seenInBatch.has(m.id))
436
+ return false;
437
+ seenInBatch.add(m.id);
438
+ return true;
439
+ });
356
440
  _messages = sortMessagesChronologically([..._messages, ...uniqueNewMessages]);
441
+ syncExecutionsFromMessages(uniqueNewMessages);
357
442
  },
358
443
  /**
359
444
  * Clear all messages
360
445
  */
361
446
  clearMessages: () => {
362
447
  _messages = [];
363
- _lastPollTimestamp = null;
364
- },
365
- /**
366
- * Set the executing state
367
- *
368
- * @param executing - Whether execution is in progress
369
- */
370
- setExecuting: (executing) => {
371
- _isExecuting = executing;
448
+ _lastPollSequenceNumber = null;
372
449
  },
373
450
  /**
374
451
  * Set the loading state
@@ -391,8 +468,8 @@ export const playgroundActions = {
391
468
  *
392
469
  * @param timestamp - ISO 8601 timestamp
393
470
  */
394
- updateLastPollTimestamp: (timestamp) => {
395
- _lastPollTimestamp = timestamp;
471
+ updateLastPollSequenceNumber: (seq) => {
472
+ _lastPollSequenceNumber = seq;
396
473
  },
397
474
  /**
398
475
  * Reset all playground state
@@ -401,11 +478,11 @@ export const playgroundActions = {
401
478
  _currentSession = null;
402
479
  _sessions = [];
403
480
  _messages = [];
404
- _isExecuting = false;
405
481
  _isLoading = false;
406
482
  _error = null;
407
483
  _currentWorkflow = null;
408
- _lastPollTimestamp = null;
484
+ _lastPollSequenceNumber = null;
485
+ _pipelineRefreshTrigger = 0;
409
486
  },
410
487
  /**
411
488
  * Switch to a different session
@@ -413,37 +490,38 @@ export const playgroundActions = {
413
490
  * @param sessionId - The session ID to switch to
414
491
  */
415
492
  switchSession: (sessionId) => {
493
+ _pinnedExecutionId = null;
416
494
  const session = _sessions.find((s) => s.id === sessionId);
417
495
  if (session) {
418
496
  _currentSession = session;
419
497
  _messages = [];
420
- _lastPollTimestamp = null;
498
+ _lastPollSequenceNumber = null;
421
499
  }
500
+ },
501
+ pinExecution(executionId) {
502
+ _pinnedExecutionId = executionId;
422
503
  }
423
504
  };
424
505
  // =========================================================================
425
- // Polling Callback Factory
506
+ // Server Response Application
426
507
  // =========================================================================
427
508
  /**
428
- * Create a polling callback that processes poll responses.
429
- * This is the single source of truth for how poll responses update stores.
430
- * Used by mount.ts, Playground.svelte, and refreshSessionMessages.
431
- *
432
- * @param isTerminalStatus - Function to determine if a status clears isExecuting (default: defaultIsTerminalStatus)
433
- * @returns A callback suitable for playgroundService.startPolling() or pushMessages()
434
- */
435
- export function createPollingCallback(isTerminalStatus = defaultIsTerminalStatus) {
436
- return (response) => {
437
- if (response.data && response.data.length > 0) {
438
- playgroundActions.addMessages(response.data);
439
- }
440
- if (response.sessionStatus) {
441
- playgroundActions.updateSessionStatus(response.sessionStatus);
442
- if (isTerminalStatus(response.sessionStatus)) {
443
- playgroundActions.setExecuting(false);
444
- }
509
+ * Apply a server response to the store. All message and status updates from
510
+ * the server flow through here polling callback, manual fetches, interrupt
511
+ * resolution. Nothing updates messages or session status except this function.
512
+ */
513
+ export function applyServerResponse(response) {
514
+ if (response.data && response.data.length > 0) {
515
+ playgroundActions.addMessages(response.data);
516
+ // Refresh pipeline when following latest or pinned to the latest execution.
517
+ // Skip only when the user is viewing a historical run.
518
+ if (_pinnedExecutionId === null || _pinnedExecutionId === _latestExecutionId) {
519
+ _pipelineRefreshTrigger++;
445
520
  }
446
- };
521
+ }
522
+ if (response.sessionStatus) {
523
+ playgroundActions.updateSessionStatus(response.sessionStatus);
524
+ }
447
525
  }
448
526
  // =========================================================================
449
527
  // Utilities
@@ -474,14 +552,17 @@ export function getMessagesSnapshot() {
474
552
  return _messages;
475
553
  }
476
554
  /**
477
- * Get the latest message timestamp for polling
555
+ * Get the sequence number of the latest message, used to seed incremental polling.
478
556
  *
479
- * @returns ISO 8601 timestamp of the latest message, or null
557
+ * @returns Sequence number of the last message, or null
480
558
  */
481
- export function getLatestMessageTimestamp() {
482
- if (_messages.length === 0)
483
- return null;
484
- return _messages[_messages.length - 1].timestamp;
559
+ export function getLatestSequenceNumber() {
560
+ for (let i = _messages.length - 1; i >= 0; i--) {
561
+ if (_messages[i].sequenceNumber !== undefined) {
562
+ return _messages[i].sequenceNumber;
563
+ }
564
+ }
565
+ return null;
485
566
  }
486
567
  /**
487
568
  * Subscribe to session status changes using $effect.root.
@@ -510,17 +591,15 @@ export function subscribeToSessionStatus(callback) {
510
591
  * has stopped but new messages may exist on the server.
511
592
  *
512
593
  * @param fetchMessages - Async function to fetch messages from the API
513
- * @param isTerminalStatus - Optional override for terminal status check
514
594
  * @returns Promise that resolves when messages are refreshed
515
595
  */
516
- export async function refreshSessionMessages(fetchMessages, isTerminalStatus) {
596
+ export async function refreshSessionMessages(fetchMessages) {
517
597
  const session = _currentSession;
518
598
  if (!session)
519
599
  return;
520
600
  try {
521
601
  const response = await fetchMessages(session.id);
522
- const callback = createPollingCallback(isTerminalStatus);
523
- callback(response);
602
+ applyServerResponse(response);
524
603
  }
525
604
  catch (err) {
526
605
  logger.error('[playgroundStore] Failed to refresh messages:', err);
@@ -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
  }
@@ -100,6 +110,8 @@ export interface PlaygroundMessageMetadata {
100
110
  outputs?: Record<string, unknown>;
101
111
  /** User's display name for user-role messages (from backend) */
102
112
  userName?: string;
113
+ /** Subsystem that produced this message (e.g. 'pipeline', 'job', 'queue', 'cron') */
114
+ source?: string;
103
115
  /** Allow additional properties */
104
116
  [key: string]: unknown;
105
117
  }
@@ -136,14 +148,12 @@ export interface PlaygroundMessage {
136
148
  timestamp: string;
137
149
  /** Message status */
138
150
  status?: PlaygroundMessageStatus;
139
- /**
140
- * Sequence number for ordering messages
141
- * - User messages: incrementing numbers (1, 2, 3, ...)
142
- * - Assistant/system responses: 0 (sorted after parent via parentMessageId)
143
- */
151
+ /** Incrementing sequence number for chronological ordering. All message roles receive unique incrementing numbers (1, 2, 3, ...). Primary sort key. */
144
152
  sequenceNumber?: number;
145
153
  /** Parent message ID (for assistant responses linked to user messages) */
146
154
  parentMessageId?: string;
155
+ /** Pipeline/execution ID that generated this message */
156
+ executionId?: string | null;
147
157
  /** Associated node ID (for log/assistant messages) */
148
158
  nodeId?: string | null;
149
159
  /** 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.10.0",
7
7
  "author": "Shibin Das (D34dMan)",
8
8
  "bugs": {
9
9
  "url": "https://github.com/flowdrop-io/flowdrop/issues"