pub(crate) mod rewind;
pub(crate) mod runtime;
use std::collections::BTreeMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BuiltInCommand {
Help,
Login,
Logout,
Compact,
New,
Changes,
Rewind,
Model,
Models,
Usage,
Mcp,
Tools,
Subagents,
SystemPrompt,
Skills,
Sessions,
PruneSessions,
Quit,
}
impl BuiltInCommand {
pub const ALL: [Self; 18] = [
Self::Help,
Self::Login,
Self::Logout,
Self::Compact,
Self::New,
Self::Changes,
Self::Rewind,
Self::Model,
Self::Models,
Self::Usage,
Self::Mcp,
Self::Tools,
Self::Subagents,
Self::SystemPrompt,
Self::Skills,
Self::Sessions,
Self::PruneSessions,
Self::Quit,
];
pub fn name(self) -> &'static str {
match self {
Self::Help => "help",
Self::Login => "login",
Self::Logout => "logout",
Self::Compact => "compact",
Self::New => "new",
Self::Changes => "changes",
Self::Rewind => "rewind",
Self::Model => "setmodel",
Self::Models => "models",
Self::Usage => "usage",
Self::Mcp => "mcp",
Self::Tools => "tools",
Self::Subagents => "subagents",
Self::SystemPrompt => "system-prompt",
Self::Skills => "skills",
Self::Sessions => "sessions",
Self::PruneSessions => "prune-sessions",
Self::Quit => "quit",
}
}
pub fn description(self) -> &'static str {
match self {
Self::Help => "open Mission Control help",
Self::Login => "connect OpenAI Codex or configure a custom provider",
Self::Logout => "remove provider auth or custom provider metadata",
Self::Compact => "summarize earlier session turns into a replay boundary",
Self::New => "start a new session",
Self::Changes => "show rewindable local filesystem changes for this session",
Self::Rewind => "preview or restore local filesystem changes from prior turns",
Self::Model => "select a provider-qualified model",
Self::Models => "manage model availability",
Self::Usage => "show provider usage and quota remaining",
Self::Mcp => "manage MCP server availability",
Self::Tools => "manage session tool availability",
Self::Subagents => "manage subagent profile availability",
Self::SystemPrompt => "show the current computed system prompt",
Self::Skills => "manage assistant skill availability",
Self::Sessions => "switch active Mission Control session",
Self::PruneSessions => "delete old local session JSONL files",
Self::Quit => "exit magi-code",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SlashCommand {
pub name: String,
pub description: String,
pub command: BuiltInCommand,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum ParsedSlashCommand<'a> {
Empty,
Prompt(&'a str),
BuiltIn {
command: BuiltInCommand,
arg: Option<&'a str>,
},
Unknown {
token: &'a str,
},
}
pub(crate) fn parse_slash_command<'a>(
input: &'a str,
commands: &CommandRegistry,
) -> ParsedSlashCommand<'a> {
let trimmed = input.trim();
if trimmed.is_empty() {
return ParsedSlashCommand::Empty;
}
if !trimmed.starts_with('/') {
return ParsedSlashCommand::Prompt(trimmed);
}
let (token, arg) = split_slash_command(trimmed);
match commands.get(token).map(|command| command.command) {
Some(command) => ParsedSlashCommand::BuiltIn { command, arg },
None => ParsedSlashCommand::Unknown { token },
}
}
fn split_slash_command(input: &str) -> (&str, Option<&str>) {
input
.split_once(char::is_whitespace)
.map_or((input, None), |(token, arg)| {
(token, Some(arg.trim()).filter(|arg| !arg.is_empty()))
})
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct CommandRegistry {
commands: BTreeMap<String, SlashCommand>,
}
impl CommandRegistry {
pub fn mvp() -> Self {
let mut registry = Self::default();
for command in BuiltInCommand::ALL {
registry.insert_builtin(command);
}
registry
}
pub fn get(&self, input: &str) -> Option<&SlashCommand> {
input
.strip_prefix('/')
.and_then(|name| self.commands.get(name))
}
pub fn list(&self) -> Vec<&SlashCommand> {
self.commands.values().collect()
}
fn insert_builtin(&mut self, command: BuiltInCommand) {
let name = command.name().to_string();
self.commands.insert(
name.clone(),
SlashCommand {
name,
description: command.description().to_string(),
command,
},
);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::skills::{SkillRoots, discover_skills};
use std::fs;
use tempfile::TempDir;
#[test]
fn slash_command_parser_is_canonical_for_shell_and_tui() {
let registry = CommandRegistry::mvp();
assert_eq!(
parse_slash_command("", ®istry),
ParsedSlashCommand::Empty
);
assert_eq!(
parse_slash_command(" \n ", ®istry),
ParsedSlashCommand::Empty
);
assert_eq!(
parse_slash_command(" hello ", ®istry),
ParsedSlashCommand::Prompt("hello")
);
assert_eq!(
parse_slash_command(" /login openai-codex ", ®istry),
ParsedSlashCommand::BuiltIn {
command: BuiltInCommand::Login,
arg: Some("openai-codex")
}
);
assert_eq!(
parse_slash_command("/compact keep file paths", ®istry),
ParsedSlashCommand::BuiltIn {
command: BuiltInCommand::Compact,
arg: Some("keep file paths")
}
);
assert_eq!(
parse_slash_command("/new\ntext", ®istry),
ParsedSlashCommand::BuiltIn {
command: BuiltInCommand::New,
arg: Some("text")
}
);
assert_eq!(
parse_slash_command("/setmodel zai/glm-5.2", ®istry),
ParsedSlashCommand::BuiltIn {
command: BuiltInCommand::Model,
arg: Some("zai/glm-5.2")
}
);
assert_eq!(
parse_slash_command("/models", ®istry),
ParsedSlashCommand::BuiltIn {
command: BuiltInCommand::Models,
arg: None
}
);
assert_eq!(
parse_slash_command("/usage", ®istry),
ParsedSlashCommand::BuiltIn {
command: BuiltInCommand::Usage,
arg: None
}
);
assert_eq!(
parse_slash_command("/model", ®istry),
ParsedSlashCommand::Unknown { token: "/model" }
);
assert_eq!(
parse_slash_command("/models zai/glm-5.2", ®istry),
ParsedSlashCommand::BuiltIn {
command: BuiltInCommand::Models,
arg: Some("zai/glm-5.2")
}
);
assert_eq!(
parse_slash_command("/missing arg", ®istry),
ParsedSlashCommand::Unknown { token: "/missing" }
);
}
#[test]
fn command_registry_registers_mvp_builtins() {
let temp = TempDir::new().unwrap();
let mc_skills = temp.path().join("mc/skills");
fs::create_dir_all(mc_skills.join("review")).unwrap();
fs::write(mc_skills.join("review/SKILL.md"), "# Review\n").unwrap();
let discovery = discover_skills(&SkillRoots {
mc_skills,
repo_skills: temp.path().join("missing-repo"),
legacy_agents_skills: temp.path().join("missing-legacy"),
additional_paths: Vec::new(),
});
assert!(discovery.skills.contains_key("review"));
let registry = CommandRegistry::mvp();
let names: Vec<_> = registry
.list()
.into_iter()
.map(|command| command.name.as_str())
.collect();
assert_eq!(
names,
vec![
"changes",
"compact",
"help",
"login",
"logout",
"mcp",
"models",
"new",
"prune-sessions",
"quit",
"rewind",
"sessions",
"setmodel",
"skills",
"subagents",
"system-prompt",
"tools",
"usage"
]
);
assert_eq!(
registry.get("/help").map(|command| command.command),
Some(BuiltInCommand::Help)
);
assert_eq!(
registry.get("/login").map(|command| command.command),
Some(BuiltInCommand::Login)
);
assert_eq!(
registry.get("/logout").map(|command| command.command),
Some(BuiltInCommand::Logout)
);
assert_eq!(
registry.get("/compact").map(|command| command.command),
Some(BuiltInCommand::Compact)
);
assert_eq!(
registry.get("/new").map(|command| command.command),
Some(BuiltInCommand::New)
);
assert_eq!(
registry.get("/changes").map(|command| command.command),
Some(BuiltInCommand::Changes)
);
assert_eq!(
registry.get("/rewind").map(|command| command.command),
Some(BuiltInCommand::Rewind)
);
assert_eq!(
registry.get("/models").map(|command| command.command),
Some(BuiltInCommand::Models)
);
assert_eq!(
registry.get("/usage").map(|command| command.command),
Some(BuiltInCommand::Usage)
);
assert_eq!(
registry.get("/mcp").map(|command| command.command),
Some(BuiltInCommand::Mcp)
);
assert_eq!(
registry.get("/setmodel").map(|command| command.command),
Some(BuiltInCommand::Model)
);
assert_eq!(
registry
.get("/system-prompt")
.map(|command| command.command),
Some(BuiltInCommand::SystemPrompt)
);
assert_eq!(
registry.get("/skills").map(|command| command.command),
Some(BuiltInCommand::Skills)
);
assert_eq!(
registry.get("/subagents").map(|command| command.command),
Some(BuiltInCommand::Subagents)
);
assert_eq!(
registry.get("/sessions").map(|command| command.command),
Some(BuiltInCommand::Sessions)
);
assert_eq!(
registry
.get("/prune-sessions")
.map(|command| command.command),
Some(BuiltInCommand::PruneSessions)
);
assert_eq!(
registry.get("/tools").map(|command| command.command),
Some(BuiltInCommand::Tools)
);
assert_eq!(
registry.get("/quit").map(|command| command.command),
Some(BuiltInCommand::Quit)
);
for removed in ["/model", "/session", "/resume", "/skill:review"] {
assert!(
registry.get(removed).is_none(),
"{removed} should be absent"
);
}
}
}