gencode-ai 0.1.2 → 0.1.3

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 (60) hide show
  1. package/CLAUDE.md +86 -0
  2. package/README.md +14 -17
  3. package/dist/agent/agent.d.ts +8 -0
  4. package/dist/agent/agent.d.ts.map +1 -1
  5. package/dist/agent/agent.js +14 -1
  6. package/dist/agent/agent.js.map +1 -1
  7. package/dist/agent/index.d.ts +1 -0
  8. package/dist/agent/index.d.ts.map +1 -1
  9. package/dist/agent/types.d.ts +14 -1
  10. package/dist/agent/types.d.ts.map +1 -1
  11. package/dist/cli/components/App.d.ts.map +1 -1
  12. package/dist/cli/components/App.js +38 -3
  13. package/dist/cli/components/App.js.map +1 -1
  14. package/dist/cli/components/Messages.d.ts.map +1 -1
  15. package/dist/cli/components/Messages.js +17 -0
  16. package/dist/cli/components/Messages.js.map +1 -1
  17. package/dist/cli/components/QuestionPrompt.d.ts +23 -0
  18. package/dist/cli/components/QuestionPrompt.d.ts.map +1 -0
  19. package/dist/cli/components/QuestionPrompt.js +231 -0
  20. package/dist/cli/components/QuestionPrompt.js.map +1 -0
  21. package/dist/cli/components/index.d.ts +1 -0
  22. package/dist/cli/components/index.d.ts.map +1 -1
  23. package/dist/cli/components/index.js +1 -0
  24. package/dist/cli/components/index.js.map +1 -1
  25. package/dist/cli/components/theme.d.ts +7 -0
  26. package/dist/cli/components/theme.d.ts.map +1 -1
  27. package/dist/cli/components/theme.js +11 -1
  28. package/dist/cli/components/theme.js.map +1 -1
  29. package/dist/permissions/types.d.ts.map +1 -1
  30. package/dist/permissions/types.js +2 -0
  31. package/dist/permissions/types.js.map +1 -1
  32. package/dist/tools/builtin/ask-user.d.ts +64 -0
  33. package/dist/tools/builtin/ask-user.d.ts.map +1 -0
  34. package/dist/tools/builtin/ask-user.js +148 -0
  35. package/dist/tools/builtin/ask-user.js.map +1 -0
  36. package/dist/tools/index.d.ts +12 -0
  37. package/dist/tools/index.d.ts.map +1 -1
  38. package/dist/tools/index.js +4 -0
  39. package/dist/tools/index.js.map +1 -1
  40. package/dist/tools/types.d.ts +17 -0
  41. package/dist/tools/types.d.ts.map +1 -1
  42. package/dist/tools/types.js.map +1 -1
  43. package/docs/proposals/0012-ask-user-question.md +66 -1
  44. package/docs/proposals/README.md +1 -1
  45. package/examples/test-ask-user.ts +167 -0
  46. package/package.json +1 -1
  47. package/src/agent/agent.ts +20 -1
  48. package/src/agent/index.ts +1 -0
  49. package/src/agent/types.ts +13 -1
  50. package/src/cli/components/App.tsx +55 -3
  51. package/src/cli/components/Messages.tsx +43 -0
  52. package/src/cli/components/QuestionPrompt.tsx +462 -0
  53. package/src/cli/components/index.ts +1 -0
  54. package/src/cli/components/theme.ts +11 -1
  55. package/src/permissions/types.ts +2 -0
  56. package/src/prompts/system/base.txt +42 -0
  57. package/src/prompts/tools/ask-user.txt +110 -0
  58. package/src/tools/builtin/ask-user.ts +185 -0
  59. package/src/tools/index.ts +15 -0
  60. package/src/tools/types.ts +18 -0
@@ -28,8 +28,10 @@ import {
28
28
  PermissionAuditDisplay,
29
29
  } from './PermissionPrompt.js';
30
30
  import { TodoList } from './TodoList.js';
