Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions codex-rs/tui/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,7 @@ struct SessionSummary {
#[derive(Debug, Default)]
struct InitialHistoryReplayBuffer {
retained_lines: VecDeque<Line<'static>>,
render_from_transcript_tail: bool,
}

pub(crate) struct App {
Expand Down
3 changes: 3 additions & 0 deletions codex-rs/tui/src/app/event_dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,9 @@ impl App {
AppEvent::BeginInitialHistoryReplayBuffer => {
self.begin_initial_history_replay_buffer();
}
AppEvent::BeginThreadSwitchHistoryReplayBuffer => {
self.begin_thread_switch_history_replay_buffer();
}
AppEvent::InsertHistoryCell(cell) => {
let cell: Arc<dyn HistoryCell> = cell.into();
if let Some(Overlay::Transcript(t)) = &mut self.overlay {
Expand Down
32 changes: 32 additions & 0 deletions codex-rs/tui/src/app/resize_reflow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,23 @@ impl App {
}
}

/// Start retaining a thread-switch transcript replay without rendering each historical cell.
///
/// Thread switches already rebuild `transcript_cells` from source. When a row cap exists, we can
/// defer terminal writes until the replay is complete and reuse the resize-reflow tail renderer
/// so only the rows the terminal would retain are formatted and inserted.
pub(super) fn begin_thread_switch_history_replay_buffer(&mut self) {
if self.terminal_resize_reflow_enabled()
&& self.resize_reflow_max_rows().is_some()
&& self.overlay.is_none()
{
self.initial_history_replay_buffer = Some(InitialHistoryReplayBuffer {
retained_lines: VecDeque::new(),
render_from_transcript_tail: true,
});
}
}

/// Flush retained initial resume replay rows into terminal scrollback.
///
/// The buffer stores display lines, not cells, because the cap is measured in terminal rows.
Expand All @@ -130,6 +147,13 @@ impl App {
};

if buffer.retained_lines.is_empty() {
if buffer.render_from_transcript_tail {
let width = tui.terminal.last_known_screen_size.width;
let reflowed_lines = self.render_transcript_lines_for_reflow(width).lines;
if !reflowed_lines.is_empty() {
tui.insert_history_lines(reflowed_lines);
}
}
return;
}

Expand All @@ -143,6 +167,14 @@ impl App {
cell: &dyn HistoryCell,
width: u16,
) {
if self
.initial_history_replay_buffer
.as_ref()
.is_some_and(|buffer| buffer.render_from_transcript_tail)
{
return;
}

let display = self.display_lines_for_history_insert(cell, width);

if display.is_empty() {
Expand Down
27 changes: 27 additions & 0 deletions codex-rs/tui/src/app/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3986,6 +3986,33 @@ async fn initial_replay_buffer_keeps_recent_rows_when_row_cap_present() {
);
}

#[tokio::test]
async fn thread_switch_replay_buffer_uses_transcript_tail_mode_when_row_cap_present() {
let (mut app, _rx, _op_rx) = make_test_app_with_channels().await;
enable_terminal_resize_reflow(&mut app);
app.config.terminal_resize_reflow.max_rows = TerminalResizeReflowMaxRows::Limit(3);

app.begin_thread_switch_history_replay_buffer();

let buffer = app
.initial_history_replay_buffer
.as_ref()
.expect("thread switch replay buffer should be active");
assert!(buffer.render_from_transcript_tail);
assert!(buffer.retained_lines.is_empty());
}

#[tokio::test]
async fn thread_switch_replay_buffer_is_disabled_without_row_cap() {
let (mut app, _rx, _op_rx) = make_test_app_with_channels().await;
enable_terminal_resize_reflow(&mut app);
app.config.terminal_resize_reflow.max_rows = TerminalResizeReflowMaxRows::Disabled;

app.begin_thread_switch_history_replay_buffer();

assert!(app.initial_history_replay_buffer.is_none());
}

#[tokio::test]
async fn height_shrink_schedules_resize_reflow() {
let (mut app, _rx, _op_rx) = make_test_app_with_channels().await;
Expand Down
10 changes: 10 additions & 0 deletions codex-rs/tui/src/app/thread_routing.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1238,6 +1238,12 @@ impl App {
snapshot: ThreadEventSnapshot,
resume_restored_queue: bool,
) {
let should_buffer_replay = self.terminal_resize_reflow_enabled()
&& (!snapshot.turns.is_empty() || !snapshot.events.is_empty());
if should_buffer_replay {
self.app_event_tx
.send(AppEvent::BeginThreadSwitchHistoryReplayBuffer);
}
let suppress_replay_notices =
replay_filter::snapshot_has_pending_interactive_request(&snapshot);
if let Some(session) = snapshot.session {
Expand All @@ -1263,6 +1269,10 @@ impl App {
}
self.handle_thread_event_replay(event);
}
if should_buffer_replay {
self.app_event_tx
.send(AppEvent::EndInitialHistoryReplayBuffer);
}
self.chat_widget
.set_queue_autosend_suppressed(/*suppressed*/ false);
self.chat_widget
Expand Down
4 changes: 4 additions & 0 deletions codex-rs/tui/src/app_event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,10 @@ pub(crate) enum AppEvent {
/// Begin buffering initial resume replay rows before they are written to scrollback.
BeginInitialHistoryReplayBuffer,

/// Begin buffering thread-switch replay cells so the final scrollback write can reuse the
/// resize-reflow tail renderer.
BeginThreadSwitchHistoryReplayBuffer,

InsertHistoryCell(Box<dyn HistoryCell>),

/// Finish buffering initial resume replay after all replay events have been queued.
Expand Down
Loading