From 523018de61bd7b135d89a555bd7f71f9aa783c47 Mon Sep 17 00:00:00 2001 From: yhangf Date: Mon, 13 Apr 2026 10:37:11 +0800 Subject: [PATCH] feat(permissions): add configurable auto-select timeout for AskUserQuestion dialogs --- .../AskUserQuestionPermissionRequest.tsx | 50 +++++++++++++++++++ .../QuestionView.tsx | 11 +++- src/utils/settings/types.ts | 8 +++ 3 files changed, 68 insertions(+), 1 deletion(-) diff --git a/src/components/permissions/AskUserQuestionPermissionRequest/AskUserQuestionPermissionRequest.tsx b/src/components/permissions/AskUserQuestionPermissionRequest/AskUserQuestionPermissionRequest.tsx index 562c6653c..960dff532 100644 --- a/src/components/permissions/AskUserQuestionPermissionRequest/AskUserQuestionPermissionRequest.tsx +++ b/src/components/permissions/AskUserQuestionPermissionRequest/AskUserQuestionPermissionRequest.tsx @@ -6,6 +6,7 @@ import React, { Suspense, use, useCallback, + useEffect, useMemo, useRef, useState, @@ -85,6 +86,7 @@ function AskUserQuestionPermissionRequestBody({ // useMemo (which runs applyMarkdown over every preview) never hits its cache. // `toolUseConfirm.input` is stable for the dialog's lifetime (this tool // returns `behavior: 'ask'` directly and never goes through the classifier). + const settings = useSettings() const result = useMemo( () => AskUserQuestionTool.inputSchema.safeParse(toolUseConfirm.input), [toolUseConfirm.input], @@ -426,6 +428,53 @@ Questions asked and answers provided:\n${questionsWithAnswers}` ], ) + // Auto-select the first option for each unanswered question. + // Configurable via askUserQuestionTimeoutSeconds in .claude/settings.json (default: 600s / 10min). + // Set to 0 to immediately auto-select without showing the dialog. + const AUTO_SELECT_TIMEOUT_S = settings.askUserQuestionTimeoutSeconds ?? 600 + const [autoSelectSecondsLeft, setAutoSelectSecondsLeft] = useState(AUTO_SELECT_TIMEOUT_S) + + // When timeout is 0, immediately submit the first option for every question without showing UI. + useEffect(() => { + if (AUTO_SELECT_TIMEOUT_S !== 0) return + const autoAnswers: Record = {} + for (const q of questions) { + if (q.options.length > 0) { + autoAnswers[q.question] = q.options[0]!.label + } + } + void submitAnswers(autoAnswers).catch(logError) + // Only run once on mount — questions and submitAnswers are stable for the dialog's lifetime. + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []) + + useEffect(() => { + if (AUTO_SELECT_TIMEOUT_S === 0) return + + setAutoSelectSecondsLeft(AUTO_SELECT_TIMEOUT_S) + + const submitTimer = setTimeout(() => { + const autoAnswers: Record = { ...answers } + for (const q of questions) { + if (!autoAnswers[q.question] && q.options.length > 0) { + autoAnswers[q.question] = q.options[0]!.label + } + } + void submitAnswers(autoAnswers).catch(logError) + }, AUTO_SELECT_TIMEOUT_S * 1000) + + const countdownInterval = setInterval(() => { + setAutoSelectSecondsLeft(s => Math.max(0, s - 1)) + }, 1000) + + return () => { + clearTimeout(submitTimer) + clearInterval(countdownInterval) + } + // Reset the timer whenever the user provides an answer + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [answers]) + const handleQuestionAnswer = useCallback( ( questionText: string, @@ -541,6 +590,7 @@ Questions asked and answers provided:\n${questionsWithAnswers}` onSubmit={nextQuestion} onTabPrev={handleTabPrev} onTabNext={handleTabNext} + autoSelectSecondsLeft={autoSelectSecondsLeft} onRespondToClaude={handleRespondToClaude} onFinishPlanInterview={handleFinishPlanInterview} onImagePaste={(base64, mediaType, filename, dims, path) => diff --git a/src/components/permissions/AskUserQuestionPermissionRequest/QuestionView.tsx b/src/components/permissions/AskUserQuestionPermissionRequest/QuestionView.tsx index ec4aa3ad7..80c565e68 100644 --- a/src/components/permissions/AskUserQuestionPermissionRequest/QuestionView.tsx +++ b/src/components/permissions/AskUserQuestionPermissionRequest/QuestionView.tsx @@ -51,6 +51,7 @@ type Props = { onSubmit: () => void onTabPrev?: () => void onTabNext?: () => void + autoSelectSecondsLeft?: number onRespondToClaude: () => void onFinishPlanInterview: () => void onImagePaste?: ( @@ -73,6 +74,7 @@ export function QuestionView({ planFilePath, minContentHeight, minContentWidth, + autoSelectSecondsLeft, onUpdateQuestionState, onAnswer, onTextInputFocus, @@ -262,7 +264,14 @@ export function QuestionView({ answers={answers} hideSubmitTab={hideSubmitTab} /> - + + + {autoSelectSecondsLeft !== undefined && ( + + ({autoSelectSecondsLeft}s) + + )} + diff --git a/src/utils/settings/types.ts b/src/utils/settings/types.ts index af3a37ea9..ec265f277 100644 --- a/src/utils/settings/types.ts +++ b/src/utils/settings/types.ts @@ -936,6 +936,14 @@ export const SettingsSchema = lazySchema(() => ), } : {}), + askUserQuestionTimeoutSeconds: z + .number() + .int() + .nonnegative() + .optional() + .describe( + 'Timeout in seconds before the first option is auto-selected in AskUserQuestion dialogs (default: 600). Set to 0 to immediately select the first option without showing the dialog.', + ), prefersReducedMotion: z .boolean() .optional()