diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index b1577bc170..e1e7196fd2 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -1711,7 +1711,7 @@ impl ChatWidget { } } self.adaptive_chunking.reset(); - if had_stream_controller { + if had_stream_controller && self.stream_controllers_idle() { self.app_event_tx.send(AppEvent::StopCommitAnimation); } } 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 76e0e34bc2..f87ff63a96 100644 --- a/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs +++ b/codex-rs/tui/src/chatwidget/tests/status_and_layout.rs @@ -214,6 +214,41 @@ async fn on_error_does_not_persist_transient_stream_tail_during_finalize_turn() ); } +#[tokio::test] +async fn flush_answer_stream_does_not_stop_animation_while_plan_stream_is_active() { + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await; + let cwd = chat.config.cwd.to_path_buf(); + + let mut plan_controller = + crate::streaming::controller::PlanStreamController::new(Some(80), cwd.as_path()); + assert!(plan_controller.push("- Step 1\n")); + assert!( + plan_controller.queued_lines() > 0, + "expected plan stream to have queued lines for this repro", + ); + chat.plan_stream_controller = Some(plan_controller); + chat.stream_controller = Some(crate::streaming::controller::StreamController::new( + Some(80), + cwd.as_path(), + )); + + while rx.try_recv().is_ok() {} + + chat.flush_answer_stream_with_separator(); + + let mut saw_stop = false; + while let Ok(event) = rx.try_recv() { + if matches!(event, AppEvent::StopCommitAnimation) { + saw_stop = true; + } + } + + assert!( + !saw_stop, + "did not expect StopCommitAnimation while plan stream still has queued lines", + ); +} + #[tokio::test] async fn helpers_are_available_and_do_not_panic() { let (tx_raw, _rx) = unbounded_channel::();