@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
@@ -98,6 +98,13 @@
98
98
  "type": "boolean",
99
99
  "default": false,
100
100
  "description": "Whether to allow multiple selections.\nWhen true, users can select multiple values displayed as tags.\n"
101
+ },
102
+ "params": {
103
+ "type": "object",
104
+ "additionalProperties": {
105
+ "type": "string"
106
+ },
107
+ "description": "Map of URL query parameter names to sibling form field names.\nWhen fetching autocomplete options, the current value of each\nreferenced sibling field is appended as a query parameter.\nWhen any dependency field changes, the autocomplete clears its\ncurrent value and invalidates the suggestion cache.\n"
101
108
  }
102
109
  },
103
110
  "required": [
@@ -272,6 +279,42 @@
272
279
  "description": "Configuration for autocomplete fields (when format is \"autocomplete\")"
273
280
  }
274
281
  ]
282
+ },
283
+ "readOnly": {
284
+ "type": "boolean",
285
+ "description": "JSON Schema `readOnly` keyword. When true, the field is displayed but\ncannot be edited (rendered in a disabled state).\n"
286
+ },
287
+ "height": {
288
+ "type": "string",
289
+ "description": "Editor height as a CSS value (e.g. `200px`).\nApplies to editor fields: `json`/`code`, `markdown`, and `template`.\nDefaults: `200px` (code), `300px` (markdown), `250px` (template).\n"
290
+ },
291
+ "darkTheme": {
292
+ "type": "boolean",
293
+ "description": "Force the editor's dark theme on or off.\nApplies to `json`/`code` and `template` fields.\nWhen omitted, the editor follows the resolved app theme.\n"
294
+ },
295
+ "autoFormat": {
296
+ "type": "boolean",
297
+ "default": true,
298
+ "description": "Whether to auto-format JSON on blur.\nApplies to `json`/`code` editor fields.\n"
299
+ },
300
+ "showToolbar": {
301
+ "type": "boolean",
302
+ "default": true,
303
+ "description": "Whether to show the editor toolbar.\nApplies to `markdown` editor fields.\n"
304
+ },
305
+ "showStatusBar": {
306
+ "type": "boolean",
307
+ "default": true,
308
+ "description": "Whether to show the editor status bar.\nApplies to `markdown` editor fields.\n"
309
+ },
310
+ "spellChecker": {
311
+ "type": "boolean",
312
+ "default": false,
313
+ "description": "Whether to enable spell checking.\nApplies to `markdown` editor fields.\n"
314
+ },
315
+ "placeholderExample": {
316
+ "type": "string",
317
+ "description": "Example template string shown as a placeholder hint.\nApplies to `template` fields.\n"
275
318
  }
276
319
  },
277
320
  "required": [
@@ -453,7 +496,9 @@
453
496
  "completed",
454
497
  "failed",
455
498
  "cancelled",
456
- "skipped"
499
+ "skipped",
500
+ "paused",
501
+ "interrupted"
457
502
  ],
458
503
  "description": "Current execution status of a node"
459
504
  },
@@ -4,10 +4,11 @@
4
4
  */
5
5
  import type { CategoryDefinition } from '../types/index.js';
6
6
  import type { EndpointConfig } from '../config/endpoints.js';
7
+ import type { AuthProvider } from '../types/auth.js';
7
8
  /**
8
9
  * Fetch category definitions from API
9
10
  */
10
- export declare function fetchCategories(endpointConfig: EndpointConfig): Promise<CategoryDefinition[]>;
11
+ export declare function fetchCategories(endpointConfig: EndpointConfig, authProvider?: AuthProvider): Promise<CategoryDefinition[]>;
11
12
  /**
12
13
  * Validate category definitions structure
13
14
  */
@@ -2,17 +2,19 @@
2
2
  * Categories API Service
3
3
  * Handles fetching category definitions from the backend
4
4
  */
