diff --git a/codex-rs/tui/src/chatwidget/input_flow.rs b/codex-rs/tui/src/chatwidget/input_flow.rs index 1e385b3d0e..6c3555c747 100644 --- a/codex-rs/tui/src/chatwidget/input_flow.rs +++ b/codex-rs/tui/src/chatwidget/input_flow.rs @@ -36,7 +36,12 @@ impl ChatWidget { .as_ref() .is_some_and(HashSet::is_empty); if startup_ready { - self.startup_draft_pending_mcp_servers = None; + self.clear_startup_draft_protection(); + } + } + + pub(super) fn clear_startup_draft_protection(&mut self) { + if self.startup_draft_pending_mcp_servers.take().is_some() { self.bottom_pane .set_startup_draft_submission_blocked(/*blocked*/ false); } diff --git a/codex-rs/tui/src/chatwidget/interaction.rs b/codex-rs/tui/src/chatwidget/interaction.rs index ba0aeb0a91..25f031bf11 100644 --- a/codex-rs/tui/src/chatwidget/interaction.rs +++ b/codex-rs/tui/src/chatwidget/interaction.rs @@ -373,6 +373,7 @@ impl ChatWidget { fn on_ctrl_c(&mut self) { let key = key_hint::ctrl(KeyCode::Char('c')); let modal_or_popup_active = !self.bottom_pane.no_modal_or_popup_active(); + let composer_had_input = !self.bottom_pane.composer_is_empty(); let should_pause_active_goal = self .bottom_pane .active_view_will_interrupt_turn_on_key_event(KeyEvent::new( @@ -380,6 +381,9 @@ impl ChatWidget { KeyModifiers::CONTROL, )); if self.bottom_pane.on_ctrl_c() == CancellationEvent::Handled { + if composer_had_input && self.bottom_pane.composer_is_empty() { + self.clear_startup_draft_protection(); + } if DOUBLE_PRESS_QUIT_SHORTCUT_ENABLED { if modal_or_popup_active { self.quit_shortcut_expires_at = None; diff --git a/codex-rs/tui/src/chatwidget/tests/review_mode.rs b/codex-rs/tui/src/chatwidget/tests/review_mode.rs index e8fe8ed300..0d46114ae2 100644 --- a/codex-rs/tui/src/chatwidget/tests/review_mode.rs +++ b/codex-rs/tui/src/chatwidget/tests/review_mode.rs @@ -1035,6 +1035,41 @@ async fn ctrl_c_cleared_prompt_is_recoverable_via_history() { assert_eq!(vec![PathBuf::from("/tmp/preview.png")], images); } +#[tokio::test] +async fn ctrl_c_clears_startup_draft_protection_only_with_the_composer() { + let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + chat.restore_startup_draft("captured during startup"); + + chat.open_review_popup(); + chat.handle_key_event(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL)); + + assert_eq!(chat.bottom_pane.composer_text(), "captured during startup"); + assert!(chat.startup_draft_pending_mcp_servers.is_some()); + + chat.handle_key_event(KeyEvent::new(KeyCode::Char('r'), KeyModifiers::CONTROL)); + assert!(!chat.bottom_pane.no_modal_or_popup_active()); + chat.handle_key_event(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL)); + + assert_eq!(chat.bottom_pane.composer_text(), "captured during startup"); + assert!(chat.startup_draft_pending_mcp_servers.is_some()); + assert_eq!( + chat.bottom_pane + .handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)), + InputResult::None + ); + + chat.handle_key_event(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL)); + + assert!(chat.bottom_pane.composer_is_empty()); + assert!(chat.startup_draft_pending_mcp_servers.is_none()); + chat.bottom_pane.insert_str("replacement draft"); + assert_matches!( + chat.bottom_pane + .handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)), + InputResult::Submitted { .. } + ); +} + /// Selecting the custom prompt option from the review popup sends /// OpenReviewCustomPrompt to the app event channel. #[tokio::test]