@flowdrop/flowdrop 1.12.0 → 1.14.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 (71) hide show
  1. package/README.md +5 -0
  2. package/dist/components/ConfigForm.svelte +1 -0
  3. package/dist/components/ConfigPanel.svelte +7 -1
  4. package/dist/components/NodeSwapPicker.svelte +5 -1
  5. package/dist/components/PipelineStatus.svelte +11 -2
  6. package/dist/components/SchemaForm.svelte +1 -0
  7. package/dist/components/SettingsPanel.svelte +5 -1
  8. package/dist/components/WorkflowEditor.svelte +5 -1
  9. package/dist/components/chat/AIChatPanel.svelte +1 -5
  10. package/dist/components/form/FormAutocomplete.svelte +69 -15
  11. package/dist/components/form/FormField.svelte +21 -0
  12. package/dist/components/form/FormFieldLight.svelte +1 -0
  13. package/dist/components/interrupt/ChoicePrompt.svelte +5 -1
  14. package/dist/components/interrupt/InterruptBubble.svelte +75 -17
  15. package/dist/components/interrupt/InterruptBubble.svelte.d.ts +11 -0
  16. package/dist/components/playground/ChatBubble.svelte +287 -0
  17. package/dist/components/playground/ChatBubble.svelte.d.ts +10 -0
  18. package/dist/components/playground/ChatInput.svelte +11 -5
  19. package/dist/components/playground/ControlPanel.svelte +42 -29
  20. package/dist/components/playground/ExecutionConsole.svelte +5 -1
  21. package/dist/components/playground/ExecutionConsole.svelte.d.ts +2 -0
  22. package/dist/components/playground/ExecutionList.svelte +7 -2
  23. package/dist/components/playground/HierarchyTrail.svelte +88 -0
  24. package/dist/components/playground/HierarchyTrail.svelte.d.ts +7 -0
  25. package/dist/components/playground/LogRow.svelte +179 -0
  26. package/dist/components/playground/LogRow.svelte.d.ts +8 -0
  27. package/dist/components/playground/MessageBubble.stories.svelte +89 -0
  28. package/dist/components/playground/MessageBubble.svelte +23 -738
  29. package/dist/components/playground/MessageBubble.svelte.d.ts +3 -11
  30. package/dist/components/playground/MessageCard.svelte +107 -0
  31. package/dist/components/playground/MessageCard.svelte.d.ts +10 -0
  32. package/dist/components/playground/MessageMarkdown.svelte +170 -0
  33. package/dist/components/playground/MessageMarkdown.svelte.d.ts +7 -0
  34. package/dist/components/playground/MessageNotice.svelte +121 -0
  35. package/dist/components/playground/MessageNotice.svelte.d.ts +9 -0
  36. package/dist/components/playground/MessageStream.svelte +215 -10
  37. package/dist/components/playground/MessageStream.svelte.d.ts +5 -0
  38. package/dist/components/playground/MessageTagChip.svelte +117 -0
  39. package/dist/components/playground/MessageTagChip.svelte.d.ts +7 -0
  40. package/dist/components/playground/MessageTagStrip.svelte +37 -0
  41. package/dist/components/playground/MessageTagStrip.svelte.d.ts +7 -0
  42. package/dist/components/playground/PipelineKanbanView.svelte +40 -11
  43. package/dist/components/playground/PipelinePanel.svelte +5 -1
  44. package/dist/components/playground/PipelineTableView.svelte +20 -6
  45. package/dist/components/playground/Playground.svelte +84 -22
  46. package/dist/components/playground/PlaygroundStudio.svelte +99 -7
  47. package/dist/components/playground/messageDisplay.d.ts +19 -0
  48. package/dist/components/playground/messageDisplay.js +62 -0
  49. package/dist/components/playground/pipelineViewUtils.svelte.js +11 -4
  50. package/dist/form/autocomplete.d.ts +1 -0
  51. package/dist/form/autocomplete.js +1 -0
  52. package/dist/form/index.d.ts +17 -0
  53. package/dist/form/index.js +19 -0
  54. package/dist/messages/defaults.d.ts +5 -0
  55. package/dist/messages/defaults.js +6 -0
  56. package/dist/openapi/v1/openapi.yaml +6403 -0
  57. package/dist/schemas/v1/workflow.schema.json +46 -1
  58. package/dist/services/categoriesApi.d.ts +2 -1
  59. package/dist/services/categoriesApi.js +5 -3
  60. package/dist/services/playgroundService.d.ts +23 -4
  61. package/dist/services/playgroundService.js +22 -9
  62. package/dist/services/portConfigApi.d.ts +2 -1
  63. package/dist/services/portConfigApi.js +5 -3
  64. package/dist/stores/playgroundStore.svelte.d.ts +22 -1
  65. package/dist/stores/playgroundStore.svelte.js +109 -32
  66. package/dist/svelte-app.d.ts +1 -0
  67. package/dist/svelte-app.js +5 -5
  68. package/dist/types/index.d.ts +13 -0
  69. package/dist/types/playground.d.ts +112 -2
  70. package/dist/types/playground.js +14 -0
  71. package/package.json +12 -1
