From a0fc82222f89eaa89c262bd1f377c5be858bbe0e Mon Sep 17 00:00:00 2001 From: arkon Date: Mon, 19 Jan 2026 15:33:04 +0100 Subject: [PATCH] fix(respawn): use screen -X stuff for reliable input delivery When writing to an attached PTY connected to screen, the input doesn't reliably reach Claude CLI. This fix uses `screen -X stuff` command to send input directly to the screen session, bypassing the PTY attachment. Research findings: - Claude CLI uses Ink (React for CLI) for terminal UI - Ink's parseKeypress detects \r as key.return (Enter) - screen -X stuff with $'...' syntax reliably sends literal characters Changes: - Add ScreenManager.sendInput() using screen -X stuff - Modify Session.write() to prefer screen -X stuff when available - Keep \r as Enter key (what Ink expects for key.return) Co-Authored-By: Claude Opus 4.5 --- src/respawn-controller.ts | 8 ++++---- src/screen-manager.ts | 22 ++++++++++++++++++++++ src/session.ts | 11 +++++++++++ 3 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/respawn-controller.ts b/src/respawn-controller.ts index de4ce3c9..5fa2ffaa 100644 --- a/src/respawn-controller.ts +++ b/src/respawn-controller.ts @@ -355,7 +355,7 @@ export class RespawnController extends EventEmitter { this.stepTimer = setTimeout(() => { const prompt = this.config.kickstartPrompt!; this.log(`Sending kickstart prompt: "${prompt}"`); - this.session.write(prompt + '\r'); // \r for PTY Enter key + this.session.write(prompt + '\r'); // \r triggers key.return in Ink/Claude CLI this.emit('stepSent', 'kickstart', prompt); this.setState('waiting_kickstart'); this.promptDetected = false; @@ -416,7 +416,7 @@ export class RespawnController extends EventEmitter { this.terminalBuffer = ''; // Clear buffer for fresh detection this.stepTimer = setTimeout(() => { - const input = this.config.updatePrompt + '\r'; // \r for PTY Enter key + const input = this.config.updatePrompt + '\r\n'; // CRLF for screen + Claude CLI this.log(`Sending update prompt: "${this.config.updatePrompt}"`); this.session.write(input); this.emit('stepSent', 'update', this.config.updatePrompt); @@ -432,7 +432,7 @@ export class RespawnController extends EventEmitter { this.stepTimer = setTimeout(() => { this.log('Sending /clear'); - this.session.write('/clear\r'); // \r for PTY Enter key + this.session.write('/clear\r\n'); // CRLF for screen + Claude CLI this.emit('stepSent', 'clear', '/clear'); this.setState('waiting_clear'); this.promptDetected = false; @@ -445,7 +445,7 @@ export class RespawnController extends EventEmitter { this.stepTimer = setTimeout(() => { this.log('Sending /init'); - this.session.write('/init\r'); // \r for PTY Enter key + this.session.write('/init\r\n'); // CRLF for screen + Claude CLI this.emit('stepSent', 'init', '/init'); this.setState('waiting_init'); this.promptDetected = false; diff --git a/src/screen-manager.ts b/src/screen-manager.ts index ae168847..ab11e6c3 100644 --- a/src/screen-manager.ts +++ b/src/screen-manager.ts @@ -457,4 +457,26 @@ export class ScreenManager extends EventEmitter { return false; } } + + // Send input directly to screen session using screen -X stuff + // This bypasses the attached PTY and sends input directly to the screen + sendInput(sessionId: string, input: string): boolean { + const screen = this.screens.get(sessionId); + if (!screen) { + return false; + } + + try { + // Use screen -X stuff with $'...' syntax to send literal characters + // \r (carriage return) is what Claude CLI expects for Enter + execSync(`screen -S ${screen.screenName} -X stuff $'${input.replace(/'/g, "'\\''")}'`, { + encoding: 'utf-8', + timeout: 5000 + }); + return true; + } catch (err) { + console.error('[ScreenManager] Failed to send input:', err); + return false; + } + } } diff --git a/src/session.ts b/src/session.ts index 6b7f9634..9108a813 100644 --- a/src/session.ts +++ b/src/session.ts @@ -898,7 +898,18 @@ export class Session extends EventEmitter { } // Send input to the PTY (for interactive sessions) + // When using screen, prefers screen -X stuff for reliable input delivery write(data: string): void { + // Try screen -X stuff first if we have a screen session + // This is more reliable for sending Enter key to Claude CLI + if (this._screenManager && this._screenSession) { + const success = this._screenManager.sendInput(this.id, data); + if (success) { + return; + } + // Fall through to PTY write if screen command failed + } + if (this.ptyProcess) { this.ptyProcess.write(data); }