@flowdrop/flowdrop 1.11.0 → 1.13.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 (84) hide show
  1. package/dist/api/enhanced-client.d.ts +29 -16
  2. package/dist/api/enhanced-client.js +0 -14
  3. package/dist/components/ConfigForm.svelte +1 -0
  4. package/dist/components/PipelineStatus.svelte +9 -12
  5. package/dist/components/SchemaForm.svelte +1 -0
  6. package/dist/components/WorkflowEditor.svelte +3 -0
  7. package/dist/components/form/FormAutocomplete.svelte +67 -10
  8. package/dist/components/form/FormField.svelte +21 -0
  9. package/dist/components/form/FormFieldLight.svelte +1 -0
  10. package/dist/components/interrupt/ChoicePrompt.svelte +24 -5
  11. package/dist/components/interrupt/ConfirmationPrompt.svelte +5 -0
  12. package/dist/components/interrupt/InterruptBubble.svelte +88 -17
  13. package/dist/components/interrupt/InterruptBubble.svelte.d.ts +11 -0
  14. package/dist/components/interrupt/ReviewPrompt.svelte +20 -0
  15. package/dist/components/interrupt/TextInputPrompt.svelte +5 -0
  16. package/dist/components/nodes/GatewayNode.svelte +2 -6
  17. package/dist/components/nodes/WorkflowNode.svelte +2 -6
  18. package/dist/components/playground/ChatBubble.svelte +289 -0
  19. package/dist/components/playground/ChatBubble.svelte.d.ts +10 -0
  20. package/dist/components/playground/ChatInput.svelte +359 -0
  21. package/dist/components/playground/ChatInput.svelte.d.ts +14 -0
  22. package/dist/components/playground/ChatPanel.svelte +100 -724
  23. package/dist/components/playground/ChatPanel.svelte.d.ts +9 -26
  24. package/dist/components/playground/ControlPanel.svelte +496 -0
  25. package/dist/components/playground/ControlPanel.svelte.d.ts +20 -0
  26. package/dist/components/playground/ExecutionConsole.svelte +163 -0
  27. package/dist/components/playground/ExecutionConsole.svelte.d.ts +14 -0
  28. package/dist/components/playground/HierarchyTrail.svelte +88 -0
  29. package/dist/components/playground/HierarchyTrail.svelte.d.ts +7 -0
  30. package/dist/components/playground/LogRow.svelte +178 -0
  31. package/dist/components/playground/LogRow.svelte.d.ts +8 -0
  32. package/dist/components/playground/MessageBubble.stories.svelte +89 -0
  33. package/dist/components/playground/MessageBubble.svelte +25 -737
  34. package/dist/components/playground/MessageBubble.svelte.d.ts +3 -11
  35. package/dist/components/playground/MessageCard.svelte +106 -0
  36. package/dist/components/playground/MessageCard.svelte.d.ts +10 -0
  37. package/dist/components/playground/MessageMarkdown.svelte +160 -0
  38. package/dist/components/playground/MessageMarkdown.svelte.d.ts +7 -0
  39. package/dist/components/playground/MessageNotice.svelte +120 -0
  40. package/dist/components/playground/MessageNotice.svelte.d.ts +9 -0
  41. package/dist/components/playground/MessageStream.svelte +367 -0
  42. package/dist/components/playground/MessageStream.svelte.d.ts +27 -0
  43. package/dist/components/playground/MessageTagChip.svelte +99 -0
  44. package/dist/components/playground/MessageTagChip.svelte.d.ts +7 -0
  45. package/dist/components/playground/MessageTagStrip.svelte +37 -0
  46. package/dist/components/playground/MessageTagStrip.svelte.d.ts +7 -0
  47. package/dist/components/playground/PipelineKanbanView.svelte +284 -0
  48. package/dist/components/playground/PipelineKanbanView.svelte.d.ts +11 -0
  49. package/dist/components/playground/PipelinePanel.svelte +204 -65
  50. package/dist/components/playground/PipelinePanel.svelte.d.ts +3 -1
  51. package/dist/components/playground/PipelineTableView.svelte +376 -0
  52. package/dist/components/playground/PipelineTableView.svelte.d.ts +11 -0
  53. package/dist/components/playground/Playground.svelte +262 -1200
  54. package/dist/components/playground/Playground.svelte.d.ts +0 -13
  55. package/dist/components/playground/PlaygroundStudio.svelte +113 -61
  56. package/dist/components/playground/PlaygroundStudio.svelte.d.ts +3 -1
  57. package/dist/components/playground/messageDisplay.d.ts +19 -0
  58. package/dist/components/playground/messageDisplay.js +62 -0
  59. package/dist/components/playground/pipelineViewUtils.svelte.d.ts +22 -0
  60. package/dist/components/playground/pipelineViewUtils.svelte.js +77 -0
  61. package/dist/form/autocomplete.d.ts +1 -0
  62. package/dist/form/autocomplete.js +1 -0
  63. package/dist/form/index.d.ts +17 -0
  64. package/dist/form/index.js +19 -0
  65. package/dist/messages/defaults.d.ts +29 -0
  66. package/dist/messages/defaults.js +30 -0
  67. package/dist/playground/index.d.ts +6 -1
  68. package/dist/playground/index.js +6 -0
  69. package/dist/playground/mount.d.ts +3 -0
  70. package/dist/playground/mount.js +3 -2
  71. package/dist/schemas/v1/workflow.schema.json +10 -1
  72. package/dist/services/categoriesApi.d.ts +2 -1
  73. package/dist/services/categoriesApi.js +5 -3
  74. package/dist/services/portConfigApi.d.ts +2 -1
  75. package/dist/services/portConfigApi.js +5 -3
  76. package/dist/stores/playgroundStore.svelte.d.ts +6 -0
  77. package/dist/stores/playgroundStore.svelte.js +21 -1
  78. package/dist/svelte-app.d.ts +1 -0
  79. package/dist/svelte-app.js +5 -5
  80. package/dist/types/index.d.ts +41 -2
  81. package/dist/types/playground.d.ts +81 -2
  82. package/dist/types/playground.js +19 -7
  83. package/dist/utils/nodeStatus.js +15 -5
  84. package/package.json +6 -1
