magi-code 0.64.0

Repository-aware CLI coding agent for terminal work
Documentation
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("", &registry),
            ParsedSlashCommand::Empty
        );
        assert_eq!(
            parse_slash_command("  \n  ", &registry),
            ParsedSlashCommand::Empty
        );
        assert_eq!(
            parse_slash_command("  hello  ", &registry),
            ParsedSlashCommand::Prompt("hello")
        );
        assert_eq!(
            parse_slash_command(" /login openai-codex ", &registry),
            ParsedSlashCommand::BuiltIn {
                command: BuiltInCommand::Login,
                arg: Some("openai-codex")
            }
        );
        assert_eq!(
            parse_slash_command("/compact keep file paths", &registry),
            ParsedSlashCommand::BuiltIn {
                command: BuiltInCommand::Compact,
                arg: Some("keep file paths")
            }
        );
        assert_eq!(
            parse_slash_command("/new\ntext", &registry),
            ParsedSlashCommand::BuiltIn {
                command: BuiltInCommand::New,
                arg: Some("text")
            }
        );
        assert_eq!(
            parse_slash_command("/setmodel zai/glm-5.2", &registry),
            ParsedSlashCommand::BuiltIn {
                command: BuiltInCommand::Model,
                arg: Some("zai/glm-5.2")
            }
        );
        assert_eq!(
            parse_slash_command("/models", &registry),
            ParsedSlashCommand::BuiltIn {
                command: BuiltInCommand::Models,
                arg: None
            }
        );
        assert_eq!(
            parse_slash_command("/usage", &registry),
            ParsedSlashCommand::BuiltIn {
                command: BuiltInCommand::Usage,
                arg: None
            }
        );
        assert_eq!(
            parse_slash_command("/model", &registry),
            ParsedSlashCommand::Unknown { token: "/model" }
        );
        assert_eq!(
            parse_slash_command("/models zai/glm-5.2", &registry),
            ParsedSlashCommand::BuiltIn {
                command: BuiltInCommand::Models,
                arg: Some("zai/glm-5.2")
            }
        );
        assert_eq!(
            parse_slash_command("/missing arg", &registry),
            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"
            );
        }
    }
}