5
- import { buildEndpointUrl } from '../config/endpoints.js';
5
+ import { buildEndpointUrl, getEndpointHeaders } from '../config/endpoints.js';
6
6
  import { DEFAULT_CATEGORIES } from '../config/defaultCategories.js';
7
7
  import { logger } from '../utils/logger.js';
8
8
  /**
9
9
  * Fetch category definitions from API
10
10
  */
11
- export async function fetchCategories(endpointConfig) {
11
+ export async function fetchCategories(endpointConfig, authProvider) {
12
12
  try {
13
13
  const url = buildEndpointUrl(endpointConfig, endpointConfig.endpoints.categories);
14
+ const configHeaders = getEndpointHeaders(endpointConfig, 'categories');
15
+ const authHeaders = authProvider ? await authProvider.getAuthHeaders() : {};
14
16
  const response = await fetch(url, {
15
- headers: { 'Content-Type': 'application/json' }
17
+ headers: { ...configHeaders, ...authHeaders }
16
18
  });
17
19
  if (!response.ok) {
18
20
  throw new Error(`HTTP ${response.status}: ${response.statusText}`);
@@ -7,6 +7,20 @@
7
7
  * @module services/playgroundService
8
8
  */
9
9
  import type { PlaygroundSession, PlaygroundMessage, PlaygroundMessagesApiResponse, PlaygroundSessionStatus } from '../types/playground.js';
10
+ /**
11
+ * Pagination options for {@link PlaygroundService.getMessages}.
12
+ * `since`, `before`, and `latest` are mutually exclusive.
13
+ */
14
+ export interface GetMessagesOptions {
15
+ /** Forward cursor — only messages with sequenceNumber greater than this value */
16
+ since?: number;
17
+ /** Backward cursor — the page of messages immediately older than this sequence number */
18
+ before?: number;
19
+ /** Return the most recent `limit` messages (conversation tail) */
20
+ latest?: boolean;
21
+ /** Maximum number of messages to return */
22
+ limit?: number;
23
+ }
10
24
  /**
11
25
  * Playground Service class
12
26
  *
@@ -75,14 +89,19 @@ export declare class PlaygroundService {
75
89
  */
76
90
  deleteSession(sessionId: string): Promise<void>;
77
91
  /**
78
- * Get messages from a playground session
92
+ * Get messages from a playground session.
93
+ *
94
+ * Three pagination modes (see the OpenAPI spec for the contract):
95
+ * - `since`: forward cursor, returns messages with sequenceNumber > value (polling the live tail)
96
+ * - `before`: backward cursor, returns the page immediately older than the value (scroll-up)
97
+ * - `latest`: returns the most recent `limit` messages (initial load)
98
+ * `since`, `before`, and `latest` are mutually exclusive.
79
99
  *
80
100
  * @param sessionId - The session UUID
81
- * @param afterSequence - Optional sequence number cursor — returns only messages with sequenceNumber > this value
82
- * @param limit - Maximum number of messages to return
101
+ * @param options - Pagination options
83
102
  * @returns Messages and session status
84
103
  */
85
- getMessages(sessionId: string, afterSequence?: number, limit?: number): Promise<PlaygroundMessagesApiResponse>;
104
+ getMessages(sessionId: string, options?: GetMessagesOptions): Promise<PlaygroundMessagesApiResponse>;
86
105
  /**
87
106
  * Send a message to a playground session
88
107
  *
@@ -168,24 +168,35 @@ export class PlaygroundService {
168
168
  // Message Handling
169
169
  // =========================================================================
170
170
  /**
171
- * Get messages from a playground session
171
+ * Get messages from a playground session.
172
+ *
173
+ * Three pagination modes (see the OpenAPI spec for the contract):
174
+ * - `since`: forward cursor, returns messages with sequenceNumber > value (polling the live tail)
175
+ * - `before`: backward cursor, returns the page immediately older than the value (scroll-up)
176
+ * - `latest`: returns the most recent `limit` messages (initial load)
177
+ * `since`, `before`, and `latest` are mutually exclusive.
172
178
  *
173
179
  * @param sessionId - The session UUID
174
- * @param afterSequence - Optional sequence number cursor — returns only messages with sequenceNumber > this value
175
- * @param limit - Maximum number of messages to return
180
+ * @param options - Pagination options
176
181
  * @returns Messages and session status
177
182
  */
178
- async getMessages(sessionId, afterSequence, limit) {
183
+ async getMessages(sessionId, options = {}) {
179
184
  const config = this.getConfig();
180
185
  let url = buildEndpointUrl(config, config.endpoints.playground.getMessages, {
181
186
  sessionId
182
187
  });
183
188
  const params = new URLSearchParams();
184
- if (afterSequence !== undefined) {
185
- params.append('since', afterSequence.toString());
189
+ if (options.since !== undefined) {
190
+ params.append('since', options.since.toString());
191
+ }
192
+ if (options.before !== undefined) {
193
+ params.append('before', options.before.toString());
186
194
  }
187
- if (limit !== undefined) {
188
- params.append('limit', limit.toString());
195
+ if (options.latest) {
196
+ params.append('latest', 'true');
197
+ }
198
+ if (options.limit !== undefined) {
199
+ params.append('limit', options.limit.toString());
189
200
  }
190
201
  const queryString = params.toString();
191
202
  if (queryString) {
@@ -257,7 +268,9 @@ export class PlaygroundService {
257
268
  return;
258
269
  }
259
270
  try {
260
- const response = await this.getMessages(sessionId, this.lastSequenceNumber ?? undefined);
271
+ const response = await this.getMessages(sessionId, {
272
+ since: this.lastSequenceNumber ?? undefined
273
+ });
261
274
  // Update last sequence number cursor
262
275
  if (response.data && response.data.length > 0) {
263
276
  const lastMessage = response.data[response.data.length - 1];
@@ -4,10 +4,11 @@
4
4
  */
5
5
  import type { PortConfig } from '../types/index.js';
6
6
  import type { EndpointConfig } from '../config/endpoints.js';
7
+ import type { AuthProvider } from '../types/auth.js';
7
8
  /**
8
9
  * Fetch port configuration from API
9
10
  */
10
- export declare function fetchPortConfig(endpointConfig: EndpointConfig): Promise<PortConfig>;
11
+ export declare function fetchPortConfig(endpointConfig: EndpointConfig, authProvider?: AuthProvider): Promise<PortConfig>;
11
12
  /**
12
13
  * Validate port configuration structure
13
14
  */
@@ -2,17 +2,19 @@
2
2
  * Port Configuration API Service
3
3
  * Handles fetching port configuration from the backend
4
4
  */
5
- import { buildEndpointUrl } from '../config/endpoints.js';
5
+ import { buildEndpointUrl, getEndpointHeaders } from '../config/endpoints.js';
6
6
  import { DEFAULT_PORT_CONFIG } from '../config/defaultPortConfig.js';
7
7
  import { logger } from '../utils/logger.js';
8
8
  /**
9
9
  * Fetch port configuration from API
10
10
  */
11
- export async function fetchPortConfig(endpointConfig) {
11
+ export async function fetchPortConfig(endpointConfig, authProvider) {
12
12
  try {
13
13
  const url = buildEndpointUrl(endpointConfig, endpointConfig.endpoints.portConfig);
14
+ const configHeaders = getEndpointHeaders(endpointConfig, 'portConfig');
15
+ const authHeaders = authProvider ? await authProvider.getAuthHeaders() : {};
14
16
  const response = await fetch(url, {
15
- headers: { 'Content-Type': 'application/json' }
17
+ headers: { ...configHeaders, ...authHeaders }
16
18
  });
17
19
  if (!response.ok) {
18
20
  throw new Error(`HTTP ${response.status}: ${response.statusText}`);
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * @module stores/playgroundStore
8
8
  */
9
- import type { PlaygroundSession, PlaygroundMessage, PlaygroundInputField, PlaygroundSessionStatus, PlaygroundMessagesApiResponse } from '../types/playground.js';
9
+ import type { PlaygroundSession, PlaygroundMessage, PlaygroundInputField, PlaygroundSessionStatus, PlaygroundMessagesApiResponse, PlaygroundExecution } from '../types/playground.js';
10
10
  import type { Workflow } from '../types/index.js';
11
11
  /**
12
12
  * Get the current session
@@ -83,6 +83,11 @@ export declare function getSessionCount(): number;
83
83
  export declare function getPinnedExecutionId(): string | null;
84
84
  export declare function getLatestExecutionId(): string | null;
85
85
  export declare function getActiveExecutionId(): string | null;
86
+ /**
87
+ * Main pipeline runs for the run-switcher. Excludes sub-flow runs, which can't
88
+ * render their own graph and so aren't user-selectable.
89
+ */
90
+ export declare function getSelectableExecutions(): PlaygroundExecution[];
86
91
  /**
87
92
  * Counter that increments whenever new messages arrive and the pipeline display
88
93
  * should re-fetch — i.e. when following latest or pinned to the latest execution.
@@ -221,6 +226,22 @@ export declare function getMessagesSnapshot(): PlaygroundMessage[];
221
226
  * @returns Sequence number of the last message, or null
222
227
  */
223
228
  export declare function getLatestSequenceNumber(): number | null;
229
+ /**
230
+ * Get the sequence number of the oldest loaded message, used as the cursor
231
+ * for backward "load older" pagination.
232
+ *
233
+ * @returns Sequence number of the first message, or null
234
+ */
235
+ export declare function getOldestSequenceNumber(): number | null;
236
+ /**
237
+ * Whether older messages exist before the oldest one currently loaded.
238
+ */
239
+ export declare function getHasOlder(): boolean;
240
+ /**
241
+ * Set whether older messages remain to be loaded, derived from a
242
+ * backward-pagination response.
243
+ */
244
+ export declare function setHasOlder(hasOlder: boolean): void;
224
245
  /**
225
246
  * Subscribe to session status changes using $effect.root.
226
247
  * This is designed for use in non-component contexts (e.g., mount.ts).
@@ -23,6 +23,12 @@ let _sessions = $state([]);
23
23
  * Messages in the current session
24
24
  */
25
25
  let _messages = $state([]);
26
+ /**
27
+ * Whether older messages exist before the oldest one currently loaded.
28
+ * Drives the scroll-up "load older" affordance. Reset whenever the message
29
+ * set is replaced (session switch / clear).
30
+ */
31
+ let _hasOlder = $state(false);
26
32
  /**
27
33
  * Whether we are currently loading data
28
34
  */
@@ -45,8 +51,19 @@ let _pinnedExecutionId = $state(null);
45
51
  let _pipelineRefreshTrigger = $state(0);
46
52
  /** Whether log messages are visible in the execution console */
47
53
  let _showLogs = $state(true);
48
- /** Latest execution ID derived from current session's executions list */
49
- const _latestExecutionId = $derived(_currentSession?.executions?.at(-1)?.id ?? null);
54
+ /**
55
+ * The main pipeline runs — the single source of truth for "what's selectable".
56
+ * Sub-flow runs are tracked for classification but excluded here: selecting one
57
+ * can't show its own graph (the panel renders the main pipeline regardless), so
58
+ * listing them would be dead UI. Feeds both the run-switcher and "latest".
59
+ */
60
+ const _selectableExecutions = $derived((_currentSession?.executions ?? []).filter((e) => !e.isSubflow));
61
+ /**
62
+ * Latest execution ID: the most recent main run, so the sidebar keeps the main
63
+ * pipeline in focus and never auto-follows a sub-flow. Null when no main run is
64
+ * known yet — better an empty panel for a poll than the wrong graph.
65
+ */
66
+ const _latestExecutionId = $derived(_selectableExecutions.at(-1)?.id ?? null);
50
67
  /** Active execution: pinned if set, otherwise latest */
51
68
  const _activeExecutionId = $derived(_pinnedExecutionId ?? _latestExecutionId);
52
69
  // Derived from server status — never manually set.
@@ -231,6 +248,13 @@ export function getLatestExecutionId() {
231
248
  export function getActiveExecutionId() {
232
249
  return _activeExecutionId;
233
250
  }
251
+ /**
252
+ * Main pipeline runs for the run-switcher. Excludes sub-flow runs, which can't
253
+ * render their own graph and so aren't user-selectable.
254
+ */
255
+ export function getSelectableExecutions() {
256
+ return _selectableExecutions;
257
+ }
234
258
  /**
235
259
  * Counter that increments whenever new messages arrive and the pipeline display
236
260
  * should re-fetch — i.e. when following latest or pinned to the latest execution.
@@ -279,33 +303,57 @@ function sortMessagesChronologically(messageList) {
279
303
  return a.id.localeCompare(b.id);
280
304
  });
281
305
  }
306
+ /**
307
+ * Whether a message was produced by a nested sub-flow (vs the main pipeline).
308
+ * `parentPipelineId` is the authoritative signal — non-null means a parent run
309
+ * triggered this one. Legacy runs that predate the field carry no nesting info,
310
+ * so they're treated as main runs (by design — we don't reclassify history).
311
+ */
312
+ function isSubflowMessage(msg) {
313
+ return msg.parentPipelineId != null;
314
+ }
282
315
  /**
283
316
  * Syncs the current session's executions list from incoming messages.
284
- * When a message has a new executionId not yet tracked, adds it as a new execution entry.
317
+ *
318
+ * Each message's `executionId` identifies the run that produced it, and
319
+ * `parentPipelineId` says whether that run is the main pipeline or a nested
320
+ * sub-flow (see {@link isSubflowMessage}). A run's classification is fixed by
321
+ * its first sighting — every message from a run reports the same parent — so we
322
+ * only act on executionIds we haven't seen before. Sub-flows are tracked but
323
+ * hidden from the run-switcher; a new *main* run clears the pin so the panel
324
+ * auto-follows it, while sub-flow runs never take focus.
325
+ *
326
+ * Executions are appended in arrival order; new runs land at the tail, which is
327
+ * why "latest" reads the last main run.
285
328
  */
286
329
  function syncExecutionsFromMessages(messages) {
287
330
  if (!_currentSession)
288
331
  return;
289
- const existingIds = new Set((_currentSession.executions ?? []).map((e) => e.id));
290
- const newExecutions = [];
332
+ const executions = [...(_currentSession.executions ?? [])];
333
+ const seenIds = new Set(executions.map((e) => e.id));
334
+ let added = false;
335
+ let gainedMainRun = false;
291
336
  for (const msg of messages) {
292
- if (msg.executionId && !existingIds.has(msg.executionId)) {
293
- existingIds.add(msg.executionId);
294
- newExecutions.push({
295
- id: msg.executionId,
296
- startedAt: msg.timestamp,
297
- status: 'running'
298
- });
299
- }
337
+ if (!msg.executionId || seenIds.has(msg.executionId))
338
+ continue;
339
+ seenIds.add(msg.executionId);
340
+ const isSubflow = isSubflowMessage(msg);
341
+ executions.push({
342
+ id: msg.executionId,
343
+ startedAt: msg.timestamp,
344
+ status: 'running',
345
+ isSubflow
346
+ });
347
+ added = true;
348
+ if (!isSubflow)
349
+ gainedMainRun = true;
300
350
  }
301
- if (newExecutions.length > 0) {
302
- _currentSession = {
303
- ..._currentSession,
304
- executions: [...(_currentSession.executions ?? []), ...newExecutions]
305
- };
306
- // Clear any manual pin so the panel automatically follows the new run.
351
+ if (!added)
352
+ return;
353
+ _currentSession = { ..._currentSession, executions };
354
+ // Auto-follow the new main run by dropping any manual pin.
355
+ if (gainedMainRun)
307
356
  _pinnedExecutionId = null;
308
- }
309
357
  }
310
358
  // =========================================================================
311
359
  // Actions
@@ -348,22 +396,23 @@ export const playgroundActions = {
348
396
  updatedAt: new Date().toISOString()
349
397
  };
350
398
  }
351
- // Update the latest execution status when the session reaches a terminal state.
352
- // Only the last execution can be running at any time (sessions are single-pipeline),
353
- // so we only need to check and update the tail entry.
399
+ // When the session reaches a terminal state, the whole run is finished
400
+ // including any sub-flow executions, which may sit anywhere in the list
401
+ // (not just the tail), so mark every still-running execution terminal.
354
402
  // 'idle' means the run finished normally (server returns 'idle' post-completion,
355
- // not 'completed'), so map it to 'completed' for the execution entry.
403
+ // not 'completed'), so map it to 'completed' for the execution entries.
356
404
  const terminalExecutionStatus = status === 'failed'
357
405
  ? 'failed'
358
406
  : status === 'completed' || status === 'idle'
359
407
  ? 'completed'
360
408
  : null;
361
409
  if (terminalExecutionStatus && _currentSession?.executions?.length) {
362
- const execs = [..._currentSession.executions];
363
- const last = execs[execs.length - 1];
364
- if (last.status === 'running') {
365
- execs[execs.length - 1] = { ...last, status: terminalExecutionStatus };
366
- _currentSession = { ..._currentSession, executions: execs };
410
+ const hasRunning = _currentSession.executions.some((e) => e.status === 'running');
411
+ if (hasRunning) {
412
+ _currentSession = {
413
+ ..._currentSession,
414
+ executions: _currentSession.executions.map((e) => e.status === 'running' ? { ...e, status: terminalExecutionStatus } : e)
415
+ };
367
416
  }
368
417
  }
369
418
  // Also update in sessions list
@@ -417,7 +466,7 @@ export const playgroundActions = {
417
466
  * @param message - The message to add
418
467
  */
419
468
  addMessage: (message) => {
420
- if (_messages.some(m => m.id === message.id))
469
+ if (_messages.some((m) => m.id === message.id))
421
470
  return;
422
471
  const seq = message.sequenceNumber ?? 0;
423
472
  let lo = 0, hi = _messages.length;
@@ -460,6 +509,7 @@ export const playgroundActions = {
460
509
  clearMessages: () => {
461
510
  _messages = [];
462
511
  _lastPollSequenceNumber = null;
512
+ _hasOlder = false;
463
513
  },
464
514
  /**
465
515
  * Set the loading state
@@ -533,8 +583,8 @@ export const playgroundActions = {
533
583
  export function applyServerResponse(response) {
534
584
  if (response.data && response.data.length > 0) {
535
585
  playgroundActions.addMessages(response.data);
536
- // Refresh pipeline when following latest or pinned to the latest execution.
537
- // Skip only when the user is viewing a historical run.
586
+ // Refresh the pipeline panel when following latest or pinned to the latest
587
+ // run. Skip when pinned to an older run — a historical view that won't change.
538
588
  if (_pinnedExecutionId === null || _pinnedExecutionId === _latestExecutionId) {
539
589
  _pipelineRefreshTrigger++;
540
590
  }
@@ -584,6 +634,33 @@ export function getLatestSequenceNumber() {
584
634
  }
585
635
  return null;
586
636
  }
637
+ /**
638
+ * Get the sequence number of the oldest loaded message, used as the cursor
639
+ * for backward "load older" pagination.
640
+ *
641
+ * @returns Sequence number of the first message, or null
642
+ */
643
+ export function getOldestSequenceNumber() {
644
+ for (let i = 0; i < _messages.length; i++) {
645
+ if (_messages[i].sequenceNumber !== undefined) {
646
+ return _messages[i].sequenceNumber;
647
+ }
648
+ }
649
+ return null;
650
+ }
651
+ /**
652
+ * Whether older messages exist before the oldest one currently loaded.
653
+ */
654
+ export function getHasOlder() {
655
+ return _hasOlder;
656
+ }
657
+ /**
658
+ * Set whether older messages remain to be loaded, derived from a
659
+ * backward-pagination response.
660
+ */
661
+ export function setHasOlder(hasOlder) {
662
+ _hasOlder = hasOlder;
663
+ }
587
664
  /**
588
665
  * Subscribe to session status changes using $effect.root.
589
666
  * This is designed for use in non-component contexts (e.g., mount.ts).
@@ -153,6 +153,7 @@ export declare function mountWorkflowEditor(container: HTMLElement, options?: {
153
153
  endpointConfig?: EndpointConfig;
154
154
  portConfig?: PortConfig;
155
155
  categories?: CategoryDefinition[];
156
+ authProvider?: AuthProvider;
156
157
  }): Promise<MountedFlowDropApp>;
157
158
  /**
158
159
  * Unmount a FlowDrop app
@@ -84,7 +84,7 @@ export async function mountFlowDropApp(container, options = {}) {
84
84
  if (!finalPortConfig && config) {
85
85
  // Try to fetch port configuration from API
86
86
  try {
87
- finalPortConfig = await fetchPortConfig(config);
87
+ finalPortConfig = await fetchPortConfig(config, authProvider);
88
88
  }
89
89
  catch (error) {
90
90
  logger.warn('Failed to fetch port config from API, using default:', error);
@@ -101,7 +101,7 @@ export async function mountFlowDropApp(container, options = {}) {
101
101
  }
102
102
  else if (config) {
103
103
  try {
104
- const fetchedCategories = await fetchCategories(config);
104
+ const fetchedCategories = await fetchCategories(config, authProvider);
105
105
  initializeCategories(fetchedCategories);
106
106
  }
107
107
  catch (error) {
@@ -244,7 +244,7 @@ export async function mountFlowDropApp(container, options = {}) {
244
244
  * @returns Promise resolving to a MountedFlowDropApp instance
245
245
  */
246
246
  export async function mountWorkflowEditor(container, options = {}) {
247
- const { nodes = [], endpointConfig, portConfig, categories } = options;
247
+ const { nodes = [], endpointConfig, portConfig, categories, authProvider } = options;
248
248
  // Create endpoint configuration
249
249
  let config;
250
250
  if (endpointConfig) {
@@ -269,7 +269,7 @@ export async function mountWorkflowEditor(container, options = {}) {
269
269
  if (!finalPortConfig && config) {
270
270
  // Try to fetch port configuration from API
271
271
  try {
272
- finalPortConfig = await fetchPortConfig(config);
272
+ finalPortConfig = await fetchPortConfig(config, authProvider);
273
273
  }
274
274
  catch (error) {
275
275
  logger.warn('Failed to fetch port config from API, using default:', error);
@@ -286,7 +286,7 @@ export async function mountWorkflowEditor(container, options = {}) {
286
286
  }
287
287
  else if (config) {
288
288
  try {
289
- const fetchedCategories = await fetchCategories(config);
289
+ const fetchedCategories = await fetchCategories(config, authProvider);
290
290
  initializeCategories(fetchedCategories);
291
291
  }
292
292
  catch (error) {
@@ -297,6 +297,19 @@ export interface AutocompleteConfig {
297
297
  * @default false
298
298
  */
299
299
  multiple?: boolean;
300
+ /**
301
+ * Map of URL query parameter names to sibling form field names.
302
+ * When fetching autocomplete options, the current value of each
303
+ * referenced sibling field is appended as a query parameter.
304
+ *
305
+ * Example: { "account": "account", "project": "project" }
306
+ * If the "account" field currently holds "my-jira", the URL becomes:
307
+ * /api/jira/issue-types?account=my-jira&q=...
308
+ *
309
+ * When any dependency field changes, the autocomplete clears its
310
+ * current value and invalidates the suggestion cache.
311
+ */
312
+ params?: Record<string, string>;
300
313
  }
301
314
  /**
302
315
  * Dynamic schema endpoint configuration