Picks the recommended option of a question that waited unanswered for a set time, so a session you left does not stop. Off until you turn it on.

You start a long task, walk away, and come back an hour later to find the session stuck on a question the model asked in the first minute. This mod keeps that from happening: when a question has waited unanswered for a set time, it picks the option the model itself recommended and lets the work go on. It is off until you turn it on.
AskUserQuestion call. The question shows up as usual and waits for you.The tool tells the model to put the option it recommends first and to end its label with (Recommended), and in a question written in another language the model writes that word in that language. So the mod takes the first option when its label ends with that word in parentheses and no other option carries it. It knows the word in English, Turkish, German, Spanish, Portuguese, French, Italian, Dutch, Polish, Russian, Chinese, Japanese and Korean, and full-width parentheses count too.
ask-autopick: no answer in 10 min, picked the recommended option: Renk? → Mavi (Önerilen)
multiSelect). Those wait for you, and one yellow entry tells you so.In the live check on 2.1.282, a question left open got Mavi (Önerilen) after 1 minute and after 5 minutes, and the model carried on with it.
/ask-autopick on or off, and the wait /ask-autopick on | off off by default, kept across sessions /ask-autopick 20 wait 20 minutes; 1 to 120, 10 by default, kept across sessions
claude plugin marketplace add KilimcininKorOglu/claude-code-mods claude plugin install ask-autopick@kilimcininkoroglu-mods
Function hooks are early access. Claude Code 2.1.288 and later load them by default, so there is nothing to switch on.
/ask-autopick on. Until then it answers nothing.Validated with claude plugin validate on Claude Code 2.1.283:
❯ ./register.ts hooks: session.start, command.run{command=ask-autopick}, tool.call{tool=/"^AskUserQuestion$"/} ❯ ./register.ts calls: $.clock.after (via answerOrPick), $.command.register, $.sidebar.set (via toPerson), $.store.get (via readSettings), $.store.set (via runCommand), $.ui.log (via toPerson)
Reach L2: it drives Claude, because a pick answers a question for you.
make install # eslint, typescript-eslint, typescript make lint # complexity limit 10, the build fails above it make typecheck # needs .claude/types/ from /plugin-types make validate make test # claude plugin test
hooks/register.ts 103 lines1import type { EngineInterface, Register, ToolCallResult } from 'claude-code'
2import { DEFAULT_MINUTES, minutesOf, pickedLine, pickedNote, picksOf, statusText, USAGE, waitsLine, type Line, type Question } from './pick.ts'
3
4const ENABLED_KEY = 'enabled'
5const MINUTES_KEY = 'minutes'
6
7/** Off until the person turns it on, and the wait in minutes. */
8type State = { enabled: boolean; minutes: number }
9
10/**
11 * Reads the on/off setting and the wait from the store, which every window shares, so a change made in
12 * another window applies here at the next hook that acts on it.
13 */
14async function readSettings($: EngineInterface, state: State): Promise<void> {
15 // Only a stored true turns the mod on, so a fresh install answers nothing for the person.
16 state.enabled = (await $.store.get(ENABLED_KEY)) === true
17 const stored = await $.store.get(MINUTES_KEY)
18 state.minutes = typeof stored === 'number' && minutesOf(String(stored)) !== undefined ? stored : DEFAULT_MINUTES
19}
20
21/** What the person reads: an entry in the shared sidebar's stream while it is open, else one transcript line. */
22async function toPerson($: EngineInterface, key: string, line: Line): Promise<void> {
23 try {
24 if (await $.sidebar.set({ consumer: 'ask-autopick', key, title: 'question answered for you', lines: [line], until: 'stream' })) return
25 } catch {
26 // The sidebar mod is not installed.
27 }
28 $.ui.log(line.text)
29}
30
31/**
32 * The person's answer, or the recommended options once the wait ran out. The dialog waits in `pending`; a
33 * pick answers the call in its place, and the engine closes the dialog and settles `pending` as rejected
34 * (measured on 2.1.282).
35 */
36async function answerOrPick($: EngineInterface, state: State, questions: Question[], answers: Record<string, string>, pending: Promise<ToolCallResult>): Promise<ToolCallResult> {
37 const minutes = state.minutes
38 let timer: { cancel: () => void } | undefined
39 const expired = new Promise<'expired'>(resolve => {
40 timer = $.clock.after(minutes * 60_000, () => resolve('expired'))
41 })
42 const first = await Promise.race([pending, expired])
43 if (first !== 'expired') {
44 timer?.cancel()
45 return first
46 }
47 // Turned off in another window during the wait: the question stays for the person.
48 await readSettings($, state)
49 if (!state.enabled) return pending
50 // The abandoned dialog settles on its own; its result is not the call's any more.
51 pending.catch(() => undefined)
52 await toPerson($, 'picked', pickedLine(minutes, answers))
53 return { result: { questions, answers, annotations: {} }, context: [pickedNote(minutes)] } as never
54}
55
56async function runCommand($: EngineInterface, state: State, args: string): Promise<string> {
57 const arg = args.trim()
58 // The status after a change also shows the other setting as the store holds it.
59 await readSettings($, state)
60 if (arg === 'on' || arg === 'off') {
61 state.enabled = arg === 'on'
62 await $.store.set(ENABLED_KEY, state.enabled)
63 } else if (arg !== '') {
64 const minutes = minutesOf(arg)
65 if (minutes === undefined) return USAGE
66 state.minutes = minutes
67 await $.store.set(MINUTES_KEY, minutes)
68 }
69 return statusText(state.enabled, state.minutes)
70}
71
72export const register: Register = on => {
73 const state: State = { enabled: false, minutes: DEFAULT_MINUTES }
74
75 on('session.start', async ($, e, next) => {
76 const r = await next(e)
77 await readSettings($, state)
78 await $.command.register({
79 name: 'ask-autopick',
80 description: 'Pick the recommended option of a question left unanswered: status, on, off, <minutes> (ask-autopick)',
81 argumentHint: '[on | off | <minutes>]',
82 immediate: true,
83 })
84 return r
85 })
86
87 // The engine prints the plugin name in front of command text and log lines, so the texts do not repeat it.
88 on('command.run', { command: 'ask-autopick' }, async ($, e) => ({ text: await runCommand($, state, String(e.args ?? '')) }))
89
90 // A RegExp literal, because a headless /plugin-types lists no AskUserQuestion, so a string matcher does not type.
91 on('tool.call', { tool: /^AskUserQuestion$/ }, async ($, e, next) => {
92 await readSettings($, state)
93 if (!state.enabled) return next(e)
94 const questions = (e as unknown as { questions?: Question[] }).questions ?? []
95 const answers = picksOf(questions)
96 if (answers === undefined) {
97 await toPerson($, 'waits', waitsLine(state.minutes))
98 return next(e)
99 }
100 return answerOrPick($, state, questions, answers, next(e))
101 })
102}
103hooks/pick.ts 111 lines1/** Which option a question that waited too long gets, and the texts this mod writes. */
2
3/** One question of an AskUserQuestion call, as far as this mod reads it. */
4export type Question = { question: string; multiSelect?: boolean; options?: { label: string }[] }
5
6/** The wait in minutes before a pick, when nothing is stored. */
7export const DEFAULT_MINUTES = 10
8
9/** The band `/ask-autopick <minutes>` takes; a value outside it is refused, never clamped. */
10const MIN_MINUTES = 1
11const MAX_MINUTES = 120
12
13/**
14 * The words a model writes in parentheses after the option it recommends. The tool tells it to write
15 * `(Recommended)`, and in a question written in another language it writes that word in the language.
16 */
17const RECOMMENDED_WORDS = [
18 'recommended', 'önerilen', 'empfohlen', 'recomendado', 'recomendada', 'recommandé', 'recommandée',
19 'consigliato', 'consigliata', 'aanbevolen', 'zalecane', 'рекомендуется', '推荐', '推薦', '推奨', 'おすすめ', '권장', '추천',
20]
21
22/** A label that ends with one of those words in parentheses, ASCII or full-width. */
23const RECOMMENDED_END = new RegExp(`[((]\\s*(?:${RECOMMENDED_WORDS.join('|')})\\s*[))]\\s*$`, 'iu')
24
25export const USAGE = 'expects nothing (the status), on, off, or a number of minutes from 1 to 120'
26
27export function isRecommended(label: string): boolean {
28 return RECOMMENDED_END.test(label)
29}
30
31/**
32 * The recommended label of one question: the first option, marked at the end of its label, with no other
33 * option marked, because the tool tells the model to put the option it recommends first. Undefined for a
34 * question that takes several answers or has no such option.
35 */
36function pickOf(q: Question): string | undefined {
37 const [first, ...rest] = q.options ?? []
38 if (q.multiSelect === true || first === undefined || !isRecommended(first.label)) return undefined
39 return rest.some(o => isRecommended(o.label)) ? undefined : first.label
40}
41
42/**
43 * The answers a pick gives, one recommended label per question, or undefined when a question has none:
44 * such a question keeps waiting on the person.
45 */
46export function picksOf(questions: readonly Question[]): Record<string, string> | undefined {
47 const answers: Record<string, string> = {}
48 for (const q of questions) {
49 const label = pickOf(q)
50 if (label === undefined) return undefined
51 answers[q.question] = label
52 }
53 return answers
54}
55
56/** The minutes a `/ask-autopick <word>` argument names, or undefined when it names none. */
57export function minutesOf(arg: string): number | undefined {
58 if (!/^\d{1,3}$/.test(arg)) return undefined
59 const n = Number(arg)
60 return n >= MIN_MINUTES && n <= MAX_MINUTES ? n : undefined
61}
62
63/** How the sidebar colours a line or a part of one. */
64type Tone = 'ok' | 'warn' | 'error' | 'dim'
65export type Part = { text: string; kind?: Tone }
66/** A sidebar line; `parts` colour pieces of it, and `text` holds the whole line for a sidebar that draws no parts. */
67export type Line = { text: string; kind?: Tone; parts?: Part[] }
68
69/** A line made of parts, its `text` their texts joined. */
70const partsLine = (parts: Part[]): Line => ({ text: parts.map(p => p.text).join(''), parts })
71
72/**
73 * The pick as a sidebar line: each picked answer yellow, the questions and the rest faint. The line is
74 * yellow as a whole for a sidebar that draws no parts.
75 */
76export function pickedLine(minutes: number, answers: Record<string, string>): Line {
77 const each = Object.entries(answers).flatMap(([q, a], i): Part[] => [
78 ...(i === 0 ? [] : [{ text: '; ', kind: 'dim' as const }]),
79 { text: `${q} → `, kind: 'dim' },
80 { text: a, kind: 'warn' },
81 ])
82 return { ...partsLine([{ text: `no answer in ${minutes} min, picked the recommended option: `, kind: 'dim' }, ...each]), kind: 'warn' }
83}
84
85/** The pick as the person reads it: each question with the option it got. */
86export function pickedLog(minutes: number, answers: Record<string, string>): string {
87 return pickedLine(minutes, answers).text
88}
89
90/** The line the person reads when a question has nothing to pick. */
91export function waitsLog(minutes: number): string {
92 return `a question has no recommended first option or takes several answers, so it waits for you past ${minutes} min`
93}
94
95/** The waiting question as a sidebar line: yellow, because nothing failed and the person is asked. */
96export function waitsLine(minutes: number): Line {
97 return { text: waitsLog(minutes), kind: 'warn' }
98}
99
100/** The note the model reads after a picked answer, so it does not read the pick as the person's choice. */
101export function pickedNote(minutes: number): string {
102 return `ask-autopick: the person did not answer within ${minutes} minutes, so the recommended option was picked for them. Treat it as a default, not as their decision, and name the pick in your next reply.`
103}
104
105/** The `/ask-autopick` status. */
106export function statusText(enabled: boolean, minutes: number): string {
107 return enabled
108 ? `on · a question unanswered for ${minutes} min gets its recommended option`
109 : `off · questions wait for you; /ask-autopick on picks the recommended option after ${minutes} min`
110}
111