@xdelivered/emberflow 0.2.0 → 0.4.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 +185 -14
- package/dist/server/runRegistry.d.ts +52 -1
- package/dist/server/runRegistry.js +170 -1
- 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/package.json +27 -6
- 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 +189 -13
- package/server/nodeRun.test.ts +189 -0
- package/server/runRegistry.ts +200 -3
- package/server/setupStatus.test.ts +3 -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/RunbookView.tsx +70 -6
- package/src/components/SettingsDialog.tsx +4 -59
- package/src/components/Sidebar.tsx +6 -26
- 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/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/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 +383 -312
- package/src/store/serverRunner.ts +56 -5
- package/src/store/setupClient.ts +7 -2
- package/src/store/updateClient.ts +50 -0
- package/studio-dist/assets/index-CAIDjNhv.css +1 -0
- package/studio-dist/assets/index-CGwEx82J.js +65 -0
- package/studio-dist/index.html +2 -2
- package/templates/skills/emberflow-model-process/SKILL.md +82 -9
- package/templates/skills/emberflow-review-workflow/SKILL.md +37 -1
- package/studio-dist/assets/index-DNJwW-hM.css +0 -1
- package/studio-dist/assets/index-O26dKRjW.js +0 -65
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { useEffect, useState } from 'react';
|
|
1
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import {
|
|
3
|
+
ArrowUpIcon,
|
|
3
4
|
CheckCircle2Icon,
|
|
4
5
|
CheckIcon,
|
|
5
6
|
CircleIcon,
|
|
6
7
|
CopyIcon,
|
|
8
|
+
LoaderCircleIcon,
|
|
7
9
|
MinusCircleIcon,
|
|
8
10
|
SparklesIcon,
|
|
9
11
|
} from 'lucide-react';
|
|
@@ -11,24 +13,34 @@ import { cn } from '@/lib/utils';
|
|
|
11
13
|
import { Button } from '@/components/ui/button';
|
|
12
14
|
import { Dialog, DialogContent, DialogDescription, DialogTitle } from '@/components/ui/dialog';
|
|
13
15
|
import { EnvironmentsDialog } from './EnvironmentsDialog';
|
|
16
|
+
import { AgentStream } from './AgentStream';
|
|
14
17
|
import { useBuilderStore } from '../store/builderStore';
|
|
15
|
-
import {
|
|
16
|
-
import type { AgentKind } from '../store/agentClient';
|
|
18
|
+
import type { SetupStatus } from '../store/setupClient';
|
|
19
|
+
import type { AgentEvent, AgentKind } from '../store/agentClient';
|
|
20
|
+
import {
|
|
21
|
+
composeAnsweredSubset,
|
|
22
|
+
extractGuidedQuestions,
|
|
23
|
+
resolveGuidedAnswers,
|
|
24
|
+
type GuidedAnswers,
|
|
25
|
+
type GuidedQuestion,
|
|
26
|
+
} from '../lib/guidedQuestions';
|
|
27
|
+
|
|
28
|
+
const WELCOME_DISMISSED_KEY = 'emberflow.welcome.dismissed';
|
|
29
|
+
/** Re-run this exact command to (re)install the agent skills only. */
|
|
30
|
+
const SKILLS_INSTALL_COMMAND = 'npx emberflow init --local --no-launch';
|
|
31
|
+
/** Initialize a repo so agent changes can be snapshotted/reverted. */
|
|
32
|
+
const GIT_INIT_COMMAND = 'git init && git add -A && git commit -m "initial"';
|
|
17
33
|
|
|
18
34
|
const AGENT_LABELS: Record<AgentKind, string> = {
|
|
19
35
|
codex: 'Codex',
|
|
20
36
|
claude: 'Claude',
|
|
21
37
|
};
|
|
22
38
|
|
|
23
|
-
const WELCOME_DISMISSED_KEY = 'emberflow.welcome.dismissed';
|
|
24
|
-
/** Re-run this exact command to (re)install the agent skills only. */
|
|
25
|
-
const SKILLS_INSTALL_COMMAND = 'npx emberflow init --local --no-launch';
|
|
26
|
-
|
|
27
39
|
type RowStatus = 'complete' | 'incomplete' | 'unavailable';
|
|
28
40
|
|
|
29
|
-
function StatusGlyph({ status }: { status: RowStatus }) {
|
|
41
|
+
function StatusGlyph({ status, emphasized }: { status: RowStatus; emphasized?: boolean }) {
|
|
30
42
|
if (status === 'complete') {
|
|
31
|
-
return <CheckCircle2Icon className="mt-0.5 size-4 shrink-0 text-success" aria-label="done" />;
|
|
43
|
+
return <CheckCircle2Icon className="mt-0.5 size-4 shrink-0 text-success/80" aria-label="done" />;
|
|
32
44
|
}
|
|
33
45
|
if (status === 'unavailable') {
|
|
34
46
|
return (
|
|
@@ -40,36 +52,78 @@ function StatusGlyph({ status }: { status: RowStatus }) {
|
|
|
40
52
|
}
|
|
41
53
|
return (
|
|
42
54
|
<CircleIcon
|
|
43
|
-
className=
|
|
55
|
+
className={`mt-0.5 size-4 shrink-0 ${emphasized ? 'text-highlight' : 'text-muted-foreground/50'}`}
|
|
44
56
|
aria-label="to do"
|
|
45
57
|
/>
|
|
46
58
|
);
|
|
47
59
|
}
|
|
48
60
|
|
|
49
|
-
|
|
61
|
+
/** Header fraction + 2px progress track. Quiet: mono numbers, ember fill. */
|
|
62
|
+
function Progress({ done, total }: { done: number; total: number }) {
|
|
50
63
|
return (
|
|
51
|
-
<div className="
|
|
52
|
-
|
|
64
|
+
<div className="flex items-center gap-2.5" aria-label={`${done} of ${total} steps done`}>
|
|
65
|
+
<div className="h-0.5 flex-1 overflow-hidden rounded-full bg-secondary/60">
|
|
66
|
+
<div
|
|
67
|
+
className="h-full rounded-full bg-highlight transition-[width] duration-300 ease-out motion-reduce:transition-none"
|
|
68
|
+
style={{ width: `${total === 0 ? 0 : Math.round((done / total) * 100)}%` }}
|
|
69
|
+
/>
|
|
70
|
+
</div>
|
|
71
|
+
<span className="shrink-0 font-mono text-[10.5px] tabular-nums text-muted-foreground">
|
|
72
|
+
{done}/{total}
|
|
73
|
+
</span>
|
|
53
74
|
</div>
|
|
54
75
|
);
|
|
55
76
|
}
|
|
56
77
|
|
|
78
|
+
/**
|
|
79
|
+
* One checklist row. Completed rows compress to a single dim line (title +
|
|
80
|
+
* inline action link) so the eye lands on what's left; the NEXT incomplete row
|
|
81
|
+
* is emphasized (tinted, primary action). Everything stays reachable.
|
|
82
|
+
*/
|
|
57
83
|
function ChecklistRow({
|
|
58
84
|
status,
|
|
85
|
+
emphasized = false,
|
|
59
86
|
title,
|
|
60
87
|
detail,
|
|
88
|
+
completedDetail,
|
|
89
|
+
completedExtra,
|
|
61
90
|
action,
|
|
62
91
|
children,
|
|
63
92
|
}: {
|
|
64
93
|
status: RowStatus;
|
|
94
|
+
emphasized?: boolean;
|
|
65
95
|
title: string;
|
|
66
96
|
detail: string;
|
|
97
|
+
completedDetail?: string;
|
|
98
|
+
/** Replaces `completedDetail` in the compressed done row — e.g. the agent
|
|
99
|
+
* picker, which stays interactive even once the row is complete. */
|
|
100
|
+
completedExtra?: React.ReactNode;
|
|
67
101
|
action?: React.ReactNode;
|
|
68
102
|
children?: React.ReactNode;
|
|
69
103
|
}) {
|
|
104
|
+
if (status === 'complete') {
|
|
105
|
+
return (
|
|
106
|
+
<div className="flex items-center gap-2.5 px-2.5 py-1.5">
|
|
107
|
+
<StatusGlyph status={status} />
|
|
108
|
+
<span className="text-[12px] text-muted-foreground">{title}</span>
|
|
109
|
+
{completedExtra ?? (
|
|
110
|
+
completedDetail ? (
|
|
111
|
+
<span className="min-w-0 flex-1 truncate text-[11px] text-muted-foreground/60">
|
|
112
|
+
{completedDetail}
|
|
113
|
+
</span>
|
|
114
|
+
) : (
|
|
115
|
+
<span className="flex-1" />
|
|
116
|
+
)
|
|
117
|
+
)}
|
|
118
|
+
{action && <div className="shrink-0">{action}</div>}
|
|
119
|
+
</div>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
70
122
|
return (
|
|
71
|
-
<div
|
|
72
|
-
|
|
123
|
+
<div
|
|
124
|
+
className={`flex items-start gap-2.5 px-2.5 py-2.5 ${emphasized ? 'bg-highlight/[0.06]' : ''}`}
|
|
125
|
+
>
|
|
126
|
+
<StatusGlyph status={status} emphasized={emphasized} />
|
|
73
127
|
<div className="flex min-w-0 flex-1 flex-col gap-1">
|
|
74
128
|
<span className="text-[12.5px] font-medium leading-tight">{title}</span>
|
|
75
129
|
<span className="text-[11px] leading-snug text-muted-foreground">{detail}</span>
|
|
@@ -80,7 +134,9 @@ function ChecklistRow({
|
|
|
80
134
|
);
|
|
81
135
|
}
|
|
82
136
|
|
|
83
|
-
|
|
137
|
+
/** Click-to-copy command pill. Shared: checklist rows here, and the
|
|
138
|
+
* StatusBar update chip's restart/manual-install commands. */
|
|
139
|
+
export function CopyCommand({ command }: { command: string }) {
|
|
84
140
|
const [copied, setCopied] = useState(false);
|
|
85
141
|
return (
|
|
86
142
|
<button
|
|
@@ -125,176 +181,820 @@ const NOOP_ACTIONS: WelcomeChecklistActions = {
|
|
|
125
181
|
* from props — the component test drives it across fresh/configured/no-agent
|
|
126
182
|
* states without a live runner.
|
|
127
183
|
*/
|
|
184
|
+
interface ChecklistItem {
|
|
185
|
+
key: string;
|
|
186
|
+
status: RowStatus;
|
|
187
|
+
title: string;
|
|
188
|
+
detail: string;
|
|
189
|
+
completedDetail?: string;
|
|
190
|
+
/** Label + handler for the row's action; the emphasized row's becomes primary. */
|
|
191
|
+
actionLabel?: string;
|
|
192
|
+
actionSparkle?: boolean;
|
|
193
|
+
actionDisabled?: boolean;
|
|
194
|
+
actionDisabledReason?: string;
|
|
195
|
+
onAction?: () => void;
|
|
196
|
+
extra?: 'skills-command' | 'manage-manually' | 'git-command';
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/** Derive the checklist rows once — the dialog reuses this for its footer CTA. */
|
|
200
|
+
export function deriveChecklist(status: SetupStatus, actions: WelcomeChecklistActions): ChecklistItem[] {
|
|
201
|
+
const { agents, git, environments, skills, ops, infrastructure } = status;
|
|
202
|
+
const hasAgent = agents.length > 0;
|
|
203
|
+
const hasGit = git.repo;
|
|
204
|
+
const skillsInstalled = skills.claude || skills.codex;
|
|
205
|
+
const agentSummary = agents.map((a) => `${a.kind}${a.version ? ` ${a.version}` : ''}`).join(', ');
|
|
206
|
+
|
|
207
|
+
return [
|
|
208
|
+
{
|
|
209
|
+
key: 'git',
|
|
210
|
+
status: hasGit ? 'complete' : 'incomplete',
|
|
211
|
+
title: 'Git repository',
|
|
212
|
+
detail: 'Agent changes are snapshotted with git so you can review and revert them.',
|
|
213
|
+
completedDetail: 'Repository ready',
|
|
214
|
+
extra: hasGit ? undefined : 'git-command',
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
key: 'agent',
|
|
218
|
+
status: hasAgent ? 'complete' : 'incomplete',
|
|
219
|
+
title: 'Coding agent',
|
|
220
|
+
detail: 'None detected on PATH — install codex or claude to let the agent build for you.',
|
|
221
|
+
completedDetail: agentSummary,
|
|
222
|
+
actionLabel: 'Settings',
|
|
223
|
+
onAction: actions.onOpenSettings,
|
|
224
|
+
},
|
|
225
|
+
// Skills sit with git + agent: all three are satisfied by `emberflow init`,
|
|
226
|
+
// so a fresh install reads as a solid completed block at the top — the
|
|
227
|
+
// remaining rows below are the journey still ahead. (No dependency forces
|
|
228
|
+
// skills after environments; the old ordering just interleaved done/todo.)
|
|
229
|
+
{
|
|
230
|
+
key: 'skills',
|
|
231
|
+
status: skillsInstalled ? 'complete' : 'incomplete',
|
|
232
|
+
title: 'Agent skills',
|
|
233
|
+
detail:
|
|
234
|
+
'Installed automatically by emberflow init — this project doesn’t have them yet. Add them (skills only, nothing else changes):',
|
|
235
|
+
completedDetail: 'Installed',
|
|
236
|
+
extra: skillsInstalled ? undefined : 'skills-command',
|
|
237
|
+
},
|
|
238
|
+
{
|
|
239
|
+
key: 'environments',
|
|
240
|
+
status: environments.configured ? 'complete' : 'incomplete',
|
|
241
|
+
title: 'Environments',
|
|
242
|
+
detail:
|
|
243
|
+
"You're in Mock — nothing real is touched. Point runs at real systems when you're ready.",
|
|
244
|
+
completedDetail:
|
|
245
|
+
`${environments.count} environment${environments.count === 1 ? '' : 's'}` +
|
|
246
|
+
(environments.protectedCount > 0 ? `, ${environments.protectedCount} protected` : ''),
|
|
247
|
+
actionLabel: environments.configured ? 'Manage' : 'Set up with AI',
|
|
248
|
+
actionSparkle: !environments.configured,
|
|
249
|
+
// The "Set up with AI" action drives the coding agent, which needs both
|
|
250
|
+
// an agent on PATH and a git repo to snapshot against — gate it the same
|
|
251
|
+
// way scout is gated. Git is the earlier prerequisite, so its reason
|
|
252
|
+
// wins when both are missing.
|
|
253
|
+
actionDisabled: !environments.configured && (!hasGit || !hasAgent),
|
|
254
|
+
actionDisabledReason: !hasGit ? 'Initialize git first' : 'Detect a coding agent first',
|
|
255
|
+
onAction: environments.configured ? actions.onOpenEnvironments : actions.onSetupEnvironments,
|
|
256
|
+
extra: environments.configured ? undefined : 'manage-manually',
|
|
257
|
+
},
|
|
258
|
+
// NOTE: secrets & auth deliberately have no checklist row — they only
|
|
259
|
+
// matter once an operation touches real infrastructure, and they live in
|
|
260
|
+
// the Manage Environment dialog when that day comes. Surfacing them at
|
|
261
|
+
// onboarding was premature noise.
|
|
262
|
+
{
|
|
263
|
+
key: 'infrastructure',
|
|
264
|
+
status: infrastructure.present ? 'complete' : 'incomplete',
|
|
265
|
+
title: 'Project scanned',
|
|
266
|
+
detail: 'The agent scans this project for databases, APIs and providers it already uses.',
|
|
267
|
+
completedDetail: `${infrastructure.itemCount ?? 0} item${infrastructure.itemCount === 1 ? '' : 's'} found`,
|
|
268
|
+
actionLabel: 'Scout',
|
|
269
|
+
actionSparkle: true,
|
|
270
|
+
// Scout drives the coding agent, which needs both an agent on PATH and a
|
|
271
|
+
// git repo to snapshot against. Git is the earlier prerequisite, so its
|
|
272
|
+
// reason wins when both are missing.
|
|
273
|
+
actionDisabled: !hasGit || !hasAgent,
|
|
274
|
+
actionDisabledReason: !hasGit ? 'Initialize git first' : 'Detect a coding agent first',
|
|
275
|
+
onAction: actions.onScoutInfrastructure,
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
key: 'first-op',
|
|
279
|
+
status: ops.count > 1 ? 'complete' : 'incomplete',
|
|
280
|
+
title: 'First operation',
|
|
281
|
+
detail: 'Open the hello example to see how an operation is built — then run it.',
|
|
282
|
+
completedDetail: `${ops.count} operations`,
|
|
283
|
+
actionLabel: 'Open hello op',
|
|
284
|
+
onAction: actions.onOpenHelloOp,
|
|
285
|
+
},
|
|
286
|
+
];
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** First incomplete row = the step we pull the user toward. */
|
|
290
|
+
export const nextStepIndex = (items: ChecklistItem[]) =>
|
|
291
|
+
items.findIndex((i) => i.status === 'incomplete');
|
|
292
|
+
|
|
293
|
+
/** Inline codex/claude choice chips — shared by the checklist's agent row and
|
|
294
|
+
* the guided card (where they must stay reachable while the checklist is
|
|
295
|
+
* collapsed behind the manual disclosure). */
|
|
296
|
+
function AgentPickerChips({
|
|
297
|
+
agents,
|
|
298
|
+
activeAgent,
|
|
299
|
+
onChoose,
|
|
300
|
+
}: {
|
|
301
|
+
agents: SetupStatus['agents'];
|
|
302
|
+
activeAgent?: AgentKind;
|
|
303
|
+
onChoose: (kind: AgentKind) => void;
|
|
304
|
+
}) {
|
|
305
|
+
return (
|
|
306
|
+
<div className="flex min-w-0 flex-1 flex-wrap gap-1" role="group" aria-label="Choose coding agent">
|
|
307
|
+
{agents.map(({ kind, version }) => (
|
|
308
|
+
<button
|
|
309
|
+
key={kind}
|
|
310
|
+
type="button"
|
|
311
|
+
onClick={() => onChoose(kind)}
|
|
312
|
+
aria-pressed={activeAgent === kind}
|
|
313
|
+
className={cn(
|
|
314
|
+
'cursor-pointer rounded-md border px-2 py-0.5 text-[11px] transition-colors',
|
|
315
|
+
activeAgent === kind
|
|
316
|
+
? 'border-ring bg-secondary/60 font-medium text-foreground'
|
|
317
|
+
: 'border-border text-muted-foreground hover:text-foreground',
|
|
318
|
+
)}
|
|
319
|
+
>
|
|
320
|
+
{AGENT_LABELS[kind]}
|
|
321
|
+
{version ? <span className="ml-1 font-normal text-muted-foreground">{version}</span> : null}
|
|
322
|
+
</button>
|
|
323
|
+
))}
|
|
324
|
+
</div>
|
|
325
|
+
);
|
|
326
|
+
}
|
|
327
|
+
|
|
128
328
|
export function WelcomeChecklist({
|
|
129
329
|
status,
|
|
130
330
|
actions = NOOP_ACTIONS,
|
|
131
331
|
chosenAgent,
|
|
132
332
|
onChooseAgent,
|
|
333
|
+
showProgress = true,
|
|
133
334
|
}: {
|
|
134
335
|
status: SetupStatus;
|
|
135
336
|
actions?: WelcomeChecklistActions;
|
|
136
337
|
/** The user's preferred agent (from Settings); defaults to the first detected. */
|
|
137
338
|
chosenAgent?: AgentKind;
|
|
138
339
|
onChooseAgent?: (kind: AgentKind) => void;
|
|
340
|
+
/** The idle screen renders its own top-level progress bar — suppress this one there. */
|
|
341
|
+
showProgress?: boolean;
|
|
139
342
|
}) {
|
|
140
|
-
const
|
|
141
|
-
const
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
const
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
: 'None detected on PATH — install codex or claude to let the agent build for you.';
|
|
152
|
-
|
|
153
|
-
const envDetail = environments.configured
|
|
154
|
-
? `${environments.count} environment${environments.count === 1 ? '' : 's'}` +
|
|
155
|
-
(environments.protectedCount > 0 ? `, ${environments.protectedCount} protected` : '')
|
|
156
|
-
: "You're in Mock — nothing real is touched. Set up environments to point runs at real systems.";
|
|
157
|
-
|
|
158
|
-
const secretsStatus: RowStatus = !environments.configured
|
|
159
|
-
? 'unavailable'
|
|
160
|
-
: environments.anyAuthConfigured
|
|
161
|
-
? 'complete'
|
|
162
|
-
: 'incomplete';
|
|
163
|
-
const secretsDetail = !environments.configured
|
|
164
|
-
? 'Configure environments first — secrets and auth live per environment.'
|
|
165
|
-
: environments.anyAuthConfigured
|
|
166
|
-
? 'Auth configured on at least one environment.'
|
|
167
|
-
: 'Add the secrets and login your operations need.';
|
|
168
|
-
|
|
169
|
-
const progress = setupProgress(status);
|
|
343
|
+
const items = deriveChecklist(status, actions);
|
|
344
|
+
const done = items.filter((i) => i.status === 'complete').length;
|
|
345
|
+
const nextIdx = nextStepIndex(items);
|
|
346
|
+
|
|
347
|
+
// With more than one agent CLI on PATH the agent row becomes a choice, not a
|
|
348
|
+
// fact: an inline picker replaces the compressed summary and stays live.
|
|
349
|
+
const multipleAgents = status.agents.length > 1 && onChooseAgent !== undefined;
|
|
350
|
+
const activeAgent = chosenAgent ?? status.agents[0]?.kind;
|
|
351
|
+
const agentPicker = multipleAgents ? (
|
|
352
|
+
<AgentPickerChips agents={status.agents} activeAgent={activeAgent} onChoose={onChooseAgent} />
|
|
353
|
+
) : undefined;
|
|
170
354
|
|
|
171
355
|
return (
|
|
172
|
-
<div className="space-y-
|
|
173
|
-
<
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
356
|
+
<div className="space-y-3">
|
|
357
|
+
{showProgress && <Progress done={done} total={items.length} />}
|
|
358
|
+
<div className="divide-y divide-border/50 rounded-md border border-border/70">
|
|
359
|
+
{items.map((item, i) => {
|
|
360
|
+
const emphasized = i === nextIdx;
|
|
361
|
+
return (
|
|
362
|
+
<ChecklistRow
|
|
363
|
+
key={item.key}
|
|
364
|
+
status={item.status}
|
|
365
|
+
emphasized={emphasized}
|
|
366
|
+
title={item.title}
|
|
367
|
+
detail={item.detail}
|
|
368
|
+
completedDetail={item.completedDetail}
|
|
369
|
+
completedExtra={item.key === 'agent' ? agentPicker : undefined}
|
|
370
|
+
action={
|
|
371
|
+
item.actionLabel && item.onAction ? (
|
|
372
|
+
item.status === 'complete' ? (
|
|
373
|
+
<button
|
|
374
|
+
type="button"
|
|
375
|
+
onClick={item.onAction}
|
|
376
|
+
className="text-[11px] text-muted-foreground underline-offset-2 hover:text-foreground hover:underline"
|
|
377
|
+
>
|
|
378
|
+
{item.actionLabel}
|
|
379
|
+
</button>
|
|
380
|
+
) : (
|
|
381
|
+
<Button
|
|
382
|
+
variant={emphasized ? 'default' : 'outline'}
|
|
383
|
+
size="sm"
|
|
384
|
+
disabled={item.actionDisabled}
|
|
385
|
+
title={item.actionDisabled ? item.actionDisabledReason : undefined}
|
|
386
|
+
onClick={item.onAction}
|
|
387
|
+
>
|
|
388
|
+
{item.actionSparkle && <SparklesIcon className="size-3.5" />}
|
|
389
|
+
{item.actionLabel}
|
|
390
|
+
</Button>
|
|
391
|
+
)
|
|
392
|
+
) : undefined
|
|
393
|
+
}
|
|
394
|
+
>
|
|
395
|
+
{item.status !== 'complete' && item.extra === 'git-command' && (
|
|
396
|
+
<CopyCommand command={GIT_INIT_COMMAND} />
|
|
397
|
+
)}
|
|
398
|
+
{item.status !== 'complete' && item.extra === 'skills-command' && (
|
|
399
|
+
<CopyCommand command={SKILLS_INSTALL_COMMAND} />
|
|
400
|
+
)}
|
|
401
|
+
{item.status !== 'complete' && item.extra === 'manage-manually' && (
|
|
193
402
|
<button
|
|
194
|
-
key={kind}
|
|
195
403
|
type="button"
|
|
196
|
-
onClick={
|
|
197
|
-
|
|
198
|
-
className={cn(
|
|
199
|
-
'cursor-pointer rounded-md border px-2 py-1 text-[11.5px] transition-colors',
|
|
200
|
-
activeAgent === kind
|
|
201
|
-
? 'border-ring bg-secondary/60 font-medium text-foreground'
|
|
202
|
-
: 'border-border text-muted-foreground hover:text-foreground',
|
|
203
|
-
)}
|
|
404
|
+
onClick={actions.onOpenEnvironments}
|
|
405
|
+
className="self-start text-[10.5px] text-muted-foreground underline-offset-2 hover:text-foreground hover:underline"
|
|
204
406
|
>
|
|
205
|
-
|
|
206
|
-
{version ? <span className="ml-1 font-normal text-muted-foreground">{version}</span> : null}
|
|
407
|
+
Manage manually
|
|
207
408
|
</button>
|
|
208
|
-
)
|
|
209
|
-
</
|
|
210
|
-
)
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
detail={envDetail}
|
|
217
|
-
action={
|
|
218
|
-
<div className="flex flex-col items-end gap-1">
|
|
219
|
-
<Button size="sm" onClick={actions.onSetupEnvironments}>
|
|
220
|
-
<SparklesIcon className="size-3.5" />
|
|
221
|
-
Set up with AI
|
|
222
|
-
</Button>
|
|
223
|
-
<button
|
|
224
|
-
type="button"
|
|
225
|
-
onClick={actions.onOpenEnvironments}
|
|
226
|
-
className="text-[10.5px] text-muted-foreground underline-offset-2 hover:text-foreground hover:underline"
|
|
227
|
-
>
|
|
228
|
-
Manage manually
|
|
229
|
-
</button>
|
|
230
|
-
</div>
|
|
231
|
-
}
|
|
232
|
-
/>
|
|
409
|
+
)}
|
|
410
|
+
</ChecklistRow>
|
|
411
|
+
);
|
|
412
|
+
})}
|
|
413
|
+
</div>
|
|
414
|
+
</div>
|
|
415
|
+
);
|
|
416
|
+
}
|
|
233
417
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
418
|
+
/**
|
|
419
|
+
* Footer: the primary CTA mirrors the NEXT incomplete step (same handler as the
|
|
420
|
+
* emphasized row), so the strongest affordance always advances setup. All
|
|
421
|
+
* steps done → pull toward the real aha: building the first API.
|
|
422
|
+
* "Don't show again" is the true dismissal (records the flag).
|
|
423
|
+
*/
|
|
424
|
+
function WelcomeFooter({
|
|
425
|
+
status,
|
|
426
|
+
actions,
|
|
427
|
+
onDismiss,
|
|
428
|
+
manualOpen,
|
|
429
|
+
onToggleManual,
|
|
430
|
+
}: {
|
|
431
|
+
status: SetupStatus | null;
|
|
432
|
+
actions: WelcomeChecklistActions;
|
|
433
|
+
onDismiss: () => void;
|
|
434
|
+
manualOpen?: boolean;
|
|
435
|
+
onToggleManual?: () => void;
|
|
436
|
+
}) {
|
|
437
|
+
const setCreateModal = useBuilderStore((s) => s.setCreateModal);
|
|
438
|
+
const setWelcomeOpen = useBuilderStore((s) => s.setWelcomeOpen);
|
|
439
|
+
const items = status ? deriveChecklist(status, actions) : [];
|
|
440
|
+
const nextIdx = status ? nextStepIndex(items) : -1;
|
|
441
|
+
const next = nextIdx === -1 ? undefined : items[nextIdx];
|
|
249
442
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
}
|
|
443
|
+
return (
|
|
444
|
+
<div className="flex items-center justify-between">
|
|
445
|
+
<div className="flex items-center gap-4">
|
|
446
|
+
<button
|
|
447
|
+
type="button"
|
|
448
|
+
onClick={onDismiss}
|
|
449
|
+
className="text-[11px] text-muted-foreground underline-offset-2 hover:text-foreground hover:underline"
|
|
258
450
|
>
|
|
259
|
-
|
|
260
|
-
</
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
451
|
+
Don't show this again
|
|
452
|
+
</button>
|
|
453
|
+
{onToggleManual && (
|
|
454
|
+
<button
|
|
455
|
+
type="button"
|
|
456
|
+
onClick={onToggleManual}
|
|
457
|
+
aria-expanded={manualOpen}
|
|
458
|
+
className="text-[11px] text-muted-foreground underline-offset-2 hover:text-foreground hover:underline"
|
|
459
|
+
>
|
|
460
|
+
{manualOpen ? 'Hide manual steps' : 'Set up manually instead'}
|
|
461
|
+
</button>
|
|
462
|
+
)}
|
|
463
|
+
</div>
|
|
464
|
+
{/* The emphasized row owns the next-step primary; the footer only takes
|
|
465
|
+
over once everything is done — pulling toward the real aha. One door:
|
|
466
|
+
the create modal. */}
|
|
467
|
+
{status && !next && (
|
|
468
|
+
<Button
|
|
469
|
+
size="sm"
|
|
470
|
+
onClick={() => {
|
|
471
|
+
setWelcomeOpen(false);
|
|
472
|
+
setCreateModal({ mode: 'api' });
|
|
473
|
+
}}
|
|
474
|
+
>
|
|
475
|
+
<SparklesIcon className="size-3.5" />
|
|
476
|
+
You're set — build your first API
|
|
477
|
+
</Button>
|
|
478
|
+
)}
|
|
479
|
+
</div>
|
|
480
|
+
);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* The Start-setup gate + summary, derived from the SAME checklist state the rows
|
|
485
|
+
* use. `remaining` names what the one guided run will work through (skipping
|
|
486
|
+
* anything already done); git + a coding agent are the two prerequisites the
|
|
487
|
+
* user must satisfy manually, so their absence disables Start with a reason.
|
|
488
|
+
*/
|
|
489
|
+
function guidedStartState(status: SetupStatus): {
|
|
490
|
+
remaining: string[];
|
|
491
|
+
disabled: boolean;
|
|
492
|
+
disabledReason?: string;
|
|
493
|
+
} {
|
|
494
|
+
const hasAgent = status.agents.length > 0;
|
|
495
|
+
const hasGit = status.git.repo;
|
|
496
|
+
const skillsInstalled = status.skills.claude || status.skills.codex;
|
|
497
|
+
|
|
498
|
+
const remaining: string[] = [];
|
|
499
|
+
if (!status.infrastructure.present) remaining.push('Scan your code for existing infrastructure');
|
|
500
|
+
if (!status.environments.configured) remaining.push('Set up environments — it asks you a few questions');
|
|
501
|
+
if (!skillsInstalled) remaining.push('Install the agent skills');
|
|
502
|
+
remaining.push('Verify the skills and its own connection');
|
|
503
|
+
|
|
504
|
+
return {
|
|
505
|
+
remaining,
|
|
506
|
+
disabled: !hasGit || !hasAgent,
|
|
507
|
+
// Git is the earlier prerequisite, so its reason wins when both are missing
|
|
508
|
+
// (mirrors the scout/environments row gating).
|
|
509
|
+
disabledReason: !hasGit ? 'Initialize git first' : !hasAgent ? 'Detect a coding agent first' : undefined,
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* idle-phase primary block: "Let the agent set this up." A short summary of what
|
|
515
|
+
* the one run will do (from `guidedStartState`) plus the Start-setup CTA. Sits
|
|
516
|
+
* ABOVE the manual checklist — the guided path is primary, the rows stay as the
|
|
517
|
+
* manual fallback.
|
|
518
|
+
*/
|
|
519
|
+
export function GuidedSetupIntro({
|
|
520
|
+
status,
|
|
521
|
+
chosenAgent,
|
|
522
|
+
onChooseAgent,
|
|
523
|
+
onStart,
|
|
524
|
+
}: {
|
|
525
|
+
status: SetupStatus;
|
|
526
|
+
chosenAgent?: AgentKind;
|
|
527
|
+
onChooseAgent?: (kind: AgentKind) => void;
|
|
528
|
+
onStart: () => void;
|
|
529
|
+
}) {
|
|
530
|
+
const { remaining, disabled, disabledReason } = guidedStartState(status);
|
|
531
|
+
const multipleAgents = status.agents.length > 1 && onChooseAgent !== undefined;
|
|
532
|
+
const activeAgent = chosenAgent ?? status.agents[0]?.kind;
|
|
533
|
+
return (
|
|
534
|
+
<div className="rounded-md border border-highlight/30 bg-highlight/[0.06] p-3.5">
|
|
535
|
+
<div className="flex items-start gap-2.5">
|
|
536
|
+
<span className="mt-0.5 flex size-7 shrink-0 items-center justify-center rounded-full bg-highlight/15 text-highlight">
|
|
537
|
+
<SparklesIcon className="size-4" />
|
|
538
|
+
</span>
|
|
539
|
+
<div className="min-w-0 flex-1">
|
|
540
|
+
<div className="text-[13px] font-medium leading-tight">Let the agent set this up</div>
|
|
541
|
+
<p className="mt-1 text-[11.5px] leading-snug text-muted-foreground">
|
|
542
|
+
One run finishes setup — it reads what's already done and skips it.
|
|
543
|
+
</p>
|
|
544
|
+
{/* The steps the run will work through: the same rows that tick live
|
|
545
|
+
in the two-pane view once started — the list IS the plan. */}
|
|
546
|
+
<ul className="mt-2.5 space-y-1.5">
|
|
547
|
+
{remaining.map((step) => (
|
|
548
|
+
<li key={step} className="flex items-center gap-2 text-[11.5px] text-foreground/85">
|
|
549
|
+
<CircleIcon className="size-3.5 shrink-0 text-highlight/60" aria-hidden />
|
|
550
|
+
{step}
|
|
551
|
+
</li>
|
|
552
|
+
))}
|
|
553
|
+
</ul>
|
|
554
|
+
</div>
|
|
555
|
+
</div>
|
|
556
|
+
<div className="mt-3.5 flex items-center justify-between gap-3">
|
|
557
|
+
{multipleAgents ? (
|
|
558
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
559
|
+
<span className="shrink-0 text-[10.5px] text-muted-foreground">Runs with</span>
|
|
560
|
+
<AgentPickerChips agents={status.agents} activeAgent={activeAgent} onChoose={onChooseAgent} />
|
|
561
|
+
</div>
|
|
562
|
+
) : (
|
|
563
|
+
<span />
|
|
564
|
+
)}
|
|
565
|
+
<Button
|
|
566
|
+
size="sm"
|
|
567
|
+
className="shrink-0"
|
|
568
|
+
disabled={disabled}
|
|
569
|
+
title={disabled ? disabledReason : undefined}
|
|
570
|
+
onClick={onStart}
|
|
571
|
+
>
|
|
572
|
+
<SparklesIcon className="size-3.5" />
|
|
573
|
+
Start setup
|
|
574
|
+
</Button>
|
|
575
|
+
</div>
|
|
576
|
+
{disabled && disabledReason && (
|
|
577
|
+
<p className="mt-1.5 text-right text-[10.5px] text-muted-foreground/70">{disabledReason}</p>
|
|
578
|
+
)}
|
|
579
|
+
</div>
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Read-only checklist for the guided two-pane view: while the agent works
|
|
585
|
+
* through setup, the left pane is a quiet progress MAP — glyph + title +
|
|
586
|
+
* completed detail, one line per step. No buttons: the interactive actions
|
|
587
|
+
* (Set up with AI, Scout, …) are exactly what the agent is doing, and
|
|
588
|
+
* clicking them mid-run would just collide with the single-flight run.
|
|
589
|
+
*/
|
|
590
|
+
export function GuidedChecklistMap({ status, running }: { status: SetupStatus; running?: boolean }) {
|
|
591
|
+
const items = deriveChecklist(status, NOOP_ACTIONS);
|
|
592
|
+
const done = items.filter((i) => i.status === 'complete').length;
|
|
593
|
+
const nextIdx = nextStepIndex(items);
|
|
594
|
+
return (
|
|
595
|
+
<div className="space-y-3">
|
|
596
|
+
<Progress done={done} total={items.length} />
|
|
597
|
+
<div className="space-y-0.5">
|
|
598
|
+
{items.map((item, i) => (
|
|
599
|
+
<div
|
|
600
|
+
key={item.key}
|
|
601
|
+
className={cn(
|
|
602
|
+
'flex min-w-0 items-center gap-2.5 rounded-md px-2 py-1.5',
|
|
603
|
+
i === nextIdx && 'bg-highlight/[0.06]',
|
|
604
|
+
)}
|
|
605
|
+
>
|
|
606
|
+
{/* While the agent runs, the NEXT step is the one being worked on —
|
|
607
|
+
its glyph becomes a spinner so the left map shows live activity.
|
|
608
|
+
Reduced motion: the spin stops but the ember ring still marks it. */}
|
|
609
|
+
{running && i === nextIdx ? (
|
|
610
|
+
<LoaderCircleIcon
|
|
611
|
+
className="mt-0.5 size-4 shrink-0 animate-spin text-highlight motion-reduce:animate-none"
|
|
612
|
+
aria-label="in progress"
|
|
613
|
+
/>
|
|
614
|
+
) : (
|
|
615
|
+
<StatusGlyph status={item.status} emphasized={i === nextIdx} />
|
|
616
|
+
)}
|
|
617
|
+
<span
|
|
618
|
+
className={cn(
|
|
619
|
+
'shrink-0 text-[12px]',
|
|
620
|
+
item.status === 'complete'
|
|
621
|
+
? 'text-muted-foreground'
|
|
622
|
+
: i === nextIdx
|
|
623
|
+
? 'font-medium text-foreground'
|
|
624
|
+
: 'text-muted-foreground',
|
|
625
|
+
)}
|
|
277
626
|
>
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
627
|
+
{item.title}
|
|
628
|
+
</span>
|
|
629
|
+
{item.status === 'complete' && item.completedDetail && (
|
|
630
|
+
<span className="min-w-0 truncate text-[11px] text-muted-foreground/60">
|
|
631
|
+
{item.completedDetail}
|
|
632
|
+
</span>
|
|
633
|
+
)}
|
|
634
|
+
</div>
|
|
635
|
+
))}
|
|
636
|
+
</div>
|
|
637
|
+
</div>
|
|
638
|
+
);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/** One dim line acknowledging what's already in place — proof of progress
|
|
642
|
+
* without a row (and a button) per item. */
|
|
643
|
+
export function DoneSummary({ status }: { status: SetupStatus }) {
|
|
644
|
+
const items = deriveChecklist(status, NOOP_ACTIONS).filter((i) => i.status === 'complete');
|
|
645
|
+
if (items.length === 0) return null;
|
|
646
|
+
return (
|
|
647
|
+
<div className="flex items-center gap-2 px-0.5 text-[11px] text-muted-foreground">
|
|
648
|
+
<CheckCircle2Icon className="size-3.5 shrink-0 text-success/70" aria-label="done" />
|
|
649
|
+
<span className="min-w-0 truncate">
|
|
650
|
+
{items.map((i) => (i.key === 'agent' ? i.completedDetail || i.title : i.title)).join(' · ')}
|
|
651
|
+
</span>
|
|
652
|
+
</div>
|
|
653
|
+
);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/**
|
|
657
|
+
* A slim follow-up input pinned under the embedded stream: answers the agent's
|
|
658
|
+
* questions by sending another guided-setup run (same panel thread). Disabled
|
|
659
|
+
* while the run is working. `focusRef` lets the done-phase "Continue setup" CTA
|
|
660
|
+
* pull focus here.
|
|
661
|
+
*/
|
|
662
|
+
function GuidedFollowUp({
|
|
663
|
+
running,
|
|
664
|
+
onSend,
|
|
665
|
+
focusRef,
|
|
666
|
+
}: {
|
|
667
|
+
running: boolean;
|
|
668
|
+
onSend: (text: string) => void;
|
|
669
|
+
focusRef?: React.RefObject<HTMLTextAreaElement | null>;
|
|
670
|
+
}) {
|
|
671
|
+
const [text, setText] = useState('');
|
|
672
|
+
const send = () => {
|
|
673
|
+
const t = text.trim();
|
|
674
|
+
if (!t || running) return;
|
|
675
|
+
onSend(t);
|
|
676
|
+
setText('');
|
|
677
|
+
};
|
|
678
|
+
return (
|
|
679
|
+
<div className="shrink-0 border-t border-border/70 p-2.5">
|
|
680
|
+
<div className="flex items-end gap-2">
|
|
681
|
+
<textarea
|
|
682
|
+
ref={focusRef}
|
|
683
|
+
value={text}
|
|
684
|
+
onChange={(e) => setText(e.target.value)}
|
|
685
|
+
onKeyDown={(e) => {
|
|
686
|
+
if ((e.metaKey || e.ctrlKey) && e.key === 'Enter') {
|
|
687
|
+
e.preventDefault();
|
|
688
|
+
send();
|
|
689
|
+
}
|
|
690
|
+
}}
|
|
691
|
+
disabled={running}
|
|
692
|
+
placeholder={running ? 'Agent is working…' : 'Answer the agent’s questions…'}
|
|
693
|
+
rows={2}
|
|
694
|
+
className="min-h-0 flex-1 resize-none rounded-md border border-input bg-input/30 px-2.5 py-1.5 text-[12px] leading-relaxed text-foreground outline-none transition-colors placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:opacity-60"
|
|
282
695
|
/>
|
|
696
|
+
<Button
|
|
697
|
+
size="icon"
|
|
698
|
+
className="size-8 shrink-0"
|
|
699
|
+
onClick={send}
|
|
700
|
+
disabled={running || !text.trim()}
|
|
701
|
+
aria-label="Send to agent"
|
|
702
|
+
title="Send (⌘⏎)"
|
|
703
|
+
>
|
|
704
|
+
<ArrowUpIcon className="size-4" />
|
|
705
|
+
</Button>
|
|
706
|
+
</div>
|
|
707
|
+
</div>
|
|
708
|
+
);
|
|
709
|
+
}
|
|
283
710
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
711
|
+
/**
|
|
712
|
+
* The agent's trailing `emberflow-questions` block rendered as a clickable
|
|
713
|
+
* form (replacing the follow-up textarea while the run waits on answers).
|
|
714
|
+
* Single-select pills per question — the same chip vocabulary as the agent
|
|
715
|
+
* picker — plus an optional "Other…" free-text field that deselects the pills.
|
|
716
|
+
* Submit composes plaintext answers for the continuation run; an option with
|
|
717
|
+
* action 'finish' ends onboarding instead (`onFinish`), sending nothing; a
|
|
718
|
+
* lone typed 'first-build' answer routes into the create flow (`onBuild`).
|
|
719
|
+
*/
|
|
720
|
+
function GuidedQuestionForm({
|
|
721
|
+
questions,
|
|
722
|
+
onSubmit,
|
|
723
|
+
onFinish,
|
|
724
|
+
onBuild,
|
|
725
|
+
onTypeInstead,
|
|
726
|
+
}: {
|
|
727
|
+
questions: GuidedQuestion[];
|
|
728
|
+
onSubmit: (composed: string) => void;
|
|
729
|
+
onFinish: () => void;
|
|
730
|
+
onBuild: (text: string) => void;
|
|
731
|
+
onTypeInstead: () => void;
|
|
732
|
+
}) {
|
|
733
|
+
const [answers, setAnswers] = useState<GuidedAnswers>({});
|
|
734
|
+
// Wizard: ONE question at a time — picking an option answers it and
|
|
735
|
+
// advances; the last answer reveals Send. `cursor` never exceeds the last
|
|
736
|
+
// question; Back steps to any earlier answer without losing later ones.
|
|
737
|
+
const [cursor, setCursor] = useState(0);
|
|
738
|
+
const outcome = resolveGuidedAnswers(questions, answers);
|
|
739
|
+
const q = questions[Math.min(cursor, questions.length - 1)];
|
|
740
|
+
const answer = answers[q.id];
|
|
741
|
+
const last = cursor >= questions.length - 1;
|
|
742
|
+
const advance = () => setCursor((c) => Math.min(c + 1, questions.length - 1));
|
|
743
|
+
const submit = () => {
|
|
744
|
+
if (outcome.kind === 'finish') onFinish();
|
|
745
|
+
else if (outcome.kind === 'build') onBuild(outcome.text);
|
|
746
|
+
else if (outcome.kind === 'send') onSubmit(outcome.text);
|
|
747
|
+
};
|
|
748
|
+
return (
|
|
749
|
+
<div className="shrink-0 space-y-3 border-t border-border/70 p-3.5">
|
|
750
|
+
<div className="flex items-baseline justify-between gap-3">
|
|
751
|
+
<div className="text-[13.5px] font-medium leading-snug">{q.text}</div>
|
|
752
|
+
{questions.length > 1 && (
|
|
753
|
+
<span className="shrink-0 font-mono text-[10.5px] tabular-nums text-muted-foreground">
|
|
754
|
+
{cursor + 1}/{questions.length}
|
|
755
|
+
</span>
|
|
756
|
+
)}
|
|
757
|
+
</div>
|
|
758
|
+
{q.why && (
|
|
759
|
+
<p className="max-w-[65ch] text-[11.5px] leading-snug text-muted-foreground">{q.why}</p>
|
|
760
|
+
)}
|
|
761
|
+
<div className="flex flex-wrap items-center gap-1.5" role="group" aria-label={q.text}>
|
|
762
|
+
{q.options.map((opt) => {
|
|
763
|
+
const selected = answer?.option?.label === opt.label;
|
|
764
|
+
return (
|
|
765
|
+
<button
|
|
766
|
+
key={opt.label}
|
|
767
|
+
type="button"
|
|
768
|
+
aria-pressed={selected}
|
|
769
|
+
onClick={() => {
|
|
770
|
+
const next: GuidedAnswers = { ...answers, [q.id]: { option: opt } };
|
|
771
|
+
setAnswers(next);
|
|
772
|
+
// A 'submit' option sends IMMEDIATELY with whatever has been
|
|
773
|
+
// answered so far — a subset; unanswered questions are skipped.
|
|
774
|
+
if (opt.action === 'submit') onSubmit(composeAnsweredSubset(questions, next));
|
|
775
|
+
else if (!last) advance();
|
|
776
|
+
}}
|
|
777
|
+
className={cn(
|
|
778
|
+
'cursor-pointer rounded-md border px-3 py-1.5 text-[12.5px] transition-colors',
|
|
779
|
+
selected
|
|
780
|
+
? 'border-ring bg-secondary/60 font-medium text-foreground'
|
|
781
|
+
: 'border-border text-foreground/80 hover:border-ring/50 hover:text-foreground',
|
|
782
|
+
)}
|
|
783
|
+
>
|
|
784
|
+
{opt.label}
|
|
785
|
+
</button>
|
|
786
|
+
);
|
|
787
|
+
})}
|
|
788
|
+
</div>
|
|
789
|
+
{q.custom && (
|
|
790
|
+
<div className="flex items-center gap-2">
|
|
791
|
+
<input
|
|
792
|
+
type="text"
|
|
793
|
+
value={answer?.text ?? ''}
|
|
794
|
+
onChange={(e) =>
|
|
795
|
+
// Typing a custom answer deselects the pills for this question.
|
|
796
|
+
setAnswers((a) => ({ ...a, [q.id]: { text: e.target.value } }))
|
|
797
|
+
}
|
|
798
|
+
onKeyDown={(e) => {
|
|
799
|
+
if (e.key === 'Enter' && (answer?.text ?? '').trim()) {
|
|
800
|
+
e.preventDefault();
|
|
801
|
+
if (last) submit();
|
|
802
|
+
else advance();
|
|
803
|
+
}
|
|
804
|
+
}}
|
|
805
|
+
placeholder="Or type your own…"
|
|
806
|
+
aria-label={`${q.text} — other`}
|
|
807
|
+
className="min-w-0 flex-1 rounded-md border border-input bg-input/30 px-2.5 py-1.5 text-[12.5px] text-foreground outline-none transition-colors placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50"
|
|
808
|
+
/>
|
|
809
|
+
{!last && (answer?.text ?? '').trim() !== '' && (
|
|
810
|
+
<Button size="sm" variant="outline" onClick={advance}>
|
|
811
|
+
Next
|
|
295
812
|
</Button>
|
|
296
|
-
}
|
|
297
|
-
|
|
813
|
+
)}
|
|
814
|
+
</div>
|
|
815
|
+
)}
|
|
816
|
+
<div className="flex items-center justify-between gap-3">
|
|
817
|
+
<div className="flex items-center gap-3">
|
|
818
|
+
{cursor > 0 && (
|
|
819
|
+
<button
|
|
820
|
+
type="button"
|
|
821
|
+
onClick={() => setCursor((c) => Math.max(0, c - 1))}
|
|
822
|
+
className="text-[11px] text-muted-foreground underline-offset-2 hover:text-foreground hover:underline"
|
|
823
|
+
>
|
|
824
|
+
Back
|
|
825
|
+
</button>
|
|
826
|
+
)}
|
|
827
|
+
<button
|
|
828
|
+
type="button"
|
|
829
|
+
onClick={onTypeInstead}
|
|
830
|
+
className="text-[11px] text-muted-foreground underline-offset-2 hover:text-foreground hover:underline"
|
|
831
|
+
>
|
|
832
|
+
Type a reply instead
|
|
833
|
+
</button>
|
|
834
|
+
</div>
|
|
835
|
+
{(last || outcome.kind !== 'incomplete') && (
|
|
836
|
+
<Button size="sm" onClick={submit} disabled={outcome.kind === 'incomplete'}>
|
|
837
|
+
Send answers
|
|
838
|
+
</Button>
|
|
839
|
+
)}
|
|
840
|
+
</div>
|
|
841
|
+
</div>
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* running/done phase: two panes. LEFT the live checklist (refetched on run
|
|
847
|
+
* finish by the dialog), RIGHT the embedded agent stream + follow-up input.
|
|
848
|
+
* `done` swaps the footer: everything complete → "You're set", else "Continue
|
|
849
|
+
* setup" focuses the follow-up so the interview can continue.
|
|
850
|
+
*/
|
|
851
|
+
export function GuidedSetupPanes({
|
|
852
|
+
status,
|
|
853
|
+
actions,
|
|
854
|
+
events,
|
|
855
|
+
running,
|
|
856
|
+
failed,
|
|
857
|
+
onFollowUp,
|
|
858
|
+
onFinishComplete,
|
|
859
|
+
onBuildFirst,
|
|
860
|
+
onContinue,
|
|
861
|
+
onRetry,
|
|
862
|
+
retryAgent,
|
|
863
|
+
onRetryWith,
|
|
864
|
+
notice,
|
|
865
|
+
followUpRef,
|
|
866
|
+
}: {
|
|
867
|
+
status: SetupStatus | null;
|
|
868
|
+
actions: WelcomeChecklistActions;
|
|
869
|
+
events: React.ComponentProps<typeof AgentStream>['events'];
|
|
870
|
+
running: boolean;
|
|
871
|
+
/** The guided run ended on an error event (agent crashed / model rejected). */
|
|
872
|
+
failed?: boolean;
|
|
873
|
+
onFollowUp: (text: string) => void;
|
|
874
|
+
onFinishComplete: () => void;
|
|
875
|
+
/** The lone 'first-build' question answered with a typed description —
|
|
876
|
+
* open the real build flow pre-filled instead of sending a continuation.
|
|
877
|
+
* Falls back to `onFollowUp` (send) when not provided. */
|
|
878
|
+
onBuildFirst?: (text: string) => void;
|
|
879
|
+
onContinue: () => void;
|
|
880
|
+
/** Re-kick the guided run after a failure. */
|
|
881
|
+
onRetry?: () => void;
|
|
882
|
+
/** When the failure was the BACKEND's fault (e.g. a stale CLI rejecting its
|
|
883
|
+
* model) and another agent is detected, retry switches to it — plain
|
|
884
|
+
* "Try again" would just replay the same broken backend. */
|
|
885
|
+
retryAgent?: AgentKind;
|
|
886
|
+
onRetryWith?: (kind: AgentKind) => void;
|
|
887
|
+
/** One-line explanation when the dialog auto-switched backend after a failure. */
|
|
888
|
+
notice?: string;
|
|
889
|
+
followUpRef: React.RefObject<HTMLTextAreaElement | null>;
|
|
890
|
+
}) {
|
|
891
|
+
const allDone = status ? nextStepIndex(deriveChecklist(status, actions)) === -1 : false;
|
|
892
|
+
|
|
893
|
+
// The agent's final message may end with an `emberflow-questions` block —
|
|
894
|
+
// pull it out here (pure derivation over the event stream) so the stream
|
|
895
|
+
// renders the prose WITHOUT the raw fenced JSON and the block becomes the
|
|
896
|
+
// clickable form below. Malformed blocks extract as null and pass through
|
|
897
|
+
// untouched.
|
|
898
|
+
const { displayEvents, questions } = useMemo((): {
|
|
899
|
+
displayEvents: AgentEvent[];
|
|
900
|
+
questions: GuidedQuestion[] | null;
|
|
901
|
+
} => {
|
|
902
|
+
const idx = events.findLastIndex((e) => e.type === 'message');
|
|
903
|
+
if (idx === -1) return { displayEvents: events, questions: null };
|
|
904
|
+
const { stripped, questions: extracted } = extractGuidedQuestions(events[idx].text ?? '');
|
|
905
|
+
if (!extracted) return { displayEvents: events, questions: null };
|
|
906
|
+
const copy = events.slice();
|
|
907
|
+
copy[idx] = { ...copy[idx], text: stripped };
|
|
908
|
+
return { displayEvents: copy, questions: extracted };
|
|
909
|
+
}, [events]);
|
|
910
|
+
|
|
911
|
+
// "Type a reply instead" swaps the form for the plain textarea; a fresh set
|
|
912
|
+
// of questions (next agent turn) resets back to the form.
|
|
913
|
+
const [typedReply, setTypedReply] = useState(false);
|
|
914
|
+
useEffect(() => setTypedReply(false), [questions]);
|
|
915
|
+
|
|
916
|
+
const showForm = !running && !failed && questions !== null && !typedReply;
|
|
917
|
+
const headerText = running
|
|
918
|
+
? 'Setting things up…'
|
|
919
|
+
: failed
|
|
920
|
+
? 'Setup hit a problem'
|
|
921
|
+
: allDone
|
|
922
|
+
? 'Setup complete'
|
|
923
|
+
: 'Waiting on you';
|
|
924
|
+
return (
|
|
925
|
+
<div className="grid min-h-0 grid-cols-1 gap-4 md:grid-cols-[minmax(0,1fr)_minmax(0,1.15fr)]">
|
|
926
|
+
<div className="min-h-0 overflow-y-auto">
|
|
927
|
+
{status ? (
|
|
928
|
+
<GuidedChecklistMap status={status} running={running} />
|
|
929
|
+
) : (
|
|
930
|
+
<div className="py-6 text-center text-[12px] text-muted-foreground">Checking your project…</div>
|
|
931
|
+
)}
|
|
932
|
+
</div>
|
|
933
|
+
<div className="flex min-h-0 flex-col overflow-hidden rounded-md border border-border/70 bg-card">
|
|
934
|
+
<div className="flex h-9 shrink-0 items-center gap-2 border-b border-border/70 px-3">
|
|
935
|
+
<span
|
|
936
|
+
className={cn(
|
|
937
|
+
'size-2 rounded-full',
|
|
938
|
+
running ? 'animate-pulse bg-highlight' : failed ? 'bg-destructive' : 'bg-success',
|
|
939
|
+
)}
|
|
940
|
+
/>
|
|
941
|
+
<span className="text-[11.5px] font-medium">{headerText}</span>
|
|
942
|
+
</div>
|
|
943
|
+
<div className="min-h-0 flex-1 space-y-1.5 overflow-y-auto px-3 py-2.5 text-[12px]">
|
|
944
|
+
{notice && <div className="text-[11px] text-highlight/90">{notice}</div>}
|
|
945
|
+
<AgentStream events={displayEvents} running={running} />
|
|
946
|
+
</div>
|
|
947
|
+
{showForm && questions ? (
|
|
948
|
+
<GuidedQuestionForm
|
|
949
|
+
questions={questions}
|
|
950
|
+
onSubmit={onFollowUp}
|
|
951
|
+
onFinish={onFinishComplete}
|
|
952
|
+
onBuild={onBuildFirst ?? onFollowUp}
|
|
953
|
+
onTypeInstead={() => setTypedReply(true)}
|
|
954
|
+
/>
|
|
955
|
+
) : (
|
|
956
|
+
<>
|
|
957
|
+
<GuidedFollowUp running={running} onSend={onFollowUp} focusRef={followUpRef} />
|
|
958
|
+
{!running && !failed && questions !== null && typedReply && (
|
|
959
|
+
<div className="shrink-0 px-2.5 pb-2">
|
|
960
|
+
<button
|
|
961
|
+
type="button"
|
|
962
|
+
onClick={() => setTypedReply(false)}
|
|
963
|
+
className="text-[11px] text-muted-foreground underline-offset-2 hover:text-foreground hover:underline"
|
|
964
|
+
>
|
|
965
|
+
Answer with the options instead
|
|
966
|
+
</button>
|
|
967
|
+
</div>
|
|
968
|
+
)}
|
|
969
|
+
</>
|
|
970
|
+
)}
|
|
971
|
+
{/* When the question form is up, the "Continue setup" nudge is redundant
|
|
972
|
+
— the form IS the continuation — so the footer only renders for the
|
|
973
|
+
failed / all-done / plain-textarea states. */}
|
|
974
|
+
{!running && (failed || allDone || questions === null) && (
|
|
975
|
+
<div className="flex shrink-0 justify-end border-t border-border/70 px-2.5 py-2">
|
|
976
|
+
{failed ? (
|
|
977
|
+
retryAgent && onRetryWith ? (
|
|
978
|
+
<Button size="sm" onClick={() => onRetryWith(retryAgent)}>
|
|
979
|
+
Try again with {AGENT_LABELS[retryAgent]}
|
|
980
|
+
</Button>
|
|
981
|
+
) : (
|
|
982
|
+
<Button size="sm" variant="outline" onClick={onRetry}>
|
|
983
|
+
Try again
|
|
984
|
+
</Button>
|
|
985
|
+
)
|
|
986
|
+
) : allDone ? (
|
|
987
|
+
<Button size="sm" onClick={onFinishComplete}>
|
|
988
|
+
<SparklesIcon className="size-3.5" />
|
|
989
|
+
You're set — build your first API
|
|
990
|
+
</Button>
|
|
991
|
+
) : (
|
|
992
|
+
<Button size="sm" variant="outline" onClick={onContinue}>
|
|
993
|
+
Continue setup
|
|
994
|
+
</Button>
|
|
995
|
+
)}
|
|
996
|
+
</div>
|
|
997
|
+
)}
|
|
298
998
|
</div>
|
|
299
999
|
</div>
|
|
300
1000
|
);
|
|
@@ -304,7 +1004,13 @@ export function WelcomeChecklist({
|
|
|
304
1004
|
* First-run Welcome/Setup dialog. Mounted once (in the Toolbar). Fetches
|
|
305
1005
|
* `/setup-status` on mount to decide whether to auto-open on a fresh project,
|
|
306
1006
|
* and again whenever it's opened so the rows reflect the latest state. Always
|
|
307
|
-
* reachable from the
|
|
1007
|
+
* reachable from the StatusBar's setup chip (via the store's `welcomeOpen`).
|
|
1008
|
+
*
|
|
1009
|
+
* Grows a `idle → running → done` guided phase machine on top of the checklist:
|
|
1010
|
+
* Start setup kicks ONE `guided-setup` agent run (owned here, not the right-hand
|
|
1011
|
+
* console), whose stream embeds in a two-pane layout. Closing the dialog mid-run
|
|
1012
|
+
* doesn't kill it — the run lives in the store's single agentRun slot, so
|
|
1013
|
+
* reopening re-attaches (phase derives from `agentRun.guided` + status).
|
|
308
1014
|
*/
|
|
309
1015
|
export function WelcomeDialog() {
|
|
310
1016
|
const welcomeOpen = useBuilderStore((s) => s.welcomeOpen);
|
|
@@ -317,7 +1023,62 @@ export function WelcomeDialog() {
|
|
|
317
1023
|
const refreshSetupStatus = useBuilderStore((s) => s.refreshSetupStatus);
|
|
318
1024
|
const agentChoice = useBuilderStore((s) => s.agentChoice);
|
|
319
1025
|
const setAgentChoice = useBuilderStore((s) => s.setAgentChoice);
|
|
1026
|
+
const beginGuidedSetup = useBuilderStore((s) => s.beginGuidedSetup);
|
|
1027
|
+
const setCreateModal = useBuilderStore((s) => s.setCreateModal);
|
|
1028
|
+
const agentRun = useBuilderStore((s) => s.agentRun);
|
|
1029
|
+
const guidedTranscript = useBuilderStore((s) => s.guidedTranscript);
|
|
320
1030
|
const [envDialogOpen, setEnvDialogOpen] = useState(false);
|
|
1031
|
+
const [manualOpen, setManualOpen] = useState(false);
|
|
1032
|
+
const followUpRef = useRef<HTMLTextAreaElement | null>(null);
|
|
1033
|
+
|
|
1034
|
+
// Guided phase: derived from the single agentRun slot. `guided` marks the run
|
|
1035
|
+
// this dialog owns, so closing + reopening re-attaches to the same stream.
|
|
1036
|
+
const guidedRun = agentRun?.guided ? agentRun : null;
|
|
1037
|
+
const phase: 'idle' | 'running' | 'done' = !guidedRun
|
|
1038
|
+
? 'idle'
|
|
1039
|
+
: guidedRun.status === 'running'
|
|
1040
|
+
? 'running'
|
|
1041
|
+
: 'done';
|
|
1042
|
+
|
|
1043
|
+
// A failed run whose terminal error blames the BACKEND (stale CLI rejected
|
|
1044
|
+
// its model, or the process died) offers a one-click switch to the other
|
|
1045
|
+
// detected agent — a plain retry would replay the same broken backend.
|
|
1046
|
+
const activeAgent = agentChoice.agent ?? status?.agents[0]?.kind;
|
|
1047
|
+
const lastError = guidedRun?.status === 'error'
|
|
1048
|
+
? [...guidedRun.events].reverse().find((e) => e.type === 'error')?.text ?? ''
|
|
1049
|
+
: '';
|
|
1050
|
+
const backendFault = /CLI may be too old|exited with code/i.test(lastError);
|
|
1051
|
+
const retryAgent = backendFault
|
|
1052
|
+
? status?.agents.find((a) => a.kind !== activeAgent)?.kind
|
|
1053
|
+
: undefined;
|
|
1054
|
+
|
|
1055
|
+
const guidedStatus = guidedRun?.status;
|
|
1056
|
+
|
|
1057
|
+
// Auto-failover, once: a backend-fault failure with another agent available
|
|
1058
|
+
// switches to it and re-kicks the run WITHOUT waiting for a click — the
|
|
1059
|
+
// default backend being broken (stale codex CLI) shouldn't strand setup on
|
|
1060
|
+
// an error pane. One-shot per dialog mount so two broken backends can't
|
|
1061
|
+
// ping-pong; the manual "Try again with X" stays as the fallback.
|
|
1062
|
+
const autoFailoverUsed = useRef(false);
|
|
1063
|
+
const [autoSwitchNotice, setAutoSwitchNotice] = useState<string | null>(null);
|
|
1064
|
+
useEffect(() => {
|
|
1065
|
+
if (guidedStatus !== 'error' || !retryAgent || autoFailoverUsed.current) return;
|
|
1066
|
+
autoFailoverUsed.current = true;
|
|
1067
|
+
const from = activeAgent ? AGENT_LABELS[activeAgent] : 'The agent';
|
|
1068
|
+
setAutoSwitchNotice(`${from} failed to start — switched to ${AGENT_LABELS[retryAgent]} and retried automatically.`);
|
|
1069
|
+
setAgentChoice({ ...agentChoice, agent: retryAgent });
|
|
1070
|
+
beginGuidedSetup();
|
|
1071
|
+
// agentChoice/activeAgent are read at fire time; keying on them would re-arm the effect.
|
|
1072
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1073
|
+
}, [guidedStatus, retryAgent]);
|
|
1074
|
+
|
|
1075
|
+
// Refetch the checklist when the guided run finishes (a completed run may have
|
|
1076
|
+
// scouted, installed skills, or written environments) — mirrors the
|
|
1077
|
+
// agentRunStatus subscription StatusBar/InfraTab use.
|
|
1078
|
+
useEffect(() => {
|
|
1079
|
+
if (!guidedStatus || guidedStatus === 'running') return;
|
|
1080
|
+
void refreshSetupStatus();
|
|
1081
|
+
}, [guidedStatus, refreshSetupStatus]);
|
|
321
1082
|
|
|
322
1083
|
// Mount: fetch once and auto-open on a fresh, undismissed project.
|
|
323
1084
|
useEffect(() => {
|
|
@@ -370,32 +1131,90 @@ export function WelcomeDialog() {
|
|
|
370
1131
|
},
|
|
371
1132
|
};
|
|
372
1133
|
|
|
1134
|
+
const twoPane = phase !== 'idle';
|
|
1135
|
+
|
|
373
1136
|
return (
|
|
374
1137
|
<>
|
|
375
1138
|
<Dialog open={welcomeOpen} onOpenChange={(o) => (o ? setWelcomeOpen(true) : dismiss())}>
|
|
376
|
-
<DialogContent className=
|
|
1139
|
+
<DialogContent className={cn('flex max-h-[85vh] flex-col', twoPane ? 'max-w-3xl' : 'max-w-lg')}>
|
|
377
1140
|
<DialogTitle>Welcome to Emberflow</DialogTitle>
|
|
378
1141
|
<DialogDescription>
|
|
379
|
-
|
|
380
|
-
|
|
1142
|
+
{twoPane
|
|
1143
|
+
? 'The agent is working through setup — answer its questions on the right; the checklist updates as it goes.'
|
|
1144
|
+
: 'Get this project ready to build and run — revisit any time from the setup chip in the status bar.'}
|
|
381
1145
|
</DialogDescription>
|
|
382
|
-
{
|
|
383
|
-
<
|
|
1146
|
+
{twoPane ? (
|
|
1147
|
+
<GuidedSetupPanes
|
|
384
1148
|
status={status}
|
|
385
1149
|
actions={actions}
|
|
386
|
-
|
|
387
|
-
|
|
1150
|
+
events={[...guidedTranscript, ...(guidedRun?.events ?? [])]}
|
|
1151
|
+
running={phase === 'running'}
|
|
1152
|
+
failed={guidedRun?.status === 'error'}
|
|
1153
|
+
onRetry={() => beginGuidedSetup()}
|
|
1154
|
+
retryAgent={retryAgent}
|
|
1155
|
+
onRetryWith={(kind) => {
|
|
1156
|
+
setAgentChoice({ ...agentChoice, agent: kind });
|
|
1157
|
+
beginGuidedSetup();
|
|
1158
|
+
}}
|
|
1159
|
+
notice={autoSwitchNotice ?? undefined}
|
|
1160
|
+
onFollowUp={(text) => beginGuidedSetup(text)}
|
|
1161
|
+
onFinishComplete={() => {
|
|
1162
|
+
// One door: setup ends in the create modal, not the agent panel.
|
|
1163
|
+
setWelcomeOpen(false);
|
|
1164
|
+
setCreateModal({ mode: 'api' });
|
|
1165
|
+
}}
|
|
1166
|
+
onBuildFirst={(text) => {
|
|
1167
|
+
// A typed first-build answer IS the goal — open the real build
|
|
1168
|
+
// flow pre-filled with it instead of another interview turn.
|
|
1169
|
+
setWelcomeOpen(false);
|
|
1170
|
+
setCreateModal({ mode: 'api', initialGoal: text });
|
|
1171
|
+
}}
|
|
1172
|
+
onContinue={() => followUpRef.current?.focus()}
|
|
1173
|
+
followUpRef={followUpRef}
|
|
388
1174
|
/>
|
|
389
1175
|
) : (
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
1176
|
+
<>
|
|
1177
|
+
{status ? (
|
|
1178
|
+
<div className="min-h-0 flex-1 space-y-3 overflow-y-auto">
|
|
1179
|
+
{/* One story: progress → the guided plan → what's done → a
|
|
1180
|
+
quiet manual fallback. The full checklist (with its many
|
|
1181
|
+
buttons) stays behind the disclosure so the idle screen
|
|
1182
|
+
has a single primary action. */}
|
|
1183
|
+
<Progress
|
|
1184
|
+
done={deriveChecklist(status, actions).filter((i) => i.status === 'complete').length}
|
|
1185
|
+
total={deriveChecklist(status, actions).length}
|
|
1186
|
+
/>
|
|
1187
|
+
<GuidedSetupIntro
|
|
1188
|
+
status={status}
|
|
1189
|
+
chosenAgent={agentChoice.agent}
|
|
1190
|
+
onChooseAgent={(kind) => setAgentChoice({ ...agentChoice, agent: kind })}
|
|
1191
|
+
onStart={() => beginGuidedSetup()}
|
|
1192
|
+
/>
|
|
1193
|
+
<DoneSummary status={status} />
|
|
1194
|
+
{manualOpen && (
|
|
1195
|
+
<WelcomeChecklist
|
|
1196
|
+
status={status}
|
|
1197
|
+
actions={actions}
|
|
1198
|
+
chosenAgent={agentChoice.agent}
|
|
1199
|
+
onChooseAgent={(kind) => setAgentChoice({ ...agentChoice, agent: kind })}
|
|
1200
|
+
showProgress={false}
|
|
1201
|
+
/>
|
|
1202
|
+
)}
|
|
1203
|
+
</div>
|
|
1204
|
+
) : (
|
|
1205
|
+
<div className="py-6 text-center text-[12px] text-muted-foreground">
|
|
1206
|
+
Checking your project…
|
|
1207
|
+
</div>
|
|
1208
|
+
)}
|
|
1209
|
+
<WelcomeFooter
|
|
1210
|
+
status={status}
|
|
1211
|
+
actions={actions}
|
|
1212
|
+
onDismiss={dismiss}
|
|
1213
|
+
manualOpen={manualOpen}
|
|
1214
|
+
onToggleManual={() => setManualOpen((o) => !o)}
|
|
1215
|
+
/>
|
|
1216
|
+
</>
|
|
393
1217
|
)}
|
|
394
|
-
<div className="flex justify-end">
|
|
395
|
-
<Button variant="ghost" size="sm" onClick={dismiss}>
|
|
396
|
-
Dismiss
|
|
397
|
-
</Button>
|
|
398
|
-
</div>
|
|
399
1218
|
</DialogContent>
|
|
400
1219
|
</Dialog>
|
|
401
1220
|
|