mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-04 06:29:42 +02:00
feat: auto-accept plan mode and question prompts in respawn controller
When Claude enters plan mode or asks a question (AskUserQuestion), output stops without a completion message. The new autoAcceptPrompts feature detects this state and sends Enter after a configurable delay (default 8s) to accept the plan or select the default option, keeping Claude working autonomously. Enabled by default. Adds UI checkbox in the respawn config panel. Safety: only fires once per silence period, requires prior output, and won't fire during active respawn cycles. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
+113
-2
@@ -267,6 +267,22 @@ export interface RespawnConfig {
|
||||
* @default 30000 (30 seconds)
|
||||
*/
|
||||
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.
|
||||
* @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.
|
||||
* @default 8000 (8 seconds)
|
||||
*/
|
||||
autoAcceptDelayMs: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -294,6 +310,8 @@ 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.) */
|
||||
autoAcceptSent: () => void;
|
||||
/** Error occurred */
|
||||
error: (error: Error) => void;
|
||||
/** Debug log message */
|
||||
@@ -310,6 +328,8 @@ 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
|
||||
autoAcceptDelayMs: 8000, // 8 seconds before auto-accepting
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -389,6 +409,12 @@ 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) */
|
||||
private autoAcceptTimer: NodeJS.Timeout | null = null;
|
||||
|
||||
/** Whether any terminal output has been received since start/last-auto-accept */
|
||||
private hasReceivedOutput: boolean = false;
|
||||
|
||||
/** Number of completed respawn cycles */
|
||||
private cycleCount: number = 0;
|
||||
|
||||
@@ -642,11 +668,15 @@ export class RespawnController extends EventEmitter {
|
||||
this.lastTokenChangeTime = now;
|
||||
this.lastWorkingPatternTime = now;
|
||||
this.completionMessageTime = null;
|
||||
this.hasReceivedOutput = false;
|
||||
|
||||
this.setState('watching');
|
||||
this.setupTerminalListener();
|
||||
this.startDetectionUpdates();
|
||||
this.startNoOutputTimer();
|
||||
if (this.config.autoAcceptPrompts) {
|
||||
this.startAutoAcceptTimer();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -736,7 +766,9 @@ export class RespawnController extends EventEmitter {
|
||||
// Track output time (Layer 2)
|
||||
this.lastOutputTime = now;
|
||||
this.lastActivityTime = now;
|
||||
this.hasReceivedOutput = true;
|
||||
this.resetNoOutputTimer();
|
||||
this.resetAutoAcceptTimer();
|
||||
|
||||
// Track token count (Layer 3)
|
||||
const tokenCount = this.extractTokenCount(data);
|
||||
@@ -772,6 +804,7 @@ export class RespawnController extends EventEmitter {
|
||||
if (this.isCompletionMessage(data)) {
|
||||
this.completionMessageTime = now;
|
||||
this.workingDetected = false;
|
||||
this.cancelAutoAcceptTimer(); // Normal idle flow handles this
|
||||
this.log(`Completion message detected: "${data.trim().substring(0, 50)}..."`);
|
||||
|
||||
// In watching state, start completion confirmation timer
|
||||
@@ -975,7 +1008,7 @@ export class RespawnController extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
/** Clear all timers (idle, step, completion confirm, no-output, step confirm, and clear fallback) */
|
||||
/** Clear all timers (idle, step, completion confirm, no-output, step confirm, auto-accept, and clear fallback) */
|
||||
private clearTimers(): void {
|
||||
this.clearIdleTimer();
|
||||
if (this.stepTimer) {
|
||||
@@ -993,7 +1026,10 @@ export class RespawnController extends EventEmitter {
|
||||
if (this.stepConfirmTimer) {
|
||||
clearTimeout(this.stepConfirmTimer);
|
||||
this.stepConfirmTimer = null;
|
||||
|
||||
}
|
||||
if (this.autoAcceptTimer) {
|
||||
clearTimeout(this.autoAcceptTimer);
|
||||
this.autoAcceptTimer = null;
|
||||
}
|
||||
if (this.noOutputTimer) {
|
||||
clearTimeout(this.noOutputTimer);
|
||||
@@ -1059,6 +1095,81 @@ export class RespawnController extends EventEmitter {
|
||||
this.startNoOutputTimer();
|
||||
}
|
||||
|
||||
// ========== Auto-Accept Prompt Methods ==========
|
||||
|
||||
/**
|
||||
* Reset the auto-accept timer.
|
||||
* Called whenever output is received. After autoAcceptDelayMs of silence
|
||||
* (without a completion message), sends Enter to accept prompts.
|
||||
*/
|
||||
private resetAutoAcceptTimer(): void {
|
||||
if (!this.config.autoAcceptPrompts) return;
|
||||
this.startAutoAcceptTimer();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
private startAutoAcceptTimer(): void {
|
||||
if (this.autoAcceptTimer) {
|
||||
clearTimeout(this.autoAcceptTimer);
|
||||
}
|
||||
this.autoAcceptTimer = setTimeout(() => {
|
||||
this.autoAcceptTimer = null;
|
||||
this.tryAutoAccept();
|
||||
}, this.config.autoAcceptDelayMs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel the auto-accept timer.
|
||||
* Called when a completion message is detected (normal idle flow handles it).
|
||||
*/
|
||||
private cancelAutoAcceptTimer(): void {
|
||||
if (this.autoAcceptTimer) {
|
||||
clearTimeout(this.autoAcceptTimer);
|
||||
this.autoAcceptTimer = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to auto-accept a 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)
|
||||
* - 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.
|
||||
*
|
||||
* @fires autoAcceptSent
|
||||
*/
|
||||
private tryAutoAccept(): void {
|
||||
// Only auto-accept in watching state (not during a respawn cycle)
|
||||
if (this._state !== 'watching') return;
|
||||
|
||||
// Don't auto-accept if a completion message was detected (normal idle handles it)
|
||||
if (this.completionMessageTime !== null) return;
|
||||
|
||||
// Don't auto-accept if disabled
|
||||
if (!this.config.autoAcceptPrompts) return;
|
||||
|
||||
// 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)`);
|
||||
|
||||
// Send Enter to accept the current prompt/selection
|
||||
this.session.writeViaScreen('\r');
|
||||
this.emit('autoAcceptSent');
|
||||
|
||||
// Reset so we don't keep spamming Enter if Claude doesn't respond
|
||||
this.hasReceivedOutput = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start completion confirmation timer.
|
||||
* After completion message, waits for output silence.
|
||||
|
||||
@@ -203,6 +203,10 @@ export interface RespawnConfig {
|
||||
completionConfirmMs?: number;
|
||||
/** Fallback timeout when no output received at all (ms) */
|
||||
noOutputTimeoutMs?: number;
|
||||
/** Whether to auto-accept prompts (plan mode approvals, question selections) by pressing Enter */
|
||||
autoAcceptPrompts?: boolean;
|
||||
/** Delay before auto-accepting prompts when no output and no completion message (ms) */
|
||||
autoAcceptDelayMs?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -484,6 +488,10 @@ export interface PersistedRespawnConfig {
|
||||
sendInit: boolean;
|
||||
/** Optional prompt to send if /init doesn't trigger work */
|
||||
kickstartPrompt?: string;
|
||||
/** Whether to auto-accept prompts (plan mode, questions) by pressing Enter */
|
||||
autoAcceptPrompts?: boolean;
|
||||
/** Delay before auto-accepting prompts (ms) */
|
||||
autoAcceptDelayMs?: number;
|
||||
/** Duration in minutes if timed respawn was set */
|
||||
durationMinutes?: number;
|
||||
}
|
||||
|
||||
@@ -503,6 +503,13 @@ class ClaudemanApp {
|
||||
}
|
||||
});
|
||||
|
||||
this.eventSource.addEventListener('respawn:autoAcceptSent', (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
if (data.sessionId === this.activeSessionId) {
|
||||
document.getElementById('respawnStep').textContent = '⏎ Auto-accepted prompt';
|
||||
}
|
||||
});
|
||||
|
||||
this.eventSource.addEventListener('respawn:detectionUpdate', (e) => {
|
||||
const data = JSON.parse(e.data);
|
||||
if (this.respawnStatus[data.sessionId]) {
|
||||
@@ -1667,6 +1674,7 @@ class ClaudemanApp {
|
||||
sendClear: document.getElementById('modalRespawnSendClear').checked,
|
||||
sendInit: document.getElementById('modalRespawnSendInit').checked,
|
||||
kickstartPrompt: document.getElementById('modalRespawnKickstart').value.trim() || undefined,
|
||||
autoAcceptPrompts: document.getElementById('modalRespawnAutoAccept').checked,
|
||||
};
|
||||
try {
|
||||
await fetch(`/api/sessions/${this.editingSessionId}/respawn/config`, {
|
||||
@@ -1689,6 +1697,7 @@ class ClaudemanApp {
|
||||
document.getElementById('modalRespawnSendClear').checked = c.sendClear ?? true;
|
||||
document.getElementById('modalRespawnSendInit').checked = c.sendInit ?? true;
|
||||
document.getElementById('modalRespawnKickstart').value = c.kickstartPrompt || '';
|
||||
document.getElementById('modalRespawnAutoAccept').checked = c.autoAcceptPrompts ?? true;
|
||||
// Restore duration if set
|
||||
if (c.durationMinutes) {
|
||||
const presetBtn = document.querySelector(`.duration-preset-btn[data-minutes="${c.durationMinutes}"]`);
|
||||
@@ -1749,6 +1758,7 @@ class ClaudemanApp {
|
||||
const sendClear = document.getElementById('modalRespawnSendClear').checked;
|
||||
const sendInit = document.getElementById('modalRespawnSendInit').checked;
|
||||
const kickstartPrompt = document.getElementById('modalRespawnKickstart').value.trim() || undefined;
|
||||
const autoAcceptPrompts = document.getElementById('modalRespawnAutoAccept').checked;
|
||||
const durationMinutes = this.getSelectedDuration();
|
||||
|
||||
// Auto-compact settings
|
||||
@@ -1766,6 +1776,7 @@ class ClaudemanApp {
|
||||
sendClear,
|
||||
sendInit,
|
||||
kickstartPrompt,
|
||||
autoAcceptPrompts,
|
||||
},
|
||||
durationMinutes,
|
||||
autoCompactEnabled,
|
||||
|
||||
@@ -333,6 +333,17 @@
|
||||
<span class="form-hint">Sent only when /init completes but Claude stays idle</span>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label>Auto-Accept Prompts</label>
|
||||
<div class="checkbox-group-inline">
|
||||
<label class="checkbox-inline">
|
||||
<input type="checkbox" id="modalRespawnAutoAccept" checked onchange="app.autoSaveRespawnConfig()">
|
||||
<span>Accept plan mode & questions</span>
|
||||
</label>
|
||||
</div>
|
||||
<span class="form-hint">Presses Enter to accept plan approvals and default question options when Claude is waiting</span>
|
||||
</div>
|
||||
|
||||
<div class="form-row">
|
||||
<label>Auto-Compact Context</label>
|
||||
<div class="auto-clear-row">
|
||||
|
||||
@@ -690,6 +690,8 @@ export class WebServer extends EventEmitter {
|
||||
sendClear: config.sendClear ?? currentConfig?.sendClear ?? true,
|
||||
sendInit: config.sendInit ?? currentConfig?.sendInit ?? true,
|
||||
kickstartPrompt: config.kickstartPrompt ?? currentConfig?.kickstartPrompt,
|
||||
autoAcceptPrompts: config.autoAcceptPrompts ?? currentConfig?.autoAcceptPrompts ?? true,
|
||||
autoAcceptDelayMs: config.autoAcceptDelayMs ?? currentConfig?.autoAcceptDelayMs ?? 8000,
|
||||
durationMinutes: currentConfig?.durationMinutes,
|
||||
};
|
||||
this.screenManager.updateRespawnConfig(id, merged);
|
||||
@@ -1306,6 +1308,8 @@ export class WebServer extends EventEmitter {
|
||||
sendClear: config.sendClear,
|
||||
sendInit: config.sendInit,
|
||||
kickstartPrompt: config.kickstartPrompt,
|
||||
autoAcceptPrompts: config.autoAcceptPrompts,
|
||||
autoAcceptDelayMs: config.autoAcceptDelayMs,
|
||||
durationMinutes,
|
||||
};
|
||||
this.screenManager.updateRespawnConfig(sessionId, persistedConfig);
|
||||
@@ -1537,6 +1541,10 @@ export class WebServer extends EventEmitter {
|
||||
this.broadcast('respawn:detectionUpdate', { sessionId, detection });
|
||||
});
|
||||
|
||||
controller.on('autoAcceptSent', () => {
|
||||
this.broadcast('respawn:autoAcceptSent', { sessionId });
|
||||
});
|
||||
|
||||
controller.on('log', (message: string) => {
|
||||
this.broadcast('respawn:log', { sessionId, message });
|
||||
});
|
||||
@@ -2008,6 +2016,8 @@ export class WebServer extends EventEmitter {
|
||||
kickstartPrompt: savedState.respawnConfig.kickstartPrompt,
|
||||
completionConfirmMs: savedState.respawnConfig.completionConfirmMs,
|
||||
noOutputTimeoutMs: savedState.respawnConfig.noOutputTimeoutMs,
|
||||
autoAcceptPrompts: savedState.respawnConfig.autoAcceptPrompts ?? true,
|
||||
autoAcceptDelayMs: savedState.respawnConfig.autoAcceptDelayMs ?? 8000,
|
||||
});
|
||||
this.respawnControllers.set(session.id, controller);
|
||||
this.setupRespawnListeners(session.id, controller);
|
||||
@@ -2035,6 +2045,8 @@ export class WebServer extends EventEmitter {
|
||||
sendClear: screen.respawnConfig.sendClear,
|
||||
sendInit: screen.respawnConfig.sendInit,
|
||||
kickstartPrompt: screen.respawnConfig.kickstartPrompt,
|
||||
autoAcceptPrompts: screen.respawnConfig.autoAcceptPrompts ?? true,
|
||||
autoAcceptDelayMs: screen.respawnConfig.autoAcceptDelayMs ?? 8000,
|
||||
});
|
||||
this.respawnControllers.set(session.id, controller);
|
||||
this.setupRespawnListeners(session.id, controller);
|
||||
|
||||
@@ -857,4 +857,203 @@ describe('RespawnController Edge Cases', () => {
|
||||
expect(status.timeSinceActivity).toBeLessThan(100);
|
||||
controller.stop();
|
||||
});
|
||||
|
||||
describe('Auto-Accept Prompts', () => {
|
||||
it('should have autoAcceptPrompts enabled by default', () => {
|
||||
const defaultController = new RespawnController(session as unknown as Session);
|
||||
const config = defaultController.getConfig();
|
||||
expect(config.autoAcceptPrompts).toBe(true);
|
||||
expect(config.autoAcceptDelayMs).toBe(8000);
|
||||
defaultController.stop();
|
||||
});
|
||||
|
||||
it('should send Enter after silence without completion message', async () => {
|
||||
const autoAcceptController = new RespawnController(session as unknown as Session, {
|
||||
autoAcceptPrompts: true,
|
||||
autoAcceptDelayMs: 100, // Short delay for testing
|
||||
completionConfirmMs: 50,
|
||||
noOutputTimeoutMs: 5000,
|
||||
});
|
||||
|
||||
let autoAcceptFired = false;
|
||||
autoAcceptController.on('autoAcceptSent', () => {
|
||||
autoAcceptFired = true;
|
||||
});
|
||||
|
||||
autoAcceptController.start();
|
||||
|
||||
// Simulate output (e.g., plan content), then silence
|
||||
session.simulateTerminalOutput('Plan: Here is my implementation plan...');
|
||||
|
||||
// Wait for autoAcceptDelayMs to expire
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
|
||||
expect(autoAcceptFired).toBe(true);
|
||||
expect(session.writeBuffer).toContain('\r');
|
||||
autoAcceptController.stop();
|
||||
});
|
||||
|
||||
it('should NOT send Enter when completion message was detected', async () => {
|
||||
const autoAcceptController = new RespawnController(session as unknown as Session, {
|
||||
autoAcceptPrompts: true,
|
||||
autoAcceptDelayMs: 100,
|
||||
completionConfirmMs: 200, // Longer than autoAcceptDelay
|
||||
noOutputTimeoutMs: 5000,
|
||||
});
|
||||
|
||||
let autoAcceptFired = false;
|
||||
autoAcceptController.on('autoAcceptSent', () => {
|
||||
autoAcceptFired = true;
|
||||
});
|
||||
|
||||
autoAcceptController.start();
|
||||
|
||||
// Simulate completion message - normal idle flow should handle this
|
||||
session.simulateCompletionMessage();
|
||||
|
||||
// Wait for autoAcceptDelayMs
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
|
||||
expect(autoAcceptFired).toBe(false);
|
||||
autoAcceptController.stop();
|
||||
});
|
||||
|
||||
it('should NOT send Enter when disabled', async () => {
|
||||
const autoAcceptController = new RespawnController(session as unknown as Session, {
|
||||
autoAcceptPrompts: false,
|
||||
autoAcceptDelayMs: 100,
|
||||
completionConfirmMs: 50,
|
||||
noOutputTimeoutMs: 5000,
|
||||
});
|
||||
|
||||
let autoAcceptFired = false;
|
||||
autoAcceptController.on('autoAcceptSent', () => {
|
||||
autoAcceptFired = true;
|
||||
});
|
||||
|
||||
autoAcceptController.start();
|
||||
session.simulateTerminalOutput('Plan: Waiting for approval...');
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
|
||||
expect(autoAcceptFired).toBe(false);
|
||||
autoAcceptController.stop();
|
||||
});
|
||||
|
||||
it('should NOT send Enter before any output is received', async () => {
|
||||
const autoAcceptController = new RespawnController(session as unknown as Session, {
|
||||
autoAcceptPrompts: true,
|
||||
autoAcceptDelayMs: 100,
|
||||
completionConfirmMs: 50,
|
||||
noOutputTimeoutMs: 5000,
|
||||
});
|
||||
|
||||
let autoAcceptFired = false;
|
||||
autoAcceptController.on('autoAcceptSent', () => {
|
||||
autoAcceptFired = true;
|
||||
});
|
||||
|
||||
autoAcceptController.start();
|
||||
|
||||
// Don't simulate any output - just wait
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
|
||||
expect(autoAcceptFired).toBe(false);
|
||||
autoAcceptController.stop();
|
||||
});
|
||||
|
||||
it('should reset timer when new output arrives', async () => {
|
||||
const autoAcceptController = new RespawnController(session as unknown as Session, {
|
||||
autoAcceptPrompts: true,
|
||||
autoAcceptDelayMs: 150,
|
||||
completionConfirmMs: 50,
|
||||
noOutputTimeoutMs: 5000,
|
||||
});
|
||||
|
||||
let autoAcceptFired = false;
|
||||
autoAcceptController.on('autoAcceptSent', () => {
|
||||
autoAcceptFired = true;
|
||||
});
|
||||
|
||||
autoAcceptController.start();
|
||||
session.simulateTerminalOutput('First output');
|
||||
|
||||
// Wait 100ms (less than 150ms delay), then send more output
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
session.simulateTerminalOutput('More output');
|
||||
|
||||
// Wait another 100ms - total 200ms from start but only 100ms from last output
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
expect(autoAcceptFired).toBe(false);
|
||||
|
||||
// Wait the remaining time
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
expect(autoAcceptFired).toBe(true);
|
||||
autoAcceptController.stop();
|
||||
});
|
||||
|
||||
it('should only send Enter once per silence period', async () => {
|
||||
const autoAcceptController = new RespawnController(session as unknown as Session, {
|
||||
autoAcceptPrompts: true,
|
||||
autoAcceptDelayMs: 100,
|
||||
completionConfirmMs: 50,
|
||||
noOutputTimeoutMs: 5000,
|
||||
});
|
||||
|
||||
let autoAcceptCount = 0;
|
||||
autoAcceptController.on('autoAcceptSent', () => {
|
||||
autoAcceptCount++;
|
||||
});
|
||||
|
||||
autoAcceptController.start();
|
||||
session.simulateTerminalOutput('Plan waiting...');
|
||||
|
||||
// Wait for first auto-accept
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
expect(autoAcceptCount).toBe(1);
|
||||
|
||||
// Wait more - should NOT fire again (hasReceivedOutput is false)
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
expect(autoAcceptCount).toBe(1);
|
||||
|
||||
// New output comes in, then silence again - should fire again
|
||||
session.simulateTerminalOutput('Another question...');
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
expect(autoAcceptCount).toBe(2);
|
||||
|
||||
autoAcceptController.stop();
|
||||
});
|
||||
|
||||
it('should NOT auto-accept during respawn cycle (non-watching state)', async () => {
|
||||
const autoAcceptController = new RespawnController(session as unknown as Session, {
|
||||
autoAcceptPrompts: true,
|
||||
autoAcceptDelayMs: 50,
|
||||
completionConfirmMs: 50,
|
||||
interStepDelayMs: 50,
|
||||
noOutputTimeoutMs: 5000,
|
||||
});
|
||||
|
||||
let autoAcceptFired = false;
|
||||
autoAcceptController.on('autoAcceptSent', () => {
|
||||
autoAcceptFired = true;
|
||||
});
|
||||
|
||||
autoAcceptController.start();
|
||||
|
||||
// Trigger a respawn cycle via completion message
|
||||
session.simulateCompletionMessage();
|
||||
await new Promise(resolve => setTimeout(resolve, 150));
|
||||
|
||||
// Now in sending_update or waiting_update state
|
||||
expect(autoAcceptController.state).not.toBe('watching');
|
||||
|
||||
// Simulate output in the waiting state, then silence
|
||||
session.simulateTerminalOutput('Processing update...');
|
||||
await new Promise(resolve => setTimeout(resolve, 150));
|
||||
|
||||
// Auto-accept should NOT fire because we're not in watching state
|
||||
expect(autoAcceptFired).toBe(false);
|
||||
autoAcceptController.stop();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user