feat(permissions): add configurable auto-select timeout for AskUserQuestion dialogs
This commit is contained in:
parent
70dda49946
commit
523018de61
|
|
@ -6,6 +6,7 @@ import React, {
|
||||||
Suspense,
|
Suspense,
|
||||||
use,
|
use,
|
||||||
useCallback,
|
useCallback,
|
||||||
|
useEffect,
|
||||||
useMemo,
|
useMemo,
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
|
|
@ -85,6 +86,7 @@ function AskUserQuestionPermissionRequestBody({
|
||||||
// useMemo (which runs applyMarkdown over every preview) never hits its cache.
|
// useMemo (which runs applyMarkdown over every preview) never hits its cache.
|
||||||
// `toolUseConfirm.input` is stable for the dialog's lifetime (this tool
|
// `toolUseConfirm.input` is stable for the dialog's lifetime (this tool
|
||||||
// returns `behavior: 'ask'` directly and never goes through the classifier).
|
// returns `behavior: 'ask'` directly and never goes through the classifier).
|
||||||
|
const settings = useSettings()
|
||||||
const result = useMemo(
|
const result = useMemo(
|
||||||
() => AskUserQuestionTool.inputSchema.safeParse(toolUseConfirm.input),
|
() => AskUserQuestionTool.inputSchema.safeParse(toolUseConfirm.input),
|
||||||
[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<string, string> = {}
|
||||||
|
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<string, string> = { ...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(
|
const handleQuestionAnswer = useCallback(
|
||||||
(
|
(
|
||||||
questionText: string,
|
questionText: string,
|
||||||
|
|
@ -541,6 +590,7 @@ Questions asked and answers provided:\n${questionsWithAnswers}`
|
||||||
onSubmit={nextQuestion}
|
onSubmit={nextQuestion}
|
||||||
onTabPrev={handleTabPrev}
|
onTabPrev={handleTabPrev}
|
||||||
onTabNext={handleTabNext}
|
onTabNext={handleTabNext}
|
||||||
|
autoSelectSecondsLeft={autoSelectSecondsLeft}
|
||||||
onRespondToClaude={handleRespondToClaude}
|
onRespondToClaude={handleRespondToClaude}
|
||||||
onFinishPlanInterview={handleFinishPlanInterview}
|
onFinishPlanInterview={handleFinishPlanInterview}
|
||||||
onImagePaste={(base64, mediaType, filename, dims, path) =>
|
onImagePaste={(base64, mediaType, filename, dims, path) =>
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ type Props = {
|
||||||
onSubmit: () => void
|
onSubmit: () => void
|
||||||
onTabPrev?: () => void
|
onTabPrev?: () => void
|
||||||
onTabNext?: () => void
|
onTabNext?: () => void
|
||||||
|
autoSelectSecondsLeft?: number
|
||||||
onRespondToClaude: () => void
|
onRespondToClaude: () => void
|
||||||
onFinishPlanInterview: () => void
|
onFinishPlanInterview: () => void
|
||||||
onImagePaste?: (
|
onImagePaste?: (
|
||||||
|
|
@ -73,6 +74,7 @@ export function QuestionView({
|
||||||
planFilePath,
|
planFilePath,
|
||||||
minContentHeight,
|
minContentHeight,
|
||||||
minContentWidth,
|
minContentWidth,
|
||||||
|
autoSelectSecondsLeft,
|
||||||
onUpdateQuestionState,
|
onUpdateQuestionState,
|
||||||
onAnswer,
|
onAnswer,
|
||||||
onTextInputFocus,
|
onTextInputFocus,
|
||||||
|
|
@ -262,7 +264,14 @@ export function QuestionView({
|
||||||
answers={answers}
|
answers={answers}
|
||||||
hideSubmitTab={hideSubmitTab}
|
hideSubmitTab={hideSubmitTab}
|
||||||
/>
|
/>
|
||||||
<PermissionRequestTitle title={question.question} color={'text'} />
|
<Box flexDirection="row" gap={1} alignItems="flex-start">
|
||||||
|
<PermissionRequestTitle title={question.question} color={'text'} />
|
||||||
|
{autoSelectSecondsLeft !== undefined && (
|
||||||
|
<Text color="inactive" dimColor>
|
||||||
|
({autoSelectSecondsLeft}s)
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
<Box flexDirection="column" minHeight={minContentHeight}>
|
<Box flexDirection="column" minHeight={minContentHeight}>
|
||||||
<Box marginTop={1}>
|
<Box marginTop={1}>
|
||||||
|
|
|
||||||
|
|
@ -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
|
prefersReducedMotion: z
|
||||||
.boolean()
|
.boolean()
|
||||||
.optional()
|
.optional()
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user