@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.
Files changed (116) hide show
  1. package/bin/commands.ts +7 -3
  2. package/bin/init.test.ts +94 -0
  3. package/bin/init.ts +108 -2
  4. package/dist/bin/commands.d.ts +1 -0
  5. package/dist/bin/commands.js +6 -3
  6. package/dist/bin/init.d.ts +4 -0
  7. package/dist/bin/init.js +96 -2
  8. package/dist/server/agents/codexParse.js +31 -0
  9. package/dist/server/agents/detect.d.ts +18 -8
  10. package/dist/server/agents/detect.js +78 -13
  11. package/dist/server/agents/modelRejectionHint.js +1 -1
  12. package/dist/server/agents/prompt.d.ts +15 -1
  13. package/dist/server/agents/prompt.js +59 -37
  14. package/dist/server/agents/runManager.d.ts +23 -2
  15. package/dist/server/agents/runManager.js +36 -8
  16. package/dist/server/client.d.ts +1 -0
  17. package/dist/server/client.js +7 -2
  18. package/dist/server/index.js +208 -16
  19. package/dist/server/nodesPayload.d.ts +14 -1
  20. package/dist/server/nodesPayload.js +15 -2
  21. package/dist/server/projectMode.js +5 -2
  22. package/dist/server/runRegistry.d.ts +52 -1
  23. package/dist/server/runRegistry.js +170 -1
  24. package/dist/server/sourceNav.d.ts +77 -0
  25. package/dist/server/sourceNav.js +503 -0
  26. package/dist/server/subflowRunner.d.ts +48 -1
  27. package/dist/server/subflowRunner.js +34 -7
  28. package/dist/server/updateCheck.d.ts +68 -0
  29. package/dist/server/updateCheck.js +142 -0
  30. package/dist/src/engine/executor.js +4 -3
  31. package/dist/src/engine/registry.d.ts +27 -2
  32. package/dist/src/engine/registry.js +84 -2
  33. package/dist/src/nodes/index.d.ts +8 -2
  34. package/dist/src/nodes/index.js +7 -3
  35. package/dist/src/nodes/login.d.ts +3 -1
  36. package/dist/src/nodes/login.js +2 -2
  37. package/package.json +28 -7
  38. package/server/agentRoute.test.ts +20 -0
  39. package/server/agents/codexParse.test.ts +37 -0
  40. package/server/agents/codexParse.ts +34 -0
  41. package/server/agents/detect.test.ts +26 -7
  42. package/server/agents/detect.ts +82 -14
  43. package/server/agents/modelRejectionHint.ts +2 -1
  44. package/server/agents/prompt.test.ts +128 -0
  45. package/server/agents/prompt.ts +102 -3
  46. package/server/agents/runManager.test.ts +9 -0
  47. package/server/agents/runManager.ts +37 -7
  48. package/server/client.ts +10 -4
  49. package/server/index.ts +213 -15
  50. package/server/nodeRun.test.ts +189 -0
  51. package/server/nodesPayload.test.ts +38 -0
  52. package/server/nodesPayload.ts +28 -3
  53. package/server/projectMode.ts +5 -2
  54. package/server/runRegistry.ts +200 -3
  55. package/server/setupStatus.test.ts +3 -0
  56. package/server/sourceNav.test.ts +382 -0
  57. package/server/sourceNav.ts +644 -0
  58. package/server/sourceNavRoute.test.ts +163 -0
  59. package/server/stepDrill.test.ts +380 -0
  60. package/server/subflowRunner.ts +92 -11
  61. package/server/updateCheck.test.ts +209 -0
  62. package/server/updateCheck.ts +175 -0
  63. package/server/workflowTestRoute.test.ts +1 -1
  64. package/src/App.tsx +82 -2
  65. package/src/components/AgentConsole.tsx +7 -280
  66. package/src/components/AgentStream.test.tsx +88 -0
  67. package/src/components/AgentStream.tsx +303 -0
  68. package/src/components/CreateModal.tsx +43 -9
  69. package/src/components/Dock.tsx +1 -1
  70. package/src/components/EmptyState.test.tsx +49 -0
  71. package/src/components/EmptyState.tsx +61 -0
  72. package/src/components/EnvironmentDialog.tsx +4 -1
  73. package/src/components/EnvironmentPicker.tsx +8 -3
  74. package/src/components/EnvironmentsDialog.tsx +4 -1
  75. package/src/components/InfraPanel.tsx +30 -2
  76. package/src/components/InfrastructureDialog.test.tsx +97 -0
  77. package/src/components/InfrastructureDialog.tsx +111 -0
  78. package/src/components/Inspector.tsx +9 -26
  79. package/src/components/RunbookView.tsx +70 -6
  80. package/src/components/SettingsDialog.tsx +4 -59
  81. package/src/components/Sidebar.tsx +6 -26
  82. package/src/components/SourceNavigator.test.tsx +226 -0
  83. package/src/components/SourceNavigator.tsx +520 -0
  84. package/src/components/StatusBar.tsx +227 -26
  85. package/src/components/Toolbar.tsx +28 -16
  86. package/src/components/UpdateChip.test.tsx +31 -0
  87. package/src/components/WelcomeDialog.test.tsx +384 -13
  88. package/src/components/WelcomeDialog.tsx +996 -177
  89. package/src/engine/executor.ts +4 -3
  90. package/src/engine/registry.sourceRef.test.ts +112 -0
  91. package/src/engine/registry.ts +103 -2
  92. package/src/lib/guidedQuestions.test.ts +224 -0
  93. package/src/lib/guidedQuestions.ts +181 -0
  94. package/src/lib/runbookModel.ts +3 -3
  95. package/src/lib/runbookProjection.test.ts +43 -0
  96. package/src/lib/runbookProjection.ts +26 -6
  97. package/src/nodes/index.ts +10 -3
  98. package/src/nodes/login.ts +5 -2
  99. package/src/store/agentClient.ts +27 -8
  100. package/src/store/apiTree.ts +12 -0
  101. package/src/store/builderStore.test.ts +441 -99
  102. package/src/store/builderStore.ts +385 -314
  103. package/src/store/nodeMeta.ts +10 -2
  104. package/src/store/serverRunner.ts +56 -5
  105. package/src/store/setupClient.ts +7 -2
  106. package/src/store/sourceNavClient.ts +48 -0
  107. package/src/store/updateClient.ts +50 -0
  108. package/studio-dist/assets/index-BbzppDpt.js +65 -0
  109. package/studio-dist/assets/index-CLf6blcA.css +1 -0
  110. package/studio-dist/index.html +2 -2
  111. package/templates/skills/emberflow-basics/SKILL.md +29 -1
  112. package/templates/skills/emberflow-model-process/SKILL.md +92 -9
  113. package/templates/skills/emberflow-new-workflow/SKILL.md +8 -1
  114. package/templates/skills/emberflow-review-workflow/SKILL.md +64 -1
  115. package/studio-dist/assets/index-DNJwW-hM.css +0 -1
  116. package/studio-dist/assets/index-O26dKRjW.js +0 -65
