From d741b82966a53c2e680452e208150de46f4891b5 Mon Sep 17 00:00:00 2001 From: arkon Date: Wed, 21 Jan 2026 04:34:45 +0100 Subject: [PATCH] fix: improve terminal buffer cleaning and test reliability - Add buffer cleaning to remove junk before Claude banner - Fix test todo text to avoid pattern conflicts - Add reset support to inner-config API endpoint Co-Authored-By: Claude Opus 4.5 --- src/web/server.ts | 53 +++++++++++++++++++++++++++++++-- test/inner-loop-tracker.test.ts | 2 +- test/respawn-controller.test.ts | 5 ++++ 3 files changed, 56 insertions(+), 4 deletions(-) diff --git a/src/web/server.ts b/src/web/server.ts index df0eac2b..77f47f01 100644 --- a/src/web/server.ts +++ b/src/web/server.ts @@ -245,12 +245,13 @@ export class WebServer extends EventEmitter { // Configure inner loop (Ralph Wiggum) settings this.app.post('/api/sessions/:id/inner-config', async (req) => { const { id } = req.params as { id: string }; - const { enabled, completionPhrase, maxIterations, maxTodos, todoExpirationMinutes } = req.body as { + const { enabled, completionPhrase, maxIterations, maxTodos, todoExpirationMinutes, reset } = req.body as { enabled?: boolean; completionPhrase?: string; maxIterations?: number; maxTodos?: number; todoExpirationMinutes?: number; + reset?: boolean | 'full'; // true = soft reset (keep enabled), 'full' = complete reset }; const session = this.sessions.get(id); @@ -258,6 +259,15 @@ export class WebServer extends EventEmitter { return { success: false, error: 'Session not found' }; } + // Handle reset first (before other config) + if (reset) { + if (reset === 'full') { + session.innerLoopTracker.fullReset(); + } else { + session.innerLoopTracker.reset(); + } + } + // Enable/disable the tracker if (enabled !== undefined) { if (enabled) { @@ -403,8 +413,29 @@ export class WebServer extends EventEmitter { return { error: 'Session not found' }; } + // Clean the buffer: remove junk before actual Claude content + let cleanBuffer = session.terminalBuffer; + + // Find where Claude banner starts (has color codes before "Claude") + // Look for the bold escape sequence followed by "Claude" + const claudeMatch = cleanBuffer.match(/\x1b\[1mClaud/); + if (claudeMatch && claudeMatch.index !== undefined && claudeMatch.index > 0) { + // Find the start of that line (look for line start or screen positioning before it) + let lineStart = claudeMatch.index; + // Go back to find color/positioning sequences that are part of the banner + while (lineStart > 0 && cleanBuffer[lineStart - 1] !== '\n') { + lineStart--; + } + cleanBuffer = cleanBuffer.slice(lineStart); + } + + // Also remove any Ctrl+L and leading whitespace + cleanBuffer = cleanBuffer + .replace(/\x0c/g, '') + .replace(/^[\s\r\n]+/, ''); + return { - terminalBuffer: session.terminalBuffer, + terminalBuffer: cleanBuffer, status: session.status, }; }); @@ -924,9 +955,25 @@ export class WebServer extends EventEmitter { this.outputBatches.delete(sessionId); this.taskUpdateBatches.delete(sessionId); - // Clear inner state + // Reset inner loop tracker on the session before cleanup + if (session) { + session.innerLoopTracker.fullReset(); + } + + // Clear inner state from store this.store.removeInnerState(sessionId); + // Broadcast inner loop cleared to update UI + this.broadcast('session:innerLoopUpdate', { + sessionId, + state: { enabled: false, active: false, completionPhrase: null, startedAt: null, cycleCount: 0, maxIterations: null, lastActivity: Date.now(), elapsedHours: null } + }); + this.broadcast('session:innerTodoUpdate', { + sessionId, + todos: [], + stats: { total: 0, pending: 0, inProgress: 0, completed: 0 } + }); + // Stop session and remove listeners if (session) { session.removeAllListeners(); diff --git a/test/inner-loop-tracker.test.ts b/test/inner-loop-tracker.test.ts index 0f327871..8c3db484 100644 --- a/test/inner-loop-tracker.test.ts +++ b/test/inner-loop-tracker.test.ts @@ -425,7 +425,7 @@ describe('InnerLoopTracker', () => { it('should clear all state', () => { tracker.startLoop('TEST'); - tracker.processTerminalData('- [ ] Task\n'); + tracker.processTerminalData('- [ ] Test task to clear\n'); expect(tracker.loopState.active).toBe(true); expect(tracker.todos).toHaveLength(1); diff --git a/test/respawn-controller.test.ts b/test/respawn-controller.test.ts index 04c46a4e..e1ca1433 100644 --- a/test/respawn-controller.test.ts +++ b/test/respawn-controller.test.ts @@ -21,6 +21,11 @@ class MockSession extends EventEmitter { this.writeBuffer.push(data); } + writeViaScreen(data: string): boolean { + this.writeBuffer.push(data); + return true; + } + // Simulate terminal output simulateTerminalOutput(data: string): void { this.emit('terminal', data);