@flowdrop/flowdrop 1.15.0 → 2.0.0-beta.1

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 (215) hide show
  1. package/CHANGELOG.md +475 -0
  2. package/MIGRATION-2.0.md +472 -0
  3. package/README.md +23 -23
  4. package/dist/adapters/WorkflowAdapter.d.ts +1 -1
  5. package/dist/adapters/WorkflowAdapter.js +14 -8
  6. package/dist/adapters/agentspec/AgentSpecAdapter.js +7 -7
  7. package/dist/chat/batchFeedback.d.ts +39 -0
  8. package/dist/chat/batchFeedback.js +51 -0
  9. package/dist/commands/executor.js +15 -1
  10. package/dist/commands/storeIntegration.svelte.d.ts +4 -1
  11. package/dist/commands/storeIntegration.svelte.js +26 -21
  12. package/dist/commands/types.d.ts +2 -0
  13. package/dist/components/App.svelte +162 -192
  14. package/dist/components/App.svelte.d.ts +47 -8
  15. package/dist/components/ConfigForm.svelte +71 -47
  16. package/dist/components/ConfigModal.svelte +7 -2
  17. package/dist/components/ConnectionLine.svelte +4 -2
  18. package/dist/components/Navbar.svelte +61 -1
  19. package/dist/components/NodeSidebar.svelte +27 -45
  20. package/dist/components/NodeStatusOverlay.svelte +94 -6
  21. package/dist/components/NodeSwapPicker.svelte +10 -8
  22. package/dist/components/PipelineStatus.svelte +16 -67
  23. package/dist/components/PortCoordinateTracker.svelte +5 -6
  24. package/dist/components/SchemaForm.stories.svelte +1 -3
  25. package/dist/components/SchemaForm.svelte +18 -25
  26. package/dist/components/SchemaForm.svelte.d.ts +0 -8
  27. package/dist/components/SettingsModal.svelte +8 -3
  28. package/dist/components/SettingsPanel.svelte +20 -4
  29. package/dist/components/SwapMappingEditor.svelte +67 -49
  30. package/dist/components/SwapMappingEditor.svelte.d.ts +0 -2
  31. package/dist/components/UniversalNode.svelte +9 -7
  32. package/dist/components/WorkflowEditor.svelte +118 -111
  33. package/dist/components/WorkflowEditor.svelte.d.ts +18 -10
  34. package/dist/components/chat/AIChatPanel.svelte +93 -89
  35. package/dist/components/chat/AIChatPanel.svelte.d.ts +0 -4
  36. package/dist/components/chat/CommandPreview.svelte +2 -1
  37. package/dist/components/console/CommandConsole.svelte +7 -5
  38. package/dist/components/console/ConsoleAutocomplete.svelte +10 -11
  39. package/dist/components/console/ConsoleAutocomplete.svelte.d.ts +6 -0
  40. package/dist/components/console/ConsoleInput.svelte +15 -6
  41. package/dist/components/console/ConsoleOutput.svelte +2 -1
  42. package/dist/components/form/FormArray.svelte +5 -9
  43. package/dist/components/form/FormArray.svelte.d.ts +2 -1
  44. package/dist/components/form/FormAutocomplete.svelte +8 -6
  45. package/dist/components/form/FormField.svelte +4 -2
  46. package/dist/components/form/FormFieldLight.svelte +4 -2
  47. package/dist/components/form/FormMarkdownEditor.svelte +9 -4
  48. package/dist/components/form/FormRangeField.svelte +1 -0
  49. package/dist/components/form/FormTemplateEditor.svelte +11 -3
  50. package/dist/components/form/FormToggle.svelte +5 -12
  51. package/dist/components/form/FormToggle.svelte.d.ts +4 -2
  52. package/dist/components/form/templateAutocomplete.js +1 -5
  53. package/dist/components/form/types.d.ts +1 -14
  54. package/dist/components/interrupt/FormPrompt.svelte +3 -2
  55. package/dist/components/interrupt/InterruptBubble.svelte +16 -17
  56. package/dist/components/interrupt/ReviewPrompt.svelte +10 -3
  57. package/dist/components/interrupt/TextInputPrompt.svelte +2 -1
  58. package/dist/components/layouts/MainLayout.svelte +20 -13
  59. package/dist/components/layouts/MainLayout.svelte.d.ts +4 -0
  60. package/dist/components/nodes/AtomNode.svelte +17 -5
  61. package/dist/components/nodes/GatewayNode.svelte +19 -10
  62. package/dist/components/nodes/IdeaNode.svelte +7 -0
  63. package/dist/components/nodes/SimpleNode.svelte +11 -6
  64. package/dist/components/nodes/SquareNode.svelte +15 -8
  65. package/dist/components/nodes/TerminalNode.svelte +9 -4
  66. package/dist/components/nodes/ToolNode.svelte +7 -1
  67. package/dist/components/nodes/WorkflowNode.svelte +16 -7
  68. package/dist/components/playground/ChatInput.svelte +11 -14
  69. package/dist/components/playground/ChatPanel.svelte +6 -49
  70. package/dist/components/playground/ChatPanel.svelte.d.ts +0 -14
  71. package/dist/components/playground/ControlPanel.svelte +134 -123
  72. package/dist/components/playground/ControlPanel.svelte.d.ts +3 -0
  73. package/dist/components/playground/ExecutionLogs.svelte +11 -9
  74. package/dist/components/playground/InputCollector.svelte +11 -9
  75. package/dist/components/playground/MessageStream.svelte +17 -23
  76. package/dist/components/playground/PipelineKanbanView.svelte +65 -6
  77. package/dist/components/playground/PipelinePanel.svelte +11 -5
  78. package/dist/components/playground/PipelineTableView.svelte +186 -44
  79. package/dist/components/playground/Playground.svelte +90 -92
  80. package/dist/components/playground/Playground.svelte.d.ts +2 -0
  81. package/dist/components/playground/PlaygroundApp.svelte +6 -1
  82. package/dist/components/playground/PlaygroundApp.svelte.d.ts +3 -0
  83. package/dist/components/playground/PlaygroundModal.svelte +13 -3
  84. package/dist/components/playground/PlaygroundModal.svelte.d.ts +3 -0
  85. package/dist/components/playground/PlaygroundStudio.svelte +34 -32
  86. package/dist/components/playground/PlaygroundStudio.svelte.d.ts +3 -0
  87. package/dist/components/playground/SessionManager.svelte +9 -12
  88. package/dist/components/playground/pipelineViewUtils.svelte.d.ts +28 -0
  89. package/dist/components/playground/pipelineViewUtils.svelte.js +38 -1
  90. package/dist/config/endpoints.d.ts +0 -7
  91. package/dist/config/endpoints.js +2 -10
  92. package/dist/core/index.d.ts +4 -4
  93. package/dist/core/index.js +6 -6
  94. package/dist/display/index.d.ts +0 -2
  95. package/dist/display/index.js +0 -6
  96. package/dist/editor/index.d.ts +19 -20
  97. package/dist/editor/index.js +25 -35
  98. package/dist/form/code.d.ts +25 -15
  99. package/dist/form/code.js +44 -41
  100. package/dist/form/fieldRegistry.d.ts +17 -13
  101. package/dist/form/fieldRegistry.js +32 -12
  102. package/dist/form/full.d.ts +17 -13
  103. package/dist/form/full.js +22 -27
  104. package/dist/form/index.d.ts +3 -3
  105. package/dist/form/index.js +3 -3
  106. package/dist/form/markdown.d.ts +13 -8
  107. package/dist/form/markdown.js +22 -23
  108. package/dist/helpers/proximityConnect.d.ts +3 -2
  109. package/dist/helpers/proximityConnect.js +2 -5
  110. package/dist/helpers/workflowEditorHelper.d.ts +12 -5
  111. package/dist/helpers/workflowEditorHelper.js +27 -25
  112. package/dist/index.d.ts +28 -24
  113. package/dist/index.js +27 -50
  114. package/dist/messages/defaults.d.ts +2 -5
  115. package/dist/messages/defaults.js +3 -6
  116. package/dist/messages/index.d.ts +0 -1
  117. package/dist/messages/index.js +0 -1
  118. package/dist/mocks/app-forms.d.ts +6 -2
  119. package/dist/mocks/app-forms.js +11 -4
  120. package/dist/openapi/v1/openapi.yaml +3 -3
  121. package/dist/playground/index.d.ts +2 -3
  122. package/dist/playground/index.js +2 -30
  123. package/dist/playground/mount.d.ts +15 -0
  124. package/dist/playground/mount.js +46 -20
  125. package/dist/registry/{BaseRegistry.d.ts → BaseRegistry.svelte.d.ts} +22 -1
  126. package/dist/registry/{BaseRegistry.js → BaseRegistry.svelte.js} +37 -1
  127. package/dist/registry/builtinFormats.d.ts +9 -18
  128. package/dist/registry/builtinFormats.js +9 -39
  129. package/dist/registry/builtinNodes.d.ts +0 -25
  130. package/dist/registry/builtinNodes.js +1 -50
  131. package/dist/registry/index.d.ts +3 -4
  132. package/dist/registry/index.js +4 -6
  133. package/dist/registry/nodeComponentRegistry.d.ts +182 -15
  134. package/dist/registry/nodeComponentRegistry.js +235 -17
  135. package/dist/registry/workflowFormatRegistry.d.ts +14 -9
  136. package/dist/registry/workflowFormatRegistry.js +24 -8
  137. package/dist/{schema → schemas}/index.d.ts +2 -2
  138. package/dist/{schema → schemas}/index.js +2 -2
  139. package/dist/schemas/v1/workflow.schema.json +3 -3
  140. package/dist/services/agentSpecExecutionService.js +0 -1
  141. package/dist/services/apiVariableService.d.ts +2 -1
  142. package/dist/services/apiVariableService.js +5 -22
  143. package/dist/services/autoSaveService.d.ts +7 -0
  144. package/dist/services/autoSaveService.js +6 -4
  145. package/dist/services/chatService.d.ts +8 -4
  146. package/dist/services/chatService.js +15 -15
  147. package/dist/services/draftStorage.d.ts +129 -13
  148. package/dist/services/draftStorage.js +185 -37
  149. package/dist/services/dynamicSchemaService.d.ts +2 -1
  150. package/dist/services/dynamicSchemaService.js +5 -22
  151. package/dist/services/globalSave.d.ts +13 -12
  152. package/dist/services/globalSave.js +29 -51
  153. package/dist/services/historyService.d.ts +9 -3
  154. package/dist/services/historyService.js +9 -3
  155. package/dist/services/interruptService.d.ts +14 -9
  156. package/dist/services/interruptService.js +27 -27
  157. package/dist/services/nodeExecutionService.d.ts +18 -3
  158. package/dist/services/nodeExecutionService.js +71 -45
  159. package/dist/services/playgroundService.d.ts +14 -9
  160. package/dist/services/playgroundService.js +31 -30
  161. package/dist/services/variableService.d.ts +2 -1
  162. package/dist/services/variableService.js +2 -2
  163. package/dist/services/workflowStorage.js +6 -6
  164. package/dist/stores/apiContext.d.ts +45 -0
  165. package/dist/stores/apiContext.js +65 -0
  166. package/dist/stores/categoriesStore.svelte.d.ts +28 -23
  167. package/dist/stores/categoriesStore.svelte.js +70 -64
  168. package/dist/stores/getInstance.svelte.d.ts +39 -0
  169. package/dist/stores/getInstance.svelte.js +65 -0
  170. package/dist/stores/historyStore.svelte.d.ts +77 -93
  171. package/dist/stores/historyStore.svelte.js +134 -160
  172. package/dist/stores/instanceContainer.svelte.d.ts +111 -0
  173. package/dist/stores/instanceContainer.svelte.js +114 -0
  174. package/dist/stores/interruptStore.svelte.d.ts +112 -82
  175. package/dist/stores/interruptStore.svelte.js +253 -226
  176. package/dist/stores/pipelinePanelStore.svelte.d.ts +27 -3
  177. package/dist/stores/pipelinePanelStore.svelte.js +61 -14
  178. package/dist/stores/playgroundStore.svelte.d.ts +169 -222
  179. package/dist/stores/playgroundStore.svelte.js +515 -580
  180. package/dist/stores/portCoordinateStore.svelte.d.ts +57 -51
  181. package/dist/stores/portCoordinateStore.svelte.js +109 -98
  182. package/dist/stores/settingsStore.svelte.d.ts +4 -1
  183. package/dist/stores/settingsStore.svelte.js +47 -12
  184. package/dist/stores/workflowStore.svelte.d.ts +178 -213
  185. package/dist/stores/workflowStore.svelte.js +449 -501
  186. package/dist/stories/EdgeDecorator.svelte +5 -2
  187. package/dist/stories/NodeDecorator.svelte +5 -3
  188. package/dist/svelte-app.d.ts +60 -10
  189. package/dist/svelte-app.js +157 -53
  190. package/dist/types/events.d.ts +6 -3
  191. package/dist/types/index.d.ts +33 -3
  192. package/dist/types/navbar.d.ts +7 -0
  193. package/dist/types/playground.d.ts +18 -3
  194. package/dist/types/settings.d.ts +13 -0
  195. package/dist/types/settings.js +1 -0
  196. package/dist/utils/colors.d.ts +47 -21
  197. package/dist/utils/colors.js +69 -68
  198. package/dist/utils/connections.d.ts +9 -15
  199. package/dist/utils/connections.js +13 -32
  200. package/dist/utils/duration.d.ts +13 -0
  201. package/dist/utils/duration.js +45 -0
  202. package/dist/utils/icons.d.ts +5 -2
  203. package/dist/utils/icons.js +6 -5
  204. package/dist/utils/nodeSwap.d.ts +6 -2
  205. package/dist/utils/nodeSwap.js +62 -126
  206. package/dist/utils/nodeTypes.d.ts +17 -8
  207. package/dist/utils/nodeTypes.js +26 -19
  208. package/dist/utils/performanceUtils.js +7 -0
  209. package/package.json +6 -5
  210. package/dist/messages/deprecation.d.ts +0 -20
  211. package/dist/messages/deprecation.js +0 -33
  212. package/dist/registry/plugin.d.ts +0 -215
  213. package/dist/registry/plugin.js +0 -249
  214. package/dist/services/api.d.ts +0 -129
  215. package/dist/services/api.js +0 -217
