fix: reset terminal state before interactive session handoff (#1934)

* 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 <noreply@anthropic.com>

* style: fix Biome formatting for Bun.spawnSync call

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: spawn-bot <spawn-bot@openrouter.ai>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Ahmed Abushagur <ahmed@abushagur.com>
Co-authored-by: L <6723574+louisgv@users.noreply.github.com>
This commit is contained in:
A 2026-02-25 15:45:27 -08:00 committed by GitHub
parent b6f021ecf2
commit 556f32ecfc
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 38 additions and 9 deletions

View file

@ -1,6 +1,6 @@
{
"name": "@openrouter/spawn",
"version": "0.10.15",
"version": "0.10.16",
"type": "module",
"bin": {
"spawn": "cli.js"

View file

@ -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();
}