mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-28 03:30:40 +00:00
fix(cli): auto-submit on number key press in AskUserQuestionDialog (#3407)
Fixes #500. Number keys in AskUserQuestionDialog previously only moved the highlight cursor without submitting, inconsistent with RadioButtonSelect and the standard tool approval dialog. Users pressed a number, saw the option highlight, and assumed it was selected, but the dialog was still waiting for Enter. - For single-select predefined options, pressing a number key now auto-submits immediately. - Multi-select, "Other" custom input, and the Submit tab remain highlight-only (unchanged). - Extracted a shared selectAndAdvance helper to deduplicate the select-and-submit/advance logic across 4 code paths (number key, Enter, multi-select submit, custom input submit). - Removed redundant isFocused guard inside the useKeypress callback; it is already handled via the isActive parameter. Tests cover all four behavioral branches: single-select auto-submits, multi-select does not, "Other" custom input does not, and the Submit tab does not.
This commit is contained in:
parent
ed6f9e056e
commit
699cb05206
2 changed files with 135 additions and 65 deletions
|
|
@ -173,6 +173,50 @@ describe('<AskUserQuestionDialog />', () => {
|
|||
);
|
||||
unmount();
|
||||
});
|
||||
it('auto-submits when pressing a number key for a predefined option', async () => {
|
||||
const onConfirm = vi.fn();
|
||||
const details = createConfirmationDetails();
|
||||
|
||||
const { stdin, unmount } = renderWithProviders(
|
||||
<AskUserQuestionDialog
|
||||
confirmationDetails={details}
|
||||
onConfirm={onConfirm}
|
||||
/>,
|
||||
);
|
||||
await wait();
|
||||
|
||||
// Press '2' to select the second option (Blue) — should auto-submit
|
||||
stdin.write('2');
|
||||
await wait();
|
||||
|
||||
expect(onConfirm).toHaveBeenCalledWith(
|
||||
ToolConfirmationOutcome.ProceedOnce,
|
||||
{ answers: { 0: 'Blue' } },
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('does not auto-submit when pressing number key for "Other" custom input', async () => {
|
||||
const onConfirm = vi.fn();
|
||||
const details = createConfirmationDetails();
|
||||
|
||||
const { stdin, unmount } = renderWithProviders(
|
||||
<AskUserQuestionDialog
|
||||
confirmationDetails={details}
|
||||
onConfirm={onConfirm}
|
||||
/>,
|
||||
);
|
||||
await wait();
|
||||
|
||||
// Press '4' to select the "Other" option (index 3, after 3 predefined options)
|
||||
stdin.write('4');
|
||||
await wait();
|
||||
|
||||
// Should NOT auto-submit — just highlight "Other" for text input
|
||||
expect(onConfirm).not.toHaveBeenCalled();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('cancels with Escape', async () => {
|
||||
const onConfirm = vi.fn();
|
||||
const details = createConfirmationDetails();
|
||||
|
|
@ -194,6 +238,28 @@ describe('<AskUserQuestionDialog />', () => {
|
|||
});
|
||||
|
||||
describe('multi-select interaction', () => {
|
||||
it('does not auto-submit when pressing number key in multi-select mode', async () => {
|
||||
const onConfirm = vi.fn();
|
||||
const details = createConfirmationDetails({
|
||||
questions: [createSingleQuestion({ multiSelect: true })],
|
||||
});
|
||||
|
||||
const { stdin, unmount } = renderWithProviders(
|
||||
<AskUserQuestionDialog
|
||||
confirmationDetails={details}
|
||||
onConfirm={onConfirm}
|
||||
/>,
|
||||
);
|
||||
await wait();
|
||||
|
||||
// Press '2' — should only move highlight, not submit
|
||||
stdin.write('2');
|
||||
await wait();
|
||||
|
||||
expect(onConfirm).not.toHaveBeenCalled();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('toggles options with Space', async () => {
|
||||
const onConfirm = vi.fn();
|
||||
const details = createConfirmationDetails({
|
||||
|
|
@ -219,6 +285,40 @@ describe('<AskUserQuestionDialog />', () => {
|
|||
});
|
||||
|
||||
describe('multiple questions', () => {
|
||||
it.skipIf(process.platform === 'win32')(
|
||||
'does not auto-submit when pressing number key on Submit tab',
|
||||
async () => {
|
||||
const onConfirm = vi.fn();
|
||||
const details = createConfirmationDetails({
|
||||
questions: [
|
||||
createSingleQuestion({ header: 'Q1' }),
|
||||
createSingleQuestion({ header: 'Q2' }),
|
||||
],
|
||||
});
|
||||
|
||||
const { stdin, unmount } = renderWithProviders(
|
||||
<AskUserQuestionDialog
|
||||
confirmationDetails={details}
|
||||
onConfirm={onConfirm}
|
||||
/>,
|
||||
);
|
||||
await wait();
|
||||
|
||||
// Navigate to Submit tab
|
||||
stdin.write('\u001B[C'); // Right
|
||||
await wait();
|
||||
stdin.write('\u001B[C'); // Right
|
||||
await wait();
|
||||
|
||||
// Press '1' on Submit tab — should only highlight, not submit
|
||||
stdin.write('1');
|
||||
await wait();
|
||||
|
||||
expect(onConfirm).not.toHaveBeenCalled();
|
||||
unmount();
|
||||
},
|
||||
);
|
||||
|
||||
it.skipIf(process.platform === 'win32')(
|
||||
'shows unanswered questions as (not answered) in Submit tab',
|
||||
async () => {
|
||||
|
|
|
|||
|
|
@ -102,6 +102,22 @@ export const AskUserQuestionDialog: React.FC<AskUserQuestionDialogProps> = ({
|
|||
await onConfirm(ToolConfirmationOutcome.ProceedOnce, { answers });
|
||||
};
|
||||
|
||||
// Select a value for the current question, then submit (single question) or advance to the next tab (multi-question).
|
||||
const selectAndAdvance = (value: string) => {
|
||||
setSelectedOptions((prev) => ({ ...prev, [currentQuestionIndex]: value }));
|
||||
|
||||
if (!hasMultipleQuestions) {
|
||||
void onConfirm(ToolConfirmationOutcome.ProceedOnce, {
|
||||
answers: { [currentQuestionIndex]: value },
|
||||
});
|
||||
} else if (currentQuestionIndex < totalTabs - 1) {
|
||||
setTimeout(() => {
|
||||
setCurrentQuestionIndex((prev) => Math.min(prev + 1, totalTabs - 1));
|
||||
setSelectedIndex(0);
|
||||
}, 150);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMultiSelectSubmit = () => {
|
||||
if (!currentQuestion) return;
|
||||
const selections = [...(multiSelectedOptions[currentQuestionIndex] ?? [])];
|
||||
|
|
@ -111,22 +127,7 @@ export const AskUserQuestionDialog: React.FC<AskUserQuestionDialogProps> = ({
|
|||
}
|
||||
if (selections.length === 0) return;
|
||||
|
||||
const value = selections.join(', ');
|
||||
const updated = { ...selectedOptions, [currentQuestionIndex]: value };
|
||||
setSelectedOptions(updated);
|
||||
|
||||
if (!hasMultipleQuestions) {
|
||||
void onConfirm(ToolConfirmationOutcome.ProceedOnce, {
|
||||
answers: { [currentQuestionIndex]: value },
|
||||
});
|
||||
} else {
|
||||
if (currentQuestionIndex < totalTabs - 1) {
|
||||
setTimeout(() => {
|
||||
setCurrentQuestionIndex((prev) => Math.min(prev + 1, totalTabs - 1));
|
||||
setSelectedIndex(0);
|
||||
}, 150);
|
||||
}
|
||||
}
|
||||
selectAndAdvance(selections.join(', '));
|
||||
};
|
||||
|
||||
const handleCustomInputSubmit = () => {
|
||||
|
|
@ -143,37 +144,13 @@ export const AskUserQuestionDialog: React.FC<AskUserQuestionDialogProps> = ({
|
|||
}
|
||||
|
||||
if (!trimmedValue) return;
|
||||
|
||||
const updated = {
|
||||
...selectedOptions,
|
||||
[currentQuestionIndex]: trimmedValue,
|
||||
};
|
||||
setSelectedOptions(updated);
|
||||
|
||||
// If single question, submit immediately
|
||||
if (!hasMultipleQuestions) {
|
||||
void onConfirm(ToolConfirmationOutcome.ProceedOnce, {
|
||||
answers: {
|
||||
[currentQuestionIndex]: trimmedValue,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// Auto-advance to next tab
|
||||
if (currentQuestionIndex < totalTabs - 1) {
|
||||
setTimeout(() => {
|
||||
setCurrentQuestionIndex((prev) => Math.min(prev + 1, totalTabs - 1));
|
||||
setSelectedIndex(0);
|
||||
}, 150);
|
||||
}
|
||||
}
|
||||
selectAndAdvance(trimmedValue);
|
||||
};
|
||||
|
||||
// Handle navigation and selection
|
||||
useKeypress(
|
||||
(key) => {
|
||||
if (!isFocused) return;
|
||||
|
||||
// When custom input is focused, still allow up/down navigation, tab switch and escape
|
||||
// When custom input is focused, still allow up/down navigation and escape
|
||||
if (isCustomInputSelected) {
|
||||
if (key.name === 'up') {
|
||||
setSelectedIndex(Math.max(0, selectedIndex - 1));
|
||||
|
|
@ -221,7 +198,21 @@ export const AskUserQuestionDialog: React.FC<AskUserQuestionDialogProps> = ({
|
|||
// Number key selection
|
||||
const numKey = parseInt(input || '', 10);
|
||||
if (!isNaN(numKey) && numKey >= 1 && numKey <= totalOptions) {
|
||||
setSelectedIndex(numKey - 1);
|
||||
const targetIndex = numKey - 1;
|
||||
setSelectedIndex(targetIndex);
|
||||
|
||||
// For single-select, auto-submit when selecting a predefined option (not "Other")
|
||||
if (
|
||||
!isMultiSelect &&
|
||||
!isSubmitTab &&
|
||||
currentQuestion &&
|
||||
targetIndex < currentQuestion.options.length
|
||||
) {
|
||||
const option = currentQuestion.options[targetIndex];
|
||||
if (option) {
|
||||
selectAndAdvance(option.label);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -272,28 +263,7 @@ export const AskUserQuestionDialog: React.FC<AskUserQuestionDialogProps> = ({
|
|||
if (currentQuestion && selectedIndex < currentQuestion.options.length) {
|
||||
const option = currentQuestion.options[selectedIndex];
|
||||
if (option) {
|
||||
const updated = {
|
||||
...selectedOptions,
|
||||
[currentQuestionIndex]: option.label,
|
||||
};
|
||||
setSelectedOptions(updated);
|
||||
|
||||
// If single question, submit immediately
|
||||
if (!hasMultipleQuestions) {
|
||||
void onConfirm(ToolConfirmationOutcome.ProceedOnce, {
|
||||
answers: { [currentQuestionIndex]: option.label },
|
||||
});
|
||||
} else {
|
||||
// Auto-advance to next tab after selection
|
||||
if (currentQuestionIndex < totalTabs - 1) {
|
||||
setTimeout(() => {
|
||||
setCurrentQuestionIndex((prev) =>
|
||||
Math.min(prev + 1, totalTabs - 1),
|
||||
);
|
||||
setSelectedIndex(0);
|
||||
}, 150);
|
||||
}
|
||||
}
|
||||
selectAndAdvance(option.label);
|
||||
}
|
||||
}
|
||||
return;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue