diff --git a/codex-rs/tui/src/insert_history.rs b/codex-rs/tui/src/insert_history.rs index bf4ad4bad4..6ee6b15ba0 100644 --- a/codex-rs/tui/src/insert_history.rs +++ b/codex-rs/tui/src/insert_history.rs @@ -46,7 +46,10 @@ use ratatui::text::Span; pub enum InsertHistoryMode { Standard, Zellij, - FullScreenReplay, + FullScreenReplayPrefill, + // Kept available while validating terminal-specific replay behavior. + #[allow(dead_code)] + FullScreenReplayDirect, } impl InsertHistoryMode { @@ -78,9 +81,10 @@ where /// emits newlines at the screen bottom to create space (since Zellij ignores scroll /// region escapes) and writes lines at computed absolute positions. Both modes /// update `terminal.viewport_area` so subsequent draw passes know where the -/// viewport moved to. In `FullScreenReplay` mode every line is replayed through -/// normal full-screen scrolling so terminals that drop scroll-region output from -/// scrollback can still rebuild deep history after a resize. +/// viewport moved to. In `FullScreenReplayPrefill` mode blank rows are used to +/// create scrollback space before replaying content, matching xterm.js behavior. +/// `FullScreenReplayDirect` keeps the no-prefill strategy isolated for terminals +/// where the prefilled blank rows become user-visible scrollback. pub fn insert_history_lines_with_mode( terminal: &mut crate::custom_terminal::Terminal, lines: Vec, @@ -126,88 +130,103 @@ where } let wrapped_lines = wrapped_rows as u16; - if matches!( - mode, - InsertHistoryMode::Zellij | InsertHistoryMode::FullScreenReplay - ) { - let space_below = screen_size.height.saturating_sub(area.bottom()); - let shift_down = if matches!(mode, InsertHistoryMode::Zellij) { - wrapped_lines.min(space_below) - } else { - 0 - }; - let scroll_up_amount = wrapped_lines.saturating_sub(shift_down); + match mode { + InsertHistoryMode::Zellij | InsertHistoryMode::FullScreenReplayPrefill => { + let space_below = screen_size.height.saturating_sub(area.bottom()); + let shift_down = if matches!(mode, InsertHistoryMode::Zellij) { + wrapped_lines.min(space_below) + } else { + 0 + }; + let scroll_up_amount = wrapped_lines.saturating_sub(shift_down); - if scroll_up_amount > 0 { - // Scroll the entire screen up by emitting \n at the bottom - queue!(writer, MoveTo(0, screen_size.height.saturating_sub(1)))?; - for _ in 0..scroll_up_amount { - queue!(writer, Print("\n"))?; + if scroll_up_amount > 0 { + // Scroll the entire screen up by emitting \n at the bottom + queue!( + writer, + MoveTo(/*x*/ 0, screen_size.height.saturating_sub(1)) + )?; + for _ in 0..scroll_up_amount { + queue!(writer, Print("\n"))?; + } + } + + if shift_down > 0 { + area.y += shift_down; + should_update_area = true; + } + + let cursor_top = area.top().saturating_sub(scroll_up_amount + shift_down); + queue!(writer, MoveTo(/*x*/ 0, cursor_top))?; + + for (i, line) in wrapped.iter().enumerate() { + if i > 0 { + queue!(writer, Print("\r\n"))?; + } + write_history_line(writer, line, wrap_width)?; } } + InsertHistoryMode::FullScreenReplayDirect => { + // Rebuild scrollback with transcript content itself. Pre-scrolling with blank + // newlines creates visible empty scrollback in terminals such as Terminal.app. + queue!(writer, MoveTo(/*x*/ 0, /*y*/ 0))?; - if shift_down > 0 { - area.y += shift_down; - should_update_area = true; + for (i, line) in wrapped.iter().enumerate() { + if i > 0 { + queue!(writer, Print("\r\n"))?; + } + write_history_line(writer, line, wrap_width)?; + } } + InsertHistoryMode::Standard => { + let cursor_top = if area.bottom() < screen_size.height { + let scroll_amount = wrapped_lines.min(screen_size.height - area.bottom()); - let cursor_top = area.top().saturating_sub(scroll_up_amount + shift_down); - queue!(writer, MoveTo(0, cursor_top))?; + let top_1based = area.top() + 1; + queue!(writer, SetScrollRegion(top_1based..screen_size.height))?; + queue!(writer, MoveTo(/*x*/ 0, area.top()))?; + for _ in 0..scroll_amount { + queue!(writer, Print("\x1bM"))?; + } + queue!(writer, ResetScrollRegion)?; - for (i, line) in wrapped.iter().enumerate() { - if i > 0 { + let cursor_top = area.top().saturating_sub(1); + area.y += scroll_amount; + should_update_area = true; + cursor_top + } else { + area.top().saturating_sub(1) + }; + + // Limit the scroll region to the lines from the top of the screen to the + // top of the viewport. With this in place, when we add lines inside this + // area, only the lines in this area will be scrolled. We place the cursor + // at the end of the scroll region, and add lines starting there. + // + // ┌─Screen───────────────────────┐ + // │┌╌Scroll region╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐│ + // │┆ ┆│ + // │┆ ┆│ + // │┆ ┆│ + // │█╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘│ + // │╭─Viewport───────────────────╮│ + // ││ ││ + // │╰────────────────────────────╯│ + // └──────────────────────────────┘ + queue!(writer, SetScrollRegion(1..area.top()))?; + + // NB: we are using MoveTo instead of set_cursor_position here to avoid messing with the + // terminal's last_known_cursor_position, which hopefully will still be accurate after we + // fetch/restore the cursor position. insert_history_lines should be cursor-position-neutral :) + queue!(writer, MoveTo(/*x*/ 0, cursor_top))?; + + for line in &wrapped { queue!(writer, Print("\r\n"))?; + write_history_line(writer, line, wrap_width)?; } - write_history_line(writer, line, wrap_width)?; - } - } else { - let cursor_top = if area.bottom() < screen_size.height { - let scroll_amount = wrapped_lines.min(screen_size.height - area.bottom()); - let top_1based = area.top() + 1; - queue!(writer, SetScrollRegion(top_1based..screen_size.height))?; - queue!(writer, MoveTo(0, area.top()))?; - for _ in 0..scroll_amount { - queue!(writer, Print("\x1bM"))?; - } queue!(writer, ResetScrollRegion)?; - - let cursor_top = area.top().saturating_sub(1); - area.y += scroll_amount; - should_update_area = true; - cursor_top - } else { - area.top().saturating_sub(1) - }; - - // Limit the scroll region to the lines from the top of the screen to the - // top of the viewport. With this in place, when we add lines inside this - // area, only the lines in this area will be scrolled. We place the cursor - // at the end of the scroll region, and add lines starting there. - // - // ┌─Screen───────────────────────┐ - // │┌╌Scroll region╌╌╌╌╌╌╌╌╌╌╌╌╌╌┐│ - // │┆ ┆│ - // │┆ ┆│ - // │┆ ┆│ - // │█╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┘│ - // │╭─Viewport───────────────────╮│ - // ││ ││ - // │╰────────────────────────────╯│ - // └──────────────────────────────┘ - queue!(writer, SetScrollRegion(1..area.top()))?; - - // NB: we are using MoveTo instead of set_cursor_position here to avoid messing with the - // terminal's last_known_cursor_position, which hopefully will still be accurate after we - // fetch/restore the cursor position. insert_history_lines should be cursor-position-neutral :) - queue!(writer, MoveTo(0, cursor_top))?; - - for line in &wrapped { - queue!(writer, Print("\r\n"))?; - write_history_line(writer, line, wrap_width)?; } - - queue!(writer, ResetScrollRegion)?; } // Restore the cursor position to where it was before we started. @@ -857,8 +876,12 @@ mod tests { let lines: Vec> = (0..12) .map(|idx| Line::from(format!("reflow line {idx:02}"))) .collect(); - insert_history_lines_with_mode(&mut term, lines, InsertHistoryMode::FullScreenReplay) - .expect("insert reflow history"); + insert_history_lines_with_mode( + &mut term, + lines, + InsertHistoryMode::FullScreenReplayPrefill, + ) + .expect("insert reflow history"); let start_row = 0; let rows: Vec = term diff --git a/codex-rs/tui/src/tui.rs b/codex-rs/tui/src/tui.rs index 3331a0eb02..4447ebd1e7 100644 --- a/codex-rs/tui/src/tui.rs +++ b/codex-rs/tui/src/tui.rs @@ -833,7 +833,7 @@ impl Tui { crate::insert_history::insert_history_lines_with_mode( terminal, pending_reflow_history_lines.clone(), - crate::insert_history::InsertHistoryMode::FullScreenReplay, + crate::insert_history::InsertHistoryMode::FullScreenReplayPrefill, )?; pending_reflow_history_lines.clear(); Ok(true)