diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 0084e4604f..e7526203bc 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -2852,11 +2852,11 @@ impl ChatComposer { return None; } if self.reject_slash_command_if_unavailable(cmd) { - self.stage_slash_command_history(); + self.stage_slash_command_history(cmd); self.record_pending_slash_command_history(); return Some(InputResult::None); } - self.stage_slash_command_history(); + self.stage_slash_command_history(cmd); self.textarea.set_text_clearing_elements(""); self.is_bash_mode = false; Some(InputResult::Command(cmd)) @@ -2884,12 +2884,12 @@ impl ChatComposer { return None; } if self.reject_slash_command_if_unavailable(cmd) { - self.stage_slash_command_history(); + self.stage_slash_command_history(cmd); self.record_pending_slash_command_history(); return Some(InputResult::None); } - self.stage_slash_command_history(); + self.stage_slash_command_history(cmd); let mut args_elements = Self::slash_command_args_elements(rest, rest_offset, &self.textarea.text_elements()); @@ -2965,7 +2965,10 @@ impl ChatComposer { /// Staging snapshots the rich composer state before the textarea is cleared. `ChatWidget` /// commits the staged entry after dispatch so command recall follows the submitted text, not /// the command outcome. - fn stage_slash_command_history(&mut self) { + fn stage_slash_command_history(&mut self, cmd: SlashCommand) { + if cmd == SlashCommand::Clear { + return; + } self.stage_slash_command_history_text(self.textarea.text().trim().to_string()); } @@ -2974,6 +2977,9 @@ impl ChatComposer { /// Popup filtering text can be partial, so recording the selected command avoids recalling /// `/di` after the user actually accepted `/diff`. fn stage_selected_slash_command_history(&mut self, cmd: SlashCommand) { + if cmd == SlashCommand::Clear { + return; + } self.stage_slash_command_history_text(format!("/{}", cmd.command())); } diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index fac5477924..6f804c44ff 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -17,6 +17,8 @@ use std::collections::VecDeque; use std::path::PathBuf; use crate::app::app_server_requests::ResolvedAppServerRequest; +use crate::app_command::AppCommand; +use crate::app_event::AppEvent; use crate::app_event::ConnectorsSnapshot; use crate::app_event_sender::AppEventSender; use crate::bottom_pane::pending_input_preview::PendingInputPreview; @@ -777,7 +779,10 @@ impl BottomPane { } pub(crate) fn clear_composer_for_ctrl_c(&mut self) { - self.composer.clear_for_ctrl_c(); + if let Some(text) = self.composer.clear_for_ctrl_c() { + self.app_event_tx + .send(AppEvent::CodexOp(AppCommand::add_to_history(text))); + } self.request_redraw(); } diff --git a/codex-rs/tui/src/chatwidget/tests/slash_commands.rs b/codex-rs/tui/src/chatwidget/tests/slash_commands.rs index 1778579808..1bb4684c94 100644 --- a/codex-rs/tui/src/chatwidget/tests/slash_commands.rs +++ b/codex-rs/tui/src/chatwidget/tests/slash_commands.rs @@ -1581,6 +1581,34 @@ async fn slash_clear_requests_ui_clear_when_idle() { assert_matches!(rx.try_recv(), Ok(AppEvent::ClearUi)); } +#[tokio::test] +async fn slash_clear_after_ctrl_c_keeps_stashed_draft_recallable() { + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + submit_composer_text(&mut chat, "ok"); + + let stashed_draft = "explain why history recall lost this draft"; + + chat.bottom_pane + .set_composer_text(stashed_draft.to_string(), Vec::new(), Vec::new()); + chat.handle_key_event(KeyEvent::new(KeyCode::Char('c'), KeyModifiers::CONTROL)); + assert_eq!(chat.bottom_pane.composer_text(), ""); + assert_matches!( + rx.try_recv(), + Ok(AppEvent::CodexOp(Op::AddToHistory { text })) if text == stashed_draft + ); + + chat.bottom_pane + .set_composer_text("/clear".to_string(), Vec::new(), Vec::new()); + chat.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); + + assert_matches!(rx.try_recv(), Ok(AppEvent::ClearUi)); + chat.handle_key_event(KeyEvent::new(KeyCode::Up, KeyModifiers::NONE)); + assert_eq!(chat.bottom_pane.composer_text(), stashed_draft); + + chat.handle_key_event(KeyEvent::new(KeyCode::Up, KeyModifiers::NONE)); + assert_eq!(chat.bottom_pane.composer_text(), "ok"); +} + #[tokio::test] async fn slash_clear_is_disabled_while_task_running() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;