@flowdrop/flowdrop 1.9.0 → 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.
@@ -6,13 +6,14 @@ interface Props {
6
6
  workflow: Workflow;
7
7
  endpointConfig: EndpointConfig;
8
8
  isPinned: boolean;
9
- runLabel?: string;
10
9
  /** All executions for the current session, oldest-first */
11
10
  executions?: PlaygroundExecution[];
12
11
  /** ID of the most-recent execution */
13
12
  latestExecutionId?: string | null;
14
13
  /** Called with an execution ID to pin it, or null to follow latest */
15
14
  onSelectExecution?: (id: string | null) => void;
15
+ /** Increments when new messages arrive — forwarded to PipelineStatus for immediate refresh */
16
+ refreshTrigger?: number;
16
17
  }
17
18
  declare const PipelinePanel: import("svelte").Component<Props, {}, "">;
18
19
  type PipelinePanel = ReturnType<typeof PipelinePanel>;
@@ -26,7 +26,9 @@
26
26
  getError,
27
27
  playgroundActions,
28
28
  getInputFields,
29
- createPollingCallback,
29
+ applyServerResponse,
30
+ getCanSendMessage,
31
+ getLatestSequenceNumber,
30
32
  getActiveExecutionId,
31
33
  getLatestExecutionId,
32
34
  getPinnedExecutionId,
@@ -98,6 +100,9 @@
98
100
  /** Whether log messages are visible in the chat panel */
99
101
  let showLogs = $state(true);
100
102
 
103
+ /** Whether a manual refresh is in flight */
104
+ let isRefreshing = $state(false);
105
+
101
106
  /** Whether the session switcher popover is open (standalone mode) */
102
107
  let sessionDropdownOpen = $state(false);
103
108
 
@@ -117,41 +122,31 @@
117
122
  * Initialize the playground on mount
118
123
  */
