@flowdrop/flowdrop 1.13.0 → 1.15.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.
- package/README.md +5 -0
- package/dist/components/ConfigForm.svelte +41 -21
- package/dist/components/ConfigPanel.svelte +7 -1
- package/dist/components/NodeSwapPicker.svelte +5 -1
- package/dist/components/PipelineStatus.svelte +11 -2
- package/dist/components/SchemaForm.svelte +28 -16
- package/dist/components/SettingsPanel.svelte +5 -1
- package/dist/components/WorkflowEditor.svelte +5 -1
- package/dist/components/chat/AIChatPanel.svelte +1 -5
- package/dist/components/form/FormAutocomplete.svelte +23 -12
- package/dist/components/interrupt/ChoicePrompt.svelte +5 -1
- package/dist/components/interrupt/InterruptBubble.svelte +4 -5
- package/dist/components/nodes/AtomNode.svelte +280 -0
- package/dist/components/nodes/AtomNode.svelte.d.ts +26 -0
- package/dist/components/playground/ChatBubble.svelte +6 -8
- package/dist/components/playground/ChatInput.svelte +11 -5
- package/dist/components/playground/ControlPanel.svelte +42 -29
- package/dist/components/playground/ExecutionConsole.svelte +5 -1
- package/dist/components/playground/ExecutionConsole.svelte.d.ts +2 -0
- package/dist/components/playground/ExecutionList.svelte +7 -2
- package/dist/components/playground/LogRow.svelte +2 -1
- package/dist/components/playground/MessageBubble.svelte +1 -4
- package/dist/components/playground/MessageCard.svelte +2 -1
- package/dist/components/playground/MessageMarkdown.svelte +15 -5
- package/dist/components/playground/MessageNotice.svelte +2 -1
- package/dist/components/playground/MessageStream.svelte +138 -17
- package/dist/components/playground/MessageStream.svelte.d.ts +5 -0
- package/dist/components/playground/MessageTagChip.svelte +24 -6
- package/dist/components/playground/PipelineKanbanView.svelte +40 -11
- package/dist/components/playground/PipelinePanel.svelte +5 -1
- package/dist/components/playground/PipelineTableView.svelte +20 -6
- package/dist/components/playground/Playground.svelte +94 -27
- package/dist/components/playground/PlaygroundStudio.svelte +21 -7
- package/dist/components/playground/pipelineViewUtils.svelte.js +11 -4
- package/dist/helpers/proximityConnect.d.ts +4 -1
- package/dist/helpers/proximityConnect.js +17 -1
- package/dist/openapi/v1/openapi.yaml +6466 -0
- package/dist/playground/mount.js +2 -2
- package/dist/registry/builtinNodes.d.ts +1 -1
- package/dist/registry/builtinNodes.js +13 -0
- package/dist/schemas/v1/workflow.schema.json +86 -3
- package/dist/services/playgroundService.d.ts +23 -4
- package/dist/services/playgroundService.js +22 -9
- package/dist/stores/playgroundStore.svelte.d.ts +29 -2
- package/dist/stores/playgroundStore.svelte.js +120 -35
- package/dist/types/index.d.ts +38 -3
- package/dist/types/playground.d.ts +36 -2
- package/dist/utils/formMerge.d.ts +36 -0
- package/dist/utils/formMerge.js +70 -0
- package/dist/utils/nodeTypes.js +1 -0
- package/package.json +7 -1
package/dist/playground/mount.js
CHANGED
|
@@ -106,10 +106,10 @@ function buildMountedPlayground(svelteApp, workflowId, config, onSessionStatusCh
|
|
|
106
106
|
startPolling: () => {
|
|
107
107
|
const session = getCurrentSession();
|
|
108
108
|
if (session) {
|
|
109
|
-
playgroundService.startPolling(session.id, (response) => applyServerResponse(response), pollingInterval, config.shouldStopPolling, playgroundService.getLastSequenceNumber());
|
|
109
|
+
playgroundService.startPolling(session.id, (response) => applyServerResponse(response, session.id), pollingInterval, config.shouldStopPolling, playgroundService.getLastSequenceNumber());
|
|
110
110
|
}
|
|
111
111
|
},
|
|
112
|
-
pushMessages: (response) => applyServerResponse(response),
|
|
112
|
+
pushMessages: (response) => applyServerResponse(response, null),
|
|
113
113
|
reset: () => {
|
|
114
114
|
playgroundService.stopPolling();
|
|
115
115
|
playgroundActions.reset();
|
|
@@ -70,7 +70,7 @@ export declare function getBuiltinTypes(): string[];
|
|
|
70
70
|
* Type for built-in node types.
|
|
71
71
|
* Use this when you specifically need a built-in type.
|
|
72
72
|
*/
|
|
73
|
-
export type BuiltinNodeType = 'workflowNode' | 'simple' | 'square' | 'tool' | 'gateway' | 'note' | 'terminal' | 'idea';
|
|
73
|
+
export type BuiltinNodeType = 'workflowNode' | 'simple' | 'square' | 'atom' | 'tool' | 'gateway' | 'note' | 'terminal' | 'idea';
|
|
74
74
|
/**
|
|
75
75
|
* Array of built-in type strings for runtime validation.
|
|
76
76
|
*/
|
|
@@ -9,6 +9,7 @@ import { nodeComponentRegistry } from './nodeComponentRegistry.js';
|
|
|
9
9
|
import WorkflowNode from '../components/nodes/WorkflowNode.svelte';
|
|
10
10
|
import SimpleNode from '../components/nodes/SimpleNode.svelte';
|
|
11
11
|
import SquareNode from '../components/nodes/SquareNode.svelte';
|
|
12
|
+
import AtomNode from '../components/nodes/AtomNode.svelte';
|
|
12
13
|
import ToolNode from '../components/nodes/ToolNode.svelte';
|
|
13
14
|
import GatewayNode from '../components/nodes/GatewayNode.svelte';
|
|
14
15
|
import NotesNode from '../components/nodes/NotesNode.svelte';
|
|
@@ -56,6 +57,17 @@ export const BUILTIN_NODE_COMPONENTS = [
|
|
|
56
57
|
statusPosition: 'top-right',
|
|
57
58
|
statusSize: 'sm'
|
|
58
59
|
},
|
|
60
|
+
{
|
|
61
|
+
type: 'atom',
|
|
62
|
+
displayName: 'Atom (Minimal Value/Transform)',
|
|
63
|
+
description: 'Low-chrome label-only node for constants and inline transforms',
|
|
64
|
+
component: AtomNode,
|
|
65
|
+
icon: 'mdi:circle-small',
|
|
66
|
+
category: 'visual',
|
|
67
|
+
source: FLOWDROP_SOURCE,
|
|
68
|
+
statusPosition: 'top-right',
|
|
69
|
+
statusSize: 'sm'
|
|
70
|
+
},
|
|
59
71
|
{
|
|
60
72
|
type: 'tool',
|
|
61
73
|
displayName: 'Tool (Agent Tool)',
|
|
@@ -197,6 +209,7 @@ export const BUILTIN_NODE_TYPES = [
|
|
|
197
209
|
'workflowNode',
|
|
198
210
|
'simple',
|
|
199
211
|
'square',
|
|
212
|
+
'atom',
|
|
200
213
|
'tool',
|
|
201
214
|
'gateway',
|
|
202
215
|
'note',
|
|
@@ -51,6 +51,46 @@
|
|
|
51
51
|
"metadata"
|
|
52
52
|
],
|
|
53
53
|
"$defs": {
|
|
54
|
+
"AtomUIConfig": {
|
|
55
|
+
"type": "object",
|
|
56
|
+
"description": "Display/behaviour settings for minimalist `atom` nodes (e.g. Constant, Cast).\nLives under `extensions.ui.atom`. The atom renderer reads these to decide what\nto show, and `valueTypeKey` drives the bound output port's `dataType` from\nconfig so connection validation matches the type the user picked.\n\nAll fields are optional — an empty object renders a label-only pill using the\nnode's `label`.\n",
|
|
57
|
+
"properties": {
|
|
58
|
+
"valueKey": {
|
|
59
|
+
"type": "string",
|
|
60
|
+
"description": "Config key whose value becomes the node body text.\nFalls back to the node `label` when unset or empty.\n"
|
|
61
|
+
},
|
|
62
|
+
"valueTypeKey": {
|
|
63
|
+
"type": "string",
|
|
64
|
+
"description": "Config key holding the selected value's type (a port dataType id).\nThe bound output port adopts this dataType.\n"
|
|
65
|
+
},
|
|
66
|
+
"outputPortId": {
|
|
67
|
+
"type": "string",
|
|
68
|
+
"description": "Output port id driven by `valueTypeKey`.\nDefaults to the first output port when unset.\n"
|
|
69
|
+
},
|
|
70
|
+
"shape": {
|
|
71
|
+
"type": "string",
|
|
72
|
+
"enum": [
|
|
73
|
+
"pill",
|
|
74
|
+
"rectangle"
|
|
75
|
+
],
|
|
76
|
+
"default": "pill",
|
|
77
|
+
"description": "Body shape. `pill` (default) is fully rounded; `rectangle` is lightly rounded.\n"
|
|
78
|
+
},
|
|
79
|
+
"prefix": {
|
|
80
|
+
"type": "string",
|
|
81
|
+
"description": "Dimmed affordance rendered before the body (e.g. `\"→ \"` to mark a transform).\nStays visible while the body value ellipsizes. Hidden in the empty state.\n"
|
|
82
|
+
},
|
|
83
|
+
"placeholder": {
|
|
84
|
+
"type": "string",
|
|
85
|
+
"description": "Text shown (dimmed) when the resolved body value is empty/unset."
|
|
86
|
+
},
|
|
87
|
+
"maxWidth": {
|
|
88
|
+
"type": "integer",
|
|
89
|
+
"description": "Max body width in px before the label ellipsizes."
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
"additionalProperties": false
|
|
93
|
+
},
|
|
54
94
|
"AutocompleteConfig": {
|
|
55
95
|
"type": "object",
|
|
56
96
|
"description": "Configuration for autocomplete fields that fetch suggestions from a callback URL.\nUsed when format is \"autocomplete\".\n",
|
|
@@ -279,6 +319,42 @@
|
|
|
279
319
|
"description": "Configuration for autocomplete fields (when format is \"autocomplete\")"
|
|
280
320
|
}
|
|
281
321
|
]
|
|
322
|
+
},
|
|
323
|
+
"readOnly": {
|
|
324
|
+
"type": "boolean",
|
|
325
|
+
"description": "JSON Schema `readOnly` keyword. When true, the field is displayed but\ncannot be edited (rendered in a disabled state).\n"
|
|
326
|
+
},
|
|
327
|
+
"height": {
|
|
328
|
+
"type": "string",
|
|
329
|
+
"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"
|
|
330
|
+
},
|
|
331
|
+
"darkTheme": {
|
|
332
|
+
"type": "boolean",
|
|
333
|
+
"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"
|
|
334
|
+
},
|
|
335
|
+
"autoFormat": {
|
|
336
|
+
"type": "boolean",
|
|
337
|
+
"default": true,
|
|
338
|
+
"description": "Whether to auto-format JSON on blur.\nApplies to `json`/`code` editor fields.\n"
|
|
339
|
+
},
|
|
340
|
+
"showToolbar": {
|
|
341
|
+
"type": "boolean",
|
|
342
|
+
"default": true,
|
|
343
|
+
"description": "Whether to show the editor toolbar.\nApplies to `markdown` editor fields.\n"
|
|
344
|
+
},
|
|
345
|
+
"showStatusBar": {
|
|
346
|
+
"type": "boolean",
|
|
347
|
+
"default": true,
|
|
348
|
+
"description": "Whether to show the editor status bar.\nApplies to `markdown` editor fields.\n"
|
|
349
|
+
},
|
|
350
|
+
"spellChecker": {
|
|
351
|
+
"type": "boolean",
|
|
352
|
+
"default": false,
|
|
353
|
+
"description": "Whether to enable spell checking.\nApplies to `markdown` editor fields.\n"
|
|
354
|
+
},
|
|
355
|
+
"placeholderExample": {
|
|
356
|
+
"type": "string",
|
|
357
|
+
"description": "Example template string shown as a placeholder hint.\nApplies to `template` fields.\n"
|
|
282
358
|
}
|
|
283
359
|
},
|
|
284
360
|
"required": [
|
|
@@ -378,15 +454,17 @@
|
|
|
378
454
|
},
|
|
379
455
|
"nodeType": {
|
|
380
456
|
"type": "string",
|
|
381
|
-
"description": "Changes how the node is visually rendered. This allows a single node definition\nto support multiple visual representations.\n\nAvailable built-in types:\n- `default`: Standard workflow node with full details\n- `simple`: Compact layout with minimal chrome\n- `square`: Geometric square layout (icon-only)\n- `tool`: Specialized style for agent tools\n- `gateway`: Branching control flow visualization\n- `terminal`: Start/end/exit node styling\n- `note`: Sticky note style for annotations\n\nThe node's `metadata.supportedTypes` defines which types are allowed.\nIf invalid or missing, falls back to `metadata.type` or \"default\".\n",
|
|
457
|
+
"description": "Changes how the node is visually rendered. This allows a single node definition\nto support multiple visual representations.\n\nAvailable built-in types:\n- `default`: Standard workflow node with full details\n- `simple`: Compact layout with minimal chrome\n- `square`: Geometric square layout (icon-only)\n- `atom`: Minimal label-only pill/rectangle (uses extensions.ui.atom)\n- `tool`: Specialized style for agent tools\n- `gateway`: Branching control flow visualization\n- `terminal`: Start/end/exit node styling\n- `note`: Sticky note style for annotations\n- `idea`: Conceptual idea node for BPMN-like flow diagrams\n\nThe node's `metadata.supportedTypes` defines which types are allowed.\nIf invalid or missing, falls back to `metadata.type` or \"default\".\n",
|
|
382
458
|
"enum": [
|
|
383
459
|
"default",
|
|
384
460
|
"simple",
|
|
385
461
|
"square",
|
|
462
|
+
"atom",
|
|
386
463
|
"tool",
|
|
387
464
|
"gateway",
|
|
388
465
|
"terminal",
|
|
389
|
-
"note"
|
|
466
|
+
"note",
|
|
467
|
+
"idea"
|
|
390
468
|
]
|
|
391
469
|
},
|
|
392
470
|
"dynamicInputs": {
|
|
@@ -641,12 +719,14 @@
|
|
|
641
719
|
"note",
|
|
642
720
|
"simple",
|
|
643
721
|
"square",
|
|
722
|
+
"atom",
|
|
644
723
|
"tool",
|
|
645
724
|
"gateway",
|
|
646
725
|
"terminal",
|
|
726
|
+
"idea",
|
|
647
727
|
"default"
|
|
648
728
|
],
|
|
649
|
-
"description": "Visual rendering type for the node.\n\nBuilt-in types:\n- `note` - Sticky note with markdown support\n- `simple` - Compact layout with header and description\n- `square` - Minimal square node with centered icon\n- `tool` - Specialized node for agent tools\n- `gateway` - Branching control flow with dynamic branches (uses config.branches)\n- `terminal` - Circular node for workflow start/end/exit points\n- `default` - Full-featured workflow node with dynamic port support\n\n## Dynamic Port Support\n\nThe `default` and `gateway` node types support dynamic ports:\n\n- **default**: Supports `config.dynamicInputs` and `config.dynamicOutputs`\n for user-defined input/output handles\n- **gateway**: Supports `config.branches` for conditional branching paths\n\n## UI Extensions\n\nAll node types support `extensions.ui.hideUnconnectedHandles` to control\nvisibility of unconnected ports.\n"
|
|
729
|
+
"description": "Visual rendering type for the node.\n\nBuilt-in types:\n- `note` - Sticky note with markdown support\n- `simple` - Compact layout with header and description\n- `square` - Minimal square node with centered icon\n- `atom` - Minimal label-only pill/rectangle for value/transform nodes (uses extensions.ui.atom)\n- `tool` - Specialized node for agent tools\n- `gateway` - Branching control flow with dynamic branches (uses config.branches)\n- `terminal` - Circular node for workflow start/end/exit points\n- `idea` - Conceptual idea node for BPMN-like flow diagrams\n- `default` - Full-featured workflow node with dynamic port support\n\n## Dynamic Port Support\n\nThe `default` and `gateway` node types support dynamic ports:\n\n- **default**: Supports `config.dynamicInputs` and `config.dynamicOutputs`\n for user-defined input/output handles\n- **gateway**: Supports `config.branches` for conditional branching paths\n\n## UI Extensions\n\nAll node types support `extensions.ui.hideUnconnectedHandles` to control\nvisibility of unconnected ports.\n"
|
|
650
730
|
},
|
|
651
731
|
"NodeUIExtensions": {
|
|
652
732
|
"type": "object",
|
|
@@ -656,6 +736,9 @@
|
|
|
656
736
|
"type": "boolean",
|
|
657
737
|
"description": "Show/hide unconnected handles (ports) to reduce visual noise.\nWhen true, only ports with active connections are displayed.\nUseful for nodes with many optional ports.\n"
|
|
658
738
|
},
|
|
739
|
+
"atom": {
|
|
740
|
+
"$ref": "#/$defs/AtomUIConfig"
|
|
741
|
+
},
|
|
659
742
|
"style": {
|
|
660
743
|
"type": "object",
|
|
661
744
|
"additionalProperties": true,
|
|
@@ -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
|
|
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,
|
|
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
|
|
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,
|
|
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 (
|
|
185
|
-
params.append('since',
|
|
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 (
|
|
188
|
-
params.append('
|
|
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,
|
|
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];
|
|
@@ -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.
|
|
@@ -194,8 +199,14 @@ export declare const playgroundActions: {
|
|
|
194
199
|
* Apply a server response to the store. All message and status updates from
|
|
195
200
|
* the server flow through here — polling callback, manual fetches, interrupt
|
|
196
201
|
* resolution. Nothing updates messages or session status except this function.
|
|
202
|
+
*
|
|
203
|
+
* Pass `sessionId` (the session the response was fetched for) so a response
|
|
204
|
+
* that resolves after the user switched sessions is dropped instead of writing
|
|
205
|
+
* the old session's status/messages onto the new current session. Pass `null`
|
|
206
|
+
* to deliberately opt out of the guard (non-session-scoped callers only) — the
|
|
207
|
+
* argument is required so every new caller has to make that choice explicitly.
|
|
197
208
|
*/
|
|
198
|
-
export declare function applyServerResponse(response: PlaygroundMessagesApiResponse): void;
|
|
209
|
+
export declare function applyServerResponse(response: PlaygroundMessagesApiResponse, sessionId: string | null): void;
|
|
199
210
|
/**
|
|
200
211
|
* Get the current session ID
|
|
201
212
|
*
|
|
@@ -221,6 +232,22 @@ export declare function getMessagesSnapshot(): PlaygroundMessage[];
|
|
|
221
232
|
* @returns Sequence number of the last message, or null
|
|
222
233
|
*/
|
|
223
234
|
export declare function getLatestSequenceNumber(): number | null;
|
|
235
|
+
/**
|
|
236
|
+
* Get the sequence number of the oldest loaded message, used as the cursor
|
|
237
|
+
* for backward "load older" pagination.
|
|
238
|
+
*
|
|
239
|
+
* @returns Sequence number of the first message, or null
|
|
240
|
+
*/
|
|
241
|
+
export declare function getOldestSequenceNumber(): number | null;
|
|
242
|
+
/**
|
|
243
|
+
* Whether older messages exist before the oldest one currently loaded.
|
|
244
|
+
*/
|
|
245
|
+
export declare function getHasOlder(): boolean;
|
|
246
|
+
/**
|
|
247
|
+
* Set whether older messages remain to be loaded, derived from a
|
|
248
|
+
* backward-pagination response.
|
|
249
|
+
*/
|
|
250
|
+
export declare function setHasOlder(hasOlder: boolean): void;
|
|
224
251
|
/**
|
|
225
252
|
* Subscribe to session status changes using $effect.root.
|
|
226
253
|
* 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
|
-
/**
|
|
49
|
-
|
|
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
|
-
*
|
|
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
|
|
290
|
-
const
|
|
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
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
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 (
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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
|
-
//
|
|
352
|
-
//
|
|
353
|
-
//
|
|
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
|
|
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
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
|
@@ -529,12 +579,20 @@ export const playgroundActions = {
|
|
|
529
579
|
* Apply a server response to the store. All message and status updates from
|
|
530
580
|
* the server flow through here — polling callback, manual fetches, interrupt
|
|
531
581
|
* resolution. Nothing updates messages or session status except this function.
|
|
532
|
-
|
|
533
|
-
|
|
582
|
+
*
|
|
583
|
+
* Pass `sessionId` (the session the response was fetched for) so a response
|
|
584
|
+
* that resolves after the user switched sessions is dropped instead of writing
|
|
585
|
+
* the old session's status/messages onto the new current session. Pass `null`
|
|
586
|
+
* to deliberately opt out of the guard (non-session-scoped callers only) — the
|
|
587
|
+
* argument is required so every new caller has to make that choice explicitly.
|
|
588
|
+
*/
|
|
589
|
+
export function applyServerResponse(response, sessionId) {
|
|
590
|
+
if (sessionId !== null && _currentSession?.id !== sessionId)
|
|
591
|
+
return;
|
|
534
592
|
if (response.data && response.data.length > 0) {
|
|
535
593
|
playgroundActions.addMessages(response.data);
|
|
536
|
-
// Refresh pipeline when following latest or pinned to the latest
|
|
537
|
-
// Skip
|
|
594
|
+
// Refresh the pipeline panel when following latest or pinned to the latest
|
|
595
|
+
// run. Skip when pinned to an older run — a historical view that won't change.
|
|
538
596
|
if (_pinnedExecutionId === null || _pinnedExecutionId === _latestExecutionId) {
|
|
539
597
|
_pipelineRefreshTrigger++;
|
|
540
598
|
}
|
|
@@ -584,6 +642,33 @@ export function getLatestSequenceNumber() {
|
|
|
584
642
|
}
|
|
585
643
|
return null;
|
|
586
644
|
}
|
|
645
|
+
/**
|
|
646
|
+
* Get the sequence number of the oldest loaded message, used as the cursor
|
|
647
|
+
* for backward "load older" pagination.
|
|
648
|
+
*
|
|
649
|
+
* @returns Sequence number of the first message, or null
|
|
650
|
+
*/
|
|
651
|
+
export function getOldestSequenceNumber() {
|
|
652
|
+
for (let i = 0; i < _messages.length; i++) {
|
|
653
|
+
if (_messages[i].sequenceNumber !== undefined) {
|
|
654
|
+
return _messages[i].sequenceNumber;
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
return null;
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Whether older messages exist before the oldest one currently loaded.
|
|
661
|
+
*/
|
|
662
|
+
export function getHasOlder() {
|
|
663
|
+
return _hasOlder;
|
|
664
|
+
}
|
|
665
|
+
/**
|
|
666
|
+
* Set whether older messages remain to be loaded, derived from a
|
|
667
|
+
* backward-pagination response.
|
|
668
|
+
*/
|
|
669
|
+
export function setHasOlder(hasOlder) {
|
|
670
|
+
_hasOlder = hasOlder;
|
|
671
|
+
}
|
|
587
672
|
/**
|
|
588
673
|
* Subscribe to session status changes using $effect.root.
|
|
589
674
|
* This is designed for use in non-component contexts (e.g., mount.ts).
|
|
@@ -619,7 +704,7 @@ export async function refreshSessionMessages(fetchMessages) {
|
|
|
619
704
|
return;
|
|
620
705
|
try {
|
|
621
706
|
const response = await fetchMessages(session.id);
|
|
622
|
-
applyServerResponse(response);
|
|
707
|
+
applyServerResponse(response, session.id);
|
|
623
708
|
}
|
|
624
709
|
catch (err) {
|
|
625
710
|
logger.error('[playgroundStore] Failed to refresh messages:', err);
|