From 9ccdf623fe61e1997af57fefcfee5b620e94e96a Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Tue, 26 May 2026 15:38:37 -0700 Subject: [PATCH 1/4] Simplify slash command completion tests --- codex-rs/tui/src/bottom_pane/chat_composer.rs | 299 ++++-------------- .../bottom_pane/chat_composer/slash_input.rs | 2 +- codex-rs/tui/src/slash_command.rs | 6 - 3 files changed, 67 insertions(+), 240 deletions(-) diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 2da6d759e9a..3fc38881dc0 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -4351,6 +4351,9 @@ mod tests { use crate::bottom_pane::chat_composer::LARGE_PASTE_CHAR_THRESHOLD; use crate::bottom_pane::textarea::TextArea; use codex_protocol::models::local_image_label_text; + use crossterm::event::KeyCode; + use crossterm::event::KeyEvent; + use crossterm::event::KeyModifiers; use tokio::sync::mpsc::unbounded_channel; #[test] @@ -8002,191 +8005,89 @@ mod tests { ); } - #[test] - fn slash_completion_preserves_existing_draft_tail_for_goal() { - use crossterm::event::KeyCode; - use crossterm::event::KeyEvent; - use crossterm::event::KeyModifiers; - - let draft = "preserve this draft as the goal objective"; - let draft_chars = draft.chars().collect::>(); - + fn test_composer() -> ChatComposer { let (tx, _rx) = unbounded_channel::(); let sender = AppEventSender::new(tx); - let mut composer = ChatComposer::new( + ChatComposer::new( /*has_input_focus*/ true, sender, /*enhanced_keys_supported*/ false, "Ask Codex to do anything".to_string(), /*disable_paste_burst*/ false, - ); - composer.set_goal_command_enabled(/*enabled*/ true); - - type_chars_humanlike(&mut composer, &draft_chars); - let (_result, _needs_redraw) = - composer.handle_key_event(KeyEvent::new(KeyCode::Home, KeyModifiers::NONE)); - type_chars_humanlike(&mut composer, &['/', 'g', 'o']); - - 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(), - "/goal preserve this draft as the goal objective" - ); - assert_eq!( - composer.draft.textarea.cursor(), - composer.draft.textarea.text().len() - ); + fn press(composer: &mut ChatComposer, code: KeyCode) -> InputResult { + composer + .handle_key_event(KeyEvent::new(code, KeyModifiers::NONE)) + .0 + } - 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.set_goal_command_enabled(/*enabled*/ true); + fn composer_with_draft_tail(prefix: &[char], draft: &str, goal_enabled: bool) -> ChatComposer { + let mut composer = test_composer(); + composer.set_goal_command_enabled(goal_enabled); + let draft_chars = draft.chars().collect::>(); type_chars_humanlike(&mut composer, &draft_chars); - let (_result, _needs_redraw) = - composer.handle_key_event(KeyEvent::new(KeyCode::Home, KeyModifiers::NONE)); - type_chars_humanlike(&mut composer, &['/', 'g', 'o']); - - let (result, _needs_redraw) = - composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); - - assert_eq!( - result, - InputResult::CommandWithArgs(SlashCommand::Goal, draft.to_string(), Vec::new()) - ); - assert_eq!( - composer.draft.textarea.text(), - "/goal preserve this draft as the goal objective" - ); - - let command = "/re use the text after the slash command as review instructions"; - let review_args = "use the text after the slash command as review instructions"; - let cursor_after_re = "/re".len(); - 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(command); - composer.draft.textarea.set_cursor(cursor_after_re); - composer.sync_popups(); - - let (result, _needs_redraw) = - composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); - - assert_eq!( - result, - InputResult::CommandWithArgs(SlashCommand::Review, review_args.to_string(), Vec::new()) - ); - assert_eq!( - composer.draft.textarea.text(), - "/review use the text after the slash command as review instructions" - ); + press(&mut composer, KeyCode::Home); + type_chars_humanlike(&mut composer, prefix); + 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(command); - composer.draft.textarea.set_cursor(cursor_after_re); + fn composer_with_text_at_cursor(text: &str, cursor: usize) -> ChatComposer { + let mut composer = test_composer(); + composer.draft.textarea.set_text_clearing_elements(text); + composer.draft.textarea.set_cursor(cursor); composer.sync_popups(); + 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 use the text after the slash command as review instructions" - ); + #[test] + fn slash_completion_preserves_existing_draft_tail_for_opted_in_commands() { + let cases: &[(SlashCommand, &[char], &str, &str, bool)] = &[ + ( + SlashCommand::Goal, + &['/', 'g', 'o'], + "preserve this draft as the goal objective", + "/goal preserve this draft as the goal objective", + true, + ), + ( + SlashCommand::Review, + &['/', 'r', 'e'], + "view the diff", + "/review view the diff", + false, + ), + ]; - let make_goal_composer = |cursor| { - 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, + for &(cmd, prefix, draft, expected_text, goal_enabled) in cases { + let mut composer = composer_with_draft_tail(prefix, draft, goal_enabled); + assert_eq!(press(&mut composer, KeyCode::Tab), InputResult::None); + assert_eq!(composer.draft.textarea.text(), expected_text); + assert_eq!( + composer.draft.textarea.cursor(), + composer.draft.textarea.text().len() ); - composer.set_goal_command_enabled(/*enabled*/ true); - composer - .draft - .textarea - .set_text_clearing_elements("/goal ship it"); - composer.draft.textarea.set_cursor(cursor); - composer.sync_popups(); - composer - }; - - for cursor in [0, 1] { - let mut composer = make_goal_composer(cursor); - 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(), "/goal ship it"); - } - - for cursor in [0, 1] { - let mut composer = make_goal_composer(cursor); - let (result, _needs_redraw) = - composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE)); + let mut composer = composer_with_draft_tail(prefix, draft, goal_enabled); assert_eq!( - result, - InputResult::CommandWithArgs(SlashCommand::Goal, "ship it".to_string(), Vec::new()) + press(&mut composer, KeyCode::Enter), + InputResult::CommandWithArgs(cmd, draft.to_string(), Vec::new()) ); - assert_eq!(composer.draft.textarea.text(), "/goal ship it"); + assert_eq!(composer.draft.textarea.text(), expected_text); } } #[test] fn slash_completion_does_not_preserve_existing_draft_tail_for_other_commands() { - use crossterm::event::KeyCode; - use crossterm::event::KeyEvent; - use crossterm::event::KeyModifiers; - - let draft = "preserve this draft only for opted-in slash commands"; - let draft_chars = draft.chars().collect::>(); - 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, + let mut composer = composer_with_draft_tail( + &['/', 'm', 'o'], + "preserve this draft only for opted-in slash commands", + false, ); - type_chars_humanlike(&mut composer, &draft_chars); - let (_result, _needs_redraw) = - composer.handle_key_event(KeyEvent::new(KeyCode::Home, KeyModifiers::NONE)); - type_chars_humanlike(&mut composer, &['/', 'm', 'o']); - - let (result, _needs_redraw) = - composer.handle_key_event(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE)); - - assert_eq!(result, InputResult::None); + assert_eq!(press(&mut composer, KeyCode::Tab), InputResult::None); assert_eq!(composer.draft.textarea.text(), "/model "); assert_eq!( composer.draft.textarea.cursor(), @@ -8196,84 +8097,16 @@ 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); + let mut composer = composer_with_text_at_cursor("/review", "/re".len()); + assert_eq!(press(&mut composer, KeyCode::Tab), 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)); - + let mut composer = composer_with_text_at_cursor("/review", "/re".len()); assert_eq!( - result, - InputResult::CommandWithArgs(SlashCommand::Review, draft.to_string(), Vec::new()) + press(&mut composer, KeyCode::Enter), + InputResult::Command(SlashCommand::Review) ); - assert_eq!(composer.draft.textarea.text(), "/review view the diff"); + assert!(composer.draft.textarea.is_empty()); } #[test] 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 a1387e6c82b..9508ac75ddf 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 @@ -388,7 +388,7 @@ impl ChatComposer { return false; }; let cmd = *cmd; - if !cmd.preserves_draft_tail_on_completion() { + if !matches!(cmd, SlashCommand::Review | SlashCommand::Goal) { return false; } diff --git a/codex-rs/tui/src/slash_command.rs b/codex-rs/tui/src/slash_command.rs index 687d50bf372..4a43409296a 100644 --- a/codex-rs/tui/src/slash_command.rs +++ b/codex-rs/tui/src/slash_command.rs @@ -163,12 +163,6 @@ impl SlashCommand { ) } - /// Whether slash-command completion should preserve draft text after the typed command prefix - /// and use it as inline arguments for this command. - pub fn preserves_draft_tail_on_completion(self) -> bool { - matches!(self, SlashCommand::Review | SlashCommand::Goal) - } - /// Whether this command remains available inside an active side conversation. pub fn available_in_side_conversation(self) -> bool { matches!( From 42f93290f5458ccbb585b51e1898d017d6a4a685 Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Tue, 26 May 2026 15:42:37 -0700 Subject: [PATCH 2/4] Preserve draft tails for more slash commands --- .../tui/src/bottom_pane/chat_composer/slash_input.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) 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 9508ac75ddf..30f73da9b06 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 @@ -388,7 +388,16 @@ impl ChatComposer { return false; }; let cmd = *cmd; - if !matches!(cmd, SlashCommand::Review | SlashCommand::Goal) { + if !matches!( + cmd, + SlashCommand::Review + | SlashCommand::Rename + | SlashCommand::Plan + | SlashCommand::Goal + | SlashCommand::Side + | SlashCommand::Btw + | SlashCommand::Resume + ) { return false; } From 1609355ae018c65dddde468dabad8dbf17f3cc2c Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Tue, 26 May 2026 15:55:24 -0700 Subject: [PATCH 3/4] Address slash completion review feedback --- codex-rs/tui/src/bottom_pane/chat_composer.rs | 107 ------------------ .../bottom_pane/chat_composer/slash_input.rs | 94 +++++++++++++-- 2 files changed, 83 insertions(+), 118 deletions(-) diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 3fc38881dc0..9fc03764da9 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -4351,9 +4351,6 @@ mod tests { use crate::bottom_pane::chat_composer::LARGE_PASTE_CHAR_THRESHOLD; use crate::bottom_pane::textarea::TextArea; use codex_protocol::models::local_image_label_text; - use crossterm::event::KeyCode; - use crossterm::event::KeyEvent; - use crossterm::event::KeyModifiers; use tokio::sync::mpsc::unbounded_channel; #[test] @@ -8005,110 +8002,6 @@ mod tests { ); } - fn test_composer() -> ChatComposer { - let (tx, _rx) = unbounded_channel::(); - let sender = AppEventSender::new(tx); - ChatComposer::new( - /*has_input_focus*/ true, - sender, - /*enhanced_keys_supported*/ false, - "Ask Codex to do anything".to_string(), - /*disable_paste_burst*/ false, - ) - } - - fn press(composer: &mut ChatComposer, code: KeyCode) -> InputResult { - composer - .handle_key_event(KeyEvent::new(code, KeyModifiers::NONE)) - .0 - } - - fn composer_with_draft_tail(prefix: &[char], draft: &str, goal_enabled: bool) -> ChatComposer { - let mut composer = test_composer(); - composer.set_goal_command_enabled(goal_enabled); - - let draft_chars = draft.chars().collect::>(); - type_chars_humanlike(&mut composer, &draft_chars); - press(&mut composer, KeyCode::Home); - type_chars_humanlike(&mut composer, prefix); - composer - } - - fn composer_with_text_at_cursor(text: &str, cursor: usize) -> ChatComposer { - let mut composer = test_composer(); - composer.draft.textarea.set_text_clearing_elements(text); - composer.draft.textarea.set_cursor(cursor); - composer.sync_popups(); - composer - } - - #[test] - fn slash_completion_preserves_existing_draft_tail_for_opted_in_commands() { - let cases: &[(SlashCommand, &[char], &str, &str, bool)] = &[ - ( - SlashCommand::Goal, - &['/', 'g', 'o'], - "preserve this draft as the goal objective", - "/goal preserve this draft as the goal objective", - true, - ), - ( - SlashCommand::Review, - &['/', 'r', 'e'], - "view the diff", - "/review view the diff", - false, - ), - ]; - - for &(cmd, prefix, draft, expected_text, goal_enabled) in cases { - let mut composer = composer_with_draft_tail(prefix, draft, goal_enabled); - assert_eq!(press(&mut composer, KeyCode::Tab), InputResult::None); - assert_eq!(composer.draft.textarea.text(), expected_text); - assert_eq!( - composer.draft.textarea.cursor(), - composer.draft.textarea.text().len() - ); - - let mut composer = composer_with_draft_tail(prefix, draft, goal_enabled); - assert_eq!( - press(&mut composer, KeyCode::Enter), - InputResult::CommandWithArgs(cmd, draft.to_string(), Vec::new()) - ); - assert_eq!(composer.draft.textarea.text(), expected_text); - } - } - - #[test] - fn slash_completion_does_not_preserve_existing_draft_tail_for_other_commands() { - let mut composer = composer_with_draft_tail( - &['/', 'm', 'o'], - "preserve this draft only for opted-in slash commands", - false, - ); - - assert_eq!(press(&mut composer, KeyCode::Tab), InputResult::None); - assert_eq!(composer.draft.textarea.text(), "/model "); - assert_eq!( - composer.draft.textarea.cursor(), - composer.draft.textarea.text().len() - ); - } - - #[test] - fn slash_completion_does_not_turn_command_suffix_into_args() { - let mut composer = composer_with_text_at_cursor("/review", "/re".len()); - assert_eq!(press(&mut composer, KeyCode::Tab), InputResult::None); - assert_eq!(composer.draft.textarea.text(), "/review "); - - let mut composer = composer_with_text_at_cursor("/review", "/re".len()); - assert_eq!( - press(&mut composer, KeyCode::Enter), - InputResult::Command(SlashCommand::Review) - ); - assert!(composer.draft.textarea.is_empty()); - } - #[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 30f73da9b06..ac0ec96414e 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 @@ -387,17 +387,7 @@ impl ChatComposer { let CommandItem::Builtin(cmd) = selected_cmd else { return false; }; - let cmd = *cmd; - if !matches!( - cmd, - SlashCommand::Review - | SlashCommand::Rename - | SlashCommand::Plan - | SlashCommand::Goal - | SlashCommand::Side - | SlashCommand::Btw - | SlashCommand::Resume - ) { + if !cmd.supports_inline_args() { return false; } @@ -574,3 +564,85 @@ fn command_under_cursor(first_line: &str, cursor: usize) -> Option<(&str, &str)> Some((name, rest)) } + +#[cfg(test)] +mod tests { + use super::*; + use crate::app_event::AppEvent; + use crate::bottom_pane::AppEventSender; + use pretty_assertions::assert_eq; + use tokio::sync::mpsc::unbounded_channel; + + fn test_composer() -> ChatComposer { + let (tx, _rx) = unbounded_channel::(); + ChatComposer::new( + /*has_input_focus*/ true, + AppEventSender::new(tx), + /*enhanced_keys_supported*/ false, + "Ask Codex to do anything".to_string(), + /*disable_paste_burst*/ false, + ) + } + + fn press(composer: &mut ChatComposer, code: KeyCode) -> InputResult { + composer + .handle_key_event(KeyEvent::new(code, KeyModifiers::NONE)) + .0 + } + + fn composer_with_text_at_cursor(text: &str, cursor: usize) -> ChatComposer { + let mut composer = test_composer(); + composer.draft.textarea.set_text_clearing_elements(text); + composer.draft.textarea.set_cursor(cursor); + composer.sync_popups(); + composer + } + + fn composer_with_draft_tail(prefix: &str, draft: &str) -> ChatComposer { + composer_with_text_at_cursor(&format!("{prefix}{draft}"), prefix.len()) + } + + #[test] + fn slash_completion_preserves_existing_draft_tail_for_inline_arg_commands() { + let draft = "view the diff"; + let expected_text = "/review view the diff"; + + let mut composer = composer_with_draft_tail("/re", draft); + assert_eq!(press(&mut composer, KeyCode::Tab), InputResult::None); + assert_eq!(composer.draft.textarea.text(), expected_text); + assert_eq!(composer.draft.textarea.cursor(), expected_text.len()); + + let mut composer = composer_with_draft_tail("/re", draft); + assert_eq!( + press(&mut composer, KeyCode::Enter), + InputResult::CommandWithArgs(SlashCommand::Review, draft.to_string(), Vec::new()) + ); + assert_eq!(composer.draft.textarea.text(), expected_text); + } + + #[test] + fn slash_completion_does_not_preserve_existing_draft_tail_for_other_commands() { + let mut composer = composer_with_draft_tail( + "/mo", + "preserve this draft only for opted-in slash commands", + ); + + assert_eq!(press(&mut composer, KeyCode::Tab), InputResult::None); + assert_eq!(composer.draft.textarea.text(), "/model "); + assert_eq!(composer.draft.textarea.cursor(), "/model ".len()); + } + + #[test] + fn slash_completion_does_not_turn_command_suffix_into_args() { + let mut composer = composer_with_text_at_cursor("/review", "/re".len()); + assert_eq!(press(&mut composer, KeyCode::Tab), InputResult::None); + assert_eq!(composer.draft.textarea.text(), "/review "); + + let mut composer = composer_with_text_at_cursor("/review", "/re".len()); + assert_eq!( + press(&mut composer, KeyCode::Enter), + InputResult::Command(SlashCommand::Review) + ); + assert!(composer.draft.textarea.is_empty()); + } +} From c7ec378b12b5065fc5bb9017abd1c5981d4b2c5f Mon Sep 17 00:00:00 2001 From: Eric Traut Date: Tue, 26 May 2026 19:18:13 -0700 Subject: [PATCH 4/4] Fix bare slash command completion dispatch --- codex-rs/tui/src/bottom_pane/chat_composer/slash_input.rs | 3 +++ 1 file changed, 3 insertions(+) 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 ac0ec96414e..bb9d2c6904d 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 @@ -404,6 +404,9 @@ impl ChatComposer { .unwrap_or(first_line_end); let typed_command_name = &text[1..command_token_end]; let rest_after_token_is_empty = text[command_token_end..].trim().is_empty(); + if rest_after_token_is_empty && (cursor <= 1 || cursor >= command_token_end) { + return false; + } let replace_end = if cursor <= 1 || (typed_command_name == cmd.command() && rest_after_token_is_empty) { command_token_end