fix: restrict auto-accept to plan mode only, block AskUserQuestion prompts

The auto-accept feature was too aggressive - it would press Enter for any
silence without a completion message, including AskUserQuestion prompts.
Now uses the elicitation_dialog notification hook to detect when Claude is
asking a question, and blocks auto-accept in that case. Only plan mode
approvals (silence with no completion message AND no elicitation signal)
trigger auto-accept.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
arkon
2026-01-24 03:10:02 +01:00
co-authored by Claude Opus 4.5
parent b4aea57d9c
commit 10222075cb
9 changed files with 136 additions and 31 deletions
+43 -18
View File
@@ -269,17 +269,20 @@ export interface RespawnConfig {
noOutputTimeoutMs: number;
/**
* Whether to auto-accept prompts (plan mode approvals, question selections).
* When Claude enters plan mode or asks a question, output stops without a completion
* message. This feature detects that state and sends Enter to accept the default option.
* Whether to auto-accept plan mode prompts by pressing Enter.
* When Claude enters plan mode and presents a plan for approval, output stops
* without a completion message. This feature detects that state and sends Enter
* to accept the plan. Does NOT auto-accept AskUserQuestion prompts (those are
* blocked via the elicitation_dialog hook signal).
* @default true
*/
autoAcceptPrompts: boolean;
/**
* Delay before auto-accepting prompts (ms).
* After no output for this duration AND no completion message detected,
* sends Enter to accept the current prompt. Must be shorter than noOutputTimeoutMs.
* Delay before auto-accepting plan mode prompts (ms).
* After no output for this duration AND no completion message detected
* AND no elicitation dialog signaled, sends Enter to accept the plan.
* Must be shorter than noOutputTimeoutMs.
* @default 8000 (8 seconds)
*/
autoAcceptDelayMs: number;
@@ -310,7 +313,7 @@ export interface RespawnEvents {
stepCompleted: (step: string) => void;
/** Detection status update for UI display */
detectionUpdate: (status: DetectionStatus) => void;
/** Auto-accept prompt sent (plan mode, question, etc.) */
/** Auto-accept sent for plan mode approval */
autoAcceptSent: () => void;
/** Error occurred */
error: (error: Error) => void;
@@ -328,7 +331,7 @@ const DEFAULT_CONFIG: RespawnConfig = {
sendInit: true, // send /init after /clear
completionConfirmMs: 5000, // 5 seconds of silence after completion message
noOutputTimeoutMs: 30000, // 30 seconds fallback if no output at all
autoAcceptPrompts: true, // auto-accept plan mode and question prompts
autoAcceptPrompts: true, // auto-accept plan mode prompts (not questions)
autoAcceptDelayMs: 8000, // 8 seconds before auto-accepting
};
@@ -409,12 +412,15 @@ export class RespawnController extends EventEmitter {
/** Timer for periodic detection status updates */
private detectionUpdateTimer: NodeJS.Timeout | null = null;
/** Timer for auto-accepting prompts (plan mode, questions) */
/** Timer for auto-accepting plan mode prompts */
private autoAcceptTimer: NodeJS.Timeout | null = null;
/** Whether any terminal output has been received since start/last-auto-accept */
private hasReceivedOutput: boolean = false;
/** Whether an elicitation dialog (AskUserQuestion) was detected via hook signal */
private elicitationDetected: boolean = false;
/** Number of completed respawn cycles */
private cycleCount: number = 0;
@@ -782,6 +788,7 @@ export class RespawnController extends EventEmitter {
if (isWorking) {
this.workingDetected = true;
this.promptDetected = false;
this.elicitationDetected = false; // Clear on new work cycle
this.lastWorkingPatternTime = now;
this.clearIdleTimer();
@@ -1109,8 +1116,8 @@ export class RespawnController extends EventEmitter {
/**
* Start the auto-accept timer.
* Fires after autoAcceptDelayMs of no output when no completion message was detected.
* This handles plan mode approvals and question prompts by pressing Enter.
* Fires after autoAcceptDelayMs of no output when no completion message
* and no elicitation dialog was detected. Only handles plan mode approvals.
*/
private startAutoAcceptTimer(): void {
if (this.autoAcceptTimer) {
@@ -1134,15 +1141,16 @@ export class RespawnController extends EventEmitter {
}
/**
* Attempt to auto-accept a prompt by sending Enter.
* Attempt to auto-accept a plan mode prompt by sending Enter.
* Only fires when:
* - In 'watching' state (not mid-cycle)
* - No completion message was detected (Claude is waiting for input, not truly idle)
* - No elicitation dialog was detected (not an AskUserQuestion prompt)
* - autoAcceptPrompts is enabled
*
* This handles Claude's plan mode (waiting for approval) and
* AskUserQuestion (waiting for option selection) by pressing Enter
* to accept the default/currently-selected option.
* This handles Claude's plan mode (waiting for approval) by pressing Enter
* to accept the plan. It does NOT auto-accept AskUserQuestion prompts -
* those require explicit user interaction.
*
* @fires autoAcceptSent
*/
@@ -1159,10 +1167,16 @@ export class RespawnController extends EventEmitter {
// Don't auto-accept if we haven't received any output yet (prevents spurious Enter on fresh start)
if (!this.hasReceivedOutput) return;
const msSinceOutput = Date.now() - this.lastOutputTime;
this.log(`Auto-accepting prompt (${msSinceOutput}ms silence, no completion message)`);
// Don't auto-accept if an elicitation dialog (AskUserQuestion) was detected
if (this.elicitationDetected) {
this.log('Skipping auto-accept: elicitation dialog detected (AskUserQuestion)');
return;
}
// Send Enter to accept the current prompt/selection
const msSinceOutput = Date.now() - this.lastOutputTime;
this.log(`Auto-accepting plan mode prompt (${msSinceOutput}ms silence, no completion message, no elicitation)`);
// Send Enter to accept the plan
this.session.writeViaScreen('\r');
this.emit('autoAcceptSent');
@@ -1170,6 +1184,17 @@ export class RespawnController extends EventEmitter {
this.hasReceivedOutput = false;
}
/**
* Signal that an elicitation dialog (AskUserQuestion) was detected via hook.
* This prevents auto-accept from firing, since the user needs to make a selection.
* The flag is cleared when working patterns are detected (new turn starts).
*/
signalElicitation(): void {
this.elicitationDetected = true;
this.cancelAutoAcceptTimer();
this.log('Elicitation dialog signaled - auto-accept blocked until next work cycle');
}
/**
* Start completion confirmation timer.
* After completion message, waits for output silence.