From ed087e1fa449a9cc4ccb0cfc950578049f1a1f87 Mon Sep 17 00:00:00 2001 From: Felipe Coury Date: Mon, 16 Feb 2026 23:29:51 -0300 Subject: [PATCH] fix(tui): avoid committing transient stream tail cells --- codex-rs/tui/src/chatwidget.rs | 15 ++++-- .../src/chatwidget/tests/status_and_layout.rs | 47 +++++++++++++++++++ 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index c80c3fcb6c..48b801f98d 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -5567,12 +5567,17 @@ impl ChatWidget { } fn flush_active_cell(&mut self) { - if self.stream_controller.is_some() - && self - .active_cell - .as_ref() - .is_some_and(|cell| cell.as_any().is::()) + if self + .active_cell + .as_ref() + .is_some_and(|cell| cell.as_any().is::()) { + if self.stream_controller.is_some() { + return; + } + // If stream cleanup already cleared the controller, drop the transient tail instead + // of committing preview-only content to transcript history. + self.active_cell.take(); return; } diff --git a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs index 503e9d7375..2bc75f67ee 100644 --- a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs +++ b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs @@ -126,6 +126,53 @@ async fn current_stream_width_returns_none_when_reserved_columns_exhaust_width() assert_eq!(chat.current_stream_width(4), Some(1)); } +#[tokio::test] +async fn add_to_history_does_not_commit_transient_stream_tail_after_controller_clear() { + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await; + + chat.active_cell = Some(Box::new( + crate::history_cell::StreamingAgentTailCell::new( + vec![ratatui::text::Line::from("transient table tail preview")], + true, + ), + )); + // Interrupt/error cleanup paths clear the controller before appending history. + chat.stream_controller = None; + + chat.add_to_history(crate::history_cell::new_error_event( + "stream interrupted".to_string(), + )); + + let mut inserted_count = 0usize; + let mut saw_transient_tail = false; + let mut saw_error = false; + while let Ok(event) = rx.try_recv() { + if let AppEvent::InsertHistoryCell(cell) = event { + inserted_count += 1; + if cell + .as_any() + .is::() + { + saw_transient_tail = true; + } + let rendered = lines_to_single_string(&cell.display_lines(80)); + if rendered.contains("stream interrupted") { + saw_error = true; + } + } + } + + assert!(saw_error, "expected error history cell to be emitted"); + assert!( + !saw_transient_tail, + "did not expect transient stream-tail cell to be committed", + ); + assert_eq!( + inserted_count, 1, + "expected only one committed history cell after cleanup" + ); +} + #[tokio::test] async fn helpers_are_available_and_do_not_panic() { let (tx_raw, _rx) = unbounded_channel::();