@compilr-dev/cli 0.7.4 → 0.7.6

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 (43) 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/tutorials/projects/new-project.js +56 -20
  34. package/dist/ui/overlay/impl/interactive-flow-overlay-v2.d.ts +79 -0
  35. package/dist/ui/overlay/impl/interactive-flow-overlay-v2.js +580 -0
  36. package/dist/ui/overlay/impl/new-overlay-v2.d.ts +32 -0
  37. package/dist/ui/overlay/impl/new-overlay-v2.js +305 -66
  38. package/dist/ui/overlay/impl/propose-alternatives-overlay-v2.d.ts +53 -0
  39. package/dist/ui/overlay/impl/propose-alternatives-overlay-v2.js +326 -0
  40. package/dist/ui/overlay/index.d.ts +2 -0
  41. package/dist/ui/overlay/index.js +2 -0
  42. package/dist/ui/tool-formatters.js +61 -3
  43. package/package.json +2 -2
@@ -0,0 +1,79 @@
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 { BaseOverlayV2 } from '../../base/index.js';
30
+ import type { RenderContext, OverlayAction, KeyEvent } from '../types.js';
31
+ import type { Flow, AnswerValue, InteractiveFlowResult, NodeId } from '../../../shared-handlers.js';
32
+ interface FlowState {
33
+ /** Stack of every node visited, in order. Last element is current. */
34
+ path: NodeId[];
35
+ /** Final answers per question node */
36
+ answers: Record<NodeId, AnswerValue>;
37
+ /** Human-readable labels for the answers */
38
+ answerLabels: Record<NodeId, AnswerValue>;
39
+ /** Wall-clock start for durationMs */
40
+ startedAt: number;
41
+ /** Cursor index for single/multi questions */
42
+ selectedIndex: number;
43
+ /** Selections for multi mode (zero-based choice indices) */
44
+ multiSelections: Set<number>;
45
+ /** Text buffer for text-input questions */
46
+ inputBuffer: string;
47
+ /** Last warning message (e.g. multi-select min/max not met) */
48
+ warning: string;
49
+ }
50
+ export interface InteractiveFlowOptionsV2 {
51
+ flow: Flow;
52
+ }
53
+ export type InteractiveFlowResultV2 = InteractiveFlowResult;
54
+ export declare class InteractiveFlowOverlayV2 extends BaseOverlayV2<FlowState, InteractiveFlowResultV2> {
55
+ readonly type: "inline";
56
+ readonly id = "interactive-flow-overlay-v2";
57
+ private readonly flow;
58
+ constructor(options: InteractiveFlowOptionsV2);
59
+ protected renderContent(context: RenderContext): string[];
60
+ handleKey(key: KeyEvent): OverlayAction<InteractiveFlowResultV2>;
61
+ getCloseSummary(result: InteractiveFlowResultV2): string | null;
62
+ private currentNodeId;
63
+ private currentNode;
64
+ private renderQuestion;
65
+ private handleQuestionKey;
66
+ private renderInfo;
67
+ private handleInfoKey;
68
+ private renderSummary;
69
+ private handleSummaryKey;
70
+ private advance;
71
+ private advanceInfo;
72
+ private handleBackOrCancel;
73
+ private resetTransientState;
74
+ private makeCompleted;
75
+ private makeAborted;
76
+ private footerHints;
77
+ private tinted;
78
+ }
79
+ export {};
@@ -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
+ }