mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-10 09:19: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)
|
* @default 30000 (30 seconds)
|
||||||
*/
|
*/
|
||||||
noOutputTimeoutMs: number;
|
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;
|
stepCompleted: (step: string) => void;
|
||||||
/** Detection status update for UI display */
|
/** Detection status update for UI display */
|
||||||
detectionUpdate: (status: DetectionStatus) => void;
|
detectionUpdate: (status: DetectionStatus) => void;
|
||||||
|
/** Auto-accept prompt sent (plan mode, question, etc.) */
|
||||||
|
autoAcceptSent: () => void;
|
||||||
/** Error occurred */
|
/** Error occurred */
|
||||||
error: (error: Error) => void;
|
error: (error: Error) => void;
|
||||||
/** Debug log message */
|
/** Debug log message */
|
||||||
@@ -310,6 +328,8 @@ const DEFAULT_CONFIG: RespawnConfig = {
|
|||||||
sendInit: true, // send /init after /clear
|
sendInit: true, // send /init after /clear
|
||||||
completionConfirmMs: 5000, // 5 seconds of silence after completion message
|
completionConfirmMs: 5000, // 5 seconds of silence after completion message
|
||||||
noOutputTimeoutMs: 30000, // 30 seconds fallback if no output at all
|
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 */
|
/** Timer for periodic detection status updates */
|
||||||
private detectionUpdateTimer: NodeJS.Timeout | null = null;
|
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 */
|
/** Number of completed respawn cycles */
|
||||||
private cycleCount: number = 0;
|
private cycleCount: number = 0;
|
||||||
|
|
||||||
@@ -642,11 +668,15 @@ export class RespawnController extends EventEmitter {
|
|||||||
this.lastTokenChangeTime = now;
|
this.lastTokenChangeTime = now;
|
||||||
this.lastWorkingPatternTime = now;
|
this.lastWorkingPatternTime = now;
|
||||||
this.completionMessageTime = null;
|
this.completionMessageTime = null;
|
||||||
|
this.hasReceivedOutput = false;
|
||||||
|
|
||||||
this.setState('watching');
|
this.setState('watching');
|
||||||
this.setupTerminalListener();
|
this.setupTerminalListener();
|
||||||
this.startDetectionUpdates();
|
this.startDetectionUpdates();
|
||||||
this.startNoOutputTimer();
|
this.startNoOutputTimer();
|
||||||
|
if (this.config.autoAcceptPrompts) {
|
||||||
|
this.startAutoAcceptTimer();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -736,7 +766,9 @@ export class RespawnController extends EventEmitter {
|
|||||||
// Track output time (Layer 2)
|
// Track output time (Layer 2)
|
||||||
this.lastOutputTime = now;
|
this.lastOutputTime = now;
|
||||||
this.lastActivityTime = now;
|
this.lastActivityTime = now;
|
||||||
|
this.hasReceivedOutput = true;
|
||||||
this.resetNoOutputTimer();
|
this.resetNoOutputTimer();
|
||||||
|
this.resetAutoAcceptTimer();
|
||||||
|
|
||||||
// Track token count (Layer 3)
|
// Track token count (Layer 3)
|
||||||
const tokenCount = this.extractTokenCount(data);
|
const tokenCount = this.extractTokenCount(data);
|
||||||
@@ -772,6 +804,7 @@ export class RespawnController extends EventEmitter {
|
|||||||
if (this.isCompletionMessage(data)) {
|
if (this.isCompletionMessage(data)) {
|
||||||
this.completionMessageTime = now;
|
this.completionMessageTime = now;
|
||||||
this.workingDetected = false;
|
this.workingDetected = false;
|
||||||
|
this.cancelAutoAcceptTimer(); // Normal idle flow handles this
|
||||||
this.log(`Completion message detected: "${data.trim().substring(0, 50)}..."`);
|
this.log(`Completion message detected: "${data.trim().substring(0, 50)}..."`);
|
||||||
|
|
||||||
// In watching state, start completion confirmation timer
|
// 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 {
|
private clearTimers(): void {
|
||||||
this.clearIdleTimer();
|
this.clearIdleTimer();
|
||||||
if (this.stepTimer) {
|
if (this.stepTimer) {
|
||||||
@@ -993,7 +1026,10 @@ export class RespawnController extends EventEmitter {
|
|||||||
if (this.stepConfirmTimer) {
|
if (this.stepConfirmTimer) {
|
||||||
clearTimeout(this.stepConfirmTimer);
|
clearTimeout(this.stepConfirmTimer);
|
||||||
this.stepConfirmTimer = null;
|
this.stepConfirmTimer = null;
|
||||||
|
}
|
||||||
|
if (this.autoAcceptTimer) {
|
||||||
|
clearTimeout(this.autoAcceptTimer);
|
||||||
|
this.autoAcceptTimer = null;
|
||||||
}
|
}
|
||||||
if (this.noOutputTimer) {
|
if (this.noOutputTimer) {
|
||||||
clearTimeout(this.noOutputTimer);
|
clearTimeout(this.noOutputTimer);
|
||||||
@@ -1059,6 +1095,81 @@ export class RespawnController extends EventEmitter {
|
|||||||
this.startNoOutputTimer();
|
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.
|
* Start completion confirmation timer.
|
||||||
* After completion message, waits for output silence.
|
* After completion message, waits for output silence.
|
||||||
|
|||||||
@@ -203,6 +203,10 @@ export interface RespawnConfig {
|
|||||||
completionConfirmMs?: number;
|
completionConfirmMs?: number;
|
||||||
/** Fallback timeout when no output received at all (ms) */
|
/** Fallback timeout when no output received at all (ms) */
|
||||||
noOutputTimeoutMs?: number;
|
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;
|
sendInit: boolean;
|
||||||
/** Optional prompt to send if /init doesn't trigger work */
|
/** Optional prompt to send if /init doesn't trigger work */
|
||||||
kickstartPrompt?: string;
|
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 */
|
/** Duration in minutes if timed respawn was set */
|
||||||
durationMinutes?: number;
|
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) => {
|
this.eventSource.addEventListener('respawn:detectionUpdate', (e) => {
|
||||||
const data = JSON.parse(e.data);
|
const data = JSON.parse(e.data);
|
||||||
if (this.respawnStatus[data.sessionId]) {
|
if (this.respawnStatus[data.sessionId]) {
|
||||||
@@ -1667,6 +1674,7 @@ class ClaudemanApp {
|
|||||||
sendClear: document.getElementById('modalRespawnSendClear').checked,
|
sendClear: document.getElementById('modalRespawnSendClear').checked,
|
||||||
sendInit: document.getElementById('modalRespawnSendInit').checked,
|
sendInit: document.getElementById('modalRespawnSendInit').checked,
|
||||||
kickstartPrompt: document.getElementById('modalRespawnKickstart').value.trim() || undefined,
|
kickstartPrompt: document.getElementById('modalRespawnKickstart').value.trim() || undefined,
|
||||||
|
autoAcceptPrompts: document.getElementById('modalRespawnAutoAccept').checked,
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
await fetch(`/api/sessions/${this.editingSessionId}/respawn/config`, {
|
await fetch(`/api/sessions/${this.editingSessionId}/respawn/config`, {
|
||||||
@@ -1689,6 +1697,7 @@ class ClaudemanApp {
|
|||||||
document.getElementById('modalRespawnSendClear').checked = c.sendClear ?? true;
|
document.getElementById('modalRespawnSendClear').checked = c.sendClear ?? true;
|
||||||
document.getElementById('modalRespawnSendInit').checked = c.sendInit ?? true;
|
document.getElementById('modalRespawnSendInit').checked = c.sendInit ?? true;
|
||||||
document.getElementById('modalRespawnKickstart').value = c.kickstartPrompt || '';
|
document.getElementById('modalRespawnKickstart').value = c.kickstartPrompt || '';
|
||||||
|
document.getElementById('modalRespawnAutoAccept').checked = c.autoAcceptPrompts ?? true;
|
||||||
// Restore duration if set
|
// Restore duration if set
|
||||||
if (c.durationMinutes) {
|
if (c.durationMinutes) {
|
||||||
const presetBtn = document.querySelector(`.duration-preset-btn[data-minutes="${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 sendClear = document.getElementById('modalRespawnSendClear').checked;
|
||||||
const sendInit = document.getElementById('modalRespawnSendInit').checked;
|
const sendInit = document.getElementById('modalRespawnSendInit').checked;
|
||||||
const kickstartPrompt = document.getElementById('modalRespawnKickstart').value.trim() || undefined;
|
const kickstartPrompt = document.getElementById('modalRespawnKickstart').value.trim() || undefined;
|
||||||
|
const autoAcceptPrompts = document.getElementById('modalRespawnAutoAccept').checked;
|
||||||
const durationMinutes = this.getSelectedDuration();
|
const durationMinutes = this.getSelectedDuration();
|
||||||
|
|
||||||
// Auto-compact settings
|
// Auto-compact settings
|
||||||
@@ -1766,6 +1776,7 @@ class ClaudemanApp {
|
|||||||
sendClear,
|
sendClear,
|
||||||
sendInit,
|
sendInit,
|
||||||
kickstartPrompt,
|
kickstartPrompt,
|
||||||
|
autoAcceptPrompts,
|
||||||
},
|
},
|
||||||
durationMinutes,
|
durationMinutes,
|
||||||
autoCompactEnabled,
|
autoCompactEnabled,
|
||||||
|
|||||||
@@ -333,6 +333,17 @@
|
|||||||
<span class="form-hint">Sent only when /init completes but Claude stays idle</span>
|
<span class="form-hint">Sent only when /init completes but Claude stays idle</span>
|
||||||
</div>
|
</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">
|
<div class="form-row">
|
||||||
<label>Auto-Compact Context</label>
|
<label>Auto-Compact Context</label>
|
||||||
<div class="auto-clear-row">
|
<div class="auto-clear-row">
|
||||||
|
|||||||
@@ -690,6 +690,8 @@ export class WebServer extends EventEmitter {
|
|||||||
sendClear: config.sendClear ?? currentConfig?.sendClear ?? true,
|
sendClear: config.sendClear ?? currentConfig?.sendClear ?? true,
|
||||||
sendInit: config.sendInit ?? currentConfig?.sendInit ?? true,
|
sendInit: config.sendInit ?? currentConfig?.sendInit ?? true,
|
||||||
kickstartPrompt: config.kickstartPrompt ?? currentConfig?.kickstartPrompt,
|
kickstartPrompt: config.kickstartPrompt ?? currentConfig?.kickstartPrompt,
|
||||||
|
autoAcceptPrompts: config.autoAcceptPrompts ?? currentConfig?.autoAcceptPrompts ?? true,
|
||||||
|
autoAcceptDelayMs: config.autoAcceptDelayMs ?? currentConfig?.autoAcceptDelayMs ?? 8000,
|
||||||
durationMinutes: currentConfig?.durationMinutes,
|
durationMinutes: currentConfig?.durationMinutes,
|
||||||
};
|
};
|
||||||
this.screenManager.updateRespawnConfig(id, merged);
|
this.screenManager.updateRespawnConfig(id, merged);
|
||||||
@@ -1306,6 +1308,8 @@ export class WebServer extends EventEmitter {
|
|||||||
sendClear: config.sendClear,
|
sendClear: config.sendClear,
|
||||||
sendInit: config.sendInit,
|
sendInit: config.sendInit,
|
||||||
kickstartPrompt: config.kickstartPrompt,
|
kickstartPrompt: config.kickstartPrompt,
|
||||||
|
autoAcceptPrompts: config.autoAcceptPrompts,
|
||||||
|
autoAcceptDelayMs: config.autoAcceptDelayMs,
|
||||||
durationMinutes,
|
durationMinutes,
|
||||||
};
|
};
|
||||||
this.screenManager.updateRespawnConfig(sessionId, persistedConfig);
|
this.screenManager.updateRespawnConfig(sessionId, persistedConfig);
|
||||||
@@ -1537,6 +1541,10 @@ export class WebServer extends EventEmitter {
|
|||||||
this.broadcast('respawn:detectionUpdate', { sessionId, detection });
|
this.broadcast('respawn:detectionUpdate', { sessionId, detection });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
controller.on('autoAcceptSent', () => {
|
||||||
|
this.broadcast('respawn:autoAcceptSent', { sessionId });
|
||||||
|
});
|
||||||
|
|
||||||
controller.on('log', (message: string) => {
|
controller.on('log', (message: string) => {
|
||||||
this.broadcast('respawn:log', { sessionId, message });
|
this.broadcast('respawn:log', { sessionId, message });
|
||||||
});
|
});
|
||||||
@@ -2008,6 +2016,8 @@ export class WebServer extends EventEmitter {
|
|||||||
kickstartPrompt: savedState.respawnConfig.kickstartPrompt,
|
kickstartPrompt: savedState.respawnConfig.kickstartPrompt,
|
||||||
completionConfirmMs: savedState.respawnConfig.completionConfirmMs,
|
completionConfirmMs: savedState.respawnConfig.completionConfirmMs,
|
||||||
noOutputTimeoutMs: savedState.respawnConfig.noOutputTimeoutMs,
|
noOutputTimeoutMs: savedState.respawnConfig.noOutputTimeoutMs,
|
||||||
|
autoAcceptPrompts: savedState.respawnConfig.autoAcceptPrompts ?? true,
|
||||||
|
autoAcceptDelayMs: savedState.respawnConfig.autoAcceptDelayMs ?? 8000,
|
||||||
});
|
});
|
||||||
this.respawnControllers.set(session.id, controller);
|
this.respawnControllers.set(session.id, controller);
|
||||||
this.setupRespawnListeners(session.id, controller);
|
this.setupRespawnListeners(session.id, controller);
|
||||||
@@ -2035,6 +2045,8 @@ export class WebServer extends EventEmitter {
|
|||||||
sendClear: screen.respawnConfig.sendClear,
|
sendClear: screen.respawnConfig.sendClear,
|
||||||
sendInit: screen.respawnConfig.sendInit,
|
sendInit: screen.respawnConfig.sendInit,
|
||||||
kickstartPrompt: screen.respawnConfig.kickstartPrompt,
|
kickstartPrompt: screen.respawnConfig.kickstartPrompt,
|
||||||
|
autoAcceptPrompts: screen.respawnConfig.autoAcceptPrompts ?? true,
|
||||||
|
autoAcceptDelayMs: screen.respawnConfig.autoAcceptDelayMs ?? 8000,
|
||||||
});
|
});
|
||||||
this.respawnControllers.set(session.id, controller);
|
this.respawnControllers.set(session.id, controller);
|
||||||
this.setupRespawnListeners(session.id, controller);
|
this.setupRespawnListeners(session.id, controller);
|
||||||
|
|||||||
@@ -857,4 +857,203 @@ describe('RespawnController Edge Cases', () => {
|
|||||||
expect(status.timeSinceActivity).toBeLessThan(100);
|
expect(status.timeSinceActivity).toBeLessThan(100);
|
||||||
controller.stop();
|
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