[codex] fix Windows ConPTY input handling (#29734)

## Why

Windows unified-exec TTY input did not behave like the non-Windows PTY
path. ConPTY sessions could receive the wrong line ending or mishandle
backspace, especially when sending input to a foreground program through
PowerShell or cmd. The local, legacy restricted, and elevated paths also
handled this normalization separately.

## What changed

- share one stateful Windows TTY input normalizer across local, legacy
restricted, and elevated runner paths
- translate LF and split CRLF into one Windows terminal Enter, encode
backspace as DEL, and preserve UTF-8 and control bytes such as Ctrl-C
- add Windows integration coverage for Unicode input, backspace, Enter,
and PowerShell foreground-child Ctrl-C behavior

## Validation

- `just test -p codex-utils-pty` (13 tests passed; the Unicode
integration test retried once)
- the Unicode integration test passed five consecutive runs with retries
disabled
- integration coverage sends `cafeé 漢字` through cmd and PowerShell and
verifies that Ctrl-C interrupts a running PowerShell foreground child
This commit is contained in:
iceweasel-oai authored and GitHub committed 2026-06-24 11:27:44 -07:00
1 parent 8a6a34be75
commit a781761eda
8 files changed
+226 -23

No files matched your search

@@ -1,5 +1,4 @@
use super::windows_common::finish_driver_spawn;
use super::windows_common::normalize_windows_tty_input;
use crate::conpty::ConptyInstance;
use crate::conpty::spawn_conpty_process_as_user;
use crate::desktop::LaunchDesktop;
@@ -22,6 +21,7 @@ use codex_utils_absolute_path::AbsolutePathBuf;
use codex_utils_pty::ProcessDriver;
use codex_utils_pty::SpawnedProcess;
use codex_utils_pty::TerminalSize;
use codex_utils_pty::WindowsTtyInputNormalizer;
use std::collections::HashMap;
use std::path::Path;
use std::ptr;
@@ -153,13 +153,13 @@ fn spawn_input_writer(
normalize_newlines: bool,
) -> tokio::task::JoinHandle<()> {
tokio::task::spawn_blocking(move || {
let mut previous_was_cr = false;
let mut windows_input = WindowsTtyInputNormalizer::default();
while let Some(bytes) = writer_rx.blocking_recv() {
let Some(handle) = input_write else {
continue;
};
let bytes = if normalize_newlines {
normalize_windows_tty_input(&bytes, &mut previous_was_cr)
windows_input.normalize(&bytes)
} else {
bytes
};
@@ -11,6 +11,7 @@ use anyhow::Result;
use codex_utils_pty::ProcessDriver;
use codex_utils_pty::SpawnedProcess;
use codex_utils_pty::TerminalSize;
use codex_utils_pty::WindowsTtyInputNormalizer;
use codex_utils_pty::spawn_from_driver;
use std::fs::File;
use tokio::sync::broadcast;
@@ -25,23 +26,6 @@ pub(crate) fn finish_driver_spawn(driver: ProcessDriver, stdin_open: bool) -> Sp
spawned
}
pub(crate) fn normalize_windows_tty_input(bytes: &[u8], previous_was_cr: &mut bool) -> Vec<u8> {
let mut normalized = Vec::with_capacity(bytes.len());
for &byte in bytes {
if byte == b'\n' {
if !*previous_was_cr {
normalized.push(b'\r');
}
normalized.push(b'\n');
*previous_was_cr = false;
} else {
normalized.push(byte);
*previous_was_cr = byte == b'\r';
}
}
normalized
}
pub(crate) fn start_runner_pipe_writer(
mut pipe_write: File,
) -> std::sync::mpsc::Sender<FramedMessage> {
@@ -63,10 +47,10 @@ pub(crate) fn start_runner_stdin_writer(
stdin_open: bool,
) -> tokio::task::JoinHandle<()> {
tokio::task::spawn_blocking(move || {
let mut previous_was_cr = false;
let mut windows_input = WindowsTtyInputNormalizer::default();
while let Some(bytes) = writer_rx.blocking_recv() {
let bytes = if normalize_newlines {
normalize_windows_tty_input(&bytes, &mut previous_was_cr)
windows_input.normalize(&bytes)
} else {
bytes
};