diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 8ff47df4bb..3d5829182a 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -8194,6 +8194,88 @@ mod tests { ); } + #[test] + fn slash_completion_does_not_turn_command_suffix_into_args() { + use crossterm::event::KeyCode; + use crossterm::event::KeyEvent; + use crossterm::event::KeyModifiers; + + let make_review_composer = || { + let (tx, _rx) = unbounded_channel::(); + let sender = AppEventSender::new(tx); + let mut composer = ChatComposer::new( + /*has_input_focus*/ true, + sender, + /*enhanced_keys_supported*/ false, + "Ask Codex to do anything".to_string(), + /*disable_paste_burst*/ false, + ); + composer + .draft + .textarea + .set_text_clearing_elements("/review"); + composer.draft.textarea.set_cursor("/re".len()); + composer.sync_popups(); + composer + }; + + let mut composer = make_review_composer(); + let (result, _needs_redraw) = + composer.handle_key_event(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE)); + + assert_eq!(result, InputResult::None); + assert_eq!(composer.draft.textarea.text(), "/review "); + + let mut composer = make_review_composer(); + let (result, _needs_redraw) = + composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); + + assert_eq!(result, InputResult::Command(SlashCommand::Review)); + assert!(composer.draft.textarea.is_empty()); + } + + #[test] + fn slash_completion_preserves_draft_tail_that_completes_command_name() { + use crossterm::event::KeyCode; + use crossterm::event::KeyEvent; + use crossterm::event::KeyModifiers; + + let draft = "view the diff"; + let make_review_composer = || { + let (tx, _rx) = unbounded_channel::(); + let sender = AppEventSender::new(tx); + let mut composer = ChatComposer::new( + /*has_input_focus*/ true, + sender, + /*enhanced_keys_supported*/ false, + "Ask Codex to do anything".to_string(), + /*disable_paste_burst*/ false, + ); + type_chars_humanlike(&mut composer, &draft.chars().collect::>()); + let (_result, _needs_redraw) = + composer.handle_key_event(KeyEvent::new(KeyCode::Home, KeyModifiers::NONE)); + type_chars_humanlike(&mut composer, &['/', 'r', 'e']); + composer + }; + + let mut composer = make_review_composer(); + let (result, _needs_redraw) = + composer.handle_key_event(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE)); + + assert_eq!(result, InputResult::None); + assert_eq!(composer.draft.textarea.text(), "/review view the diff"); + + let mut composer = make_review_composer(); + let (result, _needs_redraw) = + composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); + + assert_eq!( + result, + InputResult::CommandWithArgs(SlashCommand::Review, draft.to_string(), Vec::new()) + ); + assert_eq!(composer.draft.textarea.text(), "/review view the diff"); + } + #[test] fn slash_tab_completion_wins_over_queueing_while_task_running() { use crossterm::event::KeyCode; diff --git a/codex-rs/tui/src/bottom_pane/chat_composer/slash_input.rs b/codex-rs/tui/src/bottom_pane/chat_composer/slash_input.rs index e86f38ff05..a1387e6c82 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer/slash_input.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer/slash_input.rs @@ -403,11 +403,14 @@ impl ChatComposer { .find(char::is_whitespace) .map(|idx| 1 + idx) .unwrap_or(first_line_end); - let replace_end = if cursor <= 1 { - command_token_end - } else { - cursor - }; + let typed_command_name = &text[1..command_token_end]; + let rest_after_token_is_empty = text[command_token_end..].trim().is_empty(); + let replace_end = + if cursor <= 1 || (typed_command_name == cmd.command() && rest_after_token_is_empty) { + command_token_end + } else { + cursor + }; let tail = &text[replace_end..]; let tail_starts_with_whitespace = tail.chars().next().is_some_and(char::is_whitespace); let selected_command_text = format!("/{}", cmd.command());