31
+ import { QuestionPrompt, AnswerDisplay } from './QuestionPrompt.js';
31
32
  import { colors, icons } from './theme.js';
32
- import { getTodos } from '../../tools/index.js';
33
+ import { getTodos, formatAnswersForDisplay } from '../../tools/index.js';
34
+ import type { Question, QuestionAnswer } from '../../tools/types.js';
33
35
  import type { ProviderName } from '../../providers/index.js';
34
36
  import type { ApprovalAction, ApprovalSuggestion } from '../../permissions/types.js';
35
37
  import { gatherContextFiles, buildInitPrompt, getContextSummary } from '../../memory/index.js';
@@ -49,6 +51,11 @@ interface ConfirmState {
49
51
  resolve: (action: ApprovalAction) => void;
50
52
  }
51
53
 
54
+ interface QuestionState {
55
+ questions: Question[];
56
+ resolve: (answers: QuestionAnswer[]) => void;
57
+ }
58
+
52
59
  interface SettingsManager {
53
60
  save: (settings: { model?: string }) => Promise<void>;
54
61
  getCwd?: () => string;
@@ -235,6 +242,7 @@ export function App({ config, settingsManager, resumeLatest, permissionSettings
235
242
  const [processingStartTime, setProcessingStartTime] = useState<number | undefined>(undefined);
236
243
  const [tokenCount, setTokenCount] = useState(0);
237
244
  const [confirmState, setConfirmState] = useState<ConfirmState | null>(null);
245
+ const [questionState, setQuestionState] = useState<QuestionState | null>(null);
238
246
  const [showModelSelector, setShowModelSelector] = useState(false);
239
247
  const [showProviderManager, setShowProviderManager] = useState(false);
240
248
  const [currentModel, setCurrentModel] = useState(config.model);
@@ -276,6 +284,13 @@ export function App({ config, settingsManager, resumeLatest, permissionSettings
276
284
  });
277
285
  });
278
286
 
287
+ // Set askUser callback for AskUserQuestion tool
288
+ agent.setAskUserCallback(async (questions) => {
289
+ return new Promise<QuestionAnswer[]>((resolve) => {
290
+ setQuestionState({ questions, resolve });
291
+ });
292
+ });
293
+
279
294
  // Set callback to save permission rules to settings.local.json
280
295
  if (settingsManager?.addPermissionRule) {
281
296
  agent.setSaveRuleCallback(async (tool, pattern) => {
@@ -295,6 +310,32 @@ export function App({ config, settingsManager, resumeLatest, permissionSettings
295
310
  init();
296
311
  }, [agent, resumeLatest, addHistory, permissionSettings, settingsManager]);
297
312
 
313
+ // Handle question answers (AskUserQuestion)
314
+ const handleQuestionComplete = useCallback((answers: QuestionAnswer[]) => {
315
+ if (questionState) {
316
+ // Show confirmation in history
317
+ addHistory({
318
+ type: 'info',
319
+ content: formatAnswersForDisplay(answers),
320
+ });
321
+ questionState.resolve(answers);
322
+ setQuestionState(null);
323
+ }
324
+ }, [questionState, addHistory]);
325
+
326
+ // Handle question cancel
327
+ const handleQuestionCancel = useCallback(() => {
328
+ if (questionState) {
329
+ // Clear pending tool display (no more spinner)
330
+ pendingToolRef.current = null;
331
+ setPendingTool(null);
332
+ // Add canceled message to history
333
+ addHistory({ type: 'info', content: 'Question canceled' });
334
+ questionState.resolve([]); // Return empty answers on cancel
335
+ setQuestionState(null);
336
+ }
337
+ }, [questionState, addHistory]);
338
+
298
339
  // Handle permission decision
299
340
  const handlePermissionDecision = (action: ApprovalAction) => {
300
341
  if (confirmState) {
@@ -730,6 +771,9 @@ export function App({ config, settingsManager, resumeLatest, permissionSettings
730
771
  setIsProcessing(false);
731
772
  setStreamingText('');
732
773
  streamingTextRef.current = '';
774
+ // Clear pending tool (stop spinner)
775
+ pendingToolRef.current = null;
776
+ setPendingTool(null);
733
777
  addHistory({ type: 'info', content: 'Interrupted' });
734
778
  }
735
779
 
@@ -837,6 +881,14 @@ export function App({ config, settingsManager, resumeLatest, permissionSettings
837
881
  />
838
882
  )}
839
883
 
884
+ {questionState && (
885
+ <QuestionPrompt
886
+ questions={questionState.questions}
887
+ onComplete={handleQuestionComplete}
888
+ onCancel={handleQuestionCancel}
889
+ />
890
+ )}
891
+
840
892
  {showModelSelector && (
841
893
  <Box marginTop={1}>
842
894
  <ModelSelector
@@ -861,7 +913,7 @@ export function App({ config, settingsManager, resumeLatest, permissionSettings
861
913
  </Box>
862
914
  )}
863
915
 
864
- {!confirmState && !showModelSelector && !showProviderManager && (
916
+ {!confirmState && !questionState && !showModelSelector && !showProviderManager && (
865
917
  <Box flexDirection="column" marginTop={1}>
866
918
  <PromptInput
867
919
  key={inputKey}
@@ -875,7 +927,7 @@ export function App({ config, settingsManager, resumeLatest, permissionSettings
875
927
  </Box>
876
928
  )}
877
929
 
878
- {isProcessing && !confirmState ? (
930
+ {isProcessing && !confirmState && !questionState ? (
879
931
  <ProgressBar startTime={processingStartTime} tokenCount={tokenCount} isThinking={isThinking} />
880
932
  ) : showCmdSuggestions && cmdSuggestions.length > 0 ? (
881
933
  <Box marginTop={1}>
@@ -126,6 +126,11 @@ function formatToolInput(name: string, input: Record<string, unknown>): string {
126
126
  const todos = input.todos as Array<{ content: string; status: string }> || [];
127
127
  return `${todos.length} task${todos.length !== 1 ? 's' : ''}`;
128
128
  }
129
+ case 'AskUserQuestion': {
130
+ // Show collapsed JSON preview
131
+ const json = JSON.stringify(input);
132
+ return truncate(json, 60);
133
+ }
129
134
  default:
130
135
  return truncate(JSON.stringify(input), 40);
131
136
  }
@@ -135,6 +140,25 @@ export function ToolCall({ name, input }: ToolCallProps) {
135
140
  // Hide TodoWrite (shown in TodoList component)
136
141
  if (name === 'TodoWrite') return null;
137
142
 
143
+ // Special display for AskUserQuestion (Claude Code style with expand hint)
144
+ if (name === 'AskUserQuestion') {
145
+ const json = JSON.stringify(input);
146
+ const displayJson = truncate(json, 70);
147
+
148
+ return (
149
+ <Box marginTop={1} flexDirection="column">
150
+ <Box>
151
+ <Text color={colors.tool}>{icons.tool}</Text>
152
+ <Text> </Text>
153
+ <Text bold>{name}</Text>
154
+ <Text color={colors.textMuted}> </Text>
155
+ <Text color={colors.textSecondary}>{displayJson}</Text>
156
+ <Text color={colors.textMuted}> ctrl+o</Text>
157
+ </Box>
158
+ </Box>
159
+ );
160
+ }
161
+
138
162
  const displayInput = formatToolInput(name, input);
139
163
 
140
164
  return (
@@ -162,6 +186,25 @@ export function PendingToolCall({ name, input }: PendingToolCallProps) {
162
186
  // Hide TodoWrite (shown in TodoList component)
163
187
  if (name === 'TodoWrite') return null;
164
188
 
189
+ // Special display for AskUserQuestion
190
+ if (name === 'AskUserQuestion') {
191
+ const json = JSON.stringify(input);
192
+ const displayJson = truncate(json, 70);
193
+
194
+ return (
195
+ <Box marginTop={1}>
196
+ <Text color={colors.tool}>
197
+ <InkSpinner type="dots" />
198
+ </Text>
199
+ <Text> </Text>
200
+ <Text bold>{name}</Text>
201
+ <Text color={colors.textMuted}> </Text>
202
+ <Text color={colors.textSecondary}>{displayJson}</Text>
203
+ <Text color={colors.textMuted}> ctrl+o</Text>
204
+ </Box>
205
+ );
206
+ }
207
+
165
208
  const displayInput = formatToolInput(name, input);
166
209
 
167
210
  return (
@@ -0,0 +1,462 @@
1
+ /**
2
+ * QuestionPrompt Component - Claude Code style structured questioning UI
3
+ *
4
+ * Matches Claude Code's AskUserQuestion UI pattern:
5
+ * - Progress bar with step indicators (← □ Step1 □ Step2 ✓ Submit →)
6
+ * - Numbered options with cursor indicator
7
+ * - Description on separate line
8
+ * - Keyboard hints at bottom
9
+ */
10
+
11
+ import { useState, useCallback } from 'react';
12
+ import { Box, Text, useInput } from 'ink';
13
+ import { colors } from './theme.js';
14
+ import type { Question, QuestionAnswer } from '../../tools/types.js';
15
+
16
+ // ============================================================================
17
+ // Types
18
+ // ============================================================================
19
+
20
+ interface QuestionPromptProps {
21
+ questions: Question[];
22
+ onComplete: (answers: QuestionAnswer[]) => void;
23
+ onCancel?: () => void;
24
+ }
25
+
26
+ interface OptionWithOther {
27
+ label: string;
28
+ description: string;
29
+ isOther?: boolean;
30
+ }
31
+
32
+ // ============================================================================
33
+ // Progress Bar Component
34
+ // ============================================================================
35
+
36
+ interface ProgressBarProps {
37
+ questions: Question[];
38
+ currentIndex: number;
39
+ answers: QuestionAnswer[];
40
+ showSubmit?: boolean;
41
+ }
42
+
43
+ function ProgressBar({ questions, currentIndex, answers, showSubmit = true }: ProgressBarProps) {
44
+ const isReviewMode = currentIndex >= questions.length;
45
+
46
+ return (
47
+ <Box>
48
+ <Text color={colors.textMuted}>← </Text>
49
+ {questions.map((q, idx) => {
50
+ const isCompleted = idx < answers.length;
51
+ const isCurrent = idx === currentIndex;
52
+ const checkmark = isCompleted ? '☒' : '☐';
53
+
54
+ return (
55
+ <Box key={idx}>
56
+ {isCurrent ? (
57
+ <Text backgroundColor="#6366F1" color="#FFFFFF" bold>
58
+ {` □ ${q.header} `}
59
+ </Text>
60
+ ) : (
61
+ <Text color={isCompleted ? colors.textSecondary : colors.textMuted}>
62
+ {checkmark} {q.header}
63
+ </Text>
64
+ )}
65
+ <Text color={colors.textMuted}> </Text>
66
+ </Box>
67
+ );
68
+ })}
69
+ {showSubmit && (
70
+ <>
71
+ {isReviewMode ? (
72
+ <Text backgroundColor="#22C55E" color="#FFFFFF" bold>
73
+ {` ✓ Submit `}
74
+ </Text>
75
+ ) : (
76
+ <Text color={colors.textMuted}>✓ Submit</Text>
77
+ )}
78
+ </>
79
+ )}
80
+ <Text color={colors.textMuted}> →</Text>
81
+ </Box>
82
+ );
83
+ }
84
+
85
+ // ============================================================================
86
+ // QuestionPrompt Component
87
+ // ============================================================================
88
+
89
+ export function QuestionPrompt({ questions, onComplete, onCancel }: QuestionPromptProps) {
90
+ const [currentQuestionIndex, setCurrentQuestionIndex] = useState(0);
91
+ const [answers, setAnswers] = useState<QuestionAnswer[]>([]);
92
+ const [selectedOptions, setSelectedOptions] = useState<Set<string>>(new Set());
93
+ const [optionIndex, setOptionIndex] = useState(0);
94
+ const [showOtherInput, setShowOtherInput] = useState(false);
95
+ const [otherInput, setOtherInput] = useState('');
96
+ const [isReviewMode, setIsReviewMode] = useState(false);
97
+ const [reviewOptionIndex, setReviewOptionIndex] = useState(0);
98
+
99
+ const currentQuestion = questions[currentQuestionIndex];
100
+ const showProgressBar = questions.length > 1;
101
+
102
+ // Add "Type something." option to the list
103
+ const optionsWithOther: OptionWithOther[] = currentQuestion
104
+ ? [
105
+ ...currentQuestion.options,
106
+ { label: 'Type something.', description: '', isOther: true },
107
+ ]
108
+ : [];
109
+
110
+ // Toggle option selection (for multi-select)
111
+ const toggleOption = useCallback(() => {
112
+ const option = optionsWithOther[optionIndex];
113
+ setSelectedOptions((prev) => {
114
+ const next = new Set(prev);
115
+ if (next.has(option.label)) {
116
+ next.delete(option.label);
117
+ } else {
118
+ next.add(option.label);
119
+ }
120
+ return next;
121
+ });
122
+ }, [optionIndex, optionsWithOther]);
123
+
124
+ // Finish current question and move to next or review
125
+ const finishQuestion = useCallback(
126
+ (selected: string[], customInput?: string) => {
127
+ const answer: QuestionAnswer = {
128
+ question: currentQuestion.question,
129
+ header: currentQuestion.header,
130
+ selectedOptions: selected.filter((s) => s !== 'Type something.'),
131
+ customInput,
132
+ };
133
+
134
+ const newAnswers = [...answers, answer];
135
+
136
+ if (currentQuestionIndex < questions.length - 1) {
137
+ // Move to next question
138
+ setAnswers(newAnswers);
139
+ setCurrentQuestionIndex((i) => i + 1);
140
+ setSelectedOptions(new Set());
141
+ setOptionIndex(0);
142
+ setShowOtherInput(false);
143
+ setOtherInput('');
144
+ } else {
145
+ // All questions answered, go to review mode (for multi-question)
146
+ setAnswers(newAnswers);
147
+ if (questions.length > 1) {
148
+ setIsReviewMode(true);
149
+ setReviewOptionIndex(0);
150
+ } else {
151
+ // Single question, complete immediately
152
+ onComplete(newAnswers);
153
+ }
154
+ }
155
+ },
156
+ [currentQuestion, currentQuestionIndex, questions.length, answers, onComplete]
157
+ );
158
+
159
+ // Handle selection (Enter key)
160
+ const handleSelect = useCallback(() => {
161
+ const option = optionsWithOther[optionIndex];
162
+
163
+ if (currentQuestion.multiSelect) {
164
+ if (selectedOptions.size === 0) {
165
+ toggleOption();
166
+ return;
167
+ }
168
+ if (selectedOptions.has('Type something.')) {
169
+ setShowOtherInput(true);
170
+ } else {
171
+ finishQuestion([...selectedOptions]);
172
+ }
173
+ } else {
174
+ if (option.isOther) {
175
+ setShowOtherInput(true);
176
+ } else {
177
+ finishQuestion([option.label]);
178
+ }
179
+ }
180
+ }, [
181
+ optionIndex,
182
+ optionsWithOther,
183
+ currentQuestion?.multiSelect,
184
+ selectedOptions,
185
+ toggleOption,
186
+ finishQuestion,
187
+ ]);
188
+
189
+ // Handle keyboard input
190
+ useInput((input, key) => {
191
+ // Review mode navigation
192
+ if (isReviewMode) {
193
+ if (key.upArrow) {
194
+ setReviewOptionIndex((i) => Math.max(0, i - 1));
195
+ } else if (key.downArrow) {
196
+ setReviewOptionIndex((i) => Math.min(1, i + 1));
197
+ } else if (key.return) {
198
+ if (reviewOptionIndex === 0) {
199
+ // Submit
200
+ onComplete(answers);
201
+ } else {
202
+ // Cancel
203
+ onCancel?.();
204
+ }
205
+ } else if (key.escape) {
206
+ onCancel?.();
207
+ }
208
+ return;
209
+ }
210
+
211
+ if (showOtherInput) {
212
+ if (key.return) {
213
+ if (otherInput.trim()) {
214
+ if (currentQuestion.multiSelect) {
215
+ const selected = [...selectedOptions].filter((s) => s !== 'Type something.');
216
+ finishQuestion(selected, otherInput.trim());
217
+ } else {
218
+ finishQuestion([], otherInput.trim());
219
+ }
220
+ }
221
+ } else if (key.escape) {
222
+ setShowOtherInput(false);
223
+ setOtherInput('');
224
+ } else if (key.backspace || key.delete) {
225
+ setOtherInput((prev) => prev.slice(0, -1));
226
+ } else if (input && !key.ctrl && !key.meta) {
227
+ setOtherInput((prev) => prev + input);
228
+ }
229
+ return;
230
+ }
231
+
232
+ // Navigation
233
+ if (key.upArrow) {
234
+ setOptionIndex((i) => Math.max(0, i - 1));
235
+ } else if (key.downArrow) {
236
+ setOptionIndex((i) => Math.min(optionsWithOther.length - 1, i + 1));
237
+ } else if (key.tab) {
238
+ // Tab cycles through options
239
+ setOptionIndex((i) => (i + 1) % optionsWithOther.length);
240
+ }
241
+
242
+ // Selection
243
+ if (key.return) {
244
+ handleSelect();
245
+ }
246
+
247
+ // Toggle (multi-select only)
248
+ if (input === ' ' && currentQuestion.multiSelect) {
249
+ toggleOption();
250
+ }
251
+
252
+ // Number shortcuts (1-5)
253
+ const num = parseInt(input, 10);
254
+ if (num >= 1 && num <= optionsWithOther.length) {
255
+ setOptionIndex(num - 1);
256
+ if (!currentQuestion.multiSelect) {
257
+ const option = optionsWithOther[num - 1];
258
+ if (option.isOther) {
259
+ setShowOtherInput(true);
260
+ } else {
261
+ finishQuestion([option.label]);
262
+ }
263
+ }
264
+ }
265
+
266
+ // Escape to cancel
267
+ if (key.escape && onCancel) {
268
+ onCancel();
269
+ }
270
+ });
271
+
272
+ // Review mode UI
273
+ if (isReviewMode) {
274
+ return (
275
+ <Box flexDirection="column" marginTop={1}>
276
+ {/* Progress bar */}
277
+ {showProgressBar && (
278
+ <Box marginBottom={1}>
279
+ <ProgressBar
280
+ questions={questions}
281
+ currentIndex={questions.length}
282
+ answers={answers}
283
+ />
284
+ </Box>
285
+ )}
286
+
287
+ {/* Review title */}
288
+ <Box marginBottom={1}>
289
+ <Text bold>Review your answers</Text>
290
+ </Box>
291
+
292
+ {/* Answered questions summary */}
293
+ <Box flexDirection="column" marginBottom={1} paddingLeft={1}>
294
+ {answers.map((answer, idx) => {
295
+ const selections = answer.customInput
296
+ ? [...answer.selectedOptions, answer.customInput].join(', ')
297
+ : answer.selectedOptions.join(', ');
298
+
299
+ return (
300
+ <Box key={idx} flexDirection="column" marginBottom={1}>
301
+ <Box>
302
+ <Text color={colors.textSecondary}>● </Text>
303
+ <Text bold>{answer.question}</Text>
304
+ </Box>
305
+ <Box paddingLeft={2}>
306
+ <Text color={colors.primary}>→ {selections}</Text>
307
+ </Box>
308
+ </Box>
309
+ );
310
+ })}
311
+ </Box>
312
+
313
+ {/* Ready to submit */}
314
+ <Box marginBottom={1}>
315
+ <Text>Ready to submit your answers?</Text>
316
+ </Box>
317
+
318
+ {/* Submit/Cancel options */}
319
+ <Box flexDirection="column" paddingLeft={1}>
320
+ <Box>
321
+ <Text color={reviewOptionIndex === 0 ? colors.text : colors.textMuted}>
322
+ {reviewOptionIndex === 0 ? '❯ ' : ' '}
323
+ </Text>
324
+ <Text color={colors.primary} bold={reviewOptionIndex === 0}>
325
+ 1. Submit answers
326
+ </Text>
327
+ </Box>
328
+ <Box>
329
+ <Text color={reviewOptionIndex === 1 ? colors.text : colors.textMuted}>
330
+ {reviewOptionIndex === 1 ? '❯ ' : ' '}
331
+ </Text>
332
+ <Text bold={reviewOptionIndex === 1}>2. Cancel</Text>
333
+ </Box>
334
+ </Box>
335
+
336
+ {/* Separator */}
337
+ <Box marginTop={1} marginBottom={1}>
338
+ <Text color={colors.textMuted}>{'─'.repeat(60)}</Text>
339
+ </Box>
340
+
341
+ {/* Keyboard hints */}
342
+ <Box>
343
+ <Text color={colors.textMuted}>
344
+ Enter to select · Tab/Arrow keys to navigate · Esc to cancel
345
+ </Text>
346
+ </Box>
347
+ </Box>
348
+ );
349
+ }
350
+
351
+ // Normal question mode UI
352
+ return (
353
+ <Box flexDirection="column" marginTop={1}>
354
+ {/* Progress bar */}
355
+ {showProgressBar && (
356
+ <Box marginBottom={1}>
357
+ <ProgressBar
358
+ questions={questions}
359
+ currentIndex={currentQuestionIndex}
360
+ answers={answers}
361
+ />
362
+ </Box>
363
+ )}
364
+
365
+ {/* Question text */}
366
+ <Box marginBottom={1}>
367
+ <Text bold>{currentQuestion.question}</Text>
368
+ </Box>
369
+
370
+ {/* Options list */}
371
+ <Box flexDirection="column" paddingLeft={1}>
372
+ {optionsWithOther.map((option, index) => {
373
+ const isSelected = index === optionIndex;
374
+ const isChecked = selectedOptions.has(option.label);
375
+
376
+ return (
377
+ <Box key={option.label} flexDirection="column" marginBottom={option.description ? 1 : 0}>
378
+ {/* Option row */}
379
+ <Box>
380
+ <Text color={isSelected ? colors.text : colors.textMuted}>
381
+ {isSelected ? '❯ ' : ' '}
382
+ </Text>
383
+ <Text color={colors.textMuted}>{index + 1}. </Text>
384
+ {currentQuestion.multiSelect && (
385
+ <Text color={colors.primary}>{isChecked ? '☒ ' : '☐ '}</Text>
386
+ )}
387
+ <Text color={colors.primary} bold>
388
+ {option.label}
389
+ </Text>
390
+ </Box>
391
+
392
+ {/* Description (if exists) */}
393
+ {option.description && (
394
+ <Box paddingLeft={4}>
395
+ <Text color={colors.textMuted}>{option.description}</Text>
396
+ </Box>
397
+ )}
398
+ </Box>
399
+ );
400
+ })}
401
+ </Box>
402
+
403
+ {/* Other input field */}
404
+ {showOtherInput && (
405
+ <Box marginTop={1} paddingLeft={1}>
406
+ <Text color={colors.textMuted}>Type your answer: </Text>
407
+ <Text>{otherInput}</Text>
408
+ <Text color={colors.primary}>▋</Text>
409
+ </Box>
410
+ )}
411
+
412
+ {/* Separator */}
413
+ <Box marginTop={1} marginBottom={1}>
414
+ <Text color={colors.textMuted}>{'─'.repeat(60)}</Text>
415
+ </Box>
416
+
417
+ {/* Chat about this (placeholder) */}
418
+ <Box marginBottom={1}>
419
+ <Text color={colors.textMuted}> Chat about this</Text>
420
+ </Box>
421
+
422
+ {/* Keyboard hints */}
423
+ <Box>
424
+ <Text color={colors.textMuted}>
425
+ Enter to select · Tab/Arrow keys to navigate
426
+ {currentQuestion.multiSelect && ' · Space to toggle'}
427
+ {onCancel && ' · Esc to cancel'}
428
+ </Text>
429
+ </Box>
430
+ </Box>
431
+ );
432
+ }
433
+
434
+ // ============================================================================
435
+ // Answer Display Component (shown after completion)
436
+ // ============================================================================
437
+
438
+ interface AnswerDisplayProps {
439
+ answers: QuestionAnswer[];
440
+ compact?: boolean;
441
+ }
442
+
443
+ export function AnswerDisplay({ answers, compact = true }: AnswerDisplayProps) {
444
+ return (
445
+ <Box flexDirection="column">
446
+ {answers.map((answer, index) => {
447
+ const selections = answer.customInput
448
+ ? [...answer.selectedOptions, answer.customInput].join(', ')
449
+ : answer.selectedOptions.join(', ');
450
+
451
+ return (
452
+ <Box key={index}>
453
+ <Text color={colors.textSecondary}>● </Text>
454
+ <Text>{answer.question}</Text>
455
+ <Text color={colors.textMuted}> → </Text>
456
+ <Text color={colors.primary}>{selections || '(none)'}</Text>
457
+ </Box>
458
+ );
459
+ })}
460
+ </Box>
461
+ );
462
+ }
@@ -25,3 +25,4 @@ export {
25
25
  PermissionRulesDisplay,
26
26
  PermissionAuditDisplay,
27
27
  } from './PermissionPrompt.js';
28
+ export { QuestionPrompt, AnswerDisplay } from './QuestionPrompt.js';
@@ -35,9 +35,19 @@ export const icons = {
35
35
  // UI
36
36
  thinking: '✱', // Star for thinking state
37
37
  cursor: '▋',
38
- // Selection
38
+ // Selection (single-select)
39
39
  radio: '●', // Filled radio for selected
40
40
  radioEmpty: '○', // Empty radio for unselected
41
+ // Selection (multi-select)
42
+ checkbox: '☑', // Checked checkbox
43
+ checkboxEmpty: '☐', // Empty checkbox
44
+ // Chip/tag borders (Claude Code style headers)
45
+ chipLeft: '╭─',
46
+ chipRight: '─╮',
47
+ // Box drawing
48
+ boxTop: '╭',
49
+ boxBottom: '╰',
50
+ boxVertical: '│',
41
51
  // Tree connectors
42
52
  treeEnd: '└', // Tree end connector for tool results
43
53
  };
@@ -227,6 +227,8 @@ export const DEFAULT_PERMISSION_CONFIG: PermissionConfig = {
227
227
  { tool: 'LSP', mode: 'auto', description: 'Language server' },
228
228
  // Internal state management - auto-approve (no side effects)
229
229
  { tool: 'TodoWrite', mode: 'auto', description: 'Task tracking' },
230
+ // User interaction - auto-approve (asking user questions, not dangerous)
231
+ { tool: 'AskUserQuestion', mode: 'auto', description: 'User questioning' },
230
232
  ],
231
233
  allowedPrompts: [],
232
234
  };