@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,8 +1,13 @@
|
|
|
1
1
|
import { useEffect, useState } from 'react';
|
|
2
|
-
import { ListChecksIcon, Loader2Icon, ShieldIcon } from 'lucide-react';
|
|
2
|
+
import { CheckIcon, ChevronUpIcon, DownloadIcon, ListChecksIcon, Loader2Icon, ServerIcon, ShieldIcon } from 'lucide-react';
|
|
3
3
|
import { cn } from '@/lib/utils';
|
|
4
|
+
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
|
4
5
|
import { useBuilderStore } from '../store/builderStore';
|
|
5
6
|
import { setupProgress } from '../store/setupClient';
|
|
7
|
+
import { fetchInfrastructure, type InfrastructureResponse } from '../store/infraClient';
|
|
8
|
+
import { fetchUpdateStatus, postUpdate, type UpdateStatus } from '../store/updateClient';
|
|
9
|
+
import { InfrastructureDialog } from './InfrastructureDialog';
|
|
10
|
+
import { CopyCommand } from './WelcomeDialog';
|
|
6
11
|
import type { EnvironmentSummary } from '../store/serverRunner';
|
|
7
12
|
import type { WorkflowRun } from '../engine';
|
|
8
13
|
|
|
@@ -115,6 +120,100 @@ function EnvAuthBadge({ env }: { env: EnvironmentSummary }) {
|
|
|
115
120
|
);
|
|
116
121
|
}
|
|
117
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Package update chip: rendered only when the runner reports a newer published
|
|
125
|
+
* version. Highlight/ember tint (not warn) — an update is good news, not a
|
|
126
|
+
* problem. The popover walks the whole one-click flow: install → restart
|
|
127
|
+
* command on success, manual npm command on failure. Props-driven (the bar
|
|
128
|
+
* owns the fetch) so the component test can render it directly.
|
|
129
|
+
*/
|
|
130
|
+
export function UpdateChip({ status }: { status: UpdateStatus | null }) {
|
|
131
|
+
const [open, setOpen] = useState(false);
|
|
132
|
+
const [phase, setPhase] = useState<'idle' | 'installing' | 'done' | 'error'>('idle');
|
|
133
|
+
const [error, setError] = useState<string | null>(null);
|
|
134
|
+
if (!status?.updateAvailable || !status.latest) return null;
|
|
135
|
+
|
|
136
|
+
const onInstall = async () => {
|
|
137
|
+
setPhase('installing');
|
|
138
|
+
setError(null);
|
|
139
|
+
const result = await postUpdate();
|
|
140
|
+
if (result.ok) {
|
|
141
|
+
setPhase('done');
|
|
142
|
+
} else {
|
|
143
|
+
setError(result.error ?? 'Install failed');
|
|
144
|
+
setPhase('error');
|
|
145
|
+
}
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
return (
|
|
149
|
+
<>
|
|
150
|
+
<Divider />
|
|
151
|
+
<Popover open={open} onOpenChange={setOpen}>
|
|
152
|
+
<PopoverTrigger asChild>
|
|
153
|
+
<button
|
|
154
|
+
type="button"
|
|
155
|
+
title={`Update available — ${status.current} → ${status.latest}`}
|
|
156
|
+
className={cn(segment, interactive, 'bg-highlight/15 font-medium text-highlight')}
|
|
157
|
+
>
|
|
158
|
+
<DownloadIcon className="size-3 shrink-0" />
|
|
159
|
+
update
|
|
160
|
+
</button>
|
|
161
|
+
</PopoverTrigger>
|
|
162
|
+
<PopoverContent side="top" align="start" className="w-72 p-3">
|
|
163
|
+
<div className="flex items-center gap-2">
|
|
164
|
+
<span className="size-1.5 shrink-0 rounded-full bg-highlight" />
|
|
165
|
+
<span className="text-[12px] font-medium">
|
|
166
|
+
Update available — {status.current} → {status.latest}
|
|
167
|
+
</span>
|
|
168
|
+
</div>
|
|
169
|
+
{phase === 'done' ? (
|
|
170
|
+
<>
|
|
171
|
+
<p className="mt-1.5 text-[11.5px] leading-snug text-muted-foreground">
|
|
172
|
+
Installed — restart the runner to finish:
|
|
173
|
+
</p>
|
|
174
|
+
<CopyCommand command="npx emberflow dev" />
|
|
175
|
+
</>
|
|
176
|
+
) : (
|
|
177
|
+
<>
|
|
178
|
+
<p className="mt-1.5 text-[11.5px] leading-snug text-muted-foreground">
|
|
179
|
+
New features and fixes for the studio and runner.
|
|
180
|
+
</p>
|
|
181
|
+
{phase === 'error' && error && (
|
|
182
|
+
<>
|
|
183
|
+
<p className="mt-1.5 truncate text-[11.5px] leading-snug text-destructive" title={error}>
|
|
184
|
+
{error}
|
|
185
|
+
</p>
|
|
186
|
+
<p className="mt-1.5 text-[11.5px] leading-snug text-muted-foreground">
|
|
187
|
+
Install manually instead:
|
|
188
|
+
</p>
|
|
189
|
+
<CopyCommand command="npm install @xdelivered/emberflow@latest" />
|
|
190
|
+
</>
|
|
191
|
+
)}
|
|
192
|
+
<button
|
|
193
|
+
type="button"
|
|
194
|
+
onClick={() => void onInstall()}
|
|
195
|
+
disabled={phase === 'installing'}
|
|
196
|
+
className="mt-2.5 flex items-center gap-1.5 rounded-md bg-highlight px-2.5 py-1 text-[11.5px] font-medium text-highlight-foreground transition-colors hover:bg-highlight/90 disabled:cursor-wait disabled:opacity-60"
|
|
197
|
+
>
|
|
198
|
+
{phase === 'installing' ? (
|
|
199
|
+
<>
|
|
200
|
+
<Loader2Icon className="size-3 shrink-0 animate-spin" />
|
|
201
|
+
Installing…
|
|
202
|
+
</>
|
|
203
|
+
) : phase === 'error' ? (
|
|
204
|
+
'Retry install'
|
|
205
|
+
) : (
|
|
206
|
+
'Install update'
|
|
207
|
+
)}
|
|
208
|
+
</button>
|
|
209
|
+
</>
|
|
210
|
+
)}
|
|
211
|
+
</PopoverContent>
|
|
212
|
+
</Popover>
|
|
213
|
+
</>
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
118
217
|
/**
|
|
119
218
|
* Bottom status bar: a calm, always-present readout of where a run points and
|
|
120
219
|
* what's happening. Left = environment / runner / workspace source (interactive
|
|
@@ -136,6 +235,42 @@ export function StatusBar() {
|
|
|
136
235
|
const run = useBuilderStore((s) => s.run);
|
|
137
236
|
const setupStatus = useBuilderStore((s) => s.setupStatus);
|
|
138
237
|
const setWelcomeOpen = useBuilderStore((s) => s.setWelcomeOpen);
|
|
238
|
+
const agentRun = useBuilderStore((s) => s.agentRun);
|
|
239
|
+
|
|
240
|
+
// Infrastructure chip: fetch on mount and whenever an agent run finishes (a
|
|
241
|
+
// completed scout writes the manifest) — mirroring InfraTab's refetch. Shown
|
|
242
|
+
// only once scouted; the welcome checklist owns the not-scouted path.
|
|
243
|
+
const [infraData, setInfraData] = useState<InfrastructureResponse | null>(null);
|
|
244
|
+
const [infraOpen, setInfraOpen] = useState(false);
|
|
245
|
+
const [registerOpen, setRegisterOpen] = useState(false);
|
|
246
|
+
const [mockOpen, setMockOpen] = useState(false);
|
|
247
|
+
const agentRunStatus = agentRun?.status;
|
|
248
|
+
useEffect(() => {
|
|
249
|
+
if (agentRunStatus === 'running') return;
|
|
250
|
+
let cancelled = false;
|
|
251
|
+
void fetchInfrastructure().then((d) => {
|
|
252
|
+
if (!cancelled) setInfraData(d);
|
|
253
|
+
});
|
|
254
|
+
return () => {
|
|
255
|
+
cancelled = true;
|
|
256
|
+
};
|
|
257
|
+
}, [agentRunStatus]);
|
|
258
|
+
// Scouted (present:true) → show the chip, even for a greenfield 0-item
|
|
259
|
+
// manifest; only the not-scouted path is hidden (owned by the checklist).
|
|
260
|
+
const infraCount = infraData && infraData.present ? infraData.manifest.items.length : null;
|
|
261
|
+
|
|
262
|
+
// Update notifier: one check per studio load — the runner caches the
|
|
263
|
+
// registry answer for an hour anyway, so polling would add nothing.
|
|
264
|
+
const [updateStatus, setUpdateStatus] = useState<UpdateStatus | null>(null);
|
|
265
|
+
useEffect(() => {
|
|
266
|
+
let cancelled = false;
|
|
267
|
+
void fetchUpdateStatus().then((s) => {
|
|
268
|
+
if (!cancelled) setUpdateStatus(s);
|
|
269
|
+
});
|
|
270
|
+
return () => {
|
|
271
|
+
cancelled = true;
|
|
272
|
+
};
|
|
273
|
+
}, []);
|
|
139
274
|
|
|
140
275
|
// Setup entry point: a quiet progress chip while onboarding is unfinished.
|
|
141
276
|
// Complete setups drop the chip — the checklist auto-open and this chip are
|
|
@@ -145,7 +280,7 @@ export function StatusBar() {
|
|
|
145
280
|
|
|
146
281
|
const current = environments.find((e) => e.name === selectedEnvironment);
|
|
147
282
|
const protectedEnv = current?.protected ?? false;
|
|
148
|
-
const envLabel = selectedEnvironment || '
|
|
283
|
+
const envLabel = selectedEnvironment || (runnerOnline === false ? 'offline' : 'default');
|
|
149
284
|
|
|
150
285
|
const onEnvClick = () => {
|
|
151
286
|
if (!safeMode) {
|
|
@@ -196,13 +331,44 @@ export function StatusBar() {
|
|
|
196
331
|
{runnerMock && (
|
|
197
332
|
<>
|
|
198
333
|
<Divider />
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
>
|
|
203
|
-
<
|
|
204
|
-
|
|
205
|
-
|
|
334
|
+
{/* Discrete explainer: mock is the mode every new project starts in,
|
|
335
|
+
so the chip opens a small card saying what it means and how to
|
|
336
|
+
leave it — a tooltip alone was too easy to never discover. */}
|
|
337
|
+
<Popover open={mockOpen} onOpenChange={setMockOpen}>
|
|
338
|
+
<PopoverTrigger asChild>
|
|
339
|
+
<button
|
|
340
|
+
type="button"
|
|
341
|
+
className={cn(segment, interactive, 'bg-warn/15 font-medium uppercase tracking-wide text-warn')}
|
|
342
|
+
>
|
|
343
|
+
<span className="size-1.5 shrink-0 rounded-full bg-warn" />
|
|
344
|
+
mock
|
|
345
|
+
</button>
|
|
346
|
+
</PopoverTrigger>
|
|
347
|
+
<PopoverContent side="top" align="start" className="w-72 p-3">
|
|
348
|
+
<div className="flex items-center gap-2">
|
|
349
|
+
<span className="size-1.5 shrink-0 rounded-full bg-warn" />
|
|
350
|
+
<span className="text-[12px] font-medium">Mock mode</span>
|
|
351
|
+
</div>
|
|
352
|
+
<p className="mt-1.5 text-[11.5px] leading-snug text-muted-foreground">
|
|
353
|
+
Runs answer with the example responses from your scenarios. Nothing real executes —
|
|
354
|
+
no databases, no external calls, no auth.
|
|
355
|
+
</p>
|
|
356
|
+
<p className="mt-1.5 text-[11.5px] leading-snug text-muted-foreground">
|
|
357
|
+
That makes it safe to run anything while you build. When you're ready, point runs at
|
|
358
|
+
a real environment.
|
|
359
|
+
</p>
|
|
360
|
+
<button
|
|
361
|
+
type="button"
|
|
362
|
+
onClick={() => {
|
|
363
|
+
setMockOpen(false);
|
|
364
|
+
onEnvClick();
|
|
365
|
+
}}
|
|
366
|
+
className="mt-2.5 text-[11.5px] font-medium text-highlight underline-offset-2 hover:underline"
|
|
367
|
+
>
|
|
368
|
+
Go live — pick an environment
|
|
369
|
+
</button>
|
|
370
|
+
</PopoverContent>
|
|
371
|
+
</Popover>
|
|
206
372
|
</>
|
|
207
373
|
)}
|
|
208
374
|
<Divider />
|
|
@@ -246,28 +412,62 @@ export function StatusBar() {
|
|
|
246
412
|
</button>
|
|
247
413
|
</>
|
|
248
414
|
)}
|
|
415
|
+
<UpdateChip status={updateStatus} />
|
|
416
|
+
{infraCount !== null && (
|
|
417
|
+
<>
|
|
418
|
+
<Divider />
|
|
419
|
+
<button
|
|
420
|
+
type="button"
|
|
421
|
+
onClick={() => setInfraOpen(true)}
|
|
422
|
+
title="Project infrastructure — what the scout found that agents reuse"
|
|
423
|
+
className={cn(segment, interactive)}
|
|
424
|
+
>
|
|
425
|
+
<ServerIcon className="size-3 shrink-0" />
|
|
426
|
+
<span className="truncate text-foreground/80">infra</span>
|
|
427
|
+
<span className="shrink-0 font-mono tabular-nums text-muted-foreground/70">{infraCount}</span>
|
|
428
|
+
</button>
|
|
429
|
+
</>
|
|
430
|
+
)}
|
|
249
431
|
<Divider />
|
|
250
|
-
<
|
|
251
|
-
|
|
252
|
-
aria-label="Runbook register"
|
|
253
|
-
title="Runbook register — Simple reads outcomes; Technical adds type names + trace badges"
|
|
254
|
-
className="flex h-full items-center gap-0.5 px-1"
|
|
255
|
-
>
|
|
256
|
-
{(['simple', 'technical'] as const).map((v) => (
|
|
432
|
+
<Popover open={registerOpen} onOpenChange={setRegisterOpen}>
|
|
433
|
+
<PopoverTrigger asChild>
|
|
257
434
|
<button
|
|
258
|
-
key={v}
|
|
259
435
|
type="button"
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
className={cn(
|
|
263
|
-
'rounded-[4px] px-1.5 py-0.5 text-[11px] capitalize transition-colors',
|
|
264
|
-
register === v ? 'bg-secondary text-foreground' : 'text-muted-foreground hover:text-foreground',
|
|
265
|
-
)}
|
|
436
|
+
title="Detail level — Simple shows outcomes; Technical adds types and traces"
|
|
437
|
+
className={cn(segment, interactive)}
|
|
266
438
|
>
|
|
267
|
-
{
|
|
439
|
+
<span className="truncate capitalize text-foreground/80">{register}</span>
|
|
440
|
+
<ChevronUpIcon className="size-3 shrink-0 text-muted-foreground/70" />
|
|
268
441
|
</button>
|
|
269
|
-
|
|
270
|
-
|
|
442
|
+
</PopoverTrigger>
|
|
443
|
+
<PopoverContent
|
|
444
|
+
side="top"
|
|
445
|
+
align="start"
|
|
446
|
+
className="w-40 p-1"
|
|
447
|
+
role="radiogroup"
|
|
448
|
+
aria-label="Runbook register"
|
|
449
|
+
>
|
|
450
|
+
{(['simple', 'technical'] as const).map((v) => (
|
|
451
|
+
<button
|
|
452
|
+
key={v}
|
|
453
|
+
type="button"
|
|
454
|
+
role="radio"
|
|
455
|
+
aria-checked={register === v}
|
|
456
|
+
onClick={() => {
|
|
457
|
+
setRegister(v);
|
|
458
|
+
setRegisterOpen(false);
|
|
459
|
+
}}
|
|
460
|
+
className={cn(
|
|
461
|
+
'flex w-full items-center justify-between gap-2 rounded-[4px] px-2 py-1 text-left text-[11px] capitalize transition-colors',
|
|
462
|
+
register === v ? 'text-foreground' : 'text-muted-foreground hover:bg-secondary/50 hover:text-foreground',
|
|
463
|
+
)}
|
|
464
|
+
>
|
|
465
|
+
<span>{v}</span>
|
|
466
|
+
{register === v && <CheckIcon className="size-3 shrink-0 text-highlight" />}
|
|
467
|
+
</button>
|
|
468
|
+
))}
|
|
469
|
+
</PopoverContent>
|
|
470
|
+
</Popover>
|
|
271
471
|
|
|
272
472
|
<div className="ml-auto flex h-full min-w-0 items-center">
|
|
273
473
|
{selectedNode && (
|
|
@@ -297,6 +497,7 @@ export function StatusBar() {
|
|
|
297
497
|
</>
|
|
298
498
|
)}
|
|
299
499
|
</div>
|
|
500
|
+
<InfrastructureDialog open={infraOpen} onOpenChange={setInfraOpen} data={infraData} />
|
|
300
501
|
</footer>
|
|
301
502
|
);
|
|
302
503
|
}
|
|
@@ -55,10 +55,12 @@ function infraNodesOf(
|
|
|
55
55
|
}
|
|
56
56
|
|
|
57
57
|
/** Derives diagnoseOperation's `mutationSourcesByNode` extra from the studio's
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
58
|
+
* definitions-only node registry. The implementation source comes from
|
|
59
|
+
* `registry.getSource(type)` — the runner-synced `source` string (see
|
|
60
|
+
* syncNodeMeta / nodesPayload), not a bundled implementation — so the
|
|
61
|
+
* simulated-commit chip works even though the studio bundles no node code.
|
|
62
|
+
* Per-node degradation mirrors infraNodesOf: a node whose type isn't
|
|
63
|
+
* registered, isn't a mutation, or has no known source is simply absent. */
|
|
62
64
|
function mutationSourcesOf(
|
|
63
65
|
flow: { nodes: Array<{ id: string; type: string }> },
|
|
64
66
|
registry: NodeRegistry,
|
|
@@ -66,10 +68,10 @@ function mutationSourcesOf(
|
|
|
66
68
|
const mutationSourcesByNode: NonNullable<DiagnoseOperationExtras['mutationSourcesByNode']> = {};
|
|
67
69
|
for (const node of flow.nodes) {
|
|
68
70
|
if (!registry.has(node.type)) continue;
|
|
69
|
-
const { definition
|
|
70
|
-
if (definition.effects
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
const { definition } = registry.get(node.type);
|
|
72
|
+
if (definition.effects !== 'mutation') continue;
|
|
73
|
+
const source = registry.getSource(node.type);
|
|
74
|
+
if (source !== undefined) mutationSourcesByNode[node.id] = source;
|
|
73
75
|
}
|
|
74
76
|
return mutationSourcesByNode;
|
|
75
77
|
}
|
|
@@ -90,11 +92,14 @@ const statusDot: Record<string, string> = {
|
|
|
90
92
|
function RunSplitButton({
|
|
91
93
|
onRun,
|
|
92
94
|
runDisabled,
|
|
95
|
+
disabledReason,
|
|
93
96
|
live,
|
|
94
97
|
stepping,
|
|
95
98
|
}: {
|
|
96
99
|
onRun: () => void;
|
|
97
100
|
runDisabled: boolean;
|
|
101
|
+
/** Shown as the button title when runDisabled (e.g. the runner is offline). */
|
|
102
|
+
disabledReason?: string;
|
|
98
103
|
live: boolean;
|
|
99
104
|
stepping: boolean;
|
|
100
105
|
}) {
|
|
@@ -124,13 +129,15 @@ function RunSplitButton({
|
|
|
124
129
|
const safeMode = useBuilderStore((s) => s.safeMode);
|
|
125
130
|
// Blocked wins (it says why the button is disabled); otherwise plain-terms
|
|
126
131
|
// explainer of what a real run means.
|
|
127
|
-
const runTitle =
|
|
132
|
+
const runTitle = runDisabled && disabledReason
|
|
133
|
+
? disabledReason
|
|
134
|
+
: paramsBlocked
|
|
128
135
|
? paramBlockers.map((d) => d.message).join('\n')
|
|
129
136
|
: stepping
|
|
130
137
|
? 'Runs the stepped run to the end.'
|
|
131
138
|
: runnerMock
|
|
132
139
|
? 'Runs this operation against scenario mocks — real logic, canned infrastructure. Nothing real is touched.'
|
|
133
|
-
: `Runs this operation for real against "${selectedEnvironment || '
|
|
140
|
+
: `Runs this operation for real against "${selectedEnvironment || 'the default environment'}" — every node executes, and requests hit real services. Safe mode ${safeMode ? 'is on: writes are skipped' : 'is off: writes happen'}.`;
|
|
134
141
|
|
|
135
142
|
const lastRunFor = (sc: ScenarioDefinition) =>
|
|
136
143
|
runHistory.find((r) => r.workflowId === flow.id && r.scenarioName === sc.name);
|
|
@@ -513,7 +520,6 @@ export function Toolbar() {
|
|
|
513
520
|
const saveFlow = useBuilderStore((s) => s.saveFlow);
|
|
514
521
|
const exportFlow = useBuilderStore((s) => s.exportFlow);
|
|
515
522
|
const importFlow = useBuilderStore((s) => s.importFlow);
|
|
516
|
-
const executionMode = useBuilderStore((s) => s.executionMode);
|
|
517
523
|
const runnerOnline = useBuilderStore((s) => s.runnerOnline);
|
|
518
524
|
const environments = useBuilderStore((s) => s.environments);
|
|
519
525
|
const selectedEnvironment = useBuilderStore((s) => s.selectedEnvironment);
|
|
@@ -524,9 +530,12 @@ export function Toolbar() {
|
|
|
524
530
|
const fileRef = useRef<HTMLInputElement>(null);
|
|
525
531
|
const [moreOpen, setMoreOpen] = useState(false);
|
|
526
532
|
|
|
527
|
-
//
|
|
528
|
-
//
|
|
529
|
-
const runDisabled = busy ||
|
|
533
|
+
// Runs are always server-side: with the runner offline there is nowhere to
|
|
534
|
+
// execute, so Run/Step are disabled until it's back.
|
|
535
|
+
const runDisabled = busy || runnerOnline === false;
|
|
536
|
+
const runOfflineTitle = runnerOnline === false
|
|
537
|
+
? 'Runner offline — start it with `npx emberflow dev` to run.'
|
|
538
|
+
: undefined;
|
|
530
539
|
const currentEnv = environments.find((e) => e.name === selectedEnvironment);
|
|
531
540
|
const live = (currentEnv?.protected ?? false) && !safeMode;
|
|
532
541
|
|
|
@@ -582,6 +591,7 @@ export function Toolbar() {
|
|
|
582
591
|
<RunSplitButton
|
|
583
592
|
onRun={guard(runToEnd)}
|
|
584
593
|
runDisabled={runDisabled}
|
|
594
|
+
disabledReason={runOfflineTitle}
|
|
585
595
|
live={live}
|
|
586
596
|
stepping={stepping}
|
|
587
597
|
/>
|
|
@@ -591,11 +601,13 @@ export function Toolbar() {
|
|
|
591
601
|
onClick={guard(stepRun)}
|
|
592
602
|
disabled={runDisabled}
|
|
593
603
|
title={
|
|
594
|
-
|
|
604
|
+
runDisabled && runOfflineTitle
|
|
605
|
+
? runOfflineTitle
|
|
606
|
+
: stepping
|
|
595
607
|
? 'Runs the next node, then pauses again.'
|
|
596
608
|
: runnerMock
|
|
597
609
|
? 'Runs one node at a time against scenario mocks — real logic, canned infrastructure, paused between nodes.'
|
|
598
|
-
: `Runs one node at a time against "${selectedEnvironment || '
|
|
610
|
+
: `Runs one node at a time against "${selectedEnvironment || 'the default environment'}" — same real execution, paused between nodes.`
|
|
599
611
|
}
|
|
600
612
|
>
|
|
601
613
|
{stepping ? <PlayIcon /> : <StepForwardIcon />}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest';
|
|
2
|
+
import { renderToStaticMarkup } from 'react-dom/server';
|
|
3
|
+
import { UpdateChip } from './StatusBar';
|
|
4
|
+
|
|
5
|
+
/** Props-driven chip (StatusBar owns the /update-status fetch), so the test
|
|
6
|
+
* renders it directly — same pattern as InfrastructureDialog.test.tsx. */
|
|
7
|
+
describe('UpdateChip', () => {
|
|
8
|
+
it('renders nothing while the status is unknown (fetch failed / pending)', () => {
|
|
9
|
+
expect(renderToStaticMarkup(<UpdateChip status={null} />)).toBe('');
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it('renders nothing when the runner is current', () => {
|
|
13
|
+
const out = renderToStaticMarkup(
|
|
14
|
+
<UpdateChip status={{ current: '0.3.0', latest: '0.3.0', updateAvailable: false }} />,
|
|
15
|
+
);
|
|
16
|
+
expect(out).toBe('');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('renders nothing when the check was unavailable (no latest)', () => {
|
|
20
|
+
const out = renderToStaticMarkup(<UpdateChip status={{ current: '0.3.0', updateAvailable: false }} />);
|
|
21
|
+
expect(out).toBe('');
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it('shows the chip with both versions when an update is available', () => {
|
|
25
|
+
const out = renderToStaticMarkup(
|
|
26
|
+
<UpdateChip status={{ current: '0.3.0', latest: '0.4.0', updateAvailable: true }} />,
|
|
27
|
+
);
|
|
28
|
+
expect(out).toContain('update');
|
|
29
|
+
expect(out).toContain('Update available — 0.3.0 → 0.4.0');
|
|
30
|
+
});
|
|
31
|
+
});
|