Preserve inline TUI scrollback in Windows Terminal (#39619)

## Why

Windows Terminal can discard rows scrolled through a partial DEC scroll region instead of retaining them in terminal scrollback.

## What changed

- Detect Windows Terminal, including via `WT_SESSION`, and use full-screen scrolling for inline viewport growth and history insertion.
- Keep the existing terminal-wrapped history behavior for Zellij while centralizing terminal-specific scrollback selection.

## Testing

Add VT100-backed tests covering strategy selection and preservation of scrollback during history insertion and viewport growth.

GitOrigin-RevId: 209a7a8053a1fcf911d6e0aee764e563b8725bd1
This commit is contained in:
Benjamin Carlsson authored and copyberry committed 2026-08-20 06:39:20 +00:00
1 parent 4bb7804a23
commit da6e68951b
5 files changed
+284 -34

No files matched your search

+23 -21
View File
@@ -48,7 +48,6 @@ use self::input_boundary::terminal_input_is_readable;
use crate::custom_terminal;
use crate::custom_terminal::Terminal as CustomTerminal;
use crate::insert_history::HistoryLineWrapPolicy;
use crate::insert_history::InsertHistoryMode;
use crate::notifications::DesktopNotificationBackend;
use crate::notifications::detect_backend;
use crate::terminal_hyperlinks::HyperlinkLine;
@@ -58,6 +57,7 @@ use crate::tui::event_stream::TuiEventStream;
#[cfg(unix)]
use crate::tui::job_control::SuspendContext;
use crate::tui::screen_size::ScreenSizePolicy;
use crate::tui::scrollback::ScrollbackStrategy;
use codex_config::types::NotificationCondition;
use codex_config::types::NotificationMethod;
@@ -70,6 +70,7 @@ mod input_boundary;
mod job_control;
mod keyboard_modes;
mod screen_size;
mod scrollback;
#[cfg(all(test, unix))]
#[path = "tui_startup_tests.rs"]
mod startup_tests;
@@ -594,8 +595,7 @@ pub struct Tui {
enhanced_keys_supported: bool,
notification_backend: Option<DesktopNotificationBackend>,
notification_condition: NotificationCondition,
// Raw terminal-wrapped history needs a non-scroll-region insertion path in Zellij.
is_zellij: bool,
scrollback: ScrollbackStrategy,
// When false, enter_alt_screen() becomes a no-op.
alt_screen_enabled: bool,
// Keeps unmanaged process stderr writes out of the inline viewport.
@@ -631,7 +631,7 @@ impl Tui {
// Cache this to avoid contention with the event reader.
supports_color::on_cached(supports_color::Stream::Stdout);
let _ = crate::terminal_palette::default_colors();
let is_zellij = codex_terminal_detection::terminal_info().is_zellij();
let scrollback = ScrollbackStrategy::detect(&codex_terminal_detection::terminal_info());
Self {
frame_requester,
@@ -650,7 +650,7 @@ impl Tui {
enhanced_keys_supported,
notification_backend: Some(detect_backend(NotificationMethod::default())),
notification_condition: NotificationCondition::default(),
is_zellij,
scrollback,
alt_screen_enabled: true,
_stderr_guard: stderr_guard,
}
@@ -893,6 +893,7 @@ impl Tui {
terminal: &mut Terminal,
height: u16,
screen_size: Size,
scrollback: ScrollbackStrategy,
) -> Result<bool> {
let terminal_height_shrank = screen_size.height < terminal.last_known_screen_size.height;
let terminal_height_grew = screen_size.height > terminal.last_known_screen_size.height;
@@ -908,9 +909,7 @@ impl Tui {
if area.bottom() > screen_size.height {
let scroll_by = area.bottom() - screen_size.height;
if !terminal_height_shrank {
terminal
.backend_mut()
.scroll_region_up(0..area.top(), scroll_by)?;
scrollback.grow_viewport(terminal, area.top(), screen_size, scroll_by)?;
}
area.y = screen_size.height - area.height;
} else if terminal_height_grew && viewport_was_bottom_aligned {
@@ -931,7 +930,7 @@ impl Tui {
fn flush_pending_history_lines(
terminal: &mut Terminal,
pending_history_lines: &mut Vec<PendingHistoryLines>,
is_zellij: bool,
scrollback: ScrollbackStrategy,
screen_size: Size,
) -> Result<()> {
if pending_history_lines.is_empty() {
@@ -939,11 +938,7 @@ impl Tui {
}
for batch in pending_history_lines.iter() {
let mode = if is_zellij && batch.wrap_policy == HistoryLineWrapPolicy::Terminal {
InsertHistoryMode::ZellijRaw
} else {
InsertHistoryMode::Standard
};
let mode = scrollback.history_insertion_mode(batch.wrap_policy);
crate::insert_history::insert_history_hyperlink_lines_with_mode_and_wrap_policy(
terminal,
&batch.lines,
@@ -992,9 +987,12 @@ impl Tui {
area.width = screen_size.width;
// If the viewport has expanded, scroll everything else up to make room.
if area.bottom() > screen_size.height {
terminal
.backend_mut()
.scroll_region_up(0..area.top(), area.bottom() - screen_size.height)?;
self.scrollback.grow_viewport(
terminal,
area.top(),
screen_size,
area.bottom() - screen_size.height,
)?;
area.y = screen_size.height - area.height;
}
if area != terminal.viewport_area {
@@ -1007,7 +1005,7 @@ impl Tui {
Self::flush_pending_history_lines(
terminal,
&mut self.pending_history_lines,
self.is_zellij,
self.scrollback,
screen_size,
)?;
@@ -1114,8 +1112,12 @@ impl Tui {
}
let terminal = &mut self.terminal;
let needs_full_repaint =
Self::update_inline_viewport_for_resize_reflow(terminal, height, screen_size)?;
let needs_full_repaint = Self::update_inline_viewport_for_resize_reflow(
terminal,
height,
screen_size,
self.scrollback,
)?;
// A zero- or one-row history region cannot isolate raw history writes from the
// viewport, so replayed rows can leave stale cells inside the composer.
let history_can_overlap_viewport =
@@ -1123,7 +1125,7 @@ impl Tui {
Self::flush_pending_history_lines(
terminal,
&mut self.pending_history_lines,
self.is_zellij,
self.scrollback,
screen_size,
)?;