mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-18 01:51:20 -07:00
feat(plan): add 'All the above' option to multi-select AskUser questions (#22365)
Co-authored-by: jacob314 <jacob314@gmail.com>
This commit is contained in:
@@ -25,7 +25,8 @@ confirmation.
|
||||
- `label` (string, required): Display text (1-5 words).
|
||||
- `description` (string, required): Brief explanation.
|
||||
- `multiSelect` (boolean, optional): For `'choice'` type, allows selecting
|
||||
multiple options.
|
||||
multiple options. Automatically adds an "All the above" option if there
|
||||
are multiple standard options.
|
||||
- `placeholder` (string, optional): Hint text for input fields.
|
||||
|
||||
- **Behavior:**
|
||||
|
||||
@@ -87,6 +87,31 @@ describe('AskUserDialog', () => {
|
||||
writeKey(stdin, '\r'); // Toggle TS
|
||||
writeKey(stdin, '\x1b[B'); // Down
|
||||
writeKey(stdin, '\r'); // Toggle ESLint
|
||||
writeKey(stdin, '\x1b[B'); // Down to All of the above
|
||||
writeKey(stdin, '\x1b[B'); // Down to Other
|
||||
writeKey(stdin, '\x1b[B'); // Down to Done
|
||||
writeKey(stdin, '\r'); // Done
|
||||
},
|
||||
expectedSubmit: { '0': 'TypeScript, ESLint' },
|
||||
},
|
||||
{
|
||||
name: 'All of the above',
|
||||
questions: [
|
||||
{
|
||||
question: 'Which features?',
|
||||
header: 'Features',
|
||||
type: QuestionType.CHOICE,
|
||||
options: [
|
||||
{ label: 'TypeScript', description: '' },
|
||||
{ label: 'ESLint', description: '' },
|
||||
],
|
||||
multiSelect: true,
|
||||
},
|
||||
] as Question[],
|
||||
actions: (stdin: { write: (data: string) => void }) => {
|
||||
writeKey(stdin, '\x1b[B'); // Down to ESLint
|
||||
writeKey(stdin, '\x1b[B'); // Down to All of the above
|
||||
writeKey(stdin, '\r'); // Toggle All of the above
|
||||
writeKey(stdin, '\x1b[B'); // Down to Other
|
||||
writeKey(stdin, '\x1b[B'); // Down to Done
|
||||
writeKey(stdin, '\r'); // Done
|
||||
@@ -131,6 +156,42 @@ describe('AskUserDialog', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('verifies "All of the above" visual state with snapshot', async () => {
|
||||
const questions = [
|
||||
{
|
||||
question: 'Which features?',
|
||||
header: 'Features',
|
||||
type: QuestionType.CHOICE,
|
||||
options: [
|
||||
{ label: 'TypeScript', description: '' },
|
||||
{ label: 'ESLint', description: '' },
|
||||
],
|
||||
multiSelect: true,
|
||||
},
|
||||
] as Question[];
|
||||
|
||||
const { stdin, lastFrame, waitUntilReady } = renderWithProviders(
|
||||
<AskUserDialog
|
||||
questions={questions}
|
||||
onSubmit={vi.fn()}
|
||||
onCancel={vi.fn()}
|
||||
width={120}
|
||||
/>,
|
||||
{ width: 120 },
|
||||
);
|
||||
|
||||
// Navigate to "All of the above" and toggle it
|
||||
writeKey(stdin, '\x1b[B'); // Down to ESLint
|
||||
writeKey(stdin, '\x1b[B'); // Down to All of the above
|
||||
writeKey(stdin, '\r'); // Toggle All of the above
|
||||
|
||||
await waitFor(async () => {
|
||||
await waitUntilReady();
|
||||
// Verify visual state (checkmarks on all options)
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
it('handles custom option in single select with inline typing', async () => {
|
||||
const onSubmit = vi.fn();
|
||||
const { stdin, lastFrame, waitUntilReady } = renderWithProviders(
|
||||
|
||||
@@ -395,7 +395,7 @@ interface OptionItem {
|
||||
key: string;
|
||||
label: string;
|
||||
description: string;
|
||||
type: 'option' | 'other' | 'done';
|
||||
type: 'option' | 'other' | 'done' | 'all';
|
||||
index: number;
|
||||
}
|
||||
|
||||
@@ -407,6 +407,7 @@ interface ChoiceQuestionState {
|
||||
|
||||
type ChoiceQuestionAction =
|
||||
| { type: 'TOGGLE_INDEX'; payload: { index: number; multiSelect: boolean } }
|
||||
| { type: 'TOGGLE_ALL'; payload: { totalOptions: number } }
|
||||
| {
|
||||
type: 'SET_CUSTOM_SELECTED';
|
||||
payload: { selected: boolean; multiSelect: boolean };
|
||||
@@ -419,6 +420,25 @@ function choiceQuestionReducer(
|
||||
action: ChoiceQuestionAction,
|
||||
): ChoiceQuestionState {
|
||||
switch (action.type) {
|
||||
case 'TOGGLE_ALL': {
|
||||
const { totalOptions } = action.payload;
|
||||
const allSelected = state.selectedIndices.size === totalOptions;
|
||||
if (allSelected) {
|
||||
return {
|
||||
...state,
|
||||
selectedIndices: new Set(),
|
||||
};
|
||||
} else {
|
||||
const newIndices = new Set<number>();
|
||||
for (let i = 0; i < totalOptions; i++) {
|
||||
newIndices.add(i);
|
||||
}
|
||||
return {
|
||||
...state,
|
||||
selectedIndices: newIndices,
|
||||
};
|
||||
}
|
||||
}
|
||||
case 'TOGGLE_INDEX': {
|
||||
const { index, multiSelect } = action.payload;
|
||||
const newIndices = new Set(multiSelect ? state.selectedIndices : []);
|
||||
@@ -703,6 +723,18 @@ const ChoiceQuestionView: React.FC<ChoiceQuestionViewProps> = ({
|
||||
},
|
||||
);
|
||||
|
||||
// Add 'All of the above' for multi-select
|
||||
if (question.multiSelect && questionOptions.length > 1) {
|
||||
const allItem: OptionItem = {
|
||||
key: 'all',
|
||||
label: 'All of the above',
|
||||
description: 'Select all options',
|
||||
type: 'all',
|
||||
index: list.length,
|
||||
};
|
||||
list.push({ key: 'all', value: allItem });
|
||||
}
|
||||
|
||||
// Only add custom option for choice type, not yesno
|
||||
if (question.type !== 'yesno') {
|
||||
const otherItem: OptionItem = {
|
||||
@@ -755,6 +787,11 @@ const ChoiceQuestionView: React.FC<ChoiceQuestionViewProps> = ({
|
||||
type: 'TOGGLE_CUSTOM_SELECTED',
|
||||
payload: { multiSelect: true },
|
||||
});
|
||||
} else if (itemValue.type === 'all') {
|
||||
dispatch({
|
||||
type: 'TOGGLE_ALL',
|
||||
payload: { totalOptions: questionOptions.length },
|
||||
});
|
||||
} else if (itemValue.type === 'done') {
|
||||
// Done just triggers navigation, selections already saved via useEffect
|
||||
onAnswer(
|
||||
@@ -783,6 +820,7 @@ const ChoiceQuestionView: React.FC<ChoiceQuestionViewProps> = ({
|
||||
},
|
||||
[
|
||||
question.multiSelect,
|
||||
questionOptions.length,
|
||||
selectedIndices,
|
||||
isCustomOptionSelected,
|
||||
customOptionText,
|
||||
@@ -857,11 +895,16 @@ const ChoiceQuestionView: React.FC<ChoiceQuestionViewProps> = ({
|
||||
renderItem={(item, context) => {
|
||||
const optionItem = item.value;
|
||||
const isChecked =
|
||||
selectedIndices.has(optionItem.index) ||
|
||||
(optionItem.type === 'other' && isCustomOptionSelected);
|
||||
(optionItem.type === 'option' &&
|
||||
selectedIndices.has(optionItem.index)) ||
|
||||
(optionItem.type === 'other' && isCustomOptionSelected) ||
|
||||
(optionItem.type === 'all' &&
|
||||
selectedIndices.size === questionOptions.length);
|
||||
const showCheck =
|
||||
question.multiSelect &&
|
||||
(optionItem.type === 'option' || optionItem.type === 'other');
|
||||
(optionItem.type === 'option' ||
|
||||
optionItem.type === 'other' ||
|
||||
optionItem.type === 'all');
|
||||
|
||||
// Render inline text input for custom option
|
||||
if (optionItem.type === 'other') {
|
||||
|
||||
@@ -201,3 +201,19 @@ README → (not answered)
|
||||
Enter to submit · Tab/Shift+Tab to edit answers · Esc to cancel
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`AskUserDialog > verifies "All of the above" visual state with snapshot 1`] = `
|
||||
"Which features?
|
||||
(Select all that apply)
|
||||
|
||||
1. [x] TypeScript
|
||||
2. [x] ESLint
|
||||
● 3. [x] All of the above
|
||||
Select all options
|
||||
4. [ ] Enter a custom value
|
||||
Done
|
||||
Finish selection
|
||||
|
||||
Enter to select · ↑/↓ to navigate · Esc to cancel
|
||||
"
|
||||
`;
|
||||
|
||||
Reference in New Issue
Block a user