@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
@@ -33,8 +33,11 @@
33
33
  getError,
34
34
  playgroundActions,
35
35
  applyServerResponse,
36
- getLatestSequenceNumber
36
+ getLatestSequenceNumber,
37
+ getOldestSequenceNumber,
38
+ setHasOlder
37
39
  } from '../../stores/playgroundStore.svelte.js';
40
+ import type { PlaygroundMessagesApiResponse } from '../../types/playground.js';
38
41
  import { interruptActions } from '../../stores/interruptStore.svelte.js';
39
42
  import { logger } from '../../utils/logger.js';
40
43
 
@@ -67,6 +70,11 @@
67
70
  let loadedInitialSessionId = $state<string | undefined>(undefined);
68
71
  let autoRunTriggered = $state(false);
69
72
  let isRefreshing = $state(false);
73
+ // Monotonic token so a slow session load can't overwrite a newer one when the
74
+ // user switches sessions faster than the network responds (last-load wins).
75
+ let loadToken = 0;
76
+
77
+ const messagePageSize = $derived(config.messagePageSize ?? 50);
70
78
 
71
79
  // Vertical resizer state for the ExecutionConsole ↔ ControlPanel split.
72
80
  let playgroundContentEl = $state<HTMLElement | null>(null);
@@ -90,16 +98,15 @@
90
98
  }
91
99
  });
92
100
 
93
- const maxControlPanelHeight = $derived(
94
- containerHeight ? Math.round(containerHeight * 0.6) : 600
95
- );
101
+ const maxControlPanelHeight = $derived(containerHeight ? Math.round(containerHeight * 0.6) : 600);
96
102
 
97
103
  function clampControlPanelHeight(h: number): number {
98
104
  return Math.min(Math.max(h, 140), maxControlPanelHeight);
99
105
  }
100
106
 
