@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
@@ -425,6 +425,10 @@ export class EnhancedFlowDropApiClient {
425
425
  * Fetch pipeline data including job information and status
426
426
  */
427
427
  async getPipelineData(pipelineId) {
428
- return this.request('pipelines.get', this.config.endpoints.pipelines.get, { id: pipelineId }, {}, 'get pipeline data');
428
+ const response = await this.request('pipelines.get', this.config.endpoints.pipelines.get, { id: pipelineId }, {}, 'get pipeline data');
429
+ if (!response.success || !response.data) {
430
+ throw new Error(response.error ?? 'Failed to fetch pipeline data');
431
+ }
432
+ return response.data;
429
433
  }
430
434
  }
@@ -21,6 +21,11 @@
21
21
  apiClient?: EnhancedFlowDropApiClient;
22
22
  baseUrl?: string;
23
23
  endpointConfig?: EndpointConfig;
24
+ runLabel?: string;
25
+ /** When true, suppresses breadcrumb and layout events (used inside playground panel) */
26
+ isEmbedded?: boolean;
27
+ /** Increments when new messages arrive — triggers an immediate pipeline data refresh */
28
+ refreshTrigger?: number;
24
29
  onActionsReady?: (
25
30
  actions: Array<{
26
31
  label: string;
@@ -32,9 +37,13 @@
32
37
  ) => void;
33
38
  }
34
39
 
35
- let { pipelineId, workflow, apiClient, baseUrl, endpointConfig, onActionsReady }: Props =
40
+ let { pipelineId, workflow, apiClient, baseUrl, endpointConfig, onActionsReady, runLabel, isEmbedded = false, refreshTrigger = 0 }: Props =
36
41
  $props();
37
42
 
43
+ // Track previous trigger value so the $effect only fires on increments, not on initial mount.
44
+ // svelte-ignore state_referenced_locally
45
+ let _prevRefreshTrigger = refreshTrigger;
46
+
38
47
  // Initialize API client if not provided
39
48
  // svelte-ignore state_referenced_locally — client created once from props
40
49
  const client =
@@ -213,8 +222,9 @@
213
222
  };
214
223
  });
215
224
 
