//! Slash command trait and execution types. //! //! Pager's synchronous dispatch model. Key differences: //! //! - `run()` is synchronous (no `async_trait`). Commands that need async work //! return `CommandResult::Action(action)` and let the dispatch layer handle it. //! - `CommandResult` has additional variants: `Action`, `QueueCommand`, `PassThrough`. //! - Trait methods return `&str` (not `&'static str`) to support ACP-sourced commands. //! - `args_required()` added for the two-bit completeness model. //! - `validate_args()` intentionally omitted in phase 1 (folded into `run()`). use crate::acp::model_state::ModelState; use crate::app::actions::Action; use crate::app::bundle::BundleState; use agent_client_protocol as acp; /// Provisional scheduled task info for immediate display in the tasks pane. /// /// Created by `/loop` when the user submits the command so the task appears /// instantly, rather than waiting for the LLM round-trip through /// `scheduler_create`. #[derive(Debug, Clone)] pub struct ScheduledTaskPreview { pub prompt: String, pub human_schedule: String, pub next_fire_at: Option, /// Tag shown in the tasks pane (e.g. "loop", "check"). Defaults to "loop". pub tag: String, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum DoctorRequest { Report, ListFixes, Fix(crate::diagnostics::DiagnosticId), } /// Result of running a slash command. #[derive(Debug)] #[allow(clippy::large_enum_variant)] pub enum CommandResult { /// Command handled successfully, no visible output needed. /// Included for TUI parity; no phase-1 command uses this directly. Handled, /// Command handled but was a no-op (e.g., model already selected). /// Included for TUI parity. Dispatch treats it identically to Handled. HandledNoOp, /// Build or act on TUI doctor state from live app/session inputs. Doctor(DoctorRequest), /// Command failed with an error message. Error(String), /// Command produced a user-visible message. Message(String), /// Command produced a pager Action to dispatch (e.g., SwitchModel, Quit). Action(Action), /// Command should be sent through the queued command pipeline /// (e.g., /compact). The String is the raw command text. QueueCommand(String), /// Skill invocation: pager read the SKILL.md, applied substitutions, /// and constructed structured prompt blocks for the wire. /// `display_text` is what the user sees in scrollback. /// `prompt_blocks` is the actual content sent to the model. InjectSkill { display_text: String, prompt_blocks: Vec, /// Whether to display as a skill invocation (teal accent) in scrollback. /// `true` for real skills (e.g. /commit), `false` for built-in commands /// like /loop that inject structured prompts but aren't skills. display_as_skill: bool, /// If set, immediately show a provisional scheduled task in the tasks /// pane (replaced when the real `ScheduledTaskCreated` notification /// arrives from the shell). scheduled_task_preview: Option, }, /// Command text should be sent as a regular prompt. The shell resolves it. /// /// Phase-1 simplification: this intentionally covers two semantically /// different cases in a single variant: /// 1. ACP-advertised commands (shell explicitly supports them) /// 2. Unknown commands (pager doesn't know them, shell might) /// /// Both are sent identically today. If behavior ever needs to diverge /// (e.g., different UX confidence, error messaging, or telemetry), /// split into `AcpPassThrough` and `UnknownPassThrough` variants. PassThrough(String), } /// A suggestion item for command argument completion. #[derive(Debug, Clone)] pub struct ArgItem { /// Display text shown in the dropdown. pub display: String, /// Text used for fuzzy matching. pub match_text: String, /// Text inserted into the prompt on acceptance. pub insert_text: String, /// Description shown alongside the item. pub description: String, } /// Read-only context for generating suggestions. /// /// Passed to `SlashCommand::suggest_args()` and `SlashCommand::visible()`. /// Kept minimal -- extend as needed. pub struct AppCtx<'a> { pub models: &'a ModelState, /// Working directory of the active session (for filesystem completions). pub cwd: &'a std::path::Path, /// Session announcements (critical or promo) exist (gates `/announcements` visibility). pub has_session_announcements: bool, /// Consumer billing surface (`AppView::usage_visible`). Gates `/usage` subcommands. pub billing_surface_visible: bool, pub workflows_available: bool, /// Effective render mode of this process (gates `/minimal` and /// `/fullscreen` visibility). Same source of truth as /// [`CommandExecCtx::screen_mode`], carried by the owning /// [`SlashController`](crate::slash::SlashController). pub(crate) screen_mode: crate::app::ScreenMode, } /// Mutable execution context for `SlashCommand::run()`. /// /// Wraps only what pager can cleanly provide. Commands that need async ACP /// calls return `CommandResult::Action(...)` and let dispatch handle the effect. pub struct CommandExecCtx<'a> { pub models: &'a ModelState, pub session_id: Option<&'a acp::SessionId>, pub bundle_state: &'a BundleState, pub(crate) screen_mode: crate::app::ScreenMode, /// Consumer billing surface (`AppView::usage_visible`). Gates `/usage` subcommands. pub billing_surface_visible: bool, /// Snapshot of the active agent's PAGER-owned settings, built at /// command-build time by the dispatcher. Slash commands like /// `/multiline` read this to compute `!current` and dispatch a /// typed `Action::SetX(new)` — the dispatcher remains the single /// source of truth for the actual state mutation. pub(crate) pager_state: crate::settings::PagerLocalSnapshot, } /// A slash command. /// /// Implementors define command metadata (name, description, args) and /// synchronous execution logic. The trait uses `&str` returns (not /// `&'static str`) so ACP-sourced commands with runtime-determined data /// work from day one. pub trait SlashCommand: Send + Sync { /// Canonical command name (without leading `/`). E.g., `"exit"`. fn name(&self) -> &str; /// Alternative names for this command. E.g., `&["quit"]` for `/exit`. fn aliases(&self) -> &[&str] { &[] } /// Short human-readable description shown in the dropdown. fn description(&self) -> &str; /// Usage string shown in help. E.g., `"/model "`. fn usage(&self) -> &str; /// Whether the command accepts arguments at all. fn takes_args(&self) -> bool { false } /// Runtime args contract (e.g. subcommands only for some auth modes). /// Defaults to [`Self::takes_args`]. Dropdown/completion paths only: /// insert text (trailing space), the args-phase snapshot, and argument /// suggestions. Enter-completeness ([`crate::slash::is_command_complete`]) /// keys off the static [`Self::takes_args`] / [`Self::args_required`] pair. #[allow(unused_variables)] fn takes_args_now(&self, ctx: &AppCtx) -> bool { self.takes_args() } /// Whether arguments are required for execution. /// /// Only meaningful when `takes_args()` is true. The two-bit model: /// /// | `takes_args` | `args_required` | Example | Enter with no args | /// |-------------|----------------|------------------|-------------------| /// | `false` | `false` | `/exit` | Executes | /// | `true` | `false` | `/compact [ctx]` | Executes | /// | `true` | `true` | `/model ` | Blocks | fn args_required(&self) -> bool { false } /// Generate argument suggestions. `args_query` is the raw typed /// args text; most impls ignore it and return a static list. #[allow(unused_variables)] fn suggest_args(&self, ctx: &AppCtx, args_query: &str) -> Option> { None } /// Whether this command is currently visible / executable. /// /// Default is `true` (every command is visible). Override to gate a /// command on session state. #[allow(unused_variables)] fn visible(&self, ctx: &AppCtx) -> bool { true } /// Whether this command operates on a single agent session — its /// conversation, context, model, turns, plan, etc. — rather than the /// pager as a whole. /// /// Session-scoped commands (`/compact`, `/fork`, `/rewind`, …) need a /// "current session" to act on, so they are suppressed on session-less /// surfaces. Today that means the agent dashboard's dispatch input, /// which offers only pager-global commands (`/theme`, `/settings`, /// `/mcps`, …). Surfaces that always have a session (the agent view) /// ignore this flag and continue to show every command. /// /// Defaults to `false` (pager-global). fn session_scoped(&self) -> bool { false } /// Whether a `session_scoped()` command should still be offered on /// session-less surfaces (the agent dashboard's dispatch input). /// /// A handful of session-scoped commands have a meaningful session-less /// interpretation: `/model` and `/plan` configure the *next* agent the /// dashboard spawns; `/multiline` toggles compose mode on the dashboard /// inputs. Those override this to `true` so they appear in the dashboard /// dropdown even though `session_scoped()` is `true`. Has no effect for /// non-session-scoped commands (they're always offered). /// /// Defaults to `false`. fn offered_when_session_less(&self) -> bool { false } /// Whether this command should ONLY be offered on the session-less /// dashboard surface — the inverse of [`Self::session_scoped`]. The /// dashboard's dispatch input is the one surface where /// `hide_session_scoped` is set, so a `dashboard_only` command shows /// there and is suppressed on every session surface (the agent view) and /// the welcome screen. /// /// `/cd` changes where the dashboard dispatches new agents, so it is /// meaningless in an agent session and hidden there. Defaults to `false`. fn dashboard_only(&self) -> bool { false } /// Whether this command functions in the scrollback-native **minimal** /// mode (`grok --minimal`). /// /// Minimal mode deletes the interactive fullscreen scrollback pane, the /// in-app mouse selection path, and the agent dashboard, handing scroll / /// search / selection back to the terminal (K7). Commands that drive those /// deleted surfaces — `/find`, `/dashboard` — have nothing to act on, so /// the central dispatch gate refuses them with a "/ is not available in /// minimal mode" message (committed as a system block). Clipboard helpers /// like `/copy` stay available: they read scrollback state and do not need /// the fullscreen pane. /// /// Defaults to `true` — a **denylist, not an allowlist**: the many /// mode-agnostic commands keep working and new commands are available in /// minimal by default (the mode is converging toward parity). Override to /// `false` only for genuinely fullscreen-pane-dependent commands. fn available_in_minimal(&self) -> bool { true } /// Placeholder text shown in the prompt when args are empty. /// E.g., `"[context]"` for `/compact`. fn arg_placeholder(&self) -> Option<&str> { None } /// Whether this command is a skill (ACP-advertised with skill metadata). /// Used for visual theming (accent color, prefix glyph). fn is_skill(&self) -> bool { false } /// Tool names the agent must have registered for this command to work. /// /// Default is empty (no tool dependency). Override for commands that /// only make sense when specific tools are available -- e.g. `/loop` /// requires `scheduler_create`. The registry hides commands whose /// requirements aren't all present in the agent's advertised toolset. fn required_tools(&self) -> &[&str] { &[] } /// Whether this command supports live preview when navigating arg /// suggestions in the dropdown. /// /// When true, [`preview_arg`] is called on every selection change /// and [`cancel_preview`] on dropdown close (Esc). fn supports_preview(&self) -> bool { false } /// Capture the current preview-relevant state as a string. /// /// Called once when preview mode begins (first navigation in args /// dropdown). The returned value is stored and passed back to /// [`cancel_preview`] if the user dismisses the dropdown. fn preview_state(&self) -> Option { None } /// Live-preview the given argument suggestion. /// /// Called when the user navigates to a new suggestion in the dropdown /// (Up/Down). The command should apply a temporary/preview state. /// Only called when [`supports_preview`] returns true. #[allow(unused_variables)] fn preview_arg(&self, arg: &str) {} /// Cancel a live preview, reverting to the state before the dropdown /// opened. `previous` is the value returned by [`preview_state`] /// when preview started. /// /// Called when the user dismisses the dropdown (Esc) or clears the /// slash input. Only called when [`supports_preview`] returns true. #[allow(unused_variables)] fn cancel_preview(&self, previous: &str) {} /// Execute the command synchronously. /// /// For async work, return `CommandResult::Action(action)` and let /// the dispatch layer handle the effect pipeline. fn run(&self, ctx: &mut CommandExecCtx, args: &str) -> CommandResult; }