119
124
  onMount(() => {
120
- // Set endpoint config if provided
121
- if (endpointConfig) {
122
- setEndpointConfig(endpointConfig);
123
- }
124
-
125
- // Set workflow in store
126
- if (workflow) {
127
- playgroundActions.setWorkflow(workflow);
128
- }
129
-
130
- // Async initialization
131
- const initializePlayground = async (): Promise<void> => {
132
- try {
133
- // Load sessions
134
- await loadSessions();
135
-
136
- // Resume initial session if provided
137
- if (initialSessionId) {
138
- await loadInitialSession(initialSessionId);
139
- }
140
-
141
- // Handle auto-run after initialization is complete
142
- if (config.autoRun && !autoRunTriggered) {
143
- autoRunTriggered = true;
144
- const predefinedMessage = config.predefinedMessage ?? 'Run workflow';
145
- logger.debug('[Playground] Auto-run triggered with message:', predefinedMessage);
146
- await handleSendMessage(predefinedMessage);
125
+ if (endpointConfig) setEndpointConfig(endpointConfig);
126
+ if (workflow) playgroundActions.setWorkflow(workflow);
127
+
128
+ const handleVisibility = () => {
129
+ if (document.visibilityState === 'visible' && playgroundService.isPolling()) {
130
+ const sessionId = getCurrentSession()?.id;
131
+ if (sessionId) {
132
+ void playgroundService
133
+ .getMessages(sessionId, playgroundService.getLastSequenceNumber() ?? undefined)
134
+ .then((response) => applyServerResponse(response))
135
+ .catch((err) => logger.error('[Playground] Visibility catchup failed:', err));
147
136
  }
148
- } catch (err) {
149
- logger.error('[Playground] Initialization error:', err);
150
137
  }
151
138
  };
139
+ document.addEventListener('visibilitychange', handleVisibility);
140
+
141
+ const handleRefreshStatus = () => void refreshFromServer();
142
+ document.addEventListener('flowdrop:refresh-status', handleRefreshStatus);
152
143
 
153
- // Execute initialization
154
144
  void initializePlayground();
145
+
146
+ return () => {
147
+ document.removeEventListener('visibilitychange', handleVisibility);
148
+ document.removeEventListener('flowdrop:refresh-status', handleRefreshStatus);
149
+ };
155
150
  });
156
151
 
157
152
  /**
@@ -164,8 +159,10 @@
164
159
  return;
165
160
  }
166
161
 
167
- // Skip if this session was already loaded
168
- if (initialSessionLoaded && loadedInitialSessionId === initialSessionId) {
162
+ // Skip if this session was already loaded or is currently loading.
163
+ // loadedInitialSessionId is set synchronously at the start of loadInitialSession,
164
+ // so this prevents the effect from spawning concurrent loads when isLoading changes.
165
+ if (loadedInitialSessionId === initialSessionId) {
169
166
  return;
170
167
  }
171
168
 
@@ -176,18 +173,42 @@
176
173
  }
177
174
 
178
175
  // Load the initial session if sessions are available
179
- if (sessionList.length > 0 && !initialSessionLoaded) {
176
+ if (sessionList.length > 0) {
180
177
  void loadInitialSession(initialSessionId);
181
178
  }
182
179
  });
183
180
 
181
+ /**
182
+ * Initialize the playground: load sessions, load initial session, handle auto-run
183
+ */
184
+ async function initializePlayground(): Promise<void> {
185
+ try {
186
+ await loadSessions();
187
+
188
+ if (initialSessionId) {
189
+ await loadInitialSession(initialSessionId);
190
+ }
191
+
192
+ if (config.autoRun && !autoRunTriggered) {
193
+ autoRunTriggered = true;
194
+ const predefinedMessage = config.predefinedMessage ?? 'Run workflow';
195
+ logger.debug('[Playground] Auto-run triggered with message:', predefinedMessage);
196
+ await handleSendMessage(predefinedMessage);
197
+ }
198
+ } catch (err) {
199
+ logger.error('[Playground] Initialization error:', err);
200
+ }
201
+ }
202
+
184
203
  /**
185
204
  * Load the initial session with validation and error handling
186
205
  *
187
206
  * @param sessionId - The session ID to load
188
207
  */
189
208
  async function loadInitialSession(sessionId: string): Promise<void> {
190
- // Validate session exists in loaded sessions
209
+ // Validate session exists in loaded sessions before setting the guard.
210
+ // If not found yet, we skip setting loadedInitialSessionId so the $effect
211
+ // can retry when _sessions updates (e.g. after a new session is created).
191
212
  const sessionList = getSessions();
192
213
  const sessionExists = sessionList.some((s) => s.id === sessionId);
193
214
 
@@ -196,21 +217,19 @@
196
217
  `[Playground] Initial session "${sessionId}" not found in available sessions. ` +
197
218
  `Available sessions: ${sessionList.map((s) => s.id).join(', ') || 'none'}`
198
219
  );
199
- // Don't set error - just log warning and let user pick a session
200
220
  initialSessionLoaded = true;
201
- loadedInitialSessionId = sessionId;
202
221
  return;
203
222
  }
204
223
 
224
+ // Set guard BEFORE the first await to prevent concurrent loads.
225
+ loadedInitialSessionId = sessionId;
226
+
205
227
  try {
206
228
  await loadSession(sessionId);
207
229
  initialSessionLoaded = true;
208
- loadedInitialSessionId = sessionId;
209
230
  } catch (err) {
210
231
  logger.error('[Playground] Failed to load initial session:', err);
211
- // Mark as attempted to prevent retry loops
212
232
  initialSessionLoaded = true;
213
- loadedInitialSessionId = sessionId;
214
233
  }
215
234
  }
216
235
 
@@ -265,17 +284,14 @@
265
284
  playgroundActions.setError(null);
266
285
 
267
286
  try {
268
- // Get session details
269
287
  const session = await playgroundService.getSession(sessionId);
270
288
  playgroundActions.setCurrentSession(session);
271
289
 
272
- // Get messages
273
290
  const response = await playgroundService.getMessages(sessionId);
274
- playgroundActions.setMessages(response.data ?? []);
291
+ applyServerResponse(response);
275
292
 
276
- // Start polling if session is running
277
293
  if (session.status === 'running') {
278
- startPolling(sessionId);
294
+ startPolling(sessionId, true);
279
295
  }
280
296
  } catch (err) {
281
297
  const errorMessage = err instanceof Error ? err.message : 'Failed to load session';
@@ -326,9 +342,8 @@
326
342
  return;
327
343
  }
328
344
 
329
- // Stop polling for current session
330
345
  playgroundService.stopPolling();
331
-
346
+ playgroundActions.updateSessionStatus('idle');
332
347
  await loadSession(sessionId);
333
348
  }
334
349
 
@@ -387,9 +402,10 @@
387
402
  * Send a message
388
403
  */
389
404
  async function handleSendMessage(content: string): Promise<void> {
405
+ if (getIsExecuting()) return;
406
+
390
407
  const session = getCurrentSession();
391
408
  if (!session) {
392
- // Create a session first if none exists
393
409
  await handleCreateSession();
394
410
  const newSession = getCurrentSession();
395
411
  if (!newSession) {
@@ -397,23 +413,19 @@
397
413
  }
398
414
  }
399
415
 
400
- const sessionId = getCurrentSession()?.id;
401
- if (!sessionId) {
402
- return;
403
- }
416
+ const sessionId = getCurrentSession()!.id;
404
417
 
405
- playgroundActions.setExecuting(true);
418
+ playgroundActions.updateSessionStatus('running');
419
+ playgroundActions.pinExecution(null);
406
420
  playgroundActions.setError(null);
407
421
 
408
422
  try {
409
- // Prepare inputs from the input collector
410
423
  const inputs: Record<string, unknown> = {};
411
424
  const fields = getInputFields();
412
425
 
413
426
  fields.forEach((field) => {
414
427
  const key = `${field.nodeId}:${field.fieldId}`;
415
428
  if (inputValues[key] !== undefined) {
416
- // Map to node ID and field ID for the backend
417
429
  if (!inputs[field.nodeId]) {
418
430
  inputs[field.nodeId] = {};
419
431
  }
@@ -421,19 +433,13 @@
421
433
  }
422
434
  });
423
435
 
424
- // Send message
425
436
  const message = await playgroundService.sendMessage(sessionId, content, inputs);
426
437
  playgroundActions.addMessage(message);
427
-
428
- // Update session status
429
- playgroundActions.updateSessionStatus('running');
430
-
431
- // Start polling for responses
432
438
  startPolling(sessionId);
433
439
  } catch (err) {
434
440
  const errorMessage = err instanceof Error ? err.message : 'Failed to send message';
435
441
  playgroundActions.setError(errorMessage);
436
- playgroundActions.setExecuting(false);
442
+ playgroundActions.updateSessionStatus('idle');
437
443
  logger.error('Failed to send message:', err);
438
444
  }
439
445
  }