216
- // Send pipeline breadcrumbs to layout when they change
225
+ // Send pipeline breadcrumbs to layout when they change (skip when embedded in playground)
217
226
  $effect(() => {
227
+ if (isEmbedded) return;
218
228
  if (pipelineStatus && pipelineId && workflow) {
219
229
  const sp = m().status.pipeline;
220
230
  const breadcrumbs = [
@@ -239,7 +249,9 @@
239
249
  icon: 'mdi:source-branch'
240
250
  },
241
251
  {
242
- label: sp.pipelineCrumb({ id: pipelineId, status: pipelineStatus }),
252
+ label: runLabel
253
+ ? `${runLabel} – ${pipelineStatus}`
254
+ : sp.pipelineCrumb({ id: pipelineId, status: pipelineStatus }),
243
255
  icon: 'mdi:play-circle'
244
256
  }
245
257
  ];
@@ -279,13 +291,23 @@
279
291
 
280
292
  // Note: Interval cleanup is handled by the $effect above.
281
293
  // In Svelte 5, $effect cleanup runs both on re-execution and component destroy.
294
+
295
+ // Refresh pipeline data whenever new messages arrive (e.g. log messages during execution).
296
+ // Debounced so burst arrivals collapse into one fetch.
297
+ $effect(() => {
298
+ const t = refreshTrigger;
299
+ if (t <= 0 || t === _prevRefreshTrigger) return;
300
+ _prevRefreshTrigger = t;
301
+ const timer = setTimeout(fetchPipelineData, 300);
302
+ return () => clearTimeout(timer);
303
+ });
282
304
  </script>
283
305
 
284
- <div class="pipeline-status-container">
306
+ <div class="pipeline-status-container" class:pipeline-status-container--embedded={isEmbedded}>
285
307
  <!-- Workflow Visualization using App component -->
286
308
  <App
287
309
  {workflow}
288
- height="100vh"
310
+ height={isEmbedded ? '100%' : '100vh'}
289
311
  width="100%"
290
312
  showNavbar={false}
291
313
  disableSidebar={true}
@@ -310,8 +332,9 @@
310
332
  background: var(--fd-layout-background, var(--fd-muted));
311
333
  }
312
334
 
313
- /* Dark mode override */
314
- :global([data-theme='dark']) .pipeline-status-container {
315
- background: linear-gradient(135deg, #141418 0%, #1a1a2e 50%, #16162a 100%);
335
+ .pipeline-status-container--embedded {
336
+ height: 100%;
337
+ background: var(--fd-muted);
338
+ --fd-layout-background: var(--fd-muted);
316
339
  }
317
340
  </style>
@@ -7,6 +7,11 @@ interface Props {
7
7
  apiClient?: EnhancedFlowDropApiClient;
8
8
  baseUrl?: string;
9
9
  endpointConfig?: EndpointConfig;
10
+ runLabel?: string;
11
+ /** When true, suppresses breadcrumb and layout events (used inside playground panel) */
12
+ isEmbedded?: boolean;
13
+ /** Increments when new messages arrive — triggers an immediate pipeline data refresh */
14
+ refreshTrigger?: number;
10
15
  onActionsReady?: (actions: Array<{
11
16
  label: string;
12
17
  href: string;
@@ -266,6 +266,32 @@
266
266
  }
267
267
  });
268
268
 
269
+ // Apply nodeStatuses from the parent (PipelineStatus embedded mode) to flowNodes
270
+ // whenever they change. loadNodeExecutionInfo() only fires on pipelineId change,
271
+ // so this is the update path for subsequent refreshes (e.g. after HITL resolution).
272
+ $effect(() => {
273
+ const statuses = props.nodeStatuses;
274
+ if (!statuses || Object.keys(statuses).length === 0) return;
275
+
276
+ flowNodes = untrack(() => flowNodes).map((node) => {
277
+ const rawStatus = statuses[node.id];
278
+ if (!rawStatus) return node;
279
+
280
+ const existing = node.data.executionInfo ?? { status: 'idle' as const, executionCount: 0, isExecuting: false };
281
+ return {
282
+ ...node,
283
+ data: {
284
+ ...node.data,
285
+ executionInfo: {
286
+ ...existing,
287
+ status: rawStatus === 'error' ? ('failed' as const) : rawStatus,
288
+ isExecuting: rawStatus === 'running'
289
+ }
290
+ }
291
+ };
292
+ });
293
+ });
294
+
269
295
  // ---------------------------------------------------------------------------
270
296
  // History restore callback
271
297
  // ---------------------------------------------------------------------------
@@ -191,10 +191,22 @@
191
191
  const msg = displayMessages[messageIndex];
192
192
  if (!msg?.commandPreview) return;
193
193
 
194
- // Capture pre-existing parse errors before execution. If the LLM produced
195
- // a malformed batch (e.g. unclosed """), retrying tends to reproduce the
196
- // same shape and just locks the input behind isLoading for the cascade.
197
- const hadParseErrors = msg.commandPreview.some((c) => c.status === 'error');
194
+ // Refuse to run the batch if any command failed to parse. A corrupted batch
195
+ // (e.g. multiline set without """) causes partial execution and can hang the
196
+ // app rejecting the whole batch is safer than executing the healthy subset.
197
+ const parseErrorCount = msg.commandPreview.filter((c) => c.status === 'error').length;
198
+ if (parseErrorCount > 0) {
199
+ for (const cmd of msg.commandPreview) {
200
+ if (cmd.status === 'pending') {
201
+ cmd.status = 'error';
202
+ cmd.result = 'Batch refused: fix parse errors before executing';
203
+ }
204
+ }
205
+ appendErrorToHistory(
206
+ `Batch was not executed: ${parseErrorCount} command${parseErrorCount > 1 ? 's have' : ' has'} parse errors. Dismiss this batch and ask the AI to provide corrected commands.`
207
+ );
208
+ return;
209
+ }
198
210
 
199
211
  const context = getCommandContext();
200
212
  if (!context) {
@@ -277,7 +289,6 @@
277
289
  }
278
290
 
279
291
  if (
280
- !hadParseErrors &&
281
292
  getBehaviorSettings().chatAutoRetry &&
282
293
  workflowId &&
283
294
  autoRetryCount < MAX_AUTO_RETRIES
@@ -22,6 +22,7 @@
22
22
  getMessages,
23
23
  getChatMessages,
24
24
  getIsExecuting,
25
+ getCanSendMessage,
25
26
  getSessionStatus,
26
27
  getCurrentSession
27
28
  } from '../../stores/playgroundStore.svelte.js';
@@ -74,6 +75,8 @@
74
75
  * @default true
75
76
  */
76
77
  compactSystemMessages?: boolean;
78
+ /** Whether log messages are visible — bindable so parent can host the toggle */
79
+ showLogs?: boolean;
77
80
  }
78
81
 
79
82
  let {
@@ -88,7 +91,8 @@
88
91
  showChatInput = true,
89
92
  showRunButton = true,
90
93
  predefinedMessage,
91
- compactSystemMessages = true
94
+ compactSystemMessages = true,
95
+ showLogs = $bindable(true)
92
96
  }: Props = $props();
93
97
 
94
98
  // Hoist playground branches — states/actions are read 8+ times each in the
@@ -125,36 +129,21 @@
125
129
  let inputField = $state<HTMLTextAreaElement>();
126
130
 
127
131
  /**
128
- * Filter messages based on showLogsInline setting
132
+ * Filter messages based on local showLogs toggle.
133
+ * The showLogsInline prop is still honoured as the initial hint when explicitly set to false.
129
134
  */
130
- const displayMessages = $derived(showLogsInline ? getMessages() : getChatMessages());
135
+ const displayMessages = $derived(showLogs ? getMessages() : getChatMessages());
131
136
 
132
- /**
133
- * Track previous message count for detecting new messages.
134
- * We only want to auto-scroll when NEW messages are added,
135
- * not when existing messages are updated.
136
- */
137
+ // ---------------------------------------------------------------------------
137
138
  let previousMessageCount = $state(0);
139
+ let userScrolledUp = $state(false);
138
140
 
139
- /**
140
- * Check if user is near the bottom of the scroll container.
141
- * Used to determine if we should auto-scroll when new messages arrive.
142
- * If user has scrolled up to read previous messages, we don't interrupt them.
143
- *
144
- * @param threshold - Pixels from bottom to consider "near bottom"
145
- * @returns True if user is within threshold of the bottom
146
- */
147
- function isNearBottom(threshold: number = 100): boolean {
148
- if (!messagesContainer) return true;
141
+ function handleScroll() {
142
+ if (!messagesContainer) return;
149
143
  const { scrollTop, scrollHeight, clientHeight } = messagesContainer;
150
- return scrollHeight - scrollTop - clientHeight <= threshold;
144
+ userScrolledUp = scrollHeight - scrollTop - clientHeight > 50;
151
145
  }
152
146
 
153
- /**
154
- * Check if a form element inside the messages container has focus.
155
- * When user is interacting with a form (e.g., interrupt prompt),
156
- * we should not auto-scroll as it disrupts their input.
157
- */
158
147
  function isFormFocused(): boolean {
159
148
  if (!messagesContainer) return false;
160
149
  const activeElement = document.activeElement;
@@ -249,7 +238,7 @@
249
238
  */
250
239
  function handleSend(): void {
251
240
  const trimmedValue = inputValue.trim();
252
- if (!trimmedValue || getIsExecuting()) {
241
+ if (!trimmedValue || !getCanSendMessage()) {
253
242
  return;
254
243
  }
255
244
 
@@ -332,58 +321,25 @@
332
321
  $effect(() => {
333
322
  const session = getCurrentSession();
334
323
  if (session) {
335
- // Reset to enabled state for new/changed sessions
336
324
  runEnabled = true;
337
- // Clear processed IDs for the new session
338
325
  processedEnableRunIds = new Set();
326
+ userScrolledUp = false;
339
327
  }
340
328
  });
341
329
 
342
- /**
343
- * Smart auto-scroll to bottom when NEW messages are added.
344
- *
345
- * Only scrolls if:
346
- * 1. autoScroll prop is enabled
347
- * 2. New messages were actually added (not just updates)
348
- * 3. User is already near the bottom (hasn't scrolled up to read)
349
- * 4. User is not interacting with a form inside the chat
350
- *
351
- * This prevents disruptive scrolling when:
352
- * - User is reading previous messages
353
- * - User is filling out an interrupt form
354
- * - Messages are being updated (e.g., status changes)
355
- */
356
330
  $effect(() => {
357
331
  const currentCount = displayMessages.length;
358
332
 
359
- // Skip if auto-scroll is disabled or no container
360
333
  if (!autoScroll || !messagesContainer) {
361
334
  previousMessageCount = currentCount;
362
335
  return;
363
336
  }
364
337
 
365
- // Check if this is a NEW message (count increased)
366
338
  const hasNewMessage = currentCount > previousMessageCount;
367
-
368
- // Update the tracked count
369
339
  previousMessageCount = currentCount;
370
340
 
371
- // Only scroll if there's a new message
372
- if (!hasNewMessage) {
373
- return;
374
- }
375
-
376
- // Don't scroll if user has scrolled up to read previous messages
377
- if (!isNearBottom()) {
378
- return;
379
- }
380
-
381
- // Don't scroll if user is interacting with a form
382
- if (isFormFocused()) {
383
- return;
384
- }
341
+ if (!hasNewMessage || userScrolledUp || isFormFocused()) return;
385
342
 
386
- // Safe to scroll to bottom
387
343
  tick().then(() => {
388
344
  if (messagesContainer) {
389
345
  messagesContainer.scrollTop = messagesContainer.scrollHeight;
@@ -391,50 +347,17 @@
391
347
  });
392
348
  });
393
349
 
394
- /**
395
- * Track previous executing state to detect when execution completes
396
- */
397
- let wasExecuting = $state(false);
398
-
399
- /**
400
- * Auto-focus input when execution completes or session becomes ready
401
- */
402
- $effect(() => {
403
- const currentlyExecuting = getIsExecuting();
404
-
405
- // Focus input when execution completes (was executing, now not)
406
- if (wasExecuting && !currentlyExecuting && inputField) {
407
- tick().then(() => {
408
- inputField?.focus();
409
- });
410
- }
411
-
412
- // Update tracking state
413
- wasExecuting = currentlyExecuting;
414
- });
415
-
416
- /**
417
- * Focus input when session status changes to idle or completed
418
- */
419
- $effect(() => {
420
- const status = getSessionStatus();
421
- if ((status === 'idle' || status === 'completed') && inputField && !getIsExecuting()) {
422
- tick().then(() => {
423
- inputField?.focus();
424
- });
425
- }
426
- });
350
+ let wasExecuting = false;
427
351
 
428
352
  /**
429
- * Focus input when a new session is created/loaded
353
+ * Auto-focus input when execution completes
430
354
  */
431
355
  $effect(() => {
432
- const session = getCurrentSession();
433
- if (session && inputField && !getIsExecuting()) {
434
- tick().then(() => {
435
- inputField?.focus();
436
- });
356
+ const nowExecuting = getIsExecuting();
357
+ if (wasExecuting && !nowExecuting && inputField) {
358
+ tick().then(() => inputField?.focus());
437
359
  }
360
+ wasExecuting = nowExecuting;
438
361
  });
439
362
 
440
363
  /**
@@ -450,7 +373,7 @@
450
373
 
451
374
  <div class="chat-panel">
452
375
  <!-- Messages Container -->
453
- <div class="chat-panel__messages" bind:this={messagesContainer}>
376
+ <div class="chat-panel__messages" bind:this={messagesContainer} onscroll={handleScroll}>
454
377
  {#if showWelcome}
455
378
  <!-- Welcome State (no session) -->
456
379
  <div class="chat-panel__welcome">
@@ -537,7 +460,6 @@
537
460
  <!-- Messages -->
538
461
  {#each displayMessages as message, index (message.id)}
539
462
  {#if isInterruptMessage(message)}
540
- <!-- Render interrupt inline -->
541
463
  {@const interrupt = getInterruptForMessage(message)}
542
464
  {#if interrupt}
543
465
  <InterruptBubble
@@ -598,7 +520,7 @@
598
520
  </div>
599
521
  {/if}
600
522
 
601
- {#if getSessionStatus() === 'running' || getIsExecuting()}
523
+ {#if getIsExecuting()}
602
524
  <button
603
525
  type="button"
604
526
  class="chat-panel__stop-btn"
@@ -613,7 +535,7 @@
613
535
  type="button"
614
536
  class="chat-panel__send-btn"
615
537
  onclick={handleSend}
616
- disabled={!inputValue.trim()}
538
+ disabled={!inputValue.trim() || !getCanSendMessage()}
617
539
  title={actions.sendTitle}
618
540
  >
619
541
  {actions.send}
@@ -644,13 +566,13 @@
644
566
  background-color: var(--fd-background);
645
567
  }
646
568
 
569
+
647
570
  /* Messages Container - Scrollable area that takes remaining space */
648
571
  .chat-panel__messages {
649
572
  flex: 1;
650
573
  min-height: 0; /* Critical: allows overflow to work in flex container */
651
574
  overflow-y: auto;
652
575
  padding: var(--fd-space-3xl);
653
- scroll-behavior: smooth;
654
576
  }
655
577
 
656
578
  /* Welcome State */
@@ -757,7 +679,7 @@
757
679
  display: flex;
758
680
  align-items: flex-end;
759
681
  gap: var(--fd-space-md);
760
- max-width: 800px;
682
+ max-width: 760px;
761
683
  margin: 0 auto;
762
684
  }
763
685
 
@@ -822,8 +744,9 @@
822
744
  }
823
745
 
824
746
  .chat-panel__send-btn:disabled {
825
- background-color: var(--fd-border);
826
- color: var(--fd-muted-foreground);
747
+ background-color: var(--fd-foreground);
748
+ color: var(--fd-background);
749
+ opacity: 0.3;
827
750
  cursor: not-allowed;
828
751
  }
829
752
 
@@ -890,7 +813,7 @@
890
813
  border-radius: var(--fd-radius-lg);
891
814
  color: var(--fd-muted-foreground);
892
815
  font-size: var(--fd-text-sm);
893
- max-width: 800px;
816
+ max-width: 760px;
894
817
  margin: 0 auto;
895
818
  }
896
819
 
@@ -40,7 +40,9 @@ interface Props {
40
40
  * @default true
41
41
  */
42
42
  compactSystemMessages?: boolean;
43
+ /** Whether log messages are visible — bindable so parent can host the toggle */
44
+ showLogs?: boolean;
43
45
  }
44
- declare const ChatPanel: import("svelte").Component<Props, {}, "">;
46
+ declare const ChatPanel: import("svelte").Component<Props, {}, "showLogs">;
45
47
  type ChatPanel = ReturnType<typeof ChatPanel>;
46
48
  export default ChatPanel;
@@ -0,0 +1,138 @@
1
+ <script lang="ts">
2
+ import Icon from '@iconify/svelte';
3
+ import type { PlaygroundExecution } from '../../types/playground.js';
4
+
5
+ interface Props {
6
+ executions: PlaygroundExecution[];
7
+ activeExecutionId: string | null;
8
+ latestExecutionId: string | null;
9
+ onSelect: (executionId: string) => void;
10
+ }
11
+
12
+ let { executions, activeExecutionId, latestExecutionId, onSelect }: Props = $props();
13
+
14
+ function statusIcon(status: PlaygroundExecution['status']): string {
15
+ if (status === 'completed') return 'mdi:check-circle';
16
+ if (status === 'failed') return 'mdi:alert-circle';
17
+ return '';
18
+ }
19
+ </script>
20
+
21
+ <div class="execution-list">
22
+ {#each executions as execution (execution.id)}
23
+ <div
24
+ class="execution-list__item"
25
+ class:execution-list__item--active={execution.id === activeExecutionId}
26
+ class:execution-list__item--running={execution.status === 'running'}
27
+ class:execution-list__item--completed={execution.status === 'completed'}
28
+ class:execution-list__item--failed={execution.status === 'failed'}
29
+ role="button"
30
+ tabindex="0"
31
+ onclick={() => onSelect(execution.id)}
32
+ onkeydown={(e) => e.key === 'Enter' && onSelect(execution.id)}
33
+ >
34
+ {#if execution.status === 'running'}
35
+ <span class="execution-list__running-dot" aria-hidden="true"></span>
36
+ {:else if statusIcon(execution.status)}
37
+ <Icon
38
+ icon={statusIcon(execution.status)}
39
+ class="execution-list__status-icon execution-list__status-icon--{execution.status}"
40
+ />
41
+ {/if}
42
+ <span class="execution-list__label">{execution.id}</span>
43
+ {#if execution.id === latestExecutionId}
44
+ <span class="execution-list__badge">latest</span>
45
+ {/if}
46
+ </div>
47
+ {/each}
48
+ </div>
49
+
50
+ <style>
51
+ /* Match the visual weight of .playground__session items */
52
+ .execution-list {
53
+ display: flex;
54
+ flex-direction: column;
55
+ gap: var(--fd-space-3xs);
56
+ padding: 0 var(--fd-space-sm);
57
+ }
58
+
59
+ .execution-list__item {
60
+ display: flex;
61
+ align-items: center;
62
+ gap: var(--fd-space-xs);
63
+ padding: var(--fd-space-sm) var(--fd-space-md);
64
+ border-radius: var(--fd-radius-md);
65
+ border-left: 3px solid transparent;
66
+ cursor: pointer;
67
+ font-size: var(--fd-text-sm);
68
+ color: var(--fd-foreground);
69
+ transition:
70
+ background-color var(--fd-transition-fast),
71
+ border-left-color var(--fd-transition-fast);
72
+ user-select: none;
73
+ }
74
+
75
+ .execution-list__item:hover {
76
+ background-color: var(--fd-muted);
77
+ border-left-color: var(--fd-border);
78
+ }
79
+
80
+ .execution-list__item--active {
81
+ background-color: var(--fd-primary-muted);
82
+ border-left-color: var(--fd-primary);
83
+ }
84
+
85
+ .execution-list__item--active:hover {
86
+ background-color: var(--fd-primary-muted);
87
+ border-left-color: var(--fd-primary);
88
+ }
89
+
90
+ .execution-list__label {
91
+ flex: 1;
92
+ min-width: 0;
93
+ overflow: hidden;
94
+ text-overflow: ellipsis;
95
+ white-space: nowrap;
96
+ }
97
+
98
+ .execution-list__item--active .execution-list__label {
99
+ color: var(--fd-primary);
100
+ font-weight: 500;
101
+ }
102
+
103
+ .execution-list__badge {
104
+ font-size: var(--fd-text-2xs);
105
+ font-weight: 600;
106
+ text-transform: uppercase;
107
+ letter-spacing: 0.04em;
108
+ color: var(--fd-success);
109
+ flex-shrink: 0;
110
+ }
111
+
112
+ .execution-list__running-dot {
113
+ width: 6px;
114
+ height: 6px;
115
+ border-radius: 50%;
116
+ background-color: var(--fd-success);
117
+ flex-shrink: 0;
118
+ animation: pulse 1.5s ease-in-out infinite;
119
+ }
120
+
121
+ @keyframes pulse {
122
+ 0%, 100% { opacity: 1; }
123
+ 50% { opacity: 0.4; }
124
+ }
125
+
126
+ :global(.execution-list__status-icon) {
127
+ flex-shrink: 0;
128
+ font-size: var(--fd-text-sm);
129
+ }
130
+
131
+ :global(.execution-list__status-icon--completed) {
132
+ color: var(--fd-success, #22c55e);
133
+ }
134
+
135
+ :global(.execution-list__status-icon--failed) {
136
+ color: var(--fd-error, #ef4444);
137
+ }
138
+ </style>
@@ -0,0 +1,10 @@
1
+ import type { PlaygroundExecution } from '../../types/playground.js';
2
+ interface Props {
3
+ executions: PlaygroundExecution[];
4
+ activeExecutionId: string | null;
5
+ latestExecutionId: string | null;
6
+ onSelect: (executionId: string) => void;
7
+ }
8
+ declare const ExecutionList: import("svelte").Component<Props, {}, "">;
9
+ type ExecutionList = ReturnType<typeof ExecutionList>;
10
+ export default ExecutionList;