@compilr-dev/cli 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.
Files changed (152) hide show
  1. package/README.md +110 -0
  2. package/dist/agent.d.ts +62 -0
  3. package/dist/agent.js +317 -0
  4. package/dist/agents/registry.d.ts +66 -0
  5. package/dist/agents/registry.js +238 -0
  6. package/dist/agents/types.d.ts +40 -0
  7. package/dist/agents/types.js +94 -0
  8. package/dist/commands/custom-registry.d.ts +69 -0
  9. package/dist/commands/custom-registry.js +246 -0
  10. package/dist/commands/index.d.ts +7 -0
  11. package/dist/commands/index.js +7 -0
  12. package/dist/commands/types.d.ts +31 -0
  13. package/dist/commands/types.js +26 -0
  14. package/dist/commands.d.ts +63 -0
  15. package/dist/commands.js +324 -0
  16. package/dist/db/index.d.ts +42 -0
  17. package/dist/db/index.js +146 -0
  18. package/dist/db/repositories/document-repository.d.ts +63 -0
  19. package/dist/db/repositories/document-repository.js +184 -0
  20. package/dist/db/repositories/index.d.ts +9 -0
  21. package/dist/db/repositories/index.js +6 -0
  22. package/dist/db/repositories/project-repository.d.ts +132 -0
  23. package/dist/db/repositories/project-repository.js +337 -0
  24. package/dist/db/repositories/work-item-repository.d.ts +115 -0
  25. package/dist/db/repositories/work-item-repository.js +389 -0
  26. package/dist/db/schema.d.ts +83 -0
  27. package/dist/db/schema.js +143 -0
  28. package/dist/debug.d.ts +8 -0
  29. package/dist/debug.js +48 -0
  30. package/dist/index.d.ts +2 -0
  31. package/dist/index.js +348 -0
  32. package/dist/index.old.d.ts +7 -0
  33. package/dist/index.old.js +1014 -0
  34. package/dist/repl.d.ts +121 -0
  35. package/dist/repl.js +1878 -0
  36. package/dist/settings/index.d.ts +80 -0
  37. package/dist/settings/index.js +195 -0
  38. package/dist/shared-handlers.d.ts +63 -0
  39. package/dist/shared-handlers.js +57 -0
  40. package/dist/slash-autocomplete.d.ts +41 -0
  41. package/dist/slash-autocomplete.js +638 -0
  42. package/dist/state.d.ts +75 -0
  43. package/dist/state.js +130 -0
  44. package/dist/tabbed-menu.d.ts +11 -0
  45. package/dist/tabbed-menu.js +328 -0
  46. package/dist/templates/backlog-md.d.ts +7 -0
  47. package/dist/templates/backlog-md.js +94 -0
  48. package/dist/templates/claude-md.d.ts +7 -0
  49. package/dist/templates/claude-md.js +189 -0
  50. package/dist/templates/coding-standards.d.ts +7 -0
  51. package/dist/templates/coding-standards.js +299 -0
  52. package/dist/templates/compilr-md.d.ts +7 -0
  53. package/dist/templates/compilr-md.js +189 -0
  54. package/dist/templates/config-json.d.ts +38 -0
  55. package/dist/templates/config-json.js +39 -0
  56. package/dist/templates/gitignore.d.ts +7 -0
  57. package/dist/templates/gitignore.js +85 -0
  58. package/dist/templates/index.d.ts +19 -0
  59. package/dist/templates/index.js +302 -0
  60. package/dist/templates/package-json.d.ts +7 -0
  61. package/dist/templates/package-json.js +111 -0
  62. package/dist/templates/readme-md.d.ts +7 -0
  63. package/dist/templates/readme-md.js +161 -0
  64. package/dist/templates/tsconfig.d.ts +7 -0
  65. package/dist/templates/tsconfig.js +61 -0
  66. package/dist/templates/types.d.ts +33 -0
  67. package/dist/templates/types.js +24 -0
  68. package/dist/test-autocomplete.d.ts +7 -0
  69. package/dist/test-autocomplete.js +85 -0
  70. package/dist/test-tabbed-menu.d.ts +7 -0
  71. package/dist/test-tabbed-menu.js +25 -0
  72. package/dist/themes/colors.d.ts +49 -0
  73. package/dist/themes/colors.js +135 -0
  74. package/dist/themes/index.d.ts +23 -0
  75. package/dist/themes/index.js +24 -0
  76. package/dist/themes/registry.d.ts +60 -0
  77. package/dist/themes/registry.js +195 -0
  78. package/dist/themes/types.d.ts +82 -0
  79. package/dist/themes/types.js +7 -0
  80. package/dist/tool-selector.d.ts +71 -0
  81. package/dist/tool-selector.js +184 -0
  82. package/dist/tools/ask-user-simple.d.ts +19 -0
  83. package/dist/tools/ask-user-simple.js +86 -0
  84. package/dist/tools/ask-user.d.ts +32 -0
  85. package/dist/tools/ask-user.js +113 -0
  86. package/dist/tools/backlog.d.ts +53 -0
  87. package/dist/tools/backlog.js +709 -0
  88. package/dist/tools.d.ts +15 -0
  89. package/dist/tools.js +121 -0
  90. package/dist/ui/agents-overlay.d.ts +12 -0
  91. package/dist/ui/agents-overlay.js +501 -0
  92. package/dist/ui/arch-type-overlay.d.ts +20 -0
  93. package/dist/ui/arch-type-overlay.js +229 -0
  94. package/dist/ui/ask-user-overlay.d.ts +26 -0
  95. package/dist/ui/ask-user-overlay.js +647 -0
  96. package/dist/ui/ask-user-simple-overlay.d.ts +25 -0
  97. package/dist/ui/ask-user-simple-overlay.js +242 -0
  98. package/dist/ui/backlog-overlay.d.ts +17 -0
  99. package/dist/ui/backlog-overlay.js +786 -0
  100. package/dist/ui/commands-overlay.d.ts +11 -0
  101. package/dist/ui/commands-overlay.js +410 -0
  102. package/dist/ui/config-overlay.d.ts +34 -0
  103. package/dist/ui/config-overlay.js +977 -0
  104. package/dist/ui/conversation.d.ts +82 -0
  105. package/dist/ui/conversation.js +508 -0
  106. package/dist/ui/diff.d.ts +38 -0
  107. package/dist/ui/diff.js +182 -0
  108. package/dist/ui/ephemeral.d.ts +111 -0
  109. package/dist/ui/ephemeral.js +413 -0
  110. package/dist/ui/file-autocomplete.d.ts +45 -0
  111. package/dist/ui/file-autocomplete.js +237 -0
  112. package/dist/ui/footer.d.ts +153 -0
  113. package/dist/ui/footer.js +422 -0
  114. package/dist/ui/index.d.ts +12 -0
  115. package/dist/ui/index.js +15 -0
  116. package/dist/ui/init-overlay.d.ts +24 -0
  117. package/dist/ui/init-overlay.js +525 -0
  118. package/dist/ui/input-prompt-v2.d.ts +179 -0
  119. package/dist/ui/input-prompt-v2.js +991 -0
  120. package/dist/ui/input-prompt.d.ts +97 -0
  121. package/dist/ui/input-prompt.js +800 -0
  122. package/dist/ui/iteration-limit-overlay.d.ts +21 -0
  123. package/dist/ui/iteration-limit-overlay.js +150 -0
  124. package/dist/ui/keys-overlay.d.ts +14 -0
  125. package/dist/ui/keys-overlay.js +181 -0
  126. package/dist/ui/model-warning-overlay.d.ts +30 -0
  127. package/dist/ui/model-warning-overlay.js +171 -0
  128. package/dist/ui/overlay-controller.d.ts +25 -0
  129. package/dist/ui/overlay-controller.js +35 -0
  130. package/dist/ui/overlays.d.ts +47 -0
  131. package/dist/ui/overlays.js +627 -0
  132. package/dist/ui/permission-overlay.d.ts +16 -0
  133. package/dist/ui/permission-overlay.js +494 -0
  134. package/dist/ui/terminal.d.ts +117 -0
  135. package/dist/ui/terminal.js +237 -0
  136. package/dist/ui/todo-zone.d.ts +112 -0
  137. package/dist/ui/todo-zone.js +353 -0
  138. package/dist/ui/tools-overlay.d.ts +26 -0
  139. package/dist/ui/tools-overlay.js +278 -0
  140. package/dist/ui/tutorial-overlay.d.ts +10 -0
  141. package/dist/ui/tutorial-overlay.js +936 -0
  142. package/dist/ui/types.d.ts +103 -0
  143. package/dist/ui/types.js +33 -0
  144. package/dist/utils/credentials.d.ts +55 -0
  145. package/dist/utils/credentials.js +268 -0
  146. package/dist/utils/model-tiers.d.ts +37 -0
  147. package/dist/utils/model-tiers.js +118 -0
  148. package/dist/utils/project-memory.d.ts +47 -0
  149. package/dist/utils/project-memory.js +117 -0
  150. package/dist/utils/project-status.d.ts +56 -0
  151. package/dist/utils/project-status.js +237 -0
  152. package/package.json +66 -0
