From 556f32ecfca6417e8528dec5a2e43f35fcd598f8 Mon Sep 17 00:00:00 2001 From: A <258483684+la14-1@users.noreply.github.com> Date: Wed, 25 Feb 2026 15:45:27 -0800 Subject: [PATCH] fix: reset terminal state before interactive session handoff (#1934) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: reset terminal state before interactive session handoff The stdin handoff from TS orchestration to the interactive SSH session was leaving the terminal in a dirty state, causing users to need 2+ Enter presses or random keystrokes before input worked. Three fixes: 1. Unconditionally call setRawMode(false) instead of checking isRaw first — @clack/core's close() already resets the flag but the terminal can still be dirty after multiple readline instances 2. Run `stty sane` to fully reset the terminal line discipline, undoing any damage from readline's emitKeypressEvents 3. Resume stdin instead of pausing it — Bun.spawn with stdio:"inherit" needs an active stream, a paused stdin causes the child to see blocked input Co-Authored-By: Claude Opus 4.6 * style: fix Biome formatting for Bun.spawnSync call Co-Authored-By: Claude Opus 4.6 --------- Co-authored-by: spawn-bot Co-authored-by: Claude Opus 4.6 Co-authored-by: Ahmed Abushagur Co-authored-by: L <6723574+louisgv@users.noreply.github.com> --- packages/cli/package.json | 2 +- packages/cli/src/shared/ui.ts | 45 ++++++++++++++++++++++++++++------- 2 files changed, 38 insertions(+), 9 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index a0ab4d62..82ba8164 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@openrouter/spawn", - "version": "0.10.15", + "version": "0.10.16", "type": "module", "bin": { "spawn": "cli.js" diff --git a/packages/cli/src/shared/ui.ts b/packages/cli/src/shared/ui.ts index d58a73f1..c26d6005 100644 --- a/packages/cli/src/shared/ui.ts +++ b/packages/cli/src/shared/ui.ts @@ -263,15 +263,44 @@ export function sanitizeTermValue(term: string): string { } /** Prepare stdin for clean handoff to an interactive child process. - * Removes listeners, resets raw mode, and pauses stdin - * so that child_process.spawn gets a pristine file descriptor. */ + * Removes listeners, resets raw mode, and restores the terminal + * so that Bun.spawn with stdio:"inherit" gets a clean fd 0. */ export function prepareStdinForHandoff(): void { - // Remove any leftover keypress/data listeners (from @clack/prompts, etc.) + // Remove any leftover keypress/data listeners (from @clack/prompts, readline, etc.) process.stdin.removeAllListeners(); - // Reset raw mode if it was left on by @clack/prompts - if (process.stdin.isTTY && process.stdin.isRaw) { - process.stdin.setRawMode(false); + + // Unconditionally reset raw mode — @clack/core's close() may have already + // called setRawMode(false) making isRaw report false, but the terminal + // can still be in a dirty state after multiple readline instances + if (process.stdin.isTTY) { + try { + process.stdin.setRawMode(false); + } catch { + // ignore — not a TTY or already closed + } } - // Pause stdin so Node/Bun stops buffering input before the child takes over - process.stdin.pause(); + + // Reset terminal line discipline via stty to undo any damage from + // readline's emitKeypressEvents or leftover raw mode. This is the + // nuclear option that guarantees the terminal is in cooked mode with + // proper echo and line editing before SSH takes over. + try { + Bun.spawnSync( + [ + "stty", + "sane", + ], + { + stdin: "inherit", + stdout: "ignore", + stderr: "ignore", + }, + ); + } catch { + // ignore — stty may not be available + } + + // Resume stdin so Bun.spawn inherits an active fd 0. A paused stream + // can cause the child process to see a blocked/empty stdin on Bun. + process.stdin.resume(); }