@@ -0,0 +1,284 @@
1
+ <script module lang="ts">
2
+ import type { KanbanColumnDef } from '../../types/index.js';
3
+
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)' },
9
+ ];
10
+ </script>
11
+
12
+ <script lang="ts">
13
+ import { onMount } from 'svelte';
14
+ import Icon from '@iconify/svelte';
15
+ import { createPipelineDataFetcher, resolveStatus } from './pipelineViewUtils.svelte.js';
16
+ import { getStatusLabel, getStatusTextColor, getStatusBackgroundColor } from '../../utils/nodeStatus.js';
17
+ import type { NodeStatus } from './pipelineViewUtils.svelte.js';
18
+ import type { Workflow, WorkflowNode } from '../../types/index.js';
19
+ import type { EndpointConfig } from '../../config/endpoints.js';
20
+
21
+ interface Props {
22
+ pipelineId: string;
23
+ workflow: Workflow;
24
+ endpointConfig: EndpointConfig;
25
+ refreshTrigger?: number;
26
+ }
27
+
28
+ let { pipelineId, workflow, endpointConfig, refreshTrigger = 0 }: Props = $props();
29
+
30
+ const fetcher = createPipelineDataFetcher(() => pipelineId, endpointConfig);
31
+
32
+ $effect(() => {
33
+ if (refreshTrigger <= 0) return;
34
+ const timer = setTimeout(() => fetcher.fetchData(), 300);
35
+ return () => clearTimeout(timer);
36
+ });
37
+
38
+ interface CardItem {
39
+ node: WorkflowNode;
40
+ status: NodeStatus;
41
+ }
42
+
43
+ const columnedNodes = $derived.by(() => {
44
+ const columns = fetcher.kanbanConfig ?? DEFAULT_COLUMNS;
45
+
46
+ const statusToColumn = new Map<string, string>();
47
+ for (const col of columns) {
48
+ for (const status of col.statuses) {
49
+ statusToColumn.set(status, col.key);
50
+ }
51
+ }
52
+ const fallbackKey = columns[0]?.key ?? 'pending';
53
+
54
+ const nodesByColumn = new Map<string, CardItem[]>();
55
+ for (const col of columns) {
56
+ nodesByColumn.set(col.key, []);
57
+ }
58
+
59
+ for (const node of workflow.nodes) {
60
+ const status = resolveStatus(fetcher.nodeStatusMap[node.id]);
61
+ const colKey = statusToColumn.get(status) ?? fallbackKey;
62
+ nodesByColumn.get(colKey)?.push({ node, status });
63
+ }
64
+
65
+ return { columns, nodesByColumn };
66
+ });
67
+
68
+ onMount(() => {
69
+ fetcher.fetchData();
70
+ });
71
+ </script>
72
+
73
+ <div class="pipeline-kanban">
74
+ {#if fetcher.isError}
75
+ <div class="pipeline-kanban__error">Could not refresh status data</div>
76
+ {/if}
77
+ {#if fetcher.isLoading && Object.keys(fetcher.nodeStatusMap).length === 0}
78
+ <div class="pipeline-kanban__loading">
79
+ <Icon icon="mdi:loading" class="pipeline-kanban__spinner" />
80
+ </div>
81
+ {:else}
82
+ <div class="pipeline-kanban__board">
83
+ {#each columnedNodes.columns as col (col.key)}
84
+ {@const items = columnedNodes.nodesByColumn.get(col.key) ?? []}
85
+ {@const showStatusPill = col.statuses.length > 1}
86
+ <div
87
+ class="pipeline-kanban__column"
88
+ style="--col-color: {col.color ?? 'var(--fd-muted-foreground)'}"
89
+ >
90
+ <div class="pipeline-kanban__column-header">
91
+ <Icon
92
+ icon={col.icon ?? 'mdi:circle-outline'}
93
+ class="pipeline-kanban__col-icon"
94
+ />
95
+ <span class="pipeline-kanban__col-label">{col.label}</span>
96
+ <span class="pipeline-kanban__col-count">{items.length}</span>
97
+ </div>
98
+ <div class="pipeline-kanban__cards">
99
+ {#each items as { node, status } (node.id)}
100
+ <div class="pipeline-kanban__card">
101
+ <div class="pipeline-kanban__card-body">
102
+ <div class="pipeline-kanban__card-top">
103
+ <span class="pipeline-kanban__card-label">{node.data.label}</span>
104
+ {#if showStatusPill}
105
+ <span
106
+ class="pipeline-kanban__card-status"
107
+ style="color: {getStatusTextColor(status)}; background-color: {getStatusBackgroundColor(status)}"
108
+ >{getStatusLabel(status)}</span>
109
+ {/if}
110
+ </div>
111
+ <span class="pipeline-kanban__card-type">{node.data.metadata.id}</span>
112
+ </div>
113
+ </div>
114
+ {/each}
115
+ {#if items.length === 0}
116
+ <div class="pipeline-kanban__empty">—</div>
117
+ {/if}
118
+ </div>
119
+ </div>
120
+ {/each}
121
+ </div>
122
+ {/if}
123
+ </div>
124
+
125
+ <style>
126
+ .pipeline-kanban {
127
+ height: 100%;
128
+ overflow: hidden;
129
+ display: flex;
130
+ flex-direction: column;
131
+ padding: var(--fd-space-sm);
132
+ }
133
+
134
+ .pipeline-kanban__error {
135
+ padding: var(--fd-space-xs) var(--fd-space-md);
136
+ font-size: var(--fd-text-2xs);
137
+ color: var(--fd-error);
138
+ background-color: color-mix(in srgb, var(--fd-error) 8%, transparent);
139
+ border-bottom: 1px solid color-mix(in srgb, var(--fd-error) 20%, transparent);
140
+ flex-shrink: 0;
141
+ }
142
+
143
+ .pipeline-kanban__loading {
144
+ display: flex;
145
+ align-items: center;
146
+ justify-content: center;
147
+ flex: 1;
148
+ color: var(--fd-muted-foreground);
149
+ }
150
+
151
+ :global(.pipeline-kanban__spinner) {
152
+ font-size: 1.5rem;
153
+ animation: kanban-spin 1s linear infinite;
154
+ }
155
+
156
+ @keyframes kanban-spin {
157
+ to {
158
+ transform: rotate(360deg);
159
+ }
160
+ }
161
+
162
+ .pipeline-kanban__board {
163
+ display: flex;
164
+ gap: var(--fd-space-sm);
165
+ height: 100%;
166
+ overflow-x: auto;
167
+ overflow-y: hidden;
168
+ }
169
+
170
+ .pipeline-kanban__column {
171
+ display: flex;
172
+ flex-direction: column;
173
+ min-width: 160px;
174
+ flex: 1;
175
+ background-color: var(--fd-background);
176
+ border-radius: var(--fd-radius-md);
177
+ border: 1px solid var(--fd-border);
178
+ overflow: hidden;
179
+ }
180
+
181
+ .pipeline-kanban__column-header {
182
+ display: flex;
183
+ align-items: center;
184
+ gap: var(--fd-space-xs);
185
+ padding: var(--fd-space-sm) var(--fd-space-md);
186
+ border-bottom: 1px solid var(--fd-border);
187
+ flex-shrink: 0;
188
+ }
189
+
190
+ .pipeline-kanban__col-label {
191
+ font-size: var(--fd-text-xs);
192
+ font-weight: 600;
193
+ flex: 1;
194
+ color: var(--fd-foreground);
195
+ }
196
+
197
+ .pipeline-kanban__col-count {
198
+ font-size: var(--fd-text-2xs);
199
+ font-weight: 600;
200
+ padding: 1px var(--fd-space-xs);
201
+ border-radius: var(--fd-radius-sm);
202
+ background-color: var(--fd-muted);
203
+ color: var(--fd-muted-foreground);
204
+ min-width: 18px;
205
+ text-align: center;
206
+ }
207
+
208
+ :global(.pipeline-kanban__col-icon) {
209
+ font-size: var(--fd-text-sm);
210
+ flex-shrink: 0;
211
+ color: var(--col-color, var(--fd-muted-foreground));
212
+ }
213
+
214
+ .pipeline-kanban__cards {
215
+ display: flex;
216
+ flex-direction: column;
217
+ gap: var(--fd-space-xs);
218
+ padding: var(--fd-space-xs);
219
+ overflow-y: auto;
220
+ flex: 1;
221
+ }
222
+
223
+ .pipeline-kanban__card {
224
+ display: flex;
225
+ align-items: flex-start;
226
+ padding: var(--fd-space-sm);
227
+ border-radius: var(--fd-radius-sm);
228
+ border: 1px solid var(--fd-border);
229
+ border-left-width: 3px;
230
+ border-left-color: var(--col-color, var(--fd-border));
231
+ background-color: var(--fd-card);
232
+ font-size: var(--fd-text-xs);
233
+ }
234
+
235
+ .pipeline-kanban__card-body {
236
+ display: flex;
237
+ flex-direction: column;
238
+ gap: 3px;
239
+ min-width: 0;
240
+ flex: 1;
241
+ }
242
+
243
+ .pipeline-kanban__card-top {
244
+ display: flex;
245
+ align-items: center;
246
+ gap: var(--fd-space-xs);
247
+ min-width: 0;
248
+ }
249
+
250
+ .pipeline-kanban__card-label {
251
+ font-weight: 500;
252
+ color: var(--fd-foreground);
253
+ overflow: hidden;
254
+ text-overflow: ellipsis;
255
+ white-space: nowrap;
256
+ flex: 1;
257
+ min-width: 0;
258
+ }
259
+
260
+ .pipeline-kanban__card-status {
261
+ display: inline-block;
262
+ font-size: var(--fd-text-2xs);
263
+ font-weight: 500;
264
+ padding: 1px var(--fd-space-xs);
265
+ border-radius: var(--fd-radius-sm);
266
+ white-space: nowrap;
267
+ flex-shrink: 0;
268
+ }
269
+
270
+ .pipeline-kanban__card-type {
271
+ color: var(--fd-muted-foreground);
272
+ font-size: var(--fd-text-2xs);
273
+ overflow: hidden;
274
+ text-overflow: ellipsis;
275
+ white-space: nowrap;
276
+ }
277
+
278
+ .pipeline-kanban__empty {
279
+ color: var(--fd-muted-foreground);
280
+ font-size: var(--fd-text-xs);
281
+ text-align: center;
282
+ padding: var(--fd-space-md);
283
+ }
284
+ </style>
@@ -0,0 +1,11 @@
1
+ import type { Workflow } from '../../types/index.js';
2
+ import type { EndpointConfig } from '../../config/endpoints.js';
3
+ interface Props {
4
+ pipelineId: string;
5
+ workflow: Workflow;
6
+ endpointConfig: EndpointConfig;
7
+ refreshTrigger?: number;
8
+ }
9
+ declare const PipelineKanbanView: import("svelte").Component<Props, {}, "">;
10
+ type PipelineKanbanView = ReturnType<typeof PipelineKanbanView>;
11
+ export default PipelineKanbanView;
@@ -1,7 +1,19 @@
1
+ <script module lang="ts">
2
+ const VIEW_MODE_KEY = 'fd-pipeline-view-mode';
3
+ const BUILTIN_VIEWS = ['graph', 'kanban', 'table'] as const;
4
+ // `string & {}` preserves autocomplete for built-in values while still accepting arbitrary strings from extraViews.
5
+ type ViewMode = typeof BUILTIN_VIEWS[number] | (string & {});
6
+ </script>
7
+
1
8
  <script lang="ts">
9
+ import { onMount } from 'svelte';
2
10
  import PipelineStatus from '../PipelineStatus.svelte';
11
+ import PipelineKanbanView from './PipelineKanbanView.svelte';
12
+ import PipelineTableView from './PipelineTableView.svelte';
13
+ import App from '../App.svelte';
3
14
  import Icon from '@iconify/svelte';
4
- import type { Workflow } from '../../types/index.js';
15
+ import { logger } from '../../utils/logger.js';
16
+ import type { Workflow, PipelineViewDef } from '../../types/index.js';
5
17
  import type { EndpointConfig } from '../../config/endpoints.js';
6
18
  import type { PlaygroundExecution } from '../../types/playground.js';
7
19
 
@@ -18,6 +30,8 @@
18
30
  onSelectExecution?: (id: string | null) => void;
19
31
  /** Increments when new messages arrive — forwarded to PipelineStatus for immediate refresh */
20
32
  refreshTrigger?: number;
33
+ /** Additional views injected by the library consumer */
34
+ extraViews?: PipelineViewDef[];
21
35
  }
22
36
 
23
37
  let {
@@ -29,12 +43,38 @@
29
43
  latestExecutionId = null,
30
44
  onSelectExecution,
31
45
  refreshTrigger = 0,
46
+ extraViews = []
32
47
  }: Props = $props();
33
48
 
49
+ let viewMode = $state<ViewMode>('graph');
50
+
51
+ onMount(() => {
52
+ const stored = localStorage.getItem(VIEW_MODE_KEY);
53
+ if (!stored) return;
54
+ const validKeys = [...BUILTIN_VIEWS, ...extraViews.map((v) => v.key)];
55
+ if (validKeys.includes(stored)) viewMode = stored;
56
+ });
57
+
58
+ function selectViewMode(mode: ViewMode) {
59
+ viewMode = mode;
60
+ try {
61
+ localStorage.setItem(VIEW_MODE_KEY, mode);
62
+ } catch (e) {
63
+ logger.warn('[FlowDrop] Could not persist view mode to localStorage:', e);
64
+ }
65
+ }
66
+
34
67
  let runDropdownOpen = $state(false);
35
68
  let chipWrapEl = $state<HTMLElement | null>(null);
69
+ let runChipEl = $state<HTMLElement | null>(null);
70
+ let runPopoverEl = $state<HTMLElement | null>(null);
71
+
72
+ $effect(() => {
73
+ if (runDropdownOpen && runPopoverEl) {
74
+ runPopoverEl.querySelector<HTMLElement>('[role="menuitem"]')?.focus();
75
+ }
76
+ });
36
77
 
37
- // Close run popover on outside click
38
78
  $effect(() => {
39
79
  if (!runDropdownOpen) return;
40
80
  function handleOutside(e: MouseEvent) {
@@ -46,17 +86,17 @@
46
86
  return () => document.removeEventListener('click', handleOutside);
47
87
  });
48
88
 
49
- function statusIcon(status: PlaygroundExecution['status']): string {
50
- if (status === 'running') return 'mdi:loading';
51
- if (status === 'failed') return 'mdi:alert-circle';
52
- return 'mdi:check-circle';
53
- }
54
-
55
- function statusClass(status: PlaygroundExecution['status']): string {
56
- if (status === 'running') return 'pipeline-panel__run-status--running';
57
- if (status === 'failed') return 'pipeline-panel__run-status--failed';
58
- return 'pipeline-panel__run-status--completed';
59
- }
89
+ const STATUS_ICON: Record<PlaygroundExecution['status'], string> = {
90
+ running: 'mdi:play-circle-outline',
91
+ failed: 'mdi:alert-circle',
92
+ completed: 'mdi:check-circle'
93
+ };
94
+
95
+ const STATUS_CLASS: Record<PlaygroundExecution['status'], string> = {
96
+ running: 'pipeline-panel__run-status--running',
97
+ failed: 'pipeline-panel__run-status--failed',
98
+ completed: 'pipeline-panel__run-status--completed'
99
+ };
60
100
  </script>
61
101
 
62
102
  <div class="pipeline-panel">
@@ -64,27 +104,91 @@
64
104
  <Icon icon="mdi:source-branch" class="pipeline-panel__icon" />
65
105
  <span class="pipeline-panel__title">Pipeline</span>
66
106
 
107
+ {#if pipelineId}
108
+ <div class="pipeline-panel__view-toggle" role="group" aria-label="View mode">
109
+ <button
110
+ type="button"
111
+ class="pipeline-panel__view-btn"
112
+ class:pipeline-panel__view-btn--active={viewMode === 'graph'}
113
+ onclick={() => selectViewMode('graph')}
114
+ title="Graph view"
115
+ >
116
+ <Icon icon="mdi:sitemap-outline" />
117
+ </button>
118
+ <button
119
+ type="button"
120
+ class="pipeline-panel__view-btn"
121
+ class:pipeline-panel__view-btn--active={viewMode === 'kanban'}
122
+ onclick={() => selectViewMode('kanban')}
123
+ title="Kanban view"
124
+ >
125
+ <Icon icon="mdi:view-column-outline" />
126
+ </button>
127
+ <button
128
+ type="button"
129
+ class="pipeline-panel__view-btn"
130
+ class:pipeline-panel__view-btn--active={viewMode === 'table'}
131
+ onclick={() => selectViewMode('table')}
132
+ title="Table view"
133
+ >
134
+ <Icon icon="mdi:table-large" />
135
+ </button>
136
+ {#each extraViews as view (view.key)}
137
+ <button
138
+ type="button"
139
+ class="pipeline-panel__view-btn"
140
+ class:pipeline-panel__view-btn--active={viewMode === view.key}
141
+ onclick={() => selectViewMode(view.key)}
142
+ title={view.label}
143
+ >
144
+ <Icon icon={view.icon} />
145
+ </button>
146
+ {/each}
147
+ </div>
148
+ {/if}
149
+
67
150
  {#if pipelineId && executions.length > 0}
68
- <!-- Run picker chip -->
69
151
  <div class="pipeline-panel__run-chip-wrap" bind:this={chipWrapEl}>
70
152
  <button
71
153
  type="button"
72
154
  class="pipeline-panel__run-chip"
73
155
  class:pipeline-panel__run-chip--pinned={isPinned}
74
156
  class:pipeline-panel__run-chip--open={runDropdownOpen}
157
+ bind:this={runChipEl}
158
+ aria-haspopup="menu"
159
+ aria-expanded={runDropdownOpen}
75
160
  onclick={() => (runDropdownOpen = !runDropdownOpen)}
161
+ onkeydown={(e) => {
162
+ if (e.key === 'Escape') {
163
+ runDropdownOpen = false;
164
+ }
165
+ }}
76
166
  title="Switch run"
77
167
  >
78
- <span class="pipeline-panel__run-chip-label">{pipelineId ?? 'Run'}</span>
79
- <Icon icon={runDropdownOpen ? 'mdi:chevron-up' : 'mdi:chevron-down'} class="pipeline-panel__run-chip-chevron" />
168
+ <span class="pipeline-panel__run-chip-label">{pipelineId}</span>
169
+ <Icon
170
+ icon={runDropdownOpen ? 'mdi:chevron-up' : 'mdi:chevron-down'}
171
+ class="pipeline-panel__run-chip-chevron"
172
+ />
80
173
  </button>
81
174
 
82
175
  {#if runDropdownOpen}
83
- <div class="pipeline-panel__run-popover">
176
+ <div
177
+ class="pipeline-panel__run-popover"
178
+ bind:this={runPopoverEl}
179
+ role="menu"
180
+ onkeydown={(e) => {
181
+ if (e.key === 'Escape') {
182
+ runDropdownOpen = false;
183
+ runChipEl?.focus();
184
+ }
185
+ }}
186
+ >
84
187
  {#each [...executions].reverse() as exec (exec.id)}
85
188
  {@const isActive = pipelineId === exec.id}
86
189
  <button
87
190
  type="button"
191
+ role="menuitem"
88
192
  class="pipeline-panel__run-popover-item"
89
193
  class:pipeline-panel__run-popover-item--active={isActive}
90
194
  onclick={() => {
@@ -93,8 +197,8 @@
93
197
  }}
94
198
  >
95
199
  <Icon
96
- icon={statusIcon(exec.status)}
97
- class="pipeline-panel__run-status {statusClass(exec.status)}"
200
+ icon={STATUS_ICON[exec.status]}
201
+ class="pipeline-panel__run-status {STATUS_CLASS[exec.status]}"
98
202
  />
99
203
  <span class="pipeline-panel__run-id">{exec.id}</span>
100
204
  {#if isActive}
@@ -106,7 +210,6 @@
106
210
  {/if}
107
211
  </div>
108
212
 
109
- <!-- Latest toggle -->
110
213
  <button
111
214
  type="button"
112
215
  class="pipeline-panel__latest-toggle"
@@ -118,7 +221,9 @@
118
221
  onSelectExecution?.(latestExecutionId);
119
222
  }
120
223
  }}
121
- title={isPinned ? 'Following latest is off — click to resume' : 'Always showing the most recent run'}
224
+ title={isPinned
225
+ ? 'Following latest is off — click to resume'
226
+ : 'Always showing the most recent run'}
122
227
  >
123
228
  <Icon icon="mdi:refresh" />
124
229
  Latest
@@ -126,23 +231,48 @@
126
231
  {:else if pipelineId}
127
232
  <span
128
233
  class="pipeline-panel__status-badge pipeline-panel__status-badge--live"
129
- title="Showing the most recent execution"
130
- >Latest</span>
234
+ title="Showing the most recent execution">Latest</span
235
+ >
131
236
  {/if}
132
237
  </div>
133
238
 
134
- {#if pipelineId}
135
- {#key pipelineId}
136
- <div class="pipeline-panel__content">
137
- <PipelineStatus {pipelineId} {workflow} {endpointConfig} runLabel={pipelineId ?? undefined} {refreshTrigger} isEmbedded={true} />
138
- </div>
139
- {/key}
140
- {:else}
141
- <div class="pipeline-panel__empty">
142
- <Icon icon="mdi:source-branch" class="pipeline-panel__empty-icon" />
143
- <p class="pipeline-panel__empty-text">Run the workflow to see the pipeline.</p>
144
- </div>
145
- {/if}
239
+ <div class="pipeline-panel__content">
240
+ {#if pipelineId}
241
+ {#key pipelineId}
242
+ {#if viewMode === 'kanban'}
243
+ <PipelineKanbanView {pipelineId} {workflow} {endpointConfig} {refreshTrigger} />
244
+ {:else if viewMode === 'table'}
245
+ <PipelineTableView {pipelineId} {workflow} {endpointConfig} {refreshTrigger} />
246
+ {:else if viewMode === 'graph'}
247
+ <PipelineStatus
248
+ {pipelineId}
249
+ {workflow}
250
+ {endpointConfig}
251
+ runLabel={pipelineId}
252
+ {refreshTrigger}
253
+ isEmbedded={true}
254
+ />
255
+ {:else}
256
+ {@const activeView = extraViews.find((v) => v.key === viewMode)}
257
+ {#if activeView}
258
+ {@const View = activeView.component}
259
+ <View {pipelineId} {workflow} {endpointConfig} {refreshTrigger} />
260
+ {/if}
261
+ {/if}
262
+ {/key}
263
+ {:else}
264
+ <App
265
+ {workflow}
266
+ height="100%"
267
+ width="100%"
268
+ showNavbar={false}
269
+ disableSidebar={true}
270
+ lockWorkflow={true}
271
+ readOnly={true}
272
+ {endpointConfig}
273
+ />
274
+ {/if}
275
+ </div>
146
276
  </div>
147
277
 
148
278
  <style>
@@ -194,6 +324,45 @@
194
324
  color: var(--fd-success);
195
325
  }
196
326
 
327
+ /* View mode toggle */
328
+ .pipeline-panel__view-toggle {
329
+ display: inline-flex;
330
+ align-items: center;
331
+ gap: 2px;
332
+ padding: 2px;
333
+ border: 1px solid var(--fd-border);
334
+ border-radius: var(--fd-radius-md);
335
+ background-color: var(--fd-muted);
336
+ flex-shrink: 0;
337
+ }
338
+
339
+ .pipeline-panel__view-btn {
340
+ display: inline-flex;
341
+ align-items: center;
342
+ justify-content: center;
343
+ width: 24px;
344
+ height: 24px;
345
+ border: none;
346
+ border-radius: calc(var(--fd-radius-md) - 2px);
347
+ background: transparent;
348
+ color: var(--fd-muted-foreground);
349
+ cursor: pointer;
350
+ transition: all var(--fd-transition-fast);
351
+ font-size: var(--fd-text-sm);
352
+ line-height: 1;
353
+ }
354
+
355
+ .pipeline-panel__view-btn:hover {
356
+ background-color: var(--fd-background);
357
+ color: var(--fd-foreground);
358
+ }
359
+
360
+ .pipeline-panel__view-btn--active {
361
+ background-color: var(--fd-background);
362
+ color: var(--fd-primary);
363
+ box-shadow: var(--fd-shadow-sm);
364
+ }
365
+
197
366
  /* Run picker chip */
198
367
  .pipeline-panel__run-chip-wrap {
199
368
  position: relative;
@@ -300,7 +469,6 @@
300
469
 
301
470
  :global(.pipeline-panel__run-status--running) {
302
471
  color: var(--fd-warning);
303
- animation: pp-spin 1s linear infinite;
304
472
  }
305
473
 
306
474
  :global(.pipeline-panel__run-status--completed) {
@@ -311,11 +479,6 @@
311
479
  color: var(--fd-error);
312
480
  }
313
481
 
314
- @keyframes pp-spin {
315
- from { transform: rotate(0deg); }
316
- to { transform: rotate(360deg); }
317
- }
318
-
319
482
  /* Latest toggle */
320
483
  .pipeline-panel__latest-toggle {
321
484
  display: inline-flex;
@@ -355,28 +518,4 @@
355
518
  min-height: 0;
356
519
  overflow: hidden;
357
520
  }
358
-
359
- .pipeline-panel__empty {
360
- flex: 1;
361
- display: flex;
362
- flex-direction: column;
363
- align-items: center;
364
- justify-content: center;
365
- gap: var(--fd-space-md);
366
- color: var(--fd-muted-foreground);
367
- padding: var(--fd-space-4xl);
368
- text-align: center;
369
- }
370
-
371
- :global(.pipeline-panel__empty-icon) {
372
- font-size: var(--fd-space-6xl);
373
- opacity: 0.4;
374
- }
375
-
376
- .pipeline-panel__empty-text {
377
- font-size: var(--fd-text-sm);
378
- margin: 0;
379
- max-width: 200px;
380
- line-height: var(--fd-leading-relaxed);
381
- }
382
521
  </style>
@@ -1,4 +1,4 @@
1
- import type { Workflow } from '../../types/index.js';
1
+ import type { Workflow, PipelineViewDef } from '../../types/index.js';
2
2
  import type { EndpointConfig } from '../../config/endpoints.js';
3
3
  import type { PlaygroundExecution } from '../../types/playground.js';
4
4
  interface Props {
@@ -14,6 +14,8 @@ interface Props {
14
14
  onSelectExecution?: (id: string | null) => void;
15
15
  /** Increments when new messages arrive — forwarded to PipelineStatus for immediate refresh */
16
16
  refreshTrigger?: number;
17
+ /** Additional views injected by the library consumer */
18
+ extraViews?: PipelineViewDef[];
17
19
  }
18
20
  declare const PipelinePanel: import("svelte").Component<Props, {}, "">;
19
21
  type PipelinePanel = ReturnType<typeof PipelinePanel>;