@compilr-dev/cli 0.7.3 → 0.7.5

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 (46) hide show
  1. package/dist/agent.d.ts +7 -2
  2. package/dist/agent.js +55 -39
  3. package/dist/commands-v2/handlers/project.js +65 -2
  4. package/dist/commands-v2/handlers/settings.js +18 -26
  5. package/dist/compilr-diff-companion.vsix +0 -0
  6. package/dist/db/schema.d.ts +1 -1
  7. package/dist/episodes/index.d.ts +13 -13
  8. package/dist/episodes/index.js +13 -14
  9. package/dist/guide/cli-guide-entries.js +54 -2
  10. package/dist/handlers/delegation-handlers.js +228 -50
  11. package/dist/handlers/interactive-flow-handlers.d.ts +26 -0
  12. package/dist/handlers/interactive-flow-handlers.js +61 -0
  13. package/dist/handlers/propose-alternatives-handlers.d.ts +36 -0
  14. package/dist/handlers/propose-alternatives-handlers.js +65 -0
  15. package/dist/index.js +13 -2
  16. package/dist/repl-v2.d.ts +66 -0
  17. package/dist/repl-v2.js +382 -53
  18. package/dist/shared-handlers.d.ts +57 -5
  19. package/dist/shared-handlers.js +50 -0
  20. package/dist/tools/consult.d.ts +14 -0
  21. package/dist/tools/consult.js +73 -0
  22. package/dist/tools/delegate.d.ts +12 -6
  23. package/dist/tools/delegate.js +35 -19
  24. package/dist/tools/interactive-flow.d.ts +13 -0
  25. package/dist/tools/interactive-flow.js +19 -0
  26. package/dist/tools/platform-adapter.d.ts +14 -2
  27. package/dist/tools/platform-adapter.js +16 -4
  28. package/dist/tools/propose-alternatives.d.ts +13 -0
  29. package/dist/tools/propose-alternatives.js +19 -0
  30. package/dist/tools.d.ts +22 -98
  31. package/dist/tools.js +27 -382
  32. package/dist/ui/markdown-renderer.js +26 -7
  33. package/dist/ui/overlay/data/tutorial-registry.js +2 -0
  34. package/dist/ui/overlay/data/tutorials/projects/interactive-flows.d.ts +9 -0
  35. package/dist/ui/overlay/data/tutorials/projects/interactive-flows.js +94 -0
  36. package/dist/ui/overlay/data/tutorials/projects/new-project.js +56 -20
  37. package/dist/ui/overlay/impl/interactive-flow-overlay-v2.d.ts +79 -0
  38. package/dist/ui/overlay/impl/interactive-flow-overlay-v2.js +580 -0
  39. package/dist/ui/overlay/impl/new-overlay-v2.d.ts +32 -0
  40. package/dist/ui/overlay/impl/new-overlay-v2.js +305 -66
  41. package/dist/ui/overlay/impl/propose-alternatives-overlay-v2.d.ts +53 -0
  42. package/dist/ui/overlay/impl/propose-alternatives-overlay-v2.js +326 -0
  43. package/dist/ui/overlay/index.d.ts +2 -0
  44. package/dist/ui/overlay/index.js +2 -0
  45. package/dist/ui/tool-formatters.js +61 -3
  46. package/package.json +3 -3
