diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 0d1395d5..39a170ce 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -316,6 +316,11 @@ impl ChatComposer { self.sync_file_search_popup(); } + pub(crate) fn clear_for_ctrl_c(&mut self) { + self.set_text_content(String::new()); + self.history.reset_navigation(); + } + /// Get the current composer text. pub(crate) fn current_text(&self) -> String { self.textarea.text().to_string() diff --git a/codex-rs/tui/src/bottom_pane/chat_composer_history.rs b/codex-rs/tui/src/bottom_pane/chat_composer_history.rs index 07e3df7c..991283a5 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer_history.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer_history.rs @@ -70,6 +70,12 @@ impl ChatComposerHistory { self.local_history.push(text.to_string()); } + /// Reset navigation tracking so the next Up key resumes from the latest entry. + pub fn reset_navigation(&mut self) { + self.history_cursor = None; + self.last_history_text = None; + } + /// Should Up/Down key presses be interpreted as history navigation given /// the current content and cursor position of `textarea`? pub fn should_handle_navigation(&self, text: &str, cursor: usize) -> bool { @@ -271,4 +277,24 @@ mod tests { history.on_entry_response(1, 1, Some("older".into())) ); } + + #[test] + fn reset_navigation_resets_cursor() { + let (tx, _rx) = unbounded_channel::(); + let tx = AppEventSender::new(tx); + + let mut history = ChatComposerHistory::new(); + history.set_metadata(1, 3); + history.fetched_history.insert(1, "command2".into()); + history.fetched_history.insert(2, "command3".into()); + + assert_eq!(Some("command3".into()), history.navigate_up(&tx)); + assert_eq!(Some("command2".into()), history.navigate_up(&tx)); + + history.reset_navigation(); + assert!(history.history_cursor.is_none()); + assert!(history.last_history_text.is_none()); + + assert_eq!(Some("command3".into()), history.navigate_up(&tx)); + } } diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index db13a041..d8c3265f 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -236,7 +236,7 @@ impl BottomPane { CancellationEvent::NotHandled } else { self.view_stack.pop(); - self.set_composer_text(String::new()); + self.clear_composer_for_ctrl_c(); self.show_ctrl_c_quit_hint(); CancellationEvent::Handled } @@ -270,6 +270,11 @@ impl BottomPane { self.request_redraw(); } + pub(crate) fn clear_composer_for_ctrl_c(&mut self) { + self.composer.clear_for_ctrl_c(); + self.request_redraw(); + } + /// Get the current composer text (for tests and programmatic checks). pub(crate) fn composer_text(&self) -> String { self.composer.current_text()