@@ -0,0 +1,647 @@
1
+ /**
2
+ * Ask User Overlay
3
+ *
4
+ * Modal overlay for presenting multi-question forms to the user.
5
+ * Used by the ask_user tool during /design and /refine workflows.
6
+ *
7
+ * Features:
8
+ * - Multiple questions with navigation (←/→ or Tab)
9
+ * - Options to select from with arrow keys
10
+ * - Custom text input option
11
+ * - Multi-select support
12
+ * - Progress indicator showing answered questions
13
+ */
14
+ import chalk from 'chalk';
15
+ import * as terminal from './terminal.js';
16
+ import { getStyles } from '../themes/index.js';
17
+ // =============================================================================
18
+ // Rendering Helpers
19
+ // =============================================================================
20
+ function renderTabBar(questions, currentIndex, answers, isOnSubmitTab) {
21
+ const s = getStyles();
22
+ const lines = [];
23
+ // Tab bar like config-overlay: " Questions: App Type Users Features Submit "
24
+ // Current tab highlighted with bgBlue, answered tabs with success color
25
+ let tabLine = ' ';
26
+ for (let i = 0; i < questions.length; i++) {
27
+ const q = questions[i];
28
+ const hasAnswer = q.id in answers;
29
+ const isCurrent = i === currentIndex && !isOnSubmitTab;
30
+ // Use header if available, fallback to formatted id
31
+ const label = q.header || q.id.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
32
+ if (isCurrent) {
33
+ // Current tab: highlighted background (theme selection color)
34
+ tabLine += s.selected(` ${label} `) + ' ';
35
+ }
36
+ else if (hasAnswer) {
37
+ // Answered tab: success color with checkmark
38
+ tabLine += s.success(`✓ ${label}`) + ' ';
39
+ }
40
+ else {
41
+ // Pending tab: muted
42
+ tabLine += s.muted(` ${label} `) + ' ';
43
+ }
44
+ }
45
+ // Add Submit tab at the end
46
+ const allAnswered = questions.every((q) => q.id in answers);
47
+ if (isOnSubmitTab) {
48
+ tabLine += s.selected(' Submit ');
49
+ }
50
+ else if (allAnswered) {
51
+ tabLine += s.success(' Submit ');
52
+ }
53
+ else {
54
+ tabLine += s.muted(' Submit ');
55
+ }
56
+ lines.push(tabLine);
57
+ return lines;
58
+ }
59
+ function renderQuestion(question, questionIndex, totalQuestions, state, context) {
60
+ const s = getStyles();
61
+ const lines = [];
62
+ // Context if provided (only on first render)
63
+ if (context && questionIndex === 0) {
64
+ lines.push(s.muted(' ' + context));
65
+ lines.push('');
66
+ }
67
+ // Question number and text
68
+ lines.push(chalk.bold(` [${String(questionIndex + 1)}/${String(totalQuestions)}] ${question.question}`));
69
+ lines.push('');
70
+ // Options
71
+ const options = question.options ?? [];
72
+ const allowCustom = question.allowCustom !== false; // Default true
73
+ const isMultiSelect = question.multiSelect === true;
74
+ const customIndex = options.length; // Custom is always last option
75
+ // Check if there's an existing answer for this question
76
+ const answeredOptions = new Set();
77
+ let customAnswerValue = '';
78
+ if (question.id in state.answers) {
79
+ const existingAnswer = state.answers[question.id];
80
+ if (isMultiSelect && Array.isArray(existingAnswer)) {
81
+ for (const ans of existingAnswer) {
82
+ const idx = options.indexOf(ans);
83
+ if (idx >= 0) {
84
+ answeredOptions.add(idx);
85
+ }
86
+ else {
87
+ customAnswerValue = ans;
88
+ }
89
+ }
90
+ }
91
+ else if (typeof existingAnswer === 'string') {
92
+ const idx = options.indexOf(existingAnswer);
93
+ if (idx >= 0) {
94
+ answeredOptions.add(idx);
95
+ }
96
+ else {
97
+ customAnswerValue = existingAnswer;
98
+ }
99
+ }
100
+ }
101
+ for (let i = 0; i < options.length; i++) {
102
+ const isCursor = state.selectedIndex === i;
103
+ const isMultiSelected = isMultiSelect && state.multiSelections.has(i);
104
+ const isAnswered = answeredOptions.has(i);
105
+ const prefix = isCursor ? ' ❯ ' : ' ';
106
+ // For multi-select: show checkbox state
107
+ // For single-select: show bullet for answered option
108
+ let marker = '';
109
+ if (isMultiSelect) {
110
+ marker = isMultiSelected ? '[✓] ' : '[ ] ';
111
+ }
112
+ else if (isAnswered) {
113
+ marker = '● '; // Filled bullet for answered single-select
114
+ }
115
+ const label = `${String(i + 1)}. ${options[i]}`;
116
+ if (isCursor) {
117
+ lines.push(s.primary(prefix + marker + label));
118
+ }
119
+ else if (isMultiSelected || isAnswered) {
120
+ lines.push(s.success(prefix + marker + label));
121
+ }
122
+ else {
123
+ lines.push(s.muted(prefix + marker + label));
124
+ }
125
+ }
126
+ // Custom option
127
+ if (allowCustom) {
128
+ const isCursor = state.selectedIndex === customIndex;
129
+ const prefix = isCursor ? ' ❯ ' : ' ';
130
+ const hasCustomAnswer = customAnswerValue !== '' || state.inputBuffer !== '';
131
+ const displayValue = state.inputBuffer || customAnswerValue;
132
+ if (state.isTypingCustom) {
133
+ lines.push(s.primary(prefix + 'Custom: ' + state.inputBuffer + '█'));
134
+ }
135
+ else if (hasCustomAnswer) {
136
+ // Show the custom answer value
137
+ const marker = isMultiSelect ? '[✓] ' : '● ';
138
+ const label = `${String(options.length + 1)}. Custom: "${displayValue}"`;
139
+ lines.push(isCursor ? s.primary(prefix + marker + label) : s.success(prefix + marker + label));
140
+ }
141
+ else {
142
+ const label = `${String(options.length + 1)}. Type something custom...`;
143
+ lines.push(isCursor ? s.primary(prefix + label) : s.muted(prefix + label));
144
+ }
145
+ }
146
+ return lines;
147
+ }
148
+ function renderFooter(isTypingCustom, isMultiSelect, isOnSubmitTab) {
149
+ const s = getStyles();
150
+ const lines = [];
151
+ lines.push('');
152
+ if (isOnSubmitTab) {
153
+ lines.push(s.muted(' Enter Submit all · ←/Tab Go back · Esc Cancel'));
154
+ }
155
+ else if (isTypingCustom) {
156
+ lines.push(s.muted(' Enter Submit · Esc Cancel custom'));
157
+ }
158
+ else if (isMultiSelect) {
159
+ lines.push(s.muted(' ↑↓ Navigate · Space Toggle · Enter Confirm · Tab Next · Esc Cancel'));
160
+ }
161
+ else {
162
+ lines.push(s.muted(' ↑↓ Navigate · Enter Select · Tab Next · Esc Cancel'));
163
+ }
164
+ return lines;
165
+ }
166
+ function renderSubmitTab(questions, answers) {
167
+ const s = getStyles();
168
+ const lines = [];
169
+ lines.push('');
170
+ lines.push(chalk.bold(' Ready to submit?'));
171
+ lines.push('');
172
+ // Show summary of answers
173
+ const answeredCount = Object.keys(answers).length;
174
+ const totalCount = questions.length;
175
+ const allAnswered = answeredCount === totalCount;
176
+ if (allAnswered) {
177
+ lines.push(s.success(` ✓ All ${String(totalCount)} questions answered`));
178
+ lines.push('');
179
+ lines.push(' ' + s.muted('Press Enter to submit your answers.'));
180
+ }
181
+ else {
182
+ const unansweredCount = totalCount - answeredCount;
183
+ lines.push(s.warning(` ⚠ ${String(unansweredCount)} of ${String(totalCount)} questions not answered`));
184
+ lines.push('');
185
+ // List unanswered questions
186
+ lines.push(' ' + s.muted('Unanswered:'));
187
+ for (const q of questions) {
188
+ if (!(q.id in answers)) {
189
+ const label = q.header || q.id.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
190
+ lines.push(' ' + s.muted(`• ${label}`));
191
+ }
192
+ }
193
+ lines.push('');
194
+ lines.push(' ' + s.muted('Press ← or Tab to go back and answer.'));
195
+ }
196
+ return lines;
197
+ }
198
+ // Debug flag - set to true to enable logging to stderr
199
+ const ASK_USER_DEBUG = false;
200
+ function debugLog(msg) {
201
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
202
+ if (ASK_USER_DEBUG) {
203
+ process.stderr.write(`[ask-user] ${msg}\n`);
204
+ }
205
+ }
206
+ function render(questions, state, context, previousLineCount = 0, targetLineCount = 0) {
207
+ const s = getStyles();
208
+ const lines = [];
209
+ const cols = terminal.getTerminalWidth();
210
+ const border = s.muted('─'.repeat(Math.max(1, cols - 1)));
211
+ debugLog(`render: q=${String(state.currentQuestion)}, prevLines=${String(previousLineCount)}, target=${String(targetLineCount)}`);
212
+ // Clear previous render
213
+ if (previousLineCount > 0) {
214
+ terminal.clearLinesAbove(previousLineCount);
215
+ }
216
+ // Header
217
+ lines.push(border);
218
+ lines.push(' ' + s.primaryBold('Questions'));
219
+ lines.push('');
220
+ // Tab bar (like config-overlay) - includes Submit tab
221
+ lines.push(...renderTabBar(questions, state.currentQuestion, state.answers, state.isOnSubmitTab));
222
+ lines.push('');
223
+ // Current question or Submit tab
224
+ if (state.isOnSubmitTab) {
225
+ lines.push(...renderSubmitTab(questions, state.answers));
226
+ }
227
+ else {
228
+ const currentQ = questions[state.currentQuestion];
229
+ lines.push(...renderQuestion(currentQ, state.currentQuestion, questions.length, state, context));
230
+ }
231
+ // Footer with instructions
232
+ const currentQ = state.isOnSubmitTab ? null : questions[state.currentQuestion];
233
+ lines.push(...renderFooter(state.isTypingCustom, currentQ?.multiSelect === true, state.isOnSubmitTab));
234
+ // Warning message if present
235
+ if (state.warningMessage) {
236
+ lines.push('');
237
+ lines.push(s.warning(` ⚠ ${state.warningMessage}`));
238
+ }
239
+ // Bottom border to close the overlay
240
+ lines.push(border);
241
+ // Pad with empty lines to maintain consistent height (prevents cursor drift)
242
+ while (lines.length < targetLineCount) {
243
+ lines.push('');
244
+ }
245
+ // Render all lines (use write + join like config-overlay to match clearLinesAbove behavior)
246
+ terminal.write(lines.join('\n'));
247
+ debugLog(`render done: newLines=${String(lines.length)}, optionCount=${String(questions[state.currentQuestion].options?.length ?? 0)}`);
248
+ return lines.length;
249
+ }
250
+ // =============================================================================
251
+ // Main Export
252
+ // =============================================================================
253
+ /**
254
+ * Show the ask user overlay
255
+ */
256
+ export async function showAskUserOverlay(options) {
257
+ const { questions, context } = options;
258
+ // Check if first question should auto-start in typing mode
259
+ // (no options, custom allowed, single-select)
260
+ const firstQ = questions[0];
261
+ const firstQAutoType = (firstQ.options?.length ?? 0) === 0 &&
262
+ firstQ.allowCustom !== false &&
263
+ firstQ.multiSelect !== true;
264
+ const state = {
265
+ currentQuestion: 0,
266
+ selectedIndex: 0,
267
+ inputBuffer: '',
268
+ isTypingCustom: firstQAutoType,
269
+ answers: {},
270
+ multiSelections: new Set(),
271
+ cancelled: false,
272
+ warningMessage: '',
273
+ isOnSubmitTab: false,
274
+ };
275
+ let lineCount = 0;
276
+ let maxLineCount = 0; // Track max lines ever rendered to ensure full clearing
277
+ // NOTE: Footer is already paused by the caller (index.ts handler calls sharedState.pauseFooter)
278
+ // Do NOT call pauseForOverlay() here - it causes double-pause issues
279
+ // The caller's pause already clears the footer from screen
280
+ // Ensure we start from a fresh line (like config-overlay does)
281
+ terminal.writeLine('');
282
+ terminal.hideCursor();
283
+ const wasRawMode = process.stdin.isRaw;
284
+ terminal.enableRawMode();
285
+ // Initial render (no re-render needed - matches config-overlay pattern)
286
+ lineCount = render(questions, state, context, 0);
287
+ maxLineCount = Math.max(maxLineCount, lineCount);
288
+ // Helper: Get option count for current question
289
+ const getOptionCount = () => {
290
+ const q = questions[state.currentQuestion];
291
+ const optionCount = q.options?.length ?? 0;
292
+ const allowCustom = q.allowCustom !== false;
293
+ return allowCustom ? optionCount + 1 : optionCount;
294
+ };
295
+ // Helper: Restore state from existing answer for current question
296
+ const restoreFromAnswer = () => {
297
+ const q = questions[state.currentQuestion];
298
+ const options = q.options ?? [];
299
+ const isMultiSelect = q.multiSelect === true;
300
+ const allowCustom = q.allowCustom !== false;
301
+ // Reset state first
302
+ state.selectedIndex = 0;
303
+ state.inputBuffer = '';
304
+ state.isTypingCustom = false;
305
+ state.multiSelections.clear();
306
+ if (!(q.id in state.answers)) {
307
+ // No existing answer - check if we should auto-type
308
+ // (no options, custom allowed, single-select)
309
+ if (options.length === 0 && allowCustom && !isMultiSelect) {
310
+ state.isTypingCustom = true;
311
+ }
312
+ return;
313
+ }
314
+ const existingAnswer = state.answers[q.id];
315
+ if (isMultiSelect && Array.isArray(existingAnswer)) {
316
+ // Multi-select: restore all selected options
317
+ for (const answer of existingAnswer) {
318
+ const idx = options.indexOf(answer);
319
+ if (idx >= 0) {
320
+ state.multiSelections.add(idx);
321
+ }
322
+ else {
323
+ // Custom answer - add to custom index and set buffer
324
+ state.multiSelections.add(options.length);
325
+ state.inputBuffer = answer;
326
+ }
327
+ }
328
+ // Set selectedIndex to first selected item
329
+ if (state.multiSelections.size > 0) {
330
+ state.selectedIndex = Math.min(...state.multiSelections);
331
+ }
332
+ }
333
+ else if (typeof existingAnswer === 'string') {
334
+ // Single select: find which option was selected
335
+ const idx = options.indexOf(existingAnswer);
336
+ if (idx >= 0) {
337
+ state.selectedIndex = idx;
338
+ }
339
+ else {
340
+ // Custom answer
341
+ state.selectedIndex = options.length; // Custom option index
342
+ state.inputBuffer = existingAnswer;
343
+ // Don't enter typing mode - just show the value
344
+ }
345
+ }
346
+ };
347
+ // Helper: Navigate to next question or Submit tab
348
+ const nextQuestion = () => {
349
+ if (state.isOnSubmitTab) {
350
+ // Already on Submit tab, do nothing
351
+ return;
352
+ }
353
+ if (state.currentQuestion < questions.length - 1) {
354
+ // Move to next question
355
+ debugLog(`nextQuestion: ${String(state.currentQuestion)} -> ${String(state.currentQuestion + 1)}, lineCount=${String(lineCount)}`);
356
+ state.currentQuestion++;
357
+ restoreFromAnswer();
358
+ }
359
+ else {
360
+ // On last question - move to Submit tab
361
+ debugLog(`nextQuestion: moving to Submit tab, lineCount=${String(lineCount)}`);
362
+ state.isOnSubmitTab = true;
363
+ // IMPORTANT: Clear typing mode when entering Submit tab
364
+ // Otherwise, Enter key is captured by typing mode handler instead of Submit handler
365
+ state.isTypingCustom = false;
366
+ state.inputBuffer = '';
367
+ }
368
+ };
369
+ // Helper: Navigate to previous question (or from Submit tab to last question)
370
+ const prevQuestion = () => {
371
+ if (state.isOnSubmitTab) {
372
+ // Move from Submit tab back to last question
373
+ debugLog(`prevQuestion: leaving Submit tab to question ${String(questions.length - 1)}`);
374
+ state.isOnSubmitTab = false;
375
+ restoreFromAnswer();
376
+ return;
377
+ }
378
+ if (state.currentQuestion > 0) {
379
+ debugLog(`prevQuestion: ${String(state.currentQuestion)} -> ${String(state.currentQuestion - 1)}, lineCount=${String(lineCount)}`);
380
+ state.currentQuestion--;
381
+ restoreFromAnswer();
382
+ }
383
+ };
384
+ // Helper: Get list of unanswered question indices
385
+ const getUnansweredIndices = () => {
386
+ const unanswered = [];
387
+ for (let i = 0; i < questions.length; i++) {
388
+ if (!(questions[i].id in state.answers)) {
389
+ unanswered.push(i);
390
+ }
391
+ }
392
+ return unanswered;
393
+ };
394
+ // Helper: Check if all questions are answered and either complete or navigate to first unanswered
395
+ // Returns true if all complete and we should resolve, false if navigated to unanswered
396
+ const tryComplete = () => {
397
+ const unanswered = getUnansweredIndices();
398
+ if (unanswered.length === 0) {
399
+ // All questions answered - can complete
400
+ state.warningMessage = '';
401
+ return true;
402
+ }
403
+ // Navigate to first unanswered question and show warning
404
+ const firstUnanswered = unanswered[0];
405
+ const unansweredCount = unanswered.length;
406
+ state.warningMessage = `${String(unansweredCount)} question${unansweredCount > 1 ? 's' : ''} unanswered. Please answer all questions.`;
407
+ state.currentQuestion = firstUnanswered;
408
+ restoreFromAnswer();
409
+ return false;
410
+ };
411
+ // Helper: Submit answer for current question
412
+ const submitAnswer = () => {
413
+ const q = questions[state.currentQuestion];
414
+ const options = q.options ?? [];
415
+ const allowCustom = q.allowCustom !== false;
416
+ const isMultiSelect = q.multiSelect === true;
417
+ const customIndex = options.length;
418
+ if (isMultiSelect) {
419
+ // Multi-select: gather selected options
420
+ const selectedAnswers = [];
421
+ for (const idx of state.multiSelections) {
422
+ if (idx < options.length) {
423
+ selectedAnswers.push(options[idx]);
424
+ }
425
+ }
426
+ // Include custom if selected and typed
427
+ if (state.multiSelections.has(customIndex) && state.inputBuffer.trim()) {
428
+ selectedAnswers.push(state.inputBuffer.trim());
429
+ }
430
+ if (selectedAnswers.length > 0) {
431
+ state.answers[q.id] = selectedAnswers;
432
+ return true;
433
+ }
434
+ return false; // Nothing selected
435
+ }
436
+ else {
437
+ // Single select
438
+ if (state.isTypingCustom) {
439
+ const custom = state.inputBuffer.trim();
440
+ if (custom) {
441
+ state.answers[q.id] = custom;
442
+ return true;
443
+ }
444
+ return false; // Empty custom
445
+ }
446
+ else if (state.selectedIndex < options.length) {
447
+ state.answers[q.id] = options[state.selectedIndex];
448
+ return true;
449
+ }
450
+ else if (allowCustom && state.selectedIndex === customIndex) {
451
+ // Enter custom input mode
452
+ state.isTypingCustom = true;
453
+ return false; // Don't advance yet
454
+ }
455
+ }
456
+ return false;
457
+ };
458
+ return new Promise((resolve) => {
459
+ const cleanup = () => {
460
+ debugLog(`cleanup: clearing maxLineCount=${String(maxLineCount)}`);
461
+ // Clear the overlay - use maxLineCount to ensure full clearing
462
+ terminal.clearLinesAbove(maxLineCount);
463
+ terminal.writeLine('');
464
+ terminal.showCursor();
465
+ if (!wasRawMode) {
466
+ terminal.disableRawMode();
467
+ }
468
+ process.stdin.removeListener('data', handleData);
469
+ // NOTE: Footer resume is handled by the caller (index.ts) in the finally block
470
+ };
471
+ const handleData = (data) => {
472
+ onData(data);
473
+ };
474
+ const onData = (data) => {
475
+ const isEscape = data.length === 1 && data[0] === 0x1b;
476
+ const isUpArrow = data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x41;
477
+ const isDownArrow = data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x42;
478
+ const isLeftArrow = data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x44;
479
+ const isRightArrow = data.length === 3 && data[0] === 0x1b && data[1] === 0x5b && data[2] === 0x43;
480
+ const isCtrlC = data.length === 1 && data[0] === 0x03;
481
+ const isEnter = data.length === 1 && (data[0] === 0x0d || data[0] === 0x0a);
482
+ const isBackspace = data.length === 1 && (data[0] === 0x7f || data[0] === 0x08);
483
+ const isTab = data.length === 1 && data[0] === 0x09;
484
+ const isSpace = data.length === 1 && data[0] === 0x20;
485
+ // Helper to get skipped questions
486
+ const getSkipped = () => questions.filter((q) => !(q.id in state.answers)).map((q) => q.id);
487
+ // Ctrl+C always cancels
488
+ if (isCtrlC) {
489
+ cleanup();
490
+ state.cancelled = true;
491
+ resolve({
492
+ answers: state.answers,
493
+ skipped: getSkipped(),
494
+ });
495
+ return;
496
+ }
497
+ // Submit tab takes priority over typing mode
498
+ // (This is a safeguard - nextQuestion() should already clear isTypingCustom)
499
+ if (state.isOnSubmitTab) {
500
+ // On Submit tab - handle special navigation
501
+ if (isEscape) {
502
+ // Cancel overlay
503
+ cleanup();
504
+ resolve({
505
+ answers: state.answers,
506
+ skipped: getSkipped(),
507
+ });
508
+ return;
509
+ }
510
+ else if (isLeftArrow || isTab) {
511
+ // Go back to last question
512
+ prevQuestion();
513
+ }
514
+ else if (isEnter) {
515
+ // Try to submit - check completeness
516
+ if (tryComplete()) {
517
+ cleanup();
518
+ resolve({
519
+ answers: state.answers,
520
+ skipped: getSkipped(),
521
+ });
522
+ return;
523
+ }
524
+ // tryComplete sets warning and navigates to first unanswered
525
+ }
526
+ // Re-render and return early
527
+ lineCount = render(questions, state, context, maxLineCount, maxLineCount);
528
+ maxLineCount = Math.max(maxLineCount, lineCount);
529
+ return;
530
+ }
531
+ // If typing custom input
532
+ if (state.isTypingCustom) {
533
+ const currentQ = questions[state.currentQuestion];
534
+ const options = currentQ.options ?? [];
535
+ if (isEscape) {
536
+ // Cancel custom input
537
+ state.isTypingCustom = false;
538
+ state.inputBuffer = '';
539
+ }
540
+ else if (isUpArrow && options.length > 0) {
541
+ // Exit typing mode and navigate up (only if there are other options)
542
+ state.isTypingCustom = false;
543
+ state.inputBuffer = '';
544
+ state.selectedIndex = Math.max(0, state.selectedIndex - 1);
545
+ }
546
+ else if (isDownArrow) {
547
+ // Custom is last option, so down arrow does nothing
548
+ // But we handle it to be consistent
549
+ }
550
+ else if (isEnter) {
551
+ // Submit custom input
552
+ if (submitAnswer()) {
553
+ // Clear warning on successful submit
554
+ state.warningMessage = '';
555
+ // Move to next question (or Submit tab if on last question)
556
+ nextQuestion();
557
+ }
558
+ }
559
+ else if (isBackspace) {
560
+ state.inputBuffer = state.inputBuffer.slice(0, -1);
561
+ }
562
+ else if (data.length === 1 && data[0] >= 0x20 && data[0] < 0x7f) {
563
+ // Printable character
564
+ state.inputBuffer += String.fromCharCode(data[0]);
565
+ }
566
+ }
567
+ else {
568
+ // Navigation mode - on a question (Submit tab is handled earlier)
569
+ const optionCount = getOptionCount();
570
+ const currentQ = questions[state.currentQuestion];
571
+ const isMultiSelect = currentQ.multiSelect === true;
572
+ const options = currentQ.options ?? [];
573
+ const allowCustom = currentQ.allowCustom !== false;
574
+ const customIndex = options.length;
575
+ if (isEscape) {
576
+ // Cancel overlay
577
+ cleanup();
578
+ resolve({
579
+ answers: state.answers,
580
+ skipped: getSkipped(),
581
+ });
582
+ return;
583
+ }
584
+ else if (isUpArrow) {
585
+ state.selectedIndex = Math.max(0, state.selectedIndex - 1);
586
+ // Auto-enable typing when navigating to custom option (single-select only)
587
+ if (!isMultiSelect && allowCustom && state.selectedIndex === customIndex) {
588
+ state.isTypingCustom = true;
589
+ }
590
+ }
591
+ else if (isDownArrow) {
592
+ state.selectedIndex = Math.min(optionCount - 1, state.selectedIndex + 1);
593
+ // Auto-enable typing when navigating to custom option (single-select only)
594
+ if (!isMultiSelect && allowCustom && state.selectedIndex === customIndex) {
595
+ state.isTypingCustom = true;
596
+ }
597
+ }
598
+ else if (isLeftArrow) {
599
+ prevQuestion();
600
+ }
601
+ else if (isRightArrow || isTab) {
602
+ // Tab or right arrow: move to next question (or Submit tab)
603
+ nextQuestion();
604
+ }
605
+ else if (isSpace && isMultiSelect) {
606
+ // Toggle multi-select
607
+ if (state.multiSelections.has(state.selectedIndex)) {
608
+ state.multiSelections.delete(state.selectedIndex);
609
+ }
610
+ else {
611
+ state.multiSelections.add(state.selectedIndex);
612
+ }
613
+ }
614
+ else if (isEnter) {
615
+ const submitted = submitAnswer();
616
+ if (submitted) {
617
+ // Clear warning on successful submit
618
+ state.warningMessage = '';
619
+ // Move to next question (or Submit tab)
620
+ nextQuestion();
621
+ }
622
+ }
623
+ else if (data.length === 1 && data[0] >= 0x31 && data[0] <= 0x39) {
624
+ // Number keys 1-9
625
+ const numIndex = data[0] - 0x31; // 0-indexed
626
+ if (numIndex < optionCount) {
627
+ state.selectedIndex = numIndex;
628
+ if (!isMultiSelect) {
629
+ // Auto-select for single select
630
+ const submitted = submitAnswer();
631
+ if (submitted) {
632
+ // Clear warning on successful submit
633
+ state.warningMessage = '';
634
+ // Move to next question (or Submit tab)
635
+ nextQuestion();
636
+ }
637
+ }
638
+ }
639
+ }
640
+ }
641
+ // Re-render - use maxLineCount for both clearing AND target height to prevent cursor drift
642
+ lineCount = render(questions, state, context, maxLineCount, maxLineCount);
643
+ maxLineCount = Math.max(maxLineCount, lineCount);
644
+ };
645
+ process.stdin.on('data', handleData);
646
+ });
647
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Ask User Simple Overlay
3
+ *
4
+ * Simplified modal overlay for single-question input.
5
+ * Used by the ask_user_simple tool during /sketch workflows.
6
+ *
7
+ * Features:
8
+ * - Single question (no tabs, no navigation)
9
+ * - Options to select from with arrow keys
10
+ * - Custom text input option
11
+ * - Simple, clean UI
12
+ */
13
+ export interface AskUserSimpleOptions {
14
+ question: string;
15
+ options?: string[];
16
+ allowCustom?: boolean;
17
+ }
18
+ export interface AskUserSimpleResult {
19
+ answer: string;
20
+ skipped: boolean;
21
+ }
22
+ /**
23
+ * Show the simple ask user overlay
24
+ */
25
+ export declare function showAskUserSimpleOverlay(options: AskUserSimpleOptions): Promise<AskUserSimpleResult>;