Skip to content

Commit 52abd57

Browse files
committed
tui: add /exit and /e aliases for /quit; alias-aware resolver and popup filtering
1 parent 6ef658a commit 52abd57

File tree

5 files changed

+52
-9
lines changed

5 files changed

+52
-9
lines changed

codex-rs/Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

codex-rs/tui/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ strum_macros = { workspace = true }
6868
supports-color = { workspace = true }
6969
tempfile = { workspace = true }
7070
textwrap = { workspace = true }
71+
once_cell = "1"
7172
tree-sitter-highlight = { workspace = true }
7273
tree-sitter-bash = { workspace = true }
7374
tokio = { workspace = true, features = [

codex-rs/tui/src/bottom_pane/chat_composer.rs

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ use crate::bottom_pane::prompt_args::prompt_argument_names;
3636
use crate::bottom_pane::prompt_args::prompt_command_with_arg_placeholders;
3737
use crate::bottom_pane::prompt_args::prompt_has_numeric_placeholders;
3838
use crate::slash_command::SlashCommand;
39-
use crate::slash_command::built_in_slash_commands;
39+
use crate::slash_command::resolve_slash_command;
4040
use crate::style::user_message_style;
4141
use codex_protocol::custom_prompts::CustomPrompt;
4242
use codex_protocol::custom_prompts::PROMPTS_CMD_PREFIX;
@@ -932,9 +932,7 @@ impl ChatComposer {
932932
let first_line = self.textarea.text().lines().next().unwrap_or("");
933933
if let Some((name, rest)) = parse_slash_name(first_line)
934934
&& rest.is_empty()
935-
&& let Some((_n, cmd)) = built_in_slash_commands()
936-
.into_iter()
937-
.find(|(n, _)| *n == name)
935+
&& let Some(cmd) = resolve_slash_command(name)
938936
{
939937
self.textarea.set_text("");
940938
return (InputResult::Command(cmd), true);
@@ -1003,9 +1001,7 @@ impl ChatComposer {
10031001
if let Some((name, _rest)) = parse_slash_name(&text) {
10041002
let treat_as_plain_text = input_starts_with_space || name.contains('/');
10051003
if !treat_as_plain_text {
1006-
let is_builtin = built_in_slash_commands()
1007-
.into_iter()
1008-
.any(|(command_name, _)| command_name == name);
1004+
let is_builtin = resolve_slash_command(name).is_some();
10091005
let prompt_prefix = format!("{PROMPTS_CMD_PREFIX}:");
10101006
let is_known_prompt = name
10111007
.strip_prefix(&prompt_prefix)

codex-rs/tui/src/bottom_pane/command_popup.rs

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,12 @@ impl CommandPopup {
3434
pub(crate) fn new(mut prompts: Vec<CustomPrompt>) -> Self {
3535
let builtins = built_in_slash_commands();
3636
// Exclude prompts that collide with builtin command names and sort by name.
37-
let exclude: HashSet<String> = builtins.iter().map(|(n, _)| (*n).to_string()).collect();
37+
let mut exclude: HashSet<String> = builtins.iter().map(|(n, _)| (*n).to_string()).collect();
38+
for (_, cmd) in &builtins {
39+
for alias in cmd.aliases() {
40+
exclude.insert((*alias).to_string());
41+
}
42+
}
3843
prompts.retain(|p| !exclude.contains(&p.name));
3944
prompts.sort_by(|a, b| a.name.cmp(&b.name));
4045
Self {
@@ -46,11 +51,16 @@ impl CommandPopup {
4651
}
4752

4853
pub(crate) fn set_prompts(&mut self, mut prompts: Vec<CustomPrompt>) {
49-
let exclude: HashSet<String> = self
54+
let mut exclude: HashSet<String> = self
5055
.builtins
5156
.iter()
5257
.map(|(n, _)| (*n).to_string())
5358
.collect();
59+
for (_, cmd) in &self.builtins {
60+
for alias in cmd.aliases() {
61+
exclude.insert((*alias).to_string());
62+
}
63+
}
5464
prompts.retain(|p| !exclude.contains(&p.name));
5565
prompts.sort_by(|a, b| a.name.cmp(&b.name));
5666
self.prompts = prompts;
@@ -121,6 +131,18 @@ impl CommandPopup {
121131
for (_, cmd) in self.builtins.iter() {
122132
if let Some((indices, score)) = fuzzy_match(cmd.command(), filter) {
123133
out.push((CommandItem::Builtin(*cmd), Some(indices), score));
134+
continue;
135+
}
136+
let mut best_alias_score: Option<i32> = None;
137+
for alias in cmd.aliases() {
138+
if let Some((_indices, score)) = fuzzy_match(alias, filter)
139+
&& best_alias_score.is_none_or(|best| score < best)
140+
{
141+
best_alias_score = Some(score);
142+
}
143+
}
144+
if let Some(score) = best_alias_score {
145+
out.push((CommandItem::Builtin(*cmd), None, score));
124146
}
125147
}
126148
// Support both search styles:

codex-rs/tui/src/slash_command.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ pub enum SlashCommand {
2424
Status,
2525
Mcp,
2626
Logout,
27+
#[strum(serialize = "exit", serialize = "e")]
2728
Quit,
2829
Feedback,
2930
Rollout,
@@ -81,6 +82,22 @@ impl SlashCommand {
8182
}
8283
}
8384

85+
/// Additional slash names that map to this command.
86+
pub fn aliases(self) -> &'static [&'static str] {
87+
match self {
88+
SlashCommand::Quit => &["exit", "e"],
89+
_ => &[],
90+
}
91+
}
92+
93+
/// Return true if `name` matches this command's canonical name or an alias.
94+
pub fn matches_name(self, name: &str) -> bool {
95+
if self.command() == name {
96+
return true;
97+
}
98+
self.aliases().contains(&name)
99+
}
100+
84101
fn is_visible(self) -> bool {
85102
match self {
86103
SlashCommand::Rollout | SlashCommand::TestApproval => cfg!(debug_assertions),
@@ -96,3 +113,9 @@ pub fn built_in_slash_commands() -> Vec<(&'static str, SlashCommand)> {
96113
.map(|c| (c.command(), c))
97114
.collect()
98115
}
116+
117+
/// Resolve a slash command name (including aliases) to the corresponding command.
118+
pub fn resolve_slash_command(name: &str) -> Option<SlashCommand> {
119+
use std::str::FromStr;
120+
SlashCommand::from_str(name).ok()
121+
}

0 commit comments

Comments
 (0)