diff --git a/codex-rs/tui/src/bottom_pane/bottom_pane_view.rs b/codex-rs/tui/src/bottom_pane/bottom_pane_view.rs index 8f21a242e4..794dd8c422 100644 --- a/codex-rs/tui/src/bottom_pane/bottom_pane_view.rs +++ b/codex-rs/tui/src/bottom_pane/bottom_pane_view.rs @@ -19,7 +19,7 @@ pub(crate) trait BottomPaneView { /// Handle Ctrl-C while this view is active. fn on_ctrl_c(&mut self, _pane: &mut BottomPane) -> CancellationEvent { - CancellationEvent::Ignored + CancellationEvent::NotHandled } /// Return the desired height of the view. diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index aead743d99..a8dd56b5d5 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -79,6 +79,7 @@ pub(crate) struct ChatComposer { has_focus: bool, attached_images: Vec, placeholder_text: String, + is_task_running: bool, // Non-bracketed paste burst tracker. paste_burst: PasteBurst, // When true, disables paste-burst logic and inserts characters immediately. @@ -119,6 +120,7 @@ impl ChatComposer { has_focus: has_input_focus, attached_images: Vec::new(), placeholder_text, + is_task_running: false, paste_burst: PasteBurst::default(), disable_paste_burst: false, custom_prompts: Vec::new(), @@ -1205,6 +1207,10 @@ impl ChatComposer { self.has_focus = has_focus; } + pub fn set_task_running(&mut self, running: bool) { + self.is_task_running = running; + } + pub(crate) fn set_esc_backtrack_hint(&mut self, show: bool) { self.esc_backtrack_hint = show; } @@ -1229,11 +1235,16 @@ impl WidgetRef for ChatComposer { ActivePopup::None => { let bottom_line_rect = popup_rect; let mut hint: Vec> = if self.ctrl_c_quit_hint { + let ctrl_c_followup = if self.is_task_running { + " to interrupt" + } else { + " to quit" + }; vec![ " ".into(), key_hint::ctrl('C'), " again".into(), - " to quit".into(), + ctrl_c_followup.into(), ] } else { let newline_hint_key = if self.use_shift_enter_hint { diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index 88b3f09646..8d84ccc121 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -30,8 +30,8 @@ mod textarea; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum CancellationEvent { - Ignored, Handled, + NotHandled, } pub(crate) use chat_composer::ChatComposer; @@ -195,7 +195,15 @@ impl BottomPane { pub(crate) fn on_ctrl_c(&mut self) -> CancellationEvent { let mut view = match self.active_view.take() { Some(view) => view, - None => return CancellationEvent::Ignored, + None => { + return if self.composer_is_empty() { + CancellationEvent::NotHandled + } else { + self.set_composer_text(String::new()); + self.show_ctrl_c_quit_hint(); + CancellationEvent::Handled + }; + } }; let event = view.on_ctrl_c(self); @@ -208,7 +216,7 @@ impl BottomPane { } self.show_ctrl_c_quit_hint(); } - CancellationEvent::Ignored => { + CancellationEvent::NotHandled => { self.active_view = Some(view); } } @@ -267,6 +275,7 @@ impl BottomPane { } } + #[cfg(test)] pub(crate) fn ctrl_c_quit_hint_visible(&self) -> bool { self.ctrl_c_quit_hint } @@ -289,6 +298,7 @@ impl BottomPane { pub fn set_task_running(&mut self, running: bool) { self.is_task_running = running; + self.composer.set_task_running(running); if running { if self.status.is_none() { @@ -504,7 +514,7 @@ mod tests { let tx = AppEventSender::new(tx_raw); let mut pane = BottomPane::new(BottomPaneParams { app_event_tx: tx, - frame_requester: crate::tui::FrameRequester::test_dummy(), + frame_requester: FrameRequester::test_dummy(), has_input_focus: true, enhanced_keys_supported: false, placeholder_text: "Ask Codex to do anything".to_string(), @@ -513,7 +523,7 @@ mod tests { pane.push_approval_request(exec_request()); assert_eq!(CancellationEvent::Handled, pane.on_ctrl_c()); assert!(pane.ctrl_c_quit_hint_visible()); - assert_eq!(CancellationEvent::Ignored, pane.on_ctrl_c()); + assert_eq!(CancellationEvent::NotHandled, pane.on_ctrl_c()); } // live ring removed; related tests deleted. @@ -524,7 +534,7 @@ mod tests { let tx = AppEventSender::new(tx_raw); let mut pane = BottomPane::new(BottomPaneParams { app_event_tx: tx, - frame_requester: crate::tui::FrameRequester::test_dummy(), + frame_requester: FrameRequester::test_dummy(), has_input_focus: true, enhanced_keys_supported: false, placeholder_text: "Ask Codex to do anything".to_string(), @@ -555,7 +565,7 @@ mod tests { let tx = AppEventSender::new(tx_raw); let mut pane = BottomPane::new(BottomPaneParams { app_event_tx: tx.clone(), - frame_requester: crate::tui::FrameRequester::test_dummy(), + frame_requester: FrameRequester::test_dummy(), has_input_focus: true, enhanced_keys_supported: false, placeholder_text: "Ask Codex to do anything".to_string(), @@ -583,7 +593,7 @@ mod tests { // Render and ensure the top row includes the Working header and a composer line below. // Give the animation thread a moment to tick. - std::thread::sleep(std::time::Duration::from_millis(120)); + std::thread::sleep(Duration::from_millis(120)); let area = Rect::new(0, 0, 40, 6); let mut buf = Buffer::empty(area); (&pane).render_ref(area, &mut buf); @@ -623,7 +633,7 @@ mod tests { let tx = AppEventSender::new(tx_raw); let mut pane = BottomPane::new(BottomPaneParams { app_event_tx: tx, - frame_requester: crate::tui::FrameRequester::test_dummy(), + frame_requester: FrameRequester::test_dummy(), has_input_focus: true, enhanced_keys_supported: false, placeholder_text: "Ask Codex to do anything".to_string(), @@ -654,7 +664,7 @@ mod tests { let tx = AppEventSender::new(tx_raw); let mut pane = BottomPane::new(BottomPaneParams { app_event_tx: tx, - frame_requester: crate::tui::FrameRequester::test_dummy(), + frame_requester: FrameRequester::test_dummy(), has_input_focus: true, enhanced_keys_supported: false, placeholder_text: "Ask Codex to do anything".to_string(), @@ -705,7 +715,7 @@ mod tests { let tx = AppEventSender::new(tx_raw); let mut pane = BottomPane::new(BottomPaneParams { app_event_tx: tx, - frame_requester: crate::tui::FrameRequester::test_dummy(), + frame_requester: FrameRequester::test_dummy(), has_input_focus: true, enhanced_keys_supported: false, placeholder_text: "Ask Codex to do anything".to_string(), diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index bee9d5e175..678eb1833a 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -1288,15 +1288,17 @@ impl ChatWidget { /// Handle Ctrl-C key press. fn on_ctrl_c(&mut self) { - if self.bottom_pane.on_ctrl_c() == CancellationEvent::Ignored { - if self.bottom_pane.is_task_running() { - self.submit_op(Op::Interrupt); - } else if self.bottom_pane.ctrl_c_quit_hint_visible() { - self.submit_op(Op::Shutdown); - } else { - self.bottom_pane.show_ctrl_c_quit_hint(); - } + if self.bottom_pane.on_ctrl_c() == CancellationEvent::Handled { + return; + } + + if self.bottom_pane.is_task_running() { + self.bottom_pane.show_ctrl_c_quit_hint(); + self.submit_op(Op::Interrupt); + return; } + + self.submit_op(Op::Shutdown); } pub(crate) fn composer_is_empty(&self) -> bool {