Compare commits

...

1 Commits

Author SHA1 Message Date
Josh McKinney
6d2909ae81 fix(tui): clear scrollback and redraw history on resize to avoid terminal glitches
- 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.
2025-11-10 20:04:52 -08:00
4 changed files with 80 additions and 8 deletions

10
codex-rs/Cargo.lock generated
View File

@@ -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",

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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.