@@ -450,33 +456,56 @@
450
456
  try {
451
457
  await playgroundService.stopExecution(sessionId);
452
458
  playgroundService.stopPolling();
453
- playgroundActions.setExecuting(false);
454
459
  playgroundActions.updateSessionStatus('idle');
455
460
  } catch (err) {
456
461
  const errorMessage = err instanceof Error ? err.message : 'Failed to stop execution';
457
462
  playgroundActions.setError(errorMessage);
463
+ playgroundService.stopPolling();
464
+ playgroundActions.updateSessionStatus('idle');
458
465
  logger.error('Failed to stop execution:', err);
459
466
  }
460
467
  }
461
468
 
462
- /** Shared polling callback created from config lifecycle hooks */
463
- // svelte-ignore state_referenced_locally — config is static
464
- const pollingCallback = createPollingCallback(config.isTerminalStatus);
465
-
466
469
  /**
467
470
  * Start polling for messages
468
471
  */
469
- function startPolling(sessionId: string): void {
472
+ function startPolling(sessionId: string, seedSequence = false): void {
470
473
  const pollingInterval = config.pollingInterval ?? 1500;
474
+ const initialSequenceNumber = seedSequence ? getLatestSequenceNumber() : null;
471
475
 
472
476
  playgroundService.startPolling(
473
477
  sessionId,
474
- pollingCallback,
478
+ (response) => applyServerResponse(response),
475
479
  pollingInterval,
476
- config.shouldStopPolling
480
+ config.shouldStopPolling,
481
+ initialSequenceNumber
477
482
  );
478
483
  }
479
484
 
485
+ /**
486
+ * Fetch the latest messages and session status from the server.
487
+ * Resumes polling if the session is running but polling had stopped.
488
+ */
489
+ async function refreshFromServer(): Promise<void> {
490
+ const sessionId = getCurrentSession()?.id;
491
+ if (!sessionId || isRefreshing) return;
492
+ isRefreshing = true;
493
+ try {
494
+ const response = await playgroundService.getMessages(
495
+ sessionId,
496
+ playgroundService.getLastSequenceNumber() ?? undefined
497
+ );
498
+ applyServerResponse(response);
499
+ if (response.sessionStatus === 'running' && !playgroundService.isPolling()) {
500
+ startPolling(sessionId, true);
501
+ }
502
+ } catch (err) {
503
+ logger.error('[Playground] Status refresh failed:', err);
504
+ } finally {
505
+ isRefreshing = false;
506
+ }
507
+ }
508
+
480
509
  /**
481
510
  * Refresh messages for the current session
482
511
  * Called after interrupt resolution when polling has stopped
@@ -487,9 +516,13 @@
487
516
 
488
517
  try {
489
518
  const response = await playgroundService.getMessages(sessionId);
490
- pollingCallback(response);
519
+ applyServerResponse(response);
520
+
521
+ if (response.sessionStatus === 'running') {
522
+ startPolling(sessionId, true);
523
+ }
491
524
  } catch (err) {
492
- logger.error('[Playground] Failed to refresh messages after interrupt:', err);
525
+ logger.error('[Playground] Failed to refresh after interrupt:', err);
493
526
  }
494
527
  }
495
528
 
@@ -771,6 +804,19 @@
771
804
  Pipeline
772
805
  </button>
773
806
  {/if}
807
+ {#if getCurrentSession()}
808
+ <button
809
+ type="button"
810
+ class="playground__log-toggle"
811
+ class:playground__refresh--spinning={isRefreshing}
812
+ onclick={() => void refreshFromServer()}
813
+ disabled={isRefreshing}
814
+ title="Refresh status"
815
+ >
816
+ <Icon icon="mdi:refresh" />
817
+ Refresh
818
+ </button>
819
+ {/if}
774
820
  <button
775
821
  type="button"
776
822
  class="playground__log-toggle"
@@ -1313,6 +1359,10 @@
1313
1359
  color: var(--fd-primary);
1314
1360
  }
1315
1361
 
1362
+ .playground__refresh--spinning :global(svg) {
1363
+ animation: spin 0.8s linear infinite;
1364
+ }
1365
+
1316
1366
  /* Session chip (standalone mode) */
1317
1367
  .playground__session-chip-wrap {
1318
1368
  position: relative;