Codex Handbook
tui/src/chatwidget/slash_dispatch.rs 1140 lines
//! Slash-command dispatch and local-recall handoff for `ChatWidget`.//!//! `ChatComposer` parses slash input and stages recognized command text for local//! Up-arrow recall before returning an input result. This module owns the app-level//! dispatch step and records the staged entry once the command has been handled, so//! slash-command recall follows the same submitted-input rule as ordinary text.use super::*;use crate::app_event::ThreadGoalSetMode;use crate::bottom_pane::prompt_args::parse_slash_name;use crate::bottom_pane::slash_commands::BuiltinCommandFlags;use crate::bottom_pane::slash_commands::ServiceTierCommand;use crate::bottom_pane::slash_commands::SlashCommandItem;use crate::bottom_pane::slash_commands::find_slash_command;use crate::goal_display::GOAL_USAGE;use crate::goal_files::GoalDraft;#[derive(Clone, Copy, Debug, PartialEq, Eq)]enum SlashCommandDispatchSource {    Live,    Queued,}struct PreparedSlashCommandArgs {    args: String,    text_elements: Vec<TextElement>,    pending_pastes: Vec<(String, String)>,    local_images: Vec<LocalImageAttachment>,    remote_image_urls: Vec<String>,    mention_bindings: Vec<MentionBinding>,    source: SlashCommandDispatchSource,}const SIDE_STARTING_CONTEXT_LABEL: &str = "Side starting...";const SIDE_SLASH_COMMAND_UNAVAILABLE_HINT: &str =    "Press Ctrl+C to return to the main thread first.";const GOAL_USAGE_HINT: &str = "Example: /goal improve benchmark coverage";const RAW_USAGE: &str = "Usage: /raw [on|off]";const USAGE_CHATGPT_LOGIN_REQUIRED: &str = "Sign in with ChatGPT to use /usage.";impl ChatWidget {    /// Dispatch a bare slash command and record its staged local-history entry.    ///    /// The composer stages history before returning `InputResult::Command`; this wrapper commits    /// that staged entry after dispatch so slash-command recall follows the same "submitted input"    /// rule as normal text.    pub(super) fn handle_slash_command_dispatch(&mut self, cmd: SlashCommand) {        self.dispatch_command(cmd);        if cmd == SlashCommand::Goal {            self.bottom_pane.drain_pending_submission_state();        }        self.bottom_pane.record_pending_slash_command_history();    }    pub(super) fn handle_service_tier_command_dispatch(&mut self, command: ServiceTierCommand) {        if self.active_side_conversation {            self.add_error_message(format!(                "'/{}' is unavailable in side conversations. {SIDE_SLASH_COMMAND_UNAVAILABLE_HINT}",                command.name            ));            self.bottom_pane.drain_pending_submission_state();            self.bottom_pane.record_pending_slash_command_history();            return;        }        self.toggle_service_tier_from_ui(command);        self.bottom_pane.record_pending_slash_command_history();    }    /// Dispatch an inline slash command and record its staged local-history entry.    ///    /// Inline command arguments may later be prepared through the normal submission pipeline, but    /// local command recall still tracks the original command invocation. Treating this wrapper as    /// the only input-result entry point avoids double-recording commands with inline args.    pub(super) fn handle_slash_command_with_args_dispatch(        &mut self,        cmd: SlashCommand,        args: String,        text_elements: Vec<TextElement>,    ) {        self.dispatch_command_with_args(cmd, args, text_elements);        self.bottom_pane.record_pending_slash_command_history();    }    fn apply_plan_slash_command(&mut self) -> bool {        if !self.collaboration_modes_enabled() {            self.add_info_message(                "Collaboration modes are disabled.".to_string(),                Some("Enable collaboration modes to use /plan.".to_string()),            );            return false;        }        if let Some(mask) = collaboration_modes::plan_mask(self.model_catalog.as_ref()) {            self.set_collaboration_mask_from_user_action(mask);            true        } else {            self.add_info_message(                "Plan mode unavailable right now.".to_string(),                /*hint*/ None,            );            false        }    }    fn request_side_conversation(        &mut self,        parent_thread_id: ThreadId,        user_message: Option<UserMessage>,    ) {        self.set_side_conversation_context_label(Some(SIDE_STARTING_CONTEXT_LABEL.to_string()));        self.request_redraw();        self.app_event_tx.send(AppEvent::StartSide {            parent_thread_id,            user_message,        });    }    fn request_empty_side_conversation(&mut self, cmd: SlashCommand) {        let Some(parent_thread_id) = self.thread_id else {            let command = cmd.command();            self.add_error_message(format!(                "'/{command}' is unavailable before the session starts."            ));            return;        };        self.request_side_conversation(parent_thread_id, /*user_message*/ None);    }    fn emit_raw_output_mode_changed(&self, enabled: bool) {        self.app_event_tx            .send(AppEvent::RawOutputModeChanged { enabled });    }    pub(super) fn dispatch_command(&mut self, cmd: SlashCommand) {        if !self.ensure_slash_command_allowed_in_side_conversation(cmd) {            return;        }        if !self.ensure_side_command_allowed_outside_review(cmd) {            return;        }        if !cmd.available_during_task() && self.bottom_pane.is_task_running() {            let message = format!(                "'/{}' is disabled while a task is in progress.",                cmd.command()            );            self.add_to_history(history_cell::new_error_event(message));            self.bottom_pane.drain_pending_submission_state();            self.request_redraw();            return;        }        match cmd {            SlashCommand::Feedback => {                if !self.config.feedback_enabled {                    let params = crate::bottom_pane::feedback_disabled_params();                    self.bottom_pane.show_selection_view(params);                    self.request_redraw();                    return;                }                // Step 1: pick a category (UI built in feedback_view)                let params =                    crate::bottom_pane::feedback_selection_params(self.app_event_tx.clone());                self.bottom_pane.show_selection_view(params);                self.request_redraw();            }            SlashCommand::New => {                self.app_event_tx.send(AppEvent::NewSession);            }            SlashCommand::Archive => {                self.bottom_pane.show_selection_view(SelectionViewParams {                    title: Some("Archive this session?".to_string()),                    subtitle: Some(                        "Are you sure? This will archive the current session and exit Codex"                            .to_string(),                    ),                    footer_hint: Some(standard_popup_hint_line()),                    items: vec![                        SelectionItem {                            name: "No, don't archive".to_string(),                            description: Some("Return to the current session".to_string()),                            dismiss_on_select: true,                            ..Default::default()                        },                        SelectionItem {                            name: "Yes, archive and exit".to_string(),                            description: Some("Archive this session now".to_string()),                            actions: vec![Box::new(|tx| {                                tx.send(AppEvent::ArchiveCurrentThread);                            })],                            dismiss_on_select: true,                            ..Default::default()                        },                    ],                    ..Default::default()                });                self.request_redraw();            }            SlashCommand::Delete => {                self.bottom_pane.show_selection_view(SelectionViewParams {                    title: Some("Delete this session?".to_string()),                    subtitle: Some(                        "Cannot be undone. Subagent threads will also be deleted.".to_string(),                    ),                    footer_hint: Some(standard_popup_hint_line()),                    items: vec![                        SelectionItem {                            name: "No, keep this session".to_string(),                            description: Some("Return to the current session".to_string()),                            dismiss_on_select: true,                            ..Default::default()                        },                        SelectionItem {                            name: "Yes, delete and exit".to_string(),                            description: Some("Permanently delete this session now".to_string()),                            actions: vec![Box::new(|tx| {                                tx.send(AppEvent::DeleteCurrentThread);                            })],                            dismiss_on_select: true,                            ..Default::default()                        },                    ],                    ..Default::default()                });                self.request_redraw();            }            SlashCommand::Clear => {                self.app_event_tx.send(AppEvent::ClearUi);            }            SlashCommand::Resume => {                self.app_event_tx.send(AppEvent::OpenResumePicker);            }            SlashCommand::Fork => {                self.app_event_tx.send(AppEvent::ForkCurrentSession);            }            SlashCommand::App => {                let Some(thread_id) = self.thread_id else {                    self.add_error_message(                        "Session is still starting; try /app again in a moment.".to_string(),                    );                    return;                };                self.app_event_tx                    .send(AppEvent::OpenDesktopThread { thread_id });            }            SlashCommand::Init => {                const INIT_PROMPT: &str = include_str!("../../prompt_for_init_command.md");                self.submit_user_message(INIT_PROMPT.to_string().into());            }            SlashCommand::Compact => {                self.clear_token_usage();                if !self.bottom_pane.is_task_running() {                    self.bottom_pane.set_task_running(/*running*/ true);                }                self.app_event_tx.compact();            }            SlashCommand::Review => {                self.open_review_popup();            }            SlashCommand::Rename => {                self.session_telemetry                    .counter("codex.thread.rename", /*inc*/ 1, &[]);                self.show_rename_prompt();            }            SlashCommand::Model => {                self.open_model_popup();            }            SlashCommand::Personality => {                self.open_personality_popup();            }            SlashCommand::Plan => {                self.apply_plan_slash_command();            }            SlashCommand::Goal => {                if !self.config.features.enabled(Feature::Goals) {                    return;                }                if let Some(thread_id) = self.thread_id {                    self.app_event_tx                        .send(AppEvent::OpenThreadGoalMenu { thread_id });                    self.append_message_history_entry("/goal".to_string());                } else {                    self.add_info_message(                        GOAL_USAGE.to_string(),                        Some(GOAL_USAGE_HINT.to_string()),                    );                }            }            SlashCommand::Side | SlashCommand::Btw => {                self.request_empty_side_conversation(cmd);            }            SlashCommand::Agent | SlashCommand::MultiAgents => {                self.app_event_tx.send(AppEvent::OpenAgentPicker);            }            SlashCommand::Permissions => {                self.open_permissions_popup();            }            SlashCommand::Vim => {                self.toggle_vim_mode_and_notify();            }            SlashCommand::Keymap => {                self.open_keymap_picker();            }            SlashCommand::ElevateSandbox => {                #[cfg(target_os = "windows")]                {                    let windows_sandbox_level =                        crate::windows_sandbox::level_from_config(&self.config);                    let windows_degraded_sandbox_enabled =                        matches!(windows_sandbox_level, WindowsSandboxLevel::RestrictedToken);                    if !windows_degraded_sandbox_enabled {                        // This command should not be visible/recognized outside degraded mode,                        // but guard anyway in case something dispatches it directly.                        return;                    }                    let Some(preset) = builtin_approval_presets()                        .into_iter()                        .find(|preset| preset.id == "auto")                    else {                        // Avoid panicking in interactive UI; treat this as a recoverable                        // internal error.                        self.add_error_message(                            "Internal error: missing the 'auto' approval preset.".to_string(),                        );                        return;                    };                    if let Err(err) = self                        .config                        .permissions                        .approval_policy                        .can_set(&preset.approval)                    {                        self.add_error_message(err.to_string());                        return;                    }                    self.session_telemetry.counter(                        "codex.windows_sandbox.setup_elevated_sandbox_command",                        /*inc*/ 1,                        &[],                    );                    self.app_event_tx                        .send(AppEvent::BeginWindowsSandboxElevatedSetup {                            preset,                            profile_selection: None,                        });                }                #[cfg(not(target_os = "windows"))]                {                    let _ = &self.session_telemetry;                    // Not supported; on non-Windows this command should never be reachable.                }            }            SlashCommand::SandboxReadRoot => {                self.add_error_message(                    "Usage: /sandbox-add-read-dir <absolute-directory-path>".to_string(),                );            }            SlashCommand::Experimental => {                self.open_experimental_popup();            }            SlashCommand::AutoReview => {                self.open_auto_review_denials_popup();            }            SlashCommand::Memories => {                self.open_memories_popup();            }            SlashCommand::Quit | SlashCommand::Exit => {                self.request_quit_without_confirmation();            }            SlashCommand::Logout => {                self.app_event_tx.send(AppEvent::Logout);            }            SlashCommand::Copy => {                self.copy_last_agent_markdown();            }            SlashCommand::Raw => {                let enabled = self.toggle_raw_output_mode_and_notify();                self.emit_raw_output_mode_changed(enabled);            }            SlashCommand::Diff => {                self.add_diff_in_progress();                let tx = self.app_event_tx.clone();                let runner = self.workspace_command_runner.clone();                let cwd = self                    .current_cwd                    .clone()                    .unwrap_or_else(|| self.config.cwd.to_path_buf());                tokio::spawn(async move {                    let text = match runner {                        Some(runner) => match get_git_diff(runner.as_ref(), &cwd).await {                            Ok((is_git_repo, diff_text)) => {                                if is_git_repo {                                    diff_text                                } else {                                    "`/diff` — _not inside a git repository_".to_string()                                }                            }                            Err(e) => format!("Failed to compute diff: {e}"),                        },                        None => "Failed to compute diff: workspace command runner unavailable"                            .to_string(),                    };                    tx.send(AppEvent::DiffResult(text));                });            }            SlashCommand::Mention => {                self.insert_str("@");            }            SlashCommand::Skills => {                self.open_skills_menu();            }            SlashCommand::Import => {                self.app_event_tx                    .send(AppEvent::OpenExternalAgentConfigMigration);            }            SlashCommand::Hooks => {                self.add_hooks_output();            }            SlashCommand::Status => {                if self.should_prefetch_rate_limits() {                    let request_id = self.next_status_refresh_request_id;                    self.next_status_refresh_request_id =                        self.next_status_refresh_request_id.wrapping_add(1);                    self.add_status_output(/*refreshing_rate_limits*/ true, Some(request_id));                    self.app_event_tx.send(AppEvent::RefreshRateLimits {                        origin: RateLimitRefreshOrigin::StatusCommand { request_id },                    });                } else {                    self.add_status_output(                        /*refreshing_rate_limits*/ false, /*request_id*/ None,                    );                }            }            SlashCommand::Usage => {                if self.ensure_usage_command_available() {                    self.open_usage_menu();                }            }            SlashCommand::Ide => {                self.handle_ide_command();            }            SlashCommand::DebugConfig => {                self.add_debug_config_output();            }            SlashCommand::Title => {                self.open_terminal_title_setup();            }            SlashCommand::Statusline => {                self.open_status_line_setup();            }            SlashCommand::Theme => {                self.open_theme_picker();            }            SlashCommand::Pets => {                self.open_pets_picker();            }            SlashCommand::Ps => {                self.add_ps_output();            }            SlashCommand::Stop => {                self.clean_background_terminals();            }            SlashCommand::MemoryDrop => {                self.add_app_server_stub_message("Memory maintenance");            }            SlashCommand::MemoryUpdate => {                self.add_app_server_stub_message("Memory maintenance");            }            SlashCommand::Mcp => {                self.add_mcp_output(McpServerStatusDetail::ToolsAndAuthOnly);            }            SlashCommand::Apps => {                self.add_connectors_output();            }            SlashCommand::Plugins => {                self.add_plugins_output();            }            SlashCommand::Rollout => {                if let Some(path) = self.rollout_path() {                    self.add_info_message(                        format!("Current rollout path: {}", path.display()),                        /*hint*/ None,                    );                } else {                    self.add_info_message(                        "Rollout path is not available yet.".to_string(),                        /*hint*/ None,                    );                }            }            SlashCommand::TestApproval => {                use std::collections::HashMap;                use crate::approval_events::ApplyPatchApprovalRequestEvent;                use crate::diff_model::FileChange;                self.on_apply_patch_approval_request(                    "1".to_string(),                    ApplyPatchApprovalRequestEvent {                        call_id: "1".to_string(),                        turn_id: "turn-1".to_string(),                        changes: HashMap::from([                            (                                PathBuf::from("/tmp/test.txt"),                                FileChange::Add {                                    content: "test".to_string(),                                },                            ),                            (                                PathBuf::from("/tmp/test2.txt"),                                FileChange::Update {                                    unified_diff: "+test\n-test2".to_string(),                                    move_path: None,                                },                            ),                        ]),                        reason: None,                        grant_root: Some(PathBuf::from("/tmp")),                    },                );            }        }    }    /// Run an inline slash command.    ///    /// Branches that prepare arguments should pass `record_history: false` to the composer because    /// the staged slash-command entry is the recall record; using the normal submission-history    /// path as well would make a single command appear twice during Up-arrow navigation.    pub(super) fn dispatch_command_with_args(        &mut self,        cmd: SlashCommand,        args: String,        text_elements: Vec<TextElement>,    ) {        if !self.ensure_slash_command_allowed_in_side_conversation(cmd) {            return;        }        if !self.ensure_side_command_allowed_outside_review(cmd) {            return;        }        if !cmd.supports_inline_args() {            self.dispatch_command(cmd);            return;        }        if !cmd.available_during_task() && self.bottom_pane.is_task_running() {            let message = format!(                "'/{}' is disabled while a task is in progress.",                cmd.command()            );            self.add_to_history(history_cell::new_error_event(message));            self.request_redraw();            return;        }        let trimmed = args.trim();        if trimmed.is_empty() {            self.dispatch_command(cmd);            return;        }        if cmd == SlashCommand::Goal {            self.dispatch_prepared_command_with_args(                cmd,                PreparedSlashCommandArgs {                    args,                    text_elements,                    pending_pastes: self.bottom_pane.composer_pending_pastes(),                    local_images: self.bottom_pane.composer_local_images(),                    remote_image_urls: self.bottom_pane.remote_image_urls(),                    mention_bindings: Vec::new(),                    source: SlashCommandDispatchSource::Live,                },            );            return;        }        let Some((prepared_args, prepared_elements)) =            self.prepare_live_inline_args(args, text_elements)        else {            return;        };        self.dispatch_prepared_command_with_args(            cmd,            PreparedSlashCommandArgs {                args: prepared_args,                text_elements: prepared_elements,                pending_pastes: Vec::new(),                local_images: Vec::new(),                remote_image_urls: Vec::new(),                mention_bindings: Vec::new(),                source: SlashCommandDispatchSource::Live,            },        );    }    fn prepare_live_inline_args(        &mut self,        args: String,        text_elements: Vec<TextElement>,    ) -> Option<(String, Vec<TextElement>)> {        if self.bottom_pane.composer_text().is_empty() {            Some((args, text_elements))        } else {            self.bottom_pane                .prepare_inline_args_submission(/*record_history*/ false)        }    }    fn clear_live_goal_submission(&mut self) {        self.bottom_pane            .set_composer_text(String::new(), Vec::new(), Vec::new());        self.bottom_pane.set_composer_pending_pastes(Vec::new());        self.bottom_pane.drain_pending_submission_state();    }    fn prepared_inline_user_message(        &mut self,        args: String,        text_elements: Vec<TextElement>,        mut local_images: Vec<LocalImageAttachment>,        mut remote_image_urls: Vec<String>,        mut mention_bindings: Vec<MentionBinding>,        source: SlashCommandDispatchSource,    ) -> UserMessage {        if source == SlashCommandDispatchSource::Live {            local_images = self                .bottom_pane                .take_recent_submission_images_with_placeholders();            remote_image_urls = self.take_remote_image_urls();            mention_bindings = self.bottom_pane.take_recent_submission_mention_bindings();        }        UserMessage {            text: args,            local_images,            remote_image_urls,            text_elements,            mention_bindings,        }    }    fn dispatch_prepared_command_with_args(        &mut self,        cmd: SlashCommand,        prepared: PreparedSlashCommandArgs,    ) {        let PreparedSlashCommandArgs {            args,            text_elements,            pending_pastes,            local_images,            remote_image_urls,            mention_bindings,            source,        } = prepared;        let trimmed = args.trim();        match cmd {            SlashCommand::Usage => {                if self.ensure_usage_command_available() {                    match tokens::TokenActivityView::parse(trimmed) {                        Some(view) => self.add_token_activity_output(view),                        None => self.add_error_message(                            "Usage: /usage [daily|weekly|cumulative]".to_string(),                        ),                    }                }            }            SlashCommand::Ide => {                self.handle_ide_command_args(trimmed);            }            SlashCommand::Mcp => match trimmed.to_ascii_lowercase().as_str() {                "verbose" => self.add_mcp_output(McpServerStatusDetail::Full),                _ => self.add_error_message("Usage: /mcp [verbose]".to_string()),            },            SlashCommand::Keymap => match trimmed.to_ascii_lowercase().as_str() {                "" => self.open_keymap_picker(),                "debug" => {                    match crate::keymap::RuntimeKeymap::from_config(&self.config.tui_keymap) {                        Ok(runtime_keymap) => self.open_keymap_debug(&runtime_keymap),                        Err(err) => {                            self.add_error_message(format!(                                "Invalid `tui.keymap` configuration: {err}"                            ));                        }                    }                }                _ => self.add_error_message("Usage: /keymap [debug]".to_string()),            },            SlashCommand::Raw => match trimmed.to_ascii_lowercase().as_str() {                "on" => {                    self.set_raw_output_mode_and_notify(/*enabled*/ true);                    self.emit_raw_output_mode_changed(/*enabled*/ true);                }                "off" => {                    self.set_raw_output_mode_and_notify(/*enabled*/ false);                    self.emit_raw_output_mode_changed(/*enabled*/ false);                }                _ => self.add_error_message(RAW_USAGE.to_string()),            },            SlashCommand::Rename if !trimmed.is_empty() => {                if !self.ensure_thread_rename_allowed() {                    return;                }                self.session_telemetry                    .counter("codex.thread.rename", /*inc*/ 1, &[]);                let Some(name) = normalize_thread_name(&args) else {                    self.add_error_message("Thread name cannot be empty.".to_string());                    return;                };                self.app_event_tx.set_thread_name(name);            }            SlashCommand::Plan if !trimmed.is_empty() => {                if !self.apply_plan_slash_command() {                    return;                }                let user_message = self.prepared_inline_user_message(                    args,                    text_elements,                    local_images,                    remote_image_urls,                    mention_bindings,                    source,                );                if self.is_session_configured() {                    self.reasoning_buffer.clear();                    self.full_reasoning_buffer.clear();                    self.set_status_header(String::from("Working"));                    self.submit_user_message(user_message);                } else {                    self.queue_user_message(user_message);                }            }            SlashCommand::Goal if !trimmed.is_empty() => {                if !self.config.features.enabled(Feature::Goals) {                    if source == SlashCommandDispatchSource::Live {                        self.clear_live_goal_submission();                    }                    return;                }                enum GoalControlCommand {                    Clear,                    SetStatus(AppThreadGoalStatus),                }                let control_command = match trimmed.to_ascii_lowercase().as_str() {                    "clear" => Some(GoalControlCommand::Clear),                    "edit" => {                        self.app_event_tx.send(AppEvent::OpenThreadGoalEditor {                            thread_id: self.thread_id,                        });                        if source == SlashCommandDispatchSource::Live {                            self.clear_live_goal_submission();                        }                        return;                    }                    "pause" => Some(GoalControlCommand::SetStatus(AppThreadGoalStatus::Paused)),                    "resume" => Some(GoalControlCommand::SetStatus(AppThreadGoalStatus::Active)),                    _ => None,                };                if let Some(command) = control_command {                    let Some(thread_id) = self.thread_id else {                        self.add_info_message(                            GOAL_USAGE.to_string(),                            Some(                                "The session must start before you can change a goal.".to_string(),                            ),                        );                        if source == SlashCommandDispatchSource::Live {                            self.clear_live_goal_submission();                        }                        return;                    };                    match command {                        GoalControlCommand::Clear => {                            self.app_event_tx                                .send(AppEvent::ClearThreadGoal { thread_id });                        }                        GoalControlCommand::SetStatus(status) => {                            self.app_event_tx                                .send(AppEvent::SetThreadGoalStatus { thread_id, status });                        }                    }                    self.append_message_history_entry(format!("/goal {trimmed}"));                    if source == SlashCommandDispatchSource::Live {                        self.clear_live_goal_submission();                    }                    return;                }                let draft = GoalDraft {                    objective: args,                    text_elements,                    pending_pastes,                    local_images,                    remote_image_urls,                };                let Some(thread_id) = self.thread_id else {                    if source == SlashCommandDispatchSource::Live {                        const GOAL_PREFIX: &str = "/goal ";                        let text_elements = draft                            .text_elements                            .into_iter()                            .map(|element| {                                element.map_range(|range| ByteRange {                                    start: range.start + GOAL_PREFIX.len(),                                    end: range.end + GOAL_PREFIX.len(),                                })                            })                            .collect();                        self.queue_user_message_with_options(                            UserMessage {                                text: format!("{GOAL_PREFIX}{}", draft.objective),                                local_images: draft.local_images,                                remote_image_urls: draft.remote_image_urls,                                text_elements,                                mention_bindings: Vec::new(),                            },                            QueuedInputAction::ParseSlash,                            draft.pending_pastes,                        );                        self.clear_live_goal_submission();                    } else {                        self.add_info_message(                            GOAL_USAGE.to_string(),                            Some("The session must start before you can set a goal.".to_string()),                        );                    }                    return;                };                let history_objective = draft.objective.clone();                self.app_event_tx.send(AppEvent::SetThreadGoalDraft {                    thread_id,                    draft,                    mode: ThreadGoalSetMode::ConfirmIfExists,                });                self.append_message_history_entry(format!("/goal {history_objective}"));                if source == SlashCommandDispatchSource::Live {                    self.clear_live_goal_submission();                }            }            SlashCommand::Side | SlashCommand::Btw if !trimmed.is_empty() => {                let Some(parent_thread_id) = self.thread_id else {                    let command = cmd.command();                    self.add_error_message(format!(                        "'/{command}' is unavailable before the session starts."                    ));                    return;                };                let user_message = self.prepared_inline_user_message(                    args,                    text_elements,                    local_images,                    remote_image_urls,                    mention_bindings,                    source,                );                self.request_side_conversation(parent_thread_id, Some(user_message));            }            SlashCommand::Review if !trimmed.is_empty() => {                self.submit_op(AppCommand::review(ReviewTarget::Custom {                    instructions: args,                }));            }            SlashCommand::Resume if !trimmed.is_empty() => {                self.app_event_tx                    .send(AppEvent::ResumeSessionByIdOrName(args));            }            SlashCommand::SandboxReadRoot if !trimmed.is_empty() => {                self.app_event_tx                    .send(AppEvent::BeginWindowsSandboxGrantReadRoot { path: args });            }            SlashCommand::Pets                if matches!(                    args.trim().to_ascii_lowercase().as_str(),                    "disable" | "disabled" | "hide" | "hidden" | "off" | "none"                ) =>            {                self.app_event_tx.send(AppEvent::PetDisabled);            }            SlashCommand::Pets if !trimmed.is_empty() => {                self.select_pet_by_id(args);            }            _ => self.dispatch_command(cmd),        }        if source == SlashCommandDispatchSource::Live && cmd != SlashCommand::Goal {            self.bottom_pane.drain_pending_submission_state();        }    }    pub(super) fn submit_queued_slash_prompt(        &mut self,        queued_message: QueuedUserMessage,    ) -> QueueDrain {        let QueuedUserMessage {            user_message,            pending_pastes,            ..        } = queued_message;        let UserMessage {            text,            local_images,            remote_image_urls,            text_elements,            mention_bindings,        } = user_message;        let Some((name, rest, rest_offset)) = parse_slash_name(&text) else {            self.submit_user_message(UserMessage {                text,                local_images,                remote_image_urls,                text_elements,                mention_bindings,            });            return QueueDrain::Stop;        };        if name.contains('/') {            self.submit_user_message(UserMessage {                text,                local_images,                remote_image_urls,                text_elements,                mention_bindings,            });            return QueueDrain::Stop;        }        let service_tier_commands = self.current_model_service_tier_commands();        let Some(command) =            find_slash_command(name, self.builtin_command_flags(), &service_tier_commands)        else {            self.add_info_message(                format!(                    r#"Unrecognized command '/{name}'. Type "/" for a list of supported commands."#                ),                /*hint*/ None,            );            return QueueDrain::Continue;        };        if rest.is_empty() {            return match command {                SlashCommandItem::Builtin(cmd) => {                    self.dispatch_command(cmd);                    self.queued_command_drain_result(cmd)                }                SlashCommandItem::ServiceTier(command) => {                    self.handle_service_tier_command_dispatch(command);                    QueueDrain::Continue                }            };        }        if !command.supports_inline_args() {            self.submit_user_message(UserMessage {                text,                local_images,                remote_image_urls,                text_elements,                mention_bindings,            });            return QueueDrain::Stop;        }        let SlashCommandItem::Builtin(cmd) = command else {            self.submit_user_message(UserMessage {                text,                local_images,                remote_image_urls,                text_elements,                mention_bindings,            });            return QueueDrain::Stop;        };        let trimmed_start = rest.trim_start();        let leading_trimmed = rest.len().saturating_sub(trimmed_start.len());        let trimmed_rest = trimmed_start.trim_end();        let args_elements = Self::slash_command_args_elements(            trimmed_rest,            rest_offset + leading_trimmed,            &text_elements,        );        self.dispatch_prepared_command_with_args(            cmd,            PreparedSlashCommandArgs {                args: trimmed_rest.to_string(),                text_elements: args_elements,                pending_pastes,                local_images,                remote_image_urls,                mention_bindings,                source: SlashCommandDispatchSource::Queued,            },        );        self.queued_command_drain_result(cmd)    }    fn builtin_command_flags(&self) -> BuiltinCommandFlags {        #[cfg(target_os = "windows")]        let allow_elevate_sandbox = {            let windows_sandbox_level = crate::windows_sandbox::level_from_config(&self.config);            matches!(windows_sandbox_level, WindowsSandboxLevel::RestrictedToken)        };        #[cfg(not(target_os = "windows"))]        let allow_elevate_sandbox = false;        BuiltinCommandFlags {            collaboration_modes_enabled: self.collaboration_modes_enabled(),            connectors_enabled: self.connectors_enabled(),            plugins_command_enabled: self.config.features.enabled(Feature::Plugins),            token_activity_command_enabled: self.has_codex_backend_auth,            goal_command_enabled: self.config.features.enabled(Feature::Goals),            service_tier_commands_enabled: self.fast_mode_enabled(),            personality_command_enabled: self.config.features.enabled(Feature::Personality),            allow_elevate_sandbox,            side_conversation_active: self.active_side_conversation,        }    }    fn ensure_usage_command_available(&mut self) -> bool {        if self.has_codex_backend_auth {            return true;        }        self.add_error_message(USAGE_CHATGPT_LOGIN_REQUIRED.to_string());        false    }    fn queued_command_drain_result(&self, cmd: SlashCommand) -> QueueDrain {        if self.is_user_turn_pending_or_running() || !self.bottom_pane.no_modal_or_popup_active() {            return QueueDrain::Stop;        }        match cmd {            SlashCommand::Ide            | SlashCommand::Status            | SlashCommand::Usage            | SlashCommand::DebugConfig            | SlashCommand::Ps            | SlashCommand::Stop            | SlashCommand::MemoryDrop            | SlashCommand::MemoryUpdate            | SlashCommand::Mcp            | SlashCommand::Apps            | SlashCommand::Plugins            | SlashCommand::Rollout            | SlashCommand::Copy            | SlashCommand::Raw            | SlashCommand::Vim            | SlashCommand::Diff            | SlashCommand::App            | SlashCommand::Rename            | SlashCommand::TestApproval => QueueDrain::Continue,            SlashCommand::Feedback            | SlashCommand::New            | SlashCommand::Archive            | SlashCommand::Delete            | SlashCommand::Clear            | SlashCommand::Resume            | SlashCommand::Fork            | SlashCommand::Init            | SlashCommand::Compact            | SlashCommand::Review            | SlashCommand::Model            | SlashCommand::Personality            | SlashCommand::Plan            | SlashCommand::Goal            | SlashCommand::Side            | SlashCommand::Btw            | SlashCommand::Keymap            | SlashCommand::Agent            | SlashCommand::MultiAgents            | SlashCommand::Permissions            | SlashCommand::ElevateSandbox            | SlashCommand::SandboxReadRoot            | SlashCommand::Experimental            | SlashCommand::AutoReview            | SlashCommand::Memories            | SlashCommand::Quit            | SlashCommand::Exit            | SlashCommand::Logout            | SlashCommand::Mention            | SlashCommand::Skills            | SlashCommand::Import            | SlashCommand::Hooks            | SlashCommand::Title            | SlashCommand::Statusline            | SlashCommand::Theme            | SlashCommand::Pets => QueueDrain::Stop,        }    }    fn slash_command_args_elements(        rest: &str,        rest_offset: usize,        text_elements: &[TextElement],    ) -> Vec<TextElement> {        if rest.is_empty() || text_elements.is_empty() {            return Vec::new();        }        text_elements            .iter()            .filter_map(|elem| {                if elem.byte_range.end <= rest_offset {                    return None;                }                let start = elem.byte_range.start.saturating_sub(rest_offset);                let mut end = elem.byte_range.end.saturating_sub(rest_offset);                if start >= rest.len() {                    return None;                }                end = end.min(rest.len());                (start < end).then_some(elem.map_range(|_| ByteRange { start, end }))            })            .collect()    }    fn ensure_slash_command_allowed_in_side_conversation(&mut self, cmd: SlashCommand) -> bool {        if !self.active_side_conversation || cmd.available_in_side_conversation() {            return true;        }        self.add_error_message(format!(            "'/{}' is unavailable in side conversations. {SIDE_SLASH_COMMAND_UNAVAILABLE_HINT}",            cmd.command()        ));        self.bottom_pane.drain_pending_submission_state();        false    }    fn ensure_side_command_allowed_outside_review(&mut self, cmd: SlashCommand) -> bool {        if !matches!(cmd, SlashCommand::Side | SlashCommand::Btw) || !self.review.is_review_mode {            return true;        }        let command = cmd.command();        self.add_error_message(format!(            "'/{command}' is unavailable while code review is running."        ));        self.bottom_pane.drain_pending_submission_state();        false    }}