fix(tui): restore TUI after suspend (#28342)

## Why

On Linux, suspending Codex with `Ctrl+Z` and returning with `fg` can
leave the composer misaligned or inject terminal response bytes such as
focus reports into the prompt. Shell job-control output moves the cursor
while Codex is suspended, and terminal input polling can race with the
responses used to restore the inline viewport.

Fixes #26564.

## What changed

- preserve and restore keyboard reporting without disturbing the parent
terminal stack
- pause terminal event polling while Codex is suspended and flush
buffered input before resuming it
- force crossterm's cached raw-mode state back in sync after the shell
completes its `fg` handoff
- probe the actual post-`fg` cursor position with the tolerant
terminal-response parser, then realign the inline viewport before
redrawing

## How to Test

1. On Linux, start the development TUI with `just c`.
2. Type text into the composer without submitting it.
3. Press `Ctrl+Z`, run any harmless shell command, then run `fg`.
4. Confirm the composer redraws below the shell output, the draft text
is preserved, and no raw escape sequences appear.
5. Repeat the suspend/resume cycle and confirm normal typing still
works.

Targeted tests:

- `cargo test -p codex-tui --lib parses_cursor_position_as_zero_based -j
1`
- `cargo test -p codex-tui --lib tui::event_stream::tests -j 1`
This commit is contained in:
Felipe Coury authored and GitHub committed 2026-06-16 09:09:24 -07:00
1 parent 7162030b37
commit 76135cbe7e
4 files changed
+82 -25

No files matched your search

+14 -2
View File
@@ -281,6 +281,18 @@ pub fn restore() -> Result<()> {
restore_common(RawModeRestore::Disable, KeyboardRestore::PopStack)
}
/// Force crossterm's cached raw-mode state back in sync with the terminal after `fg`.
///
/// A shell may restore the job's saved termios after the process receives `SIGCONT`. When that
/// races with [`set_modes`], crossterm still believes raw mode is enabled even though the terminal
/// has returned to canonical, echoing mode. Clearing crossterm's saved state before enabling raw
/// mode again makes the kernel state authoritative once the shell has completed its handoff.
#[cfg(unix)]
pub(super) fn reapply_raw_mode_after_resume() -> Result<()> {
disable_raw_mode()?;
enable_raw_mode()
}
/// Restore the terminal after Codex is exiting.
///
/// Uses a stronger keyboard reset than [`restore`] so the parent shell recovers even if a
@@ -873,7 +885,7 @@ impl Tui {
#[cfg(unix)]
let mut prepared_resume = self
.suspend_context
.prepare_resume_action(&mut self.terminal, &mut self.alt_saved_viewport);
.prepare_resume_action(&mut self.alt_saved_viewport);
// Precompute any viewport updates that need a cursor-position query before entering
// the synchronized update, to avoid racing with the event reader.
@@ -1009,7 +1021,7 @@ impl Tui {
#[cfg(unix)]
let mut prepared_resume = self
.suspend_context
.prepare_resume_action(&mut self.terminal, &mut self.alt_saved_viewport);
.prepare_resume_action(&mut self.alt_saved_viewport);
ensure_virtual_terminal_processing()?;