From 708a95ed4a92399a45af9e79226040e3a543ece8 Mon Sep 17 00:00:00 2001 From: Charles Cunningham Date: Fri, 20 Mar 2026 23:26:26 -0700 Subject: [PATCH] tui: unify slash command registration Move usage strings, bare-form behavior, and inline-arg parser selection into the per-command slash spec so command registration lives in one authoritative table in both tui implementations. Co-authored-by: Codex --- codex-rs/tui/src/slash_command.rs | 210 +++++++++++++----- codex-rs/tui/src/slash_command_input.rs | 118 ++-------- codex-rs/tui_app_server/src/slash_command.rs | 206 ++++++++++++----- .../tui_app_server/src/slash_command_input.rs | 116 ++-------- 4 files changed, 341 insertions(+), 309 deletions(-) diff --git a/codex-rs/tui/src/slash_command.rs b/codex-rs/tui/src/slash_command.rs index d47fb07007..44443809b9 100644 --- a/codex-rs/tui/src/slash_command.rs +++ b/codex-rs/tui/src/slash_command.rs @@ -66,272 +66,420 @@ pub enum SlashCommand { MemoryUpdate, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum SlashCommandBareBehavior { + DispatchesDirectly, + OpensUi, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum SlashCommandParseKind { + NoArgs, + Fast, + Rename, + Plan, + Review, + SandboxReadRoot, +} + impl SlashCommand { - fn spec(self) -> SlashCommandSpec { + pub(crate) fn spec(self) -> SlashCommandSpec { match self { SlashCommand::Model => SlashCommandSpec { description: "choose what model and reasoning effort to use", available_during_task: false, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/model"], + bare_behavior: SlashCommandBareBehavior::OpensUi, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Fast => SlashCommandSpec { description: "toggle Fast mode to enable fastest inference at 2X plan usage", available_during_task: false, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/fast", "/fast [on|off|status]"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::Fast, }, SlashCommand::Approvals => SlashCommandSpec { description: "choose what Codex is allowed to do", available_during_task: false, is_disabled: false, hide_in_command_popup: true, + usage_lines: &["/approvals"], + bare_behavior: SlashCommandBareBehavior::OpensUi, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Permissions => SlashCommandSpec { description: "choose what Codex is allowed to do", available_during_task: false, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/permissions"], + bare_behavior: SlashCommandBareBehavior::OpensUi, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::ElevateSandbox => SlashCommandSpec { description: "set up elevated agent sandbox", available_during_task: false, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/setup-default-sandbox"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::SandboxReadRoot => SlashCommandSpec { description: "let sandbox read a directory: /sandbox-add-read-dir ", available_during_task: false, is_disabled: !cfg!(target_os = "windows"), hide_in_command_popup: false, + usage_lines: &["/sandbox-add-read-dir "], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::SandboxReadRoot, }, SlashCommand::Experimental => SlashCommandSpec { description: "toggle experimental features", available_during_task: false, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/experimental"], + bare_behavior: SlashCommandBareBehavior::OpensUi, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Skills => SlashCommandSpec { description: "use skills to improve how Codex performs specific tasks", available_during_task: true, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/skills"], + bare_behavior: SlashCommandBareBehavior::OpensUi, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Review => SlashCommandSpec { description: "review my current changes and find issues", available_during_task: false, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/review", "/review "], + bare_behavior: SlashCommandBareBehavior::OpensUi, + parse_kind: SlashCommandParseKind::Review, }, SlashCommand::Rename => SlashCommandSpec { description: "rename the current thread", available_during_task: true, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/rename", "/rename "], + bare_behavior: SlashCommandBareBehavior::OpensUi, + parse_kind: SlashCommandParseKind::Rename, }, SlashCommand::New => SlashCommandSpec { description: "start a new chat during a conversation", available_during_task: false, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/new"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Resume => SlashCommandSpec { description: "resume a saved chat", available_during_task: false, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/resume"], + bare_behavior: SlashCommandBareBehavior::OpensUi, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Fork => SlashCommandSpec { description: "fork the current chat", available_during_task: false, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/fork"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Init => SlashCommandSpec { description: "create an AGENTS.md file with instructions for Codex", available_during_task: false, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/init"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Compact => SlashCommandSpec { description: "summarize conversation to prevent hitting the context limit", available_during_task: false, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/compact"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Plan => SlashCommandSpec { description: "switch to Plan mode", available_during_task: false, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/plan", "/plan <prompt>"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::Plan, }, SlashCommand::Collab => SlashCommandSpec { description: "change collaboration mode (experimental)", available_during_task: true, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/collab"], + bare_behavior: SlashCommandBareBehavior::OpensUi, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Agent => SlashCommandSpec { description: "switch the active agent thread", available_during_task: true, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/agent"], + bare_behavior: SlashCommandBareBehavior::OpensUi, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Diff => SlashCommandSpec { description: "show git diff (including untracked files)", available_during_task: true, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/diff"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Copy => SlashCommandSpec { description: "copy the latest Codex output to your clipboard", available_during_task: true, is_disabled: cfg!(target_os = "android"), hide_in_command_popup: false, + usage_lines: &["/copy"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Mention => SlashCommandSpec { description: "mention a file", available_during_task: true, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/mention"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Status => SlashCommandSpec { description: "show current session configuration and token usage", available_during_task: true, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/status"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::DebugConfig => SlashCommandSpec { description: "show config layers and requirement sources for debugging", available_during_task: true, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/debug-config"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Title => SlashCommandSpec { description: "configure which items appear in the terminal title", available_during_task: false, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/title"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Statusline => SlashCommandSpec { description: "configure which items appear in the status line", available_during_task: false, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/statusline"], + bare_behavior: SlashCommandBareBehavior::OpensUi, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Theme => SlashCommandSpec { description: "choose a syntax highlighting theme", available_during_task: false, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/theme"], + bare_behavior: SlashCommandBareBehavior::OpensUi, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Mcp => SlashCommandSpec { description: "list configured MCP tools", available_during_task: true, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/mcp"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Apps => SlashCommandSpec { description: "manage apps", available_during_task: true, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/apps"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Plugins => SlashCommandSpec { description: "browse plugins", available_during_task: true, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/plugins"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Logout => SlashCommandSpec { description: "log out of Codex", available_during_task: false, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/logout"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Quit => SlashCommandSpec { description: "exit Codex", available_during_task: true, is_disabled: false, hide_in_command_popup: true, + usage_lines: &["/quit"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Exit => SlashCommandSpec { description: "exit Codex", available_during_task: true, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/exit"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Feedback => SlashCommandSpec { description: "send logs to maintainers", available_during_task: true, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/feedback"], + bare_behavior: SlashCommandBareBehavior::OpensUi, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Rollout => SlashCommandSpec { description: "print the rollout file path", available_during_task: true, is_disabled: !cfg!(debug_assertions), hide_in_command_popup: false, + usage_lines: &["/rollout"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Ps => SlashCommandSpec { description: "list background terminals", available_during_task: true, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/ps"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Stop => SlashCommandSpec { description: "stop all background terminals", available_during_task: true, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/stop"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Clear => SlashCommandSpec { description: "clear the terminal and start a new chat", available_during_task: false, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/clear"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Personality => SlashCommandSpec { description: "choose a communication style for Codex", available_during_task: false, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/personality"], + bare_behavior: SlashCommandBareBehavior::OpensUi, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Realtime => SlashCommandSpec { description: "toggle realtime voice mode (experimental)", available_during_task: true, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/realtime"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Settings => SlashCommandSpec { description: "configure realtime microphone/speaker", available_during_task: true, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/settings"], + bare_behavior: SlashCommandBareBehavior::OpensUi, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::TestApproval => SlashCommandSpec { description: "test approval request", available_during_task: true, is_disabled: !cfg!(debug_assertions), hide_in_command_popup: false, + usage_lines: &["/test-approval"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::MultiAgents => SlashCommandSpec { description: "switch the active agent thread", available_during_task: true, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/subagents"], + bare_behavior: SlashCommandBareBehavior::OpensUi, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::MemoryDrop => SlashCommandSpec { description: "DO NOT USE", available_during_task: false, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/debug-m-drop"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::MemoryUpdate => SlashCommandSpec { description: "DO NOT USE", available_during_task: false, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/debug-m-update"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::NoArgs, }, } } @@ -349,52 +497,7 @@ impl SlashCommand { /// User-visible usage forms for this command. pub(crate) fn usage_lines(self) -> &'static [&'static str] { - match self { - SlashCommand::Model => &["/model"], - SlashCommand::Fast => &["/fast", "/fast [on|off|status]"], - SlashCommand::Approvals => &["/approvals"], - SlashCommand::Permissions => &["/permissions"], - SlashCommand::ElevateSandbox => &["/setup-default-sandbox"], - SlashCommand::SandboxReadRoot => &["/sandbox-add-read-dir <absolute-path>"], - SlashCommand::Experimental => &["/experimental"], - SlashCommand::Skills => &["/skills"], - SlashCommand::Review => &["/review", "/review <instructions>"], - SlashCommand::Rename => &["/rename", "/rename <title>"], - SlashCommand::New => &["/new"], - SlashCommand::Resume => &["/resume"], - SlashCommand::Fork => &["/fork"], - SlashCommand::Init => &["/init"], - SlashCommand::Compact => &["/compact"], - SlashCommand::Plan => &["/plan", "/plan <prompt>"], - SlashCommand::Collab => &["/collab"], - SlashCommand::Agent => &["/agent"], - SlashCommand::Diff => &["/diff"], - SlashCommand::Copy => &["/copy"], - SlashCommand::Mention => &["/mention"], - SlashCommand::Status => &["/status"], - SlashCommand::DebugConfig => &["/debug-config"], - SlashCommand::Title => &["/title"], - SlashCommand::Statusline => &["/statusline"], - SlashCommand::Theme => &["/theme"], - SlashCommand::Mcp => &["/mcp"], - SlashCommand::Apps => &["/apps"], - SlashCommand::Plugins => &["/plugins"], - SlashCommand::Logout => &["/logout"], - SlashCommand::Quit => &["/quit"], - SlashCommand::Exit => &["/exit"], - SlashCommand::Feedback => &["/feedback"], - SlashCommand::Rollout => &["/rollout"], - SlashCommand::Ps => &["/ps"], - SlashCommand::Stop => &["/stop"], - SlashCommand::Clear => &["/clear"], - SlashCommand::Personality => &["/personality"], - SlashCommand::Realtime => &["/realtime"], - SlashCommand::Settings => &["/settings"], - SlashCommand::TestApproval => &["/test-approval"], - SlashCommand::MultiAgents => &["/subagents"], - SlashCommand::MemoryDrop => &["/debug-m-drop"], - SlashCommand::MemoryUpdate => &["/debug-m-update"], - } + self.spec().usage_lines } /// Whether this command can be run while a task is in progress. @@ -423,11 +526,14 @@ pub fn built_in_slash_commands() -> Vec<(&'static str, SlashCommand)> { .collect() } -struct SlashCommandSpec { - description: &'static str, - available_during_task: bool, - is_disabled: bool, - hide_in_command_popup: bool, +pub(crate) struct SlashCommandSpec { + pub(crate) description: &'static str, + pub(crate) available_during_task: bool, + pub(crate) is_disabled: bool, + pub(crate) hide_in_command_popup: bool, + pub(crate) usage_lines: &'static [&'static str], + pub(crate) bare_behavior: SlashCommandBareBehavior, + pub(crate) parse_kind: SlashCommandParseKind, } #[cfg(test)] diff --git a/codex-rs/tui/src/slash_command_input.rs b/codex-rs/tui/src/slash_command_input.rs index 8402474511..b288517c42 100644 --- a/codex-rs/tui/src/slash_command_input.rs +++ b/codex-rs/tui/src/slash_command_input.rs @@ -1,4 +1,6 @@ use crate::slash_command::SlashCommand; +use crate::slash_command::SlashCommandBareBehavior; +use crate::slash_command::SlashCommandParseKind; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum FastSlashCommandArgs { @@ -7,12 +9,6 @@ pub(crate) enum FastSlashCommandArgs { Status, } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum SlashCommandBareBehavior { - DispatchesDirectly, - OpensUi, -} - #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum ParsedSlashCommand { Bare(SlashCommandBareBehavior), @@ -49,66 +45,22 @@ impl SlashCommandUsageError { } impl SlashCommand { - pub(crate) fn bare_behavior(self) -> SlashCommandBareBehavior { - match self { - SlashCommand::Model - | SlashCommand::Approvals - | SlashCommand::Permissions - | SlashCommand::Experimental - | SlashCommand::Skills - | SlashCommand::Review - | SlashCommand::Rename - | SlashCommand::Resume - | SlashCommand::Collab - | SlashCommand::Agent - | SlashCommand::Statusline - | SlashCommand::Theme - | SlashCommand::Feedback - | SlashCommand::Personality - | SlashCommand::Settings - | SlashCommand::MultiAgents => SlashCommandBareBehavior::OpensUi, - SlashCommand::Fast - | SlashCommand::ElevateSandbox - | SlashCommand::SandboxReadRoot - | SlashCommand::New - | SlashCommand::Fork - | SlashCommand::Init - | SlashCommand::Compact - | SlashCommand::Plan - | SlashCommand::Diff - | SlashCommand::Copy - | SlashCommand::Mention - | SlashCommand::Status - | SlashCommand::DebugConfig - | SlashCommand::Title - | SlashCommand::Mcp - | SlashCommand::Apps - | SlashCommand::Plugins - | SlashCommand::Logout - | SlashCommand::Quit - | SlashCommand::Exit - | SlashCommand::Rollout - | SlashCommand::Ps - | SlashCommand::Stop - | SlashCommand::Clear - | SlashCommand::Realtime - | SlashCommand::TestApproval - | SlashCommand::MemoryDrop - | SlashCommand::MemoryUpdate => SlashCommandBareBehavior::DispatchesDirectly, - } - } - pub(crate) fn parse_invocation( self, args: &str, ) -> Result<ParsedSlashCommand, SlashCommandUsageError> { let trimmed = args.trim(); + let spec = self.spec(); if trimmed.is_empty() { - return Ok(ParsedSlashCommand::Bare(self.bare_behavior())); + return Ok(ParsedSlashCommand::Bare(spec.bare_behavior)); } - match self { - SlashCommand::Fast => match trimmed.to_ascii_lowercase().as_str() { + match spec.parse_kind { + SlashCommandParseKind::NoArgs => Err(SlashCommandUsageError { + command: self, + kind: SlashCommandUsageErrorKind::UnexpectedInlineArgs, + }), + SlashCommandParseKind::Fast => match trimmed.to_ascii_lowercase().as_str() { "on" => Ok(ParsedSlashCommand::Fast(FastSlashCommandArgs::On)), "off" => Ok(ParsedSlashCommand::Fast(FastSlashCommandArgs::Off)), "status" => Ok(ParsedSlashCommand::Fast(FastSlashCommandArgs::Status)), @@ -117,52 +69,10 @@ impl SlashCommand { kind: SlashCommandUsageErrorKind::InvalidInlineArgs, }), }, - SlashCommand::Rename => Ok(ParsedSlashCommand::Rename), - SlashCommand::Plan => Ok(ParsedSlashCommand::Plan), - SlashCommand::Review => Ok(ParsedSlashCommand::Review), - SlashCommand::SandboxReadRoot => Ok(ParsedSlashCommand::SandboxReadRoot), - SlashCommand::Model - | SlashCommand::Approvals - | SlashCommand::Permissions - | SlashCommand::ElevateSandbox - | SlashCommand::Experimental - | SlashCommand::Skills - | SlashCommand::New - | SlashCommand::Resume - | SlashCommand::Fork - | SlashCommand::Init - | SlashCommand::Compact - | SlashCommand::Collab - | SlashCommand::Agent - | SlashCommand::Diff - | SlashCommand::Copy - | SlashCommand::Mention - | SlashCommand::Status - | SlashCommand::DebugConfig - | SlashCommand::Title - | SlashCommand::Statusline - | SlashCommand::Theme - | SlashCommand::Mcp - | SlashCommand::Apps - | SlashCommand::Plugins - | SlashCommand::Logout - | SlashCommand::Quit - | SlashCommand::Exit - | SlashCommand::Feedback - | SlashCommand::Rollout - | SlashCommand::Ps - | SlashCommand::Stop - | SlashCommand::Clear - | SlashCommand::Personality - | SlashCommand::Realtime - | SlashCommand::Settings - | SlashCommand::TestApproval - | SlashCommand::MultiAgents - | SlashCommand::MemoryDrop - | SlashCommand::MemoryUpdate => Err(SlashCommandUsageError { - command: self, - kind: SlashCommandUsageErrorKind::UnexpectedInlineArgs, - }), + SlashCommandParseKind::Rename => Ok(ParsedSlashCommand::Rename), + SlashCommandParseKind::Plan => Ok(ParsedSlashCommand::Plan), + SlashCommandParseKind::Review => Ok(ParsedSlashCommand::Review), + SlashCommandParseKind::SandboxReadRoot => Ok(ParsedSlashCommand::SandboxReadRoot), } } } diff --git a/codex-rs/tui_app_server/src/slash_command.rs b/codex-rs/tui_app_server/src/slash_command.rs index 9d8bc6d114..578382296c 100644 --- a/codex-rs/tui_app_server/src/slash_command.rs +++ b/codex-rs/tui_app_server/src/slash_command.rs @@ -65,266 +65,411 @@ pub enum SlashCommand { MemoryUpdate, } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum SlashCommandBareBehavior { + DispatchesDirectly, + OpensUi, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum SlashCommandParseKind { + NoArgs, + Fast, + Rename, + Plan, + Review, + SandboxReadRoot, +} + impl SlashCommand { - fn spec(self) -> SlashCommandSpec { + pub(crate) fn spec(self) -> SlashCommandSpec { match self { SlashCommand::Model => SlashCommandSpec { description: "choose what model and reasoning effort to use", available_during_task: false, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/model"], + bare_behavior: SlashCommandBareBehavior::OpensUi, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Fast => SlashCommandSpec { description: "toggle Fast mode to enable fastest inference at 2X plan usage", available_during_task: false, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/fast", "/fast [on|off|status]"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::Fast, }, SlashCommand::Approvals => SlashCommandSpec { description: "choose what Codex is allowed to do", available_during_task: false, is_disabled: false, hide_in_command_popup: true, + usage_lines: &["/approvals"], + bare_behavior: SlashCommandBareBehavior::OpensUi, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Permissions => SlashCommandSpec { description: "choose what Codex is allowed to do", available_during_task: false, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/permissions"], + bare_behavior: SlashCommandBareBehavior::OpensUi, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::ElevateSandbox => SlashCommandSpec { description: "set up elevated agent sandbox", available_during_task: false, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/setup-default-sandbox"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::SandboxReadRoot => SlashCommandSpec { description: "let sandbox read a directory: /sandbox-add-read-dir <absolute_path>", available_during_task: false, is_disabled: !cfg!(target_os = "windows"), hide_in_command_popup: false, + usage_lines: &["/sandbox-add-read-dir <absolute-path>"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::SandboxReadRoot, }, SlashCommand::Experimental => SlashCommandSpec { description: "toggle experimental features", available_during_task: false, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/experimental"], + bare_behavior: SlashCommandBareBehavior::OpensUi, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Skills => SlashCommandSpec { description: "use skills to improve how Codex performs specific tasks", available_during_task: true, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/skills"], + bare_behavior: SlashCommandBareBehavior::OpensUi, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Review => SlashCommandSpec { description: "review my current changes and find issues", available_during_task: false, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/review", "/review <instructions>"], + bare_behavior: SlashCommandBareBehavior::OpensUi, + parse_kind: SlashCommandParseKind::Review, }, SlashCommand::Rename => SlashCommandSpec { description: "rename the current thread", available_during_task: true, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/rename", "/rename <title>"], + bare_behavior: SlashCommandBareBehavior::OpensUi, + parse_kind: SlashCommandParseKind::Rename, }, SlashCommand::New => SlashCommandSpec { description: "start a new chat during a conversation", available_during_task: false, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/new"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Resume => SlashCommandSpec { description: "resume a saved chat", available_during_task: false, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/resume"], + bare_behavior: SlashCommandBareBehavior::OpensUi, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Fork => SlashCommandSpec { description: "fork the current chat", available_during_task: false, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/fork"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Init => SlashCommandSpec { description: "create an AGENTS.md file with instructions for Codex", available_during_task: false, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/init"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Compact => SlashCommandSpec { description: "summarize conversation to prevent hitting the context limit", available_during_task: false, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/compact"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Plan => SlashCommandSpec { description: "switch to Plan mode", available_during_task: false, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/plan", "/plan <prompt>"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::Plan, }, SlashCommand::Collab => SlashCommandSpec { description: "change collaboration mode (experimental)", available_during_task: true, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/collab"], + bare_behavior: SlashCommandBareBehavior::OpensUi, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Agent => SlashCommandSpec { description: "switch the active agent thread", available_during_task: true, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/agent"], + bare_behavior: SlashCommandBareBehavior::OpensUi, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Diff => SlashCommandSpec { description: "show git diff (including untracked files)", available_during_task: true, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/diff"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Copy => SlashCommandSpec { description: "copy the latest Codex output to your clipboard", available_during_task: true, is_disabled: cfg!(target_os = "android"), hide_in_command_popup: false, + usage_lines: &["/copy"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Mention => SlashCommandSpec { description: "mention a file", available_during_task: true, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/mention"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Status => SlashCommandSpec { description: "show current session configuration and token usage", available_during_task: true, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/status"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::DebugConfig => SlashCommandSpec { description: "show config layers and requirement sources for debugging", available_during_task: true, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/debug-config"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Statusline => SlashCommandSpec { description: "configure which items appear in the status line", available_during_task: false, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/statusline"], + bare_behavior: SlashCommandBareBehavior::OpensUi, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Theme => SlashCommandSpec { description: "choose a syntax highlighting theme", available_during_task: false, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/theme"], + bare_behavior: SlashCommandBareBehavior::OpensUi, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Mcp => SlashCommandSpec { description: "list configured MCP tools", available_during_task: true, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/mcp"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Apps => SlashCommandSpec { description: "manage apps", available_during_task: true, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/apps"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Plugins => SlashCommandSpec { description: "browse plugins", available_during_task: true, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/plugins"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Logout => SlashCommandSpec { description: "log out of Codex", available_during_task: false, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/logout"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Quit => SlashCommandSpec { description: "exit Codex", available_during_task: true, is_disabled: false, hide_in_command_popup: true, + usage_lines: &["/quit"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Exit => SlashCommandSpec { description: "exit Codex", available_during_task: true, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/exit"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Feedback => SlashCommandSpec { description: "send logs to maintainers", available_during_task: true, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/feedback"], + bare_behavior: SlashCommandBareBehavior::OpensUi, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Rollout => SlashCommandSpec { description: "print the rollout file path", available_during_task: true, is_disabled: !cfg!(debug_assertions), hide_in_command_popup: false, + usage_lines: &["/rollout"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Ps => SlashCommandSpec { description: "list background terminals", available_during_task: true, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/ps"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Stop => SlashCommandSpec { description: "stop all background terminals", available_during_task: true, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/stop"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Clear => SlashCommandSpec { description: "clear the terminal and start a new chat", available_during_task: false, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/clear"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Personality => SlashCommandSpec { description: "choose a communication style for Codex", available_during_task: false, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/personality"], + bare_behavior: SlashCommandBareBehavior::OpensUi, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Realtime => SlashCommandSpec { description: "toggle realtime voice mode (experimental)", available_during_task: true, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/realtime"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::Settings => SlashCommandSpec { description: "configure realtime microphone/speaker", available_during_task: true, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/settings"], + bare_behavior: SlashCommandBareBehavior::OpensUi, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::TestApproval => SlashCommandSpec { description: "test approval request", available_during_task: true, is_disabled: !cfg!(debug_assertions), hide_in_command_popup: false, + usage_lines: &["/test-approval"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::MultiAgents => SlashCommandSpec { description: "switch the active agent thread", available_during_task: true, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/subagents"], + bare_behavior: SlashCommandBareBehavior::OpensUi, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::MemoryDrop => SlashCommandSpec { description: "DO NOT USE", available_during_task: false, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/debug-m-drop"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::NoArgs, }, SlashCommand::MemoryUpdate => SlashCommandSpec { description: "DO NOT USE", available_during_task: false, is_disabled: false, hide_in_command_popup: false, + usage_lines: &["/debug-m-update"], + bare_behavior: SlashCommandBareBehavior::DispatchesDirectly, + parse_kind: SlashCommandParseKind::NoArgs, }, } } @@ -342,51 +487,7 @@ impl SlashCommand { /// User-visible usage forms for this command. pub(crate) fn usage_lines(self) -> &'static [&'static str] { - match self { - SlashCommand::Model => &["/model"], - SlashCommand::Fast => &["/fast", "/fast [on|off|status]"], - SlashCommand::Approvals => &["/approvals"], - SlashCommand::Permissions => &["/permissions"], - SlashCommand::ElevateSandbox => &["/setup-default-sandbox"], - SlashCommand::SandboxReadRoot => &["/sandbox-add-read-dir <absolute-path>"], - SlashCommand::Experimental => &["/experimental"], - SlashCommand::Skills => &["/skills"], - SlashCommand::Review => &["/review", "/review <instructions>"], - SlashCommand::Rename => &["/rename", "/rename <title>"], - SlashCommand::New => &["/new"], - SlashCommand::Resume => &["/resume"], - SlashCommand::Fork => &["/fork"], - SlashCommand::Init => &["/init"], - SlashCommand::Compact => &["/compact"], - SlashCommand::Plan => &["/plan", "/plan <prompt>"], - SlashCommand::Collab => &["/collab"], - SlashCommand::Agent => &["/agent"], - SlashCommand::Diff => &["/diff"], - SlashCommand::Copy => &["/copy"], - SlashCommand::Mention => &["/mention"], - SlashCommand::Status => &["/status"], - SlashCommand::DebugConfig => &["/debug-config"], - SlashCommand::Statusline => &["/statusline"], - SlashCommand::Theme => &["/theme"], - SlashCommand::Mcp => &["/mcp"], - SlashCommand::Apps => &["/apps"], - SlashCommand::Plugins => &["/plugins"], - SlashCommand::Logout => &["/logout"], - SlashCommand::Quit => &["/quit"], - SlashCommand::Exit => &["/exit"], - SlashCommand::Feedback => &["/feedback"], - SlashCommand::Rollout => &["/rollout"], - SlashCommand::Ps => &["/ps"], - SlashCommand::Stop => &["/stop"], - SlashCommand::Clear => &["/clear"], - SlashCommand::Personality => &["/personality"], - SlashCommand::Realtime => &["/realtime"], - SlashCommand::Settings => &["/settings"], - SlashCommand::TestApproval => &["/test-approval"], - SlashCommand::MultiAgents => &["/subagents"], - SlashCommand::MemoryDrop => &["/debug-m-drop"], - SlashCommand::MemoryUpdate => &["/debug-m-update"], - } + self.spec().usage_lines } /// Whether this command can be run while a task is in progress. @@ -415,11 +516,14 @@ pub fn built_in_slash_commands() -> Vec<(&'static str, SlashCommand)> { .collect() } -struct SlashCommandSpec { - description: &'static str, - available_during_task: bool, - is_disabled: bool, - hide_in_command_popup: bool, +pub(crate) struct SlashCommandSpec { + pub(crate) description: &'static str, + pub(crate) available_during_task: bool, + pub(crate) is_disabled: bool, + pub(crate) hide_in_command_popup: bool, + pub(crate) usage_lines: &'static [&'static str], + pub(crate) bare_behavior: SlashCommandBareBehavior, + pub(crate) parse_kind: SlashCommandParseKind, } #[cfg(test)] diff --git a/codex-rs/tui_app_server/src/slash_command_input.rs b/codex-rs/tui_app_server/src/slash_command_input.rs index f76cddc696..b288517c42 100644 --- a/codex-rs/tui_app_server/src/slash_command_input.rs +++ b/codex-rs/tui_app_server/src/slash_command_input.rs @@ -1,4 +1,6 @@ use crate::slash_command::SlashCommand; +use crate::slash_command::SlashCommandBareBehavior; +use crate::slash_command::SlashCommandParseKind; #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum FastSlashCommandArgs { @@ -7,12 +9,6 @@ pub(crate) enum FastSlashCommandArgs { Status, } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum SlashCommandBareBehavior { - DispatchesDirectly, - OpensUi, -} - #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum ParsedSlashCommand { Bare(SlashCommandBareBehavior), @@ -49,65 +45,22 @@ impl SlashCommandUsageError { } impl SlashCommand { - pub(crate) fn bare_behavior(self) -> SlashCommandBareBehavior { - match self { - SlashCommand::Model - | SlashCommand::Approvals - | SlashCommand::Permissions - | SlashCommand::Experimental - | SlashCommand::Skills - | SlashCommand::Review - | SlashCommand::Rename - | SlashCommand::Resume - | SlashCommand::Collab - | SlashCommand::Agent - | SlashCommand::Statusline - | SlashCommand::Theme - | SlashCommand::Feedback - | SlashCommand::Personality - | SlashCommand::Settings - | SlashCommand::MultiAgents => SlashCommandBareBehavior::OpensUi, - SlashCommand::Fast - | SlashCommand::ElevateSandbox - | SlashCommand::SandboxReadRoot - | SlashCommand::New - | SlashCommand::Fork - | SlashCommand::Init - | SlashCommand::Compact - | SlashCommand::Plan - | SlashCommand::Diff - | SlashCommand::Copy - | SlashCommand::Mention - | SlashCommand::Status - | SlashCommand::DebugConfig - | SlashCommand::Mcp - | SlashCommand::Apps - | SlashCommand::Plugins - | SlashCommand::Logout - | SlashCommand::Quit - | SlashCommand::Exit - | SlashCommand::Rollout - | SlashCommand::Ps - | SlashCommand::Stop - | SlashCommand::Clear - | SlashCommand::Realtime - | SlashCommand::TestApproval - | SlashCommand::MemoryDrop - | SlashCommand::MemoryUpdate => SlashCommandBareBehavior::DispatchesDirectly, - } - } - pub(crate) fn parse_invocation( self, args: &str, ) -> Result<ParsedSlashCommand, SlashCommandUsageError> { let trimmed = args.trim(); + let spec = self.spec(); if trimmed.is_empty() { - return Ok(ParsedSlashCommand::Bare(self.bare_behavior())); + return Ok(ParsedSlashCommand::Bare(spec.bare_behavior)); } - match self { - SlashCommand::Fast => match trimmed.to_ascii_lowercase().as_str() { + match spec.parse_kind { + SlashCommandParseKind::NoArgs => Err(SlashCommandUsageError { + command: self, + kind: SlashCommandUsageErrorKind::UnexpectedInlineArgs, + }), + SlashCommandParseKind::Fast => match trimmed.to_ascii_lowercase().as_str() { "on" => Ok(ParsedSlashCommand::Fast(FastSlashCommandArgs::On)), "off" => Ok(ParsedSlashCommand::Fast(FastSlashCommandArgs::Off)), "status" => Ok(ParsedSlashCommand::Fast(FastSlashCommandArgs::Status)), @@ -116,51 +69,10 @@ impl SlashCommand { kind: SlashCommandUsageErrorKind::InvalidInlineArgs, }), }, - SlashCommand::Rename => Ok(ParsedSlashCommand::Rename), - SlashCommand::Plan => Ok(ParsedSlashCommand::Plan), - SlashCommand::Review => Ok(ParsedSlashCommand::Review), - SlashCommand::SandboxReadRoot => Ok(ParsedSlashCommand::SandboxReadRoot), - SlashCommand::Model - | SlashCommand::Approvals - | SlashCommand::Permissions - | SlashCommand::ElevateSandbox - | SlashCommand::Experimental - | SlashCommand::Skills - | SlashCommand::New - | SlashCommand::Resume - | SlashCommand::Fork - | SlashCommand::Init - | SlashCommand::Compact - | SlashCommand::Collab - | SlashCommand::Agent - | SlashCommand::Diff - | SlashCommand::Copy - | SlashCommand::Mention - | SlashCommand::Status - | SlashCommand::DebugConfig - | SlashCommand::Statusline - | SlashCommand::Theme - | SlashCommand::Mcp - | SlashCommand::Apps - | SlashCommand::Plugins - | SlashCommand::Logout - | SlashCommand::Quit - | SlashCommand::Exit - | SlashCommand::Feedback - | SlashCommand::Rollout - | SlashCommand::Ps - | SlashCommand::Stop - | SlashCommand::Clear - | SlashCommand::Personality - | SlashCommand::Realtime - | SlashCommand::Settings - | SlashCommand::TestApproval - | SlashCommand::MultiAgents - | SlashCommand::MemoryDrop - | SlashCommand::MemoryUpdate => Err(SlashCommandUsageError { - command: self, - kind: SlashCommandUsageErrorKind::UnexpectedInlineArgs, - }), + SlashCommandParseKind::Rename => Ok(ParsedSlashCommand::Rename), + SlashCommandParseKind::Plan => Ok(ParsedSlashCommand::Plan), + SlashCommandParseKind::Review => Ok(ParsedSlashCommand::Review), + SlashCommandParseKind::SandboxReadRoot => Ok(ParsedSlashCommand::SandboxReadRoot), } } }