101
107
  function handleVerticalResizerPointerDown(e: PointerEvent) {
102
- if (playgroundContentEl) dragContainerBottom = playgroundContentEl.getBoundingClientRect().bottom;
108
+ if (playgroundContentEl)
109
+ dragContainerBottom = playgroundContentEl.getBoundingClientRect().bottom;
103
110
  isVerticalResizing = true;
104
111
  (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
105
112
  }
@@ -133,7 +140,9 @@
133
140
  const sessionId = getCurrentSession()?.id;
134
141
  if (sessionId) {
135
142
  void playgroundService
136
- .getMessages(sessionId, playgroundService.getLastSequenceNumber() ?? undefined)
143
+ .getMessages(sessionId, {
144
+ since: playgroundService.getLastSequenceNumber() ?? undefined
145
+ })
137
146
  .then((response) => applyServerResponse(response))
138
147
  .catch((err) => logger.error('[Playground] Visibility catchup failed:', err));
139
148
  }
@@ -231,26 +240,79 @@
231
240
  async function loadSession(sessionId: string): Promise<void> {
232
241
  playgroundActions.setLoading(true);
233
242
  playgroundActions.setError(null);
243
+ const token = ++loadToken;
234
244
 
235
245
  try {
236
246
  const session = await playgroundService.getSession(sessionId);
247
+ if (token !== loadToken) return; // a newer session load superseded us
237
248
  playgroundActions.setCurrentSession(session);
238
249
 
239
- const response = await playgroundService.getMessages(sessionId);
250
+ // Load only the most recent page; older messages load on demand when the
251
+ // user scrolls up (loadOlderMessages). Clear right before applying the
252
+ // fresh page — not before the await — so switching sessions doesn't blank
253
+ // the view for the duration of the fetch.
254
+ const response = await playgroundService.getMessages(sessionId, {
255
+ latest: true,
256
+ limit: messagePageSize
257
+ });
258
+ if (token !== loadToken) return;
259
+ playgroundActions.clearMessages();
240
260
  applyServerResponse(response);
261
+ setHasOlder(deriveHasOlder(response));
241
262
 
242
263
  if (session.status !== 'idle') {
264
+ // Seed polling from the newest loaded message so it tails live updates
265
+ // instead of crawling forward from the start of the conversation.
243
266
  startPolling(sessionId, true);
244
267
  }
245
268
  } catch (err) {
269
+ if (token !== loadToken) return; // don't surface a superseded load's error
246
270
  const errorMessage = err instanceof Error ? err.message : 'Failed to load session';
247
271
  playgroundActions.setError(errorMessage);
248
272
  logger.error('Failed to load session:', err);
249
273
  } finally {
250
- playgroundActions.setLoading(false);
274
+ if (token === loadToken) playgroundActions.setLoading(false);
251
275
  }
252
276
  }
253
277
 
278
+ /**
279
+ * Load the page of messages immediately older than the oldest one currently
280
+ * shown. Triggered by scroll-up in MessageStream, which serializes calls and
281
+ * owns the in-flight/anchoring state. Bypasses applyServerResponse so a
282
+ * historical fetch never disturbs the live polling cursor or pipeline view.
283
+ */
284
+ async function loadOlderMessages(): Promise<void> {
285
+ const sessionId = getCurrentSession()?.id;
286
+ const before = getOldestSequenceNumber();
287
+ if (!sessionId || before === null) return;
288
+
289
+ try {
290
+ const response = await playgroundService.getMessages(sessionId, {
291
+ before,
292
+ limit: messagePageSize
293
+ });
294
+ // The session may have changed while the fetch was in flight — don't
295
+ // splice an old session's page into the new session's store.
296
+ if (getCurrentSession()?.id !== sessionId) return;
297
+ if (response.data && response.data.length > 0) {
298
+ playgroundActions.addMessages(response.data);
299
+ }
300
+ setHasOlder(deriveHasOlder(response));
301
+ } catch (err) {
302
+ logger.error('[Playground] Failed to load older messages:', err);
303
+ }
304
+ }
305
+
306
+ /**
307
+ * Whether older messages remain after a backward-pagination response. Prefer
308
+ * the server's explicit `hasOlder` flag; fall back to inferring from page
309
+ * fullness for backends that haven't adopted the field yet.
310
+ */
311
+ function deriveHasOlder(response: PlaygroundMessagesApiResponse): boolean {
312
+ if (typeof response.hasOlder === 'boolean') return response.hasOlder;
313
+ return (response.data?.length ?? 0) >= messagePageSize;
314
+ }
315
+
254
316
  async function handleCreateSession(): Promise<void> {
255
317
  playgroundActions.setLoading(true);
256
318
  playgroundActions.setError(null);
@@ -320,8 +382,10 @@
320
382
  playgroundActions.addMessage(message);
321
383
  // Only start polling if not already active — avoids resetting the cursor
322
384
  // mid-session and re-fetching messages that are already in the store.
385
+ // Seed from the newest loaded message so polling tails live updates
386
+ // rather than crawling forward from the start of the conversation.
323
387
  if (!playgroundService.isPolling()) {
324
- startPolling(sessionId);
388
+ startPolling(sessionId, true);
325
389
  }
326
390
  } catch (err) {
327
391
  const errorMessage = err instanceof Error ? err.message : 'Failed to send message';
@@ -370,10 +434,9 @@
370
434
  if (!sessionId || isRefreshing) return;
371
435
  isRefreshing = true;
372
436
  try {
373
- const response = await playgroundService.getMessages(
374
- sessionId,
375
- playgroundService.getLastSequenceNumber() ?? undefined
376
- );
437
+ const response = await playgroundService.getMessages(sessionId, {
438
+ since: playgroundService.getLastSequenceNumber() ?? undefined
439
+ });
377
440
  applyServerResponse(response);
378
441
  if (response.sessionStatus === 'running' && !playgroundService.isPolling()) {
379
442
  startPolling(sessionId, true);
@@ -392,10 +455,9 @@
392
455
  try {
393
456
  // Catch up immediately rather than waiting for the next poll interval.
394
457
  // Use the service's sequence cursor so we only fetch new messages.
395
- const response = await playgroundService.getMessages(
396
- sessionId,
397
- playgroundService.getLastSequenceNumber() ?? undefined
398
- );
458
+ const response = await playgroundService.getMessages(sessionId, {
459
+ since: playgroundService.getLastSequenceNumber() ?? undefined
460
+ });
399
461
  applyServerResponse(response);
400
462
  } catch (err) {
401
463
  logger.error('[Playground] Failed to refresh after interrupt:', err);
@@ -430,10 +492,7 @@
430
492
  </div>
431
493
  {/if}
432
494
 
433
- <div
434
- class="playground__content"
435
- bind:this={playgroundContentEl}
436
- >
495
+ <div class="playground__content" bind:this={playgroundContentEl}>
437
496
  {#if getIsLoading() && !getCurrentSession()}
438
497
  <div class="playground__loading">
439
498
  <Icon icon="mdi:loading" class="playground__loading-icon" />
@@ -444,11 +503,14 @@
444
503
  showTimestamps={config.showTimestamps ?? true}
445
504
  autoScroll={config.autoScroll ?? true}
446
505
  enableMarkdown={config.enableMarkdown ?? true}
447
- showLogsInline={config.logDisplayMode === 'inline'}
448
506
  onInterruptResolved={handleInterruptResolved}
449
507
  onCreateSession={getSessions().length === 0 ? handleCreateSession : undefined}
508
+ onLoadOlder={loadOlderMessages}
450
509
  />
451
510
 
511
+ <!-- Focusable ARIA splitter: keyboard/pointer handlers drive the resize -->
512
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
513
+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
452
514
  <div
453
515
  class="playground__vertical-resizer"
454
516
  class:playground__vertical-resizer--active={isVerticalResizing}
@@ -3,14 +3,17 @@
3
3
  import Icon from '@iconify/svelte';
4
4
  import Playground from './Playground.svelte';
5
5
  import PipelinePanel from './PipelinePanel.svelte';
6
- import { getPipelinePanelOpen, pipelinePanelActions } from '../../stores/pipelinePanelStore.svelte.js';
6
+ import {
7
+ getPipelinePanelOpen,
8
+ pipelinePanelActions
9
+ } from '../../stores/pipelinePanelStore.svelte.js';
7
10
  import {
8
11
  getActiveExecutionId,
9
12
  getPinnedExecutionId,
10
13
  getLatestExecutionId,
11
14
  getPipelineRefreshTrigger,
12
- getCurrentSession,
13
- playgroundActions,
15
+ getSelectableExecutions,
16
+ playgroundActions
14
17
  } from '../../stores/playgroundStore.svelte.js';
15
18
  import { setEndpointConfig, workflowApi } from '../../services/api.js';
16
19
  import { logger } from '../../utils/logger.js';
@@ -57,14 +60,17 @@
57
60
  initialPipelineWidth = 500,
58
61
  onSessionNavigate,
59
62
  onClose,
60
- extraPipelineViews = [],
63
+ extraPipelineViews = []
61
64
  }: Props = $props();
62
65
 
66
+ // svelte-ignore state_referenced_locally — seed mutable state from the prop's initial value; workflow may load asynchronously below
63
67
  let resolvedWorkflow = $state<Workflow | null>(workflowProp ?? null);
68
+ // svelte-ignore state_referenced_locally — initial loading flag derived from whether a workflow was provided up front
64
69
  let workflowLoading = $state(workflowProp === undefined);
65
70
  let workflowError = $state<string | null>(null);
66
71
 
67
72
  let splitEl = $state<HTMLElement | null>(null);
73
+ // svelte-ignore state_referenced_locally — seed mutable width from the initial prop; it changes as the user drags the resizer
68
74
  let pipelineWidth = $state(initialPipelineWidth);
69
75
  let isResizing = $state(false);
70
76
  let containerWidth = $state(0);
@@ -100,7 +106,8 @@
100
106
 
101
107
  async function loadWorkflow(): Promise<void> {
102
108
  if (!endpointConfig) {
103
- workflowError = 'Provide a workflow prop or an endpointConfig so the workflow can be fetched.';
109
+ workflowError =
110
+ 'Provide a workflow prop or an endpointConfig so the workflow can be fetched.';
104
111
  workflowLoading = false;
105
112
  return;
106
113
  }
@@ -149,13 +156,26 @@
149
156
  }
150
157
  </script>
151
158
 
152
- <div class="playground-studio" class:playground-studio--resizing={isResizing} style="--playground-studio-min-chat-width: {minChatWidth}px">
159
+ <div
160
+ class="playground-studio"
161
+ class:playground-studio--resizing={isResizing}
162
+ style="--playground-studio-min-chat-width: {minChatWidth}px"
163
+ >
153
164
  <div class="playground-studio__panes" bind:this={splitEl}>
154
165
  {#if getPipelinePanelOpen() && resolvedWorkflow && endpointConfig}
155
166
  {@const activeId = getActiveExecutionId()}
156
- {@const executions = getCurrentSession()?.executions ?? []}
167
+ {@const executions = getSelectableExecutions()}
157
168
 
158
169
  <div class="playground-studio__pipeline" style="width: {pipelineWidth}px;">
170
+ <button
171
+ type="button"
172
+ class="playground-studio__back-to-chat"
173
+ aria-label="Back to chat"
174
+ onclick={pipelinePanelActions.toggle}
175
+ >
176
+ <Icon icon="mdi:arrow-left" aria-hidden="true" />
177
+ <span>Back to chat</span>
178
+ </button>
159
179
  <PipelinePanel
160
180
  pipelineId={activeId}
161
181
  workflow={resolvedWorkflow}
@@ -169,6 +189,9 @@
169
189
  />
170
190
  </div>
171
191
 
192
+ <!-- Focusable ARIA splitter: keyboard/pointer handlers drive the resize -->
193
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
194
+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
172
195
  <div
173
196
  class="playground-studio__resizer"
174
197
  class:playground-studio__resizer--active={isResizing}
@@ -254,6 +277,13 @@
254
277
  .playground-studio__pipeline {
255
278
  overflow: hidden;
256
279
  flex-shrink: 0;
280
+ position: relative;
281
+ }
282
+
283
+ /* Mobile-only "back to chat" affordance. Hidden on wider viewports where
284
+ the ControlPanel's pipeline toggle remains reachable. */
285
+ .playground-studio__back-to-chat {
286
+ display: none;
257
287
  }
258
288
 
259
289
  /* Drag handle between the two panes */
@@ -375,4 +405,66 @@
375
405
  .playground-studio__retry-btn:hover {
376
406
  background-color: var(--fd-primary-hover);
377
407
  }
408
+
409
+ /* ============================================================
410
+ Mobile layout (< 768px)
411
+ Switch from side-by-side panes to one-at-a-time fullscreen.
412
+ The pipeline panel, when open, covers the chat. Users toggle
413
+ between them via the pipeline panel button. The resizer is
414
+ hidden — at this width there's nothing to resize.
415
+ ============================================================ */
416
+ @media (max-width: 768px) {
417
+ .playground-studio__pipeline {
418
+ /* Override the JS-driven width — take the whole row */
419
+ width: 100% !important;
420
+ flex: 1 1 auto;
421
+ display: flex;
422
+ flex-direction: column;
423
+ }
424
+
425
+ .playground-studio__resizer {
426
+ display: none;
427
+ }
428
+
429
+ /* When pipeline is open (chat is NOT solo), hide chat to give the
430
+ pipeline the full viewport. When pipeline closes, chat goes back
431
+ to full-width via the existing --solo class. */
432
+ .playground-studio__chat:not(.playground-studio__chat--solo) {
433
+ display: none;
434
+ }
435
+
436
+ .playground-studio__back-to-chat {
437
+ display: inline-flex;
438
+ align-items: center;
439
+ gap: var(--fd-space-2xs);
440
+ align-self: flex-start;
441
+ margin: var(--fd-space-xs);
442
+ padding: var(--fd-space-2xs) var(--fd-space-md);
443
+ min-height: 2.5rem;
444
+ font-size: var(--fd-text-sm);
445
+ font-weight: 500;
446
+ font-family: inherit;
447
+ color: var(--fd-foreground);
448
+ background-color: var(--fd-card);
449
+ border: 1px solid var(--fd-border);
450
+ border-radius: var(--fd-radius-md);
451
+ cursor: pointer;
452
+ transition: background-color var(--fd-transition-fast);
453
+ }
454
+
455
+ .playground-studio__back-to-chat:hover {
456
+ background-color: var(--fd-muted);
457
+ }
458
+
459
+ .playground-studio__back-to-chat:focus-visible {
460
+ outline: 2px solid var(--fd-ring);
461
+ outline-offset: 2px;
462
+ }
463
+ }
464
+
465
+ @media (prefers-reduced-motion: reduce) {
466
+ .playground-studio__back-to-chat {
467
+ transition: none;
468
+ }
469
+ }
378
470
  </style>
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Shared formatters / icon maps / label resolver for the message layout
3
+ * components. Pure functions only — i18n strings come in via the `roles`
4
+ * argument; the helper has no runtime dependency on the messages context.
5
+ */
6
+ import type { PlaygroundMessage, PlaygroundMessageLevel, PlaygroundMessageRole } from '../../types/playground.js';
7
+ import type { Messages } from '../../messages/types.js';
8
+ export type RoleLabels = Messages['playground']['roles'];
9
+ export declare function formatTimestamp(timestamp: string): string;
10
+ export declare function formatDuration(ms: number): string;
11
+ export declare function getLogLevelIcon(level: PlaygroundMessageLevel | undefined): string;
12
+ export declare function getRoleIcon(role: PlaygroundMessageRole): string;
13
+ /**
14
+ * Localised author label. Backend-supplied overrides win:
15
+ * - user → metadata.userName (display name)
16
+ * - log → metadata.nodeLabel (human-readable node label)
17
+ * Anything else returns the role's i18n default.
18
+ */
19
+ export declare function getRoleLabel(message: Pick<PlaygroundMessage, 'role' | 'metadata'>, roles: RoleLabels): string;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Shared formatters / icon maps / label resolver for the message layout
3
+ * components. Pure functions only — i18n strings come in via the `roles`
4
+ * argument; the helper has no runtime dependency on the messages context.
5
+ */
6
+ export function formatTimestamp(timestamp) {
7
+ return new Date(timestamp).toLocaleTimeString('en-US', {
8
+ hour12: false,
9
+ hour: '2-digit',
10
+ minute: '2-digit',
11
+ second: '2-digit'
12
+ });
13
+ }
14
+ export function formatDuration(ms) {
15
+ return ms < 1000 ? `${ms}ms` : `${(ms / 1000).toFixed(2)}s`;
16
+ }
17
+ export function getLogLevelIcon(level) {
18
+ switch (level) {
19
+ case 'error':
20
+ return 'mdi:alert-circle';
21
+ case 'warning':
22
+ return 'mdi:alert';
23
+ case 'debug':
24
+ return 'mdi:bug';
25
+ default:
26
+ return 'mdi:information';
27
+ }
28
+ }
29
+ export function getRoleIcon(role) {
30
+ switch (role) {
31
+ case 'user':
32
+ return 'mdi:account';
33
+ case 'assistant':
34
+ return 'mdi:robot';
35
+ case 'system':
36
+ return 'mdi:cog';
37
+ case 'log':
38
+ return 'mdi:console';
39
+ default:
40
+ return 'mdi:message';
41
+ }
42
+ }
43
+ /**
44
+ * Localised author label. Backend-supplied overrides win:
45
+ * - user → metadata.userName (display name)
46
+ * - log → metadata.nodeLabel (human-readable node label)
47
+ * Anything else returns the role's i18n default.
48
+ */
49
+ export function getRoleLabel(message, roles) {
50
+ switch (message.role) {
51
+ case 'user':
52
+ return message.metadata?.userName ?? roles.you;
53
+ case 'assistant':
54
+ return roles.assistant;
55
+ case 'system':
56
+ return roles.system;
57
+ case 'log':
58
+ return message.metadata?.nodeLabel ?? roles.log;
59
+ default:
60
+ return roles.message;
61
+ }
62
+ }
@@ -1,8 +1,15 @@
1
1
  import { EnhancedFlowDropApiClient } from '../../api/enhanced-client.js';
2
2
  import { logger } from '../../utils/logger.js';
3
3
  const KNOWN_STATUSES = new Set([
4
- 'idle', 'pending', 'running', 'paused', 'interrupted',
5
- 'completed', 'skipped', 'failed', 'cancelled'
4
+ 'idle',
5
+ 'pending',
6
+ 'running',
7
+ 'paused',
8
+ 'interrupted',
9
+ 'completed',
10
+ 'skipped',
11
+ 'failed',
12
+ 'cancelled'
6
13
  ]);
7
14
  export function resolveStatus(raw) {
8
15
  if (!raw)
@@ -36,7 +43,7 @@ export function createPipelineDataFetcher(getPipelineId, endpointConfig) {
36
43
  status: info.status,
37
44
  last_executed: info.last_executed,
38
45
  execution_time: info.execution_time,
39
- error: info.error,
46
+ error: info.error
40
47
  };
41
48
  }
42
49
  nodeStatusMap = map;
@@ -48,7 +55,7 @@ export function createPipelineDataFetcher(getPipelineId, endpointConfig) {
48
55
  label: col.label,
49
56
  statuses: col.statuses,
50
57
  icon: col.icon,
51
- color: col.color,
58
+ color: col.color
52
59
  }));
53
60
  }
54
61
  }
@@ -0,0 +1 @@
1
+ export { default as FormAutocomplete } from '../components/form/FormAutocomplete.svelte';
@@ -0,0 +1 @@
1
+ export { default as FormAutocomplete } from '../components/form/FormAutocomplete.svelte';
@@ -75,3 +75,20 @@ export type { FieldSchema, FieldType, FieldFormat, FieldOption, OneOfItem, Schem
75
75
  export { isFieldOptionArray, isOneOfArray, normalizeOptions, oneOfToOptions, getSchemaOptions } from '../components/form/types.js';
76
76
  export { fieldComponentRegistry, hiddenFieldMatcher, checkboxGroupMatcher, enumSelectMatcher, textareaMatcher, rangeMatcher, textFieldMatcher, numberFieldMatcher, toggleMatcher, selectOptionsMatcher, arrayMatcher } from './fieldRegistry.js';
77
77
  export type { FieldComponentProps, FieldMatcher, FieldMatcherRegistration, FieldComponent, FieldComponentRegistration } from './fieldRegistry.js';
78
+ /**
79
+ * Use with Svelte's `getContext` inside custom field components registered
80
+ * via `fieldComponentRegistry` to read sibling form field values.
81
+ *
82
+ * @example
83
+ * ```svelte
84
+ * <script lang="ts">
85
+ * import { getContext } from 'svelte';
86
+ * import { FORM_VALUES_KEY, type FormValuesGetter } from '@flowdrop/flowdrop/form';
87
+ *
88
+ * const getFormValues = getContext<FormValuesGetter | undefined>(FORM_VALUES_KEY);
89
+ * const account = $derived(getFormValues?.()['account']);
90
+ * </script>
91
+ * ```
92
+ */
93
+ export declare const FORM_VALUES_KEY: "flowdrop:getFormValues";
94
+ export type FormValuesGetter = () => Record<string, unknown>;
@@ -89,3 +89,22 @@ export {
89
89
  fieldComponentRegistry,
90
90
  // Built-in matchers for custom components
91
91
  hiddenFieldMatcher, checkboxGroupMatcher, enumSelectMatcher, textareaMatcher, rangeMatcher, textFieldMatcher, numberFieldMatcher, toggleMatcher, selectOptionsMatcher, arrayMatcher } from './fieldRegistry.js';
92
+ // ============================================================================
93
+ // Context keys (for custom field components)
94
+ // ============================================================================
95
+ /**
96
+ * Use with Svelte's `getContext` inside custom field components registered
97
+ * via `fieldComponentRegistry` to read sibling form field values.
98
+ *
99
+ * @example
100
+ * ```svelte
101
+ * <script lang="ts">
102
+ * import { getContext } from 'svelte';
103
+ * import { FORM_VALUES_KEY, type FormValuesGetter } from '@flowdrop/flowdrop/form';
104
+ *
105
+ * const getFormValues = getContext<FormValuesGetter | undefined>(FORM_VALUES_KEY);
106
+ * const account = $derived(getFormValues?.()['account']);
107
+ * </script>
108
+ * ```
109
+ */
110
+ export const FORM_VALUES_KEY = 'flowdrop:getFormValues';
@@ -310,6 +310,11 @@ export declare const defaultMessages: {
310
310
  }) => string;
311
311
  readonly executionDuration: "Execution duration";
312
312
  };
313
+ readonly messageAnnotations: {
314
+ readonly hierarchyOf: ({ path }: {
315
+ path: string;
316
+ }) => string;
317
+ };
313
318
  readonly sessions: {
314
319
  readonly header: "Sessions";
315
320
  readonly newSession: "New Session";
@@ -288,6 +288,12 @@ export const defaultMessages = {
288
288
  nodeId: ({ id }) => `Node ID: ${id}`,
289
289
  executionDuration: 'Execution duration'
290
290
  },
291
+ // ARIA labels for message annotations. The hierarchy trail names the
292
+ // actual path so AT users hear "From: ForEach Loop / Greeter" rather
293
+ // than a generic "hierarchy".
294
+ messageAnnotations: {
295
+ hierarchyOf: ({ path }) => `From: ${path}`
296
+ },
291
297
  sessions: {
292
298
  header: 'Sessions',
293
299
  newSession: 'New Session',