@@ -42,6 +42,7 @@
42
42
  getStatusTextColor,
43
43
  getStatusBackgroundColor
44
44
  } from '../../utils/nodeStatus.js';
45
+ import { formatMicroseconds } from '../../utils/duration.js';
45
46
  import type { NodeStatus } from './pipelineViewUtils.svelte.js';
46
47
  import type { Workflow, WorkflowNode } from '../../types/index.js';
47
48
  import type { EndpointConfig } from '../../config/endpoints.js';
@@ -55,7 +56,8 @@
55
56
 
56
57
  let { pipelineId, workflow, endpointConfig, refreshTrigger = 0 }: Props = $props();
57
58
 
58
- // svelte-ignore state_referenced_locally — endpointConfig is consumed once to build the API client; it must be stable
59
+ // endpointConfig is consumed once to build the API client; it must be stable
60
+ // svelte-ignore state_referenced_locally
59
61
  const fetcher = createPipelineDataFetcher(() => pipelineId, endpointConfig);
60
62
 
61
63
  $effect(() => {
@@ -65,8 +67,13 @@
65
67
  });
66
68
 
67
69
  interface CardItem {
68
- node: WorkflowNode;
70
+ /** Stable key: job id, or node id for nodes without a job yet */
71
+ key: string;
72
+ label: string;
73
+ typeId: string;
69
74
  status: NodeStatus;
75
+ /** Duration in microseconds, for finished jobs */
76
+ durationUs?: number | null;
70
77
  }
