tui: simplify slash command policy wiring

Centralize slash command metadata, dedupe runtime builtin gating, and make popup-driven queued replay resume explicit.

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Charles Cunningham
2026-03-15 23:36:33 -07:00
co-authored by Codex
parent 5d81980593
commit 9ec1452655
4 changed files with 415 additions and 248 deletions
+21 -4
View File
@@ -730,6 +730,7 @@ pub(crate) struct App {
primary_thread_id: Option<ThreadId>,
primary_session_configured: Option<SessionConfiguredEvent>,
pending_primary_events: VecDeque<Event>,
resume_queued_inputs_after_app_events: bool,
}
#[derive(Default)]
@@ -757,6 +758,19 @@ fn normalize_harness_overrides_for_cwd(
}
impl App {
fn on_bottom_pane_view_completed(&mut self) {
self.resume_queued_inputs_after_app_events = true;
}
fn maybe_resume_queued_inputs_after_app_events(&mut self, app_events_drained: bool) {
if !self.resume_queued_inputs_after_app_events || !app_events_drained {
return;
}
self.resume_queued_inputs_after_app_events = false;
self.chat_widget.maybe_resume_queued_inputs_when_idle();
}
pub fn chatwidget_init_for_forked_or_resumed_thread(
&self,
tui: &mut tui::Tui,
@@ -2293,6 +2307,7 @@ impl App {
primary_thread_id: None,
primary_session_configured: None,
pending_primary_events: VecDeque::new(),
resume_queued_inputs_after_app_events: false,
};
// On startup, if Agent mode (workspace-write) or ReadOnly is active, warn about world-writable dirs on Windows.
@@ -2425,9 +2440,7 @@ impl App {
// or popup flows settle. Only resume once the app-event queue is fully drained so
// multi-event commands (for example approvals updates) cannot interleave later
// queued input.
if app_event_rx.is_empty() {
app.chat_widget.maybe_resume_queued_inputs_when_idle();
}
app.maybe_resume_queued_inputs_after_app_events(app_event_rx.is_empty());
match control {
AppRunControl::Continue => {}
AppRunControl::Exit(reason) => break Ok(reason),
@@ -2772,7 +2785,9 @@ impl App {
self.chat_widget.handle_serialized_slash_command(draft);
self.refresh_status_line();
}
AppEvent::BottomPaneViewCompleted => {}
AppEvent::BottomPaneViewCompleted => {
self.on_bottom_pane_view_completed();
}
AppEvent::UpdateCollaborationMode(mask) => {
self.chat_widget.set_collaboration_mask(mask);
self.refresh_status_line();
@@ -6549,6 +6564,7 @@ guardian_approval = true
primary_thread_id: None,
primary_session_configured: None,
pending_primary_events: VecDeque::new(),
resume_queued_inputs_after_app_events: false,
}
}
@@ -6609,6 +6625,7 @@ guardian_approval = true
primary_thread_id: None,
primary_session_configured: None,
pending_primary_events: VecDeque::new(),
resume_queued_inputs_after_app_events: false,
},
rx,
op_rx,
+9 -20
View File
@@ -12,12 +12,6 @@ use crate::render::RectExt;
use crate::slash_command::SlashCommand;
use codex_protocol::custom_prompts::CustomPrompt;
use codex_protocol::custom_prompts::PROMPTS_CMD_PREFIX;
use std::collections::HashSet;
// Hide alias commands in the default popup list so each unique action appears once.
// `quit` is an alias of `exit`, so we skip `quit` here.
// `approvals` is an alias of `permissions`.
const ALIAS_COMMANDS: &[SlashCommand] = &[SlashCommand::Quit, SlashCommand::Approvals];
/// A selectable item in the popup: either a built-in command or a user prompt.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
@@ -30,6 +24,7 @@ pub(crate) enum CommandItem {
pub(crate) struct CommandPopup {
command_filter: String,
builtins: Vec<SlashCommand>,
reserved_builtin_names: std::collections::HashSet<String>,
prompts: Vec<CustomPrompt>,
state: ScrollState,
}
@@ -62,25 +57,27 @@ impl From<CommandPopupFlags> for slash_commands::BuiltinCommandFlags {
impl CommandPopup {
pub(crate) fn new(mut prompts: Vec<CustomPrompt>, flags: CommandPopupFlags) -> Self {
// Keep built-in availability in sync with the composer.
let builtins: Vec<SlashCommand> = slash_commands::visible_builtins_for_input(flags.into())
let builtin_flags = flags.into();
let builtins = slash_commands::visible_builtins_for_input(builtin_flags)
.into_iter()
.filter(|cmd| !cmd.command().starts_with("debug"))
.collect();
// Exclude prompts that collide with builtin command names and sort by name.
let exclude = reserved_builtin_names(&builtins);
prompts.retain(|p| !exclude.contains(&p.name));
let reserved_builtin_names =
slash_commands::reserved_builtin_names_for_input(builtin_flags);
prompts.retain(|p| !reserved_builtin_names.contains(&p.name));
prompts.sort_by(|a, b| a.name.cmp(&b.name));
Self {
command_filter: String::new(),
builtins,
reserved_builtin_names,
prompts,
state: ScrollState::new(),
}
}
pub(crate) fn set_prompts(&mut self, mut prompts: Vec<CustomPrompt>) {
let exclude = reserved_builtin_names(&self.builtins);
prompts.retain(|p| !exclude.contains(&p.name));
prompts.retain(|p| !self.reserved_builtin_names.contains(&p.name));
prompts.sort_by(|a, b| a.name.cmp(&b.name));
self.prompts = prompts;
}
@@ -138,7 +135,7 @@ impl CommandPopup {
if filter.is_empty() {
// Built-ins first, in presentation order.
for cmd in self.builtins.iter() {
if ALIAS_COMMANDS.contains(cmd) {
if !cmd.show_in_command_popup() {
continue;
}
out.push((CommandItem::Builtin(*cmd), None));
@@ -265,14 +262,6 @@ impl CommandPopup {
}
}
fn reserved_builtin_names(builtins: &[SlashCommand]) -> HashSet<String> {
builtins
.iter()
.flat_map(|cmd| std::iter::once(cmd.command()).chain(cmd.command_aliases().iter().copied()))
.map(str::to_string)
.collect()
}
impl WidgetRef for CommandPopup {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
let rows = self.rows_from_matches(self.filtered());
+41 -24
View File
@@ -3,7 +3,7 @@
//! The same sandbox- and feature-gating rules are used by both the composer
//! and the command popup. Centralizing them here keeps those call sites small
//! and ensures they stay in sync.
use std::str::FromStr;
use std::collections::HashSet;
use codex_utils_fuzzy_match::fuzzy_match;
@@ -22,20 +22,38 @@ pub(crate) struct BuiltinCommandFlags {
pub(crate) allow_elevate_sandbox: bool,
}
fn command_enabled_for_input(cmd: SlashCommand, flags: BuiltinCommandFlags) -> bool {
if !flags.allow_elevate_sandbox && cmd == SlashCommand::ElevateSandbox {
return false;
}
if !flags.collaboration_modes_enabled
&& matches!(cmd, SlashCommand::Collab | SlashCommand::Plan)
{
return false;
}
if !flags.connectors_enabled && cmd == SlashCommand::Apps {
return false;
}
if !flags.fast_command_enabled && cmd == SlashCommand::Fast {
return false;
}
if !flags.personality_command_enabled && cmd == SlashCommand::Personality {
return false;
}
if !flags.realtime_conversation_enabled && cmd == SlashCommand::Realtime {
return false;
}
if !flags.audio_device_selection_enabled && cmd == SlashCommand::Settings {
return false;
}
true
}
/// Return the built-ins that should be visible/usable for the current input.
pub(crate) fn builtins_for_input(flags: BuiltinCommandFlags) -> Vec<(&'static str, SlashCommand)> {
built_in_slash_commands()
.into_iter()
.filter(|(_, cmd)| flags.allow_elevate_sandbox || *cmd != SlashCommand::ElevateSandbox)
.filter(|(_, cmd)| {
flags.collaboration_modes_enabled
|| !matches!(*cmd, SlashCommand::Collab | SlashCommand::Plan)
})
.filter(|(_, cmd)| flags.connectors_enabled || *cmd != SlashCommand::Apps)
.filter(|(_, cmd)| flags.fast_command_enabled || *cmd != SlashCommand::Fast)
.filter(|(_, cmd)| flags.personality_command_enabled || *cmd != SlashCommand::Personality)
.filter(|(_, cmd)| flags.realtime_conversation_enabled || *cmd != SlashCommand::Realtime)
.filter(|(_, cmd)| flags.audio_device_selection_enabled || *cmd != SlashCommand::Settings)
.filter(|(_, cmd)| command_enabled_for_input(*cmd, flags))
.collect()
}
@@ -43,26 +61,25 @@ pub(crate) fn builtins_for_input(flags: BuiltinCommandFlags) -> Vec<(&'static st
pub(crate) fn visible_builtins_for_input(flags: BuiltinCommandFlags) -> Vec<SlashCommand> {
visible_built_in_slash_commands()
.into_iter()
.filter(|cmd| flags.allow_elevate_sandbox || *cmd != SlashCommand::ElevateSandbox)
.filter(|cmd| {
flags.collaboration_modes_enabled
|| !matches!(*cmd, SlashCommand::Collab | SlashCommand::Plan)
})
.filter(|cmd| flags.connectors_enabled || *cmd != SlashCommand::Apps)
.filter(|cmd| flags.fast_command_enabled || *cmd != SlashCommand::Fast)
.filter(|cmd| flags.personality_command_enabled || *cmd != SlashCommand::Personality)
.filter(|cmd| flags.realtime_conversation_enabled || *cmd != SlashCommand::Realtime)
.filter(|cmd| flags.audio_device_selection_enabled || *cmd != SlashCommand::Settings)
.filter(|cmd| command_enabled_for_input(*cmd, flags))
.collect()
}
/// Find a single built-in command by exact name, after applying the gating rules.
pub(crate) fn find_builtin_command(name: &str, flags: BuiltinCommandFlags) -> Option<SlashCommand> {
let cmd = SlashCommand::from_str(name).ok()?;
builtins_for_input(flags)
.into_iter()
.any(|(_, visible_cmd)| visible_cmd == cmd)
.then_some(cmd)
.find(|(command_name, _)| *command_name == name)
.map(|(_, cmd)| cmd)
}
/// Return every builtin name that should be reserved against custom prompt collisions.
pub(crate) fn reserved_builtin_names_for_input(flags: BuiltinCommandFlags) -> HashSet<String> {
visible_builtins_for_input(flags)
.into_iter()
.flat_map(SlashCommand::all_command_names)
.map(str::to_string)
.collect()
}
/// Whether any visible built-in fuzzily matches the provided prefix.
+344 -200
View File
@@ -66,54 +66,330 @@ pub enum SlashCommand {
}
impl SlashCommand {
fn spec(self) -> SlashCommandSpec {
match self {
SlashCommand::Help => SlashCommandSpec {
description: "show slash command help",
help_forms: &[""],
requires_interaction: false,
execution_kind: SlashCommandExecutionKind::Immediate,
show_in_command_popup: true,
},
SlashCommand::Model => SlashCommandSpec {
description: "choose what model and reasoning effort to use",
help_forms: &[
"",
"<model> [default|none|minimal|low|medium|high|xhigh] [plan-only|all-modes]",
],
requires_interaction: true,
execution_kind: SlashCommandExecutionKind::ChangesTurnContext,
show_in_command_popup: true,
},
SlashCommand::Fast => SlashCommandSpec {
description: "toggle Fast mode to enable fastest inference at 2X plan usage",
help_forms: &["", "<on|off|status>"],
requires_interaction: false,
execution_kind: SlashCommandExecutionKind::ChangesTurnContext,
show_in_command_popup: true,
},
SlashCommand::Approvals => SlashCommandSpec {
description: "choose what Codex is allowed to do",
help_forms: &[
"",
"<read-only|auto|full-access> [--smart-approvals] [--confirm-full-access] [--remember-full-access] [--confirm-world-writable] [--remember-world-writable] [--enable-windows-sandbox=elevated|legacy]",
],
requires_interaction: true,
execution_kind: SlashCommandExecutionKind::ChangesTurnContext,
show_in_command_popup: false,
},
SlashCommand::Permissions => SlashCommandSpec {
description: "choose what Codex is allowed to do",
help_forms: &[
"",
"<read-only|auto|full-access> [--smart-approvals] [--confirm-full-access] [--remember-full-access] [--confirm-world-writable] [--remember-world-writable] [--enable-windows-sandbox=elevated|legacy]",
],
requires_interaction: true,
execution_kind: SlashCommandExecutionKind::ChangesTurnContext,
show_in_command_popup: true,
},
SlashCommand::ElevateSandbox => SlashCommandSpec {
description: "set up elevated agent sandbox",
help_forms: &[""],
requires_interaction: false,
execution_kind: SlashCommandExecutionKind::ChangesTurnContext,
show_in_command_popup: true,
},
SlashCommand::SandboxReadRoot => SlashCommandSpec {
description: "let sandbox read a directory: /sandbox-add-read-dir <absolute_path>",
help_forms: &["<absolute-directory-path>"],
requires_interaction: false,
execution_kind: SlashCommandExecutionKind::ChangesTurnContext,
show_in_command_popup: true,
},
SlashCommand::Experimental => SlashCommandSpec {
description: "toggle experimental features",
help_forms: &["", "<feature-key>=on|off ..."],
requires_interaction: true,
execution_kind: SlashCommandExecutionKind::ChangesTurnContext,
show_in_command_popup: true,
},
SlashCommand::Skills => SlashCommandSpec {
description: "use skills to improve how Codex performs specific tasks",
help_forms: &["", "<list|manage>"],
requires_interaction: true,
execution_kind: SlashCommandExecutionKind::Immediate,
show_in_command_popup: true,
},
SlashCommand::Review => SlashCommandSpec {
description: "review my current changes and find issues",
help_forms: &[
"",
"uncommitted",
"branch <name>",
"commit <sha> [title]",
"<instructions>",
],
requires_interaction: true,
execution_kind: SlashCommandExecutionKind::ChangesTurnContext,
show_in_command_popup: true,
},
SlashCommand::Rename => SlashCommandSpec {
description: "rename the current thread",
help_forms: &["", "<title...>"],
requires_interaction: true,
execution_kind: SlashCommandExecutionKind::Immediate,
show_in_command_popup: true,
},
SlashCommand::New => SlashCommandSpec {
description: "start a new chat during a conversation",
help_forms: &[""],
requires_interaction: false,
execution_kind: SlashCommandExecutionKind::ChangesTurnContext,
show_in_command_popup: true,
},
SlashCommand::Resume => SlashCommandSpec {
description: "resume a saved chat",
help_forms: &["", "<thread-id>", "<thread-id> --path <rollout-path>"],
requires_interaction: true,
execution_kind: SlashCommandExecutionKind::ChangesTurnContext,
show_in_command_popup: true,
},
SlashCommand::Fork => SlashCommandSpec {
description: "fork the current chat",
help_forms: &[""],
requires_interaction: false,
execution_kind: SlashCommandExecutionKind::ChangesTurnContext,
show_in_command_popup: true,
},
SlashCommand::Init => SlashCommandSpec {
description: "create an AGENTS.md file with instructions for Codex",
help_forms: &[""],
requires_interaction: false,
execution_kind: SlashCommandExecutionKind::JustLikeUserMessage,
show_in_command_popup: true,
},
SlashCommand::Compact => SlashCommandSpec {
description: "summarize conversation to prevent hitting the context limit",
help_forms: &[""],
requires_interaction: false,
execution_kind: SlashCommandExecutionKind::ChangesTurnContext,
show_in_command_popup: true,
},
SlashCommand::Plan => SlashCommandSpec {
description: "switch to Plan mode",
help_forms: &["", "<prompt...>"],
requires_interaction: false,
execution_kind: SlashCommandExecutionKind::JustLikeUserMessage,
show_in_command_popup: true,
},
SlashCommand::Collab => SlashCommandSpec {
description: "change collaboration mode (experimental)",
help_forms: &["", "<default|plan>"],
requires_interaction: true,
execution_kind: SlashCommandExecutionKind::Immediate,
show_in_command_popup: true,
},
SlashCommand::Agent => SlashCommandSpec {
description: "switch the active agent thread",
help_forms: &["", "<thread-id>"],
requires_interaction: true,
execution_kind: SlashCommandExecutionKind::Immediate,
show_in_command_popup: true,
},
SlashCommand::Diff => SlashCommandSpec {
description: "show git diff (including untracked files)",
help_forms: &[""],
requires_interaction: false,
execution_kind: SlashCommandExecutionKind::Immediate,
show_in_command_popup: true,
},
SlashCommand::Copy => SlashCommandSpec {
description: "copy the latest Codex output to your clipboard",
help_forms: &[""],
requires_interaction: false,
execution_kind: SlashCommandExecutionKind::Immediate,
show_in_command_popup: true,
},
SlashCommand::Mention => SlashCommandSpec {
description: "mention a file",
help_forms: &[""],
requires_interaction: false,
execution_kind: SlashCommandExecutionKind::Immediate,
show_in_command_popup: true,
},
SlashCommand::Status => SlashCommandSpec {
description: "show current session configuration and token usage",
help_forms: &[""],
requires_interaction: false,
execution_kind: SlashCommandExecutionKind::Immediate,
show_in_command_popup: true,
},
SlashCommand::DebugConfig => SlashCommandSpec {
description: "show config layers and requirement sources for debugging",
help_forms: &[""],
requires_interaction: false,
execution_kind: SlashCommandExecutionKind::Immediate,
show_in_command_popup: true,
},
SlashCommand::Statusline => SlashCommandSpec {
description: "configure which items appear in the status line",
help_forms: &["", "<item-id>...", "none"],
requires_interaction: true,
execution_kind: SlashCommandExecutionKind::ChangesTurnContext,
show_in_command_popup: true,
},
SlashCommand::Theme => SlashCommandSpec {
description: "choose a syntax highlighting theme",
help_forms: &["", "<theme-name>"],
requires_interaction: true,
execution_kind: SlashCommandExecutionKind::ChangesTurnContext,
show_in_command_popup: true,
},
SlashCommand::Mcp => SlashCommandSpec {
description: "list configured MCP tools",
help_forms: &[""],
requires_interaction: false,
execution_kind: SlashCommandExecutionKind::Immediate,
show_in_command_popup: true,
},
SlashCommand::Apps => SlashCommandSpec {
description: "manage apps",
help_forms: &[""],
requires_interaction: false,
execution_kind: SlashCommandExecutionKind::Immediate,
show_in_command_popup: true,
},
SlashCommand::Logout => SlashCommandSpec {
description: "log out of Codex",
help_forms: &[""],
requires_interaction: false,
execution_kind: SlashCommandExecutionKind::ChangesTurnContext,
show_in_command_popup: true,
},
SlashCommand::Quit => SlashCommandSpec {
description: "exit Codex",
help_forms: &[""],
requires_interaction: false,
execution_kind: SlashCommandExecutionKind::Immediate,
show_in_command_popup: false,
},
SlashCommand::Exit => SlashCommandSpec {
description: "exit Codex",
help_forms: &[""],
requires_interaction: false,
execution_kind: SlashCommandExecutionKind::Immediate,
show_in_command_popup: true,
},
SlashCommand::Feedback => SlashCommandSpec {
description: "send logs to maintainers",
help_forms: &["", "<bug|bad-result|good-result|safety-check|other>"],
requires_interaction: true,
execution_kind: SlashCommandExecutionKind::Immediate,
show_in_command_popup: true,
},
SlashCommand::Rollout => SlashCommandSpec {
description: "print the rollout file path",
help_forms: &[""],
requires_interaction: false,
execution_kind: SlashCommandExecutionKind::Immediate,
show_in_command_popup: true,
},
SlashCommand::Ps => SlashCommandSpec {
description: "list background terminals",
help_forms: &[""],
requires_interaction: false,
execution_kind: SlashCommandExecutionKind::Immediate,
show_in_command_popup: true,
},
SlashCommand::Stop => SlashCommandSpec {
description: "stop all background terminals",
help_forms: &[""],
requires_interaction: false,
execution_kind: SlashCommandExecutionKind::Immediate,
show_in_command_popup: true,
},
SlashCommand::Clear => SlashCommandSpec {
description: "clear the terminal and start a new chat",
help_forms: &[""],
requires_interaction: false,
execution_kind: SlashCommandExecutionKind::ChangesTurnContext,
show_in_command_popup: true,
},
SlashCommand::Personality => SlashCommandSpec {
description: "choose a communication style for Codex",
help_forms: &["", "<none|friendly|pragmatic>"],
requires_interaction: true,
execution_kind: SlashCommandExecutionKind::ChangesTurnContext,
show_in_command_popup: true,
},
SlashCommand::Realtime => SlashCommandSpec {
description: "toggle realtime voice mode (experimental)",
help_forms: &[""],
requires_interaction: false,
execution_kind: SlashCommandExecutionKind::Immediate,
show_in_command_popup: true,
},
SlashCommand::Settings => SlashCommandSpec {
description: "configure realtime microphone/speaker",
help_forms: &["", "<microphone|speaker> [default|<device-name>]"],
requires_interaction: true,
execution_kind: SlashCommandExecutionKind::Immediate,
show_in_command_popup: true,
},
SlashCommand::TestApproval => SlashCommandSpec {
description: "test approval request",
help_forms: &[""],
requires_interaction: false,
execution_kind: SlashCommandExecutionKind::Immediate,
show_in_command_popup: true,
},
SlashCommand::MultiAgents => SlashCommandSpec {
description: "switch the active agent thread",
help_forms: &["", "<thread-id>"],
requires_interaction: true,
execution_kind: SlashCommandExecutionKind::Immediate,
show_in_command_popup: true,
},
SlashCommand::MemoryDrop => SlashCommandSpec {
description: "DO NOT USE",
help_forms: &[""],
requires_interaction: false,
execution_kind: SlashCommandExecutionKind::ChangesTurnContext,
show_in_command_popup: true,
},
SlashCommand::MemoryUpdate => SlashCommandSpec {
description: "DO NOT USE",
help_forms: &[""],
requires_interaction: false,
execution_kind: SlashCommandExecutionKind::ChangesTurnContext,
show_in_command_popup: true,
},
}
}
/// User-visible description shown in the popup.
pub fn description(self) -> &'static str {
match self {
SlashCommand::Help => "show slash command help",
SlashCommand::Feedback => "send logs to maintainers",
SlashCommand::New => "start a new chat during a conversation",
SlashCommand::Init => "create an AGENTS.md file with instructions for Codex",
SlashCommand::Compact => "summarize conversation to prevent hitting the context limit",
SlashCommand::Review => "review my current changes and find issues",
SlashCommand::Rename => "rename the current thread",
SlashCommand::Resume => "resume a saved chat",
SlashCommand::Clear => "clear the terminal and start a new chat",
SlashCommand::Fork => "fork the current chat",
// SlashCommand::Undo => "ask Codex to undo a turn",
SlashCommand::Quit | SlashCommand::Exit => "exit Codex",
SlashCommand::Diff => "show git diff (including untracked files)",
SlashCommand::Copy => "copy the latest Codex output to your clipboard",
SlashCommand::Mention => "mention a file",
SlashCommand::Skills => "use skills to improve how Codex performs specific tasks",
SlashCommand::Status => "show current session configuration and token usage",
SlashCommand::DebugConfig => "show config layers and requirement sources for debugging",
SlashCommand::Statusline => "configure which items appear in the status line",
SlashCommand::Theme => "choose a syntax highlighting theme",
SlashCommand::Ps => "list background terminals",
SlashCommand::Stop => "stop all background terminals",
SlashCommand::MemoryDrop => "DO NOT USE",
SlashCommand::MemoryUpdate => "DO NOT USE",
SlashCommand::Model => "choose what model and reasoning effort to use",
SlashCommand::Fast => "toggle Fast mode to enable fastest inference at 2X plan usage",
SlashCommand::Personality => "choose a communication style for Codex",
SlashCommand::Realtime => "toggle realtime voice mode (experimental)",
SlashCommand::Settings => "configure realtime microphone/speaker",
SlashCommand::Plan => "switch to Plan mode",
SlashCommand::Collab => "change collaboration mode (experimental)",
SlashCommand::Agent | SlashCommand::MultiAgents => "switch the active agent thread",
SlashCommand::Approvals => "choose what Codex is allowed to do",
SlashCommand::Permissions => "choose what Codex is allowed to do",
SlashCommand::ElevateSandbox => "set up elevated agent sandbox",
SlashCommand::SandboxReadRoot => {
"let sandbox read a directory: /sandbox-add-read-dir <absolute_path>"
}
SlashCommand::Experimental => "toggle experimental features",
SlashCommand::Mcp => "list configured MCP tools",
SlashCommand::Apps => "manage apps",
SlashCommand::Logout => "log out of Codex",
SlashCommand::Rollout => "print the rollout file path",
SlashCommand::TestApproval => "test approval request",
}
self.spec().description
}
/// Command string without the leading '/'. Provided for compatibility with
@@ -162,7 +438,6 @@ impl SlashCommand {
| SlashCommand::Feedback
| SlashCommand::Rollout
| SlashCommand::Ps
| SlashCommand::Stop
| SlashCommand::Clear
| SlashCommand::Personality
| SlashCommand::Realtime
@@ -170,166 +445,34 @@ impl SlashCommand {
| SlashCommand::TestApproval
| SlashCommand::MemoryDrop
| SlashCommand::MemoryUpdate => &[],
SlashCommand::Stop => &["clean"],
SlashCommand::MultiAgents => &["multi-agents"],
}
}
pub fn all_command_names(self) -> impl Iterator<Item = &'static str> {
std::iter::once(self.command()).chain(self.command_aliases().iter().copied())
}
/// Human-facing forms accepted by the TUI.
///
/// An empty string represents the bare `/command` form.
pub fn help_forms(self) -> &'static [&'static str] {
match self {
SlashCommand::Help => &[""],
SlashCommand::Model => &[
"",
"<model> [default|none|minimal|low|medium|high|xhigh] [plan-only|all-modes]",
],
SlashCommand::Fast => &["", "<on|off|status>"],
SlashCommand::Approvals | SlashCommand::Permissions => &[
"",
"<read-only|auto|full-access> [--smart-approvals] [--confirm-full-access] [--remember-full-access] [--confirm-world-writable] [--remember-world-writable] [--enable-windows-sandbox=elevated|legacy]",
],
SlashCommand::ElevateSandbox => &[""],
SlashCommand::SandboxReadRoot => &["<absolute-directory-path>"],
SlashCommand::Experimental => &["", "<feature-key>=on|off ..."],
SlashCommand::Skills => &["", "<list|manage>"],
SlashCommand::Review => &[
"",
"uncommitted",
"branch <name>",
"commit <sha> [title]",
"<instructions>",
],
SlashCommand::Rename => &["", "<title...>"],
SlashCommand::New => &[""],
SlashCommand::Resume => &["", "<thread-id>", "<thread-id> --path <rollout-path>"],
SlashCommand::Fork => &[""],
SlashCommand::Init => &[""],
SlashCommand::Compact => &[""],
SlashCommand::Plan => &["", "<prompt...>"],
SlashCommand::Collab => &["", "<default|plan>"],
SlashCommand::Agent | SlashCommand::MultiAgents => &["", "<thread-id>"],
SlashCommand::Diff => &[""],
SlashCommand::Copy => &[""],
SlashCommand::Mention => &[""],
SlashCommand::Status => &[""],
SlashCommand::DebugConfig => &[""],
SlashCommand::Statusline => &["", "<item-id>...", "none"],
SlashCommand::Theme => &["", "<theme-name>"],
SlashCommand::Mcp => &[""],
SlashCommand::Apps => &[""],
SlashCommand::Logout => &[""],
SlashCommand::Quit | SlashCommand::Exit => &[""],
SlashCommand::Feedback => &["", "<bug|bad-result|good-result|safety-check|other>"],
SlashCommand::Rollout => &[""],
SlashCommand::Ps => &[""],
SlashCommand::Stop => &[""],
SlashCommand::Clear => &[""],
SlashCommand::Personality => &["", "<none|friendly|pragmatic>"],
SlashCommand::Realtime => &[""],
SlashCommand::Settings => &["", "<microphone|speaker> [default|<device-name>]"],
SlashCommand::TestApproval => &[""],
SlashCommand::MemoryDrop | SlashCommand::MemoryUpdate => &[""],
}
self.spec().help_forms
}
/// Whether bare dispatch opens interactive UI that should be resolved before queueing.
pub fn requires_interaction(self) -> bool {
match self {
SlashCommand::Help => false,
SlashCommand::Feedback
| SlashCommand::Resume
| SlashCommand::Review
| SlashCommand::Rename
| SlashCommand::Model
| SlashCommand::Settings
| SlashCommand::Personality
| SlashCommand::Collab
| SlashCommand::Agent
| SlashCommand::MultiAgents
| SlashCommand::Approvals
| SlashCommand::Permissions
| SlashCommand::Experimental
| SlashCommand::Skills
| SlashCommand::Statusline
| SlashCommand::Theme => true,
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::Logout
| SlashCommand::Quit
| SlashCommand::Exit
| SlashCommand::Rollout
| SlashCommand::Ps
| SlashCommand::Stop
| SlashCommand::Clear
| SlashCommand::Realtime
| SlashCommand::TestApproval
| SlashCommand::MemoryDrop
| SlashCommand::MemoryUpdate => false,
}
self.spec().requires_interaction
}
/// How this command should behave when dispatched while another turn is running.
pub fn execution_kind(self) -> SlashCommandExecutionKind {
match self {
SlashCommand::Plan | SlashCommand::Init => {
SlashCommandExecutionKind::JustLikeUserMessage
}
SlashCommand::Model
| SlashCommand::Fast
| SlashCommand::Approvals
| SlashCommand::Permissions
| SlashCommand::ElevateSandbox
| SlashCommand::SandboxReadRoot
| SlashCommand::Experimental
| SlashCommand::Review
| SlashCommand::New
| SlashCommand::Resume
| SlashCommand::Fork
| SlashCommand::Compact
| SlashCommand::Clear
| SlashCommand::Logout
| SlashCommand::Personality
| SlashCommand::Statusline
| SlashCommand::Theme
| SlashCommand::MemoryDrop
| SlashCommand::MemoryUpdate => SlashCommandExecutionKind::ChangesTurnContext,
SlashCommand::Help
| SlashCommand::Skills
| SlashCommand::Rename
| SlashCommand::Collab
| SlashCommand::Agent
| SlashCommand::MultiAgents
| SlashCommand::Diff
| SlashCommand::Copy
| SlashCommand::Mention
| SlashCommand::Status
| SlashCommand::DebugConfig
| SlashCommand::Mcp
| SlashCommand::Apps
| SlashCommand::Quit
| SlashCommand::Exit
| SlashCommand::Feedback
| SlashCommand::Rollout
| SlashCommand::Ps
| SlashCommand::Stop
| SlashCommand::Realtime
| SlashCommand::Settings
| SlashCommand::TestApproval => SlashCommandExecutionKind::Immediate,
}
self.spec().execution_kind
}
pub fn show_in_command_popup(self) -> bool {
self.spec().show_in_command_popup
}
fn is_visible(self) -> bool {
@@ -361,19 +504,20 @@ pub enum SlashCommandExecutionKind {
Immediate,
}
#[derive(Clone, Copy)]
struct SlashCommandSpec {
description: &'static str,
help_forms: &'static [&'static str],
requires_interaction: bool,
execution_kind: SlashCommandExecutionKind,
show_in_command_popup: bool,
}
/// Return all built-in commands in a Vec paired with their command string.
pub fn built_in_slash_commands() -> Vec<(&'static str, SlashCommand)> {
SlashCommand::iter()
.filter(|command| command.is_visible())
.flat_map(|command| {
std::iter::once((command.command(), command)).chain(
command
.command_aliases()
.iter()
.copied()
.map(move |alias| (alias, command)),
)
})
.flat_map(|command| command.all_command_names().map(move |name| (name, command)))
.collect()
}