diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index ad30dedf5a..a5d9c9d993 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -1298,6 +1298,14 @@ impl BottomPane { } pub(crate) fn should_interrupt_running_task(&self, key_event: KeyEvent) -> bool { + self.should_interrupt_running_task_allowing_composer_popup(key_event) + && !self.composer.popup_active() + } + + pub(crate) fn should_interrupt_running_task_allowing_composer_popup( + &self, + key_event: KeyEvent, + ) -> bool { let is_agent_command = self .composer_text() .lines() @@ -1308,7 +1316,7 @@ impl BottomPane { self.keymap.chat.interrupt_turn.is_pressed(key_event) && self.is_task_running && !(is_agent_command && key_event.code == KeyCode::Esc) - && self.no_modal_or_popup_active() + && self.view_stack.is_empty() && !self.composer_should_handle_vim_insert_escape(key_event) && self.status.is_some() } diff --git a/codex-rs/tui/src/chatwidget/interaction.rs b/codex-rs/tui/src/chatwidget/interaction.rs index 27c228eafd..6b3147c107 100644 --- a/codex-rs/tui/src/chatwidget/interaction.rs +++ b/codex-rs/tui/src/chatwidget/interaction.rs @@ -159,7 +159,9 @@ impl ChatWidget { // hint authoritative even after typing opens a composer popup such as slash completion. if self.mcp_startup_status.is_some() && !self.turn_lifecycle.agent_turn_running - && self.chat_keymap.interrupt_turn.is_pressed(key_event) + && self + .bottom_pane + .should_interrupt_running_task_allowing_composer_popup(key_event) { self.submit_op(AppCommand::interrupt()); return; diff --git a/codex-rs/tui/src/chatwidget/tests/mcp_startup.rs b/codex-rs/tui/src/chatwidget/tests/mcp_startup.rs index 832deff55f..48a02b1957 100644 --- a/codex-rs/tui/src/chatwidget/tests/mcp_startup.rs +++ b/codex-rs/tui/src/chatwidget/tests/mcp_startup.rs @@ -138,6 +138,35 @@ async fn esc_interrupts_mcp_startup_while_slash_popup_is_open() { assert_eq!(chat.bottom_pane.composer_text(), "/review"); } +#[tokio::test] +async fn esc_during_mcp_startup_leaves_vim_insert_mode_without_interrupting() { + let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + let esc = KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE); + notify_mcp_status(&mut chat, "alpha", McpServerStartupState::Starting); + chat.toggle_vim_mode_and_notify(); + chat.handle_key_event(KeyEvent::new(KeyCode::Char('i'), KeyModifiers::NONE)); + assert!(chat.should_handle_vim_insert_escape(esc)); + + chat.handle_key_event(esc); + + assert!(!chat.should_handle_vim_insert_escape(esc)); + assert!(op_rx.try_recv().is_err()); +} + +#[tokio::test] +async fn esc_during_mcp_startup_does_not_interrupt_agent_draft() { + let (mut chat, _rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + notify_mcp_status(&mut chat, "alpha", McpServerStartupState::Starting); + chat.bottom_pane + .set_composer_text("/agent ".to_string(), Vec::new(), Vec::new()); + assert!(chat.bottom_pane.no_modal_or_popup_active()); + + chat.handle_key_event(KeyEvent::new(KeyCode::Esc, KeyModifiers::NONE)); + + assert!(op_rx.try_recv().is_err()); + assert_eq!(chat.bottom_pane.composer_text(), "/agent "); +} + #[tokio::test] async fn mcp_startup_complete_does_not_clear_running_task() { let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;