Compare commits

...

1 Commits

Author SHA1 Message Date
Eric Traut
6acf705441 Limit side restore replay work 2026-05-02 12:58:24 -07:00
6 changed files with 77 additions and 0 deletions

View File

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

View File

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

View File

@@ -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.
@@ -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;
}
@@ -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() {

View File

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

View File

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

View File

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