From 6d2909ae81abf09f3dfa5af93d87866f641c0546 Mon Sep 17 00:00:00 2001 From: Josh McKinney Date: Mon, 10 Nov 2025 19:56:52 -0800 Subject: [PATCH] fix(tui): clear scrollback and redraw history on resize to avoid terminal glitches MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Clear scrollback and screen via ANSI/OSC 1337 before re-rendering history, reducing chopped or re-drawn text outside the scroll area on resize. - Handle resize explicitly, rebuilding transcript cells at the new width and redrawing, and treat onboarding resize like draw to ensure a full clear. - Add ClearScrollback command leveraging ESC[3J][H][2J plus iTerm’s 1337 code; works on Ghostty/WezTerm/Kitty, partial on Terminal.app, not on Alacritty. - Note: may introduce resize flicker; debouncing/notification parity still TBD. --- codex-rs/Cargo.lock | 10 ++-- codex-rs/tui/src/app.rs | 28 +++++++++++ .../tui/src/onboarding/onboarding_screen.rs | 2 +- codex-rs/tui/src/tui.rs | 48 ++++++++++++++++++- 4 files changed, 80 insertions(+), 8 deletions(-) diff --git a/codex-rs/Cargo.lock b/codex-rs/Cargo.lock index cf5f961a62..8794f54af0 100644 --- a/codex-rs/Cargo.lock +++ b/codex-rs/Cargo.lock @@ -4450,7 +4450,7 @@ checksum = "3af6b589e163c5a788fab00ce0c0366f6efbb9959c2f9874b224936af7fce7e1" dependencies = [ "base64", "indexmap 2.12.0", - "quick-xml", + "quick-xml 0.38.0", "serde", "time", ] @@ -7093,7 +7093,7 @@ version = "0.31.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c66a47e840dc20793f2264eb4b3e4ecb4b75d91c0dd4af04b456128e0bdd449d" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "rustix 1.0.8", "wayland-backend", "wayland-scanner", @@ -7105,7 +7105,7 @@ version = "0.32.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efa790ed75fbfd71283bd2521a1cfdc022aabcc28bdcff00851f9e4ae88d9901" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "wayland-backend", "wayland-client", "wayland-scanner", @@ -7117,7 +7117,7 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efd94963ed43cf9938a090ca4f7da58eb55325ec8200c3848963e98dc25b78ec" dependencies = [ - "bitflags 2.9.1", + "bitflags 2.10.0", "wayland-backend", "wayland-client", "wayland-protocols", @@ -7726,7 +7726,7 @@ dependencies = [ "os_pipe", "rustix 0.38.44", "tempfile", - "thiserror 2.0.16", + "thiserror 2.0.17", "tree_magic_mini", "wayland-backend", "wayland-client", diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs index ed5a2415b8..5355e07ff0 100644 --- a/codex-rs/tui/src/app.rs +++ b/codex-rs/tui/src/app.rs @@ -268,6 +268,34 @@ impl App { }, )?; } + TuiEvent::Resize(size) => { + // This code change addresses visual artifacts that can occur during terminal + // resize events by completely redrawing the scrollback history instead of + // attempting to incrementally adjust it. This approach helps ensure that the + // display remains consistent and free of glitches as the terminal dimensions + // change. + // + // TODO (joshka). This can cause flicker during resize, so we likely need to + // investigate debouncing resize events or optimizing the redraw. + // + // TODO (joshka). I'm unsure whether this also needs to post notifications like + // in the normal Draw case above. + // + // TODO (joshka). `size` is the size that comes from the event, which may differ + // from the terminal's actual size. Some parts of the draw path use the terminal + // size directly. We should standardize on one or the other so that there's no + // mismatch, but debouncing first seems more important. + tui.clear_scrollback()?; + for cell in &self.transcript_cells { + tui.insert_history_lines(cell.display_lines(size.width)); + } + tui.draw(self.chat_widget.desired_height(size.width), |frame| { + self.chat_widget.render(frame.area(), frame.buffer); + if let Some((x, y)) = self.chat_widget.cursor_pos(frame.area()) { + frame.set_cursor_position((x, y)); + } + })?; + } } } Ok(true) diff --git a/codex-rs/tui/src/onboarding/onboarding_screen.rs b/codex-rs/tui/src/onboarding/onboarding_screen.rs index 02c7be7ad2..b82a67edce 100644 --- a/codex-rs/tui/src/onboarding/onboarding_screen.rs +++ b/codex-rs/tui/src/onboarding/onboarding_screen.rs @@ -411,7 +411,7 @@ pub(crate) async fn run_onboarding_app( TuiEvent::Paste(text) => { onboarding_screen.handle_paste(text); } - TuiEvent::Draw => { + TuiEvent::Draw | TuiEvent::Resize(_) => { if !did_full_clear_after_success && onboarding_screen.steps.iter().any(|step| { if let Step::Auth(w) = step { diff --git a/codex-rs/tui/src/tui.rs b/codex-rs/tui/src/tui.rs index d758996d26..9364dcbe98 100644 --- a/codex-rs/tui/src/tui.rs +++ b/codex-rs/tui/src/tui.rs @@ -12,6 +12,7 @@ use std::time::Duration; use std::time::Instant; use crossterm::Command; +use crossterm::ExecutableCommand; use crossterm::SynchronizedUpdate; use crossterm::event::DisableBracketedPaste; use crossterm::event::DisableFocusChange; @@ -31,6 +32,7 @@ use ratatui::crossterm::execute; use ratatui::crossterm::terminal::disable_raw_mode; use ratatui::crossterm::terminal::enable_raw_mode; use ratatui::layout::Offset; +use ratatui::layout::Size; use ratatui::text::Line; use tokio::select; use tokio_stream::Stream; @@ -152,6 +154,7 @@ pub enum TuiEvent { Key(KeyEvent), Paste(String), Draw, + Resize(Size), } pub struct Tui { @@ -270,8 +273,8 @@ impl Tui { } yield TuiEvent::Key(key_event); } - Event::Resize(_, _) => { - yield TuiEvent::Draw; + Event::Resize(columns, rows) => { + yield TuiEvent::Resize(Size::new(columns, rows)); } Event::Paste(pasted) => { yield TuiEvent::Paste(pasted); @@ -436,6 +439,47 @@ impl Tui { }) })? } + + pub(crate) fn clear_scrollback(&mut self) -> Result<()> { + self.terminal.backend_mut().execute(ClearScrollback)?; + Ok(()) + } +} + +/// Command that emits an OSC 1337 ClearScrollback sequence. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct ClearScrollback; + +impl Command for ClearScrollback { + fn write_ansi(&self, f: &mut impl fmt::Write) -> fmt::Result { + // This sequence clears the scrollback and the screen. It aligns to the standard that the clear command currently outputs. + // - ESC[3J clears the scrollback buffer + // - ESC[H moves the cursor to the home position + // - ESC[2J clears the screen + // + // works on ghostty, wezterm, kitty + // does not work on alacritty, iterm + // sort of works on terminal.app (leftover content above the viewport) + write!(f, "\x1b[3J\x1b[H\x1b[2J")?; + + // iterm specific sequence to clear scrollback + // see https://iterm2.com/documentation-escape-codes.html + write!(f, "\x1b]1337;ClearScrollback=yes\x07")?; + + Ok(()) + } + + #[cfg(windows)] + fn execute_winapi(&self) -> Result<()> { + Err(std::io::Error::other( + "tried to execute ClearScrollback using WinAPI; use ANSI instead", + )) + } + + #[cfg(windows)] + fn is_ansi_code_supported(&self) -> bool { + true + } } /// Spawn background scheduler to coalesce frame requests and emit draws at deadlines.