@flowdrop/flowdrop 1.13.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 (38) hide show
  1. package/README.md +5 -0
  2. package/dist/components/ConfigPanel.svelte +7 -1
  3. package/dist/components/NodeSwapPicker.svelte +5 -1
  4. package/dist/components/PipelineStatus.svelte +11 -2
  5. package/dist/components/SettingsPanel.svelte +5 -1
  6. package/dist/components/WorkflowEditor.svelte +5 -1
  7. package/dist/components/chat/AIChatPanel.svelte +1 -5
  8. package/dist/components/form/FormAutocomplete.svelte +2 -5
  9. package/dist/components/interrupt/ChoicePrompt.svelte +5 -1
  10. package/dist/components/interrupt/InterruptBubble.svelte +4 -5
  11. package/dist/components/playground/ChatBubble.svelte +6 -8
  12. package/dist/components/playground/ChatInput.svelte +11 -5
  13. package/dist/components/playground/ControlPanel.svelte +42 -29
  14. package/dist/components/playground/ExecutionConsole.svelte +5 -1
  15. package/dist/components/playground/ExecutionConsole.svelte.d.ts +2 -0
  16. package/dist/components/playground/ExecutionList.svelte +7 -2
  17. package/dist/components/playground/LogRow.svelte +2 -1
  18. package/dist/components/playground/MessageBubble.svelte +1 -4
  19. package/dist/components/playground/MessageCard.svelte +2 -1
  20. package/dist/components/playground/MessageMarkdown.svelte +15 -5
  21. package/dist/components/playground/MessageNotice.svelte +2 -1
  22. package/dist/components/playground/MessageStream.svelte +138 -17
  23. package/dist/components/playground/MessageStream.svelte.d.ts +5 -0
  24. package/dist/components/playground/MessageTagChip.svelte +24 -6
  25. package/dist/components/playground/PipelineKanbanView.svelte +40 -11
  26. package/dist/components/playground/PipelinePanel.svelte +5 -1
  27. package/dist/components/playground/PipelineTableView.svelte +20 -6
  28. package/dist/components/playground/Playground.svelte +84 -22
  29. package/dist/components/playground/PlaygroundStudio.svelte +21 -7
  30. package/dist/components/playground/pipelineViewUtils.svelte.js +11 -4
  31. package/dist/openapi/v1/openapi.yaml +6403 -0
  32. package/dist/schemas/v1/workflow.schema.json +36 -0
  33. package/dist/services/playgroundService.d.ts +23 -4
  34. package/dist/services/playgroundService.js +22 -9
  35. package/dist/stores/playgroundStore.svelte.d.ts +22 -1
  36. package/dist/stores/playgroundStore.svelte.js +109 -32
  37. package/dist/types/playground.d.ts +36 -2
  38. package/package.json +7 -1
package/README.md CHANGED
@@ -158,6 +158,11 @@ FlowDrop provides tree-shakeable sub-module exports so you can import only what
158
158
  | `@flowdrop/flowdrop/settings` | SettingsPanel, stores, services |
159
159
  | `@flowdrop/flowdrop/styles` | Base CSS stylesheet |
160
160
  | `@flowdrop/flowdrop/schema` | Workflow JSON schema |
161
+ | `@flowdrop/flowdrop/openapi` | OpenAPI spec (YAML) for the FlowDrop backend API |
162
+
163
+ ### OpenAPI spec
164
+
165
+ The full OpenAPI spec for the FlowDrop backend API ships with the package, version-matched to your installed release. It defines the node-config / form-element schema (`ConfigProperty`), playground messages, and every endpoint. Resolve it from `@flowdrop/flowdrop/openapi`, or read it directly at `node_modules/@flowdrop/flowdrop/dist/openapi/v1/openapi.yaml`. Point your AI assistant at that file when authoring node config schemas. The latest spec is also browsable at [api.flowdrop.io](https://api.flowdrop.io).
161
166
 
162
167
  ## Integration
163
168
 
@@ -77,7 +77,13 @@
77
77
  <Icon icon="heroicons:arrows-right-left" />
78
78
  </button>
79
79
  {/if}
80
- <button class="config-panel__close" onclick={onClose} aria-label={m().layout.closeConfigPanel}> × </button>
80
+ <button
81
+ class="config-panel__close"
82
+ onclick={onClose}
83
+ aria-label={m().layout.closeConfigPanel}
84
+ >
85
+ ×
86
+ </button>
81
87
  </div>
82
88
  </div>
83
89
 
@@ -81,7 +81,11 @@
81
81
  <div class="swap-picker">
82
82
  <!-- Header -->
83
83
  <div class="swap-picker__header">
84
- <button class="swap-picker__back" onclick={onCancel} aria-label={m().layout.backToConfiguration}>
84
+ <button
85
+ class="swap-picker__back"
86
+ onclick={onCancel}
87
+ aria-label={m().layout.backToConfiguration}
88
+ >
85
89
  <Icon icon="heroicons:arrow-left" />
86
90
  </button>
87
91
  <h2 class="swap-picker__title">Swap Node</h2>
@@ -37,8 +37,17 @@
37
37
  ) => void;
