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 |
|---|---|
| <img width="2100" height="1359" alt="CleanShot 2026-05-22 at 11 23 21"
src="https://github.com/user-attachments/assets/3218c379-5f97-4c71-ab25-805c9d20578a"
/> | <img width="2100" height="1359" alt="CleanShot 2026-05-22 at 11 23
58"
src="https://github.com/user-attachments/assets/55ac72bb-37d0-400e-99bc-12dd5ea4092d"
/> |


## 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.
This commit is contained in:
Felipe Coury
2026-05-22 16:20:09 -03:00
committed by GitHub
parent 75b7e06621
commit acd851e89f

View File

@@ -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<crate::pets::AmbientPetDraw>,
) -> 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<crate::pets::AmbientPetDraw>,
) -> 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(())
}