71
78
 
72
79
  const columnedNodes = $derived.by(() => {
@@ -85,10 +92,37 @@
85
92
  nodesByColumn.set(col.key, []);
86
93
  }
87
94
 
95
+ const nodesById = new Map<string, WorkflowNode>(workflow.nodes.map((node) => [node.id, node]));
96
+
97
+ // One card per job: loop iterations create multiple jobs for the same
98
+ // node, and each deserves its own card (label carries the #N suffix).
99
+ const nodesWithJobs = new Set<string>();
100
+ for (const job of fetcher.jobs) {
101
+ const node = nodesById.get(job.nodeId);
102
+ if (!node) continue;
103
+ nodesWithJobs.add(job.nodeId);
104
+ const status = resolveStatus({ status: job.status });
105
+ const colKey = statusToColumn.get(status) ?? fallbackKey;
106
+ nodesByColumn.get(colKey)?.push({
107
+ key: job.id,
108
+ label: job.label || node.data.label,
109
+ typeId: node.data.metadata.id,
110
+ status,
111
+ durationUs: job.executionTimeUs
112
+ });
113
+ }
114
+
115
+ // Nodes without a job yet keep a single card (pending / not reached).
88
116
  for (const node of workflow.nodes) {
117
+ if (nodesWithJobs.has(node.id)) continue;
89
118
  const status = resolveStatus(fetcher.nodeStatusMap[node.id]);
90
119
  const colKey = statusToColumn.get(status) ?? fallbackKey;
91
- nodesByColumn.get(colKey)?.push({ node, status });
120
+ nodesByColumn.get(colKey)?.push({
121
+ key: node.id,
122
+ label: node.data.label,
123
+ typeId: node.data.metadata.id,
124
+ status
125
+ });
92
126
  }
93
127
 
94
128
  return { columns, nodesByColumn };
@@ -122,11 +156,11 @@
122
156
  <span class="pipeline-kanban__col-count">{items.length}</span>
123
157
  </div>
124
158
  <div class="pipeline-kanban__cards">
125
- {#each items as { node, status } (node.id)}
159
+ {#each items as { key, label, typeId, status, durationUs } (key)}
126
160
  <div class="pipeline-kanban__card">
127
161
  <div class="pipeline-kanban__card-body">
128
162
  <div class="pipeline-kanban__card-top">
129
- <span class="pipeline-kanban__card-label">{node.data.label}</span>
163
+ <span class="pipeline-kanban__card-label">{label}</span>
130
164
  {#if showStatusPill}
131
165
  <span
132
166
  class="pipeline-kanban__card-status"
@@ -137,7 +171,14 @@
137
171
  >
138
172
  {/if}
139
173
  </div>
140
- <span class="pipeline-kanban__card-type">{node.data.metadata.id}</span>
174
+ <div class="pipeline-kanban__card-meta">
175
+ <span class="pipeline-kanban__card-type">{typeId}</span>
176
+ {#if durationUs != null}
177
+ <span class="pipeline-kanban__card-duration"
178
+ >{formatMicroseconds(durationUs)}</span
179
+ >
180
+ {/if}
181
+ </div>
141
182
  </div>
142
183
  </div>
143
184
  {/each}
@@ -296,12 +337,30 @@
296
337
  flex-shrink: 0;
297
338
  }
298
339
 
340
+ .pipeline-kanban__card-meta {
341
+ display: flex;
342
+ align-items: center;
343
+ gap: var(--fd-space-xs);
344
+ min-width: 0;
345
+ }
346
+
299
347
  .pipeline-kanban__card-type {
300
348
  color: var(--fd-muted-foreground);
301
349
  font-size: var(--fd-text-2xs);
302
350
  overflow: hidden;
303
351
  text-overflow: ellipsis;
304
352
  white-space: nowrap;
353
+ flex: 1;
354
+ min-width: 0;
355
+ }
356
+
357
+ .pipeline-kanban__card-duration {
358
+ color: var(--fd-muted-foreground);
359
+ font-size: var(--fd-text-2xs);
360
+ font-family: var(--fd-font-mono, monospace);
361
+ font-variant-numeric: tabular-nums;
362
+ white-space: nowrap;
363
+ flex-shrink: 0;
305
364
  }
306
365
 
307
366
  .pipeline-kanban__empty {
@@ -1,5 +1,5 @@
1
1
  <script module lang="ts">
2
- const VIEW_MODE_KEY = 'fd-pipeline-view-mode';
2
+ const VIEW_MODE_KEY_BASE = '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
5
  type ViewMode = (typeof BUILTIN_VIEWS)[number] | (string & {});
@@ -7,6 +7,7 @@
7
7
 
8
8
  <script lang="ts">
9
9
  import { onMount } from 'svelte';
10
+ import { getInstance } from '../../stores/getInstance.svelte.js';
10
11
  import PipelineStatus from '../PipelineStatus.svelte';
11
12
  import PipelineKanbanView from './PipelineKanbanView.svelte';
12
13
  import PipelineTableView from './PipelineTableView.svelte';
@@ -46,10 +47,16 @@
46
47
  extraViews = []
47
48
  }: Props = $props();
48
49
 
50
+ const fd = getInstance();
51
+
52
+ // The default instance keeps the legacy bare key; additional instances get
53
+ // a scoped key so two editors' view-mode choices don't overwrite each other.
54
+ const viewModeKey = fd.isDefault ? VIEW_MODE_KEY_BASE : `${VIEW_MODE_KEY_BASE}:${fd.id}`;
55
+
49
56
  let viewMode = $state<ViewMode>('graph');
50
57
 
51
58
  onMount(() => {
52
- const stored = localStorage.getItem(VIEW_MODE_KEY);
59
+ const stored = localStorage.getItem(viewModeKey);
53
60
  if (!stored) return;
54
61
  const validKeys = [...BUILTIN_VIEWS, ...extraViews.map((v) => v.key)];
55
62
  if (validKeys.includes(stored)) viewMode = stored;
@@ -58,7 +65,7 @@
58
65
  function selectViewMode(mode: ViewMode) {
59
66
  viewMode = mode;
60
67
  try {
61
- localStorage.setItem(VIEW_MODE_KEY, mode);
68
+ localStorage.setItem(viewModeKey, mode);
62
69
  } catch (e) {
63
70
  logger.warn('[FlowDrop] Could not persist view mode to localStorage:', e);
64
71
  }
@@ -268,8 +275,7 @@
268
275
  width="100%"
269
276
  showNavbar={false}
270
277
  disableSidebar={true}
271
- lockWorkflow={true}
272
- readOnly={true}
278
+ mode="locked"
273
279
  {endpointConfig}
274
280
  />
275
281
  {/if}
@@ -25,19 +25,26 @@
25
25
  idle: 'mdi:circle-outline'
26
26
  };
27
27
 
28
- function formatDuration(ms: number | null | undefined): string | null {
29
- if (ms == null) return null;
30
- if (ms < 1000) return `${ms}ms`;
31
- if (ms < 60000) return `${(ms / 1000).toFixed(2)}s`;
32
- const mins = Math.floor(ms / 60000);
33
- const secs = Math.floor((ms % 60000) / 1000);
34
- return `${mins}m ${secs}s`;
35
- }
36
-
37
28
  function formatDateTime(iso: string | null | undefined): string | null {
38
29
  if (!iso) return null;
39
30
  return new Date(iso).toLocaleString();
40
31
  }
32
+
33
+ function formatJson(value: unknown): string {
34
+ if (typeof value === 'string') return value;
35
+ try {
36
+ return JSON.stringify(value, null, 2);
37
+ } catch {
38
+ return String(value);
39
+ }
40
+ }
41
+
42
+ /** Treat null/undefined and empty objects/arrays as "nothing to show". */
43
+ function hasData(value: unknown): boolean {
44
+ if (value == null) return false;
45
+ if (typeof value === 'object') return Object.keys(value).length > 0;
46
+ return value !== '';
47
+ }
41
48
  </script>
42
49
 
43
50
  <script lang="ts">
@@ -45,7 +52,7 @@
45
52
  import Icon from '@iconify/svelte';
46
53
  import { createPipelineDataFetcher, resolveStatus } from './pipelineViewUtils.svelte.js';
47
54
  import { getStatusTextColor } from '../../utils/nodeStatus.js';
48
- import type { NodeStatusData } from './pipelineViewUtils.svelte.js';
55
+ import { formatMicroseconds } from '../../utils/duration.js';
49
56
  import type { Workflow, WorkflowNode } from '../../types/index.js';
50
57
  import type { EndpointConfig } from '../../config/endpoints.js';
51
58
 
@@ -58,13 +65,26 @@
58
65
 
59
66
  let { pipelineId, workflow, endpointConfig, refreshTrigger = 0 }: Props = $props();
60
67
 
61
- interface NodeRow {
62
- node: WorkflowNode;
68
+ interface JobRow {
69
+ /** Stable key: job id, or node id for nodes without a job yet */
70
+ key: string;
71
+ label: string;
72
+ typeId: string;
73
+ nodeId: string;
63
74
  status: NodeStatus;
64
- statusData: NodeStatusData | undefined;
65
- }
66
-
67
- // svelte-ignore state_referenced_locally — endpointConfig is consumed once to build the API client; it must be stable
75
+ started?: string | null;
76
+ completed?: string | null;
77
+ /** Duration in microseconds */
78
+ executionTimeUs?: number | null;
79
+ error?: string | null;
80
+ retryCount?: number | null;
81
+ maxRetries?: number | null;
82
+ inputData?: unknown;
83
+ outputData?: unknown;
84
+ }
85
+
86
+ // endpointConfig is consumed once to build the API client; it must be stable
87
+ // svelte-ignore state_referenced_locally
68
88
  const fetcher = createPipelineDataFetcher(() => pipelineId, endpointConfig);
69
89
 
70
90
  $effect(() => {
@@ -73,28 +93,76 @@
73
93
  return () => clearTimeout(timer);
74
94
  });
75
95
 
76
- const sortedRows = $derived.by((): NodeRow[] =>
77
- workflow.nodes
96
+ // One row per job, timeline style: loop iterations create multiple jobs
97
+ // for the same node and each shows as its own row (label carries the #N
98
+ // suffix). Executed jobs sort by start time; never-started jobs keep
99
+ // pipeline order at the end, followed by nodes that have no job yet.
100
+ const sortedRows = $derived.by((): JobRow[] => {
101
+ const nodesById = new Map<string, WorkflowNode>(workflow.nodes.map((node) => [node.id, node]));
102
+
103
+ const jobRows: JobRow[] = [];
104
+ const nodesWithJobs = new Set<string>();
105
+ for (const job of fetcher.jobs) {
106
+ const node = nodesById.get(job.nodeId);
107
+ if (!node) continue;
108
+ nodesWithJobs.add(job.nodeId);
109
+ jobRows.push({
110
+ key: job.id,
111
+ label: job.label || node.data.label,
112
+ typeId: node.data.metadata.id,
113
+ nodeId: job.nodeId,
114
+ status: resolveStatus({ status: job.status }),
115
+ started: job.started,
116
+ completed: job.completed,
117
+ executionTimeUs: job.executionTimeUs,
118
+ error: job.error,
119
+ retryCount: job.retryCount,
120
+ maxRetries: job.maxRetries,
121
+ inputData: job.inputData,
122
+ outputData: job.outputData
123
+ });
124
+ }
125
+
126
+ const startedRows = jobRows
127
+ .filter((row) => row.started)
128
+ .sort((a, b) => Date.parse(a.started!) - Date.parse(b.started!));
129
+ const neverStartedRows = jobRows.filter((row) => !row.started);
130
+
131
+ const nodeRows: JobRow[] = workflow.nodes
132
+ .filter((node) => !nodesWithJobs.has(node.id))
78
133
  .map((node) => {
79
134
  const statusData = fetcher.nodeStatusMap[node.id];
80
- return { node, status: resolveStatus(statusData), statusData };
135
+ return {
136
+ key: node.id,
137
+ label: node.data.label,
138
+ typeId: node.data.metadata.id,
139
+ nodeId: node.id,
140
+ status: resolveStatus(statusData),
141
+ started: statusData?.last_executed,
142
+ executionTimeUs:
143
+ statusData?.execution_time_us ??
144
+ (statusData?.execution_time != null ? statusData.execution_time * 1000 : null),
145
+ error: statusData?.error
146
+ };
81
147
  })
82
- .sort((a, b) => (STATUS_ORDER[a.status] ?? Infinity) - (STATUS_ORDER[b.status] ?? Infinity))
83
- );
148
+ .sort((a, b) => (STATUS_ORDER[a.status] ?? Infinity) - (STATUS_ORDER[b.status] ?? Infinity));
149
+
150
+ return [...startedRows, ...neverStartedRows, ...nodeRows];
151
+ });
84
152
 
85
153
  let expandedIds = $state(new Set<string>());
86
154
 
87
- function hasDetails(row: NodeRow): boolean {
88
- return !!(row.statusData?.last_executed || row.statusData?.error);
155
+ function hasDetails(row: JobRow): boolean {
156
+ return !!(row.started || row.error || hasData(row.inputData) || hasData(row.outputData));
89
157
  }
90
158
 
91
- function toggleRow(row: NodeRow) {
159
+ function toggleRow(row: JobRow) {
92
160
  if (!hasDetails(row)) return;
93
161
  const next = new Set(expandedIds);
94
- if (next.has(row.node.id)) {
95
- next.delete(row.node.id);
162
+ if (next.has(row.key)) {
163
+ next.delete(row.key);
96
164
  } else {
97
- next.add(row.node.id);
165
+ next.add(row.key);
98
166
  }
99
167
  expandedIds = next;
100
168
  }
@@ -121,12 +189,13 @@
121
189
  <th class="pipeline-table__th">Node</th>
122
190
  <th class="pipeline-table__th">Type</th>
123
191
  <th class="pipeline-table__th">Status</th>
192
+ <th class="pipeline-table__th pipeline-table__th--duration">Duration</th>
124
193
  <th class="pipeline-table__th pipeline-table__th--id">ID</th>
125
194
  </tr>
126
195
  </thead>
127
196
  <tbody>
128
- {#each sortedRows as row (row.node.id)}
129
- {@const expanded = expandedIds.has(row.node.id)}
197
+ {#each sortedRows as row (row.key)}
198
+ {@const expanded = expandedIds.has(row.key)}
130
199
  {@const expandable = hasDetails(row)}
131
200
  <tr
132
201
  class="pipeline-table__row"
@@ -144,12 +213,11 @@
144
213
  />
145
214
  {/if}
146
215
  </td>
147
- <td class="pipeline-table__td pipeline-table__td--label" title={row.node.data.label}
148
- >{row.node.data.label}</td
216
+ <td class="pipeline-table__td pipeline-table__td--label" title={row.label}
217
+ >{row.label}</td
149
218
  >
150
- <td
151
- class="pipeline-table__td pipeline-table__td--muted"
152
- title={row.node.data.metadata.id}>{row.node.data.metadata.id}</td
219
+ <td class="pipeline-table__td pipeline-table__td--muted" title={row.typeId}
220
+ >{row.typeId}</td
153
221
  >
154
222
  <td class="pipeline-table__td">
155
223
  <span
@@ -163,33 +231,62 @@
163
231
  {row.status}
164
232
  </span>
165
233
  </td>
166
- <td class="pipeline-table__td pipeline-table__td--id" title={row.node.id}
167
- >{row.node.id}</td
234
+ <td class="pipeline-table__td pipeline-table__td--duration">
235
+ {formatMicroseconds(row.executionTimeUs) ?? '—'}
236
+ </td>
237
+ <td class="pipeline-table__td pipeline-table__td--id" title={row.nodeId}
238
+ >{row.nodeId}</td
168
239
  >
169
240
  </tr>
170
241
  {#if expanded && expandable}
171
242
  <tr class="pipeline-table__detail-row">
172
- <td colspan="5" class="pipeline-table__detail-cell">
243
+ <td colspan="6" class="pipeline-table__detail-cell">
173
244
  <dl class="pipeline-table__details">
174
- {#if row.statusData?.last_executed}
245
+ {#if row.started}
246
+ <div class="pipeline-table__detail-item">
247
+ <dt>Started</dt>
248
+ <dd>{formatDateTime(row.started)}</dd>
249
+ </div>
250
+ {/if}
251
+ {#if row.completed}
175
252
  <div class="pipeline-table__detail-item">
176
- <dt>Last executed</dt>
177
- <dd>{formatDateTime(row.statusData.last_executed)}</dd>
253
+ <dt>Completed</dt>
254
+ <dd>{formatDateTime(row.completed)}</dd>
178
255
  </div>
179
256
  {/if}
180
- {#if row.statusData?.execution_time != null}
257
+ {#if row.executionTimeUs != null}
181
258
  <div class="pipeline-table__detail-item">
182
259
  <dt>Duration</dt>
183
- <dd>{formatDuration(row.statusData.execution_time)}</dd>
260
+ <dd>{formatMicroseconds(row.executionTimeUs)}</dd>
184
261
  </div>
185
262
  {/if}
186
- {#if row.statusData?.error}
263
+ {#if row.retryCount != null && row.retryCount > 0}
264
+ <div class="pipeline-table__detail-item">
265
+ <dt>Retries</dt>
266
+ <dd>
267
+ {row.retryCount}{row.maxRetries != null ? ` / ${row.maxRetries}` : ''}
268
+ </dd>
269
+ </div>
270
+ {/if}
271
+ {#if row.error}
187
272
  <div class="pipeline-table__detail-item pipeline-table__detail-item--error">
188
273
  <dt>Error</dt>
189
- <dd>{row.statusData.error}</dd>
274
+ <dd>{row.error}</dd>
190
275
  </div>
191
276
  {/if}
192
277
  </dl>
278
+ {#if hasData(row.inputData)}
279
+ <details class="pipeline-table__data">
280
+ <summary class="pipeline-table__data-summary">Input data</summary>
281
+ <pre class="pipeline-table__data-pre">{formatJson(row.inputData)}</pre>
282
+ </details>
283
+ {/if}
284
+ {#if hasData(row.outputData)}
285
+ <details class="pipeline-table__data">
286
+ <summary class="pipeline-table__data-summary">Output data</summary>
287
+ <pre class="pipeline-table__data-pre">{formatJson(row.outputData)}</pre>
288
+ </details>
289
+ {/if}
193
290
  </td>
194
291
  </tr>
195
292
  {/if}
@@ -270,6 +367,19 @@
270
367
  font-family: var(--fd-font-mono, monospace);
271
368
  }
272
369
 
370
+ .pipeline-table__th--duration,
371
+ .pipeline-table__td--duration {
372
+ text-align: right;
373
+ white-space: nowrap;
374
+ font-variant-numeric: tabular-nums;
375
+ }
376
+
377
+ .pipeline-table__td--duration {
378
+ font-family: var(--fd-font-mono, monospace);
379
+ font-size: var(--fd-text-2xs);
380
+ color: var(--fd-muted-foreground);
381
+ }
382
+
273
383
  .pipeline-table__th--expand {
274
384
  width: 1.5rem;
275
385
  padding-right: 0;
@@ -375,6 +485,38 @@
375
485
  font-size: var(--fd-text-2xs);
376
486
  }
377
487
 
488
+ .pipeline-table__data {
489
+ margin-top: var(--fd-space-sm);
490
+ }
491
+
492
+ .pipeline-table__data-summary {
493
+ cursor: pointer;
494
+ font-size: var(--fd-text-2xs);
495
+ font-weight: 600;
496
+ text-transform: uppercase;
497
+ letter-spacing: 0.05em;
498
+ color: var(--fd-muted-foreground);
499
+ user-select: none;
500
+ }
501
+
502
+ .pipeline-table__data-summary:hover {
503
+ color: var(--fd-foreground);
504
+ }
505
+
506
+ .pipeline-table__data-pre {
507
+ margin: var(--fd-space-2xs) 0 0;
508
+ padding: var(--fd-space-sm);
509
+ max-height: 16rem;
510
+ overflow: auto;
511
+ font-family: var(--fd-font-mono, monospace);
512
+ font-size: var(--fd-text-2xs);
513
+ background-color: var(--fd-muted);
514
+ border: 1px solid var(--fd-border);
515
+ border-radius: var(--fd-radius-sm, 4px);
516
+ white-space: pre-wrap;
517
+ word-break: break-word;
518
+ }
519
+
378
520
  .pipeline-table__status {
379
521
  display: inline-flex;
380
522
  align-items: center;