@@ -0,0 +1,580 @@
1
+ /**
2
+ * Interactive Flow Overlay V2
3
+ *
4
+ * Multi-screen wizard for the `build_interactive_flow` DSL. Walks the
5
+ * user through a tree of nodes (question / info / branch / summary)
6
+ * defined by the agent. Branches are pure routing — never rendered.
7
+ *
8
+ * The state-machine logic (next-resolution, branch evaluation,
9
+ * backtrack) lives in `@compilr-dev/sdk/flow-runner` as pure helpers
10
+ * shared with Desktop. This overlay only handles per-node rendering +
11
+ * keyboard input + the renderer-specific transient state (cursor
12
+ * position, multi-selections, text buffer).
13
+ *
14
+ * Renderer collapses: the SDK has many `render` variants (tile-row,
15
+ * card-grid, segmented, …) that target GUIs. The terminal collapses
16
+ * them all to one rendering per input mode — `render` is ignored, as
17
+ * are `tone`, `density`, `icon`.
18
+ *
19
+ * Phase coverage (per spec):
20
+ * - Phase A: single + text(single-line) + info + summary + branch ✓
21
+ * - Phase B: multi + text(multiline) ✓
22
+ * - Phase C: proposal mode — rendered inline as a numbered list with
23
+ * pros + cons under each option. Same selection UX as 'single' mode
24
+ * (arrows / 1-9 / Enter). Result is the chosen Proposal's id, which
25
+ * `lookupLabel` resolves to the option's label for the summary table.
26
+ *
27
+ * Spec: project-docs/00-requirements/compilr-dev-cli/interactive-flow-cli-spec.md
28
+ */
29
+ import { resolveNext, walkPastBranches, applyBacktrack, lookupLabel, } from '@compilr-dev/sdk';
30
+ import { BaseOverlayV2, renderBorder, wrapText } from '../../base/index.js';
31
+ import { renderMarkdown } from '../../conversation.js';
32
+ // =============================================================================
33
+ // Overlay
34
+ // =============================================================================
35
+ export class InteractiveFlowOverlayV2 extends BaseOverlayV2 {
36
+ type = 'inline';
37
+ id = 'interactive-flow-overlay-v2';
38
+ flow;
39
+ constructor(options) {
40
+ const initialPath = walkPastBranches(options.flow, [options.flow.startNode], {});
41
+ super({
42
+ path: initialPath,
43
+ answers: {},
44
+ answerLabels: {},
45
+ startedAt: Date.now(),
46
+ selectedIndex: 0,
47
+ multiSelections: new Set(),
48
+ inputBuffer: '',
49
+ warning: '',
50
+ });
51
+ this.flow = options.flow;
52
+ this.minHeight = 22;
53
+ }
54
+ // ───────────────────────────────────────────────────────────────────────────
55
+ // Public API
56
+ // ───────────────────────────────────────────────────────────────────────────
57
+ renderContent(context) {
58
+ const s = context.styles;
59
+ const cols = context.width;
60
+ const border = renderBorder(cols, s);
61
+ const lines = [];
62
+ const node = this.currentNode();
63
+ // Header
64
+ lines.push(border);
65
+ for (const line of wrapText(this.flow.title, cols - 2)) {
66
+ lines.push(' ' + s.primaryBold(line));
67
+ }
68
+ if (this.flow.description) {
69
+ for (const line of wrapText(this.flow.description, cols - 4)) {
70
+ lines.push(' ' + s.muted(line));
71
+ }
72
+ }
73
+ lines.push('');
74
+ // Body — dispatch on node type
75
+ if (node === undefined) {
76
+ lines.push(' ' + s.error('Malformed flow: current node is missing'));
77
+ }
78
+ else if (node.type === 'question') {
79
+ lines.push(...this.renderQuestion(node, s, cols));
80
+ }
81
+ else if (node.type === 'info') {
82
+ lines.push(...this.renderInfo(node, s, cols));
83
+ }
84
+ else if (node.type === 'summary') {
85
+ lines.push(...this.renderSummary(node, s, cols));
86
+ }
87
+ // Footer / warnings
88
+ lines.push('');
89
+ if (this.state.warning) {
90
+ lines.push(' ' + s.warning(`! ${this.state.warning}`));
91
+ lines.push('');
92
+ }
93
+ lines.push(' ' + s.muted(this.footerHints(node)));
94
+ lines.push(border);
95
+ return lines;
96
+ }
97
+ handleKey(key) {
98
+ // Ctrl+C — hard cancel
99
+ if (key.ctrl && key.name === 'c') {
100
+ return this.close(this.makeAborted('escape'));
101
+ }
102
+ const node = this.currentNode();
103
+ if (node === undefined) {
104
+ // Malformed flow — close as aborted
105
+ return this.close(this.makeAborted('unknown'));
106
+ }
107
+ // 'q' — soft cancel from any node
108
+ if (key.char === 'q' && !key.ctrl && !key.meta) {
109
+ return this.close(this.makeAborted('escape'));
110
+ }
111
+ // Esc — back one user-visible step, or cancel if no prior visible node
112
+ if (key.name === 'escape') {
113
+ return this.handleBackOrCancel();
114
+ }
115
+ if (node.type === 'question') {
116
+ return this.handleQuestionKey(node, key);
117
+ }
118
+ if (node.type === 'info') {
119
+ return this.handleInfoKey(node, key);
120
+ }
121
+ if (node.type === 'summary') {
122
+ return this.handleSummaryKey(key);
123
+ }
124
+ return this.noAction();
125
+ }
126
+ getCloseSummary(result) {
127
+ const s = this.getStyles();
128
+ if (!result.completed) {
129
+ const stepCount = result.path.length;
130
+ return s.muted('Flow: ') + s.warning(`cancelled at step ${String(stepCount)}`);
131
+ }
132
+ const stepCount = result.path.length;
133
+ return s.muted('Flow: ') + s.success(`completed — ${String(stepCount)} steps`);
134
+ }
135
+ // ───────────────────────────────────────────────────────────────────────────
136
+ // Current node helpers
137
+ // ───────────────────────────────────────────────────────────────────────────
138
+ currentNodeId() {
139
+ return this.state.path[this.state.path.length - 1];
140
+ }
141
+ currentNode() {
142
+ const id = this.currentNodeId();
143
+ if (id === undefined)
144
+ return undefined;
145
+ return this.flow.nodes[id];
146
+ }
147
+ // ───────────────────────────────────────────────────────────────────────────
148
+ // QuestionNode rendering & input
149
+ // ───────────────────────────────────────────────────────────────────────────
150
+ renderQuestion(node, s, cols) {
151
+ const out = [];
152
+ for (const line of wrapText(node.prompt, cols - 2)) {
153
+ out.push(' ' + s.primaryBold(line));
154
+ }
155
+ if (node.description) {
156
+ for (const line of wrapText(node.description, cols - 4)) {
157
+ out.push(' ' + s.muted(line));
158
+ }
159
+ }
160
+ out.push('');
161
+ const mode = node.input.mode;
162
+ if (mode === 'single') {
163
+ for (let i = 0; i < node.input.choices.length; i++) {
164
+ const choice = node.input.choices[i];
165
+ const isCursor = i === this.state.selectedIndex;
166
+ const cursor = isCursor ? ' ▶' : ' ';
167
+ const num = i + 1 <= 9 ? ` ${String(i + 1)} ` : ' ';
168
+ const label = this.tinted(choice.label, choice.tint, s);
169
+ out.push(`${cursor}${num}${label}`);
170
+ if (choice.description) {
171
+ for (const line of wrapText(choice.description, cols - 8)) {
172
+ out.push(' ' + s.muted(line));
173
+ }
174
+ }
175
+ }
176
+ return out;
177
+ }
178
+ if (mode === 'multi') {
179
+ for (let i = 0; i < node.input.choices.length; i++) {
180
+ const choice = node.input.choices[i];
181
+ const isCursor = i === this.state.selectedIndex;
182
+ const isSelected = this.state.multiSelections.has(i);
183
+ const cursor = isCursor ? ' ▶' : ' ';
184
+ const box = isSelected ? '[x]' : '[ ]';
185
+ const num = i + 1 <= 9 ? ` ${String(i + 1)} ` : ' ';
186
+ const label = this.tinted(choice.label, choice.tint, s);
187
+ out.push(`${cursor} ${box}${num}${label}`);
188
+ if (choice.description) {
189
+ for (const line of wrapText(choice.description, cols - 10)) {
190
+ out.push(' ' + s.muted(line));
191
+ }
192
+ }
193
+ }
194
+ out.push('');
195
+ const min = node.input.min;
196
+ const max = node.input.max;
197
+ const constraintParts = [];
198
+ if (min !== undefined)
199
+ constraintParts.push(`min ${String(min)}`);
200
+ if (max !== undefined)
201
+ constraintParts.push(`max ${String(max)}`);
202
+ const constraints = constraintParts.length > 0 ? ` (${constraintParts.join(', ')})` : '';
203
+ out.push(' ' + s.muted(`Selected: ${String(this.state.multiSelections.size)}${constraints}`));
204
+ return out;
205
+ }
206
+ if (mode === 'text') {
207
+ const isMultiline = node.input.multiline === true;
208
+ const placeholder = node.input.placeholder ?? '';
209
+ if (isMultiline) {
210
+ // Multi-line: show buffer in a box. Up to 8 lines visible.
211
+ const bufLines = this.state.inputBuffer.split('\n');
212
+ const display = bufLines.length === 1 && bufLines[0].length === 0
213
+ ? [placeholder ? s.muted(placeholder) : '']
214
+ : bufLines;
215
+ const visible = display.slice(-8);
216
+ for (const line of visible) {
217
+ out.push(' ▶ ' + line);
218
+ }
219
+ out.push(' ' + s.muted(' (Ctrl+D submits, Enter adds a line)'));
220
+ return out;
221
+ }
222
+ const display = this.state.inputBuffer.length === 0 && placeholder
223
+ ? s.muted(placeholder)
224
+ : this.state.inputBuffer;
225
+ out.push(' ▶ ' + display + '_');
226
+ return out;
227
+ }
228
+ // mode === 'proposal' — numbered list with pros/cons inline.
229
+ // Same selection UX as 'single' mode (arrows / 1-9 / Enter) but each
230
+ // option also surfaces its pros + cons below the label so the user can
231
+ // compare without leaving the flow modal.
232
+ for (let i = 0; i < node.input.options.length; i++) {
233
+ const opt = node.input.options[i];
234
+ const isCursor = i === this.state.selectedIndex;
235
+ const cursor = isCursor ? ' ▶' : ' ';
236
+ const num = i + 1 <= 9 ? ` ${String(i + 1)} ` : ' ';
237
+ const label = this.tinted(opt.label, opt.tint, s);
238
+ out.push(`${cursor}${num}${label}`);
239
+ if (opt.pros && opt.pros.length > 0) {
240
+ for (const pro of opt.pros) {
241
+ for (const w of wrapText(pro, cols - 10)) {
242
+ out.push(' ' + s.success('+ ' + w));
243
+ }
244
+ }
245
+ }
246
+ if (opt.cons && opt.cons.length > 0) {
247
+ for (const con of opt.cons) {
248
+ for (const w of wrapText(con, cols - 10)) {
249
+ out.push(' ' + s.warning('− ' + w));
250
+ }
251
+ }
252
+ }
253
+ // Spacer between options (skip after last to keep footer tight)
254
+ if (i < node.input.options.length - 1)
255
+ out.push('');
256
+ }
257
+ return out;
258
+ }
259
+ handleQuestionKey(node, key) {
260
+ const mode = node.input.mode;
261
+ if (mode === 'proposal') {
262
+ // node.input is the proposal variant (TS narrows via the mode check)
263
+ const input = node.input;
264
+ const count = input.options.length;
265
+ if (key.name === 'up') {
266
+ this.state.selectedIndex = Math.max(0, this.state.selectedIndex - 1);
267
+ return this.rerender();
268
+ }
269
+ if (key.name === 'down') {
270
+ this.state.selectedIndex = Math.min(count - 1, this.state.selectedIndex + 1);
271
+ return this.rerender();
272
+ }
273
+ if (key.char && /^[1-9]$/.test(key.char)) {
274
+ const idx = parseInt(key.char, 10) - 1;
275
+ if (idx < count) {
276
+ this.state.selectedIndex = idx;
277
+ return this.rerender();
278
+ }
279
+ }
280
+ if (key.name === 'return' || key.name === 'enter') {
281
+ const opt = input.options[this.state.selectedIndex];
282
+ return this.advance(opt.id, lookupLabel(node, opt.id));
283
+ }
284
+ return this.noAction();
285
+ }
286
+ if (mode === 'single') {
287
+ const count = node.input.choices.length;
288
+ if (key.name === 'up') {
289
+ this.state.selectedIndex = Math.max(0, this.state.selectedIndex - 1);
290
+ return this.rerender();
291
+ }
292
+ if (key.name === 'down') {
293
+ this.state.selectedIndex = Math.min(count - 1, this.state.selectedIndex + 1);
294
+ return this.rerender();
295
+ }
296
+ if (key.char && /^[1-9]$/.test(key.char)) {
297
+ const idx = parseInt(key.char, 10) - 1;
298
+ if (idx < count) {
299
+ this.state.selectedIndex = idx;
300
+ return this.rerender();
301
+ }
302
+ }
303
+ if (key.name === 'return' || key.name === 'enter') {
304
+ const choice = node.input.choices[this.state.selectedIndex];
305
+ return this.advance(choice.id, lookupLabel(node, choice.id));
306
+ }
307
+ return this.noAction();
308
+ }
309
+ if (mode === 'multi') {
310
+ // node.input is the multi variant here (TS narrows via mode check)
311
+ const input = node.input;
312
+ const count = input.choices.length;
313
+ if (key.name === 'up') {
314
+ this.state.selectedIndex = Math.max(0, this.state.selectedIndex - 1);
315
+ return this.rerender();
316
+ }
317
+ if (key.name === 'down') {
318
+ this.state.selectedIndex = Math.min(count - 1, this.state.selectedIndex + 1);
319
+ return this.rerender();
320
+ }
321
+ if (key.char === ' ' || key.name === 'space') {
322
+ if (this.state.multiSelections.has(this.state.selectedIndex)) {
323
+ this.state.multiSelections.delete(this.state.selectedIndex);
324
+ }
325
+ else {
326
+ this.state.multiSelections.add(this.state.selectedIndex);
327
+ }
328
+ return this.rerender();
329
+ }
330
+ if (key.char && /^[1-9]$/.test(key.char)) {
331
+ const idx = parseInt(key.char, 10) - 1;
332
+ if (idx < count) {
333
+ this.state.selectedIndex = idx;
334
+ return this.rerender();
335
+ }
336
+ }
337
+ if (key.name === 'return' || key.name === 'enter') {
338
+ const selected = [...this.state.multiSelections].sort((a, b) => a - b);
339
+ const min = input.min ?? 0;
340
+ const max = input.max ?? Infinity;
341
+ if (selected.length < min) {
342
+ this.state.warning = `Pick at least ${String(min)}`;
343
+ return this.rerender();
344
+ }
345
+ if (selected.length > max) {
346
+ this.state.warning = `Pick at most ${String(max)}`;
347
+ return this.rerender();
348
+ }
349
+ const answerIds = selected.map((i) => input.choices[i].id);
350
+ return this.advance(answerIds, lookupLabel(node, answerIds));
351
+ }
352
+ return this.noAction();
353
+ }
354
+ // mode === 'text' (after eliminating single/multi/proposal above).
355
+ // node.input is the text variant; access `multiline` directly.
356
+ const isMultiline = 'multiline' in node.input && node.input.multiline === true;
357
+ if (key.name === 'backspace') {
358
+ this.state.inputBuffer = this.state.inputBuffer.slice(0, -1);
359
+ return this.rerender();
360
+ }
361
+ if (isMultiline) {
362
+ // Ctrl+D submits, Enter adds a newline
363
+ if (key.ctrl && (key.name === 'd' || key.char === 'd')) {
364
+ return this.advance(this.state.inputBuffer, lookupLabel(node, this.state.inputBuffer));
365
+ }
366
+ if (key.name === 'return' || key.name === 'enter') {
367
+ this.state.inputBuffer += '\n';
368
+ return this.rerender();
369
+ }
370
+ }
371
+ else {
372
+ // Single-line: Enter submits
373
+ if (key.name === 'return' || key.name === 'enter') {
374
+ return this.advance(this.state.inputBuffer, lookupLabel(node, this.state.inputBuffer));
375
+ }
376
+ }
377
+ if (key.char && key.char.length === 1 && !key.ctrl && !key.meta) {
378
+ this.state.inputBuffer += key.char;
379
+ return this.rerender();
380
+ }
381
+ return this.noAction();
382
+ }
383
+ // ───────────────────────────────────────────────────────────────────────────
384
+ // InfoNode
385
+ // ───────────────────────────────────────────────────────────────────────────
386
+ renderInfo(node, s, cols) {
387
+ const out = [];
388
+ out.push(' ' + s.primaryBold(node.title));
389
+ out.push('');
390
+ let body;
391
+ try {
392
+ body = renderMarkdown(node.body);
393
+ }
394
+ catch {
395
+ body = node.body;
396
+ }
397
+ for (const line of body.split('\n')) {
398
+ // Wrap each rendered line within terminal width
399
+ for (const w of wrapText(line.trimEnd(), cols - 4)) {
400
+ out.push(' ' + w);
401
+ }
402
+ }
403
+ return out;
404
+ }
405
+ handleInfoKey(node, key) {
406
+ if (key.name === 'return' || key.name === 'enter') {
407
+ return this.advanceInfo(node);
408
+ }
409
+ return this.noAction();
410
+ }
411
+ // ───────────────────────────────────────────────────────────────────────────
412
+ // SummaryNode
413
+ // ───────────────────────────────────────────────────────────────────────────
414
+ renderSummary(node, s, cols) {
415
+ const out = [];
416
+ out.push(' ' + s.primaryBold(node.title));
417
+ if (node.recap) {
418
+ out.push('');
419
+ let recap;
420
+ try {
421
+ recap = renderMarkdown(node.recap);
422
+ }
423
+ catch {
424
+ recap = node.recap;
425
+ }
426
+ for (const line of recap.split('\n')) {
427
+ for (const w of wrapText(line.trimEnd(), cols - 4)) {
428
+ out.push(' ' + w);
429
+ }
430
+ }
431
+ }
432
+ out.push('');
433
+ // Answer table — iterate path entries that are question nodes
434
+ const rows = [];
435
+ for (const id of this.state.path) {
436
+ const n = this.flow.nodes[id];
437
+ // Defensive against missing-node references in path (shouldn't happen
438
+ // but path entries come from runtime, not the type system)
439
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
440
+ if (n && n.type === 'question' && id in this.state.answerLabels) {
441
+ const label = n.prompt.slice(0, 30);
442
+ const raw = this.state.answerLabels[id];
443
+ const value = Array.isArray(raw) ? raw.join(', ') : raw;
444
+ rows.push({ label, value });
445
+ }
446
+ }
447
+ if (rows.length === 0) {
448
+ out.push(' ' + s.muted('(no answers collected)'));
449
+ return out;
450
+ }
451
+ const maxLabelWidth = Math.max(...rows.map((r) => r.label.length));
452
+ for (const row of rows) {
453
+ const padded = row.label.padEnd(maxLabelWidth);
454
+ out.push(` ${s.muted(padded)} ${row.value}`);
455
+ }
456
+ return out;
457
+ }
458
+ handleSummaryKey(key) {
459
+ if (key.name === 'return' || key.name === 'enter') {
460
+ return this.close(this.makeCompleted());
461
+ }
462
+ return this.noAction();
463
+ }
464
+ // ───────────────────────────────────────────────────────────────────────────
465
+ // Transitions — delegate to SDK helpers
466
+ // ───────────────────────────────────────────────────────────────────────────
467
+ advance(answer, label) {
468
+ const node = this.currentNode();
469
+ if (node === undefined || node.type !== 'question')
470
+ return this.noAction();
471
+ const updatedAnswers = { ...this.state.answers, [node.id]: answer };
472
+ const updatedLabels = { ...this.state.answerLabels, [node.id]: label };
473
+ const nextId = resolveNext(node.next, answer);
474
+ if (!nextId || !(nextId in this.flow.nodes)) {
475
+ // Malformed — abort with warning
476
+ return this.close(this.makeAborted('unknown'));
477
+ }
478
+ const newPath = walkPastBranches(this.flow, [...this.state.path, nextId], updatedAnswers);
479
+ this.state.answers = updatedAnswers;
480
+ this.state.answerLabels = updatedLabels;
481
+ this.state.path = newPath;
482
+ this.resetTransientState();
483
+ return this.rerender();
484
+ }
485
+ advanceInfo(node) {
486
+ const nextId = resolveNext(node.next);
487
+ if (!nextId || !(nextId in this.flow.nodes)) {
488
+ return this.close(this.makeAborted('unknown'));
489
+ }
490
+ const newPath = walkPastBranches(this.flow, [...this.state.path, nextId], this.state.answers);
491
+ this.state.path = newPath;
492
+ this.resetTransientState();
493
+ return this.rerender();
494
+ }
495
+ handleBackOrCancel() {
496
+ const result = applyBacktrack(this.flow, this.state.path, this.state.answers, this.state.answerLabels);
497
+ if (result === null) {
498
+ // No prior visible node — treat Esc as cancel
499
+ return this.close(this.makeAborted('escape'));
500
+ }
501
+ this.state.path = result.path;
502
+ this.state.answers = result.answers;
503
+ this.state.answerLabels = result.answerLabels;
504
+ this.resetTransientState();
505
+ return this.rerender();
506
+ }
507
+ resetTransientState() {
508
+ this.state.selectedIndex = 0;
509
+ this.state.multiSelections = new Set();
510
+ this.state.inputBuffer = '';
511
+ this.state.warning = '';
512
+ }
513
+ // ───────────────────────────────────────────────────────────────────────────
514
+ // Result builders
515
+ // ───────────────────────────────────────────────────────────────────────────
516
+ makeCompleted() {
517
+ return {
518
+ completed: true,
519
+ path: [...this.state.path],
520
+ answers: { ...this.state.answers },
521
+ answerLabels: { ...this.state.answerLabels },
522
+ durationMs: Date.now() - this.state.startedAt,
523
+ };
524
+ }
525
+ makeAborted(reason) {
526
+ return {
527
+ completed: false,
528
+ path: [...this.state.path],
529
+ answers: { ...this.state.answers },
530
+ answerLabels: { ...this.state.answerLabels },
531
+ abortedAt: this.currentNodeId(),
532
+ abortReason: reason,
533
+ durationMs: Date.now() - this.state.startedAt,
534
+ };
535
+ }
536
+ // ───────────────────────────────────────────────────────────────────────────
537
+ // Footer + tints
538
+ // ───────────────────────────────────────────────────────────────────────────
539
+ footerHints(node) {
540
+ if (node === undefined)
541
+ return '[q] cancel';
542
+ if (node.type === 'question') {
543
+ const mode = node.input.mode;
544
+ if (mode === 'single') {
545
+ return '[↑↓/1-9] navigate · [Enter] confirm · [Esc] back · [q] cancel';
546
+ }
547
+ if (mode === 'multi') {
548
+ return '[↑↓/1-9] navigate · [Space] toggle · [Enter] confirm · [Esc] back · [q] cancel';
549
+ }
550
+ if (mode === 'text') {
551
+ const isMultiline = 'multiline' in node.input && node.input.multiline === true;
552
+ return isMultiline
553
+ ? '[Ctrl+D] submit · [Enter] new line · [Esc] back · [q] cancel'
554
+ : '[Enter] submit · [Esc] back · [q] cancel';
555
+ }
556
+ // mode === 'proposal'
557
+ return '[↑↓/1-9] navigate · [Enter] pick · [Esc] back · [q] cancel';
558
+ }
559
+ if (node.type === 'info') {
560
+ return '[Enter] continue · [Esc] back · [q] cancel';
561
+ }
562
+ if (node.type === 'summary') {
563
+ return '[Enter] confirm · [Esc] back · [q] cancel';
564
+ }
565
+ return '[q] cancel';
566
+ }
567
+ tinted(label, tint, s) {
568
+ if (tint === 'success')
569
+ return s.success(label);
570
+ if (tint === 'warning')
571
+ return s.warning(label);
572
+ if (tint === 'danger')
573
+ return s.error(label);
574
+ if (tint === 'accent')
575
+ return s.primary(label);
576
+ if (tint === 'muted')
577
+ return s.muted(label);
578
+ return label;
579
+ }
580
+ }
@@ -26,6 +26,10 @@ type WorkflowMode = 'flexible' | 'guided';
26
26
  interface NewProjectState {
27
27
  step: WizardStep;
28
28
  projectType: 'new' | 'existing' | null;
29
+ /** SDK project type id picked at step 3 ('web', 'general', 'research', 'business-plan',
30
+ * 'content', 'tech-docs', 'course', 'book'). Drives whether steps 4+5 are shown and
31
+ * populates the roster step. */
32
+ projectTypeId: string;
29
33
  projectName: string;
30
34
  description: string;
31
35
  repoPattern: RepoPattern | null;
@@ -39,6 +43,13 @@ interface NewProjectState {
39
43
  gitUserName: string | null;
40
44
  gitUserEmail: string | null;
41
45
  gitRemoteInputMode: boolean;
46
+ rosterMode: 'auto' | 'custom' | 'skip';
47
+ /** Per-role include flag, pre-filled from the picked type's suggestedAgents.
48
+ * Only used when rosterMode === 'custom'. */
49
+ rosterToggles: Record<string, boolean>;
50
+ /** Sub-mode of step 7 — when true, render the toggle grid instead of the
51
+ * three-option selector. Same pattern as gitRemoteInputMode on step 6. */
52
+ rosterCustomizeMode: boolean;
42
53
  importPath: string;
43
54
  detectedProject: DetectedProject | null;
44
55
  importGitChoice: 'detected' | 'custom' | 'skip' | 'init' | 'init-skip' | null;
@@ -59,6 +70,13 @@ export interface NewProjectResultV2 {
59
70
  projectName?: string;
60
71
  description?: string;
61
72
  gitRemote?: string;
73
+ /** SDK project type id chosen at step 3. Used by the close handler to register the
74
+ * project with the correct `type` instead of the hardcoded 'general' fallback. */
75
+ projectTypeId?: string;
76
+ /** SDK-canonical short role keys to add to the team after generation. Resolved at
77
+ * close based on rosterMode ('auto' → all suggestedAgents, 'custom' → toggled ones,
78
+ * 'skip' → empty). */
79
+ rosterRolesToAdd?: string[];
62
80
  imported?: boolean;
63
81
  detectedProject?: DetectedProject;
64
82
  }
@@ -78,7 +96,17 @@ export declare class NewProjectOverlayV2 extends BaseOverlayV2<NewProjectState,
78
96
  private renderStep5;
79
97
  private renderStep6;
80
98
  private renderStep7;
99
+ /** Step 7 sub-mode: toggle each suggested agent on/off via Space. */
100
+ private renderStep7Customize;
81
101
  private renderStep8;
102
+ /** After the git step ends, advance to step 7 (roster) — but auto-skip to
103
+ * step 8 (final confirm) when the picked type has no suggested agents
104
+ * (e.g. 'general'). Centralised so both Enter-confirm and Esc-skip on the
105
+ * gitRemoteInputMode sub-mode take the same path. */
106
+ private advancePastGitStep;
107
+ /** Resolve the roles to add based on the chosen rosterMode + toggles. Used by
108
+ * both the step 8 summary render and the result builder at close time. */
109
+ private resolveRosterRolesToAdd;
82
110
  private renderImportStep1;
83
111
  private renderImportStep2;
84
112
  private renderImportStep3;
@@ -87,6 +115,10 @@ export declare class NewProjectOverlayV2 extends BaseOverlayV2<NewProjectState,
87
115
  private renderImportStepFooter;
88
116
  handleKey(key: KeyEvent): OverlayAction<NewProjectResultV2> | Promise<OverlayAction<NewProjectResultV2>>;
89
117
  private handleSelectionKey;
118
+ /** Keys for the step 7 customize sub-mode: ↑/↓ moves between agents, Space
119
+ * toggles the highlighted agent's include flag, Enter confirms the current
120
+ * selection and advances to step 8 (or back to mode picker if nothing left). */
121
+ private handleRosterCustomizeKey;
90
122
  private handleSelectionEnter;
91
123
  private handleInputKey;
92
124
  private handleGitRemoteInput;