@@ -24,7 +24,8 @@
24
24
  getChatMessages,
25
25
  getIsExecuting,
26
26
  getCurrentSession,
27
- getShowLogs
27
+ getShowLogs,
28
+ getHasOlder
28
29
  } from '../../stores/playgroundStore.svelte.js';
29
30
  import {
30
31
  getInterruptsMap,
@@ -51,6 +52,11 @@
51
52
  compactSystemMessages?: boolean;
52
53
  /** Called when an interrupt is resolved */
53
54
  onInterruptResolved?: () => void;
55
+ /**
56
+ * Called when the user scrolls near the top, to load older messages.
57
+ * When omitted, scroll-up paging is disabled (e.g. view-only surfaces).
58
+ */
59
+ onLoadOlder?: () => void | Promise<void>;
54
60
  /** Custom render for the no-session welcome state */
55
61
  welcome?: Snippet;
56
62
  /** Custom render for the empty-session state */
@@ -64,6 +70,7 @@
64
70
  allowLogs = false,
65
71
  compactSystemMessages = true,
66
72
  onInterruptResolved,
73
+ onLoadOlder,
67
74
  welcome,
68
75
  emptySession
69
76
  }: Props = $props();
@@ -71,14 +78,14 @@
71
78
  const states = $derived(m().playground.states);
72
79
 
73
80
  /** Reference to the messages container for scrolling */
74
- let messagesContainer: HTMLDivElement | undefined;
81
+ let messagesContainer = $state<HTMLDivElement | undefined>();
75
82
 
76
- const displayMessages = $derived(
77
- allowLogs && getShowLogs() ? getMessages() : getChatMessages()
78
- );
83
+ const displayMessages = $derived(allowLogs && getShowLogs() ? getMessages() : getChatMessages());
79
84
 
80
85
  let previousMessageCount = 0;
81
86
  let userScrolledUp = false;
87
+ let isLoadingOlder = $state(false);
88
+ let topSentinel = $state<HTMLDivElement | undefined>();
82
89
 
83
90
  function handleScroll() {
84
91
  if (!messagesContainer) return;
@@ -86,6 +93,48 @@
86
93
  userScrolledUp = scrollHeight - scrollTop - clientHeight > 50;
87
94
  }
88
95
 
