tui: add /exit and /e aliases for /quit; alias-aware resolver and popup filtering

This commit is contained in:
Ahmed Ibrahim
2025-10-30 13:03:30 -07:00
parent 6ef658a9f9
commit 52abd57726
5 changed files with 52 additions and 9 deletions

1
codex-rs/Cargo.lock generated
View File

@@ -1457,6 +1457,7 @@ dependencies = [
"lazy_static",
"libc",
"mcp-types",
"once_cell",
"opentelemetry-appender-tracing",
"pathdiff",
"pretty_assertions",

View File

@@ -68,6 +68,7 @@ strum_macros = { workspace = true }
supports-color = { workspace = true }
tempfile = { workspace = true }
textwrap = { workspace = true }
once_cell = "1"
tree-sitter-highlight = { workspace = true }
tree-sitter-bash = { workspace = true }
tokio = { workspace = true, features = [

View File

@@ -36,7 +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_has_numeric_placeholders;
use crate::slash_command::SlashCommand;
use crate::slash_command::built_in_slash_commands;
use crate::slash_command::resolve_slash_command;
use crate::style::user_message_style;
use codex_protocol::custom_prompts::CustomPrompt;
use codex_protocol::custom_prompts::PROMPTS_CMD_PREFIX;
@@ -932,9 +932,7 @@ impl ChatComposer {
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)
&& let Some(cmd) = resolve_slash_command(name)
{
self.textarea.set_text("");
return (InputResult::Command(cmd), true);
@@ -1003,9 +1001,7 @@ impl ChatComposer {
if let Some((name, _rest)) = parse_slash_name(&text) {
let treat_as_plain_text = input_starts_with_space || name.contains('/');
if !treat_as_plain_text {
let is_builtin = built_in_slash_commands()
.into_iter()
.any(|(command_name, _)| command_name == name);
let is_builtin = resolve_slash_command(name).is_some();
let prompt_prefix = format!("{PROMPTS_CMD_PREFIX}:");
let is_known_prompt = name
.strip_prefix(&prompt_prefix)

View File

@@ -34,7 +34,12 @@ impl CommandPopup {
pub(crate) fn new(mut prompts: Vec<CustomPrompt>) -> Self {
let builtins = built_in_slash_commands();
// Exclude prompts that collide with builtin command names and sort by name.
let exclude: HashSet<String> = builtins.iter().map(|(n, _)| (*n).to_string()).collect();
let mut exclude: HashSet<String> = builtins.iter().map(|(n, _)| (*n).to_string()).collect();
for (_, cmd) in &builtins {
for alias in cmd.aliases() {
exclude.insert((*alias).to_string());
}
}
prompts.retain(|p| !exclude.contains(&p.name));
prompts.sort_by(|a, b| a.name.cmp(&b.name));
Self {
@@ -46,11 +51,16 @@ impl CommandPopup {
}
pub(crate) fn set_prompts(&mut self, mut prompts: Vec<CustomPrompt>) {
let exclude: HashSet<String> = self
let mut exclude: HashSet<String> = self
.builtins
.iter()
.map(|(n, _)| (*n).to_string())
.collect();
for (_, cmd) in &self.builtins {
for alias in cmd.aliases() {
exclude.insert((*alias).to_string());
}
}
prompts.retain(|p| !exclude.contains(&p.name));
prompts.sort_by(|a, b| a.name.cmp(&b.name));
self.prompts = prompts;
@@ -121,6 +131,18 @@ impl CommandPopup {
for (_, cmd) in self.builtins.iter() {
if let Some((indices, score)) = fuzzy_match(cmd.command(), filter) {
out.push((CommandItem::Builtin(*cmd), Some(indices), score));
continue;
}
let mut best_alias_score: Option<i32> = None;
for alias in cmd.aliases() {
if let Some((_indices, score)) = fuzzy_match(alias, filter)
&& best_alias_score.is_none_or(|best| score < best)
{
best_alias_score = Some(score);
}
}
if let Some(score) = best_alias_score {
out.push((CommandItem::Builtin(*cmd), None, score));
}
}
// Support both search styles:

View File

@@ -24,6 +24,7 @@ pub enum SlashCommand {
Status,
Mcp,
Logout,
#[strum(serialize = "exit", serialize = "e")]
Quit,
Feedback,
Rollout,
@@ -81,6 +82,22 @@ impl SlashCommand {
}
}
/// Additional slash names that map to this command.
pub fn aliases(self) -> &'static [&'static str] {
match self {
SlashCommand::Quit => &["exit", "e"],
_ => &[],
}
}
/// Return true if `name` matches this command's canonical name or an alias.
pub fn matches_name(self, name: &str) -> bool {
if self.command() == name {
return true;
}
self.aliases().contains(&name)
}
fn is_visible(self) -> bool {
match self {
SlashCommand::Rollout | SlashCommand::TestApproval => cfg!(debug_assertions),
@@ -96,3 +113,9 @@ pub fn built_in_slash_commands() -> Vec<(&'static str, SlashCommand)> {
.map(|c| (c.command(), c))
.collect()
}
/// Resolve a slash command name (including aliases) to the corresponding command.
pub fn resolve_slash_command(name: &str) -> Option<SlashCommand> {
use std::str::FromStr;
SlashCommand::from_str(name).ok()
}