@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.
- package/README.md +110 -0
- package/dist/agent.d.ts +62 -0
- package/dist/agent.js +317 -0
- package/dist/agents/registry.d.ts +66 -0
- package/dist/agents/registry.js +238 -0
- package/dist/agents/types.d.ts +40 -0
- package/dist/agents/types.js +94 -0
- package/dist/commands/custom-registry.d.ts +69 -0
- package/dist/commands/custom-registry.js +246 -0
- package/dist/commands/index.d.ts +7 -0
- package/dist/commands/index.js +7 -0
- package/dist/commands/types.d.ts +31 -0
- package/dist/commands/types.js +26 -0
- package/dist/commands.d.ts +63 -0
- package/dist/commands.js +324 -0
- package/dist/db/index.d.ts +42 -0
- package/dist/db/index.js +146 -0
- package/dist/db/repositories/document-repository.d.ts +63 -0
- package/dist/db/repositories/document-repository.js +184 -0
- package/dist/db/repositories/index.d.ts +9 -0
- package/dist/db/repositories/index.js +6 -0
- package/dist/db/repositories/project-repository.d.ts +132 -0
- package/dist/db/repositories/project-repository.js +337 -0
- package/dist/db/repositories/work-item-repository.d.ts +115 -0
- package/dist/db/repositories/work-item-repository.js +389 -0
- package/dist/db/schema.d.ts +83 -0
- package/dist/db/schema.js +143 -0
- package/dist/debug.d.ts +8 -0
- package/dist/debug.js +48 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +348 -0
- package/dist/index.old.d.ts +7 -0
- package/dist/index.old.js +1014 -0
- package/dist/repl.d.ts +121 -0
- package/dist/repl.js +1878 -0
- package/dist/settings/index.d.ts +80 -0
- package/dist/settings/index.js +195 -0
- package/dist/shared-handlers.d.ts +63 -0
- package/dist/shared-handlers.js +57 -0
- package/dist/slash-autocomplete.d.ts +41 -0
- package/dist/slash-autocomplete.js +638 -0
- package/dist/state.d.ts +75 -0
- package/dist/state.js +130 -0
- package/dist/tabbed-menu.d.ts +11 -0
- package/dist/tabbed-menu.js +328 -0
- package/dist/templates/backlog-md.d.ts +7 -0
- package/dist/templates/backlog-md.js +94 -0
- package/dist/templates/claude-md.d.ts +7 -0
- package/dist/templates/claude-md.js +189 -0
- package/dist/templates/coding-standards.d.ts +7 -0
- package/dist/templates/coding-standards.js +299 -0
- package/dist/templates/compilr-md.d.ts +7 -0
- package/dist/templates/compilr-md.js +189 -0
- package/dist/templates/config-json.d.ts +38 -0
- package/dist/templates/config-json.js +39 -0
- package/dist/templates/gitignore.d.ts +7 -0
- package/dist/templates/gitignore.js +85 -0
- package/dist/templates/index.d.ts +19 -0
- package/dist/templates/index.js +302 -0
- package/dist/templates/package-json.d.ts +7 -0
- package/dist/templates/package-json.js +111 -0
- package/dist/templates/readme-md.d.ts +7 -0
- package/dist/templates/readme-md.js +161 -0
- package/dist/templates/tsconfig.d.ts +7 -0
- package/dist/templates/tsconfig.js +61 -0
- package/dist/templates/types.d.ts +33 -0
- package/dist/templates/types.js +24 -0
- package/dist/test-autocomplete.d.ts +7 -0
- package/dist/test-autocomplete.js +85 -0
- package/dist/test-tabbed-menu.d.ts +7 -0
- package/dist/test-tabbed-menu.js +25 -0
- package/dist/themes/colors.d.ts +49 -0
- package/dist/themes/colors.js +135 -0
- package/dist/themes/index.d.ts +23 -0
- package/dist/themes/index.js +24 -0
- package/dist/themes/registry.d.ts +60 -0
- package/dist/themes/registry.js +195 -0
- package/dist/themes/types.d.ts +82 -0
- package/dist/themes/types.js +7 -0
- package/dist/tool-selector.d.ts +71 -0
- package/dist/tool-selector.js +184 -0
- package/dist/tools/ask-user-simple.d.ts +19 -0
- package/dist/tools/ask-user-simple.js +86 -0
- package/dist/tools/ask-user.d.ts +32 -0
- package/dist/tools/ask-user.js +113 -0
- package/dist/tools/backlog.d.ts +53 -0
- package/dist/tools/backlog.js +709 -0
- package/dist/tools.d.ts +15 -0
- package/dist/tools.js +121 -0
- package/dist/ui/agents-overlay.d.ts +12 -0
- package/dist/ui/agents-overlay.js +501 -0
- package/dist/ui/arch-type-overlay.d.ts +20 -0
- package/dist/ui/arch-type-overlay.js +229 -0
- package/dist/ui/ask-user-overlay.d.ts +26 -0
- package/dist/ui/ask-user-overlay.js +647 -0
- package/dist/ui/ask-user-simple-overlay.d.ts +25 -0
- package/dist/ui/ask-user-simple-overlay.js +242 -0
- package/dist/ui/backlog-overlay.d.ts +17 -0
- package/dist/ui/backlog-overlay.js +786 -0
- package/dist/ui/commands-overlay.d.ts +11 -0
- package/dist/ui/commands-overlay.js +410 -0
- package/dist/ui/config-overlay.d.ts +34 -0
- package/dist/ui/config-overlay.js +977 -0
- package/dist/ui/conversation.d.ts +82 -0
- package/dist/ui/conversation.js +508 -0
- package/dist/ui/diff.d.ts +38 -0
- package/dist/ui/diff.js +182 -0
- package/dist/ui/ephemeral.d.ts +111 -0
- package/dist/ui/ephemeral.js +413 -0
- package/dist/ui/file-autocomplete.d.ts +45 -0
- package/dist/ui/file-autocomplete.js +237 -0
- package/dist/ui/footer.d.ts +153 -0
- package/dist/ui/footer.js +422 -0
- package/dist/ui/index.d.ts +12 -0
- package/dist/ui/index.js +15 -0
- package/dist/ui/init-overlay.d.ts +24 -0
- package/dist/ui/init-overlay.js +525 -0
- package/dist/ui/input-prompt-v2.d.ts +179 -0
- package/dist/ui/input-prompt-v2.js +991 -0
- package/dist/ui/input-prompt.d.ts +97 -0
- package/dist/ui/input-prompt.js +800 -0
- package/dist/ui/iteration-limit-overlay.d.ts +21 -0
- package/dist/ui/iteration-limit-overlay.js +150 -0
- package/dist/ui/keys-overlay.d.ts +14 -0
- package/dist/ui/keys-overlay.js +181 -0
- package/dist/ui/model-warning-overlay.d.ts +30 -0
- package/dist/ui/model-warning-overlay.js +171 -0
- package/dist/ui/overlay-controller.d.ts +25 -0
- package/dist/ui/overlay-controller.js +35 -0
- package/dist/ui/overlays.d.ts +47 -0
- package/dist/ui/overlays.js +627 -0
- package/dist/ui/permission-overlay.d.ts +16 -0
- package/dist/ui/permission-overlay.js +494 -0
- package/dist/ui/terminal.d.ts +117 -0
- package/dist/ui/terminal.js +237 -0
- package/dist/ui/todo-zone.d.ts +112 -0
- package/dist/ui/todo-zone.js +353 -0
- package/dist/ui/tools-overlay.d.ts +26 -0
- package/dist/ui/tools-overlay.js +278 -0
- package/dist/ui/tutorial-overlay.d.ts +10 -0
- package/dist/ui/tutorial-overlay.js +936 -0
- package/dist/ui/types.d.ts +103 -0
- package/dist/ui/types.js +33 -0
- package/dist/utils/credentials.d.ts +55 -0
- package/dist/utils/credentials.js +268 -0
- package/dist/utils/model-tiers.d.ts +37 -0
- package/dist/utils/model-tiers.js +118 -0
- package/dist/utils/project-memory.d.ts +47 -0
- package/dist/utils/project-memory.js +117 -0
- package/dist/utils/project-status.d.ts +56 -0
- package/dist/utils/project-status.js +237 -0
- 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>;
|