From 4795329ff3ca3ea5bdcc5e14a66f548abb21762f Mon Sep 17 00:00:00 2001 From: Felipe Coury Date: Thu, 9 Jul 2026 14:08:10 -0300 Subject: [PATCH] fix(tui): interrupt pending turns before quitting --- codex-rs/tui/src/chatwidget/interaction.rs | 5 +++-- .../src/chatwidget/tests/composer_submission.rs | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/codex-rs/tui/src/chatwidget/interaction.rs b/codex-rs/tui/src/chatwidget/interaction.rs index ba0aeb0a91..48b0a488d7 100644 --- a/codex-rs/tui/src/chatwidget/interaction.rs +++ b/codex-rs/tui/src/chatwidget/interaction.rs @@ -480,9 +480,10 @@ impl ChatWidget { self.bottom_pane.show_quit_shortcut_hint(key); } - // Review mode counts as cancellable work so Ctrl+C interrupts instead of quitting. + // A submitted turn waiting for TurnStarted is cancellable too; otherwise an immediate Ctrl+C + // is misclassified as an idle quit. fn is_cancellable_work_active(&self) -> bool { - self.bottom_pane.is_task_running() || self.review.is_review_mode + self.is_user_turn_pending_or_running() || self.review.is_review_mode } fn pause_active_goal_for_interrupt(&self) { diff --git a/codex-rs/tui/src/chatwidget/tests/composer_submission.rs b/codex-rs/tui/src/chatwidget/tests/composer_submission.rs index 238bc3609c..f54cec30fb 100644 --- a/codex-rs/tui/src/chatwidget/tests/composer_submission.rs +++ b/codex-rs/tui/src/chatwidget/tests/composer_submission.rs @@ -991,6 +991,23 @@ async fn patch_activity_prevents_cancelled_turn_prompt_restore() { } } +#[tokio::test] +async fn ctrl_c_interrupts_turn_pending_start_instead_of_quitting() { + let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + chat.thread_id = Some(ThreadId::new()); + chat.submit_user_message(UserMessage::from("commit")); + assert_matches!(next_submit_op(&mut op_rx), Op::UserTurn { .. }); + assert!(chat.input_queue.user_turn_pending_start); + assert!(!chat.bottom_pane.is_task_running()); + + chat.handle_key_event(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL)); + + next_interrupt_op(&mut op_rx); + while let Ok(event) = rx.try_recv() { + assert!(!matches!(event, AppEvent::Exit(_))); + } +} + #[tokio::test] async fn pending_steer_esc_does_not_steal_vim_insert_escape() { let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;