38
38
  }
39
39
 
40
- let { pipelineId, workflow, apiClient, baseUrl, endpointConfig, onActionsReady, runLabel, isEmbedded = false, refreshTrigger = 0 }: Props =
41
- $props();
40
+ let {
41
+ pipelineId,
42
+ workflow,
43
+ apiClient,
44
+ baseUrl,
45
+ endpointConfig,
46
+ onActionsReady,
47
+ runLabel,
48
+ isEmbedded = false,
49
+ refreshTrigger = 0
50
+ }: Props = $props();
42
51
 
43
52
  // Track previous trigger value so the $effect only fires on increments, not on initial mount.
44
53
  // svelte-ignore state_referenced_locally
@@ -362,7 +362,11 @@
362
362
 
363
363
  <div class="flowdrop-settings-panel {className}">
364
364
  <!-- Tab Navigation -->
365
- <div class="flowdrop-settings-panel__tabs" role="tablist" aria-label={m().layout.settingsCategories}>
365
+ <div
366
+ class="flowdrop-settings-panel__tabs"
367
+ role="tablist"
368
+ aria-label={m().layout.settingsCategories}
369
+ >
366
370
  {#each categories as category, index (category)}
367
371
  <button
368
372
  class="flowdrop-settings-panel__tab"
@@ -277,7 +277,11 @@
277
277
  const rawStatus = statuses[node.id];
278
278
  if (!rawStatus) return node;
279
279
 
280
- const existing = node.data.executionInfo ?? { status: 'idle' as const, executionCount: 0, isExecuting: false };
280
+ const existing = node.data.executionInfo ?? {
281
+ status: 'idle' as const,
282
+ executionCount: 0,
283
+ isExecuting: false
284
+ };
281
285
  return {
282
286
  ...node,
283
287
  data: {
@@ -288,11 +288,7 @@
288
288
  return;
289
289
  }
290
290
 
291
- if (
292
- getBehaviorSettings().chatAutoRetry &&
293
- workflowId &&
294
- autoRetryCount < MAX_AUTO_RETRIES
295
- ) {
291
+ if (getBehaviorSettings().chatAutoRetry && workflowId && autoRetryCount < MAX_AUTO_RETRIES) {
296
292
  autoRetryCount++;
297
293
  const errorText = buildBatchErrorMessage(
298
294
  completedCount,
@@ -121,6 +121,7 @@
121
121
  // Stable fingerprint — any change triggers selection clearing.
122
122
  // JSON.stringify gives a canonical string without null-byte ambiguity.
123
123
  const depFingerprint = $derived(JSON.stringify(depParamValues));
124
+ // svelte-ignore state_referenced_locally — intentional initial snapshot; the effect below tracks subsequent changes
124
125
  let prevDepFingerprint = depFingerprint;
125
126
 
126
127
  $effect(() => {
@@ -721,11 +722,7 @@
721
722
  style={popoverStyle}
722
723
  onmousedown={(e) => e.preventDefault()}
723
724
  >
724
- <ul
725
- class="form-autocomplete__listbox"
726
- role="listbox"
727
- aria-label={t.suggestions}
728
- >
725
+ <ul class="form-autocomplete__listbox" role="listbox" aria-label={t.suggestions}>
729
726
  {#if isLoading}
730
727
  <li class="form-autocomplete__status form-autocomplete__status--loading">
731
728
  <Icon icon="heroicons:arrow-path" class="form-autocomplete__status-icon" />
@@ -135,7 +135,11 @@
135
135
  {/if}
136
136
 
137
137
  <!-- Options -->
138
- <div class="choice-prompt__options" role={isMultiple ? 'group' : 'radiogroup'} aria-label={config.message}>
138
+ <div
139
+ class="choice-prompt__options"
140
+ role={isMultiple ? 'group' : 'radiogroup'}
141
+ aria-label={config.message}
142
+ >
139
143
  {#each config.options as option (option.value)}
140
144
  {@const isChecked = isResolved ? isOptionResolved(option) : selectedValues.has(option.value)}
141
145
  <label
@@ -16,10 +16,7 @@
16
16
  import ReviewPrompt from './ReviewPrompt.svelte';
17
17
  import MessageTagStrip from '../playground/MessageTagStrip.svelte';
18
18
  import HierarchyTrail from '../playground/HierarchyTrail.svelte';
19
- import type {
20
- MessageHierarchyItem,
21
- MessageTag
22
- } from '../../types/playground.js';
19
+ import type { MessageHierarchyItem, MessageTag } from '../../types/playground.js';
23
20
  import type {
24
21
  Interrupt,
25
22
  InterruptType,
@@ -304,7 +301,9 @@
304
301
  <time
305
302
  class="interrupt-bubble__timestamp"
306
303
  datetime={currentInterrupt.resolvedAt ?? currentInterrupt.createdAt}
307
- aria-label="sent at {formatTimestamp(currentInterrupt.resolvedAt ?? currentInterrupt.createdAt)}"
304
+ aria-label="sent at {formatTimestamp(
305
+ currentInterrupt.resolvedAt ?? currentInterrupt.createdAt
306
+ )}"
308
307
  >
309
308
  {formatTimestamp(currentInterrupt.resolvedAt ?? currentInterrupt.createdAt)}
310
309
  </time>
@@ -10,12 +10,7 @@
10
10
  import HierarchyTrail from './HierarchyTrail.svelte';
11
11
  import MessageTagStrip from './MessageTagStrip.svelte';
12
12
  import MessageMarkdown from './MessageMarkdown.svelte';
13
- import {
14
- formatDuration,
15
- formatTimestamp,
16
- getRoleIcon,
17
- getRoleLabel
18
- } from './messageDisplay.js';
13
+ import { formatDuration, formatTimestamp, getRoleIcon, getRoleLabel } from './messageDisplay.js';
19
14
  import { m } from '../../messages/index.js';
20
15
 
21
16
  interface Props {
@@ -55,7 +50,8 @@
55
50
  class="message-bubble__timestamp"
56
51
  datetime={message.timestamp}
57
52
  aria-label="sent at {formatTimestamp(message.timestamp)}"
58
- >{formatTimestamp(message.timestamp)}</time>
53
+ >{formatTimestamp(message.timestamp)}</time
54
+ >
59
55
  {/if}
60
56
  </div>
61
57
 
@@ -157,7 +153,9 @@
157
153
  background-color: var(--fd-card);
158
154
  border: 1px solid var(--fd-border);
159
155
  color: var(--fd-card-foreground);
160
- box-shadow: 0 1px 3px 0 oklch(0% 0 0 / 0.06), 0 1px 2px -1px oklch(0% 0 0 / 0.04);
156
+ box-shadow:
157
+ 0 1px 3px 0 oklch(0% 0 0 / 0.06),
158
+ 0 1px 2px -1px oklch(0% 0 0 / 0.04);
161
159
  border-bottom-left-radius: var(--fd-radius-sm);
162
160
  }
163
161
 
@@ -58,23 +58,27 @@
58
58
  let runEnabled = $state(true);
59
59
 
60
60
  let inputValue = $state('');
61
- let inputField: HTMLTextAreaElement | undefined;
61
+ let inputField: HTMLTextAreaElement | undefined = $state();
62
62
 
63
63
  // Count of enableRun messages seen so far — plain let, not $state.
64
64
  // Written with untrack to make the bookkeeping intent explicit.
65
65
  let seenEnableRunCount = 0;
66
66
 
67
67
  $effect(() => {
68
- const count = getMessages().filter(m => hasEnableRunFlag(m.metadata)).length;
68
+ const count = getMessages().filter((m) => hasEnableRunFlag(m.metadata)).length;
69
69
  if (count > seenEnableRunCount) {
70
- untrack(() => { seenEnableRunCount = count; });
70
+ untrack(() => {
71
+ seenEnableRunCount = count;
72
+ });
71
73
  runEnabled = true;
72
74
  }
73
75
  });
74
76
 
75
77
  $effect(() => {
76
78
  if (getCurrentSession()?.id) {
77
- untrack(() => { seenEnableRunCount = 0; });
79
+ untrack(() => {
80
+ seenEnableRunCount = 0;
81
+ });
78
82
  runEnabled = true;
79
83
  }
80
84
  });
@@ -87,7 +91,9 @@
87
91
  if (wasExecuting && !nowExecuting && inputField) {
88
92
  tick().then(() => inputField?.focus({ preventScroll: true }));
89
93
  }
90
- untrack(() => { wasExecuting = nowExecuting; });
94
+ untrack(() => {
95
+ wasExecuting = nowExecuting;
96
+ });
91
97
  });
92
98
 
93
99
  function handleSend(): void {
@@ -159,35 +159,37 @@
159
159
  </button>
160
160
  {#if getSessions().length > 0}
161
161
  <div class="control-panel__session-popover-divider"></div>
162
- {#each getSessions() as session (session.id)}
163
- {@const isActive = getCurrentSession()?.id === session.id}
164
- <div class="control-panel__session-popover-row">
165
- <button
166
- type="button"
167
- role="menuitem"
168
- class="control-panel__session-popover-item"
169
- class:control-panel__session-popover-item--active={isActive}
170
- onclick={() => handleSelect(session.id)}
171
- >
172
- {#if isActive}
173
- <Icon icon="mdi:check" class="control-panel__session-popover-check" />
174
- {:else}
175
- <Icon icon="mdi:message-outline" />
176
- {/if}
177
- <span>{session.name}</span>
178
- </button>
179
- <button
180
- type="button"
181
- role="menuitem"
182
- class="control-panel__session-popover-delete"
183
- onclick={(e) => handleDelete(e, session.id)}
184
- title={cp.deleteSession}
185
- aria-label={cp.deleteSession}
186
- >
187
- <Icon icon="mdi:delete-outline" />
188
- </button>
189
- </div>
190
- {/each}
162
+ <div class="control-panel__session-popover-list">
163
+ {#each getSessions() as session (session.id)}
164
+ {@const isActive = getCurrentSession()?.id === session.id}
165
+ <div class="control-panel__session-popover-row">
166
+ <button
167
+ type="button"
168
+ role="menuitem"
169
+ class="control-panel__session-popover-item"
170
+ class:control-panel__session-popover-item--active={isActive}
171
+ onclick={() => handleSelect(session.id)}
172
+ >
173
+ {#if isActive}
174
+ <Icon icon="mdi:check" class="control-panel__session-popover-check" />
175
+ {:else}
176
+ <Icon icon="mdi:message-outline" />
177
+ {/if}
178
+ <span>{session.name}</span>
179
+ </button>
180
+ <button
181
+ type="button"
182
+ role="menuitem"
183
+ class="control-panel__session-popover-delete"
184
+ onclick={(e) => handleDelete(e, session.id)}
185
+ title={cp.deleteSession}
186
+ aria-label={cp.deleteSession}
187
+ >
188
+ <Icon icon="mdi:delete-outline" />
189
+ </button>
190
+ </div>
191
+ {/each}
192
+ </div>
191
193
  {/if}
192
194
  </div>
193
195
  {/if}
@@ -333,6 +335,9 @@
333
335
  z-index: 50;
334
336
  min-width: 220px;
335
337
  max-width: 300px;
338
+ max-height: min(60vh, 420px);
339
+ display: flex;
340
+ flex-direction: column;
336
341
  padding: var(--fd-space-xs);
337
342
  background-color: var(--fd-background);
338
343
  border: 1px solid var(--fd-border);
@@ -344,6 +349,13 @@
344
349
  height: 1px;
345
350
  background-color: var(--fd-border-muted);
346
351
  margin: var(--fd-space-xs) 0;
352
+ flex-shrink: 0;
353
+ }
354
+
355
+ .control-panel__session-popover-list {
356
+ flex: 1 1 auto;
357
+ min-height: 0;
358
+ overflow-y: auto;
347
359
  }
348
360
 
349
361
  .control-panel__session-popover-row {
@@ -396,6 +408,7 @@
396
408
  color: var(--fd-primary);
397
409
  font-weight: 500;
398
410
  width: 100%;
411
+ flex: 0 0 auto;
399
412
  }
400
413
 
401
414
  .control-panel__session-popover-item--new :global(svg) {
@@ -22,6 +22,8 @@
22
22
  onInterruptResolved?: () => void;
23
23
  /** Optional callback that, when provided, shows a "New session" CTA in the welcome state */
24
24
  onCreateSession?: () => void;
25
+ /** Called when the user scrolls near the top to load older messages */
26
+ onLoadOlder?: () => void | Promise<void>;
25
27
  }
26
28
 
27
29
  let {
@@ -31,7 +33,8 @@
31
33
  allowLogs = true,
32
34
  compactSystemMessages = true,
33
35
  onInterruptResolved,
34
- onCreateSession
36
+ onCreateSession,
37
+ onLoadOlder
35
38
  }: Props = $props();
36
39
 
37
40
  const ec = $derived(m().playground.executionConsole);
@@ -50,6 +53,7 @@
50
53
  {allowLogs}
51
54
  {compactSystemMessages}
52
55
  {onInterruptResolved}
56
+ {onLoadOlder}
53
57
  welcome={welcomeState}
54
58
  emptySession={readyState}
55
59
  />
@@ -8,6 +8,8 @@ interface Props {
8
8
  onInterruptResolved?: () => void;
9
9
  /** Optional callback that, when provided, shows a "New session" CTA in the welcome state */
10
10
  onCreateSession?: () => void;
11
+ /** Called when the user scrolls near the top to load older messages */
12
+ onLoadOlder?: () => void | Promise<void>;
11
13
  }
12
14
  declare const ExecutionConsole: import("svelte").Component<Props, {}, "">;
13
15
  type ExecutionConsole = ReturnType<typeof ExecutionConsole>;
@@ -119,8 +119,13 @@
119
119
  }
120
120
 
121
121
  @keyframes pulse {
122
- 0%, 100% { opacity: 1; }
123
- 50% { opacity: 0.4; }
122
+ 0%,
123
+ 100% {
124
+ opacity: 1;
125
+ }
126
+ 50% {
127
+ opacity: 0.4;
128
+ }
124
129
  }
125
130
 
126
131
  :global(.execution-list__status-icon) {
@@ -55,7 +55,8 @@
55
55
  class="log-row__timestamp"
56
56
  datetime={message.timestamp}
57
57
  aria-label="sent at {formatTimestamp(message.timestamp)}"
58
- >{formatTimestamp(message.timestamp)}</time>
58
+ >{formatTimestamp(message.timestamp)}</time
59
+ >
59
60
  {/if}
60
61
  </div>
61
62
 
@@ -10,10 +10,7 @@
10
10
  -->
11
11
 
12
12
  <script lang="ts">
13
- import {
14
- resolveMessageDisplay,
15
- type PlaygroundMessage
16
- } from '../../types/playground.js';
13
+ import { resolveMessageDisplay, type PlaygroundMessage } from '../../types/playground.js';
17
14
  import ChatBubble from './ChatBubble.svelte';
18
15
  import LogRow from './LogRow.svelte';
19
16
  import MessageNotice from './MessageNotice.svelte';
@@ -43,7 +43,8 @@
43
43
  class="message-card__timestamp"
44
44
  datetime={message.timestamp}
45
45
  aria-label="sent at {formatTimestamp(message.timestamp)}"
46
- >{formatTimestamp(message.timestamp)}</time>
46
+ >{formatTimestamp(message.timestamp)}</time
47
+ >
47
48
  {/if}
48
49
  </header>
49
50
  {/if}
@@ -72,9 +72,15 @@
72
72
  margin-top: 0;
73
73
  }
74
74
 
75
- .message-markdown :global(h1) { font-size: var(--fd-text-xl); }
76
- .message-markdown :global(h2) { font-size: var(--fd-text-lg); }
77
- .message-markdown :global(h3) { font-size: var(--fd-text-base); }
75
+ .message-markdown :global(h1) {
76
+ font-size: var(--fd-text-xl);
77
+ }
78
+ .message-markdown :global(h2) {
79
+ font-size: var(--fd-text-lg);
80
+ }
81
+ .message-markdown :global(h3) {
82
+ font-size: var(--fd-text-base);
83
+ }
78
84
 
79
85
  .message-markdown :global(ul),
80
86
  .message-markdown :global(ol) {
@@ -155,6 +161,10 @@
155
161
  font-weight: 600;
156
162
  }
157
163
 
158
- .message-markdown :global(strong) { font-weight: 600; }
159
- .message-markdown :global(em) { font-style: italic; }
164
+ .message-markdown :global(strong) {
165
+ font-weight: 600;
166
+ }
167
+ .message-markdown :global(em) {
168
+ font-style: italic;
169
+ }
160
170
  </style>
@@ -42,7 +42,8 @@
42
42
  class="system-notice__timestamp"
43
43
  datetime={message.timestamp}
44
44
  aria-label="sent at {formatTimestamp(message.timestamp)}"
45
- >{formatTimestamp(message.timestamp)}</time>
45
+ >{formatTimestamp(message.timestamp)}</time
46
+ >
46
47
  {/if}
47
48
  </div>
48
49