From 391438bb22061ae143cae5236fb2bec899088829 Mon Sep 17 00:00:00 2001 From: arkon Date: Mon, 19 Jan 2026 15:35:41 +0100 Subject: [PATCH] fix(session): separate PTY write from screen -X stuff Regular user input should go through PTY, only respawn controller should use screen -X stuff. Added writeViaScreen() method for programmatic input that needs reliable Enter key delivery. Co-Authored-By: Claude Opus 4.5 --- src/respawn-controller.ts | 8 ++++---- src/session.ts | 25 ++++++++++++++----------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/respawn-controller.ts b/src/respawn-controller.ts index 5fa2ffaa..c50cde6e 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 triggers key.return in Ink/Claude CLI + this.session.writeViaScreen(prompt + '\r'); // \r triggers key.return in Ink/Claude CLI this.emit('stepSent', 'kickstart', prompt); this.setState('waiting_kickstart'); this.promptDetected = false; @@ -418,7 +418,7 @@ export class RespawnController extends EventEmitter { this.stepTimer = setTimeout(() => { 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.session.writeViaScreen(input); this.emit('stepSent', 'update', this.config.updatePrompt); this.setState('waiting_update'); this.promptDetected = false; @@ -432,7 +432,7 @@ export class RespawnController extends EventEmitter { this.stepTimer = setTimeout(() => { this.log('Sending /clear'); - this.session.write('/clear\r\n'); // CRLF for screen + Claude CLI + this.session.writeViaScreen('/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\n'); // CRLF for screen + Claude CLI + this.session.writeViaScreen('/init\r\n'); // CRLF for screen + Claude CLI this.emit('stepSent', 'init', '/init'); this.setState('waiting_init'); this.promptDetected = false; diff --git a/src/session.ts b/src/session.ts index 9108a813..270b17e2 100644 --- a/src/session.ts +++ b/src/session.ts @@ -898,23 +898,26 @@ 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); } } + // Send input via screen -X stuff (for programmatic input like respawn controller) + // This bypasses PTY and sends directly to screen, more reliable for Enter key + writeViaScreen(data: string): boolean { + if (this._screenManager && this._screenSession) { + return this._screenManager.sendInput(this.id, data); + } + // Fallback to PTY write + if (this.ptyProcess) { + this.ptyProcess.write(data); + return true; + } + return false; + } + // Resize the PTY resize(cols: number, rows: number): void { if (this.ptyProcess) {