diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 9b67e29b5..1a0e39d9c 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -1160,7 +1160,7 @@ impl ChatComposer { /// Convert canonical composer text into the textarea's internal representation. /// - /// Bash mode stores the leading `!` as prompt state instead of editable text, + /// Shell mode stores the leading `!` as prompt state instead of editable text, /// so full-buffer imports must absorb that prefix before rebuilding the textarea. fn imported_text_for_textarea( &mut self, @@ -2934,7 +2934,7 @@ impl ChatComposer { fn shell_mode_footer_line(&self) -> Option> { self.is_bang_shell_command() .then_some(()) - .map(|_| Line::from(vec![Span::from("Bash mode").light_red()])) + .map(|_| Line::from(vec![Span::from("Shell mode").light_red()])) } /// Applies any due `PasteBurst` flush at time `now`. @@ -4660,7 +4660,7 @@ mod tests { } #[test] - fn shell_command_uses_bash_accent_style() { + fn shell_command_uses_shell_accent_style() { let (tx, _rx) = unbounded_channel::(); let sender = AppEventSender::new(tx); let mut composer = ChatComposer::new( @@ -4688,11 +4688,11 @@ mod tests { let footer_text = (0..area.width) .map(|x| buf[(x, footer_y)].symbol().chars().next().unwrap_or(' ')) .collect::(); - let bash_label_x = footer_text - .find("Bash mode") - .expect("expected bash mode footer label"); + let shell_label_x = footer_text + .find("Shell mode") + .expect("expected shell mode footer label"); assert_eq!( - buf[(bash_label_x as u16, footer_y)].style().fg, + buf[(shell_label_x as u16, footer_y)].style().fg, Some(Color::LightRed) ); } diff --git a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_shell_command_absorbs_bang.snap b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_shell_command_absorbs_bang.snap index 2b44670df..0b09860b5 100644 --- a/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_shell_command_absorbs_bang.snap +++ b/codex-rs/tui/src/bottom_pane/snapshots/codex_tui__bottom_pane__chat_composer__tests__footer_mode_shell_command_absorbs_bang.snap @@ -10,4 +10,4 @@ expression: terminal.backend() " " " " " " -" gpt-5.4 high fast · ~/code/codex-1 · Context 0% used Bash mode " +" gpt-5.4 high fast · ~/code/codex-1 · Context 0% used Shell mode " diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 1c08c7748..f5454c2bf 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -6151,9 +6151,26 @@ impl ChatWidget { } } + fn submit_shell_command_with_history( + &mut self, + command: &str, + history_text: &str, + ) -> QueueDrain { + let drain = self.submit_shell_command(command); + if drain == QueueDrain::Stop { + self.submit_op(Op::AddToHistory { + text: history_text.to_string(), + }); + } + drain + } + fn submit_queued_shell_prompt(&mut self, user_message: UserMessage) -> QueueDrain { match user_message.text.strip_prefix('!') { - Some(command) => self.submit_shell_command(command), + Some(command) => { + let history_text = user_message.text.clone(); + self.submit_shell_command_with_history(command, &history_text) + } None => { self.submit_user_message(user_message); QueueDrain::Stop @@ -6249,7 +6266,7 @@ impl ChatWidget { if shell_escape_policy == ShellEscapePolicy::Allow && let Some(stripped) = text.strip_prefix('!') { - let app_command = match self.submit_shell_command(stripped) { + let app_command = match self.submit_shell_command_with_history(stripped, &text) { QueueDrain::Continue => None, QueueDrain::Stop => Some(AppCommand::run_user_shell_command( stripped.trim().to_string(), diff --git a/codex-rs/tui/src/chatwidget/tests/exec_flow.rs b/codex-rs/tui/src/chatwidget/tests/exec_flow.rs index 206564d06..84d7cd721 100644 --- a/codex-rs/tui/src/chatwidget/tests/exec_flow.rs +++ b/codex-rs/tui/src/chatwidget/tests/exec_flow.rs @@ -1038,6 +1038,10 @@ async fn bang_shell_enter_while_task_running_submits_run_user_shell_command() { Ok(Op::RunUserShellCommand { command }) => assert_eq!(command, "echo hi"), other => panic!("expected RunUserShellCommand op, got {other:?}"), } + assert_matches!( + op_rx.try_recv(), + Ok(Op::AddToHistory { text }) if text == "!echo hi" + ); assert_matches!(rx.try_recv(), Err(TryRecvError::Empty)); } diff --git a/codex-rs/tui/src/chatwidget/tests/slash_commands.rs b/codex-rs/tui/src/chatwidget/tests/slash_commands.rs index 6da41423f..f88846be9 100644 --- a/codex-rs/tui/src/chatwidget/tests/slash_commands.rs +++ b/codex-rs/tui/src/chatwidget/tests/slash_commands.rs @@ -210,6 +210,10 @@ async fn queued_bang_shell_dispatches_after_active_turn() { Ok(Op::RunUserShellCommand { command }) => assert_eq!(command, "echo hi"), other => panic!("expected queued shell command op, got {other:?}"), } + assert_matches!( + op_rx.try_recv(), + Ok(Op::AddToHistory { text }) if text == "!echo hi" + ); assert!(chat.queued_user_messages.is_empty()); } @@ -287,7 +291,10 @@ async fn queued_bang_shell_waits_for_user_shell_completion_before_next_input() { Ok(Op::RunUserShellCommand { command }) => assert_eq!(command, "echo hi"), other => panic!("expected queued shell command op, got {other:?}"), } - assert_matches!(op_rx.try_recv(), Err(TryRecvError::Empty)); + assert_matches!( + op_rx.try_recv(), + Ok(Op::AddToHistory { text }) if text == "!echo hi" + ); assert_eq!(chat.queued_user_messages.len(), 1); let begin = begin_exec_with_source(