96
+ // Load older messages when the top sentinel scrolls into view. An observer is
97
+ // self-throttling and keeps layout reads off the scroll path; rootMargin
98
+ // pre-fetches the next page slightly before the user reaches the very top.
99
+ $effect(() => {
100
+ if (!topSentinel || !messagesContainer || !onLoadOlder) return;
101
+ const observer = new IntersectionObserver(
102
+ (entries) => {
103
+ if (entries[0]?.isIntersecting) void loadOlder();
104
+ },
105
+ { root: messagesContainer, rootMargin: '300px 0px 0px 0px' }
106
+ );
107
+ observer.observe(topSentinel);
108
+ return () => observer.disconnect();
109
+ });
110
+
111
+ /**
112
+ * Fetch the previous page and keep the viewport pinned to the message the
113
+ * user is reading. We anchor on a real DOM node (the first rendered message)
114
+ * and compensate scrollTop by however far it moved — robust against the
115
+ * loading spinner (which is out of flow) and any late reflow above it.
116
+ * A scroll during the in-flight fetch is intentionally overridden so the
117
+ * prepend doesn't shift the reading position.
118
+ */
119
+ async function loadOlder() {
120
+ if (!onLoadOlder || !messagesContainer || isLoadingOlder || !getHasOlder()) return;
121
+
122
+ const anchor = topSentinel?.nextElementSibling as HTMLElement | null;
123
+ const anchorTopBefore = anchor?.getBoundingClientRect().top ?? 0;
124
+
125
+ isLoadingOlder = true;
126
+ try {
127
+ await onLoadOlder();
128
+ await tick();
129
+ if (messagesContainer && anchor) {
130
+ const shift = anchor.getBoundingClientRect().top - anchorTopBefore;
131
+ messagesContainer.scrollTop += shift;
132
+ }
133
+ } finally {
134
+ isLoadingOlder = false;
135
+ }
136
+ }
137
+
89
138
  function isFormFocused(): boolean {
90
139
  if (!messagesContainer) return false;
91
140
  const activeElement = document.activeElement;
@@ -154,14 +203,20 @@
154
203
  const currentCount = displayMessages.length;
155
204
 
156
205
  if (!autoScroll || !messagesContainer) {
157
- untrack(() => { previousMessageCount = currentCount; });
206
+ untrack(() => {
207
+ previousMessageCount = currentCount;
208
+ });
158
209
  return;
159
210
  }
160
211
 
161
212
  const hasNewMessage = currentCount > previousMessageCount;
162
- untrack(() => { previousMessageCount = currentCount; });
213
+ untrack(() => {
214
+ previousMessageCount = currentCount;
215
+ });
163
216
 
164
- if (!hasNewMessage || userScrolledUp || isFormFocused()) return;
217
+ // Don't chase the bottom while a backward page is landing — loadOlder owns
218
+ // scroll position during a prepend and anchors it to the message in view.
219
+ if (!hasNewMessage || userScrolledUp || isFormFocused() || isLoadingOlder) return;
165
220
 
166
221
  tick().then(() => {
167
222
  if (messagesContainer) {
@@ -171,7 +226,13 @@
171
226
  });
172
227
  </script>
173
228
 
174
- <div class="message-stream" role="log" aria-label={m().playground.controlPanel.messageStreamLabel} bind:this={messagesContainer} onscroll={handleScroll}>
229
+ <div
230
+ class="message-stream"
231
+ role="log"
232
+ aria-label={m().playground.controlPanel.messageStreamLabel}
233
+ bind:this={messagesContainer}
234
+ onscroll={handleScroll}
235
+ >
175
236
  {#if showWelcome}
176
237
  {#if welcome}
177
238
  {@render welcome()}
@@ -181,6 +242,12 @@
181
242
  {@render emptySession()}
182
243
  {/if}
183
244
  {:else}
245
+ <div bind:this={topSentinel} class="message-stream__sentinel" aria-hidden="true"></div>
246
+ {#if isLoadingOlder}
247
+ <div class="message-stream__loading-older" aria-hidden="true">
248
+ <span class="message-stream__loading-older-spinner"></span>
249
+ </div>
250
+ {/if}
184
251
  {#each displayMessages as message, index (message.id)}
185
252
  {#if isInterruptMessage(message)}
186
253
  {@const interrupt = getInterruptForMessage(message)}
@@ -189,6 +256,8 @@
189
256
  {interrupt}
190
257
  showTimestamp={showTimestamps}
191
258
  onResolved={onInterruptResolved}
259
+ hierarchy={message.hierarchy}
260
+ tags={message.tags}
192
261
  />
193
262
  {/if}
194
263
  {:else}
@@ -217,10 +286,146 @@
217
286
 
218
287
  <style>
219
288
  .message-stream {
289
+ position: relative;
220
290
  flex: 1;
221
291
  min-height: 0;
222
292
  overflow-y: auto;
223
293
  padding: var(--fd-space-3xl);
294
+
295
+ /* Establish a containment context so message rows can adapt to the
296
+ stream's actual width (not the viewport's). The matching @container
297
+ queries (for .log-row) live below in the same <style> block, so
298
+ renaming the container only requires editing this file. */
299
+ container-type: inline-size;
300
+ container-name: fd-message-stream;
301
+ }
302
+
303
+ /* Zero-height marker the IntersectionObserver watches to trigger
304
+ backward pagination as it nears the top of the scroll area. */
305
+ .message-stream__sentinel {
306
+ height: 0;
307
+ }
308
+
309
+ /* Shared fade-in for newly-appended message rows. `-global-` so
310
+ ChatBubble.svelte / MessageCard.svelte can reference it without
311
+ redeclaring. Honour reduced-motion in the same place. */
312
+ @keyframes -global-fd-fade-in {
313
+ from {
314
+ opacity: 0;
315
+ transform: translateY(6px);
316
+ }
317
+ to {
318
+ opacity: 1;
319
+ transform: translateY(0);
320
+ }
321
+ }
322
+
323
+ @media (prefers-reduced-motion: reduce) {
324
+ :global(.message-bubble),
325
+ :global(.message-card) {
326
+ animation: none;
327
+ }
328
+ }
329
+
330
+ /* Container-query reshaping for log rows. Lives next to the
331
+ container-name declaration so the coupling is local — selectors are
332
+ :global because .log-row is a sibling component's class.
333
+
334
+ Tier 1 (≤720px): two rows — level/body, then tags/timestamp.
335
+ Tier 2 (≤480px): collapse further; body forces internal line break. */
336
+ @container fd-message-stream (max-width: 720px) {
337
+ :global(.log-row) {
338
+ display: grid;
339
+ grid-template-columns: auto 1fr auto;
340
+ grid-template-areas:
341
+ 'level body body'
342
+ '. tags timestamp';
343
+ align-items: baseline;
344
+ row-gap: var(--fd-space-2xs);
345
+ column-gap: var(--fd-space-sm);
346
+ }
347
+ :global(.log-row__level) {
348
+ grid-area: level;
349
+ }
350
+ :global(.log-row__body) {
351
+ grid-area: body;
352
+ min-width: 0;
353
+ }
354
+ :global(.log-row__tags) {
355
+ grid-area: tags;
356
+ justify-self: start;
357
+ }
358
+ :global(.log-row__timestamp) {
359
+ grid-area: timestamp;
360
+ justify-self: end;
361
+ }
362
+ }
363
+
364
+ @container fd-message-stream (max-width: 480px) {
365
+ :global(.log-row) {
366
+ grid-template-columns: auto 1fr;
367
+ grid-template-areas:
368
+ 'level body'
369
+ '. tags';
370
+ }
371
+ :global(.log-row__text) {
372
+ flex-basis: 100%;
373
+ min-width: 0;
374
+ }
375
+ :global(.log-row__timestamp) {
376
+ display: none;
377
+ }
378
+ /* Drop the source + node chips: source is implied by the level
379
+ colour, node duplicates the hierarchy trail's last entry. Keeping
380
+ them at this width forced each chip onto its own line and made
381
+ log rows 5–6 lines tall. */
382
+ :global(.log-row__source),
383
+ :global(.log-row__node) {
384
+ display: none;
385
+ }
386
+ /* Reclaim horizontal room by tightening the row's own padding —
387
+ can't shrink the stream's padding from inside its own
388
+ container query. */
389
+ :global(.log-row) {
390
+ padding-left: var(--fd-space-xs);
391
+ padding-right: var(--fd-space-xs);
392
+ }
393
+ }
394
+
395
+ /* Overlay, out of flow — its presence must not shift message layout, or it
396
+ would corrupt the scroll anchoring in loadOlder(). */
397
+ .message-stream__loading-older {
398
+ position: absolute;
399
+ top: 0;
400
+ left: 0;
401
+ right: 0;
402
+ display: flex;
403
+ align-items: center;
404
+ justify-content: center;
405
+ padding: var(--fd-space-sm) 0;
406
+ pointer-events: none;
407
+ z-index: 1;
408
+ }
409
+
410
+ .message-stream__loading-older-spinner {
411
+ width: var(--fd-space-lg);
412
+ height: var(--fd-space-lg);
413
+ border: 2px solid var(--fd-border-strong);
414
+ border-top-color: transparent;
415
+ border-radius: var(--fd-radius-full);
416
+ animation: message-stream-spin 0.8s linear infinite;
417
+ }
418
+
419
+ @keyframes message-stream-spin {
420
+ to {
421
+ transform: rotate(360deg);
422
+ }
423
+ }
424
+
425
+ @media (prefers-reduced-motion: reduce) {
426
+ .message-stream__loading-older-spinner {
427
+ animation: none;
428
+ }
224
429
  }
225
430
 
226
431
  .message-stream__typing {
@@ -277,7 +482,7 @@
277
482
 
278
483
  @media (max-width: 640px) {
279
484
  .message-stream {
280
- padding: var(--fd-space-xl);
485
+ padding: var(--fd-space-md) 0;
281
486
  }
282
487
  }
283
488
  </style>
@@ -17,6 +17,11 @@ interface Props {
17
17
  compactSystemMessages?: boolean;
18
18
  /** Called when an interrupt is resolved */
19
19
  onInterruptResolved?: () => void;
20
+ /**
21
+ * Called when the user scrolls near the top, to load older messages.
22
+ * When omitted, scroll-up paging is disabled (e.g. view-only surfaces).
23
+ */
24
+ onLoadOlder?: () => void | Promise<void>;
20
25
  /** Custom render for the no-session welcome state */
21
26
  welcome?: Snippet;
22
27
  /** Custom render for the empty-session state */
@@ -0,0 +1,117 @@
1
+ <!--
2
+ MessageTagChip Component
3
+
4
+ Renders a single server-emitted MessageTag as a compact chip. Semantic
5
+ color comes from tag.color, visual emphasis from tag.variant. Used by
6
+ MessageBubble and InterruptBubble.
7
+
8
+ Styling: a single base rule reads from CSS custom properties; one rule
9
+ per color sets --chip-c, one rule per variant sets bg/fg/border in terms
10
+ of --chip-c. Adding a color is one line.
11
+ -->
12
+
13
+ <script lang="ts">
14
+ import Icon from '@iconify/svelte';
15
+ import type { MessageTag } from '../../types/playground.js';
16
+
17
+ interface Props {
18
+ tag: MessageTag;
19
+ }
20
+
21
+ let { tag }: Props = $props();
22
+
23
+ const color = $derived(tag.color ?? 'muted');
24
+ const variant = $derived(tag.variant ?? 'subtle');
25
+ </script>
26
+
27
+ <span
28
+ class="message-tag-chip"
29
+ data-color={color}
30
+ data-variant={variant}
31
+ aria-label={tag.type ? `${tag.type}: ${tag.label}` : undefined}
32
+ >
33
+ {#if tag.icon}
34
+ <Icon icon={tag.icon} class="message-tag-chip__icon" aria-hidden="true" />
35
+ {/if}
36
+ <span class="message-tag-chip__label">{tag.label}</span>
37
+ </span>
38
+
39
+ <style>
40
+ .message-tag-chip {
41
+ display: inline-flex;
42
+ align-items: center;
43
+ gap: var(--fd-space-3xs);
44
+ padding: 0 var(--fd-space-3xs);
45
+ border-radius: var(--fd-radius-sm);
46
+ font-family: var(--fd-font-mono);
47
+ font-size: var(--fd-text-2xs);
48
+ line-height: 1.4;
49
+ white-space: nowrap;
50
+ min-width: 0;
51
+ max-width: 100%;
52
+ background-color: var(--chip-bg);
53
+ color: var(--chip-fg);
54
+ border: 1px solid var(--chip-border, transparent);
55
+ }
56
+
57
+ .message-tag-chip__label {
58
+ overflow: hidden;
59
+ text-overflow: ellipsis;
60
+ }
61
+
62
+ .message-tag-chip :global(.message-tag-chip__icon) {
63
+ flex-shrink: 0;
64
+ font-size: 0.875em;
65
+ opacity: 0.8;
66
+ }
67
+
68
+ /* Color hooks — one line per color. To add a color, add a row here. */
69
+ .message-tag-chip[data-color='muted'] {
70
+ --chip-c: var(--fd-muted-foreground);
71
+ --chip-c-on: var(--fd-background);
72
+ }
73
+ .message-tag-chip[data-color='primary'] {
74
+ --chip-c: var(--fd-primary);
75
+ --chip-c-on: var(--fd-primary-foreground);
76
+ }
77
+ .message-tag-chip[data-color='success'] {
78
+ --chip-c: var(--fd-success, oklch(55% 0.15 145));
79
+ --chip-c-on: white;
80
+ }
81
+ .message-tag-chip[data-color='warning'] {
82
+ --chip-c: var(--fd-warning);
83
+ --chip-c-on: var(--fd-background);
84
+ }
85
+ .message-tag-chip[data-color='error'] {
86
+ --chip-c: var(--fd-error);
87
+ --chip-c-on: white;
88
+ }
89
+ .message-tag-chip[data-color='info'] {
90
+ --chip-c: var(--fd-info);
91
+ --chip-c-on: var(--fd-background);
92
+ }
93
+
94
+ /* Variants — derive bg/fg/border from --chip-c. */
95
+ .message-tag-chip[data-variant='subtle'] {
96
+ --chip-bg: color-mix(in srgb, var(--chip-c) 14%, transparent);
97
+ --chip-fg: var(--chip-c);
98
+ }
99
+ .message-tag-chip[data-variant='subtle'][data-color='muted'] {
100
+ /* Muted is the only color we render against the design's --fd-muted
101
+ surface for legibility; the color-mix path would lose contrast. */
102
+ --chip-bg: var(--fd-muted);
103
+ --chip-fg: var(--fd-muted-foreground);
104
+ }
105
+ .message-tag-chip[data-variant='solid'] {
106
+ --chip-bg: var(--chip-c);
107
+ --chip-fg: var(--chip-c-on);
108
+ }
109
+ .message-tag-chip[data-variant='outline'] {
110
+ --chip-bg: transparent;
111
+ --chip-fg: var(--chip-c);
112
+ --chip-border: var(--chip-c);
113
+ }
114
+ .message-tag-chip[data-variant='outline'][data-color='muted'] {
115
+ --chip-border: var(--fd-border);
116
+ }
117
+ </style>
@@ -0,0 +1,7 @@
1
+ import type { MessageTag } from '../../types/playground.js';
2
+ interface Props {
3
+ tag: MessageTag;
4
+ }
5
+ declare const MessageTagChip: import("svelte").Component<Props, {}, "">;
6
+ type MessageTagChip = ReturnType<typeof MessageTagChip>;
7
+ export default MessageTagChip;
@@ -0,0 +1,37 @@
1
+ <!--
2
+ MessageTagStrip Component
3
+
4
+ A flex-wrapped row of MessageTagChips. Encapsulates the layout the four
5
+ message variants used to copy individually.
6
+ -->
7
+
8
+ <script lang="ts">
9
+ import type { MessageTag } from '../../types/playground.js';
10
+ import MessageTagChip from './MessageTagChip.svelte';
11
+
12
+ interface Props {
13
+ tags: MessageTag[];
14
+ }
15
+
16
+ let { tags }: Props = $props();
17
+ </script>
18
+
19
+ {#if tags.length > 0}
20
+ <!-- No role/label — each chip is already labelled (via tag.type when set)
21
+ and a one-element group adds nothing for AT users. -->
22
+ <span class="message-tag-strip">
23
+ {#each tags as tag (tag.id)}
24
+ <MessageTagChip {tag} />
25
+ {/each}
26
+ </span>
27
+ {/if}
28
+
29
+ <style>
30
+ .message-tag-strip {
31
+ display: inline-flex;
32
+ flex-wrap: wrap;
33
+ align-items: center;
34
+ gap: var(--fd-space-2xs);
35
+ min-width: 0;
36
+ }
37
+ </style>
@@ -0,0 +1,7 @@
1
+ import type { MessageTag } from '../../types/playground.js';
2
+ interface Props {
3
+ tags: MessageTag[];
4
+ }
5
+ declare const MessageTagStrip: import("svelte").Component<Props, {}, "">;
6
+ type MessageTagStrip = ReturnType<typeof MessageTagStrip>;
7
+ export default MessageTagStrip;
@@ -2,10 +2,34 @@
2
2
  import type { KanbanColumnDef } from '../../types/index.js';
3
3
 
4
4
  const DEFAULT_COLUMNS: KanbanColumnDef[] = [
5
- { key: 'pending', label: 'Pending', statuses: ['idle', 'pending'], icon: 'mdi:clock-outline', color: 'var(--fd-muted-foreground)' },
6
- { key: 'in_progress', label: 'In Progress', statuses: ['running', 'paused', 'interrupted'], icon: 'mdi:play-circle-outline', color: 'var(--fd-warning)' },
7
- { key: 'done', label: 'Done', statuses: ['completed', 'skipped'], icon: 'mdi:check-circle', color: 'var(--fd-success)' },
8
- { key: 'failed', label: 'Failed', statuses: ['failed', 'cancelled'], icon: 'mdi:alert-circle', color: 'var(--fd-error)' },
5
+ {
6
+ key: 'pending',
7
+ label: 'Pending',
8
+ statuses: ['idle', 'pending'],
9
+ icon: 'mdi:clock-outline',
10
+ color: 'var(--fd-muted-foreground)'
11
+ },
12
+ {
13
+ key: 'in_progress',
14
+ label: 'In Progress',
15
+ statuses: ['running', 'paused', 'interrupted'],
16
+ icon: 'mdi:play-circle-outline',
17
+ color: 'var(--fd-warning)'
18
+ },
19
+ {
20
+ key: 'done',
21
+ label: 'Done',
22
+ statuses: ['completed', 'skipped'],
23
+ icon: 'mdi:check-circle',
24
+ color: 'var(--fd-success)'
25
+ },
26
+ {
27
+ key: 'failed',
28
+ label: 'Failed',
29
+ statuses: ['failed', 'cancelled'],
30
+ icon: 'mdi:alert-circle',
31
+ color: 'var(--fd-error)'
32
+ }
9
33
  ];
10
34
  </script>
11
35
 
@@ -13,7 +37,11 @@
13
37
  import { onMount } from 'svelte';
14
38
  import Icon from '@iconify/svelte';
15
39
  import { createPipelineDataFetcher, resolveStatus } from './pipelineViewUtils.svelte.js';
16
- import { getStatusLabel, getStatusTextColor, getStatusBackgroundColor } from '../../utils/nodeStatus.js';
40
+ import {
41
+ getStatusLabel,
42
+ getStatusTextColor,
43
+ getStatusBackgroundColor
44
+ } from '../../utils/nodeStatus.js';
17
45
  import type { NodeStatus } from './pipelineViewUtils.svelte.js';
18
46
  import type { Workflow, WorkflowNode } from '../../types/index.js';
19
47
  import type { EndpointConfig } from '../../config/endpoints.js';
@@ -27,6 +55,7 @@
27
55
 
28
56
  let { pipelineId, workflow, endpointConfig, refreshTrigger = 0 }: Props = $props();
29
57
 
58
+ // svelte-ignore state_referenced_locally — endpointConfig is consumed once to build the API client; it must be stable
30
59
  const fetcher = createPipelineDataFetcher(() => pipelineId, endpointConfig);
31
60
 
32
61
  $effect(() => {
@@ -88,10 +117,7 @@
88
117
  style="--col-color: {col.color ?? 'var(--fd-muted-foreground)'}"
89
118
  >
90
119
  <div class="pipeline-kanban__column-header">
91
- <Icon
92
- icon={col.icon ?? 'mdi:circle-outline'}
93
- class="pipeline-kanban__col-icon"
94
- />
120
+ <Icon icon={col.icon ?? 'mdi:circle-outline'} class="pipeline-kanban__col-icon" />
95
121
  <span class="pipeline-kanban__col-label">{col.label}</span>
96
122
  <span class="pipeline-kanban__col-count">{items.length}</span>
97
123
  </div>
@@ -104,8 +130,11 @@
104
130
  {#if showStatusPill}
105
131
  <span
106
132
  class="pipeline-kanban__card-status"
107
- style="color: {getStatusTextColor(status)}; background-color: {getStatusBackgroundColor(status)}"
108
- >{getStatusLabel(status)}</span>
133
+ style="color: {getStatusTextColor(
134
+ status
135
+ )}; background-color: {getStatusBackgroundColor(status)}"
136
+ >{getStatusLabel(status)}</span
137
+ >
109
138
  {/if}
110
139
  </div>
111
140
  <span class="pipeline-kanban__card-type">{node.data.metadata.id}</span>
@@ -2,7 +2,7 @@
2
2
  const VIEW_MODE_KEY = 'fd-pipeline-view-mode';
3
3
  const BUILTIN_VIEWS = ['graph', 'kanban', 'table'] as const;
4
4
  // `string & {}` preserves autocomplete for built-in values while still accepting arbitrary strings from extraViews.
5
- type ViewMode = typeof BUILTIN_VIEWS[number] | (string & {});
5
+ type ViewMode = (typeof BUILTIN_VIEWS)[number] | (string & {});
6
6
  </script>
7
7
 
8
8
  <script lang="ts">
@@ -177,6 +177,7 @@
177
177
  class="pipeline-panel__run-popover"
178
178
  bind:this={runPopoverEl}
179
179
  role="menu"
180
+ tabindex="-1"
180
181
  onkeydown={(e) => {
181
182
  if (e.key === 'Escape') {
182
183
  runDropdownOpen = false;
@@ -415,6 +416,9 @@
415
416
  right: 0;
416
417
  z-index: 50;
417
418
  min-width: 160px;
419
+ max-width: 320px;
420
+ max-height: min(60vh, 420px);
421
+ overflow-y: auto;
418
422
  padding: var(--fd-space-xs);
419
423
  background-color: var(--fd-background);
420
424
  border: 1px solid var(--fd-border);
@@ -64,6 +64,7 @@
64
64
  statusData: NodeStatusData | undefined;
65
65
  }
66
66
 
67
+ // svelte-ignore state_referenced_locally — endpointConfig is consumed once to build the API client; it must be stable
67
68
  const fetcher = createPipelineDataFetcher(() => pipelineId, endpointConfig);
68
69
 
69
70
  $effect(() => {
@@ -137,14 +138,24 @@
137
138
  {#if expandable}
138
139
  <Icon
139
140
  icon="mdi:chevron-right"
140
- class="pipeline-table__chevron {expanded ? 'pipeline-table__chevron--open' : ''}"
141
+ class="pipeline-table__chevron {expanded
142
+ ? 'pipeline-table__chevron--open'
143
+ : ''}"
141
144
  />
142
145
  {/if}
143
146
  </td>
144
- <td class="pipeline-table__td pipeline-table__td--label" title={row.node.data.label}>{row.node.data.label}</td>
145
- <td class="pipeline-table__td pipeline-table__td--muted" title={row.node.data.metadata.id}>{row.node.data.metadata.id}</td>
147
+ <td class="pipeline-table__td pipeline-table__td--label" title={row.node.data.label}
148
+ >{row.node.data.label}</td
149
+ >
150
+ <td
151
+ class="pipeline-table__td pipeline-table__td--muted"
152
+ title={row.node.data.metadata.id}>{row.node.data.metadata.id}</td
153
+ >
146
154
  <td class="pipeline-table__td">
147
- <span class="pipeline-table__status" style="color: {getStatusTextColor(row.status)}">
155
+ <span
156
+ class="pipeline-table__status"
157
+ style="color: {getStatusTextColor(row.status)}"
158
+ >
148
159
  <Icon
149
160
  icon={STATUS_ICON[row.status] ?? 'mdi:circle-outline'}
150
161
  class="pipeline-table__status-icon"
@@ -152,7 +163,9 @@
152
163
  {row.status}
153
164
  </span>
154
165
  </td>
155
- <td class="pipeline-table__td pipeline-table__td--id" title={row.node.id}>{row.node.id}</td>
166
+ <td class="pipeline-table__td pipeline-table__td--id" title={row.node.id}
167
+ >{row.node.id}</td
168
+ >
156
169
  </tr>
157
170
  {#if expanded && expandable}
158
171
  <tr class="pipeline-table__detail-row">
@@ -320,7 +333,8 @@
320
333
  }
321
334
 
322
335
  .pipeline-table__detail-cell {
323
- padding: var(--fd-space-sm) var(--fd-space-md) var(--fd-space-sm) calc(1.5rem + var(--fd-space-md));
336
+ padding: var(--fd-space-sm) var(--fd-space-md) var(--fd-space-sm)
337
+ calc(1.5rem + var(--fd-space-md));
324
338
  border-bottom: 1px solid var(--fd-border);
325
339
  }
326
340