@flowdrop/flowdrop 1.15.0 → 2.0.0-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +475 -0
- package/MIGRATION-2.0.md +472 -0
- package/README.md +23 -23
- package/dist/adapters/WorkflowAdapter.d.ts +1 -1
- package/dist/adapters/WorkflowAdapter.js +14 -8
- package/dist/adapters/agentspec/AgentSpecAdapter.js +7 -7
- package/dist/chat/batchFeedback.d.ts +39 -0
- package/dist/chat/batchFeedback.js +51 -0
- package/dist/commands/executor.js +15 -1
- package/dist/commands/storeIntegration.svelte.d.ts +4 -1
- package/dist/commands/storeIntegration.svelte.js +26 -21
- package/dist/commands/types.d.ts +2 -0
- package/dist/components/App.svelte +162 -192
- package/dist/components/App.svelte.d.ts +47 -8
- package/dist/components/ConfigForm.svelte +71 -47
- package/dist/components/ConfigModal.svelte +7 -2
- package/dist/components/ConnectionLine.svelte +4 -2
- package/dist/components/Navbar.svelte +61 -1
- package/dist/components/NodeSidebar.svelte +27 -45
- package/dist/components/NodeStatusOverlay.svelte +94 -6
- package/dist/components/NodeSwapPicker.svelte +10 -8
- package/dist/components/PipelineStatus.svelte +16 -67
- package/dist/components/PortCoordinateTracker.svelte +5 -6
- package/dist/components/SchemaForm.stories.svelte +1 -3
- package/dist/components/SchemaForm.svelte +18 -25
- package/dist/components/SchemaForm.svelte.d.ts +0 -8
- package/dist/components/SettingsModal.svelte +8 -3
- package/dist/components/SettingsPanel.svelte +20 -4
- package/dist/components/SwapMappingEditor.svelte +67 -49
- package/dist/components/SwapMappingEditor.svelte.d.ts +0 -2
- package/dist/components/UniversalNode.svelte +9 -7
- package/dist/components/WorkflowEditor.svelte +118 -111
- package/dist/components/WorkflowEditor.svelte.d.ts +18 -10
- package/dist/components/chat/AIChatPanel.svelte +93 -89
- package/dist/components/chat/AIChatPanel.svelte.d.ts +0 -4
- package/dist/components/chat/CommandPreview.svelte +2 -1
- package/dist/components/console/CommandConsole.svelte +7 -5
- package/dist/components/console/ConsoleAutocomplete.svelte +10 -11
- package/dist/components/console/ConsoleAutocomplete.svelte.d.ts +6 -0
- package/dist/components/console/ConsoleInput.svelte +15 -6
- package/dist/components/console/ConsoleOutput.svelte +2 -1
- package/dist/components/form/FormArray.svelte +5 -9
- package/dist/components/form/FormArray.svelte.d.ts +2 -1
- package/dist/components/form/FormAutocomplete.svelte +8 -6
- package/dist/components/form/FormField.svelte +4 -2
- package/dist/components/form/FormFieldLight.svelte +4 -2
- package/dist/components/form/FormMarkdownEditor.svelte +9 -4
- package/dist/components/form/FormRangeField.svelte +1 -0
- package/dist/components/form/FormTemplateEditor.svelte +11 -3
- package/dist/components/form/FormToggle.svelte +5 -12
- package/dist/components/form/FormToggle.svelte.d.ts +4 -2
- package/dist/components/form/templateAutocomplete.js +1 -5
- package/dist/components/form/types.d.ts +1 -14
- package/dist/components/interrupt/FormPrompt.svelte +3 -2
- package/dist/components/interrupt/InterruptBubble.svelte +16 -17
- package/dist/components/interrupt/ReviewPrompt.svelte +10 -3
- package/dist/components/interrupt/TextInputPrompt.svelte +2 -1
- package/dist/components/layouts/MainLayout.svelte +20 -13
- package/dist/components/layouts/MainLayout.svelte.d.ts +4 -0
- package/dist/components/nodes/AtomNode.svelte +17 -5
- package/dist/components/nodes/GatewayNode.svelte +19 -10
- package/dist/components/nodes/IdeaNode.svelte +7 -0
- package/dist/components/nodes/SimpleNode.svelte +11 -6
- package/dist/components/nodes/SquareNode.svelte +15 -8
- package/dist/components/nodes/TerminalNode.svelte +9 -4
- package/dist/components/nodes/ToolNode.svelte +7 -1
- package/dist/components/nodes/WorkflowNode.svelte +16 -7
- package/dist/components/playground/ChatInput.svelte +11 -14
- package/dist/components/playground/ChatPanel.svelte +6 -49
- package/dist/components/playground/ChatPanel.svelte.d.ts +0 -14
- package/dist/components/playground/ControlPanel.svelte +134 -123
- package/dist/components/playground/ControlPanel.svelte.d.ts +3 -0
- package/dist/components/playground/ExecutionLogs.svelte +11 -9
- package/dist/components/playground/InputCollector.svelte +11 -9
- package/dist/components/playground/MessageStream.svelte +17 -23
- package/dist/components/playground/PipelineKanbanView.svelte +65 -6
- package/dist/components/playground/PipelinePanel.svelte +11 -5
- package/dist/components/playground/PipelineTableView.svelte +186 -44
- package/dist/components/playground/Playground.svelte +90 -92
- package/dist/components/playground/Playground.svelte.d.ts +2 -0
- package/dist/components/playground/PlaygroundApp.svelte +6 -1
- package/dist/components/playground/PlaygroundApp.svelte.d.ts +3 -0
- package/dist/components/playground/PlaygroundModal.svelte +13 -3
- package/dist/components/playground/PlaygroundModal.svelte.d.ts +3 -0
- package/dist/components/playground/PlaygroundStudio.svelte +34 -32
- package/dist/components/playground/PlaygroundStudio.svelte.d.ts +3 -0
- package/dist/components/playground/SessionManager.svelte +9 -12
- package/dist/components/playground/pipelineViewUtils.svelte.d.ts +28 -0
- package/dist/components/playground/pipelineViewUtils.svelte.js +38 -1
- package/dist/config/endpoints.d.ts +0 -7
- package/dist/config/endpoints.js +2 -10
- package/dist/core/index.d.ts +4 -4
- package/dist/core/index.js +6 -6
- package/dist/display/index.d.ts +0 -2
- package/dist/display/index.js +0 -6
- package/dist/editor/index.d.ts +19 -20
- package/dist/editor/index.js +25 -35
- package/dist/form/code.d.ts +25 -15
- package/dist/form/code.js +44 -41
- package/dist/form/fieldRegistry.d.ts +17 -13
- package/dist/form/fieldRegistry.js +32 -12
- package/dist/form/full.d.ts +17 -13
- package/dist/form/full.js +22 -27
- package/dist/form/index.d.ts +3 -3
- package/dist/form/index.js +3 -3
- package/dist/form/markdown.d.ts +13 -8
- package/dist/form/markdown.js +22 -23
- package/dist/helpers/proximityConnect.d.ts +3 -2
- package/dist/helpers/proximityConnect.js +2 -5
- package/dist/helpers/workflowEditorHelper.d.ts +12 -5
- package/dist/helpers/workflowEditorHelper.js +27 -25
- package/dist/index.d.ts +28 -24
- package/dist/index.js +27 -50
- package/dist/messages/defaults.d.ts +2 -5
- package/dist/messages/defaults.js +3 -6
- package/dist/messages/index.d.ts +0 -1
- package/dist/messages/index.js +0 -1
- package/dist/mocks/app-forms.d.ts +6 -2
- package/dist/mocks/app-forms.js +11 -4
- package/dist/openapi/v1/openapi.yaml +3 -3
- package/dist/playground/index.d.ts +2 -3
- package/dist/playground/index.js +2 -30
- package/dist/playground/mount.d.ts +15 -0
- package/dist/playground/mount.js +46 -20
- package/dist/registry/{BaseRegistry.d.ts → BaseRegistry.svelte.d.ts} +22 -1
- package/dist/registry/{BaseRegistry.js → BaseRegistry.svelte.js} +37 -1
- package/dist/registry/builtinFormats.d.ts +9 -18
- package/dist/registry/builtinFormats.js +9 -39
- package/dist/registry/builtinNodes.d.ts +0 -25
- package/dist/registry/builtinNodes.js +1 -50
- package/dist/registry/index.d.ts +3 -4
- package/dist/registry/index.js +4 -6
- package/dist/registry/nodeComponentRegistry.d.ts +182 -15
- package/dist/registry/nodeComponentRegistry.js +235 -17
- package/dist/registry/workflowFormatRegistry.d.ts +14 -9
- package/dist/registry/workflowFormatRegistry.js +24 -8
- package/dist/{schema → schemas}/index.d.ts +2 -2
- package/dist/{schema → schemas}/index.js +2 -2
- package/dist/schemas/v1/workflow.schema.json +3 -3
- package/dist/services/agentSpecExecutionService.js +0 -1
- package/dist/services/apiVariableService.d.ts +2 -1
- package/dist/services/apiVariableService.js +5 -22
- package/dist/services/autoSaveService.d.ts +7 -0
- package/dist/services/autoSaveService.js +6 -4
- package/dist/services/chatService.d.ts +8 -4
- package/dist/services/chatService.js +15 -15
- package/dist/services/draftStorage.d.ts +129 -13
- package/dist/services/draftStorage.js +185 -37
- package/dist/services/dynamicSchemaService.d.ts +2 -1
- package/dist/services/dynamicSchemaService.js +5 -22
- package/dist/services/globalSave.d.ts +13 -12
- package/dist/services/globalSave.js +29 -51
- package/dist/services/historyService.d.ts +9 -3
- package/dist/services/historyService.js +9 -3
- package/dist/services/interruptService.d.ts +14 -9
- package/dist/services/interruptService.js +27 -27
- package/dist/services/nodeExecutionService.d.ts +18 -3
- package/dist/services/nodeExecutionService.js +71 -45
- package/dist/services/playgroundService.d.ts +14 -9
- package/dist/services/playgroundService.js +31 -30
- package/dist/services/variableService.d.ts +2 -1
- package/dist/services/variableService.js +2 -2
- package/dist/services/workflowStorage.js +6 -6
- package/dist/stores/apiContext.d.ts +45 -0
- package/dist/stores/apiContext.js +65 -0
- package/dist/stores/categoriesStore.svelte.d.ts +28 -23
- package/dist/stores/categoriesStore.svelte.js +70 -64
- package/dist/stores/getInstance.svelte.d.ts +39 -0
- package/dist/stores/getInstance.svelte.js +65 -0
- package/dist/stores/historyStore.svelte.d.ts +77 -93
- package/dist/stores/historyStore.svelte.js +134 -160
- package/dist/stores/instanceContainer.svelte.d.ts +111 -0
- package/dist/stores/instanceContainer.svelte.js +114 -0
- package/dist/stores/interruptStore.svelte.d.ts +112 -82
- package/dist/stores/interruptStore.svelte.js +253 -226
- package/dist/stores/pipelinePanelStore.svelte.d.ts +27 -3
- package/dist/stores/pipelinePanelStore.svelte.js +61 -14
- package/dist/stores/playgroundStore.svelte.d.ts +169 -222
- package/dist/stores/playgroundStore.svelte.js +515 -580
- package/dist/stores/portCoordinateStore.svelte.d.ts +57 -51
- package/dist/stores/portCoordinateStore.svelte.js +109 -98
- package/dist/stores/settingsStore.svelte.d.ts +4 -1
- package/dist/stores/settingsStore.svelte.js +47 -12
- package/dist/stores/workflowStore.svelte.d.ts +178 -213
- package/dist/stores/workflowStore.svelte.js +449 -501
- package/dist/stories/EdgeDecorator.svelte +5 -2
- package/dist/stories/NodeDecorator.svelte +5 -3
- package/dist/svelte-app.d.ts +60 -10
- package/dist/svelte-app.js +157 -53
- package/dist/types/events.d.ts +6 -3
- package/dist/types/index.d.ts +33 -3
- package/dist/types/navbar.d.ts +7 -0
- package/dist/types/playground.d.ts +18 -3
- package/dist/types/settings.d.ts +13 -0
- package/dist/types/settings.js +1 -0
- package/dist/utils/colors.d.ts +47 -21
- package/dist/utils/colors.js +69 -68
- package/dist/utils/connections.d.ts +9 -15
- package/dist/utils/connections.js +13 -32
- package/dist/utils/duration.d.ts +13 -0
- package/dist/utils/duration.js +45 -0
- package/dist/utils/icons.d.ts +5 -2
- package/dist/utils/icons.js +6 -5
- package/dist/utils/nodeSwap.d.ts +6 -2
- package/dist/utils/nodeSwap.js +62 -126
- package/dist/utils/nodeTypes.d.ts +17 -8
- package/dist/utils/nodeTypes.js +26 -19
- package/dist/utils/performanceUtils.js +7 -0
- package/package.json +6 -5
- package/dist/messages/deprecation.d.ts +0 -20
- package/dist/messages/deprecation.js +0 -33
- package/dist/registry/plugin.d.ts +0 -215
- package/dist/registry/plugin.js +0 -249
- package/dist/services/api.d.ts +0 -129
- package/dist/services/api.js +0 -217
|
@@ -4,273 +4,16 @@
|
|
|
4
4
|
* Svelte 5 rune-based state for managing playground state including sessions,
|
|
5
5
|
* messages, and execution status.
|
|
6
6
|
*
|
|
7
|
+
* The reactive state lives in the {@link PlaygroundStore} class — one per
|
|
8
|
+
* FlowDrop instance, created by `createFlowDropInstance()` and resolved in
|
|
9
|
+
* components via `getInstance().playground`.
|
|
10
|
+
*
|
|
7
11
|
* @module stores/playgroundStore
|
|
8
12
|
*/
|
|
9
13
|
import { isChatInputNode } from '../types/playground.js';
|
|
10
14
|
import { logger } from '../utils/logger.js';
|
|
11
15
|
// =========================================================================
|
|
12
|
-
//
|
|
13
|
-
// =========================================================================
|
|
14
|
-
/**
|
|
15
|
-
* Currently active playground session
|
|
16
|
-
*/
|
|
17
|
-
let _currentSession = $state(null);
|
|
18
|
-
/**
|
|
19
|
-
* List of all sessions for the current workflow
|
|
20
|
-
*/
|
|
21
|
-
let _sessions = $state([]);
|
|
22
|
-
/**
|
|
23
|
-
* Messages in the current session
|
|
24
|
-
*/
|
|
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);
|
|
32
|
-
/**
|
|
33
|
-
* Whether we are currently loading data
|
|
34
|
-
*/
|
|
35
|
-
let _isLoading = $state(false);
|
|
36
|
-
/**
|
|
37
|
-
* Current error message, if any
|
|
38
|
-
*/
|
|
39
|
-
let _error = $state(null);
|
|
40
|
-
/**
|
|
41
|
-
* Current workflow being tested
|
|
42
|
-
*/
|
|
43
|
-
let _currentWorkflow = $state(null);
|
|
44
|
-
/**
|
|
45
|
-
* Last polling timestamp for incremental message fetching
|
|
46
|
-
*/
|
|
47
|
-
let _lastPollSequenceNumber = $state(null);
|
|
48
|
-
/** Execution ID explicitly pinned by the user (null = follow latest) */
|
|
49
|
-
let _pinnedExecutionId = $state(null);
|
|
50
|
-
/** Incremented on every message batch that should trigger a pipeline re-fetch */
|
|
51
|
-
let _pipelineRefreshTrigger = $state(0);
|
|
52
|
-
/** Whether log messages are visible in the execution console */
|
|
53
|
-
let _showLogs = $state(true);
|
|
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);
|
|
67
|
-
/** Active execution: pinned if set, otherwise latest */
|
|
68
|
-
const _activeExecutionId = $derived(_pinnedExecutionId ?? _latestExecutionId);
|
|
69
|
-
// Derived from server status — never manually set.
|
|
70
|
-
// Exception: updateSessionStatus('running') in handleSendMessage is an
|
|
71
|
-
// acknowledged optimistic write, overwritten by the next server response.
|
|
72
|
-
const _isExecuting = $derived(_currentSession?.status === 'running');
|
|
73
|
-
// =========================================================================
|
|
74
|
-
// Getter Functions (for reactive access in components)
|
|
75
|
-
// =========================================================================
|
|
76
|
-
/**
|
|
77
|
-
* Get the current session
|
|
78
|
-
*/
|
|
79
|
-
export function getCurrentSession() {
|
|
80
|
-
return _currentSession;
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Get all sessions
|
|
84
|
-
*/
|
|
85
|
-
export function getSessions() {
|
|
86
|
-
return _sessions;
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* Get all messages
|
|
90
|
-
*/
|
|
91
|
-
export function getMessages() {
|
|
92
|
-
return _messages;
|
|
93
|
-
}
|
|
94
|
-
/**
|
|
95
|
-
* Get executing state
|
|
96
|
-
*/
|
|
97
|
-
export function getIsExecuting() {
|
|
98
|
-
return _isExecuting;
|
|
99
|
-
}
|
|
100
|
-
/**
|
|
101
|
-
* Get loading state
|
|
102
|
-
*/
|
|
103
|
-
export function getIsLoading() {
|
|
104
|
-
return _isLoading;
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* Get error state
|
|
108
|
-
*/
|
|
109
|
-
export function getError() {
|
|
110
|
-
return _error;
|
|
111
|
-
}
|
|
112
|
-
/**
|
|
113
|
-
* Get the current workflow
|
|
114
|
-
*/
|
|
115
|
-
export function getCurrentWorkflow() {
|
|
116
|
-
return _currentWorkflow;
|
|
117
|
-
}
|
|
118
|
-
/**
|
|
119
|
-
* Get the last poll sequence number cursor
|
|
120
|
-
*/
|
|
121
|
-
export function getLastPollSequenceNumber() {
|
|
122
|
-
return _lastPollSequenceNumber;
|
|
123
|
-
}
|
|
124
|
-
// =========================================================================
|
|
125
|
-
// Derived Getters
|
|
126
|
-
// =========================================================================
|
|
127
|
-
/**
|
|
128
|
-
* Get current session status
|
|
129
|
-
*/
|
|
130
|
-
export function getSessionStatus() {
|
|
131
|
-
return _currentSession?.status ?? 'idle';
|
|
132
|
-
}
|
|
133
|
-
/**
|
|
134
|
-
* Whether the user can currently send a message.
|
|
135
|
-
* False when executing, when awaiting input, or when no session exists.
|
|
136
|
-
*/
|
|
137
|
-
export function getCanSendMessage() {
|
|
138
|
-
const status = _currentSession?.status ?? 'idle';
|
|
139
|
-
return _currentSession !== null && !_isExecuting && status !== 'awaiting_input';
|
|
140
|
-
}
|
|
141
|
-
/**
|
|
142
|
-
* Get message count
|
|
143
|
-
*/
|
|
144
|
-
export function getMessageCount() {
|
|
145
|
-
return _messages.length;
|
|
146
|
-
}
|
|
147
|
-
/**
|
|
148
|
-
* Get chat messages (excludes log messages)
|
|
149
|
-
*/
|
|
150
|
-
export function getChatMessages() {
|
|
151
|
-
return _messages.filter((m) => m.role !== 'log');
|
|
152
|
-
}
|
|
153
|
-
/**
|
|
154
|
-
* Get log messages only
|
|
155
|
-
*/
|
|
156
|
-
export function getLogMessages() {
|
|
157
|
-
return _messages.filter((m) => m.role === 'log');
|
|
158
|
-
}
|
|
159
|
-
/**
|
|
160
|
-
* Get the latest message
|
|
161
|
-
*/
|
|
162
|
-
export function getLatestMessage() {
|
|
163
|
-
return _messages.length > 0 ? _messages[_messages.length - 1] : null;
|
|
164
|
-
}
|
|
165
|
-
/**
|
|
166
|
-
* Get input fields from workflow input nodes
|
|
167
|
-
*
|
|
168
|
-
* Analyzes the workflow to extract input nodes and their configuration
|
|
169
|
-
* schemas for auto-generating input forms.
|
|
170
|
-
*/
|
|
171
|
-
export function getInputFields() {
|
|
172
|
-
const workflow = _currentWorkflow;
|
|
173
|
-
if (!workflow) {
|
|
174
|
-
return [];
|
|
175
|
-
}
|
|
176
|
-
const fields = [];
|
|
177
|
-
// Find input nodes in the workflow
|
|
178
|
-
workflow.nodes.forEach((node) => {
|
|
179
|
-
const category = node.data.metadata?.category;
|
|
180
|
-
const nodeTypeId = node.data.metadata?.id ?? node.type;
|
|
181
|
-
// Check if this is an input-type node
|
|
182
|
-
// The category can be "inputs" (standard) or variations like "input"
|
|
183
|
-
const categoryStr = String(category || '');
|
|
184
|
-
const isInputCategory = categoryStr === 'inputs' || categoryStr === 'input';
|
|
185
|
-
if (isInputCategory || isChatInputNode(nodeTypeId)) {
|
|
186
|
-
// Get output ports that provide data
|
|
187
|
-
const outputs = node.data.metadata?.outputs ?? [];
|
|
188
|
-
outputs.forEach((output) => {
|
|
189
|
-
if (output.type === 'output') {
|
|
190
|
-
// Create a field for each output
|
|
191
|
-
const field = {
|
|
192
|
-
nodeId: node.id,
|
|
193
|
-
fieldId: output.id,
|
|
194
|
-
label: node.data.label || output.name || nodeTypeId,
|
|
195
|
-
type: output.dataType || 'string',
|
|
196
|
-
defaultValue: node.data.config?.[output.id],
|
|
197
|
-
required: output.required ?? false
|
|
198
|
-
};
|
|
199
|
-
// Check for schema in configSchema
|
|
200
|
-
const configSchema = node.data.metadata?.configSchema;
|
|
201
|
-
if (configSchema?.properties?.[output.id]) {
|
|
202
|
-
field.schema = configSchema.properties[output.id];
|
|
203
|
-
}
|
|
204
|
-
fields.push(field);
|
|
205
|
-
}
|
|
206
|
-
});
|
|
207
|
-
// If no outputs defined, create a default field based on node config
|
|
208
|
-
if (outputs.length === 0) {
|
|
209
|
-
const configSchema = node.data.metadata?.configSchema;
|
|
210
|
-
if (configSchema?.properties) {
|
|
211
|
-
Object.entries(configSchema.properties).forEach(([key, schema]) => {
|
|
212
|
-
const field = {
|
|
213
|
-
nodeId: node.id,
|
|
214
|
-
fieldId: key,
|
|
215
|
-
label: schema.title || key,
|
|
216
|
-
type: schema.type || 'string',
|
|
217
|
-
defaultValue: node.data.config?.[key] ?? schema.default,
|
|
218
|
-
required: configSchema.required?.includes(key) ?? false,
|
|
219
|
-
schema
|
|
220
|
-
};
|
|
221
|
-
fields.push(field);
|
|
222
|
-
});
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
}
|
|
226
|
-
});
|
|
227
|
-
return fields;
|
|
228
|
-
}
|
|
229
|
-
/**
|
|
230
|
-
* Check if workflow has a chat input
|
|
231
|
-
*/
|
|
232
|
-
export function getHasChatInput() {
|
|
233
|
-
const fields = getInputFields();
|
|
234
|
-
return fields.some((field) => isChatInputNode(field.nodeId) || field.type === 'string');
|
|
235
|
-
}
|
|
236
|
-
/**
|
|
237
|
-
* Get session count
|
|
238
|
-
*/
|
|
239
|
-
export function getSessionCount() {
|
|
240
|
-
return _sessions.length;
|
|
241
|
-
}
|
|
242
|
-
export function getPinnedExecutionId() {
|
|
243
|
-
return _pinnedExecutionId;
|
|
244
|
-
}
|
|
245
|
-
export function getLatestExecutionId() {
|
|
246
|
-
return _latestExecutionId;
|
|
247
|
-
}
|
|
248
|
-
export function getActiveExecutionId() {
|
|
249
|
-
return _activeExecutionId;
|
|
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
|
-
}
|
|
258
|
-
/**
|
|
259
|
-
* Counter that increments whenever new messages arrive and the pipeline display
|
|
260
|
-
* should re-fetch — i.e. when following latest or pinned to the latest execution.
|
|
261
|
-
* Pass to PipelinePanel's refreshTrigger prop.
|
|
262
|
-
*/
|
|
263
|
-
export function getPipelineRefreshTrigger() {
|
|
264
|
-
return _pipelineRefreshTrigger;
|
|
265
|
-
}
|
|
266
|
-
/**
|
|
267
|
-
* Whether log messages should be shown in the execution console
|
|
268
|
-
*/
|
|
269
|
-
export function getShowLogs() {
|
|
270
|
-
return _showLogs;
|
|
271
|
-
}
|
|
272
|
-
// =========================================================================
|
|
273
|
-
// Helper Functions
|
|
16
|
+
// Helper Functions (pure, instance-independent)
|
|
274
17
|
// =========================================================================
|
|
275
18
|
/**
|
|
276
19
|
* Sort messages chronologically by sequenceNumber
|
|
@@ -312,86 +55,352 @@ function sortMessagesChronologically(messageList) {
|
|
|
312
55
|
function isSubflowMessage(msg) {
|
|
313
56
|
return msg.parentPipelineId != null;
|
|
314
57
|
}
|
|
315
|
-
/**
|
|
316
|
-
* Syncs the current session's executions list from incoming messages.
|
|
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.
|
|
328
|
-
*/
|
|
329
|
-
function syncExecutionsFromMessages(messages) {
|
|
330
|
-
if (!_currentSession)
|
|
331
|
-
return;
|
|
332
|
-
const executions = [...(_currentSession.executions ?? [])];
|
|
333
|
-
const seenIds = new Set(executions.map((e) => e.id));
|
|
334
|
-
let added = false;
|
|
335
|
-
let gainedMainRun = false;
|
|
336
|
-
for (const msg of messages) {
|
|
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;
|
|
350
|
-
}
|
|
351
|
-
if (!added)
|
|
352
|
-
return;
|
|
353
|
-
_currentSession = { ..._currentSession, executions };
|
|
354
|
-
// Auto-follow the new main run by dropping any manual pin.
|
|
355
|
-
if (gainedMainRun)
|
|
356
|
-
_pinnedExecutionId = null;
|
|
357
|
-
}
|
|
358
58
|
// =========================================================================
|
|
359
|
-
//
|
|
59
|
+
// PlaygroundStore (per-instance reactive state)
|
|
360
60
|
// =========================================================================
|
|
361
61
|
/**
|
|
362
|
-
*
|
|
62
|
+
* Per-instance playground state: sessions, messages, executions, and the
|
|
63
|
+
* polling/refresh machinery around them.
|
|
363
64
|
*/
|
|
364
|
-
export
|
|
65
|
+
export class PlaygroundStore {
|
|
66
|
+
/** Currently active playground session */
|
|
67
|
+
#currentSession = $state(null);
|
|
68
|
+
/** List of all sessions for the current workflow */
|
|
69
|
+
#sessions = $state([]);
|
|
70
|
+
/** Messages in the current session */
|
|
71
|
+
#messages = $state([]);
|
|
365
72
|
/**
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
*
|
|
73
|
+
* Whether older messages exist before the oldest one currently loaded.
|
|
74
|
+
* Drives the scroll-up "load older" affordance. Reset whenever the message
|
|
75
|
+
* set is replaced (session switch / clear).
|
|
76
|
+
*/
|
|
77
|
+
#hasOlder = $state(false);
|
|
78
|
+
/** Whether we are currently loading data */
|
|
79
|
+
#isLoading = $state(false);
|
|
80
|
+
/** Current error message, if any */
|
|
81
|
+
#error = $state(null);
|
|
82
|
+
/** Current workflow being tested */
|
|
83
|
+
#currentWorkflow = $state(null);
|
|
84
|
+
/** Last polling cursor for incremental message fetching */
|
|
85
|
+
#lastPollSequenceNumber = $state(null);
|
|
86
|
+
/** Execution ID explicitly pinned by the user (null = follow latest) */
|
|
87
|
+
#pinnedExecutionId = $state(null);
|
|
88
|
+
/** Incremented on every message batch that should trigger a pipeline re-fetch */
|
|
89
|
+
#pipelineRefreshTrigger = $state(0);
|
|
90
|
+
/** Whether log messages are visible in the execution console */
|
|
91
|
+
#showLogs = $state(true);
|
|
92
|
+
/**
|
|
93
|
+
* The main pipeline runs — the single source of truth for "what's selectable".
|
|
94
|
+
* Sub-flow runs are tracked for classification but excluded here: selecting one
|
|
95
|
+
* can't show its own graph (the panel renders the main pipeline regardless), so
|
|
96
|
+
* listing them would be dead UI. Feeds both the run-switcher and "latest".
|
|
97
|
+
*/
|
|
98
|
+
#selectableExecutions = $derived((this.#currentSession?.executions ?? []).filter((e) => !e.isSubflow));
|
|
99
|
+
/**
|
|
100
|
+
* Latest execution ID: the most recent main run, so the sidebar keeps the main
|
|
101
|
+
* pipeline in focus and never auto-follows a sub-flow. Null when no main run is
|
|
102
|
+
* known yet — better an empty panel for a poll than the wrong graph.
|
|
103
|
+
*/
|
|
104
|
+
#latestExecutionId = $derived(this.#selectableExecutions.at(-1)?.id ?? null);
|
|
105
|
+
/** Active execution: pinned if set, otherwise latest */
|
|
106
|
+
#activeExecutionId = $derived(this.#pinnedExecutionId ?? this.#latestExecutionId);
|
|
107
|
+
// Derived from server status — never manually set.
|
|
108
|
+
// Exception: updateSessionStatus('running') in handleSendMessage is an
|
|
109
|
+
// acknowledged optimistic write, overwritten by the next server response.
|
|
110
|
+
#isExecuting = $derived(this.#currentSession?.status === 'running');
|
|
111
|
+
/** Cleanups for active subscribeToSessionStatus effect roots. */
|
|
112
|
+
// eslint-disable-next-line svelte/prefer-svelte-reactivity -- non-reactive bookkeeping registry; nothing renders from it
|
|
113
|
+
#statusSubscriptions = new Set();
|
|
114
|
+
/** Bound mutation facade — see {@link PlaygroundStoreActions}. */
|
|
115
|
+
actions;
|
|
116
|
+
constructor() {
|
|
117
|
+
this.actions = Object.freeze({
|
|
118
|
+
setWorkflow: this.setWorkflow.bind(this),
|
|
119
|
+
setCurrentSession: this.setCurrentSession.bind(this),
|
|
120
|
+
updateSessionStatus: this.updateSessionStatus.bind(this),
|
|
121
|
+
setSessions: this.setSessions.bind(this),
|
|
122
|
+
addSession: this.addSession.bind(this),
|
|
123
|
+
removeSession: this.removeSession.bind(this),
|
|
124
|
+
setMessages: this.setMessages.bind(this),
|
|
125
|
+
addMessage: this.addMessage.bind(this),
|
|
126
|
+
addMessages: this.addMessages.bind(this),
|
|
127
|
+
clearMessages: this.clearMessages.bind(this),
|
|
128
|
+
setLoading: this.setLoading.bind(this),
|
|
129
|
+
setError: this.setError.bind(this),
|
|
130
|
+
updateLastPollSequenceNumber: this.updateLastPollSequenceNumber.bind(this),
|
|
131
|
+
reset: this.reset.bind(this),
|
|
132
|
+
switchSession: this.switchSession.bind(this),
|
|
133
|
+
pinExecution: this.pinExecution.bind(this),
|
|
134
|
+
setShowLogs: this.setShowLogs.bind(this),
|
|
135
|
+
toggleShowLogs: this.toggleShowLogs.bind(this)
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
// -----------------------------------------------------------------------
|
|
139
|
+
// Reactive getters
|
|
140
|
+
// -----------------------------------------------------------------------
|
|
141
|
+
/** The current session. */
|
|
142
|
+
get currentSession() {
|
|
143
|
+
return this.#currentSession;
|
|
144
|
+
}
|
|
145
|
+
/** All sessions. */
|
|
146
|
+
get sessions() {
|
|
147
|
+
return this.#sessions;
|
|
148
|
+
}
|
|
149
|
+
/** All messages (chronological). */
|
|
150
|
+
get messages() {
|
|
151
|
+
return this.#messages;
|
|
152
|
+
}
|
|
153
|
+
/** Executing state (derived from server status). */
|
|
154
|
+
get isExecuting() {
|
|
155
|
+
return this.#isExecuting;
|
|
156
|
+
}
|
|
157
|
+
/** Loading state. */
|
|
158
|
+
get isLoading() {
|
|
159
|
+
return this.#isLoading;
|
|
160
|
+
}
|
|
161
|
+
/** Error state. */
|
|
162
|
+
get error() {
|
|
163
|
+
return this.#error;
|
|
164
|
+
}
|
|
165
|
+
/** The current workflow. */
|
|
166
|
+
get currentWorkflow() {
|
|
167
|
+
return this.#currentWorkflow;
|
|
168
|
+
}
|
|
169
|
+
/** The last poll sequence number cursor. */
|
|
170
|
+
get lastPollSequenceNumber() {
|
|
171
|
+
return this.#lastPollSequenceNumber;
|
|
172
|
+
}
|
|
173
|
+
/** Current session status. */
|
|
174
|
+
get sessionStatus() {
|
|
175
|
+
return this.#currentSession?.status ?? 'idle';
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Whether the user can currently send a message.
|
|
179
|
+
* False when executing, when awaiting input, or when no session exists.
|
|
369
180
|
*/
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
181
|
+
get canSendMessage() {
|
|
182
|
+
const status = this.#currentSession?.status ?? 'idle';
|
|
183
|
+
return this.#currentSession !== null && !this.#isExecuting && status !== 'awaiting_input';
|
|
184
|
+
}
|
|
185
|
+
/** Message count. */
|
|
186
|
+
get messageCount() {
|
|
187
|
+
return this.#messages.length;
|
|
188
|
+
}
|
|
189
|
+
/** Chat messages (excludes log messages). */
|
|
190
|
+
get chatMessages() {
|
|
191
|
+
return this.#messages.filter((m) => m.role !== 'log');
|
|
192
|
+
}
|
|
193
|
+
/** Log messages only. */
|
|
194
|
+
get logMessages() {
|
|
195
|
+
return this.#messages.filter((m) => m.role === 'log');
|
|
196
|
+
}
|
|
197
|
+
/** The latest message, or null. */
|
|
198
|
+
get latestMessage() {
|
|
199
|
+
return this.#messages.length > 0 ? this.#messages[this.#messages.length - 1] : null;
|
|
200
|
+
}
|
|
373
201
|
/**
|
|
374
|
-
*
|
|
202
|
+
* Input fields from workflow input nodes.
|
|
375
203
|
*
|
|
376
|
-
*
|
|
204
|
+
* Analyzes the workflow to extract input nodes and their configuration
|
|
205
|
+
* schemas for auto-generating input forms.
|
|
377
206
|
*/
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
207
|
+
get inputFields() {
|
|
208
|
+
const workflow = this.#currentWorkflow;
|
|
209
|
+
if (!workflow) {
|
|
210
|
+
return [];
|
|
211
|
+
}
|
|
212
|
+
const fields = [];
|
|
213
|
+
// Find input nodes in the workflow
|
|
214
|
+
workflow.nodes.forEach((node) => {
|
|
215
|
+
const category = node.data.metadata?.category;
|
|
216
|
+
const nodeTypeId = node.data.metadata?.id ?? node.type;
|
|
217
|
+
// Check if this is an input-type node
|
|
218
|
+
// The category can be "inputs" (standard) or variations like "input"
|
|
219
|
+
const categoryStr = String(category || '');
|
|
220
|
+
const isInputCategory = categoryStr === 'inputs' || categoryStr === 'input';
|
|
221
|
+
if (isInputCategory || isChatInputNode(nodeTypeId)) {
|
|
222
|
+
// Get output ports that provide data
|
|
223
|
+
const outputs = node.data.metadata?.outputs ?? [];
|
|
224
|
+
outputs.forEach((output) => {
|
|
225
|
+
if (output.type === 'output') {
|
|
226
|
+
// Create a field for each output
|
|
227
|
+
const field = {
|
|
228
|
+
nodeId: node.id,
|
|
229
|
+
fieldId: output.id,
|
|
230
|
+
label: node.data.label || output.name || nodeTypeId,
|
|
231
|
+
type: output.dataType || 'string',
|
|
232
|
+
defaultValue: node.data.config?.[output.id],
|
|
233
|
+
required: output.required ?? false
|
|
234
|
+
};
|
|
235
|
+
// Check for schema in configSchema
|
|
236
|
+
const configSchema = node.data.metadata?.configSchema;
|
|
237
|
+
if (configSchema?.properties?.[output.id]) {
|
|
238
|
+
field.schema = configSchema.properties[output.id];
|
|
239
|
+
}
|
|
240
|
+
fields.push(field);
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
// If no outputs defined, create a default field based on node config
|
|
244
|
+
if (outputs.length === 0) {
|
|
245
|
+
const configSchema = node.data.metadata?.configSchema;
|
|
246
|
+
if (configSchema?.properties) {
|
|
247
|
+
Object.entries(configSchema.properties).forEach(([key, schema]) => {
|
|
248
|
+
const field = {
|
|
249
|
+
nodeId: node.id,
|
|
250
|
+
fieldId: key,
|
|
251
|
+
label: schema.title || key,
|
|
252
|
+
type: schema.type || 'string',
|
|
253
|
+
defaultValue: node.data.config?.[key] ?? schema.default,
|
|
254
|
+
required: configSchema.required?.includes(key) ?? false,
|
|
255
|
+
schema
|
|
256
|
+
};
|
|
257
|
+
fields.push(field);
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
});
|
|
263
|
+
return fields;
|
|
264
|
+
}
|
|
265
|
+
/** Whether the workflow has a chat input. */
|
|
266
|
+
get hasChatInput() {
|
|
267
|
+
const fields = this.inputFields;
|
|
268
|
+
return fields.some((field) => isChatInputNode(field.nodeId) || field.type === 'string');
|
|
269
|
+
}
|
|
270
|
+
/** Session count. */
|
|
271
|
+
get sessionCount() {
|
|
272
|
+
return this.#sessions.length;
|
|
273
|
+
}
|
|
274
|
+
/** Execution ID explicitly pinned by the user (null = follow latest). */
|
|
275
|
+
get pinnedExecutionId() {
|
|
276
|
+
return this.#pinnedExecutionId;
|
|
277
|
+
}
|
|
278
|
+
/** Latest main-run execution ID. */
|
|
279
|
+
get latestExecutionId() {
|
|
280
|
+
return this.#latestExecutionId;
|
|
281
|
+
}
|
|
282
|
+
/** Active execution: pinned if set, otherwise latest. */
|
|
283
|
+
get activeExecutionId() {
|
|
284
|
+
return this.#activeExecutionId;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Main pipeline runs for the run-switcher. Excludes sub-flow runs, which can't
|
|
288
|
+
* render their own graph and so aren't user-selectable.
|
|
289
|
+
*/
|
|
290
|
+
get selectableExecutions() {
|
|
291
|
+
return this.#selectableExecutions;
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Counter that increments whenever new messages arrive and the pipeline display
|
|
295
|
+
* should re-fetch — i.e. when following latest or pinned to the latest execution.
|
|
296
|
+
* Pass to PipelinePanel's refreshTrigger prop.
|
|
297
|
+
*/
|
|
298
|
+
get pipelineRefreshTrigger() {
|
|
299
|
+
return this.#pipelineRefreshTrigger;
|
|
300
|
+
}
|
|
301
|
+
/** Whether log messages should be shown in the execution console. */
|
|
302
|
+
get showLogs() {
|
|
303
|
+
return this.#showLogs;
|
|
304
|
+
}
|
|
305
|
+
/** The current session ID, or null. */
|
|
306
|
+
get currentSessionId() {
|
|
307
|
+
return this.#currentSession?.id ?? null;
|
|
308
|
+
}
|
|
309
|
+
/** Whether older messages exist before the oldest one currently loaded. */
|
|
310
|
+
get hasOlder() {
|
|
311
|
+
return this.#hasOlder;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* The sequence number of the latest message, used to seed incremental polling.
|
|
315
|
+
*/
|
|
316
|
+
get latestSequenceNumber() {
|
|
317
|
+
for (let i = this.#messages.length - 1; i >= 0; i--) {
|
|
318
|
+
if (this.#messages[i].sequenceNumber !== undefined) {
|
|
319
|
+
return this.#messages[i].sequenceNumber;
|
|
320
|
+
}
|
|
384
321
|
}
|
|
385
|
-
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
386
324
|
/**
|
|
387
|
-
*
|
|
325
|
+
* The sequence number of the oldest loaded message, used as the cursor
|
|
326
|
+
* for backward "load older" pagination.
|
|
327
|
+
*/
|
|
328
|
+
get oldestSequenceNumber() {
|
|
329
|
+
for (let i = 0; i < this.#messages.length; i++) {
|
|
330
|
+
if (this.#messages[i].sequenceNumber !== undefined) {
|
|
331
|
+
return this.#messages[i].sequenceNumber;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
// -----------------------------------------------------------------------
|
|
337
|
+
// Internal helpers
|
|
338
|
+
// -----------------------------------------------------------------------
|
|
339
|
+
/**
|
|
340
|
+
* Syncs the current session's executions list from incoming messages.
|
|
388
341
|
*
|
|
389
|
-
*
|
|
342
|
+
* Each message's `executionId` identifies the run that produced it, and
|
|
343
|
+
* `parentPipelineId` says whether that run is the main pipeline or a nested
|
|
344
|
+
* sub-flow (see {@link isSubflowMessage}). A run's classification is fixed by
|
|
345
|
+
* its first sighting — every message from a run reports the same parent — so we
|
|
346
|
+
* only act on executionIds we haven't seen before. Sub-flows are tracked but
|
|
347
|
+
* hidden from the run-switcher; a new *main* run clears the pin so the panel
|
|
348
|
+
* auto-follows it, while sub-flow runs never take focus.
|
|
349
|
+
*
|
|
350
|
+
* Executions are appended in arrival order; new runs land at the tail, which is
|
|
351
|
+
* why "latest" reads the last main run.
|
|
390
352
|
*/
|
|
391
|
-
|
|
392
|
-
if (
|
|
393
|
-
|
|
394
|
-
|
|
353
|
+
#syncExecutionsFromMessages(messages) {
|
|
354
|
+
if (!this.#currentSession)
|
|
355
|
+
return;
|
|
356
|
+
const executions = [...(this.#currentSession.executions ?? [])];
|
|
357
|
+
// eslint-disable-next-line svelte/prefer-svelte-reactivity -- transient local for dedup within this call
|
|
358
|
+
const seenIds = new Set(executions.map((e) => e.id));
|
|
359
|
+
let added = false;
|
|
360
|
+
let gainedMainRun = false;
|
|
361
|
+
for (const msg of messages) {
|
|
362
|
+
if (!msg.executionId || seenIds.has(msg.executionId))
|
|
363
|
+
continue;
|
|
364
|
+
seenIds.add(msg.executionId);
|
|
365
|
+
const isSubflow = isSubflowMessage(msg);
|
|
366
|
+
executions.push({
|
|
367
|
+
id: msg.executionId,
|
|
368
|
+
startedAt: msg.timestamp,
|
|
369
|
+
status: 'running',
|
|
370
|
+
isSubflow
|
|
371
|
+
});
|
|
372
|
+
added = true;
|
|
373
|
+
if (!isSubflow)
|
|
374
|
+
gainedMainRun = true;
|
|
375
|
+
}
|
|
376
|
+
if (!added)
|
|
377
|
+
return;
|
|
378
|
+
this.#currentSession = { ...this.#currentSession, executions };
|
|
379
|
+
// Auto-follow the new main run by dropping any manual pin.
|
|
380
|
+
if (gainedMainRun)
|
|
381
|
+
this.#pinnedExecutionId = null;
|
|
382
|
+
}
|
|
383
|
+
// -----------------------------------------------------------------------
|
|
384
|
+
// Mutation actions
|
|
385
|
+
// -----------------------------------------------------------------------
|
|
386
|
+
/** Set the current workflow. */
|
|
387
|
+
setWorkflow(workflow) {
|
|
388
|
+
this.#currentWorkflow = workflow;
|
|
389
|
+
}
|
|
390
|
+
/** Set the current session. */
|
|
391
|
+
setCurrentSession(session) {
|
|
392
|
+
this.#pinnedExecutionId = null;
|
|
393
|
+
this.#currentSession = session;
|
|
394
|
+
if (session) {
|
|
395
|
+
// Update session in the list
|
|
396
|
+
this.#sessions = this.#sessions.map((s) => (s.id === session.id ? session : s));
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
/** Update session status. */
|
|
400
|
+
updateSessionStatus(status) {
|
|
401
|
+
if (this.#currentSession) {
|
|
402
|
+
this.#currentSession = {
|
|
403
|
+
...this.#currentSession,
|
|
395
404
|
status,
|
|
396
405
|
updatedAt: new Date().toISOString()
|
|
397
406
|
};
|
|
@@ -406,93 +415,75 @@ export const playgroundActions = {
|
|
|
406
415
|
: status === 'completed' || status === 'idle'
|
|
407
416
|
? 'completed'
|
|
408
417
|
: null;
|
|
409
|
-
if (terminalExecutionStatus &&
|
|
410
|
-
const hasRunning =
|
|
418
|
+
if (terminalExecutionStatus && this.#currentSession?.executions?.length) {
|
|
419
|
+
const hasRunning = this.#currentSession.executions.some((e) => e.status === 'running');
|
|
411
420
|
if (hasRunning) {
|
|
412
|
-
|
|
413
|
-
...
|
|
414
|
-
executions:
|
|
421
|
+
this.#currentSession = {
|
|
422
|
+
...this.#currentSession,
|
|
423
|
+
executions: this.#currentSession.executions.map((e) => e.status === 'running' ? { ...e, status: terminalExecutionStatus } : e)
|
|
415
424
|
};
|
|
416
425
|
}
|
|
417
426
|
}
|
|
418
427
|
// Also update in sessions list
|
|
419
|
-
const session =
|
|
428
|
+
const session = this.#currentSession;
|
|
420
429
|
if (session) {
|
|
421
|
-
|
|
430
|
+
this.#sessions = this.#sessions.map((s) => (s.id === session.id ? { ...s, status } : s));
|
|
422
431
|
}
|
|
423
|
-
}
|
|
424
|
-
/**
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
}
|
|
432
|
-
/**
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
* @param session - The session to add
|
|
436
|
-
*/
|
|
437
|
-
addSession: (session) => {
|
|
438
|
-
_sessions = [session, ..._sessions];
|
|
439
|
-
},
|
|
440
|
-
/**
|
|
441
|
-
* Remove a session from the list
|
|
442
|
-
*
|
|
443
|
-
* @param sessionId - The session ID to remove
|
|
444
|
-
*/
|
|
445
|
-
removeSession: (sessionId) => {
|
|
446
|
-
_sessions = _sessions.filter((s) => s.id !== sessionId);
|
|
432
|
+
}
|
|
433
|
+
/** Set the sessions list. */
|
|
434
|
+
setSessions(sessionList) {
|
|
435
|
+
this.#sessions = sessionList;
|
|
436
|
+
}
|
|
437
|
+
/** Add a new session to the list. */
|
|
438
|
+
addSession(session) {
|
|
439
|
+
this.#sessions = [session, ...this.#sessions];
|
|
440
|
+
}
|
|
441
|
+
/** Remove a session from the list. */
|
|
442
|
+
removeSession(sessionId) {
|
|
443
|
+
this.#sessions = this.#sessions.filter((s) => s.id !== sessionId);
|
|
447
444
|
// Clear current session if it was removed
|
|
448
|
-
if (
|
|
449
|
-
|
|
450
|
-
|
|
445
|
+
if (this.#currentSession?.id === sessionId) {
|
|
446
|
+
this.#currentSession = null;
|
|
447
|
+
this.#messages = [];
|
|
451
448
|
}
|
|
452
|
-
}
|
|
449
|
+
}
|
|
453
450
|
/**
|
|
454
|
-
* Set messages for the current session
|
|
455
|
-
* Messages are automatically sorted chronologically
|
|
456
|
-
*
|
|
457
|
-
* @param messageList - Array of messages
|
|
451
|
+
* Set messages for the current session.
|
|
452
|
+
* Messages are automatically sorted chronologically.
|
|
458
453
|
*/
|
|
459
|
-
setMessages
|
|
460
|
-
|
|
461
|
-
}
|
|
454
|
+
setMessages(messageList) {
|
|
455
|
+
this.#messages = sortMessagesChronologically(messageList);
|
|
456
|
+
}
|
|
462
457
|
/**
|
|
463
|
-
* Add a message to the current session
|
|
458
|
+
* Add a message to the current session.
|
|
464
459
|
* Uses binary search insertion for O(log n) instead of full sort.
|
|
465
|
-
*
|
|
466
|
-
* @param message - The message to add
|
|
467
460
|
*/
|
|
468
|
-
addMessage
|
|
469
|
-
if (
|
|
461
|
+
addMessage(message) {
|
|
462
|
+
if (this.#messages.some((m) => m.id === message.id))
|
|
470
463
|
return;
|
|
471
464
|
const seq = message.sequenceNumber ?? 0;
|
|
472
|
-
let lo = 0, hi =
|
|
465
|
+
let lo = 0, hi = this.#messages.length;
|
|
473
466
|
while (lo < hi) {
|
|
474
467
|
const mid = (lo + hi) >>> 1;
|
|
475
|
-
if ((
|
|
468
|
+
if ((this.#messages[mid].sequenceNumber ?? 0) <= seq)
|
|
476
469
|
lo = mid + 1;
|
|
477
470
|
else
|
|
478
471
|
hi = mid;
|
|
479
472
|
}
|
|
480
|
-
|
|
481
|
-
}
|
|
473
|
+
this.#messages = [...this.#messages.slice(0, lo), message, ...this.#messages.slice(lo)];
|
|
474
|
+
}
|
|
482
475
|
/**
|
|
483
|
-
* Add multiple messages to the current session
|
|
484
|
-
* Messages are deduplicated and automatically sorted chronologically
|
|
485
|
-
*
|
|
486
|
-
* @param newMessages - Array of messages to add
|
|
476
|
+
* Add multiple messages to the current session.
|
|
477
|
+
* Messages are deduplicated and automatically sorted chronologically.
|
|
487
478
|
*/
|
|
488
|
-
addMessages
|
|
479
|
+
addMessages(newMessages) {
|
|
489
480
|
if (newMessages.length === 0)
|
|
490
481
|
return;
|
|
491
482
|
// Deduplicate against existing messages AND within the incoming batch itself.
|
|
492
483
|
// The latter matters when the backend returns the same page twice (e.g. broken
|
|
493
|
-
// offset pagination), which would otherwise create duplicate IDs in
|
|
484
|
+
// offset pagination), which would otherwise create duplicate IDs in #messages
|
|
494
485
|
// and trigger Svelte's each_key_duplicate error.
|
|
495
|
-
const existingIds = new Set(
|
|
486
|
+
const existingIds = new Set(this.#messages.map((m) => m.id));
|
|
496
487
|
const seenInBatch = new Set();
|
|
497
488
|
const uniqueNewMessages = newMessages.filter((m) => {
|
|
498
489
|
if (existingIds.has(m.id) || seenInBatch.has(m.id))
|
|
@@ -500,213 +491,157 @@ export const playgroundActions = {
|
|
|
500
491
|
seenInBatch.add(m.id);
|
|
501
492
|
return true;
|
|
502
493
|
});
|
|
503
|
-
|
|
504
|
-
syncExecutionsFromMessages(uniqueNewMessages);
|
|
505
|
-
}
|
|
506
|
-
/**
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
494
|
+
this.#messages = sortMessagesChronologically([...this.#messages, ...uniqueNewMessages]);
|
|
495
|
+
this.#syncExecutionsFromMessages(uniqueNewMessages);
|
|
496
|
+
}
|
|
497
|
+
/** Clear all messages. */
|
|
498
|
+
clearMessages() {
|
|
499
|
+
this.#messages = [];
|
|
500
|
+
this.#lastPollSequenceNumber = null;
|
|
501
|
+
this.#hasOlder = false;
|
|
502
|
+
}
|
|
503
|
+
/** Set the loading state. */
|
|
504
|
+
setLoading(loading) {
|
|
505
|
+
this.#isLoading = loading;
|
|
506
|
+
}
|
|
507
|
+
/** Set an error message (or null to clear). */
|
|
508
|
+
setError(errorMessage) {
|
|
509
|
+
this.#error = errorMessage;
|
|
510
|
+
}
|
|
511
|
+
/** Update the last poll cursor. */
|
|
512
|
+
updateLastPollSequenceNumber(seq) {
|
|
513
|
+
this.#lastPollSequenceNumber = seq;
|
|
514
|
+
}
|
|
515
|
+
/** Reset all playground state. */
|
|
516
|
+
reset() {
|
|
517
|
+
this.#currentSession = null;
|
|
518
|
+
this.#sessions = [];
|
|
519
|
+
this.#messages = [];
|
|
520
|
+
this.#isLoading = false;
|
|
521
|
+
this.#error = null;
|
|
522
|
+
this.#currentWorkflow = null;
|
|
523
|
+
this.#lastPollSequenceNumber = null;
|
|
524
|
+
this.#pipelineRefreshTrigger = 0;
|
|
525
|
+
}
|
|
526
|
+
/** Switch to a different session. */
|
|
527
|
+
switchSession(sessionId) {
|
|
528
|
+
this.#pinnedExecutionId = null;
|
|
529
|
+
const session = this.#sessions.find((s) => s.id === sessionId);
|
|
530
|
+
if (session) {
|
|
531
|
+
this.#currentSession = session;
|
|
532
|
+
this.#messages = [];
|
|
533
|
+
this.#lastPollSequenceNumber = null;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
/** Pin an execution (null = follow latest). */
|
|
537
|
+
pinExecution(executionId) {
|
|
538
|
+
this.#pinnedExecutionId = executionId;
|
|
539
|
+
}
|
|
540
|
+
/** Set log message visibility. */
|
|
541
|
+
setShowLogs(value) {
|
|
542
|
+
this.#showLogs = value;
|
|
543
|
+
}
|
|
544
|
+
/** Toggle log message visibility. */
|
|
545
|
+
toggleShowLogs() {
|
|
546
|
+
this.#showLogs = !this.#showLogs;
|
|
547
|
+
}
|
|
548
|
+
// -----------------------------------------------------------------------
|
|
549
|
+
// Server response application & utilities
|
|
550
|
+
// -----------------------------------------------------------------------
|
|
514
551
|
/**
|
|
515
|
-
*
|
|
552
|
+
* Apply a server response to the store. All message and status updates from
|
|
553
|
+
* the server flow through here — polling callback, manual fetches, interrupt
|
|
554
|
+
* resolution. Nothing updates messages or session status except this function.
|
|
516
555
|
*
|
|
517
|
-
*
|
|
556
|
+
* Pass `sessionId` (the session the response was fetched for) so a response
|
|
557
|
+
* that resolves after the user switched sessions is dropped instead of writing
|
|
558
|
+
* the old session's status/messages onto the new current session. Pass `null`
|
|
559
|
+
* to deliberately opt out of the guard (non-session-scoped callers only) — the
|
|
560
|
+
* argument is required so every new caller has to make that choice explicitly.
|
|
518
561
|
*/
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
562
|
+
applyServerResponse(response, sessionId) {
|
|
563
|
+
if (sessionId !== null && this.#currentSession?.id !== sessionId)
|
|
564
|
+
return;
|
|
565
|
+
if (response.data && response.data.length > 0) {
|
|
566
|
+
this.addMessages(response.data);
|
|
567
|
+
// Refresh the pipeline panel when following latest or pinned to the latest
|
|
568
|
+
// run. Skip when pinned to an older run — a historical view that won't change.
|
|
569
|
+
if (this.#pinnedExecutionId === null || this.#pinnedExecutionId === this.#latestExecutionId) {
|
|
570
|
+
this.#pipelineRefreshTrigger++;
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
if (response.sessionStatus) {
|
|
574
|
+
this.updateSessionStatus(response.sessionStatus);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
/** Check if a specific session is selected. */
|
|
578
|
+
isSessionSelected(sessionId) {
|
|
579
|
+
return this.#currentSession?.id === sessionId;
|
|
580
|
+
}
|
|
522
581
|
/**
|
|
523
|
-
* Set
|
|
524
|
-
*
|
|
525
|
-
* @param errorMessage - The error message or null to clear
|
|
582
|
+
* Set whether older messages remain to be loaded, derived from a
|
|
583
|
+
* backward-pagination response.
|
|
526
584
|
*/
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
}
|
|
585
|
+
setHasOlder(hasOlder) {
|
|
586
|
+
this.#hasOlder = hasOlder;
|
|
587
|
+
}
|
|
530
588
|
/**
|
|
531
|
-
*
|
|
589
|
+
* Subscribe to session status changes using $effect.root.
|
|
590
|
+
* This is designed for use in non-component contexts (e.g., mount.ts).
|
|
532
591
|
*
|
|
533
|
-
*
|
|
592
|
+
* The effect root is tracked by the store and also disposed by
|
|
593
|
+
* {@link dispose} (via the owning instance's `destroy()`), so a forgotten
|
|
594
|
+
* unsubscribe can't outlive the instance.
|
|
595
|
+
*
|
|
596
|
+
* @param callback - Called when session status changes
|
|
597
|
+
* @returns Cleanup function to stop the subscription
|
|
534
598
|
*/
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
599
|
+
subscribeToSessionStatus(callback) {
|
|
600
|
+
let previousStatus = this.sessionStatus;
|
|
601
|
+
const cleanup = $effect.root(() => {
|
|
602
|
+
$effect(() => {
|
|
603
|
+
const status = this.sessionStatus;
|
|
604
|
+
if (status !== previousStatus) {
|
|
605
|
+
callback(status, previousStatus);
|
|
606
|
+
previousStatus = status;
|
|
607
|
+
}
|
|
608
|
+
});
|
|
609
|
+
});
|
|
610
|
+
const tracked = () => {
|
|
611
|
+
this.#statusSubscriptions.delete(tracked);
|
|
612
|
+
cleanup();
|
|
613
|
+
};
|
|
614
|
+
this.#statusSubscriptions.add(tracked);
|
|
615
|
+
return tracked;
|
|
616
|
+
}
|
|
538
617
|
/**
|
|
539
|
-
*
|
|
618
|
+
* Dispose all active session-status effect roots.
|
|
619
|
+
* Called by the owning instance's destroy(); safe to call repeatedly.
|
|
540
620
|
*/
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
_error = null;
|
|
547
|
-
_currentWorkflow = null;
|
|
548
|
-
_lastPollSequenceNumber = null;
|
|
549
|
-
_pipelineRefreshTrigger = 0;
|
|
550
|
-
},
|
|
621
|
+
dispose() {
|
|
622
|
+
for (const cleanup of [...this.#statusSubscriptions]) {
|
|
623
|
+
cleanup();
|
|
624
|
+
}
|
|
625
|
+
}
|
|
551
626
|
/**
|
|
552
|
-
*
|
|
627
|
+
* Refresh messages for the current session.
|
|
553
628
|
*
|
|
554
|
-
*
|
|
629
|
+
* This function is useful after interrupt resolution when polling
|
|
630
|
+
* has stopped but new messages may exist on the server.
|
|
631
|
+
*
|
|
632
|
+
* @param fetchMessages - Async function to fetch messages from the API
|
|
633
|
+
* @returns Promise that resolves when messages are refreshed
|
|
555
634
|
*/
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
}
|
|
564
|
-
},
|
|
565
|
-
pinExecution(executionId) {
|
|
566
|
-
_pinnedExecutionId = executionId;
|
|
567
|
-
},
|
|
568
|
-
setShowLogs(value) {
|
|
569
|
-
_showLogs = value;
|
|
570
|
-
},
|
|
571
|
-
toggleShowLogs() {
|
|
572
|
-
_showLogs = !_showLogs;
|
|
573
|
-
}
|
|
574
|
-
};
|
|
575
|
-
// =========================================================================
|
|
576
|
-
// Server Response Application
|
|
577
|
-
// =========================================================================
|
|
578
|
-
/**
|
|
579
|
-
* Apply a server response to the store. All message and status updates from
|
|
580
|
-
* the server flow through here — polling callback, manual fetches, interrupt
|
|
581
|
-
* resolution. Nothing updates messages or session status except this function.
|
|
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;
|
|
592
|
-
if (response.data && response.data.length > 0) {
|
|
593
|
-
playgroundActions.addMessages(response.data);
|
|
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.
|
|
596
|
-
if (_pinnedExecutionId === null || _pinnedExecutionId === _latestExecutionId) {
|
|
597
|
-
_pipelineRefreshTrigger++;
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
if (response.sessionStatus) {
|
|
601
|
-
playgroundActions.updateSessionStatus(response.sessionStatus);
|
|
602
|
-
}
|
|
603
|
-
}
|
|
604
|
-
// =========================================================================
|
|
605
|
-
// Utilities
|
|
606
|
-
// =========================================================================
|
|
607
|
-
/**
|
|
608
|
-
* Get the current session ID
|
|
609
|
-
*
|
|
610
|
-
* @returns The current session ID or null
|
|
611
|
-
*/
|
|
612
|
-
export function getCurrentSessionId() {
|
|
613
|
-
return _currentSession?.id ?? null;
|
|
614
|
-
}
|
|
615
|
-
/**
|
|
616
|
-
* Check if a specific session is selected
|
|
617
|
-
*
|
|
618
|
-
* @param sessionId - The session ID to check
|
|
619
|
-
* @returns True if the session is currently selected
|
|
620
|
-
*/
|
|
621
|
-
export function isSessionSelected(sessionId) {
|
|
622
|
-
return _currentSession?.id === sessionId;
|
|
623
|
-
}
|
|
624
|
-
/**
|
|
625
|
-
* Get all messages as a snapshot
|
|
626
|
-
*
|
|
627
|
-
* @returns Array of all messages
|
|
628
|
-
*/
|
|
629
|
-
export function getMessagesSnapshot() {
|
|
630
|
-
return _messages;
|
|
631
|
-
}
|
|
632
|
-
/**
|
|
633
|
-
* Get the sequence number of the latest message, used to seed incremental polling.
|
|
634
|
-
*
|
|
635
|
-
* @returns Sequence number of the last message, or null
|
|
636
|
-
*/
|
|
637
|
-
export function getLatestSequenceNumber() {
|
|
638
|
-
for (let i = _messages.length - 1; i >= 0; i--) {
|
|
639
|
-
if (_messages[i].sequenceNumber !== undefined) {
|
|
640
|
-
return _messages[i].sequenceNumber;
|
|
635
|
+
async refreshSessionMessages(fetchMessages) {
|
|
636
|
+
const session = this.#currentSession;
|
|
637
|
+
if (!session)
|
|
638
|
+
return;
|
|
639
|
+
try {
|
|
640
|
+
const response = await fetchMessages(session.id);
|
|
641
|
+
this.applyServerResponse(response, session.id);
|
|
641
642
|
}
|
|
642
|
-
|
|
643
|
-
|
|
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;
|
|
643
|
+
catch (err) {
|
|
644
|
+
logger.error('[playgroundStore] Failed to refresh messages:', err);
|
|
655
645
|
}
|
|
656
646
|
}
|
|
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
|
-
}
|
|
672
|
-
/**
|
|
673
|
-
* Subscribe to session status changes using $effect.root.
|
|
674
|
-
* This is designed for use in non-component contexts (e.g., mount.ts).
|
|
675
|
-
*
|
|
676
|
-
* @param callback - Called when session status changes
|
|
677
|
-
* @returns Cleanup function to stop the subscription
|
|
678
|
-
*/
|
|
679
|
-
export function subscribeToSessionStatus(callback) {
|
|
680
|
-
let previousStatus = getSessionStatus();
|
|
681
|
-
const cleanup = $effect.root(() => {
|
|
682
|
-
$effect(() => {
|
|
683
|
-
const status = getSessionStatus();
|
|
684
|
-
if (status !== previousStatus) {
|
|
685
|
-
callback(status, previousStatus);
|
|
686
|
-
previousStatus = status;
|
|
687
|
-
}
|
|
688
|
-
});
|
|
689
|
-
});
|
|
690
|
-
return cleanup;
|
|
691
|
-
}
|
|
692
|
-
/**
|
|
693
|
-
* Refresh messages for the current session
|
|
694
|
-
*
|
|
695
|
-
* This function is useful after interrupt resolution when polling
|
|
696
|
-
* has stopped but new messages may exist on the server.
|
|
697
|
-
*
|
|
698
|
-
* @param fetchMessages - Async function to fetch messages from the API
|
|
699
|
-
* @returns Promise that resolves when messages are refreshed
|
|
700
|
-
*/
|
|
701
|
-
export async function refreshSessionMessages(fetchMessages) {
|
|
702
|
-
const session = _currentSession;
|
|
703
|
-
if (!session)
|
|
704
|
-
return;
|
|
705
|
-
try {
|
|
706
|
-
const response = await fetchMessages(session.id);
|
|
707
|
-
applyServerResponse(response, session.id);
|
|
708
|
-
}
|
|
709
|
-
catch (err) {
|
|
710
|
-
logger.error('[playgroundStore] Failed to refresh messages:', err);
|
|
711
|
-
}
|
|
712
647
|
}
|