@xdelivered/emberflow 0.2.0 → 0.5.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/bin/commands.ts +7 -3
- package/bin/init.test.ts +94 -0
- package/bin/init.ts +108 -2
- package/dist/bin/commands.d.ts +1 -0
- package/dist/bin/commands.js +6 -3
- package/dist/bin/init.d.ts +4 -0
- package/dist/bin/init.js +96 -2
- package/dist/server/agents/codexParse.js +31 -0
- package/dist/server/agents/detect.d.ts +18 -8
- package/dist/server/agents/detect.js +78 -13
- package/dist/server/agents/modelRejectionHint.js +1 -1
- package/dist/server/agents/prompt.d.ts +15 -1
- package/dist/server/agents/prompt.js +59 -37
- package/dist/server/agents/runManager.d.ts +23 -2
- package/dist/server/agents/runManager.js +36 -8
- package/dist/server/client.d.ts +1 -0
- package/dist/server/client.js +7 -2
- package/dist/server/index.js +208 -16
- package/dist/server/nodesPayload.d.ts +14 -1
- package/dist/server/nodesPayload.js +15 -2
- package/dist/server/projectMode.js +5 -2
- package/dist/server/runRegistry.d.ts +52 -1
- package/dist/server/runRegistry.js +170 -1
- package/dist/server/sourceNav.d.ts +77 -0
- package/dist/server/sourceNav.js +503 -0
- package/dist/server/subflowRunner.d.ts +48 -1
- package/dist/server/subflowRunner.js +34 -7
- package/dist/server/updateCheck.d.ts +68 -0
- package/dist/server/updateCheck.js +142 -0
- package/dist/src/engine/executor.js +4 -3
- package/dist/src/engine/registry.d.ts +27 -2
- package/dist/src/engine/registry.js +84 -2
- package/dist/src/nodes/index.d.ts +8 -2
- package/dist/src/nodes/index.js +7 -3
- package/dist/src/nodes/login.d.ts +3 -1
- package/dist/src/nodes/login.js +2 -2
- package/package.json +28 -7
- package/server/agentRoute.test.ts +20 -0
- package/server/agents/codexParse.test.ts +37 -0
- package/server/agents/codexParse.ts +34 -0
- package/server/agents/detect.test.ts +26 -7
- package/server/agents/detect.ts +82 -14
- package/server/agents/modelRejectionHint.ts +2 -1
- package/server/agents/prompt.test.ts +128 -0
- package/server/agents/prompt.ts +102 -3
- package/server/agents/runManager.test.ts +9 -0
- package/server/agents/runManager.ts +37 -7
- package/server/client.ts +10 -4
- package/server/index.ts +213 -15
- package/server/nodeRun.test.ts +189 -0
- package/server/nodesPayload.test.ts +38 -0
- package/server/nodesPayload.ts +28 -3
- package/server/projectMode.ts +5 -2
- package/server/runRegistry.ts +200 -3
- package/server/setupStatus.test.ts +3 -0
- package/server/sourceNav.test.ts +382 -0
- package/server/sourceNav.ts +644 -0
- package/server/sourceNavRoute.test.ts +163 -0
- package/server/stepDrill.test.ts +380 -0
- package/server/subflowRunner.ts +92 -11
- package/server/updateCheck.test.ts +209 -0
- package/server/updateCheck.ts +175 -0
- package/server/workflowTestRoute.test.ts +1 -1
- package/src/App.tsx +82 -2
- package/src/components/AgentConsole.tsx +7 -280
- package/src/components/AgentStream.test.tsx +88 -0
- package/src/components/AgentStream.tsx +303 -0
- package/src/components/CreateModal.tsx +43 -9
- package/src/components/Dock.tsx +1 -1
- package/src/components/EmptyState.test.tsx +49 -0
- package/src/components/EmptyState.tsx +61 -0
- package/src/components/EnvironmentDialog.tsx +4 -1
- package/src/components/EnvironmentPicker.tsx +8 -3
- package/src/components/EnvironmentsDialog.tsx +4 -1
- package/src/components/InfraPanel.tsx +30 -2
- package/src/components/InfrastructureDialog.test.tsx +97 -0
- package/src/components/InfrastructureDialog.tsx +111 -0
- package/src/components/Inspector.tsx +9 -26
- package/src/components/RunbookView.tsx +70 -6
- package/src/components/SettingsDialog.tsx +4 -59
- package/src/components/Sidebar.tsx +6 -26
- package/src/components/SourceNavigator.test.tsx +226 -0
- package/src/components/SourceNavigator.tsx +520 -0
- package/src/components/StatusBar.tsx +227 -26
- package/src/components/Toolbar.tsx +28 -16
- package/src/components/UpdateChip.test.tsx +31 -0
- package/src/components/WelcomeDialog.test.tsx +384 -13
- package/src/components/WelcomeDialog.tsx +996 -177
- package/src/engine/executor.ts +4 -3
- package/src/engine/registry.sourceRef.test.ts +112 -0
- package/src/engine/registry.ts +103 -2
- package/src/lib/guidedQuestions.test.ts +224 -0
- package/src/lib/guidedQuestions.ts +181 -0
- package/src/lib/runbookModel.ts +3 -3
- package/src/lib/runbookProjection.test.ts +43 -0
- package/src/lib/runbookProjection.ts +26 -6
- package/src/nodes/index.ts +10 -3
- package/src/nodes/login.ts +5 -2
- package/src/store/agentClient.ts +27 -8
- package/src/store/apiTree.ts +12 -0
- package/src/store/builderStore.test.ts +441 -99
- package/src/store/builderStore.ts +385 -314
- package/src/store/nodeMeta.ts +10 -2
- package/src/store/serverRunner.ts +56 -5
- package/src/store/setupClient.ts +7 -2
- package/src/store/sourceNavClient.ts +48 -0
- package/src/store/updateClient.ts +50 -0
- package/studio-dist/assets/index-BbzppDpt.js +65 -0
- package/studio-dist/assets/index-CLf6blcA.css +1 -0
- package/studio-dist/index.html +2 -2
- package/templates/skills/emberflow-basics/SKILL.md +29 -1
- package/templates/skills/emberflow-model-process/SKILL.md +92 -9
- package/templates/skills/emberflow-new-workflow/SKILL.md +8 -1
- package/templates/skills/emberflow-review-workflow/SKILL.md +64 -1
- package/studio-dist/assets/index-DNJwW-hM.css +0 -1
- package/studio-dist/assets/index-O26dKRjW.js +0 -65
|
@@ -1,15 +1,11 @@
|
|
|
1
1
|
import { create } from 'zustand';
|
|
2
|
-
import { InMemoryTraceSink
|
|
2
|
+
import { InMemoryTraceSink } from '../engine/trace';
|
|
3
|
+
import { NodeRegistry } from '../engine/registry';
|
|
4
|
+
import type { FlowRun } from '../engine/executor';
|
|
3
5
|
import type {
|
|
4
|
-
FieldMapping,
|
|
5
|
-
|
|
6
|
-
} from '../engine';
|
|
7
|
-
import { createDefaultRegistry } from '../nodes';
|
|
8
|
-
import { createLoginFlow } from '../flows/login-flow';
|
|
9
|
-
import { createWeatherFlow } from '../flows/weather-flow';
|
|
10
|
-
import { createAnomalyFlows } from '../flows/anomaly-flows';
|
|
11
|
-
import { createCitySweepFlow } from '../flows/city-sweep-flow';
|
|
12
|
-
import { createPradarFlows } from '../flows/pradar-flows';
|
|
6
|
+
FieldMapping, LogLine, ScenarioDefinition,
|
|
7
|
+
WorkflowDefinition, WorkflowNode, WorkflowRun,
|
|
8
|
+
} from '../engine/types';
|
|
13
9
|
import { loadWorkspace, parseFlow, saveWorkspace, serializeFlow } from './persistence';
|
|
14
10
|
import { fetchNodeMeta } from './nodeMeta';
|
|
15
11
|
import {
|
|
@@ -22,6 +18,7 @@ import {
|
|
|
22
18
|
loginEnvironment as loginEnvironmentOnServer,
|
|
23
19
|
putWorkflow,
|
|
24
20
|
runnerHealthy,
|
|
21
|
+
runNodeOnServer,
|
|
25
22
|
setEnvironmentAuth as setEnvironmentAuthOnServer,
|
|
26
23
|
setEnvironmentSecret as setEnvironmentSecretOnServer,
|
|
27
24
|
setServingMode as setServingModeOnServer,
|
|
@@ -31,12 +28,13 @@ import {
|
|
|
31
28
|
testWorkflow as testWorkflowOnServer,
|
|
32
29
|
} from './serverRunner';
|
|
33
30
|
import type {
|
|
34
|
-
EnvAuth, ErrorHandlerTag, EnvironmentSummary, ScenarioTestReport, ServerRunOptions,
|
|
31
|
+
EnvAuth, ErrorHandlerTag, EnvironmentSummary, ScenarioTestReport, ServerRunOptions, StepResult,
|
|
35
32
|
} from './serverRunner';
|
|
36
33
|
import { cancelAgent, fetchAgentDiff, revertAgent, startAgent, streamAgent } from './agentClient';
|
|
37
34
|
import type { AgentEvent, AgentIntent, AgentKind, StartAgentOptions } from './agentClient';
|
|
38
35
|
import { fetchSetupStatus } from './setupClient';
|
|
39
36
|
import type { SetupStatus } from './setupClient';
|
|
37
|
+
import { extractGuidedQuestions } from '../lib/guidedQuestions';
|
|
40
38
|
|
|
41
39
|
export interface WorkflowSummary {
|
|
42
40
|
id: string;
|
|
@@ -48,6 +46,14 @@ export interface WorkflowSummary {
|
|
|
48
46
|
http?: { method: string; path: string };
|
|
49
47
|
}
|
|
50
48
|
|
|
49
|
+
/** Configures the two entry points that share the create modal: "New API"
|
|
50
|
+
* (name + first operation) and "New operation" (optionally scoped to an API
|
|
51
|
+
* via `location`). Owned here so any surface — Sidebar, canvas empty state —
|
|
52
|
+
* opens the SAME modal (hosted once by CreateModalHost). */
|
|
53
|
+
export type CreateModalState =
|
|
54
|
+
| { mode: 'api'; initialGoal?: string }
|
|
55
|
+
| { mode: 'operation'; location?: string };
|
|
56
|
+
|
|
51
57
|
/** Per-operation path/http metadata, keyed by id — sourced from the runner's
|
|
52
58
|
* `/workflows` `operations` array (the store's `WorkflowDefinition`s don't
|
|
53
59
|
* carry `path`, since path is a filesystem concept, not a flow field). */
|
|
@@ -65,6 +71,30 @@ function dropReport(
|
|
|
65
71
|
}
|
|
66
72
|
|
|
67
73
|
|
|
74
|
+
/**
|
|
75
|
+
* One level of the stepped-run drill stack: entered when a step response
|
|
76
|
+
* reports `entered` (execution moved inside a Subflow node), popped on
|
|
77
|
+
* `exited`. The parent's flow/run/selection are stashed here so the canvas
|
|
78
|
+
* can show the child while the parent keeps receiving its own SSE states
|
|
79
|
+
* (routed into `savedRun` by workflowId), and be restored intact on exit.
|
|
80
|
+
*/
|
|
81
|
+
export interface StepDrillEntry {
|
|
82
|
+
/** The child flow's id (the level the drill entered INTO). */
|
|
83
|
+
workflowId: string;
|
|
84
|
+
/** The PARENT's Subflow node id that execution entered through. */
|
|
85
|
+
viaNodeId: string;
|
|
86
|
+
/** The parent level's flow, restored on exit. */
|
|
87
|
+
savedFlow: WorkflowDefinition;
|
|
88
|
+
/** The parent level's run — still updated by routed nodeState events while drilled. */
|
|
89
|
+
savedRun: WorkflowRun | null;
|
|
90
|
+
savedSelectedNodeId: string | null;
|
|
91
|
+
/** True when the child flow wasn't found in the workspace: the view stayed
|
|
92
|
+
* on the parent, but the level is still pushed so the client stack depth
|
|
93
|
+
* mirrors the server's and the matching `exited` pops correctly. Popping a
|
|
94
|
+
* placeholder leaves flow/run/selection untouched (they never changed). */
|
|
95
|
+
placeholder?: true;
|
|
96
|
+
}
|
|
97
|
+
|
|
68
98
|
/** A finished run plus builder-side context the engine doesn't track. */
|
|
69
99
|
export type RunHistoryEntry = WorkflowRun & {
|
|
70
100
|
scenarioName?: string;
|
|
@@ -116,12 +146,8 @@ interface BuilderState {
|
|
|
116
146
|
activeRun: FlowRun | null;
|
|
117
147
|
dockTab: 'logs' | 'output' | 'infra';
|
|
118
148
|
setDockTab(tab: 'logs' | 'output' | 'infra'): void;
|
|
119
|
-
/**
|
|
120
|
-
|
|
121
|
-
setExecutionMode(mode: 'auto' | 'browser' | 'server'): void;
|
|
122
|
-
/** Where the next run will actually execute. */
|
|
123
|
-
effectiveMode(): 'browser' | 'server';
|
|
124
|
-
/** null = not yet checked. */
|
|
149
|
+
/** null = not yet checked. Runs are always server-side; when the runner is
|
|
150
|
+
* offline the studio surfaces a calm offline state instead of executing. */
|
|
125
151
|
runnerOnline: boolean | null;
|
|
126
152
|
/** True when the reachable runner is serving EMBERFLOW_MOCK responses
|
|
127
153
|
* (from /healthz's `mock` field) rather than live execution. False when
|
|
@@ -138,7 +164,7 @@ interface BuilderState {
|
|
|
138
164
|
environments: EnvironmentSummary[];
|
|
139
165
|
/** The runner's default environment name. */
|
|
140
166
|
environmentsDefault: string;
|
|
141
|
-
/** The environment the next run points at (persisted
|
|
167
|
+
/** The environment the next run points at (persisted). */
|
|
142
168
|
selectedEnvironment: string;
|
|
143
169
|
/** When on, mutation nodes dry-run their writes (persisted). */
|
|
144
170
|
safeMode: boolean;
|
|
@@ -180,6 +206,18 @@ interface BuilderState {
|
|
|
180
206
|
/** True while a stepped run is in progress (started via Step, not Run) — drives
|
|
181
207
|
* the toolbar's step-over affordance. Cleared when the run finishes or resets. */
|
|
182
208
|
stepMode: boolean;
|
|
209
|
+
/** Stepped-run subflow drill stack, deepest last. Non-empty while execution
|
|
210
|
+
* is inside one (or more, when nested) Subflow nodes and the canvas shows
|
|
211
|
+
* the drilled child. Empty otherwise. */
|
|
212
|
+
stepDrill: StepDrillEntry[];
|
|
213
|
+
/** View-only peek back up the drill stack: an index into `stepDrill` shows
|
|
214
|
+
* that ancestor level's stashed flow/run in the runbook WITHOUT popping the
|
|
215
|
+
* stack or touching the server; null shows the deepest (live) level.
|
|
216
|
+
* Reset to null whenever execution enters or exits a subflow. */
|
|
217
|
+
drillPeek: number | null;
|
|
218
|
+
/** Peek at ancestor level `index` of the drill stack (null = back to the
|
|
219
|
+
* deepest level). Out-of-range indexes clear the peek. */
|
|
220
|
+
peekDrill(index: number | null): void;
|
|
183
221
|
sidebarOpen: boolean;
|
|
184
222
|
toggleSidebar(): void;
|
|
185
223
|
/** Whether the bottom dock panel is mounted (persisted). */
|
|
@@ -199,12 +237,19 @@ interface BuilderState {
|
|
|
199
237
|
beginEnvironmentSetup(): void;
|
|
200
238
|
/** Dispatch the infrastructure scout (scout-infrastructure intent): reads the
|
|
201
239
|
* project's deps/config/ORM/env-refs and writes emberflow/infrastructure.json.
|
|
202
|
-
* Shared by the Welcome checklist and the Dock's Infra tab empty state.
|
|
203
|
-
|
|
240
|
+
* Shared by the Welcome checklist and the Dock's Infra tab empty state. An
|
|
241
|
+
* optional free-text `instruction` (from the Infrastructure modal's "Update
|
|
242
|
+
* with AI") amends the manifest; empty/omitted runs a full rescan. */
|
|
243
|
+
beginInfrastructureScout(instruction?: string): void;
|
|
204
244
|
/** Whether the first-run Welcome/Setup checklist dialog is open. Auto-opened
|
|
205
245
|
* on a fresh project (see WelcomeDialog); always reachable from the Toolbar. */
|
|
206
246
|
welcomeOpen: boolean;
|
|
207
247
|
setWelcomeOpen(open: boolean): void;
|
|
248
|
+
/** The New API / New operation modal's open state. Lifted to the store so
|
|
249
|
+
* surfaces outside the Sidebar (e.g. the canvas empty state) share the ONE
|
|
250
|
+
* modal instead of duplicating it; null = closed. Hosted by CreateModalHost. */
|
|
251
|
+
createModal: CreateModalState | null;
|
|
252
|
+
setCreateModal(state: CreateModalState | null): void;
|
|
208
253
|
/** Whether the Settings dialog is open. Lifted to the store so the Welcome
|
|
209
254
|
* checklist's "coding agent" row can deep-link into it. */
|
|
210
255
|
settingsOpen: boolean;
|
|
@@ -295,12 +340,28 @@ interface BuilderState {
|
|
|
295
340
|
instruction?: string;
|
|
296
341
|
diff?: string;
|
|
297
342
|
files?: string[];
|
|
343
|
+
/** True when this run is a `guided-setup` run OWNED by the WelcomeDialog's
|
|
344
|
+
* two-pane phase machine. It suppresses the right-hand AgentConsole
|
|
345
|
+
* auto-open (the dialog embeds the stream itself) and, since the run lives
|
|
346
|
+
* in this single slot, lets the dialog re-attach after being closed
|
|
347
|
+
* mid-run — the phase derives from this flag + `status`. */
|
|
348
|
+
guided?: boolean;
|
|
298
349
|
} | null;
|
|
350
|
+
/** Prior guided-setup conversation (finished runs' events + the user's
|
|
351
|
+
* answers), preserved across continuation runs — each continuation REPLACES
|
|
352
|
+
* the agentRun slot, so without this the onboarding pane would wipe to
|
|
353
|
+
* "Thinking…" on every answer. Cleared on a fresh (no-notes) start. */
|
|
354
|
+
guidedTranscript: AgentEvent[];
|
|
299
355
|
/** Persisted agent+model picked in Settings; used as runAgent's default opts. */
|
|
300
356
|
agentChoice: AgentChoice;
|
|
301
357
|
setAgentChoice(choice: AgentChoice): void;
|
|
302
358
|
/** Start a coding-agent run and stream its events into `agentRun`. */
|
|
303
359
|
runAgent(intent: AgentIntent, opts?: StartAgentOptions): Promise<void>;
|
|
360
|
+
/** Kick off (or continue) the WelcomeDialog's guided-setup run: one agent run
|
|
361
|
+
* that reads state, scouts/skips, installs skills, and interviews about
|
|
362
|
+
* environments. Marks the run `guided` so the dialog owns its stream and the
|
|
363
|
+
* right-hand console stays closed. */
|
|
364
|
+
beginGuidedSetup(instruction?: string): void;
|
|
304
365
|
/**
|
|
305
366
|
* The operation currently being scaffolded by the create flow. Its stub is
|
|
306
367
|
* already selected; the canvas shows a "waiting for the agent" holding pattern
|
|
@@ -352,8 +413,9 @@ interface BuilderState {
|
|
|
352
413
|
pinNodeOutput(nodeId: string, output: unknown): void;
|
|
353
414
|
unpinNode(nodeId: string): void;
|
|
354
415
|
/**
|
|
355
|
-
* Execute one node
|
|
356
|
-
* capturing logs and recording a trace sample. Does not
|
|
416
|
+
* Execute one node in isolation against the given input on the runner (POST
|
|
417
|
+
* /node-run), capturing logs and recording a local trace sample. Does not
|
|
418
|
+
* touch run state. Requires the runner to be online.
|
|
357
419
|
*/
|
|
358
420
|
runNodeIsolated(
|
|
359
421
|
nodeId: string,
|
|
@@ -370,35 +432,11 @@ interface BuilderState {
|
|
|
370
432
|
importFlow(json: string): void;
|
|
371
433
|
}
|
|
372
434
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
/**
|
|
379
|
-
* A prefixed copy of a child-run log line, folded into the parent's stream:
|
|
380
|
-
* label becomes "<ChildFlow> › <nodeLabel>" and the console id is namespaced
|
|
381
|
-
* under the calling Subflow node so console clicks don't collide with parent
|
|
382
|
-
* ids.
|
|
383
|
-
*/
|
|
384
|
-
function prefixSubflowLog(line: LogLine, childName: string, callerNodeId: string): LogLine {
|
|
385
|
-
return {
|
|
386
|
-
...line,
|
|
387
|
-
nodeLabel: `${childName} › ${line.nodeLabel ?? ''}`,
|
|
388
|
-
nodeId: line.nodeId ? `${callerNodeId}/${line.nodeId}` : callerNodeId,
|
|
389
|
-
};
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
/** Build the cycle/depth error a subflow guard reports for a given ancestry. */
|
|
393
|
-
function subflowGuardError(ancestry: string[], workflowId: string): string | null {
|
|
394
|
-
if (ancestry.length >= SUBFLOW_DEPTH_CAP) {
|
|
395
|
-
return `subflow depth cap (${SUBFLOW_DEPTH_CAP}) exceeded`;
|
|
396
|
-
}
|
|
397
|
-
if (ancestry.includes(workflowId)) {
|
|
398
|
-
return `subflow cycle: ${[...ancestry, workflowId].join(' → ')}`;
|
|
399
|
-
}
|
|
400
|
-
return null;
|
|
401
|
-
}
|
|
435
|
+
// The studio registry is definitions-only: it carries node metadata (labels,
|
|
436
|
+
// schemas, categories) synced from the runner via syncNodeMeta, never the node
|
|
437
|
+
// implementations. All execution happens on the runner — the studio is a pure
|
|
438
|
+
// client. syncNodeMeta populates this from GET /api/nodes.
|
|
439
|
+
const registry = new NodeRegistry();
|
|
402
440
|
|
|
403
441
|
function touched(flow: WorkflowDefinition): WorkflowDefinition {
|
|
404
442
|
return { ...flow, updatedAt: new Date().toISOString() };
|
|
@@ -451,29 +489,15 @@ function emptyFlow(): WorkflowDefinition {
|
|
|
451
489
|
};
|
|
452
490
|
}
|
|
453
491
|
|
|
492
|
+
// The workspace comes from the runner (adopted on first health check via
|
|
493
|
+
// syncFromRunner). At boot we hydrate only from any locally-persisted workspace;
|
|
494
|
+
// with none, we open an empty flow. No example flows are seeded client-side —
|
|
495
|
+
// the studio bundles no node implementations, so it can't execute them anyway.
|
|
454
496
|
const workspace = loadWorkspace();
|
|
455
|
-
const
|
|
456
|
-
createWeatherFlow(),
|
|
457
|
-
createLoginFlow(),
|
|
458
|
-
...createAnomalyFlows(),
|
|
459
|
-
createCitySweepFlow(),
|
|
460
|
-
...createPradarFlows(),
|
|
461
|
-
];
|
|
462
|
-
// Saved flows win; example flows the workspace doesn't know yet are merged in.
|
|
463
|
-
const exampleById = new Map(exampleFlows.map((f) => [f.id, f]));
|
|
464
|
-
const savedFlows = (workspace?.flows ?? []).map((f) => {
|
|
465
|
-
// Backfill the preferred environment onto older saved copies of built-in
|
|
466
|
-
// flows that predate the field, without clobbering user edits.
|
|
467
|
-
if (f.environment === undefined && exampleById.get(f.id)?.environment) {
|
|
468
|
-
return { ...f, environment: exampleById.get(f.id)!.environment };
|
|
469
|
-
}
|
|
470
|
-
return f;
|
|
471
|
-
});
|
|
472
|
-
const savedIds = new Set(savedFlows.map((f) => f.id));
|
|
473
|
-
const initialFlows = [...savedFlows, ...exampleFlows.filter((f) => !savedIds.has(f.id))];
|
|
497
|
+
const savedFlows = workspace?.flows ?? [];
|
|
474
498
|
const initialFlow =
|
|
475
|
-
|
|
476
|
-
const initialShelf =
|
|
499
|
+
savedFlows.find((f) => f.id === workspace?.activeId) ?? savedFlows[0] ?? emptyFlow();
|
|
500
|
+
const initialShelf = savedFlows.filter((f) => f.id !== initialFlow.id);
|
|
477
501
|
|
|
478
502
|
const ENV_KEY = 'emberflow.environment';
|
|
479
503
|
const SAFE_KEY = 'emberflow.safeMode';
|
|
@@ -551,10 +575,6 @@ let flowEnvSynced = false;
|
|
|
551
575
|
let stepFollow = false;
|
|
552
576
|
|
|
553
577
|
export const useBuilderStore = create<BuilderState>((set, get) => {
|
|
554
|
-
const snapshotRun = (handle: FlowRun): void => {
|
|
555
|
-
set({ run: { ...handle.run, nodeStates: { ...handle.run.nodeStates } } });
|
|
556
|
-
};
|
|
557
|
-
|
|
558
578
|
/** Whether the selected environment is protected (writes are expensive there). */
|
|
559
579
|
const selectedEnvProtected = (): boolean => {
|
|
560
580
|
const s = get();
|
|
@@ -594,126 +614,143 @@ export const useBuilderStore = create<BuilderState>((set, get) => {
|
|
|
594
614
|
get().selectEnvironment(preferred);
|
|
595
615
|
};
|
|
596
616
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
latestAt = st.completedAt;
|
|
605
|
-
latest = id;
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
if (latest) set({ selectedNodeId: latest });
|
|
617
|
+
const reportRunError = (message: string): void => {
|
|
618
|
+
set((s) => ({
|
|
619
|
+
logs: [
|
|
620
|
+
...s.logs,
|
|
621
|
+
{ timestamp: new Date().toISOString(), level: 'error', runId: 'runner', message },
|
|
622
|
+
],
|
|
623
|
+
}));
|
|
609
624
|
};
|
|
610
625
|
|
|
611
626
|
/**
|
|
612
|
-
*
|
|
613
|
-
*
|
|
614
|
-
*
|
|
615
|
-
*
|
|
616
|
-
*
|
|
617
|
-
*
|
|
627
|
+
* Execution stepped INTO a Subflow node: stash the current level and show
|
|
628
|
+
* the child flow with a fresh synthetic run (status running, no node states
|
|
629
|
+
* yet — the child's first node executes on the NEXT step and lights up as
|
|
630
|
+
* its SSE states arrive). Logs are the root run's and keep streaming; the
|
|
631
|
+
* server run id and step mode are untouched. If the child flow isn't in the
|
|
632
|
+
* workspace (shouldn't happen for project flows), stay on the parent but
|
|
633
|
+
* push a placeholder level so stack depths stay aligned with the server.
|
|
618
634
|
*/
|
|
619
|
-
const
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
635
|
+
const enterDrill = (entered: { workflowId: string; nodeId: string }): void => {
|
|
636
|
+
const s = get();
|
|
637
|
+
const child = s.shelf.find((f) => f.id === entered.workflowId);
|
|
638
|
+
if (!child || child.id === s.flow.id) {
|
|
639
|
+
// Unknown child — keep stepping the parent view rather than crash, but
|
|
640
|
+
// still push a PLACEHOLDER level: the server's drill stack grew, and
|
|
641
|
+
// the client's must stay depth-aligned so the matching `exited` pops
|
|
642
|
+
// the right level (and deeper enters keep routing correctly).
|
|
643
|
+
hydrating(() =>
|
|
644
|
+
set({
|
|
645
|
+
stepDrill: [
|
|
646
|
+
...s.stepDrill,
|
|
647
|
+
{
|
|
648
|
+
workflowId: entered.workflowId,
|
|
649
|
+
viaNodeId: entered.nodeId,
|
|
650
|
+
savedFlow: s.flow,
|
|
651
|
+
savedRun: s.run,
|
|
652
|
+
savedSelectedNodeId: s.selectedNodeId,
|
|
653
|
+
placeholder: true,
|
|
654
|
+
},
|
|
655
|
+
],
|
|
656
|
+
drillPeek: null,
|
|
657
|
+
}),
|
|
658
|
+
);
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
const runId = s.activeServerRunId ?? s.run?.id;
|
|
662
|
+
if (!runId) return;
|
|
663
|
+
// Same id as the root run: child states stream on the root run's SSE, and
|
|
664
|
+
// the visible-run guard in onNodeState keys off this id.
|
|
665
|
+
const childRun: WorkflowRun = {
|
|
666
|
+
id: runId,
|
|
667
|
+
workflowId: child.id,
|
|
668
|
+
status: 'running',
|
|
669
|
+
startedAt: new Date().toISOString(),
|
|
670
|
+
nodeStates: {},
|
|
653
671
|
};
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
const existing = get().activeRun;
|
|
661
|
-
if (existing && existing.run.status === 'running') return existing;
|
|
662
|
-
const mockRun = get().runnerMock;
|
|
663
|
-
set({ activeRunMock: mockRun });
|
|
664
|
-
try {
|
|
665
|
-
const handle = startRun({
|
|
666
|
-
flow: get().flow,
|
|
667
|
-
registry: get().registry,
|
|
668
|
-
trace: get().trace,
|
|
669
|
-
pins: pinsOf(get().flow),
|
|
670
|
-
input,
|
|
671
|
-
// Browser-engine runs honour Mock mode the same way the runner does:
|
|
672
|
-
// op-level mocks under any scenario's own map, fail-loud at infra.
|
|
673
|
-
...(mockRun ? { mockRun: true, mocks: { ...get().flow.mocks, ...scenarioMocks } } : {}),
|
|
674
|
-
// Browser execution has no env file: it runs as environment 'browser'
|
|
675
|
-
// (empty secrets/vars) but still honours safe mode over HTTP.
|
|
676
|
-
environment: 'browser',
|
|
677
|
-
safeMode: get().safeMode,
|
|
678
|
-
subflowRunner: makeSubflowRunner([get().flow.id]),
|
|
679
|
-
events: {
|
|
680
|
-
onNodeStateChange: () => snapshotRun(handle),
|
|
681
|
-
onLog: (line) => set((s) => ({ logs: [...s.logs, line] })),
|
|
682
|
-
onRunFinished: () => {
|
|
683
|
-
snapshotRun(handle);
|
|
684
|
-
get().recordRun({ ...handle.run, nodeStates: { ...handle.run.nodeStates } });
|
|
685
|
-
set({ activeRun: null });
|
|
686
|
-
revertUnsafeAfterRun();
|
|
687
|
-
},
|
|
688
|
-
},
|
|
689
|
-
});
|
|
690
|
-
set({ activeRun: handle, logs: [] });
|
|
691
|
-
snapshotRun(handle);
|
|
692
|
-
return handle;
|
|
693
|
-
} catch (err) {
|
|
694
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
695
|
-
set((s) => ({
|
|
696
|
-
logs: [
|
|
697
|
-
...s.logs,
|
|
672
|
+
// hydrating: a drill swap is a view change, not an edit — it must not
|
|
673
|
+
// trigger the autosave that a `flow` reference change normally implies.
|
|
674
|
+
hydrating(() =>
|
|
675
|
+
set({
|
|
676
|
+
stepDrill: [
|
|
677
|
+
...s.stepDrill,
|
|
698
678
|
{
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
679
|
+
workflowId: child.id,
|
|
680
|
+
viaNodeId: entered.nodeId,
|
|
681
|
+
savedFlow: s.flow,
|
|
682
|
+
savedRun: s.run,
|
|
683
|
+
savedSelectedNodeId: s.selectedNodeId,
|
|
703
684
|
},
|
|
704
685
|
],
|
|
705
|
-
|
|
706
|
-
|
|
686
|
+
drillPeek: null,
|
|
687
|
+
flow: child,
|
|
688
|
+
run: childRun,
|
|
689
|
+
selectedNodeId: null,
|
|
690
|
+
}),
|
|
691
|
+
);
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
/** The drilled child completed: pop one level and restore the parent's
|
|
695
|
+
* flow/run/selection (its run kept receiving its own routed SSE states —
|
|
696
|
+
* including the Subflow node's terminal state — while we were inside). */
|
|
697
|
+
const exitDrill = (): void => {
|
|
698
|
+
const s = get();
|
|
699
|
+
const entry = s.stepDrill[s.stepDrill.length - 1];
|
|
700
|
+
if (!entry) return;
|
|
701
|
+
if (entry.placeholder) {
|
|
702
|
+
// The view never left the parent for this level — just drop it. The
|
|
703
|
+
// live flow/run kept receiving the parent's states while "inside".
|
|
704
|
+
hydrating(() => set({ stepDrill: s.stepDrill.slice(0, -1), drillPeek: null }));
|
|
705
|
+
return;
|
|
707
706
|
}
|
|
707
|
+
hydrating(() =>
|
|
708
|
+
set({
|
|
709
|
+
stepDrill: s.stepDrill.slice(0, -1),
|
|
710
|
+
drillPeek: null,
|
|
711
|
+
flow: entry.savedFlow,
|
|
712
|
+
run: entry.savedRun,
|
|
713
|
+
selectedNodeId: entry.savedSelectedNodeId,
|
|
714
|
+
}),
|
|
715
|
+
);
|
|
708
716
|
};
|
|
709
717
|
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
718
|
+
/** Unwind the whole drill stack back to the root level (run finish/reset/
|
|
719
|
+
* flow switch): restore the root's flow/run/selection and drop the stack. */
|
|
720
|
+
const drainDrill = (): void => {
|
|
721
|
+
const s = get();
|
|
722
|
+
if (s.stepDrill.length === 0) {
|
|
723
|
+
if (s.drillPeek !== null) set({ drillPeek: null });
|
|
724
|
+
return;
|
|
725
|
+
}
|
|
726
|
+
// All-placeholder stack: the view never left the root, and the live run
|
|
727
|
+
// (not the stashes) kept receiving its states — just drop the levels.
|
|
728
|
+
if (s.stepDrill.every((d) => d.placeholder)) {
|
|
729
|
+
hydrating(() => set({ stepDrill: [], drillPeek: null }));
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
const root = s.stepDrill[0];
|
|
733
|
+
hydrating(() =>
|
|
734
|
+
set({
|
|
735
|
+
stepDrill: [],
|
|
736
|
+
drillPeek: null,
|
|
737
|
+
flow: root.savedFlow,
|
|
738
|
+
run: root.savedRun,
|
|
739
|
+
selectedNodeId: root.savedSelectedNodeId,
|
|
740
|
+
}),
|
|
741
|
+
);
|
|
742
|
+
};
|
|
743
|
+
|
|
744
|
+
/** Consume one step response: drive the drill stack from entered/exited
|
|
745
|
+
* markers and clear the live-run id when the run is done. `exited` can
|
|
746
|
+
* co-occur with `done` (child was the last node, or the child failed). */
|
|
747
|
+
const handleStepResult = (result: StepResult): void => {
|
|
748
|
+
// Order matters when both markers co-occur: a retrying Subflow node pops
|
|
749
|
+
// its failed child AND re-enters a fresh one in the same step — the pop
|
|
750
|
+
// must apply before the push, or the exit would pop the new level.
|
|
751
|
+
if (result.exited) exitDrill();
|
|
752
|
+
if (result.entered) enterDrill(result.entered);
|
|
753
|
+
if (result.done) set({ activeServerRunId: null });
|
|
717
754
|
};
|
|
718
755
|
|
|
719
756
|
/** Start a server-side run (if none live) and wire its SSE events into the store. */
|
|
@@ -742,18 +779,40 @@ export const useBuilderStore = create<BuilderState>((set, get) => {
|
|
|
742
779
|
};
|
|
743
780
|
set({ activeServerRunId: runId, run: initial, logs: [] });
|
|
744
781
|
subscribeServerRun(runId, {
|
|
745
|
-
onNodeState: (nodeId, state) => {
|
|
782
|
+
onNodeState: (workflowId, nodeId, state) => {
|
|
746
783
|
set((s) => {
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
784
|
+
const followed =
|
|
785
|
+
stepFollow && (state.status === 'succeeded' || state.status === 'failed');
|
|
786
|
+
// The level whose flow is loaded owns this event (deepest drilled
|
|
787
|
+
// level, or the root when not drilled) → apply to the live run.
|
|
788
|
+
if (s.flow.id === workflowId) {
|
|
789
|
+
if (!s.run || s.run.id !== runId) return {};
|
|
790
|
+
return {
|
|
791
|
+
run: { ...s.run, nodeStates: { ...s.run.nodeStates, [nodeId]: state } },
|
|
792
|
+
...(followed ? { selectedNodeId: nodeId } : {}),
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
// A stashed drill level owns it (e.g. the parent's Subflow node
|
|
796
|
+
// updating while we're inside the child) → apply to its stashed
|
|
797
|
+
// run so nothing is lost when that level is restored or peeked.
|
|
798
|
+
const idx = s.stepDrill.findIndex((d) => d.savedFlow.id === workflowId);
|
|
799
|
+
if (idx === -1) return {}; // unknown workflowId — ignore
|
|
800
|
+
const entry = s.stepDrill[idx];
|
|
801
|
+
if (!entry.savedRun || entry.savedRun.id !== runId) return {};
|
|
802
|
+
const savedRun = {
|
|
803
|
+
...entry.savedRun,
|
|
804
|
+
nodeStates: { ...entry.savedRun.nodeStates, [nodeId]: state },
|
|
805
|
+
};
|
|
806
|
+
return {
|
|
807
|
+
stepDrill: s.stepDrill.map((d, i) => (i === idx ? { ...d, savedRun } : d)),
|
|
808
|
+
};
|
|
753
809
|
});
|
|
754
810
|
},
|
|
755
811
|
onLog: (line) => set((s) => ({ logs: [...s.logs, line] })),
|
|
756
812
|
onFinished: (run, errorHandler) => {
|
|
813
|
+
// The final run is the ROOT run — unwind any remaining drill levels
|
|
814
|
+
// (cancel/failure can finish a run while drilled) before showing it.
|
|
815
|
+
drainDrill();
|
|
757
816
|
set({ run });
|
|
758
817
|
get().recordRun(run, errorHandler);
|
|
759
818
|
set({ activeServerRunId: null });
|
|
@@ -789,6 +848,7 @@ export const useBuilderStore = create<BuilderState>((set, get) => {
|
|
|
789
848
|
agentPanelOpen: false,
|
|
790
849
|
agentEnvSetup: false,
|
|
791
850
|
welcomeOpen: false,
|
|
851
|
+
createModal: null,
|
|
792
852
|
settingsOpen: false,
|
|
793
853
|
settingsFromWelcome: false,
|
|
794
854
|
setupStatus: null,
|
|
@@ -803,22 +863,26 @@ export const useBuilderStore = create<BuilderState>((set, get) => {
|
|
|
803
863
|
activeRunMock: false,
|
|
804
864
|
scenarioTestPending: null,
|
|
805
865
|
agentRun: null,
|
|
866
|
+
guidedTranscript: [],
|
|
806
867
|
buildingOperationId: null,
|
|
807
868
|
agentChoice: initialAgentChoice,
|
|
808
869
|
setAgentChoice(choice) {
|
|
809
870
|
set({ agentChoice: choice });
|
|
810
871
|
persistAgentChoice(choice);
|
|
811
872
|
},
|
|
812
|
-
executionMode: (() => {
|
|
813
|
-
const saved =
|
|
814
|
-
typeof localStorage !== 'undefined' ? localStorage.getItem('emberflow.mode') : null;
|
|
815
|
-
return saved === 'browser' || saved === 'server' ? saved : 'auto';
|
|
816
|
-
})(),
|
|
817
873
|
runnerOnline: null,
|
|
818
874
|
runnerMock: false,
|
|
819
875
|
workspaceSource: 'local',
|
|
820
876
|
activeServerRunId: null,
|
|
821
877
|
stepMode: false,
|
|
878
|
+
stepDrill: [],
|
|
879
|
+
drillPeek: null,
|
|
880
|
+
peekDrill(index) {
|
|
881
|
+
set((s) => {
|
|
882
|
+
if (index === null || s.stepDrill.length === 0) return { drillPeek: null };
|
|
883
|
+
return { drillPeek: index >= 0 && index < s.stepDrill.length ? index : null };
|
|
884
|
+
});
|
|
885
|
+
},
|
|
822
886
|
environments: [],
|
|
823
887
|
environmentsDefault: '',
|
|
824
888
|
selectedEnvironment: initialSelectedEnvironment,
|
|
@@ -906,18 +970,6 @@ export const useBuilderStore = create<BuilderState>((set, get) => {
|
|
|
906
970
|
return true;
|
|
907
971
|
},
|
|
908
972
|
|
|
909
|
-
setExecutionMode(mode) {
|
|
910
|
-
set({ executionMode: mode });
|
|
911
|
-
if (typeof localStorage !== 'undefined') localStorage.setItem('emberflow.mode', mode);
|
|
912
|
-
if (mode !== 'browser') void get().checkRunner();
|
|
913
|
-
},
|
|
914
|
-
|
|
915
|
-
effectiveMode() {
|
|
916
|
-
const s = get();
|
|
917
|
-
if (s.executionMode === 'auto') return s.runnerOnline ? 'server' : 'browser';
|
|
918
|
-
return s.executionMode;
|
|
919
|
-
},
|
|
920
|
-
|
|
921
973
|
async setServingMode(mode) {
|
|
922
974
|
await setServingModeOnServer(mode);
|
|
923
975
|
await get().checkRunner();
|
|
@@ -942,8 +994,8 @@ export const useBuilderStore = create<BuilderState>((set, get) => {
|
|
|
942
994
|
if (meta.length === 0) return;
|
|
943
995
|
const registry = get().registry;
|
|
944
996
|
for (const m of meta) {
|
|
945
|
-
const { source, ...definition } = m;
|
|
946
|
-
registry.registerDefinition(definition, source);
|
|
997
|
+
const { source, sourceRef, builtin, ...definition } = m;
|
|
998
|
+
registry.registerDefinition(definition, source, { sourceRef, builtin });
|
|
947
999
|
}
|
|
948
1000
|
// Force palette/inspector consumers to re-read the registry: a new
|
|
949
1001
|
// top-level reference (Zustand uses Object.is) sharing the same node data.
|
|
@@ -971,6 +1023,8 @@ export const useBuilderStore = create<BuilderState>((set, get) => {
|
|
|
971
1023
|
logs: [],
|
|
972
1024
|
activeRun: null,
|
|
973
1025
|
selectedNodeId: null,
|
|
1026
|
+
stepDrill: [],
|
|
1027
|
+
drillPeek: null,
|
|
974
1028
|
}),
|
|
975
1029
|
);
|
|
976
1030
|
},
|
|
@@ -1050,6 +1104,11 @@ export const useBuilderStore = create<BuilderState>((set, get) => {
|
|
|
1050
1104
|
|
|
1051
1105
|
async runAgent(intent, opts) {
|
|
1052
1106
|
const effectiveOpts = opts ?? get().agentChoice;
|
|
1107
|
+
// A guided-setup run is OWNED by the WelcomeDialog's embedded stream, so
|
|
1108
|
+
// it must NOT auto-open the right-hand AgentConsole panel. The `guided`
|
|
1109
|
+
// marker on the run slot drives both the suppression here and the dialog's
|
|
1110
|
+
// phase machine (which re-attaches to this same slot when reopened).
|
|
1111
|
+
const guided = intent.action === 'guided-setup';
|
|
1053
1112
|
// If a build is in flight, remember the op being built + the op ids that
|
|
1054
1113
|
// existed before the run. If the agent renames the op (the id changes),
|
|
1055
1114
|
// syncFromRunner can't find the old id and falls back to flows[0], leaving
|
|
@@ -1067,18 +1126,22 @@ export const useBuilderStore = create<BuilderState>((set, get) => {
|
|
|
1067
1126
|
id: '',
|
|
1068
1127
|
events: [{ type: 'error', text: err instanceof Error ? err.message : String(err) }],
|
|
1069
1128
|
status: 'error',
|
|
1129
|
+
guided,
|
|
1070
1130
|
},
|
|
1071
1131
|
// Open the panel so the failure is visible — without this, a start
|
|
1072
1132
|
// error (e.g. no agent CLI on PATH) lands in a closed panel and the
|
|
1073
|
-
// click appears to do nothing.
|
|
1074
|
-
|
|
1133
|
+
// click appears to do nothing. A guided run's failure surfaces in the
|
|
1134
|
+
// WelcomeDialog's own pane, so it stays closed.
|
|
1135
|
+
...(guided ? {} : { agentPanelOpen: true }),
|
|
1075
1136
|
});
|
|
1076
1137
|
return;
|
|
1077
1138
|
}
|
|
1078
1139
|
|
|
1079
1140
|
set({
|
|
1080
|
-
agentRun: { id: agentRunId, events: [], status: 'running', instruction: intent.instruction },
|
|
1081
|
-
|
|
1141
|
+
agentRun: { id: agentRunId, events: [], status: 'running', instruction: intent.instruction, guided },
|
|
1142
|
+
// The WelcomeDialog embeds the stream for guided runs — don't also pop
|
|
1143
|
+
// the right-hand console.
|
|
1144
|
+
...(guided ? {} : { agentPanelOpen: true }),
|
|
1082
1145
|
});
|
|
1083
1146
|
|
|
1084
1147
|
// Live canvas: while the agent works, poll the runner (GET /workflows reads
|
|
@@ -1210,18 +1273,55 @@ export const useBuilderStore = create<BuilderState>((set, get) => {
|
|
|
1210
1273
|
set({ agentPanelOpen: true, agentEnvSetup: true });
|
|
1211
1274
|
},
|
|
1212
1275
|
|
|
1213
|
-
beginInfrastructureScout() {
|
|
1276
|
+
beginInfrastructureScout(instruction) {
|
|
1277
|
+
// Defensive: callers wired as onClick handlers leak a MouseEvent as the
|
|
1278
|
+
// first arg — anything that isn't a string means "full rescan".
|
|
1279
|
+
const amendment = typeof instruction === 'string' ? instruction.trim() : '';
|
|
1214
1280
|
void get().runAgent({
|
|
1215
1281
|
action: 'scout-infrastructure',
|
|
1216
1282
|
instruction:
|
|
1217
|
-
|
|
1283
|
+
amendment && amendment.length > 0
|
|
1284
|
+
? amendment
|
|
1285
|
+
: "Scan this project's dependencies, config files, ORM schemas, env-var references and HTTP clients, and write emberflow/infrastructure.json describing the databases, APIs and providers it already uses.",
|
|
1218
1286
|
});
|
|
1219
1287
|
},
|
|
1220
1288
|
|
|
1289
|
+
beginGuidedSetup(instruction) {
|
|
1290
|
+
// onClick handlers leak a MouseEvent as the first arg — only a real string
|
|
1291
|
+
// is a continuation answer; anything else means "start with no notes".
|
|
1292
|
+
const notes = typeof instruction === 'string' ? instruction.trim() : '';
|
|
1293
|
+
const prior = get().agentRun;
|
|
1294
|
+
if (notes && prior?.guided) {
|
|
1295
|
+
// Continuation: the new run REPLACES the agentRun slot, so fold the
|
|
1296
|
+
// finished run's events plus the user's answer into the persistent
|
|
1297
|
+
// transcript — otherwise the pane wipes to "Thinking…" and the whole
|
|
1298
|
+
// conversation vanishes. Question blocks are stripped here (they were
|
|
1299
|
+
// answered; re-rendering them as raw fences would be noise).
|
|
1300
|
+
const stripped = prior.events.map((e) =>
|
|
1301
|
+
e.type === 'message' && e.text ? { ...e, text: extractGuidedQuestions(e.text).stripped } : e,
|
|
1302
|
+
);
|
|
1303
|
+
set((s) => ({
|
|
1304
|
+
guidedTranscript: [
|
|
1305
|
+
...s.guidedTranscript,
|
|
1306
|
+
...stripped,
|
|
1307
|
+
{ type: 'message', text: `**You:** ${notes}` },
|
|
1308
|
+
],
|
|
1309
|
+
}));
|
|
1310
|
+
} else if (!notes) {
|
|
1311
|
+
// Fresh start: a brand-new guided run begins a brand-new conversation.
|
|
1312
|
+
set({ guidedTranscript: [] });
|
|
1313
|
+
}
|
|
1314
|
+
void get().runAgent({ action: 'guided-setup', instruction: notes });
|
|
1315
|
+
},
|
|
1316
|
+
|
|
1221
1317
|
setWelcomeOpen(open) {
|
|
1222
1318
|
set({ welcomeOpen: open });
|
|
1223
1319
|
},
|
|
1224
1320
|
|
|
1321
|
+
setCreateModal(state) {
|
|
1322
|
+
set({ createModal: state });
|
|
1323
|
+
},
|
|
1324
|
+
|
|
1225
1325
|
setSettingsOpen(open) {
|
|
1226
1326
|
set(open ? { settingsOpen: true } : { settingsOpen: false, settingsFromWelcome: false });
|
|
1227
1327
|
},
|
|
@@ -1328,6 +1428,9 @@ export const useBuilderStore = create<BuilderState>((set, get) => {
|
|
|
1328
1428
|
},
|
|
1329
1429
|
|
|
1330
1430
|
switchWorkflow(id) {
|
|
1431
|
+
// Switching flows while drilled would strand the stashed parent — put
|
|
1432
|
+
// the root level back first so flow/shelf are consistent again.
|
|
1433
|
+
drainDrill();
|
|
1331
1434
|
let switched = false;
|
|
1332
1435
|
set((s) => {
|
|
1333
1436
|
if (s.flow.id === id) return {};
|
|
@@ -1590,57 +1693,45 @@ export const useBuilderStore = create<BuilderState>((set, get) => {
|
|
|
1590
1693
|
const s = get();
|
|
1591
1694
|
const node = s.flow.nodes.find((n) => n.id === nodeId);
|
|
1592
1695
|
if (!node) return { error: `Unknown node: ${nodeId}`, logs: [] };
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
const { implementation } = s.registry.get(node.type);
|
|
1597
|
-
const logs: LogLine[] = [];
|
|
1598
|
-
const runId = `isolated-${crypto.randomUUID().slice(0, 8)}`;
|
|
1599
|
-
const log = (level: LogLine['level'], message: string): void => {
|
|
1600
|
-
logs.push({
|
|
1601
|
-
timestamp: new Date().toISOString(),
|
|
1602
|
-
level,
|
|
1603
|
-
runId,
|
|
1604
|
-
nodeId: node.id,
|
|
1605
|
-
nodeLabel: node.label,
|
|
1606
|
-
message,
|
|
1607
|
-
});
|
|
1608
|
-
};
|
|
1696
|
+
// Isolated node runs execute on the runner (POST /node-run) against the
|
|
1697
|
+
// selected environment's secrets/vars, honouring safe mode — the studio
|
|
1698
|
+
// bundles no implementations. Runner offline → surface the offline notice.
|
|
1609
1699
|
const startedAt = new Date().toISOString();
|
|
1610
|
-
|
|
1611
|
-
s.trace.record({
|
|
1612
|
-
id: crypto.randomUUID(),
|
|
1613
|
-
workflowId: s.flow.id,
|
|
1614
|
-
runId,
|
|
1615
|
-
nodeId: node.id,
|
|
1616
|
-
nodeType: node.type,
|
|
1617
|
-
nodeLabel: node.label,
|
|
1618
|
-
input,
|
|
1619
|
-
output,
|
|
1620
|
-
status,
|
|
1621
|
-
startedAt,
|
|
1622
|
-
completedAt: new Date().toISOString(),
|
|
1623
|
-
});
|
|
1624
|
-
};
|
|
1700
|
+
let result: { output?: unknown; error?: string; logs: LogLine[] };
|
|
1625
1701
|
try {
|
|
1626
|
-
|
|
1702
|
+
result = await runNodeOnServer({
|
|
1703
|
+
type: node.type,
|
|
1627
1704
|
input,
|
|
1628
|
-
config: node.config,
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
safeMode:
|
|
1632
|
-
runInput: {},
|
|
1633
|
-
log,
|
|
1634
|
-
runSubflow: (workflowId, subInput) =>
|
|
1635
|
-
makeSubflowRunner([s.flow.id])(workflowId, subInput, node.id),
|
|
1705
|
+
config: node.config ?? {},
|
|
1706
|
+
...(s.selectedEnvironment ? { environment: s.selectedEnvironment } : {}),
|
|
1707
|
+
safeMode: s.safeMode,
|
|
1708
|
+
...(selectedEnvProtected() && !s.safeMode ? { confirm: s.selectedEnvironment } : {}),
|
|
1636
1709
|
});
|
|
1637
|
-
sample('succeeded', output);
|
|
1638
|
-
return { output, logs };
|
|
1639
1710
|
} catch (err) {
|
|
1640
|
-
const
|
|
1641
|
-
|
|
1642
|
-
|
|
1711
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1712
|
+
return {
|
|
1713
|
+
error: get().runnerOnline === false
|
|
1714
|
+
? 'Runner offline — start it with `npx emberflow dev`, then run this node again.'
|
|
1715
|
+
: message,
|
|
1716
|
+
logs: [],
|
|
1717
|
+
};
|
|
1643
1718
|
}
|
|
1719
|
+
// Record a local trace sample so the modal's "previous executions" list
|
|
1720
|
+
// and pin affordance keep working alongside runner-recorded samples.
|
|
1721
|
+
s.trace.record({
|
|
1722
|
+
id: crypto.randomUUID(),
|
|
1723
|
+
workflowId: s.flow.id,
|
|
1724
|
+
runId: `isolated-${crypto.randomUUID().slice(0, 8)}`,
|
|
1725
|
+
nodeId: node.id,
|
|
1726
|
+
nodeType: node.type,
|
|
1727
|
+
nodeLabel: node.label,
|
|
1728
|
+
input,
|
|
1729
|
+
output: result.output,
|
|
1730
|
+
status: result.error ? 'failed' : 'succeeded',
|
|
1731
|
+
startedAt,
|
|
1732
|
+
completedAt: new Date().toISOString(),
|
|
1733
|
+
});
|
|
1734
|
+
return { output: result.output, error: result.error, logs: result.logs ?? [] };
|
|
1644
1735
|
},
|
|
1645
1736
|
|
|
1646
1737
|
async testWorkflow(flowId, environment) {
|
|
@@ -1662,12 +1753,7 @@ export const useBuilderStore = create<BuilderState>((set, get) => {
|
|
|
1662
1753
|
get().resetRun();
|
|
1663
1754
|
set({ stepMode: false });
|
|
1664
1755
|
set({ activeScenarioId: scenarioId });
|
|
1665
|
-
|
|
1666
|
-
await ensureServerRun('run', scenario.input, scenario.name);
|
|
1667
|
-
return;
|
|
1668
|
-
}
|
|
1669
|
-
const handle = ensureRun(scenario.input, scenario.mocks);
|
|
1670
|
-
if (handle) await handle.runToEnd();
|
|
1756
|
+
await ensureServerRun('run', scenario.input, scenario.name);
|
|
1671
1757
|
},
|
|
1672
1758
|
|
|
1673
1759
|
async stepScenario(scenarioId) {
|
|
@@ -1677,22 +1763,13 @@ export const useBuilderStore = create<BuilderState>((set, get) => {
|
|
|
1677
1763
|
get().resetRun();
|
|
1678
1764
|
set({ stepMode: true });
|
|
1679
1765
|
set({ activeScenarioId: scenarioId });
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
} catch (err) {
|
|
1687
|
-
reportRunError(err instanceof Error ? err.message : String(err));
|
|
1688
|
-
}
|
|
1766
|
+
const runId = await ensureServerRun('step', scenario.input, scenario.name);
|
|
1767
|
+
if (runId) {
|
|
1768
|
+
try {
|
|
1769
|
+
handleStepResult(await stepServerRun(runId));
|
|
1770
|
+
} catch (err) {
|
|
1771
|
+
reportRunError(err instanceof Error ? err.message : String(err));
|
|
1689
1772
|
}
|
|
1690
|
-
return;
|
|
1691
|
-
}
|
|
1692
|
-
const handle = ensureRun(scenario.input, scenario.mocks);
|
|
1693
|
-
if (handle) {
|
|
1694
|
-
await handle.step();
|
|
1695
|
-
selectLatestCompleted();
|
|
1696
1773
|
}
|
|
1697
1774
|
},
|
|
1698
1775
|
|
|
@@ -1740,53 +1817,47 @@ export const useBuilderStore = create<BuilderState>((set, get) => {
|
|
|
1740
1817
|
async stepRun() {
|
|
1741
1818
|
// A plain step/run is not scenario-driven; only tag history for runs
|
|
1742
1819
|
// started via runScenario.
|
|
1743
|
-
if (!get().
|
|
1820
|
+
if (!get().activeServerRunId) set({ activeScenarioId: null });
|
|
1744
1821
|
stepFollow = true;
|
|
1745
1822
|
set({ stepMode: true, runConsoleDismissedId: null });
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
} catch (err) {
|
|
1753
|
-
reportRunError(err instanceof Error ? err.message : String(err));
|
|
1754
|
-
}
|
|
1823
|
+
const runId = await ensureServerRun('step');
|
|
1824
|
+
if (runId) {
|
|
1825
|
+
try {
|
|
1826
|
+
handleStepResult(await stepServerRun(runId));
|
|
1827
|
+
} catch (err) {
|
|
1828
|
+
reportRunError(err instanceof Error ? err.message : String(err));
|
|
1755
1829
|
}
|
|
1756
|
-
return;
|
|
1757
|
-
}
|
|
1758
|
-
const handle = ensureRun();
|
|
1759
|
-
if (handle) {
|
|
1760
|
-
await handle.step();
|
|
1761
|
-
selectLatestCompleted();
|
|
1762
1830
|
}
|
|
1763
1831
|
},
|
|
1764
1832
|
|
|
1765
1833
|
async runToEnd() {
|
|
1766
1834
|
stepFollow = false;
|
|
1767
1835
|
set({ stepMode: false, runConsoleDismissedId: null });
|
|
1768
|
-
if (!get().
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1836
|
+
if (!get().activeServerRunId) set({ activeScenarioId: null });
|
|
1837
|
+
const existing = get().activeServerRunId;
|
|
1838
|
+
if (existing) {
|
|
1839
|
+
// A stepped run is already live on the runner — walk it to the end.
|
|
1840
|
+
// Drill markers still apply (entered/exited keep the view balanced;
|
|
1841
|
+
// handleStepResult clears activeServerRunId on done).
|
|
1842
|
+
try {
|
|
1843
|
+
let done = false;
|
|
1844
|
+
while (!done) {
|
|
1845
|
+
const result = await stepServerRun(existing);
|
|
1846
|
+
handleStepResult(result);
|
|
1847
|
+
done = result.done;
|
|
1779
1848
|
}
|
|
1780
|
-
|
|
1849
|
+
} catch (err) {
|
|
1850
|
+
reportRunError(err instanceof Error ? err.message : String(err));
|
|
1781
1851
|
}
|
|
1782
|
-
await ensureServerRun('run');
|
|
1783
1852
|
return;
|
|
1784
1853
|
}
|
|
1785
|
-
|
|
1786
|
-
if (handle) await handle.runToEnd();
|
|
1854
|
+
await ensureServerRun('run');
|
|
1787
1855
|
},
|
|
1788
1856
|
|
|
1789
1857
|
resetRun() {
|
|
1858
|
+
// Unwind any subflow drill first so the root flow is back on the canvas
|
|
1859
|
+
// before the run state clears.
|
|
1860
|
+
drainDrill();
|
|
1790
1861
|
get().activeRun?.cancel();
|
|
1791
1862
|
const serverRunId = get().activeServerRunId;
|
|
1792
1863
|
if (serverRunId) void cancelServerRun(serverRunId);
|