@@ -1,12 +1,14 @@
1
1
  import { describe, expect, it } from 'vitest';
2
2
  import { renderToStaticMarkup } from 'react-dom/server';
3
- import { WelcomeChecklist } from './WelcomeDialog';
3
+ import { DoneSummary, GuidedSetupIntro, GuidedSetupPanes, WelcomeChecklist } from './WelcomeDialog';
4
+ import type { WelcomeChecklistActions } from './WelcomeDialog';
4
5
  import type { SetupStatus } from '../store/setupClient';
5
6
 
6
7
  /** A pristine, just-initialized project: no agent, no environments, no skills,
7
8
  * only the hello example op. */
8
9
  const FRESH: SetupStatus = {
9
10
  agents: [],
11
+ git: { repo: false },
10
12
  environments: { configured: false, count: 0, protectedCount: 0, anyAuthConfigured: false },
11
13
  skills: { claude: false, codex: false },
12
14
  language: 'typescript',
@@ -22,6 +24,7 @@ const CONFIGURED: SetupStatus = {
22
24
  { kind: 'claude', version: '2.1.0' },
23
25
  { kind: 'codex', version: '0.5.3' },
24
26
  ],
27
+ git: { repo: true },
25
28
  environments: { configured: true, count: 2, protectedCount: 1, anyAuthConfigured: true },
26
29
  skills: { claude: true, codex: false },
27
30
  language: 'typescript',
@@ -37,15 +40,30 @@ describe('WelcomeChecklist', () => {
37
40
  it('renders every checklist row', () => {
38
41
  const out = html(FRESH);
39
42
  for (const title of [
40
- 'Coding agent detected',
41
- 'Environments configured',
42
- 'Secrets & auth',
43
- 'Agent skills installed',
44
- 'Infrastructure scouted',
43
+ 'Git repository',
44
+ 'Coding agent',
45
+ 'Environments',
46
+ 'Agent skills',
47
+ 'Project scanned',
45
48
  'First operation',
46
49
  ]) {
47
50
  expect(out).toContain(title);
48
51
  }
52
+ // Secrets & auth deliberately has no row — it lives in Manage Environment.
53
+ expect(out).not.toContain('Secrets');
54
+ });
55
+
56
+ it('fresh project: shows the git init command and disables agent-driven actions', () => {
57
+ const out = html(FRESH);
58
+ expect(out).toContain('git init && git add -A && git commit -m "initial"');
59
+ // With no git repo, the agent-dependent rows say to initialize git first.
60
+ expect(out).toContain('Initialize git first');
61
+ });
62
+
63
+ it('git row completes once the repo exists', () => {
64
+ const out = html(CONFIGURED);
65
+ expect(out).toContain('Repository ready');
66
+ expect(out).not.toContain('Initialize git first');
49
67
  });
50
68
 
51
69
  it('fresh project: shows the install hint and no completed rows', () => {
@@ -57,9 +75,10 @@ describe('WelcomeChecklist', () => {
57
75
  expect(out).not.toContain('aria-label="done"');
58
76
  });
59
77
 
60
- it('fresh project: the scout button is disabled without an agent', () => {
61
- const out = html(FRESH);
62
- // The Scout button carries the "detect an agent first" tooltip and is disabled.
78
+ it('git present but no agent: the scout button is disabled with the agent reason', () => {
79
+ // Git is the earlier prerequisite; with it satisfied the scout row falls
80
+ // through to the "detect an agent" reason.
81
+ const out = html({ ...FRESH, git: { repo: true } });
63
82
  expect(out).toContain('Detect a coding agent first');
64
83
  expect(out).toMatch(/Scout/);
65
84
  expect(out).toContain('disabled');
@@ -85,7 +104,6 @@ describe('WelcomeChecklist', () => {
85
104
  <WelcomeChecklist status={CONFIGURED} chosenAgent="codex" onChooseAgent={() => {}} />,
86
105
  );
87
106
  expect(out).toContain('Choose coding agent');
88
- expect(out).toContain('Both detected');
89
107
  // Codex is the active choice; Claude is not.
90
108
  expect(out).toMatch(/aria-pressed="true"[^>]*>Codex/);
91
109
  expect(out).toMatch(/aria-pressed="false"[^>]*>Claude/);
@@ -100,8 +118,361 @@ describe('WelcomeChecklist', () => {
100
118
  expect(out).toContain('claude 2.1.0');
101
119
  });
102
120
 
103
- it('shows checklist progress', () => {
104
- expect(html(FRESH)).toContain('0/6 done');
105
- expect(html(CONFIGURED)).toContain('6/6 done');
121
+ it('progress + next-step emphasis: fresh shows 0/6 with the first row emphasized; fully-done has no next step', () => {
122
+ const fresh = html(FRESH);
123
+ expect(fresh).toContain('0 of 6 steps done');
124
+ expect(fresh).toContain('bg-highlight/[0.06]'); // exactly one emphasized row
125
+ expect(fresh.split('bg-highlight/[0.06]').length - 1).toBe(1);
126
+
127
+ const done = html(CONFIGURED);
128
+ expect(done).toContain('6 of 6 steps done');
129
+ expect(done).not.toContain('bg-highlight/[0.06]'); // nothing left to emphasize
130
+ });
131
+ });
132
+
133
+ /** Git + a coding agent (the two manual prerequisites) satisfied, but the rest
134
+ * of setup still to do — the state in which Start setup is enabled. */
135
+ const READY: SetupStatus = {
136
+ ...FRESH,
137
+ git: { repo: true },
138
+ agents: [{ kind: 'claude', version: '2.1.0' }],
139
+ };
140
+
141
+ const NOOP: WelcomeChecklistActions = {
142
+ onOpenSettings: () => {},
143
+ onSetupEnvironments: () => {},
144
+ onOpenEnvironments: () => {},
145
+ onOpenHelloOp: () => {},
146
+ onScoutInfrastructure: () => {},
147
+ };
148
+
149
+ describe('GuidedSetupIntro (idle phase)', () => {
150
+ it('fresh project (no git): Start setup is disabled with the git reason', () => {
151
+ const out = renderToStaticMarkup(<GuidedSetupIntro status={FRESH} onStart={() => {}} />);
152
+ expect(out).toContain('Let the agent set this up');
153
+ expect(out).toContain('Start setup');
154
+ expect(out).toContain('disabled=""');
155
+ expect(out).toContain('Initialize git first');
156
+ });
157
+
158
+ it('git present but no agent: disabled with the agent reason', () => {
159
+ const out = renderToStaticMarkup(
160
+ <GuidedSetupIntro status={{ ...FRESH, git: { repo: true } }} onStart={() => {}} />,
161
+ );
162
+ expect(out).toContain('disabled=""');
163
+ expect(out).toContain('Detect a coding agent first');
164
+ });
165
+
166
+ it('ready project: Start enabled, the step-list names what still remains', () => {
167
+ const out = renderToStaticMarkup(<GuidedSetupIntro status={READY} onStart={() => {}} />);
168
+ expect(out).not.toContain('disabled=""');
169
+ // Remaining steps derived from checklist state.
170
+ expect(out).toContain('Scan your code for existing infrastructure');
171
+ expect(out).toContain('Set up environments');
172
+ expect(out).toContain('Install the agent skills');
173
+ expect(out).toContain('Verify the skills and its own connection');
174
+ });
175
+
176
+ it('configured project: step-list drops done steps but always verifies connection', () => {
177
+ const out = renderToStaticMarkup(<GuidedSetupIntro status={CONFIGURED} onStart={() => {}} />);
178
+ expect(out).not.toContain('Scan your code');
179
+ expect(out).not.toContain('Set up environments');
180
+ expect(out).not.toContain('Install the agent skills');
181
+ expect(out).toContain('Verify the skills and its own connection');
182
+ });
183
+
184
+ it('two agents: the guided card hosts the backend picker with the chosen agent pressed', () => {
185
+ const twoAgents: SetupStatus = { ...READY, agents: CONFIGURED.agents };
186
+ const out = renderToStaticMarkup(
187
+ <GuidedSetupIntro status={twoAgents} chosenAgent="claude" onChooseAgent={() => {}} onStart={() => {}} />,
188
+ );
189
+ expect(out).toContain('Runs with');
190
+ expect(out).toMatch(/aria-pressed="true"[^>]*>Claude/);
191
+ });
192
+
193
+ it('single agent: no picker in the guided card', () => {
194
+ const out = renderToStaticMarkup(
195
+ <GuidedSetupIntro status={READY} chosenAgent="claude" onChooseAgent={() => {}} onStart={() => {}} />,
196
+ );
197
+ expect(out).not.toContain('Choose coding agent');
198
+ });
199
+ });
200
+
201
+ describe('DoneSummary', () => {
202
+ it('joins completed step titles into one line, with agent versions', () => {
203
+ const out = renderToStaticMarkup(<DoneSummary status={READY} />);
204
+ expect(out).toContain('Git repository');
205
+ expect(out).toContain('claude 2.1.0');
206
+ expect(out).not.toContain('Environments');
207
+ });
208
+
209
+ it('renders nothing when no steps are complete', () => {
210
+ const none: SetupStatus = { ...FRESH };
211
+ expect(renderToStaticMarkup(<DoneSummary status={none} />)).toBe('');
212
+ });
213
+ });
214
+
215
+ describe('GuidedSetupPanes (running/done phase)', () => {
216
+ const stream = [
217
+ { type: 'message' as const, text: 'Reading the ground truth first.' },
218
+ { type: 'message' as const, text: 'Which environments do you want?' },
219
+ ];
220
+
221
+ it('running: two panes — the live checklist AND the embedded stream + working input', () => {
222
+ const out = renderToStaticMarkup(
223
+ <GuidedSetupPanes
224
+ status={READY}
225
+ actions={NOOP}
226
+ events={stream}
227
+ running={true}
228
+ onFollowUp={() => {}}
229
+ onFinishComplete={() => {}}
230
+ onContinue={() => {}}
231
+ followUpRef={{ current: null }}
232
+ />,
233
+ );
234
+ // Left: a checklist row title. Right: the stream prose + the running header.
235
+ expect(out).toContain('Git repository');
236
+ expect(out).toContain('Reading the ground truth first.');
237
+ expect(out).toContain('Setting things up…');
238
+ // While the agent works, the left pane is a READ-ONLY progress map — the
239
+ // interactive actions are exactly what the agent is doing, so no buttons.
240
+ expect(out).not.toContain('Set up with AI');
241
+ expect(out).not.toContain('Scout');
242
+ expect(out).not.toContain('Open hello op');
243
+ // The step being worked on (next incomplete) spins while the run is live.
244
+ expect(out).toContain('aria-label="in progress"');
245
+ expect(out).toContain('animate-spin');
246
+ // Follow-up input is disabled while working.
247
+ expect(out).toContain('Agent is working…');
248
+ // No done-phase footer CTA while running.
249
+ expect(out).not.toContain('Continue setup');
250
+ expect(out).not.toContain("You're set");
251
+ });
252
+
253
+ it('done + incomplete: shows the Continue setup CTA', () => {
254
+ const out = renderToStaticMarkup(
255
+ <GuidedSetupPanes
256
+ status={READY}
257
+ actions={NOOP}
258
+ events={stream}
259
+ running={false}
260
+ onFollowUp={() => {}}
261
+ onFinishComplete={() => {}}
262
+ onContinue={() => {}}
263
+ followUpRef={{ current: null }}
264
+ />,
265
+ );
266
+ expect(out).toContain('Waiting on you');
267
+ // Not running → no spinner on the map.
268
+ expect(out).not.toContain('aria-label="in progress"');
269
+ expect(out).toContain('Continue setup');
270
+ expect(out).not.toContain("You're set");
271
+ });
272
+
273
+ it('done + failed: red header and a Try again CTA instead of Continue', () => {
274
+ const out = renderToStaticMarkup(
275
+ <GuidedSetupPanes
276
+ status={READY}
277
+ actions={NOOP}
278
+ events={[{ type: 'error', text: 'codex exited with code 1' }]}
279
+ running={false}
280
+ failed={true}
281
+ onFollowUp={() => {}}
282
+ onFinishComplete={() => {}}
283
+ onContinue={() => {}}
284
+ onRetry={() => {}}
285
+ followUpRef={{ current: null }}
286
+ />,
287
+ );
288
+ expect(out).toContain('Setup hit a problem');
289
+ expect(out).toContain('bg-destructive');
290
+ expect(out).toContain('Try again');
291
+ expect(out).not.toContain('Continue setup');
292
+ expect(out).not.toContain('Setup complete');
293
+ });
294
+
295
+ it('failed with a backend at fault + another agent detected: CTA switches backend', () => {
296
+ const out = renderToStaticMarkup(
297
+ <GuidedSetupPanes
298
+ status={READY}
299
+ actions={NOOP}
300
+ events={[{ type: 'error', text: 'model rejected (hint: your codex CLI may be too old…)' }]}
301
+ running={false}
302
+ failed={true}
303
+ onFollowUp={() => {}}
304
+ onFinishComplete={() => {}}
305
+ onContinue={() => {}}
306
+ onRetry={() => {}}
307
+ retryAgent="claude"
308
+ onRetryWith={() => {}}
309
+ followUpRef={{ current: null }}
310
+ />,
311
+ );
312
+ expect(out).toContain('Try again with Claude');
313
+ });
314
+
315
+ it('renders the auto-failover notice above the stream', () => {
316
+ const out = renderToStaticMarkup(
317
+ <GuidedSetupPanes
318
+ status={READY}
319
+ actions={NOOP}
320
+ events={stream}
321
+ running={true}
322
+ notice="Codex failed to start — switched to Claude and retried automatically."
323
+ onFollowUp={() => {}}
324
+ onFinishComplete={() => {}}
325
+ onContinue={() => {}}
326
+ followUpRef={{ current: null }}
327
+ />,
328
+ );
329
+ expect(out).toContain('switched to Claude and retried automatically');
330
+ });
331
+
332
+ /** Agent final message ending with the emberflow-questions contract block. */
333
+ const QUESTION_TEXT =
334
+ 'Steps 1–4 done. Two things to decide.\n\n' +
335
+ '```emberflow-questions\n' +
336
+ '{"questions":[' +
337
+ '{"id":"envs","text":"Which environments?","options":["dev + prod","dev + staging + prod"],"custom":true},' +
338
+ '{"id":"first-build","text":"What do you want to build first?","options":[{"label":"Just look around","action":"finish"}],"custom":true}' +
339
+ ']}\n' +
340
+ '```\n';
341
+ const questionStream = [
342
+ { type: 'message' as const, text: 'Reading the ground truth first.' },
343
+ { type: 'message' as const, text: QUESTION_TEXT },
344
+ ];
345
+
346
+ it('questions block: renders clickable pills instead of the textarea, raw block stripped', () => {
347
+ const out = renderToStaticMarkup(
348
+ <GuidedSetupPanes
349
+ status={READY}
350
+ actions={NOOP}
351
+ events={questionStream}
352
+ running={false}
353
+ onFollowUp={() => {}}
354
+ onFinishComplete={() => {}}
355
+ onContinue={() => {}}
356
+ followUpRef={{ current: null }}
357
+ />,
358
+ );
359
+ // The raw fenced block never renders — not as a code block, not as text.
360
+ expect(out).not.toContain('emberflow-questions');
361
+ // Prose before the block still shows.
362
+ expect(out).toContain('Two things to decide.');
363
+ // Wizard: ONE question at a time — the FIRST question's pills show, with
364
+ // a position counter; the second question is not rendered yet.
365
+ expect(out).toContain('Which environments?');
366
+ expect(out).toMatch(/aria-pressed="false"[^>]*>dev \+ prod</);
367
+ expect(out).toContain('dev + staging + prod');
368
+ expect(out).not.toContain('Just look around');
369
+ expect(out).toContain('1/2');
370
+ // One custom free-text field (current question only).
371
+ expect(out.split('Or type your own…').length - 1).toBe(1);
372
+ // Nothing answered yet → no Send button (it appears on the last step),
373
+ // but the escape hatch is always there.
374
+ expect(out).not.toContain('Send answers');
375
+ expect(out).toContain('Type a reply instead');
376
+ // The form replaces the textarea AND the redundant Continue-setup nudge.
377
+ expect(out).not.toContain('Answer the agent’s questions…');
378
+ expect(out).not.toContain('Continue setup');
379
+ });
380
+
381
+ it('questions block: a why renders as quiet rationale under the question text', () => {
382
+ const whyStream = [
383
+ {
384
+ type: 'message' as const,
385
+ text:
386
+ 'One thing to decide.\n\n```emberflow-questions\n' +
387
+ '{"questions":[{"id":"envs","text":"Which environments?","options":["dev + prod"],' +
388
+ '"why":"Runs stay in Mock until you point them somewhere real."}]}\n```\n',
389
+ },
390
+ ];
391
+ const out = renderToStaticMarkup(
392
+ <GuidedSetupPanes
393
+ status={READY}
394
+ actions={NOOP}
395
+ events={whyStream}
396
+ running={false}
397
+ onFollowUp={() => {}}
398
+ onFinishComplete={() => {}}
399
+ onContinue={() => {}}
400
+ followUpRef={{ current: null }}
401
+ />,
402
+ );
403
+ expect(out).toContain('Which environments?');
404
+ expect(out).toContain('Runs stay in Mock until you point them somewhere real.');
405
+ });
406
+
407
+ it('questions block while running: form suppressed, textarea stays (disabled), block still stripped', () => {
408
+ const out = renderToStaticMarkup(
409
+ <GuidedSetupPanes
410
+ status={READY}
411
+ actions={NOOP}
412
+ events={questionStream}
413
+ running={true}
414
+ onFollowUp={() => {}}
415
+ onFinishComplete={() => {}}
416
+ onContinue={() => {}}
417
+ followUpRef={{ current: null }}
418
+ />,
419
+ );
420
+ expect(out).not.toContain('Send answers');
421
+ expect(out).toContain('Agent is working…');
422
+ expect(out).not.toContain('emberflow-questions');
423
+ });
424
+
425
+ it('questions block + failed run: retry CTA wins, no form', () => {
426
+ const out = renderToStaticMarkup(
427
+ <GuidedSetupPanes
428
+ status={READY}
429
+ actions={NOOP}
430
+ events={[...questionStream, { type: 'error' as const, text: 'codex exited with code 1' }]}
431
+ running={false}
432
+ failed={true}
433
+ onFollowUp={() => {}}
434
+ onFinishComplete={() => {}}
435
+ onContinue={() => {}}
436
+ onRetry={() => {}}
437
+ followUpRef={{ current: null }}
438
+ />,
439
+ );
440
+ expect(out).not.toContain('Send answers');
441
+ expect(out).toContain('Try again');
442
+ });
443
+
444
+ it('no questions block: the follow-up textarea renders unchanged', () => {
445
+ const out = renderToStaticMarkup(
446
+ <GuidedSetupPanes
447
+ status={READY}
448
+ actions={NOOP}
449
+ events={stream}
450
+ running={false}
451
+ onFollowUp={() => {}}
452
+ onFinishComplete={() => {}}
453
+ onContinue={() => {}}
454
+ followUpRef={{ current: null }}
455
+ />,
456
+ );
457
+ expect(out).toContain('Answer the agent’s questions…');
458
+ expect(out).not.toContain('Send answers');
459
+ expect(out).toContain('Continue setup');
460
+ });
461
+
462
+ it('done + complete: shows the "You\'re set" CTA', () => {
463
+ const out = renderToStaticMarkup(
464
+ <GuidedSetupPanes
465
+ status={CONFIGURED}
466
+ actions={NOOP}
467
+ events={stream}
468
+ running={false}
469
+ onFollowUp={() => {}}
470
+ onFinishComplete={() => {}}
471
+ onContinue={() => {}}
472
+ followUpRef={{ current: null }}
473
+ />,
474
+ );
475
+ expect(out).toContain('build your first API');
476
+ expect(out).not.toContain('Continue setup');
106
477
  });
107
478
  });