From c0a84473a44773e9468b15c82c57b61faf737d8f Mon Sep 17 00:00:00 2001 From: Jeremy Rose <172423086+nornagon-openai@users.noreply.github.com> Date: Thu, 2 Oct 2025 15:41:25 -0700 Subject: [PATCH] fix false "task complete" state during agent message (#4627) fixes an issue where user messages wouldn't be queued and ctrl + c would quit the app instead of canceling the stream during the final agent message. --- codex-rs/tui/src/bottom_pane/mod.rs | 9 +++++++- codex-rs/tui/src/chatwidget.rs | 4 ++-- codex-rs/tui/src/chatwidget/tests.rs | 31 ++++++++++++++++++++++++++++ 3 files changed, 41 insertions(+), 3 deletions(-) diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index 32ce90d0..88a9e88d 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -339,7 +339,14 @@ impl BottomPane { self.request_redraw(); } else { // Hide the status indicator when a task completes, but keep other modal views. - self.status = None; + self.hide_status_indicator(); + } + } + + /// Hide the status indicator while leaving task-running state untouched. + pub(crate) fn hide_status_indicator(&mut self) { + if self.status.take().is_some() { + self.request_redraw(); } } diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 1292aafd..38510c5c 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -632,7 +632,7 @@ impl ChatWidget { if let Some(controller) = self.stream_controller.as_mut() { let (cell, is_idle) = controller.on_commit_tick(); if let Some(cell) = cell { - self.bottom_pane.set_task_running(false); + self.bottom_pane.hide_status_indicator(); self.add_boxed_history(cell); } if is_idle { @@ -665,7 +665,7 @@ impl ChatWidget { fn handle_stream_finished(&mut self) { if self.task_complete_pending { - self.bottom_pane.set_task_running(false); + self.bottom_pane.hide_status_indicator(); self.task_complete_pending = false; } // A completed stream indicates non-exec content was just inserted. diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index b61d3d2e..a1623e36 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -47,6 +47,7 @@ use std::io::BufRead; use std::io::BufReader; use std::path::PathBuf; use tempfile::NamedTempFile; +use tokio::sync::mpsc::error::TryRecvError; use tokio::sync::mpsc::unbounded_channel; fn test_config() -> Config { @@ -612,6 +613,36 @@ fn alt_up_edits_most_recent_queued_message() { ); } +#[test] +fn streaming_final_answer_keeps_task_running_state() { + let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(); + + chat.on_task_started(); + chat.on_agent_message_delta("Final answer line\n".to_string()); + chat.on_commit_tick(); + + assert!(chat.bottom_pane.is_task_running()); + assert!(chat.bottom_pane.status_widget().is_none()); + + chat.bottom_pane + .set_composer_text("queued submission".to_string()); + chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); + + assert_eq!(chat.queued_user_messages.len(), 1); + assert_eq!( + chat.queued_user_messages.front().unwrap().text, + "queued submission" + ); + assert!(matches!(op_rx.try_recv(), Err(TryRecvError::Empty))); + + chat.handle_key_event(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL)); + match op_rx.try_recv() { + Ok(Op::Interrupt) => {} + other => panic!("expected Op::Interrupt, got {other:?}"), + } + assert!(chat.bottom_pane.ctrl_c_quit_hint_visible()); +} + #[test] fn exec_history_cell_shows_working_then_completed() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual();