From 0cb3a8ba6d4b8a07d35b0beba3633f05095896cf Mon Sep 17 00:00:00 2001 From: Sanskar Singh <116519896+itzrnvr@users.noreply.github.com> Date: Sat, 16 May 2026 11:08:31 +0530 Subject: [PATCH] fix: respond to ConPTY DSR cursor query on spawn MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ConPTY sends ESC[6n (Device Status Report — report cursor position) immediately on session start and blocks the child process until it receives a reply. Since portable-pty never responds, the child hangs forever and the PTY times out. Fix: write ESC[1;1R (cursor at row 1, col 1) to the PTY input pipe right after spawning, before reading any output. This unblocks ConPTY and allows the child process to start normally. --- .gitignore | 1 + crates/pi-natives/src/pty.rs | 18 +++++++++++++-- packages/natives/native/index.d.ts | 5 ++++ repro_pty.rs | 20 ++++++++++++++++ test-pty.ts | 37 ++++++++++++++++++++++++++++++ 5 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 repro_pty.rs create mode 100644 test-pty.ts diff --git a/.gitignore b/.gitignore index b17590ad7..bf283ba5f 100644 --- a/.gitignore +++ b/.gitignore @@ -61,3 +61,4 @@ python/omp-rpc/src/omp_rpc.egg-info/ # parallel-agent worktrees .wt/ CPU*.md +packages/coding-agent/binaries/ diff --git a/crates/pi-natives/src/pty.rs b/crates/pi-natives/src/pty.rs index 6fcb2137a..1b4a33c84 100644 --- a/crates/pi-natives/src/pty.rs +++ b/crates/pi-natives/src/pty.rs @@ -252,8 +252,18 @@ fn run_pty_sync( .map_err(|err| Error::from_reason(format!("Failed to open PTY: {err}")))? }; - let mut cmd = CommandBuilder::new(config.shell.as_deref().unwrap_or("sh")); - cmd.arg("-lc"); + let shell = config.shell.as_deref().unwrap_or("sh"); + let mut cmd = CommandBuilder::new(shell); + // Use shell-appropriate command execution flags + let lower = shell.to_lowercase(); + if lower.ends_with("cmd.exe") || lower.ends_with("cmd") { + cmd.arg("/c"); + } else if lower.contains("powershell") || lower.contains("pwsh") { + cmd.arg("-Command"); + } else { + // sh/bash/zsh/fish etc. + cmd.arg("-lc"); + } cmd.arg(&config.command); if let Some(cwd) = config.cwd.as_ref() { cmd.cwd(cwd); @@ -276,6 +286,10 @@ fn run_pty_sync( let mut writer = master .take_writer() .map_err(|err| Error::from_reason(format!("Failed to create PTY writer: {err}")))?; + // ConPTY sends ESC[6n (cursor position query) and blocks until we reply. + // Reply with cursor at 1,1 so it unblocks the child spawn. + let _ = writer.write_all(b"\x1b[1;1R"); + let _ = writer.flush(); let mut reader = master .try_clone_reader() .map_err(|err| Error::from_reason(format!("Failed to create PTY reader: {err}")))?; diff --git a/packages/natives/native/index.d.ts b/packages/natives/native/index.d.ts index d3a32e970..7153c09d6 100644 --- a/packages/natives/native/index.d.ts +++ b/packages/natives/native/index.d.ts @@ -1137,6 +1137,11 @@ export interface PtyStartOptions { cols?: number /** PTY row count. */ rows?: number + /** + * Shell binary to use (e.g. "sh", "bash", or an absolute path). + * Defaults to "sh" if not provided. + */ + shell?: string } /** diff --git a/repro_pty.rs b/repro_pty.rs new file mode 100644 index 000000000..c9f2c4f51 --- /dev/null +++ b/repro_pty.rs @@ -0,0 +1,20 @@ +use portable_pty::{native_pty_system, PtySize, CommandBuilder}; + +fn main() { + println!("Starting PTY reproduction..."); + let pty_system = native_pty_system(); + println!("Got pty system"); + + let pair = pty_system.openpty(PtySize { + rows: 40, + cols: 120, + pixel_width: 0, + pixel_height: 0, + }); + match pair { + Ok(p) => println!("openpty succeeded!"), + Err(e) => println!("openpty failed: {}", e), + } + + println!("Done."); +} diff --git a/test-pty.ts b/test-pty.ts new file mode 100644 index 000000000..f8de4fb69 --- /dev/null +++ b/test-pty.ts @@ -0,0 +1,37 @@ +import { PtySession } from "@oh-my-pi/pi-natives"; + +async function testPty(shell: string, command: string, label: string) { + const session = new PtySession(); + const t0 = Date.now(); + let output = ""; + + await new Promise((resolve) => { + session.start( + { + command, + shell, + timeoutMs: 5000, + cols: 80, + rows: 24, + }, + (_err, chunk) => { + if (chunk) output += chunk; + }, + ).then((result) => { + const ms = Date.now() - t0; + // Strip VT sequences for readable output + const clean = output.replace(/\x1b\[[^a-zA-Z]*[a-zA-Z]/g, "").replace(/\x1b\][^\x07]*\x07/g, "").trim(); + console.log(`[${label}] ${ms}ms timedOut=${result.timedOut} exitCode=${result.exitCode} output="${clean}"`); + resolve(); + }).catch((err) => { + console.log(`[${label}] ${Date.now() - t0}ms ERROR: ${err.message}`); + resolve(); + }); + setTimeout(() => { console.log(`[${label}] HUNG`); resolve(); }, 10000); + }); +} + +await testPty("cmd.exe", "echo hello from cmd", "cmd"); +await testPty("powershell.exe", "Write-Host 'hello from pwsh'", "pwsh"); +await testPty("sh", "echo hello from bash", "sh-git-bash"); +process.exit(0);