Fix tab+enter regression on slash commands (#4639)

Before when you would enter `/di`, hit tab on `/diff`, and then hit
enter, it would execute `/diff`. But now it's just sending it as a text.
This fixes the issue.
This commit is contained in:
dedrisian-oai
2025-10-02 20:14:28 -07:00
committed by GitHub
parent 7be3b484ad
commit 1e4541b982

View File

@@ -36,6 +36,7 @@ use crate::bottom_pane::prompt_args::prompt_argument_names;
use crate::bottom_pane::prompt_args::prompt_command_with_arg_placeholders; use crate::bottom_pane::prompt_args::prompt_command_with_arg_placeholders;
use crate::bottom_pane::prompt_args::prompt_has_numeric_placeholders; use crate::bottom_pane::prompt_args::prompt_has_numeric_placeholders;
use crate::slash_command::SlashCommand; use crate::slash_command::SlashCommand;
use crate::slash_command::built_in_slash_commands;
use crate::style::user_message_style; use crate::style::user_message_style;
use crate::terminal_palette; use crate::terminal_palette;
use codex_protocol::custom_prompts::CustomPrompt; use codex_protocol::custom_prompts::CustomPrompt;
@@ -894,6 +895,23 @@ impl ChatComposer {
modifiers: KeyModifiers::NONE, modifiers: KeyModifiers::NONE,
.. ..
} => { } => {
// If the first line is a bare built-in slash command (no args),
// dispatch it even when the slash popup isn't visible. This preserves
// the workflow: type a prefix ("/di"), press Tab to complete to
// "/diff ", then press Enter to run it. Tab moves the cursor beyond
// the '/name' token and our caret-based heuristic hides the popup,
// but Enter should still dispatch the command rather than submit
// literal text.
let first_line = self.textarea.text().lines().next().unwrap_or("");
if let Some((name, rest)) = parse_slash_name(first_line)
&& rest.is_empty()
&& let Some((_n, cmd)) = built_in_slash_commands()
.into_iter()
.find(|(n, _)| *n == name)
{
self.textarea.set_text("");
return (InputResult::Command(cmd), true);
}
// If we're in a paste-like burst capture, treat Enter as part of the burst // If we're in a paste-like burst capture, treat Enter as part of the burst
// and accumulate it rather than submitting or inserting immediately. // and accumulate it rather than submitting or inserting immediately.
// Do not treat Enter as paste inside a slash-command context. // Do not treat Enter as paste inside a slash-command context.
@@ -2277,6 +2295,38 @@ mod tests {
assert_eq!(composer.textarea.cursor(), composer.textarea.text().len()); assert_eq!(composer.textarea.cursor(), composer.textarea.text().len());
} }
#[test]
fn slash_tab_then_enter_dispatches_builtin_command() {
let (tx, _rx) = unbounded_channel::<AppEvent>();
let sender = AppEventSender::new(tx);
let mut composer = ChatComposer::new(
true,
sender,
false,
"Ask Codex to do anything".to_string(),
false,
);
// Type a prefix and complete with Tab, which inserts a trailing space
// and moves the cursor beyond the '/name' token (hides the popup).
type_chars_humanlike(&mut composer, &['/', 'd', 'i']);
let (_res, _redraw) =
composer.handle_key_event(KeyEvent::new(KeyCode::Tab, KeyModifiers::NONE));
assert_eq!(composer.textarea.text(), "/diff ");
// Press Enter: should dispatch the command, not submit literal text.
let (result, _needs_redraw) =
composer.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
match result {
InputResult::Command(cmd) => assert_eq!(cmd.command(), "diff"),
InputResult::Submitted(text) => {
panic!("expected command dispatch after Tab completion, got literal submit: {text}")
}
InputResult::None => panic!("expected Command result for '/diff'"),
}
assert!(composer.textarea.is_empty());
}
#[test] #[test]
fn slash_mention_dispatches_command_and_inserts_at() { fn slash_mention_dispatches_command_and_inserts_at() {
use crossterm::event::KeyCode; use crossterm::event::KeyCode;