mirror of
https://github.com/openai/codex.git
synced 2026-09-08 15:50:34 +00:00
wip(tui): preserve resize reflow replay tail
This commit is contained in:
@@ -285,6 +285,21 @@ impl ChatWidget {
|
||||
self.open_memories_popup();
|
||||
}
|
||||
SlashCommand::Quit | SlashCommand::Exit => {
|
||||
let command = if matches!(cmd, SlashCommand::Quit) {
|
||||
"/quit"
|
||||
} else {
|
||||
"/exit"
|
||||
};
|
||||
self.add_to_history(history_cell::new_user_prompt(
|
||||
command.to_string(),
|
||||
/*text_elements*/ Vec::new(),
|
||||
/*local_image_paths*/ Vec::new(),
|
||||
/*remote_image_urls*/ Vec::new(),
|
||||
));
|
||||
self.add_to_history(history_cell::new_info_event(
|
||||
"Exiting.".to_string(),
|
||||
/*hint*/ None,
|
||||
));
|
||||
self.request_quit_without_confirmation();
|
||||
}
|
||||
SlashCommand::Logout => {
|
||||
|
||||
@@ -784,7 +784,28 @@ async fn slash_quit_requests_exit() {
|
||||
|
||||
chat.dispatch_command(SlashCommand::Quit);
|
||||
|
||||
assert_matches!(rx.try_recv(), Ok(AppEvent::Exit(ExitMode::ShutdownFirst)));
|
||||
let mut rendered = String::new();
|
||||
let mut saw_exit = false;
|
||||
while let Ok(event) = rx.try_recv() {
|
||||
match event {
|
||||
AppEvent::InsertHistoryCell(cell) => {
|
||||
rendered.push_str(&lines_to_single_string(&cell.display_lines(/*width*/ 80)));
|
||||
}
|
||||
AppEvent::Exit(ExitMode::ShutdownFirst) => {
|
||||
saw_exit = true;
|
||||
}
|
||||
event => panic!("unexpected event: {event:?}"),
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
rendered.contains("/quit"),
|
||||
"expected quit command in history, got: {rendered:?}"
|
||||
);
|
||||
assert!(
|
||||
rendered.contains("Exiting."),
|
||||
"expected exiting notice in history, got: {rendered:?}"
|
||||
);
|
||||
assert!(saw_exit, "expected shutdown-first exit event");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
@@ -165,6 +165,21 @@ where
|
||||
}
|
||||
write_history_line(writer, line, wrap_width)?;
|
||||
}
|
||||
|
||||
if matches!(mode, InsertHistoryMode::FullScreenReplayPrefill) {
|
||||
let reserve_rows = wrapped_lines
|
||||
.min(screen_size.height)
|
||||
.saturating_sub(area.top());
|
||||
if reserve_rows > 0 {
|
||||
queue!(
|
||||
writer,
|
||||
MoveTo(/*x*/ 0, screen_size.height.saturating_sub(1))
|
||||
)?;
|
||||
for _ in 0..reserve_rows {
|
||||
queue!(writer, Print("\n"))?;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
InsertHistoryMode::FullScreenReplayDirect => {
|
||||
// Rebuild scrollback with transcript content itself. Pre-scrolling with blank
|
||||
@@ -897,4 +912,42 @@ mod tests {
|
||||
assert_eq!(term.viewport_area, viewport);
|
||||
assert_eq!(term.visible_history_rows(), viewport.top());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn vt100_full_screen_replay_reserves_viewport_rows_for_tui() {
|
||||
let width: u16 = 32;
|
||||
let height: u16 = 8;
|
||||
let backend = VT100Backend::new(width, height);
|
||||
let mut term = crate::custom_terminal::Terminal::with_options(backend).expect("terminal");
|
||||
let viewport = Rect::new(/*x*/ 0, /*y*/ 4, width, /*height*/ 3);
|
||||
term.set_viewport_area(viewport);
|
||||
|
||||
let lines: Vec<Line<'static>> = (0..6)
|
||||
.map(|idx| Line::from(format!("reflow line {idx:02}")))
|
||||
.collect();
|
||||
insert_history_lines_with_mode(
|
||||
&mut term,
|
||||
lines,
|
||||
InsertHistoryMode::FullScreenReplayPrefill,
|
||||
)
|
||||
.expect("insert reflow history");
|
||||
|
||||
let rows: Vec<String> = term
|
||||
.backend()
|
||||
.vt100()
|
||||
.screen()
|
||||
.rows(/*start*/ 0, width)
|
||||
.collect();
|
||||
assert!(
|
||||
rows[viewport.top() as usize - 1].contains("reflow line 05"),
|
||||
"expected replayed tail immediately above viewport, rows: {rows:?}"
|
||||
);
|
||||
assert!(
|
||||
rows[viewport.top() as usize..]
|
||||
.iter()
|
||||
.all(|row| !row.contains("reflow line")),
|
||||
"expected replay not to occupy reserved viewport rows, rows: {rows:?}"
|
||||
);
|
||||
assert_eq!(term.viewport_area, viewport);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,8 +175,10 @@ fn should_emit_notification(condition: NotificationCondition, terminal_focused:
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::ResizeReflowDrawRefresh;
|
||||
use super::keyboard_enhancement_disabled_for;
|
||||
use super::parse_bool_env;
|
||||
use super::resize_reflow_draw_refresh;
|
||||
use super::should_emit_notification;
|
||||
use super::vscode_terminal_detected;
|
||||
use codex_config::types::NotificationCondition;
|
||||
@@ -266,6 +268,32 @@ mod tests {
|
||||
/*linux_term_program*/ None, /*windows_term_program*/ None
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resize_reflow_replay_invalidates_without_clearing_after_insert() {
|
||||
assert_eq!(
|
||||
resize_reflow_draw_refresh(
|
||||
/*needs_full_repaint*/ true, /*flushed_reflow_history*/ true
|
||||
),
|
||||
ResizeReflowDrawRefresh {
|
||||
clear_after_history_flush: false,
|
||||
invalidate_viewport: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resize_reflow_draw_refresh_preserves_legacy_clear_without_replay() {
|
||||
assert_eq!(
|
||||
resize_reflow_draw_refresh(
|
||||
/*needs_full_repaint*/ true, /*flushed_reflow_history*/ false
|
||||
),
|
||||
ResizeReflowDrawRefresh {
|
||||
clear_after_history_flush: true,
|
||||
invalidate_viewport: true,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_modes() -> Result<()> {
|
||||
@@ -481,6 +509,24 @@ pub struct Tui {
|
||||
alt_screen_enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
struct ResizeReflowDrawRefresh {
|
||||
clear_after_history_flush: bool,
|
||||
invalidate_viewport: bool,
|
||||
}
|
||||
|
||||
fn resize_reflow_draw_refresh(
|
||||
needs_full_repaint: bool,
|
||||
flushed_reflow_history: bool,
|
||||
) -> ResizeReflowDrawRefresh {
|
||||
ResizeReflowDrawRefresh {
|
||||
// Resize reflow clears the terminal before queueing rebuilt rows. Clearing again after
|
||||
// replay can erase the source-backed tail rows that were just inserted.
|
||||
clear_after_history_flush: needs_full_repaint && !flushed_reflow_history,
|
||||
invalidate_viewport: needs_full_repaint,
|
||||
}
|
||||
}
|
||||
|
||||
impl Tui {
|
||||
pub fn new(terminal: Terminal) -> Self {
|
||||
let (draw_tx, _) = broadcast::channel(1);
|
||||
@@ -937,8 +983,11 @@ impl Tui {
|
||||
)?;
|
||||
needs_full_repaint |= flushed_reflow_history;
|
||||
|
||||
if needs_full_repaint {
|
||||
let refresh = resize_reflow_draw_refresh(needs_full_repaint, flushed_reflow_history);
|
||||
if refresh.clear_after_history_flush {
|
||||
terminal.clear()?;
|
||||
}
|
||||
if refresh.invalidate_viewport {
|
||||
terminal.invalidate_viewport();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user