From acd851e89f31a74c2140f167dcb922dc08becc44 Mon Sep 17 00:00:00 2001 From: Felipe Coury Date: Fri, 22 May 2026 16:20:09 -0300 Subject: [PATCH] fix(tui): restore Windows VT before TUI renders (#24082) ## Why Older Git for Windows versions can leave the Windows console output mode without virtual terminal processing after Codex runs git metadata commands in a repository. When the TUI later emits ANSI control sequences for redraws, restore, or image rendering, Windows Terminal can show raw escape bytes or leave the prompt/status area corrupted. This is a targeted mitigation for the repo-conditioned Windows rendering corruption reported in #23888 and related reports #23512 and #23628. Updating Git avoids the trigger for affected users, but Codex should also reassert the terminal mode before it writes TUI control sequences. | Before | After | |---|---| | CleanShot 2026-05-22 at 11 23 21 | CleanShot 2026-05-22 at 11 23
58 | ## What Changed - Re-enable Windows virtual terminal processing for stdout and stderr before TUI mode setup, restore, redraw, resume, and pet image render paths. - Treat invalid, null, or non-console handles as no-ops so redirected or non-console output is unaffected. - Keep the helper as a no-op on non-Windows platforms. ## How to Test 1. On Windows Terminal with a Git 2.28.0 for Windows install, start Codex inside a valid Git repository. 2. Start a new Codex CLI session. 3. Confirm the prompt, working indicator, and bottom status line remain readable instead of showing raw ANSI escape sequences. 4. Repeat outside a Git repository to confirm the ordinary non-repo startup path is unchanged. Targeted tests: - Not run locally; the behavior depends on Windows console mode APIs and the current worktree is on macOS. --- codex-rs/tui/src/tui.rs | 72 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 71 insertions(+), 1 deletion(-) diff --git a/codex-rs/tui/src/tui.rs b/codex-rs/tui/src/tui.rs index 32123b787c..47a6aeb6ee 100644 --- a/codex-rs/tui/src/tui.rs +++ b/codex-rs/tui/src/tui.rs @@ -166,6 +166,8 @@ mod tests { } pub fn set_modes() -> Result<()> { + ensure_virtual_terminal_processing()?; + execute!(stdout(), EnableBracketedPaste)?; enable_raw_mode()?; @@ -239,12 +241,16 @@ fn restore_common( raw_mode_restore: RawModeRestore, keyboard_restore: KeyboardRestore, ) -> Result<()> { + let mut first_error = ensure_virtual_terminal_processing().err(); + match keyboard_restore { KeyboardRestore::PopStack => keyboard_modes::restore_keyboard_enhancement_stack(), KeyboardRestore::ResetAfterExit => keyboard_modes::reset_keyboard_reporting_after_exit(), } - let mut first_error = execute!(stdout(), DisableBracketedPaste).err(); + if let Err(err) = execute!(stdout(), DisableBracketedPaste) { + first_error.get_or_insert(err); + } let _ = execute!(stdout(), DisableFocusChange); if matches!(raw_mode_restore, RawModeRestore::Disable) && let Err(err) = disable_raw_mode() @@ -797,6 +803,8 @@ impl Tui { // the synchronized update, to avoid racing with the event reader. let mut pending_viewport_area = self.pending_viewport_area()?; + ensure_virtual_terminal_processing()?; + stdout().sync_update(|_| { #[cfg(unix)] if let Some(prepared) = prepared_resume.take() { @@ -854,6 +862,10 @@ impl Tui { &mut self, request: Option, ) -> std::result::Result<(), crate::pets::PetImageRenderError> { + if let Err(err) = ensure_virtual_terminal_processing() { + return Err(crate::pets::PetImageRenderError::Terminal(err)); + } + let terminal = &mut self.terminal; let state = &mut self.ambient_pet_image_state; stdout().sync_update(|_| { @@ -869,6 +881,10 @@ impl Tui { &mut self, request: Option, ) -> std::result::Result<(), crate::pets::PetImageRenderError> { + if let Err(err) = ensure_virtual_terminal_processing() { + return Err(crate::pets::PetImageRenderError::Terminal(err)); + } + let terminal = &mut self.terminal; let state = &mut self.pet_picker_preview_image_state; stdout().sync_update(|_| { @@ -887,6 +903,10 @@ impl Tui { pub fn clear_ambient_pet_image( &mut self, ) -> std::result::Result<(), crate::pets::PetImageRenderError> { + if let Err(err) = ensure_virtual_terminal_processing() { + return Err(crate::pets::PetImageRenderError::Terminal(err)); + } + crate::pets::render_ambient_pet_image( self.terminal.backend_mut(), &mut self.ambient_pet_image_state, @@ -911,6 +931,8 @@ impl Tui { .suspend_context .prepare_resume_action(&mut self.terminal, &mut self.alt_saved_viewport); + ensure_virtual_terminal_processing()?; + stdout().sync_update(|_| { #[cfg(unix)] if let Some(prepared) = prepared_resume.take() { @@ -968,3 +990,51 @@ impl Tui { Ok(None) } } + +#[cfg(windows)] +fn ensure_virtual_terminal_processing() -> Result<()> { + use windows_sys::Win32::Foundation::HANDLE; + use windows_sys::Win32::Foundation::INVALID_HANDLE_VALUE; + use windows_sys::Win32::System::Console::ENABLE_PROCESSED_OUTPUT; + use windows_sys::Win32::System::Console::ENABLE_VIRTUAL_TERMINAL_PROCESSING; + use windows_sys::Win32::System::Console::GetConsoleMode; + use windows_sys::Win32::System::Console::GetStdHandle; + use windows_sys::Win32::System::Console::STD_ERROR_HANDLE; + use windows_sys::Win32::System::Console::STD_OUTPUT_HANDLE; + use windows_sys::Win32::System::Console::SetConsoleMode; + + fn enable_for_handle(handle: HANDLE) -> Result<()> { + if handle == INVALID_HANDLE_VALUE || handle == 0 { + return Ok(()); + } + + let mut mode = 0; + if unsafe { GetConsoleMode(handle, &mut mode) } == 0 { + return Ok(()); + } + + let requested = ENABLE_PROCESSED_OUTPUT | ENABLE_VIRTUAL_TERMINAL_PROCESSING; + if mode & requested == requested { + return Ok(()); + } + + if unsafe { SetConsoleMode(handle, mode | requested) } == 0 { + return Err(std::io::Error::last_os_error()); + } + + Ok(()) + } + + let stdout_handle = unsafe { GetStdHandle(STD_OUTPUT_HANDLE) }; + enable_for_handle(stdout_handle)?; + + let stderr_handle = unsafe { GetStdHandle(STD_ERROR_HANDLE) }; + enable_for_handle(stderr_handle)?; + + Ok(()) +} + +#[cfg(not(windows))] +fn ensure_virtual_terminal_processing() -> Result<()> { + Ok(()) +}