mirror of
https://github.com/openai/codex.git
synced 2026-10-20 00:18:49 +00:00
Preserve streamed output during capped history replay (#33093)
## Why Stream consolidation can replace transcript cells while initial history replay is buffering capped display rows, leaving those rows stale and omitting the finalized output. ## What changed - Discard buffered rows when required stream reflow occurs during capped initial replay and rebuild the capped tail from the canonical transcript after replay finishes. - Schedule the rebuild through the resize reflow path so it waits for an active transcript overlay to close. ## Testing Added coverage for consolidated Markdown output during capped initial replay, both with and without an active transcript overlay. GitOrigin-RevId: 1c184ff561a50362daf11c1c629be59882f27a4a
This commit is contained in:
@@ -144,17 +144,14 @@ impl App {
|
||||
return;
|
||||
};
|
||||
|
||||
if buffer.render_from_transcript_tail || self.overlay.is_some() {
|
||||
// Reflow clears any pre-replay or partially emitted history and applies the reserved
|
||||
// history width. It also waits for an active overlay to close before rebuilding.
|
||||
self.schedule_immediate_resize_reflow(tui);
|
||||
return;
|
||||
}
|
||||
|
||||
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_hyperlink_lines_with_wrap_policy(
|
||||
reflowed_lines,
|
||||
self.history_line_wrap_policy(),
|
||||
);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -274,6 +271,19 @@ impl App {
|
||||
/// replaced as one styled source-backed cell. If this reflow is skipped after a stream-time
|
||||
/// resize, the visible scrollback can keep the pre-consolidation wrapping.
|
||||
pub(super) fn finish_required_stream_reflow(&mut self, tui: &mut tui::Tui) -> Result<()> {
|
||||
// Capped initial replay normally buffers per-cell display rows. A live stream tail is
|
||||
// consolidated directly into `transcript_cells`, so any retained rows no longer describe
|
||||
// the canonical transcript. Let the replay-end event render the capped transcript tail
|
||||
// once all consolidation events have been processed.
|
||||
if self.resize_reflow_max_rows().is_some()
|
||||
&& let Some(buffer) = self.initial_history_replay_buffer.as_mut()
|
||||
{
|
||||
buffer.retained_lines.clear();
|
||||
buffer.render_from_transcript_tail = true;
|
||||
self.transcript_reflow.clear_stream_flags();
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
self.schedule_immediate_resize_reflow(tui);
|
||||
self.maybe_run_resize_reflow(tui)?;
|
||||
if !self.transcript_reflow.has_pending_reflow() {
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
---
|
||||
source: tui/src/app/tests.rs
|
||||
expression: "rendered.lines.iter().map(rendered_line_text).collect::<Vec<_>>().join(\"\\n\")"
|
||||
---
|
||||
latest user question
|
||||
|
||||
• Final answer:
|
||||
|
||||
Pattern Outcome
|
||||
━━━━━━━━━━━━ ━━━━━━━━━━━
|
||||
Table tail Preserved
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
---
|
||||
source: tui/src/app/tests.rs
|
||||
expression: "rendered.lines.iter().map(rendered_line_text).collect::<Vec<_>>().join(\"\\n\")"
|
||||
---
|
||||
latest user question
|
||||
|
||||
• Final answer:
|
||||
|
||||
Pattern Outcome
|
||||
━━━━━━━━━━━━ ━━━━━━━━━━━
|
||||
Table tail Preserved
|
||||
@@ -31,6 +31,7 @@ use crate::multi_agents::SubAgentActivityDisplay;
|
||||
use assert_matches::assert_matches;
|
||||
|
||||
use crate::app_command::AppCommand as Op;
|
||||
use crate::app_event::ConsolidationScrollbackReflow;
|
||||
use crate::diff_model::FileChange;
|
||||
use crate::legacy_core::config::ConfigBuilder;
|
||||
use crate::legacy_core::config::ConfigOverrides;
|
||||
@@ -4561,6 +4562,119 @@ async fn initial_replay_buffer_keeps_recent_rows_when_row_cap_present() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn required_stream_reflow_during_capped_initial_replay_uses_transcript_tail() -> Result<()> {
|
||||
let (mut app, _rx, _op_rx) = make_test_app_with_channels().await;
|
||||
app.config.terminal_resize_reflow.max_rows = TerminalResizeReflowMaxRows::Limit(20);
|
||||
app.transcript_cells = vec![
|
||||
plain_line_cell("latest user question"),
|
||||
Arc::new(AgentMarkdownCell::new(
|
||||
"Final answer:\n\n| Pattern | Outcome |\n| --- | --- |\n| Table tail | Preserved |"
|
||||
.to_string(),
|
||||
Path::new("/tmp"),
|
||||
)),
|
||||
];
|
||||
|
||||
app.begin_initial_history_replay_buffer();
|
||||
App::buffer_initial_history_replay_display_lines(
|
||||
app.initial_history_replay_buffer
|
||||
.as_mut()
|
||||
.expect("initial replay buffer active"),
|
||||
vec![Line::from("latest user question").into()],
|
||||
/*max_rows*/ 20,
|
||||
);
|
||||
|
||||
let mut tui = crate::tui::test_support::make_test_tui()?;
|
||||
app.finish_required_stream_reflow(&mut tui)?;
|
||||
|
||||
let buffer = app
|
||||
.initial_history_replay_buffer
|
||||
.as_ref()
|
||||
.expect("initial replay buffer should remain active");
|
||||
assert_eq!(
|
||||
(
|
||||
buffer.retained_lines.len(),
|
||||
buffer.render_from_transcript_tail
|
||||
),
|
||||
(0, true),
|
||||
);
|
||||
|
||||
let rendered = app.render_transcript_lines_for_reflow(/*width*/ 80);
|
||||
assert_snapshot!(
|
||||
"required_stream_reflow_during_capped_initial_replay",
|
||||
rendered
|
||||
.lines
|
||||
.iter()
|
||||
.map(rendered_line_text)
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
);
|
||||
|
||||
app.finish_initial_history_replay_buffer(&mut tui);
|
||||
assert!(app.initial_history_replay_buffer.is_none());
|
||||
assert!(app.transcript_reflow.has_pending_reflow());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn required_stream_reflow_during_capped_initial_replay_survives_transcript_overlay()
|
||||
-> Result<()> {
|
||||
let (mut app, _rx, _op_rx) = make_test_app_with_channels().await;
|
||||
app.config.terminal_resize_reflow.max_rows = TerminalResizeReflowMaxRows::Limit(7);
|
||||
app.transcript_cells = vec![
|
||||
plain_line_cell("latest user question"),
|
||||
Arc::new(AgentMessageCell::new(
|
||||
vec![Line::from("stale streamed table tail")],
|
||||
/*is_first_line*/ true,
|
||||
)),
|
||||
];
|
||||
|
||||
app.begin_initial_history_replay_buffer();
|
||||
App::buffer_initial_history_replay_display_lines(
|
||||
app.initial_history_replay_buffer
|
||||
.as_mut()
|
||||
.expect("initial replay buffer active"),
|
||||
vec![Line::from("stale streamed table tail").into()],
|
||||
/*max_rows*/ 7,
|
||||
);
|
||||
|
||||
let mut tui = crate::tui::test_support::make_test_tui()?;
|
||||
app.handle_consolidate_agent_message(
|
||||
&mut tui,
|
||||
"Final answer:\n\n| Pattern | Outcome |\n| --- | --- |\n| Table tail | Preserved |"
|
||||
.to_string(),
|
||||
PathBuf::from("/tmp"),
|
||||
ConsolidationScrollbackReflow::Required,
|
||||
/*deferred_history_cell*/ None,
|
||||
)?;
|
||||
app.open_transcript_overlay(&mut tui);
|
||||
assert!(tui.is_alt_screen_active());
|
||||
|
||||
app.finish_initial_history_replay_buffer(&mut tui);
|
||||
assert!(app.initial_history_replay_buffer.is_none());
|
||||
assert!(app.transcript_reflow.has_pending_reflow());
|
||||
|
||||
app.maybe_run_resize_reflow(&mut tui)?;
|
||||
assert!(app.transcript_reflow.has_pending_reflow());
|
||||
|
||||
app.close_transcript_overlay(&mut tui);
|
||||
assert!(!tui.is_alt_screen_active());
|
||||
assert!(app.transcript_reflow.has_pending_reflow());
|
||||
|
||||
let rendered = app.render_transcript_lines_for_reflow(/*width*/ 80);
|
||||
assert_eq!(rendered.lines.len(), 7);
|
||||
assert_snapshot!(
|
||||
"required_stream_reflow_during_capped_initial_replay_survives_transcript_overlay",
|
||||
rendered
|
||||
.lines
|
||||
.iter()
|
||||
.map(rendered_line_text)
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[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;
|
||||
|
||||
Reference in New Issue
Block a user