mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-09 16:59:43 +02:00
refactor: hybrid indicator+timeout detection in respawn controller
- Primary: Use '↵ send' indicator for immediate response when ready - Fallback: Use timeout-based detection for prompt patterns - Add working indicators: Synthesizing, Brewing, ✻, ✽ - Cleaner step completion handlers (checkUpdateComplete, etc.) The hybrid approach responds immediately when Claude shows the suggestion indicator, while still having timeout fallback for reliability. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
+66
-88
@@ -10,6 +10,9 @@ const RESPAWN_BUFFER_TRIM_SIZE = 512 * 1024;
|
|||||||
const ANSI_ESCAPE_PATTERN = /\x1b\[[0-9;]*[HJKmsu?lh]/g;
|
const ANSI_ESCAPE_PATTERN = /\x1b\[[0-9;]*[HJKmsu?lh]/g;
|
||||||
const WHITESPACE_PATTERN = /\s+/g;
|
const WHITESPACE_PATTERN = /\s+/g;
|
||||||
|
|
||||||
|
// The definitive "ready for input" indicator - when Claude shows a suggestion
|
||||||
|
const READY_INDICATOR = '↵ send';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Respawn sequence states
|
* Respawn sequence states
|
||||||
*
|
*
|
||||||
@@ -98,8 +101,6 @@ export class RespawnController extends EventEmitter {
|
|||||||
'⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏', // Spinner chars
|
'⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏', // Spinner chars
|
||||||
'✻', '✽', // Activity indicators (spinning star)
|
'✻', '✽', // Activity indicators (spinning star)
|
||||||
];
|
];
|
||||||
private readonly CLEAR_COMPLETE_PATTERN = /conversation cleared|cleared|❯|⏵/i;
|
|
||||||
private readonly INIT_COMPLETE_PATTERN = /initialized|analyzing|❯|⏵|CLAUDE\.md/i;
|
|
||||||
|
|
||||||
constructor(session: Session, config: Partial<RespawnConfig> = {}) {
|
constructor(session: Session, config: Partial<RespawnConfig> = {}) {
|
||||||
super();
|
super();
|
||||||
@@ -199,15 +200,8 @@ export class RespawnController extends EventEmitter {
|
|||||||
this.terminalBuffer = this.terminalBuffer.slice(-RESPAWN_BUFFER_TRIM_SIZE);
|
this.terminalBuffer = this.terminalBuffer.slice(-RESPAWN_BUFFER_TRIM_SIZE);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter out noise - only count meaningful data as activity
|
// Check for the definitive "ready for input" indicator
|
||||||
// Ignore: cursor movements, color codes alone, small whitespace-only data
|
const isReady = data.includes(READY_INDICATOR);
|
||||||
// Uses pre-compiled patterns for performance
|
|
||||||
const meaningfulData = data
|
|
||||||
.replace(ANSI_ESCAPE_PATTERN, '') // Remove ANSI escape sequences
|
|
||||||
.replace(WHITESPACE_PATTERN, '') // Remove whitespace
|
|
||||||
.trim();
|
|
||||||
|
|
||||||
const isMeaningfulActivity = meaningfulData.length > 0;
|
|
||||||
|
|
||||||
// Detect working state
|
// Detect working state
|
||||||
const isWorking = this.WORKING_PATTERNS.some(pattern => data.includes(pattern));
|
const isWorking = this.WORKING_PATTERNS.some(pattern => data.includes(pattern));
|
||||||
@@ -219,47 +213,84 @@ export class RespawnController extends EventEmitter {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Detect prompt (idle) state
|
// Detect ready state (↵ send indicator) - immediate response
|
||||||
|
if (isReady) {
|
||||||
|
this.promptDetected = true;
|
||||||
|
this.workingDetected = false;
|
||||||
|
this.lastActivityTime = Date.now();
|
||||||
|
this.log('Ready indicator detected (↵ send)');
|
||||||
|
|
||||||
|
// Handle based on current state - immediate action
|
||||||
|
switch (this._state) {
|
||||||
|
case 'watching':
|
||||||
|
this.clearIdleTimer();
|
||||||
|
this.onIdleDetected();
|
||||||
|
break;
|
||||||
|
case 'waiting_update':
|
||||||
|
this.checkUpdateComplete();
|
||||||
|
break;
|
||||||
|
case 'waiting_clear':
|
||||||
|
this.checkClearComplete();
|
||||||
|
break;
|
||||||
|
case 'waiting_init':
|
||||||
|
this.checkInitComplete();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: detect prompt characters - start timeout-based check
|
||||||
const hasPrompt = this.PROMPT_PATTERNS.some(pattern => data.includes(pattern));
|
const hasPrompt = this.PROMPT_PATTERNS.some(pattern => data.includes(pattern));
|
||||||
if (hasPrompt) {
|
if (hasPrompt) {
|
||||||
const wasPromptDetected = this.promptDetected;
|
const wasPromptDetected = this.promptDetected;
|
||||||
this.promptDetected = true;
|
this.promptDetected = true;
|
||||||
this.workingDetected = false;
|
this.workingDetected = false;
|
||||||
|
this.lastActivityTime = Date.now();
|
||||||
|
|
||||||
if (!wasPromptDetected) {
|
if (!wasPromptDetected) {
|
||||||
// First time seeing prompt (or after being cleared) - log it
|
|
||||||
this.lastActivityTime = Date.now();
|
|
||||||
this.log('Prompt detected');
|
this.log('Prompt detected');
|
||||||
|
// Start fallback timeout for watching state
|
||||||
// Only start idle timer in watching state
|
|
||||||
if (this._state === 'watching') {
|
if (this._state === 'watching') {
|
||||||
this.startIdleTimer();
|
this.startIdleTimer();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// In waiting_* states, checkXxxComplete will handle the state transition
|
|
||||||
} else if (isMeaningfulActivity) {
|
|
||||||
// Meaningful activity that's not a prompt or working indicator
|
|
||||||
this.lastActivityTime = Date.now();
|
|
||||||
if (this.promptDetected && this._state === 'watching') {
|
|
||||||
// Still at prompt but got some other data - restart timer
|
|
||||||
this.startIdleTimer();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle state-specific terminal data
|
// Step completion handlers - called when ready indicator is detected
|
||||||
switch (this._state) {
|
private checkUpdateComplete(): void {
|
||||||
case 'waiting_update':
|
this.clearIdleTimer();
|
||||||
this.checkUpdateComplete(data);
|
this.log('Update completed (ready indicator)');
|
||||||
break;
|
this.emit('stepCompleted', 'update');
|
||||||
case 'waiting_clear':
|
|
||||||
this.checkClearComplete(data);
|
if (this.config.sendClear) {
|
||||||
break;
|
this.sendClear();
|
||||||
case 'waiting_init':
|
} else if (this.config.sendInit) {
|
||||||
this.checkInitComplete(data);
|
this.sendInit();
|
||||||
break;
|
} else {
|
||||||
|
this.completeCycle();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private checkClearComplete(): void {
|
||||||
|
this.clearIdleTimer();
|
||||||
|
this.log('/clear completed (ready indicator)');
|
||||||
|
this.emit('stepCompleted', 'clear');
|
||||||
|
|
||||||
|
if (this.config.sendInit) {
|
||||||
|
this.sendInit();
|
||||||
|
} else {
|
||||||
|
this.completeCycle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private checkInitComplete(): void {
|
||||||
|
this.clearIdleTimer();
|
||||||
|
this.log('/init completed (ready indicator)');
|
||||||
|
this.emit('stepCompleted', 'init');
|
||||||
|
this.completeCycle();
|
||||||
|
}
|
||||||
|
|
||||||
private startIdleTimer(): void {
|
private startIdleTimer(): void {
|
||||||
this.clearIdleTimer();
|
this.clearIdleTimer();
|
||||||
|
|
||||||
@@ -316,28 +347,6 @@ export class RespawnController extends EventEmitter {
|
|||||||
}, this.config.interStepDelayMs);
|
}, this.config.interStepDelayMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
private checkUpdateComplete(data: string): void {
|
|
||||||
// Update is complete when we see the prompt again after working
|
|
||||||
if (this.promptDetected && !this.workingDetected) {
|
|
||||||
// Wait to make sure it's truly done (use configured idle timeout)
|
|
||||||
this.clearIdleTimer();
|
|
||||||
this.idleTimer = setTimeout(() => {
|
|
||||||
if (this.promptDetected && !this.workingDetected) {
|
|
||||||
this.log('Update docs completed');
|
|
||||||
this.emit('stepCompleted', 'update');
|
|
||||||
// Proceed based on config
|
|
||||||
if (this.config.sendClear) {
|
|
||||||
this.sendClear();
|
|
||||||
} else if (this.config.sendInit) {
|
|
||||||
this.sendInit();
|
|
||||||
} else {
|
|
||||||
this.completeCycle();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, this.config.idleTimeoutMs); // Use configured idle timeout
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private sendClear(): void {
|
private sendClear(): void {
|
||||||
this.setState('sending_clear');
|
this.setState('sending_clear');
|
||||||
this.terminalBuffer = '';
|
this.terminalBuffer = '';
|
||||||
@@ -351,23 +360,6 @@ export class RespawnController extends EventEmitter {
|
|||||||
}, this.config.interStepDelayMs);
|
}, this.config.interStepDelayMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
private checkClearComplete(data: string): void {
|
|
||||||
// Clear is fast, but wait for prompt and verify idle
|
|
||||||
if (this.CLEAR_COMPLETE_PATTERN.test(data) || this.promptDetected) {
|
|
||||||
this.clearIdleTimer();
|
|
||||||
this.idleTimer = setTimeout(() => {
|
|
||||||
this.log('/clear completed');
|
|
||||||
this.emit('stepCompleted', 'clear');
|
|
||||||
// Proceed based on config
|
|
||||||
if (this.config.sendInit) {
|
|
||||||
this.sendInit();
|
|
||||||
} else {
|
|
||||||
this.completeCycle();
|
|
||||||
}
|
|
||||||
}, this.config.idleTimeoutMs); // Use configured idle timeout
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private sendInit(): void {
|
private sendInit(): void {
|
||||||
this.setState('sending_init');
|
this.setState('sending_init');
|
||||||
this.terminalBuffer = '';
|
this.terminalBuffer = '';
|
||||||
@@ -382,20 +374,6 @@ export class RespawnController extends EventEmitter {
|
|||||||
}, this.config.interStepDelayMs);
|
}, this.config.interStepDelayMs);
|
||||||
}
|
}
|
||||||
|
|
||||||
private checkInitComplete(data: string): void {
|
|
||||||
// Init completes when we see the prompt after it finishes
|
|
||||||
if (this.promptDetected && !this.workingDetected) {
|
|
||||||
this.clearIdleTimer();
|
|
||||||
this.idleTimer = setTimeout(() => {
|
|
||||||
if (this.promptDetected && !this.workingDetected) {
|
|
||||||
this.log('/init completed');
|
|
||||||
this.emit('stepCompleted', 'init');
|
|
||||||
this.completeCycle();
|
|
||||||
}
|
|
||||||
}, this.config.idleTimeoutMs); // Use configured idle timeout
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private completeCycle(): void {
|
private completeCycle(): void {
|
||||||
this.log(`Respawn cycle #${this.cycleCount} completed`);
|
this.log(`Respawn cycle #${this.cycleCount} completed`);
|
||||||
this.emit('respawnCycleCompleted', this.cycleCount);
|
this.emit('respawnCycleCompleted', this.cycleCount);
|
||||||
|
|||||||
@@ -26,25 +26,31 @@ class MockSession extends EventEmitter {
|
|||||||
this.emit('terminal', data);
|
this.emit('terminal', data);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simulate prompt appearing
|
// Simulate prompt appearing (basic prompt character)
|
||||||
simulatePrompt(): void {
|
simulatePrompt(): void {
|
||||||
this.emit('terminal', '❯ ');
|
this.emit('terminal', '❯ ');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Simulate ready state with the definitive indicator
|
||||||
|
simulateReady(): void {
|
||||||
|
this.emit('terminal', '↵ send');
|
||||||
|
}
|
||||||
|
|
||||||
// Simulate working state
|
// Simulate working state
|
||||||
simulateWorking(): void {
|
simulateWorking(): void {
|
||||||
this.emit('terminal', 'Thinking... ⠋');
|
this.emit('terminal', 'Thinking... ⠋');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simulate clear completion
|
// Simulate clear completion (followed by ready indicator)
|
||||||
simulateClearComplete(): void {
|
simulateClearComplete(): void {
|
||||||
this.emit('terminal', 'conversation cleared');
|
this.emit('terminal', 'conversation cleared');
|
||||||
|
setTimeout(() => this.simulateReady(), 50);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Simulate init completion
|
// Simulate init completion (followed by ready indicator)
|
||||||
simulateInitComplete(): void {
|
simulateInitComplete(): void {
|
||||||
this.emit('terminal', 'Analyzing CLAUDE.md...');
|
this.emit('terminal', 'Analyzing CLAUDE.md...');
|
||||||
setTimeout(() => this.simulatePrompt(), 100);
|
setTimeout(() => this.simulateReady(), 100);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user