Publish harness and TUI open-source
initial sync from the monorepo
This commit is contained in:
commit
c68e39f604
2734 changed files with 1437016 additions and 0 deletions
306
crates/codegen/xai-grok-pager/src/slash/command.rs
Normal file
306
crates/codegen/xai-grok-pager/src/slash/command.rs
Normal file
|
|
@ -0,0 +1,306 @@
|
|||
//! 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<String>,
|
||||
/// Tag shown in the tasks pane (e.g. "loop", "check"). Defaults to "loop".
|
||||
pub tag: String,
|
||||
}
|
||||
|
||||
/// 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,
|
||||
/// 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<agent_client_protocol::ContentBlock>,
|
||||
/// 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<ScheduledTaskPreview>,
|
||||
},
|
||||
/// 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,
|
||||
/// 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,
|
||||
/// 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 <name>"`.
|
||||
fn usage(&self) -> &str;
|
||||
|
||||
/// Whether the command accepts arguments at all.
|
||||
fn takes_args(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// 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 <id>` | 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<Vec<ArgItem>> {
|
||||
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/copy path, and the agent dashboard, handing
|
||||
/// scroll / search / selection back to the terminal (K7). Commands that
|
||||
/// drive those deleted surfaces — `/find`, `/copy`, `/dashboard` — have
|
||||
/// nothing to act on, so the central dispatch gate refuses them with a
|
||||
/// "/<x> is not available in minimal mode" message (committed as a system
|
||||
/// block).
|
||||
///
|
||||
/// 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<String> {
|
||||
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;
|
||||
}
|
||||
Loading…
Reference in a new issue