use std::collections::BTreeMap;
use std::env;
use std::fmt;
use std::fs;
use std::path::{Path, PathBuf};

use plugins::{PluginError, PluginManager, PluginSummary};
use runtime::{
    compact_session, CompactionConfig, ConfigLoader, ConfigSource, McpOAuthConfig, McpServerConfig,
    ScopedMcpServerConfig, Session,
};
use serde_json::{json, Value};

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommandManifestEntry {
    pub name: String,
    pub source: CommandSource,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CommandSource {
    Builtin,
    InternalOnly,
    FeatureGated,
}

#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct CommandRegistry {
    entries: Vec<CommandManifestEntry>,
}

impl CommandRegistry {
    #[must_use]
    pub fn new(entries: Vec<CommandManifestEntry>) -> Self {
        Self { entries }
    }

    #[must_use]
    pub fn entries(&self) -> &[CommandManifestEntry] {
        &self.entries
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SlashCommandSpec {
    pub name: &'static str,
    pub aliases: &'static [&'static str],
    pub summary: &'static str,
    pub argument_hint: Option<&'static str>,
    pub resume_supported: bool,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SkillSlashDispatch {
    Local,
    Invoke(String),
}

const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
    SlashCommandSpec {
        name: "help",
        aliases: &[],
        summary: "Show available slash commands",
        argument_hint: None,
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "status",
        aliases: &[],
        summary: "Show current session status",
        argument_hint: None,
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "sandbox",
        aliases: &[],
        summary: "Show sandbox isolation status",
        argument_hint: None,
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "compact",
        aliases: &[],
        summary: "Compact local session history",
        argument_hint: None,
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "model",
        aliases: &[],
        summary: "Show or switch the active model",
        argument_hint: Some("[model]"),
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "permissions",
        aliases: &[],
        summary: "Show or switch the active permission mode",
        argument_hint: Some("[read-only|workspace-write|danger-full-access]"),
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "clear",
        aliases: &[],
        summary: "Start a fresh local session",
        argument_hint: Some("[--confirm]"),
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "cost",
        aliases: &[],
        summary: "Show cumulative token usage for this session",
        argument_hint: None,
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "resume",
        aliases: &[],
        summary: "Load a saved session into the REPL",
        argument_hint: Some("<session-path>"),
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "config",
        aliases: &[],
        summary: "Inspect Claude config files or merged sections",
        argument_hint: Some("[env|hooks|model|plugins]"),
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "mcp",
        aliases: &[],
        summary: "Inspect configured MCP servers",
        argument_hint: Some("[list|show <server>|help]"),
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "memory",
        aliases: &[],
        summary: "Inspect loaded Claude instruction memory files",
        argument_hint: None,
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "init",
        aliases: &[],
        summary: "Create a starter CLAUDE.md for this repo",
        argument_hint: None,
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "diff",
        aliases: &[],
        summary: "Show git diff for current workspace changes",
        argument_hint: None,
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "version",
        aliases: &[],
        summary: "Show CLI version and build information",
        argument_hint: None,
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "bughunter",
        aliases: &[],
        summary: "Inspect the codebase for likely bugs",
        argument_hint: Some("[scope]"),
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "commit",
        aliases: &[],
        summary: "Generate a commit message and create a git commit",
        argument_hint: None,
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "pr",
        aliases: &[],
        summary: "Draft or create a pull request from the conversation",
        argument_hint: Some("[context]"),
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "issue",
        aliases: &[],
        summary: "Draft or create a GitHub issue from the conversation",
        argument_hint: Some("[context]"),
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "ultraplan",
        aliases: &[],
        summary: "Run a deep planning prompt with multi-step reasoning",
        argument_hint: Some("[task]"),
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "teleport",
        aliases: &[],
        summary: "Jump to a file or symbol by searching the workspace",
        argument_hint: Some("<symbol-or-path>"),
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "debug-tool-call",
        aliases: &[],
        summary: "Replay the last tool call with debug details",
        argument_hint: None,
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "export",
        aliases: &[],
        summary: "Export the current conversation to a file",
        argument_hint: Some("[file]"),
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "session",
        aliases: &[],
        summary: "List, switch, or fork managed local sessions",
        argument_hint: Some("[list|switch <session-id>|fork [branch-name]]"),
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "plugin",
        aliases: &["plugins", "marketplace"],
        summary: "Manage DreamForge plugins",
        argument_hint: Some(
            "[list|install <path>|enable <name>|disable <name>|uninstall <id>|update <id>]",
        ),
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "agents",
        aliases: &[],
        summary: "List configured agents",
        argument_hint: Some("[list|help]"),
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "skills",
        aliases: &["skill"],
        summary: "List, install, or invoke available skills",
        argument_hint: Some("[list|install <path>|help|<skill> [args]]"),
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "doctor",
        aliases: &[],
        summary: "Diagnose setup issues and environment health",
        argument_hint: None,
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "login",
        aliases: &[],
        summary: "Log in to the service",
        argument_hint: None,
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "logout",
        aliases: &[],
        summary: "Log out of the current session",
        argument_hint: None,
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "plan",
        aliases: &[],
        summary: "Toggle or inspect planning mode",
        argument_hint: Some("[on|off]"),
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "review",
        aliases: &[],
        summary: "Run a code review on current changes",
        argument_hint: Some("[scope]"),
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "tasks",
        aliases: &[],
        summary: "List and manage background tasks",
        argument_hint: Some("[list|get <id>|stop <id>]"),
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "theme",
        aliases: &[],
        summary: "Switch the terminal color theme",
        argument_hint: Some("[theme-name]"),
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "vim",
        aliases: &[],
        summary: "Toggle vim keybinding mode",
        argument_hint: None,
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "voice",
        aliases: &[],
        summary: "Toggle voice input mode",
        argument_hint: Some("[on|off]"),
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "upgrade",
        aliases: &[],
        summary: "Check for and install CLI updates",
        argument_hint: None,
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "usage",
        aliases: &[],
        summary: "Show detailed API usage statistics",
        argument_hint: None,
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "stats",
        aliases: &[],
        summary: "Show workspace and session statistics",
        argument_hint: None,
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "rename",
        aliases: &[],
        summary: "Rename the current session",
        argument_hint: Some("<name>"),
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "copy",
        aliases: &[],
        summary: "Copy conversation or output to clipboard",
        argument_hint: Some("[last|all]"),
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "share",
        aliases: &[],
        summary: "Share the current conversation",
        argument_hint: None,
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "feedback",
        aliases: &[],
        summary: "Submit feedback about the current session",
        argument_hint: None,
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "hooks",
        aliases: &[],
        summary: "List and manage lifecycle hooks",
        argument_hint: Some("[list|run <hook>]"),
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "files",
        aliases: &[],
        summary: "List files in the current context window",
        argument_hint: None,
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "context",
        aliases: &[],
        summary: "Inspect or manage the conversation context",
        argument_hint: Some("[show|clear]"),
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "color",
        aliases: &[],
        summary: "Configure terminal color settings",
        argument_hint: Some("[scheme]"),
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "effort",
        aliases: &[],
        summary: "Set the effort level for responses",
        argument_hint: Some("[low|medium|high]"),
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "fast",
        aliases: &[],
        summary: "Toggle fast/concise response mode",
        argument_hint: None,
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "exit",
        aliases: &[],
        summary: "Exit the REPL session",
        argument_hint: None,
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "branch",
        aliases: &[],
        summary: "Create or switch git branches",
        argument_hint: Some("[name]"),
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "rewind",
        aliases: &[],
        summary: "Rewind the conversation to a previous state",
        argument_hint: Some("[steps]"),
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "summary",
        aliases: &[],
        summary: "Generate a summary of the conversation",
        argument_hint: None,
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "desktop",
        aliases: &[],
        summary: "Open or manage the desktop app integration",
        argument_hint: None,
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "ide",
        aliases: &[],
        summary: "Open or configure IDE integration",
        argument_hint: Some("[vscode|cursor]"),
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "tag",
        aliases: &[],
        summary: "Tag the current conversation point",
        argument_hint: Some("[label]"),
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "brief",
        aliases: &[],
        summary: "Toggle brief output mode",
        argument_hint: None,
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "advisor",
        aliases: &[],
        summary: "Toggle advisor mode for guidance-only responses",
        argument_hint: None,
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "stickers",
        aliases: &[],
        summary: "Browse and manage sticker packs",
        argument_hint: None,
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "insights",
        aliases: &[],
        summary: "Show AI-generated insights about the session",
        argument_hint: None,
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "thinkback",
        aliases: &[],
        summary: "Replay the thinking process of the last response",
        argument_hint: None,
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "release-notes",
        aliases: &[],
        summary: "Generate release notes from recent changes",
        argument_hint: None,
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "security-review",
        aliases: &[],
        summary: "Run a security review on the codebase",
        argument_hint: Some("[scope]"),
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "keybindings",
        aliases: &[],
        summary: "Show or configure keyboard shortcuts",
        argument_hint: None,
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "privacy-settings",
        aliases: &[],
        summary: "View or modify privacy settings",
        argument_hint: None,
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "output-style",
        aliases: &[],
        summary: "Switch output formatting style",
        argument_hint: Some("[style]"),
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "add-dir",
        aliases: &[],
        summary: "Add an additional directory to the context",
        argument_hint: Some("<path>"),
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "allowed-tools",
        aliases: &[],
        summary: "Show or modify the allowed tools list",
        argument_hint: Some("[add|remove|list] [tool]"),
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "api-key",
        aliases: &[],
        summary: "Show or set the Anthropic API key",
        argument_hint: Some("[key]"),
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "approve",
        aliases: &["yes", "y"],
        summary: "Approve a pending tool execution",
        argument_hint: None,
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "deny",
        aliases: &["no", "n"],
        summary: "Deny a pending tool execution",
        argument_hint: None,
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "undo",
        aliases: &[],
        summary: "Undo the last file write or edit",
        argument_hint: None,
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "stop",
        aliases: &[],
        summary: "Stop the current generation",
        argument_hint: None,
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "retry",
        aliases: &[],
        summary: "Retry the last failed message",
        argument_hint: None,
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "paste",
        aliases: &[],
        summary: "Paste clipboard content as input",
        argument_hint: None,
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "screenshot",
        aliases: &[],
        summary: "Take a screenshot and add to conversation",
        argument_hint: None,
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "image",
        aliases: &[],
        summary: "Add an image file to the conversation",
        argument_hint: Some("<path>"),
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "terminal-setup",
        aliases: &[],
        summary: "Configure terminal integration settings",
        argument_hint: None,
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "search",
        aliases: &[],
        summary: "Search files in the workspace",
        argument_hint: Some("<query>"),
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "listen",
        aliases: &[],
        summary: "Listen for voice input",
        argument_hint: None,
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "speak",
        aliases: &[],
        summary: "Read the last response aloud",
        argument_hint: None,
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "language",
        aliases: &[],
        summary: "Set the interface language",
        argument_hint: Some("[language]"),
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "profile",
        aliases: &[],
        summary: "Show or switch user profile",
        argument_hint: Some("[name]"),
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "max-tokens",
        aliases: &[],
        summary: "Show or set the max output tokens",
        argument_hint: Some("[count]"),
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "temperature",
        aliases: &[],
        summary: "Show or set the sampling temperature",
        argument_hint: Some("[value]"),
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "system-prompt",
        aliases: &[],
        summary: "Show the active system prompt",
        argument_hint: None,
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "tool-details",
        aliases: &[],
        summary: "Show detailed info about a specific tool",
        argument_hint: Some("<tool-name>"),
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "format",
        aliases: &[],
        summary: "Format the last response in a different style",
        argument_hint: Some("[markdown|plain|json]"),
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "pin",
        aliases: &[],
        summary: "Pin a message to persist across compaction",
        argument_hint: Some("[message-index]"),
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "unpin",
        aliases: &[],
        summary: "Unpin a previously pinned message",
        argument_hint: Some("[message-index]"),
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "bookmarks",
        aliases: &[],
        summary: "List or manage conversation bookmarks",
        argument_hint: Some("[add|remove|list]"),
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "workspace",
        aliases: &["cwd"],
        summary: "Show or change the working directory",
        argument_hint: Some("[path]"),
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "history",
        aliases: &[],
        summary: "Show conversation history summary",
        argument_hint: Some("[count]"),
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "tokens",
        aliases: &[],
        summary: "Show token count for the current conversation",
        argument_hint: None,
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "cache",
        aliases: &[],
        summary: "Show prompt cache statistics",
        argument_hint: None,
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "providers",
        aliases: &[],
        summary: "List available model providers",
        argument_hint: None,
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "notifications",
        aliases: &[],
        summary: "Show or configure notification settings",
        argument_hint: Some("[on|off|status]"),
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "changelog",
        aliases: &[],
        summary: "Show recent changes to the codebase",
        argument_hint: Some("[count]"),
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "test",
        aliases: &[],
        summary: "Run tests for the current project",
        argument_hint: Some("[filter]"),
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "lint",
        aliases: &[],
        summary: "Run linting for the current project",
        argument_hint: Some("[filter]"),
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "build",
        aliases: &[],
        summary: "Build the current project",
        argument_hint: Some("[target]"),
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "run",
        aliases: &[],
        summary: "Run a command in the project context",
        argument_hint: Some("<command>"),
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "git",
        aliases: &[],
        summary: "Run a git command in the workspace",
        argument_hint: Some("<subcommand>"),
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "stash",
        aliases: &[],
        summary: "Stash or unstash workspace changes",
        argument_hint: Some("[pop|list|apply]"),
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "blame",
        aliases: &[],
        summary: "Show git blame for a file",
        argument_hint: Some("<file> [line]"),
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "log",
        aliases: &[],
        summary: "Show git log for the workspace",
        argument_hint: Some("[count]"),
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "cron",
        aliases: &[],
        summary: "Manage scheduled tasks",
        argument_hint: Some("[list|add|remove]"),
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "team",
        aliases: &[],
        summary: "Manage agent teams",
        argument_hint: Some("[list|create|delete]"),
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "benchmark",
        aliases: &[],
        summary: "Run performance benchmarks",
        argument_hint: Some("[suite]"),
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "migrate",
        aliases: &[],
        summary: "Run pending data migrations",
        argument_hint: None,
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "reset",
        aliases: &[],
        summary: "Reset configuration to defaults",
        argument_hint: Some("[section]"),
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "telemetry",
        aliases: &[],
        summary: "Show or configure telemetry settings",
        argument_hint: Some("[on|off|status]"),
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "env",
        aliases: &[],
        summary: "Show environment variables visible to tools",
        argument_hint: None,
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "project",
        aliases: &[],
        summary: "Show project detection info",
        argument_hint: None,
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "templates",
        aliases: &[],
        summary: "List or apply prompt templates",
        argument_hint: Some("[list|apply <name>]"),
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "explain",
        aliases: &[],
        summary: "Explain a file or code snippet",
        argument_hint: Some("<path> [line-range]"),
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "refactor",
        aliases: &[],
        summary: "Suggest refactoring for a file or function",
        argument_hint: Some("<path> [scope]"),
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "docs",
        aliases: &[],
        summary: "Generate or show documentation",
        argument_hint: Some("[path]"),
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "fix",
        aliases: &[],
        summary: "Fix errors in a file or project",
        argument_hint: Some("[path]"),
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "perf",
        aliases: &[],
        summary: "Analyze performance of a function or file",
        argument_hint: Some("<path>"),
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "chat",
        aliases: &[],
        summary: "Switch to free-form chat mode",
        argument_hint: None,
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "focus",
        aliases: &[],
        summary: "Focus context on specific files or directories",
        argument_hint: Some("<path> [path...]"),
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "unfocus",
        aliases: &[],
        summary: "Remove focus from files or directories",
        argument_hint: Some("[path...]"),
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "web",
        aliases: &[],
        summary: "Fetch and summarize a web page",
        argument_hint: Some("<url>"),
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "map",
        aliases: &[],
        summary: "Show a visual map of the codebase structure",
        argument_hint: Some("[depth]"),
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "symbols",
        aliases: &[],
        summary: "List symbols (functions, classes, etc.) in a file",
        argument_hint: Some("<path>"),
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "references",
        aliases: &[],
        summary: "Find all references to a symbol",
        argument_hint: Some("<symbol>"),
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "definition",
        aliases: &[],
        summary: "Go to the definition of a symbol",
        argument_hint: Some("<symbol>"),
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "hover",
        aliases: &[],
        summary: "Show hover information for a symbol",
        argument_hint: Some("<symbol>"),
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "diagnostics",
        aliases: &[],
        summary: "Show LSP diagnostics for a file",
        argument_hint: Some("[path]"),
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "autofix",
        aliases: &[],
        summary: "Auto-fix all fixable diagnostics",
        argument_hint: Some("[path]"),
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "multi",
        aliases: &[],
        summary: "Execute multiple slash commands in sequence",
        argument_hint: Some("<commands>"),
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "macro",
        aliases: &[],
        summary: "Record or replay command macros",
        argument_hint: Some("[record|stop|play <name>]"),
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "alias",
        aliases: &[],
        summary: "Create a command alias",
        argument_hint: Some("<name> <command>"),
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "parallel",
        aliases: &[],
        summary: "Run commands in parallel subagents",
        argument_hint: Some("<count> <prompt>"),
        resume_supported: false,
    },
    SlashCommandSpec {
        name: "agent",
        aliases: &[],
        summary: "Manage sub-agents and spawned sessions",
        argument_hint: Some("[list|spawn|kill]"),
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "subagent",
        aliases: &[],
        summary: "Control active subagent execution",
        argument_hint: Some("[list|steer <target> <msg>|kill <id>]"),
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "reasoning",
        aliases: &[],
        summary: "Toggle extended reasoning mode",
        argument_hint: Some("[on|off|stream]"),
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "budget",
        aliases: &[],
        summary: "Show or set token budget limits",
        argument_hint: Some("[show|set <limit>]"),
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "rate-limit",
        aliases: &[],
        summary: "Configure API rate limiting",
        argument_hint: Some("[status|set <rpm>]"),
        resume_supported: true,
    },
    SlashCommandSpec {
        name: "metrics",
        aliases: &[],
        summary: "Show performance and usage metrics",
        argument_hint: None,
        resume_supported: true,
    },
];

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SlashCommand {
    Help,
    Status,
    Sandbox,
    Compact,
    Bughunter {
        scope: Option<String>,
    },
    Commit,
    Pr {
        context: Option<String>,
    },
    Issue {
        context: Option<String>,
    },
    Ultraplan {
        task: Option<String>,
    },
    Teleport {
        target: Option<String>,
    },
    DebugToolCall,
    Model {
        model: Option<String>,
    },
    Permissions {
        mode: Option<String>,
    },
    Clear {
        confirm: bool,
    },
    Cost,
    Resume {
        session_path: Option<String>,
    },
    Config {
        section: Option<String>,
    },
    Mcp {
        action: Option<String>,
        target: Option<String>,
    },
    Memory,
    Init,
    Diff,
    Version,
    Export {
        path: Option<String>,
    },
    Session {
        action: Option<String>,
        target: Option<String>,
    },
    Plugins {
        action: Option<String>,
        target: Option<String>,
    },
    Agents {
        args: Option<String>,
    },
    Skills {
        args: Option<String>,
    },
    Doctor,
    Login,
    Logout,
    Vim,
    Upgrade,
    Stats,
    Share,
    Feedback,
    Files,
    Fast,
    Exit,
    Summary,
    Desktop,
    Brief,
    Advisor,
    Stickers,
    Insights,
    Thinkback,
    ReleaseNotes,
    SecurityReview,
    Keybindings,
    PrivacySettings,
    Plan {
        mode: Option<String>,
    },
    Review {
        scope: Option<String>,
    },
    Tasks {
        args: Option<String>,
    },
    Theme {
        name: Option<String>,
    },
    Voice {
        mode: Option<String>,
    },
    Usage {
        scope: Option<String>,
    },
    Rename {
        name: Option<String>,
    },
    Copy {
        target: Option<String>,
    },
    Hooks {
        args: Option<String>,
    },
    Context {
        action: Option<String>,
    },
    Color {
        scheme: Option<String>,
    },
    Effort {
        level: Option<String>,
    },
    Branch {
        name: Option<String>,
    },
    Rewind {
        steps: Option<String>,
    },
    Ide {
        target: Option<String>,
    },
    Tag {
        label: Option<String>,
    },
    OutputStyle {
        style: Option<String>,
    },
    AddDir {
        path: Option<String>,
    },
    Unknown(String),
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SlashCommandParseError {
    message: String,
}

impl SlashCommandParseError {
    fn new(message: impl Into<String>) -> Self {
        Self {
            message: message.into(),
        }
    }
}

impl fmt::Display for SlashCommandParseError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.message)
    }
}

impl std::error::Error for SlashCommandParseError {}

impl SlashCommand {
    pub fn parse(input: &str) -> Result<Option<Self>, SlashCommandParseError> {
        validate_slash_command_input(input)
    }
}

#[allow(clippy::too_many_lines)]
pub fn validate_slash_command_input(
    input: &str,
) -> Result<Option<SlashCommand>, SlashCommandParseError> {
    let trimmed = input.trim();
    if !trimmed.starts_with('/') {
        return Ok(None);
    }

    let mut parts = trimmed.trim_start_matches('/').split_whitespace();
    let command = parts.next().unwrap_or_default();
    if command.is_empty() {
        return Err(SlashCommandParseError::new(
            "Slash command name is missing. Use /help to list available slash commands.",
        ));
    }

    let args = parts.collect::<Vec<_>>();
    let remainder = remainder_after_command(trimmed, command);

    Ok(Some(match command {
        "help" => {
            validate_no_args(command, &args)?;
            SlashCommand::Help
        }
        "status" => {
            validate_no_args(command, &args)?;
            SlashCommand::Status
        }
        "sandbox" => {
            validate_no_args(command, &args)?;
            SlashCommand::Sandbox
        }
        "compact" => {
            validate_no_args(command, &args)?;
            SlashCommand::Compact
        }
        "bughunter" => SlashCommand::Bughunter { scope: remainder },
        "commit" => {
            validate_no_args(command, &args)?;
            SlashCommand::Commit
        }
        "pr" => SlashCommand::Pr { context: remainder },
        "issue" => SlashCommand::Issue { context: remainder },
        "ultraplan" => SlashCommand::Ultraplan { task: remainder },
        "teleport" => SlashCommand::Teleport {
            target: Some(require_remainder(command, remainder, "<symbol-or-path>")?),
        },
        "debug-tool-call" => {
            validate_no_args(command, &args)?;
            SlashCommand::DebugToolCall
        }
        "model" => SlashCommand::Model {
            model: optional_single_arg(command, &args, "[model]")?,
        },
        "permissions" => SlashCommand::Permissions {
            mode: parse_permissions_mode(&args)?,
        },
        "clear" => SlashCommand::Clear {
            confirm: parse_clear_args(&args)?,
        },
        "cost" => {
            validate_no_args(command, &args)?;
            SlashCommand::Cost
        }
        "resume" => SlashCommand::Resume {
            session_path: Some(require_remainder(command, remainder, "<session-path>")?),
        },
        "config" => SlashCommand::Config {
            section: parse_config_section(&args)?,
        },
        "mcp" => parse_mcp_command(&args)?,
        "memory" => {
            validate_no_args(command, &args)?;
            SlashCommand::Memory
        }
        "init" => {
            validate_no_args(command, &args)?;
            SlashCommand::Init
        }
        "diff" => {
            validate_no_args(command, &args)?;
            SlashCommand::Diff
        }
        "version" => {
            validate_no_args(command, &args)?;
            SlashCommand::Version
        }
        "export" => SlashCommand::Export { path: remainder },
        "session" => parse_session_command(&args)?,
        "plugin" | "plugins" | "marketplace" => parse_plugin_command(&args)?,
        "agents" => SlashCommand::Agents {
            args: parse_list_or_help_args(command, remainder)?,
        },
        "skills" | "skill" => SlashCommand::Skills {
            args: parse_skills_args(remainder.as_deref())?,
        },
        "doctor" => {
            validate_no_args(command, &args)?;
            SlashCommand::Doctor
        }
        "login" => {
            validate_no_args(command, &args)?;
            SlashCommand::Login
        }
        "logout" => {
            validate_no_args(command, &args)?;
            SlashCommand::Logout
        }
        "vim" => {
            validate_no_args(command, &args)?;
            SlashCommand::Vim
        }
        "upgrade" => {
            validate_no_args(command, &args)?;
            SlashCommand::Upgrade
        }
        "stats" => {
            validate_no_args(command, &args)?;
            SlashCommand::Stats
        }
        "share" => {
            validate_no_args(command, &args)?;
            SlashCommand::Share
        }
        "feedback" => {
            validate_no_args(command, &args)?;
            SlashCommand::Feedback
        }
        "files" => {
            validate_no_args(command, &args)?;
            SlashCommand::Files
        }
        "fast" => {
            validate_no_args(command, &args)?;
            SlashCommand::Fast
        }
        "exit" => {
            validate_no_args(command, &args)?;
            SlashCommand::Exit
        }
        "summary" => {
            validate_no_args(command, &args)?;
            SlashCommand::Summary
        }
        "desktop" => {
            validate_no_args(command, &args)?;
            SlashCommand::Desktop
        }
        "brief" => {
            validate_no_args(command, &args)?;
            SlashCommand::Brief
        }
        "advisor" => {
            validate_no_args(command, &args)?;
            SlashCommand::Advisor
        }
        "stickers" => {
            validate_no_args(command, &args)?;
            SlashCommand::Stickers
        }
        "insights" => {
            validate_no_args(command, &args)?;
            SlashCommand::Insights
        }
        "thinkback" => {
            validate_no_args(command, &args)?;
            SlashCommand::Thinkback
        }
        "release-notes" => {
            validate_no_args(command, &args)?;
            SlashCommand::ReleaseNotes
        }
        "security-review" => {
            validate_no_args(command, &args)?;
            SlashCommand::SecurityReview
        }
        "keybindings" => {
            validate_no_args(command, &args)?;
            SlashCommand::Keybindings
        }
        "privacy-settings" => {
            validate_no_args(command, &args)?;
            SlashCommand::PrivacySettings
        }
        "plan" => SlashCommand::Plan { mode: remainder },
        "review" => SlashCommand::Review { scope: remainder },
        "tasks" => SlashCommand::Tasks { args: remainder },
        "theme" => SlashCommand::Theme { name: remainder },
        "voice" => SlashCommand::Voice { mode: remainder },
        "usage" => SlashCommand::Usage { scope: remainder },
        "rename" => SlashCommand::Rename { name: remainder },
        "copy" => SlashCommand::Copy { target: remainder },
        "hooks" => SlashCommand::Hooks { args: remainder },
        "context" => SlashCommand::Context { action: remainder },
        "color" => SlashCommand::Color { scheme: remainder },
        "effort" => SlashCommand::Effort { level: remainder },
        "branch" => SlashCommand::Branch { name: remainder },
        "rewind" => SlashCommand::Rewind { steps: remainder },
        "ide" => SlashCommand::Ide { target: remainder },
        "tag" => SlashCommand::Tag { label: remainder },
        "output-style" => SlashCommand::OutputStyle { style: remainder },
        "add-dir" => SlashCommand::AddDir { path: remainder },
        other => SlashCommand::Unknown(other.to_string()),
    }))
}
fn validate_no_args(command: &str, args: &[&str]) -> Result<(), SlashCommandParseError> {
    if args.is_empty() {
        return Ok(());
    }

    Err(command_error(
        &format!("Unexpected arguments for /{command}."),
        command,
        &format!("/{command}"),
    ))
}

fn optional_single_arg(
    command: &str,
    args: &[&str],
    argument_hint: &str,
) -> Result<Option<String>, SlashCommandParseError> {
    match args {
        [] => Ok(None),
        [value] => Ok(Some((*value).to_string())),
        _ => Err(usage_error(command, argument_hint)),
    }
}

fn require_remainder(
    command: &str,
    remainder: Option<String>,
    argument_hint: &str,
) -> Result<String, SlashCommandParseError> {
    remainder.ok_or_else(|| usage_error(command, argument_hint))
}

fn parse_permissions_mode(args: &[&str]) -> Result<Option<String>, SlashCommandParseError> {
    let mode = optional_single_arg(
        "permissions",
        args,
        "[read-only|workspace-write|danger-full-access]",
    )?;
    if let Some(mode) = mode {
        if matches!(
            mode.as_str(),
            "read-only" | "workspace-write" | "danger-full-access"
        ) {
            return Ok(Some(mode));
        }
        return Err(command_error(
            &format!(
                "Unsupported /permissions mode '{mode}'. Use read-only, workspace-write, or danger-full-access."
            ),
            "permissions",
            "/permissions [read-only|workspace-write|danger-full-access]",
        ));
    }

    Ok(None)
}

fn parse_clear_args(args: &[&str]) -> Result<bool, SlashCommandParseError> {
    match args {
        [] => Ok(false),
        ["--confirm"] => Ok(true),
        [unexpected] => Err(command_error(
            &format!("Unsupported /clear argument '{unexpected}'. Use /clear or /clear --confirm."),
            "clear",
            "/clear [--confirm]",
        )),
        _ => Err(usage_error("clear", "[--confirm]")),
    }
}

fn parse_config_section(args: &[&str]) -> Result<Option<String>, SlashCommandParseError> {
    let section = optional_single_arg("config", args, "[env|hooks|model|plugins]")?;
    if let Some(section) = section {
        if matches!(section.as_str(), "env" | "hooks" | "model" | "plugins") {
            return Ok(Some(section));
        }
        return Err(command_error(
            &format!("Unsupported /config section '{section}'. Use env, hooks, model, or plugins."),
            "config",
            "/config [env|hooks|model|plugins]",
        ));
    }

    Ok(None)
}

fn parse_session_command(args: &[&str]) -> Result<SlashCommand, SlashCommandParseError> {
    match args {
        [] => Ok(SlashCommand::Session {
            action: None,
            target: None,
        }),
        ["list"] => Ok(SlashCommand::Session {
            action: Some("list".to_string()),
            target: None,
        }),
        ["list", ..] => Err(usage_error("session", "[list|switch <session-id>|fork [branch-name]]")),
        ["switch"] => Err(usage_error("session switch", "<session-id>")),
        ["switch", target] => Ok(SlashCommand::Session {
            action: Some("switch".to_string()),
            target: Some((*target).to_string()),
        }),
        ["switch", ..] => Err(command_error(
            "Unexpected arguments for /session switch.",
            "session",
            "/session switch <session-id>",
        )),
        ["fork"] => Ok(SlashCommand::Session {
            action: Some("fork".to_string()),
            target: None,
        }),
        ["fork", target] => Ok(SlashCommand::Session {
            action: Some("fork".to_string()),
            target: Some((*target).to_string()),
        }),
        ["fork", ..] => Err(command_error(
            "Unexpected arguments for /session fork.",
            "session",
            "/session fork [branch-name]",
        )),
        [action, ..] => Err(command_error(
            &format!(
                "Unknown /session action '{action}'. Use list, switch <session-id>, or fork [branch-name]."
            ),
            "session",
            "/session [list|switch <session-id>|fork [branch-name]]",
        )),
    }
}

fn parse_mcp_command(args: &[&str]) -> Result<SlashCommand, SlashCommandParseError> {
    match args {
        [] => Ok(SlashCommand::Mcp {
            action: None,
            target: None,
        }),
        ["list"] => Ok(SlashCommand::Mcp {
            action: Some("list".to_string()),
            target: None,
        }),
        ["list", ..] => Err(usage_error("mcp list", "")),
        ["show"] => Err(usage_error("mcp show", "<server>")),
        ["show", target] => Ok(SlashCommand::Mcp {
            action: Some("show".to_string()),
            target: Some((*target).to_string()),
        }),
        ["show", ..] => Err(command_error(
            "Unexpected arguments for /mcp show.",
            "mcp",
            "/mcp show <server>",
        )),
        ["help" | "-h" | "--help"] => Ok(SlashCommand::Mcp {
            action: Some("help".to_string()),
            target: None,
        }),
        [action, ..] => Err(command_error(
            &format!("Unknown /mcp action '{action}'. Use list, show <server>, or help."),
            "mcp",
            "/mcp [list|show <server>|help]",
        )),
    }
}

fn parse_plugin_command(args: &[&str]) -> Result<SlashCommand, SlashCommandParseError> {
    match args {
        [] => Ok(SlashCommand::Plugins {
            action: None,
            target: None,
        }),
        ["list"] => Ok(SlashCommand::Plugins {
            action: Some("list".to_string()),
            target: None,
        }),
        ["list", ..] => Err(usage_error("plugin list", "")),
        ["install"] => Err(usage_error("plugin install", "<path>")),
        ["install", target @ ..] => Ok(SlashCommand::Plugins {
            action: Some("install".to_string()),
            target: Some(target.join(" ")),
        }),
        ["enable"] => Err(usage_error("plugin enable", "<name>")),
        ["enable", target] => Ok(SlashCommand::Plugins {
            action: Some("enable".to_string()),
            target: Some((*target).to_string()),
        }),
        ["enable", ..] => Err(command_error(
            "Unexpected arguments for /plugin enable.",
            "plugin",
            "/plugin enable <name>",
        )),
        ["disable"] => Err(usage_error("plugin disable", "<name>")),
        ["disable", target] => Ok(SlashCommand::Plugins {
            action: Some("disable".to_string()),
            target: Some((*target).to_string()),
        }),
        ["disable", ..] => Err(command_error(
            "Unexpected arguments for /plugin disable.",
            "plugin",
            "/plugin disable <name>",
        )),
        ["uninstall"] => Err(usage_error("plugin uninstall", "<id>")),
        ["uninstall", target] => Ok(SlashCommand::Plugins {
            action: Some("uninstall".to_string()),
            target: Some((*target).to_string()),
        }),
        ["uninstall", ..] => Err(command_error(
            "Unexpected arguments for /plugin uninstall.",
            "plugin",
            "/plugin uninstall <id>",
        )),
        ["update"] => Err(usage_error("plugin update", "<id>")),
        ["update", target] => Ok(SlashCommand::Plugins {
            action: Some("update".to_string()),
            target: Some((*target).to_string()),
        }),
        ["update", ..] => Err(command_error(
            "Unexpected arguments for /plugin update.",
            "plugin",
            "/plugin update <id>",
        )),
        [action, ..] => Err(command_error(
            &format!(
                "Unknown /plugin action '{action}'. Use list, install <path>, enable <name>, disable <name>, uninstall <id>, or update <id>."
            ),
            "plugin",
            "/plugin [list|install <path>|enable <name>|disable <name>|uninstall <id>|update <id>]",
        )),
    }
}

fn parse_list_or_help_args(
    command: &str,
    args: Option<String>,
) -> Result<Option<String>, SlashCommandParseError> {
    match normalize_optional_args(args.as_deref()) {
        None | Some("list" | "help" | "-h" | "--help") => Ok(args),
        Some(unexpected) => Err(command_error(
            &format!(
                "Unexpected arguments for /{command}: {unexpected}. Use /{command}, /{command} list, or /{command} help."
            ),
            command,
            &format!("/{command} [list|help]"),
        )),
    }
}

fn parse_skills_args(args: Option<&str>) -> Result<Option<String>, SlashCommandParseError> {
    let Some(args) = normalize_optional_args(args) else {
        return Ok(None);
    };

    if matches!(args, "list" | "help" | "-h" | "--help") {
        return Ok(Some(args.to_string()));
    }

    if args == "install" {
        return Err(command_error(
            "Usage: /skills install <path>",
            "skills",
            "/skills install <path>",
        ));
    }

    if let Some(target) = args.strip_prefix("install").map(str::trim) {
        if !target.is_empty() {
            return Ok(Some(format!("install {target}")));
        }
    }

    Ok(Some(args.to_string()))
}

fn usage_error(command: &str, argument_hint: &str) -> SlashCommandParseError {
    let usage = format!("/{command} {argument_hint}");
    let usage = usage.trim_end().to_string();
    command_error(
        &format!("Usage: {usage}"),
        command_root_name(command),
        &usage,
    )
}

fn command_error(message: &str, command: &str, usage: &str) -> SlashCommandParseError {
    let detail = render_slash_command_help_detail(command)
        .map(|detail| format!("\n\n{detail}"))
        .unwrap_or_default();
    SlashCommandParseError::new(format!("{message}\n  Usage            {usage}{detail}"))
}

fn remainder_after_command(input: &str, command: &str) -> Option<String> {
    input
        .trim()
        .strip_prefix(&format!("/{command}"))
        .map(str::trim)
        .filter(|value| !value.is_empty())
        .map(ToOwned::to_owned)
}

fn find_slash_command_spec(name: &str) -> Option<&'static SlashCommandSpec> {
    slash_command_specs().iter().find(|spec| {
        spec.name.eq_ignore_ascii_case(name)
            || spec
                .aliases
                .iter()
                .any(|alias| alias.eq_ignore_ascii_case(name))
    })
}

fn command_root_name(command: &str) -> &str {
    command.split_whitespace().next().unwrap_or(command)
}

fn slash_command_usage(spec: &SlashCommandSpec) -> String {
    match spec.argument_hint {
        Some(argument_hint) => format!("/{} {argument_hint}", spec.name),
        None => format!("/{}", spec.name),
    }
}

fn slash_command_detail_lines(spec: &SlashCommandSpec) -> Vec<String> {
    let mut lines = vec![format!("/{}", spec.name)];
    lines.push(format!("  Summary          {}", spec.summary));
    lines.push(format!("  Usage            {}", slash_command_usage(spec)));
    lines.push(format!(
        "  Category         {}",
        slash_command_category(spec.name)
    ));
    if !spec.aliases.is_empty() {
        lines.push(format!(
            "  Aliases          {}",
            spec.aliases
                .iter()
                .map(|alias| format!("/{alias}"))
                .collect::<Vec<_>>()
                .join(", ")
        ));
    }
    if spec.resume_supported {
        lines.push("  Resume           Supported with --resume SESSION.jsonl".to_string());
    }
    lines
}

#[must_use]
pub fn render_slash_command_help_detail(name: &str) -> Option<String> {
    find_slash_command_spec(name).map(|spec| slash_command_detail_lines(spec).join("\n"))
}

#[must_use]
pub fn slash_command_specs() -> &'static [SlashCommandSpec] {
    SLASH_COMMAND_SPECS
}

#[must_use]
pub fn resume_supported_slash_commands() -> Vec<&'static SlashCommandSpec> {
    slash_command_specs()
        .iter()
        .filter(|spec| spec.resume_supported)
        .collect()
}

fn slash_command_category(name: &str) -> &'static str {
    match name {
        "help" | "status" | "sandbox" | "model" | "permissions" | "cost" | "resume" | "session"
        | "version" | "login" | "logout" | "usage" | "stats" | "rename" | "privacy-settings" => {
            "Session & visibility"
        }
        "compact" | "clear" | "config" | "memory" | "init" | "diff" | "commit" | "pr" | "issue"
        | "export" | "plugin" | "branch" | "add-dir" | "files" | "hooks" | "release-notes" => {
            "Workspace & git"
        }
        "agents" | "skills" | "teleport" | "debug-tool-call" | "mcp" | "context" | "tasks"
        | "doctor" | "ide" | "desktop" => "Discovery & debugging",
        "bughunter" | "ultraplan" | "review" | "security-review" | "advisor" | "insights" => {
            "Analysis & automation"
        }
        "theme" | "vim" | "voice" | "color" | "effort" | "fast" | "brief" | "output-style"
        | "keybindings" | "stickers" => "Appearance & input",
        "copy" | "share" | "feedback" | "summary" | "tag" | "thinkback" | "plan" | "exit"
        | "upgrade" | "rewind" => "Communication & control",
        _ => "Other",
    }
}

fn format_slash_command_help_line(spec: &SlashCommandSpec) -> String {
    let name = slash_command_usage(spec);
    let alias_suffix = if spec.aliases.is_empty() {
        String::new()
    } else {
        format!(
            " (aliases: {})",
            spec.aliases
                .iter()
                .map(|alias| format!("/{alias}"))
                .collect::<Vec<_>>()
                .join(", ")
        )
    };
    let resume = if spec.resume_supported {
        " [resume]"
    } else {
        ""
    };
    format!("  {name:<66} {}{alias_suffix}{resume}", spec.summary)
}

fn levenshtein_distance(left: &str, right: &str) -> usize {
    if left == right {
        return 0;
    }
    if left.is_empty() {
        return right.chars().count();
    }
    if right.is_empty() {
        return left.chars().count();
    }

    let right_chars = right.chars().collect::<Vec<_>>();
    let mut previous = (0..=right_chars.len()).collect::<Vec<_>>();
    let mut current = vec![0; right_chars.len() + 1];

    for (left_index, left_char) in left.chars().enumerate() {
        current[0] = left_index + 1;
        for (right_index, right_char) in right_chars.iter().enumerate() {
            let substitution_cost = usize::from(left_char != *right_char);
            current[right_index + 1] = (current[right_index] + 1)
                .min(previous[right_index + 1] + 1)
                .min(previous[right_index] + substitution_cost);
        }
        previous.clone_from(&current);
    }

    previous[right_chars.len()]
}

#[must_use]
pub fn suggest_slash_commands(input: &str, limit: usize) -> Vec<String> {
    let query = input.trim().trim_start_matches('/').to_ascii_lowercase();
    if query.is_empty() || limit == 0 {
        return Vec::new();
    }

    let mut suggestions = slash_command_specs()
        .iter()
        .filter_map(|spec| {
            let best = std::iter::once(spec.name)
                .chain(spec.aliases.iter().copied())
                .map(str::to_ascii_lowercase)
                .map(|candidate| {
                    let prefix_rank =
                        if candidate.starts_with(&query) || query.starts_with(&candidate) {
                            0
                        } else if candidate.contains(&query) || query.contains(&candidate) {
                            1
                        } else {
                            2
                        };
                    let distance = levenshtein_distance(&candidate, &query);
                    (prefix_rank, distance)
                })
                .min();

            best.and_then(|(prefix_rank, distance)| {
                if prefix_rank <= 1 || distance <= 2 {
                    Some((prefix_rank, distance, spec.name.len(), spec.name))
                } else {
                    None
                }
            })
        })
        .collect::<Vec<_>>();

    suggestions.sort_unstable();
    suggestions
        .into_iter()
        .map(|(_, _, _, name)| format!("/{name}"))
        .take(limit)
        .collect()
}

#[must_use]
pub fn render_slash_command_help() -> String {
    let mut lines = vec![
        "Slash commands".to_string(),
        "  Start here        /status, /diff, /agents, /skills, /commit".to_string(),
        "  [resume]          also works with --resume SESSION.jsonl".to_string(),
        String::new(),
    ];

    let categories = [
        "Session & visibility",
        "Workspace & git",
        "Discovery & debugging",
        "Analysis & automation",
    ];

    for category in categories {
        lines.push(category.to_string());
        for spec in slash_command_specs()
            .iter()
            .filter(|spec| slash_command_category(spec.name) == category)
        {
            lines.push(format_slash_command_help_line(spec));
        }
        lines.push(String::new());
    }

    lines
        .into_iter()
        .rev()
        .skip_while(String::is_empty)
        .collect::<Vec<_>>()
        .into_iter()
        .rev()
        .collect::<Vec<_>>()
        .join("\n")
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SlashCommandResult {
    pub message: String,
    pub session: Session,
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PluginsCommandResult {
    pub message: String,
    pub reload_runtime: bool,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum DefinitionSource {
    ProjectForge,
    ProjectCodex,
    ProjectClaude,
    UserForgeConfigHome,
    UserCodexHome,
    UserForge,
    UserCodex,
    UserClaude,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
enum DefinitionScope {
    Project,
    UserConfigHome,
    UserHome,
}

impl DefinitionScope {
    fn label(self) -> &'static str {
        match self {
            Self::Project => "Project roots",
            Self::UserConfigHome => "User config roots",
            Self::UserHome => "User home roots",
        }
    }
}

impl DefinitionSource {
    fn report_scope(self) -> DefinitionScope {
        match self {
            Self::ProjectForge | Self::ProjectCodex | Self::ProjectClaude => {
                DefinitionScope::Project
            }
            Self::UserForgeConfigHome | Self::UserCodexHome => DefinitionScope::UserConfigHome,
            Self::UserForge | Self::UserCodex | Self::UserClaude => DefinitionScope::UserHome,
        }
    }

    fn label(self) -> &'static str {
        self.report_scope().label()
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct AgentSummary {
    name: String,
    description: Option<String>,
    model: Option<String>,
    reasoning_effort: Option<String>,
    source: DefinitionSource,
    shadowed_by: Option<DefinitionSource>,
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct SkillSummary {
    name: String,
    description: Option<String>,
    source: DefinitionSource,
    shadowed_by: Option<DefinitionSource>,
    origin: SkillOrigin,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum SkillOrigin {
    SkillsDir,
    LegacyCommandsDir,
}

impl SkillOrigin {
    fn detail_label(self) -> Option<&'static str> {
        match self {
            Self::SkillsDir => None,
            Self::LegacyCommandsDir => Some("legacy /commands"),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct SkillRoot {
    source: DefinitionSource,
    path: PathBuf,
    origin: SkillOrigin,
}

#[derive(Debug, Clone, PartialEq, Eq)]
struct InstalledSkill {
    invocation_name: String,
    display_name: Option<String>,
    source: PathBuf,
    registry_root: PathBuf,
    installed_path: PathBuf,
}

#[derive(Debug, Clone, PartialEq, Eq)]
enum SkillInstallSource {
    Directory { root: PathBuf, prompt_path: PathBuf },
    MarkdownFile { path: PathBuf },
}

#[allow(clippy::too_many_lines)]
pub fn handle_plugins_slash_command(
    action: Option<&str>,
    target: Option<&str>,
    manager: &mut PluginManager,
) -> Result<PluginsCommandResult, PluginError> {
    match action {
        None | Some("list") => Ok(PluginsCommandResult {
            message: render_plugins_report(&manager.list_installed_plugins()?),
            reload_runtime: false,
        }),
        Some("install") => {
            let Some(target) = target else {
                return Ok(PluginsCommandResult {
                    message: "Usage: /plugins install <path>".to_string(),
                    reload_runtime: false,
                });
            };
            let install = manager.install(target)?;
            let plugin = manager
                .list_installed_plugins()?
                .into_iter()
                .find(|plugin| plugin.metadata.id == install.plugin_id);
            Ok(PluginsCommandResult {
                message: render_plugin_install_report(&install.plugin_id, plugin.as_ref()),
                reload_runtime: true,
            })
        }
        Some("enable") => {
            let Some(target) = target else {
                return Ok(PluginsCommandResult {
                    message: "Usage: /plugins enable <name>".to_string(),
                    reload_runtime: false,
                });
            };
            let plugin = resolve_plugin_target(manager, target)?;
            manager.enable(&plugin.metadata.id)?;
            Ok(PluginsCommandResult {
                message: format!(
                    "Plugins\n  Result           enabled {}\n  Name             {}\n  Version          {}\n  Status           enabled",
                    plugin.metadata.id, plugin.metadata.name, plugin.metadata.version
                ),
                reload_runtime: true,
            })
        }
        Some("disable") => {
            let Some(target) = target else {
                return Ok(PluginsCommandResult {
                    message: "Usage: /plugins disable <name>".to_string(),
                    reload_runtime: false,
                });
            };
            let plugin = resolve_plugin_target(manager, target)?;
            manager.disable(&plugin.metadata.id)?;
            Ok(PluginsCommandResult {
                message: format!(
                    "Plugins\n  Result           disabled {}\n  Name             {}\n  Version          {}\n  Status           disabled",
                    plugin.metadata.id, plugin.metadata.name, plugin.metadata.version
                ),
                reload_runtime: true,
            })
        }
        Some("uninstall") => {
            let Some(target) = target else {
                return Ok(PluginsCommandResult {
                    message: "Usage: /plugins uninstall <plugin-id>".to_string(),
                    reload_runtime: false,
                });
            };
            manager.uninstall(target)?;
            Ok(PluginsCommandResult {
                message: format!("Plugins\n  Result           uninstalled {target}"),
                reload_runtime: true,
            })
        }
        Some("update") => {
            let Some(target) = target else {
                return Ok(PluginsCommandResult {
                    message: "Usage: /plugins update <plugin-id>".to_string(),
                    reload_runtime: false,
                });
            };
            let update = manager.update(target)?;
            let plugin = manager
                .list_installed_plugins()?
                .into_iter()
                .find(|plugin| plugin.metadata.id == update.plugin_id);
            Ok(PluginsCommandResult {
                message: format!(
                    "Plugins\n  Result           updated {}\n  Name             {}\n  Old version      {}\n  New version      {}\n  Status           {}",
                    update.plugin_id,
                    plugin
                        .as_ref()
                        .map_or_else(|| update.plugin_id.clone(), |plugin| plugin.metadata.name.clone()),
                    update.old_version,
                    update.new_version,
                    plugin
                        .as_ref()
                        .map_or("unknown", |plugin| if plugin.enabled { "enabled" } else { "disabled" }),
                ),
                reload_runtime: true,
            })
        }
        Some(other) => Ok(PluginsCommandResult {
            message: format!(
                "Unknown /plugins action '{other}'. Use list, install, enable, disable, uninstall, or update."
            ),
            reload_runtime: false,
        }),
    }
}

pub fn handle_agents_slash_command(args: Option<&str>, cwd: &Path) -> std::io::Result<String> {
    if let Some(args) = normalize_optional_args(args) {
        if let Some(help_path) = help_path_from_args(args) {
            return Ok(match help_path.as_slice() {
                [] => render_agents_usage(None),
                _ => render_agents_usage(Some(&help_path.join(" "))),
            });
        }
    }

    match normalize_optional_args(args) {
        None | Some("list") => {
            let roots = discover_definition_roots(cwd, "agents");
            let agents = load_agents_from_roots(&roots)?;
            Ok(render_agents_report(&agents))
        }
        Some(args) if is_help_arg(args) => Ok(render_agents_usage(None)),
        Some(args) => Ok(render_agents_usage(Some(args))),
    }
}

pub fn handle_agents_slash_command_json(args: Option<&str>, cwd: &Path) -> std::io::Result<Value> {
    if let Some(args) = normalize_optional_args(args) {
        if let Some(help_path) = help_path_from_args(args) {
            return Ok(match help_path.as_slice() {
                [] => render_agents_usage_json(None),
                _ => render_agents_usage_json(Some(&help_path.join(" "))),
            });
        }
    }

    match normalize_optional_args(args) {
        None | Some("list") => {
            let roots = discover_definition_roots(cwd, "agents");
            let agents = load_agents_from_roots(&roots)?;
            Ok(render_agents_report_json(cwd, &agents))
        }
        Some(args) if is_help_arg(args) => Ok(render_agents_usage_json(None)),
        Some(args) => Ok(render_agents_usage_json(Some(args))),
    }
}

pub fn handle_mcp_slash_command(
    args: Option<&str>,
    cwd: &Path,
) -> Result<String, runtime::ConfigError> {
    let loader = ConfigLoader::default_for(cwd);
    render_mcp_report_for(&loader, cwd, args)
}

pub fn handle_mcp_slash_command_json(
    args: Option<&str>,
    cwd: &Path,
) -> Result<Value, runtime::ConfigError> {
    let loader = ConfigLoader::default_for(cwd);
    render_mcp_report_json_for(&loader, cwd, args)
}

pub fn handle_skills_slash_command(args: Option<&str>, cwd: &Path) -> std::io::Result<String> {
    if let Some(args) = normalize_optional_args(args) {
        if let Some(help_path) = help_path_from_args(args) {
            return Ok(match help_path.as_slice() {
                [] => render_skills_usage(None),
                ["install", ..] => render_skills_usage(Some("install")),
                _ => render_skills_usage(Some(&help_path.join(" "))),
            });
        }
    }

    match normalize_optional_args(args) {
        None | Some("list") => {
            let roots = discover_skill_roots(cwd);
            let skills = load_skills_from_roots(&roots)?;
            Ok(render_skills_report(&skills))
        }
        Some("install") => Ok(render_skills_usage(Some("install"))),
        Some(args) if args.starts_with("install ") => {
            let target = args["install ".len()..].trim();
            if target.is_empty() {
                return Ok(render_skills_usage(Some("install")));
            }
            let install = install_skill(target, cwd)?;
            Ok(render_skill_install_report(&install))
        }
        Some(args) if is_help_arg(args) => Ok(render_skills_usage(None)),
        Some(args) => Ok(render_skills_usage(Some(args))),
    }
}

pub fn handle_skills_slash_command_json(args: Option<&str>, cwd: &Path) -> std::io::Result<Value> {
    if let Some(args) = normalize_optional_args(args) {
        if let Some(help_path) = help_path_from_args(args) {
            return Ok(match help_path.as_slice() {
                [] => render_skills_usage_json(None),
                ["install", ..] => render_skills_usage_json(Some("install")),
                _ => render_skills_usage_json(Some(&help_path.join(" "))),
            });
        }
    }

    match normalize_optional_args(args) {
        None | Some("list") => {
            let roots = discover_skill_roots(cwd);
            let skills = load_skills_from_roots(&roots)?;
            Ok(render_skills_report_json(&skills))
        }
        Some("install") => Ok(render_skills_usage_json(Some("install"))),
        Some(args) if args.starts_with("install ") => {
            let target = args["install ".len()..].trim();
            if target.is_empty() {
                return Ok(render_skills_usage_json(Some("install")));
            }
            let install = install_skill(target, cwd)?;
            Ok(render_skill_install_report_json(&install))
        }
        Some(args) if is_help_arg(args) => Ok(render_skills_usage_json(None)),
        Some(args) => Ok(render_skills_usage_json(Some(args))),
    }
}

#[must_use]
pub fn classify_skills_slash_command(args: Option<&str>) -> SkillSlashDispatch {
    match normalize_optional_args(args) {
        None | Some("list" | "help" | "-h" | "--help") => SkillSlashDispatch::Local,
        Some(args) if args == "install" || args.starts_with("install ") => {
            SkillSlashDispatch::Local
        }
        Some(args) => SkillSlashDispatch::Invoke(format!("${}", args.trim_start_matches('/'))),
    }
}

/// Resolve a skill invocation by validating the skill exists on disk before
/// returning the dispatch.  When the skill is not found, returns `Err` with a
/// human-readable message that lists nearby skill names.
pub fn resolve_skill_invocation(
    cwd: &Path,
    args: Option<&str>,
) -> Result<SkillSlashDispatch, String> {
    let dispatch = classify_skills_slash_command(args);
    if let SkillSlashDispatch::Invoke(ref prompt) = dispatch {
        // Extract the skill name from the "$skill [args]" prompt.
        let skill_token = prompt
            .trim_start_matches('$')
            .split_whitespace()
            .next()
            .unwrap_or_default();
        if !skill_token.is_empty() {
            if let Err(error) = resolve_skill_path(cwd, skill_token) {
                let mut message =
                    format!("Unknown skill: {skill_token} ({error})");
                let roots = discover_skill_roots(cwd);
                if let Ok(available) = load_skills_from_roots(&roots) {
                    let names: Vec<String> = available
                        .iter()
                        .filter(|s| s.shadowed_by.is_none())
                        .map(|s| s.name.clone())
                        .collect();
                    if !names.is_empty() {
                        message.push_str(&format!(
                            "\n  Available skills: {}",
                            names.join(", ")
                        ));
                    }
                }
                message.push_str(
                    "\n  Usage: /skills [list|install <path>|help|<skill> [args]]",
                );
                return Err(message);
            }
        }
    }
    Ok(dispatch)
}

pub fn resolve_skill_path(cwd: &Path, skill: &str) -> std::io::Result<PathBuf> {
    let requested = skill.trim().trim_start_matches('/').trim_start_matches('$');
    if requested.is_empty() {
        return Err(std::io::Error::new(
            std::io::ErrorKind::InvalidInput,
            "skill must not be empty",
        ));
    }

    let roots = discover_skill_roots(cwd);
    for root in &roots {
        let mut entries = Vec::new();
        for entry in fs::read_dir(&root.path)? {
            let entry = entry?;
            match root.origin {
                SkillOrigin::SkillsDir => {
                    if !entry.path().is_dir() {
                        continue;
                    }
                    let skill_path = entry.path().join("SKILL.md");
                    if !skill_path.is_file() {
                        continue;
                    }
                    let contents = fs::read_to_string(&skill_path)?;
                    let (name, _) = parse_skill_frontmatter(&contents);
                    entries.push((
                        name.unwrap_or_else(|| entry.file_name().to_string_lossy().to_string()),
                        skill_path,
                    ));
                }
                SkillOrigin::LegacyCommandsDir => {
                    let path = entry.path();
                    let markdown_path = if path.is_dir() {
                        let skill_path = path.join("SKILL.md");
                        if !skill_path.is_file() {
                            continue;
                        }
                        skill_path
                    } else if path
                        .extension()
                        .is_some_and(|ext| ext.to_string_lossy().eq_ignore_ascii_case("md"))
                    {
                        path
                    } else {
                        continue;
                    };

                    let contents = fs::read_to_string(&markdown_path)?;
                    let fallback_name = markdown_path.file_stem().map_or_else(
                        || entry.file_name().to_string_lossy().to_string(),
                        |stem| stem.to_string_lossy().to_string(),
                    );
                    let (name, _) = parse_skill_frontmatter(&contents);
                    entries.push((name.unwrap_or(fallback_name), markdown_path));
                }
            }
        }
        entries.sort_by(|left, right| left.0.cmp(&right.0));
        if let Some((_, path)) = entries
            .into_iter()
            .find(|(name, _)| name.eq_ignore_ascii_case(requested))
        {
            return Ok(path);
        }
    }

    Err(std::io::Error::new(
        std::io::ErrorKind::NotFound,
        format!("unknown skill: {requested}"),
    ))
}

fn render_mcp_report_for(
    loader: &ConfigLoader,
    cwd: &Path,
    args: Option<&str>,
) -> Result<String, runtime::ConfigError> {
    if let Some(args) = normalize_optional_args(args) {
        if let Some(help_path) = help_path_from_args(args) {
            return Ok(match help_path.as_slice() {
                [] => render_mcp_usage(None),
                ["show", ..] => render_mcp_usage(Some("show")),
                _ => render_mcp_usage(Some(&help_path.join(" "))),
            });
        }
    }

    match normalize_optional_args(args) {
        None | Some("list") => {
            let runtime_config = loader.load()?;
            Ok(render_mcp_summary_report(
                cwd,
                runtime_config.mcp().servers(),
            ))
        }
        Some(args) if is_help_arg(args) => Ok(render_mcp_usage(None)),
        Some("show") => Ok(render_mcp_usage(Some("show"))),
        Some(args) if args.split_whitespace().next() == Some("show") => {
            let mut parts = args.split_whitespace();
            let _ = parts.next();
            let Some(server_name) = parts.next() else {
                return Ok(render_mcp_usage(Some("show")));
            };
            if parts.next().is_some() {
                return Ok(render_mcp_usage(Some(args)));
            }
            let runtime_config = loader.load()?;
            Ok(render_mcp_server_report(
                cwd,
                server_name,
                runtime_config.mcp().get(server_name),
            ))
        }
        Some(args) => Ok(render_mcp_usage(Some(args))),
    }
}

fn render_mcp_report_json_for(
    loader: &ConfigLoader,
    cwd: &Path,
    args: Option<&str>,
) -> Result<Value, runtime::ConfigError> {
    if let Some(args) = normalize_optional_args(args) {
        if let Some(help_path) = help_path_from_args(args) {
            return Ok(match help_path.as_slice() {
                [] => render_mcp_usage_json(None),
                ["show", ..] => render_mcp_usage_json(Some("show")),
                _ => render_mcp_usage_json(Some(&help_path.join(" "))),
            });
        }
    }

    match normalize_optional_args(args) {
        None | Some("list") => {
            let runtime_config = loader.load()?;
            Ok(render_mcp_summary_report_json(
                cwd,
                runtime_config.mcp().servers(),
            ))
        }
        Some(args) if is_help_arg(args) => Ok(render_mcp_usage_json(None)),
        Some("show") => Ok(render_mcp_usage_json(Some("show"))),
        Some(args) if args.split_whitespace().next() == Some("show") => {
            let mut parts = args.split_whitespace();
            let _ = parts.next();
            let Some(server_name) = parts.next() else {
                return Ok(render_mcp_usage_json(Some("show")));
            };
            if parts.next().is_some() {
                return Ok(render_mcp_usage_json(Some(args)));
            }
            let runtime_config = loader.load()?;
            Ok(render_mcp_server_report_json(
                cwd,
                server_name,
                runtime_config.mcp().get(server_name),
            ))
        }
        Some(args) => Ok(render_mcp_usage_json(Some(args))),
    }
}

#[must_use]
pub fn render_plugins_report(plugins: &[PluginSummary]) -> String {
    let mut lines = vec!["Plugins".to_string()];
    if plugins.is_empty() {
        lines.push("  No plugins installed.".to_string());
        return lines.join("\n");
    }
    for plugin in plugins {
        let enabled = if plugin.enabled {
            "enabled"
        } else {
            "disabled"
        };
        lines.push(format!(
            "  {name:<20} v{version:<10} {enabled}",
            name = plugin.metadata.name,
            version = plugin.metadata.version,
        ));
    }
    lines.join("\n")
}

fn render_plugin_install_report(plugin_id: &str, plugin: Option<&PluginSummary>) -> String {
    let name = plugin.map_or(plugin_id, |plugin| plugin.metadata.name.as_str());
    let version = plugin.map_or("unknown", |plugin| plugin.metadata.version.as_str());
    let enabled = plugin.is_some_and(|plugin| plugin.enabled);
    format!(
        "Plugins\n  Result           installed {plugin_id}\n  Name             {name}\n  Version          {version}\n  Status           {}",
        if enabled { "enabled" } else { "disabled" }
    )
}

fn resolve_plugin_target(
    manager: &PluginManager,
    target: &str,
) -> Result<PluginSummary, PluginError> {
    let mut matches = manager
        .list_installed_plugins()?
        .into_iter()
        .filter(|plugin| plugin.metadata.id == target || plugin.metadata.name == target)
        .collect::<Vec<_>>();
    match matches.len() {
        1 => Ok(matches.remove(0)),
        0 => Err(PluginError::NotFound(format!(
            "plugin `{target}` is not installed or discoverable"
        ))),
        _ => Err(PluginError::InvalidManifest(format!(
            "plugin name `{target}` is ambiguous; use the full plugin id"
        ))),
    }
}

fn discover_definition_roots(cwd: &Path, leaf: &str) -> Vec<(DefinitionSource, PathBuf)> {
    let mut roots = Vec::new();

    for ancestor in cwd.ancestors() {
        push_unique_root(
            &mut roots,
            DefinitionSource::ProjectForge,
            ancestor.join(".forge").join(leaf),
        );
        push_unique_root(
            &mut roots,
            DefinitionSource::ProjectCodex,
            ancestor.join(".codex").join(leaf),
        );
        push_unique_root(
            &mut roots,
            DefinitionSource::ProjectClaude,
            ancestor.join(".claude").join(leaf),
        );
    }

    if let Ok(forge_config_home) = env::var("FORGE_CONFIG_HOME") {
        push_unique_root(
            &mut roots,
            DefinitionSource::UserForgeConfigHome,
            PathBuf::from(forge_config_home).join(leaf),
        );
    }

    if let Ok(codex_home) = env::var("CODEX_HOME") {
        push_unique_root(
            &mut roots,
            DefinitionSource::UserCodexHome,
            PathBuf::from(codex_home).join(leaf),
        );
    }

    if let Ok(claude_config_dir) = env::var("CLAUDE_CONFIG_DIR") {
        push_unique_root(
            &mut roots,
            DefinitionSource::UserClaude,
            PathBuf::from(claude_config_dir).join(leaf),
        );
    }

    if let Some(home) = env::var_os("HOME") {
        let home = PathBuf::from(home);
        push_unique_root(
            &mut roots,
            DefinitionSource::UserForge,
            home.join(".forge").join(leaf),
        );
        push_unique_root(
            &mut roots,
            DefinitionSource::UserCodex,
            home.join(".codex").join(leaf),
        );
        push_unique_root(
            &mut roots,
            DefinitionSource::UserClaude,
            home.join(".claude").join(leaf),
        );
    }

    roots
}

#[allow(clippy::too_many_lines)]
fn discover_skill_roots(cwd: &Path) -> Vec<SkillRoot> {
    let mut roots = Vec::new();

    for ancestor in cwd.ancestors() {
        push_unique_skill_root(
            &mut roots,
            DefinitionSource::ProjectForge,
            ancestor.join(".forge").join("skills"),
            SkillOrigin::SkillsDir,
        );
        push_unique_skill_root(
            &mut roots,
            DefinitionSource::ProjectForge,
            ancestor.join(".omc").join("skills"),
            SkillOrigin::SkillsDir,
        );
        push_unique_skill_root(
            &mut roots,
            DefinitionSource::ProjectForge,
            ancestor.join(".agents").join("skills"),
            SkillOrigin::SkillsDir,
        );
        push_unique_skill_root(
            &mut roots,
            DefinitionSource::ProjectCodex,
            ancestor.join(".codex").join("skills"),
            SkillOrigin::SkillsDir,
        );
        push_unique_skill_root(
            &mut roots,
            DefinitionSource::ProjectClaude,
            ancestor.join(".claude").join("skills"),
            SkillOrigin::SkillsDir,
        );
        push_unique_skill_root(
            &mut roots,
            DefinitionSource::ProjectForge,
            ancestor.join(".forge").join("commands"),
            SkillOrigin::LegacyCommandsDir,
        );
        push_unique_skill_root(
            &mut roots,
            DefinitionSource::ProjectCodex,
            ancestor.join(".codex").join("commands"),
            SkillOrigin::LegacyCommandsDir,
        );
        push_unique_skill_root(
            &mut roots,
            DefinitionSource::ProjectClaude,
            ancestor.join(".claude").join("commands"),
            SkillOrigin::LegacyCommandsDir,
        );
    }

    if let Ok(forge_config_home) = env::var("FORGE_CONFIG_HOME") {
        let forge_config_home = PathBuf::from(forge_config_home);
        push_unique_skill_root(
            &mut roots,
            DefinitionSource::UserForgeConfigHome,
            forge_config_home.join("skills"),
            SkillOrigin::SkillsDir,
        );
        push_unique_skill_root(
            &mut roots,
            DefinitionSource::UserForgeConfigHome,
            forge_config_home.join("commands"),
            SkillOrigin::LegacyCommandsDir,
        );
    }

    if let Ok(codex_home) = env::var("CODEX_HOME") {
        let codex_home = PathBuf::from(codex_home);
        push_unique_skill_root(
            &mut roots,
            DefinitionSource::UserCodexHome,
            codex_home.join("skills"),
            SkillOrigin::SkillsDir,
        );
        push_unique_skill_root(
            &mut roots,
            DefinitionSource::UserCodexHome,
            codex_home.join("commands"),
            SkillOrigin::LegacyCommandsDir,
        );
    }

    if let Some(home) = env::var_os("HOME") {
        let home = PathBuf::from(home);
        push_unique_skill_root(
            &mut roots,
            DefinitionSource::UserForge,
            home.join(".forge").join("skills"),
            SkillOrigin::SkillsDir,
        );
        push_unique_skill_root(
            &mut roots,
            DefinitionSource::UserForge,
            home.join(".omc").join("skills"),
            SkillOrigin::SkillsDir,
        );
        push_unique_skill_root(
            &mut roots,
            DefinitionSource::UserForge,
            home.join(".forge").join("commands"),
            SkillOrigin::LegacyCommandsDir,
        );
        push_unique_skill_root(
            &mut roots,
            DefinitionSource::UserCodex,
            home.join(".codex").join("skills"),
            SkillOrigin::SkillsDir,
        );
        push_unique_skill_root(
            &mut roots,
            DefinitionSource::UserCodex,
            home.join(".codex").join("commands"),
            SkillOrigin::LegacyCommandsDir,
        );
        push_unique_skill_root(
            &mut roots,
            DefinitionSource::UserClaude,
            home.join(".claude").join("skills"),
            SkillOrigin::SkillsDir,
        );
        push_unique_skill_root(
            &mut roots,
            DefinitionSource::UserClaude,
            home.join(".claude").join("skills").join("omc-learned"),
            SkillOrigin::SkillsDir,
        );
        push_unique_skill_root(
            &mut roots,
            DefinitionSource::UserClaude,
            home.join(".claude").join("commands"),
            SkillOrigin::LegacyCommandsDir,
        );
    }

    if let Ok(claude_config_dir) = env::var("CLAUDE_CONFIG_DIR") {
        let claude_config_dir = PathBuf::from(claude_config_dir);
        let skills_dir = claude_config_dir.join("skills");
        push_unique_skill_root(
            &mut roots,
            DefinitionSource::UserClaude,
            skills_dir.clone(),
            SkillOrigin::SkillsDir,
        );
        push_unique_skill_root(
            &mut roots,
            DefinitionSource::UserClaude,
            skills_dir.join("omc-learned"),
            SkillOrigin::SkillsDir,
        );
        push_unique_skill_root(
            &mut roots,
            DefinitionSource::UserClaude,
            claude_config_dir.join("commands"),
            SkillOrigin::LegacyCommandsDir,
        );
    }

    roots
}

fn install_skill(source: &str, cwd: &Path) -> std::io::Result<InstalledSkill> {
    let registry_root = default_skill_install_root()?;
    install_skill_into(source, cwd, &registry_root)
}

fn install_skill_into(
    source: &str,
    cwd: &Path,
    registry_root: &Path,
) -> std::io::Result<InstalledSkill> {
    let source = resolve_skill_install_source(source, cwd)?;
    let prompt_path = source.prompt_path();
    let contents = fs::read_to_string(prompt_path)?;
    let display_name = parse_skill_frontmatter(&contents).0;
    let invocation_name = derive_skill_install_name(&source, display_name.as_deref())?;
    let installed_path = registry_root.join(&invocation_name);

    if installed_path.exists() {
        return Err(std::io::Error::new(
            std::io::ErrorKind::AlreadyExists,
            format!(
                "skill '{invocation_name}' is already installed at {}",
                installed_path.display()
            ),
        ));
    }

    fs::create_dir_all(&installed_path)?;
    let install_result = match &source {
        SkillInstallSource::Directory { root, .. } => {
            copy_directory_contents(root, &installed_path)
        }
        SkillInstallSource::MarkdownFile { path } => {
            fs::copy(path, installed_path.join("SKILL.md")).map(|_| ())
        }
    };
    if let Err(error) = install_result {
        let _ = fs::remove_dir_all(&installed_path);
        return Err(error);
    }

    Ok(InstalledSkill {
        invocation_name,
        display_name,
        source: source.report_path().to_path_buf(),
        registry_root: registry_root.to_path_buf(),
        installed_path,
    })
}

fn default_skill_install_root() -> std::io::Result<PathBuf> {
    if let Ok(forge_config_home) = env::var("FORGE_CONFIG_HOME") {
        return Ok(PathBuf::from(forge_config_home).join("skills"));
    }
    if let Ok(codex_home) = env::var("CODEX_HOME") {
        return Ok(PathBuf::from(codex_home).join("skills"));
    }
    if let Some(home) = env::var_os("HOME") {
        return Ok(PathBuf::from(home).join(".forge").join("skills"));
    }
    Err(std::io::Error::new(
        std::io::ErrorKind::NotFound,
        "unable to resolve a skills install root; set FORGE_CONFIG_HOME or HOME",
    ))
}

fn resolve_skill_install_source(source: &str, cwd: &Path) -> std::io::Result<SkillInstallSource> {
    let candidate = PathBuf::from(source);
    let source = if candidate.is_absolute() {
        candidate
    } else {
        cwd.join(candidate)
    };
    let source = fs::canonicalize(&source)?;

    if source.is_dir() {
        let prompt_path = source.join("SKILL.md");
        if !prompt_path.is_file() {
            return Err(std::io::Error::new(
                std::io::ErrorKind::InvalidInput,
                format!(
                    "skill directory '{}' must contain SKILL.md",
                    source.display()
                ),
            ));
        }
        return Ok(SkillInstallSource::Directory {
            root: source,
            prompt_path,
        });
    }

    if source
        .extension()
        .is_some_and(|ext| ext.to_string_lossy().eq_ignore_ascii_case("md"))
    {
        return Ok(SkillInstallSource::MarkdownFile { path: source });
    }

    Err(std::io::Error::new(
        std::io::ErrorKind::InvalidInput,
        format!(
            "skill source '{}' must be a directory with SKILL.md or a markdown file",
            source.display()
        ),
    ))
}

fn derive_skill_install_name(
    source: &SkillInstallSource,
    declared_name: Option<&str>,
) -> std::io::Result<String> {
    for candidate in [declared_name, source.fallback_name().as_deref()] {
        if let Some(candidate) = candidate.and_then(sanitize_skill_invocation_name) {
            return Ok(candidate);
        }
    }

    Err(std::io::Error::new(
        std::io::ErrorKind::InvalidInput,
        format!(
            "unable to derive an installable invocation name from '{}'",
            source.report_path().display()
        ),
    ))
}

fn sanitize_skill_invocation_name(candidate: &str) -> Option<String> {
    let trimmed = candidate
        .trim()
        .trim_start_matches('/')
        .trim_start_matches('$');
    if trimmed.is_empty() {
        return None;
    }

    let mut sanitized = String::new();
    let mut last_was_separator = false;
    for ch in trimmed.chars() {
        if ch.is_ascii_alphanumeric() || matches!(ch, '-' | '_' | '.') {
            sanitized.push(ch.to_ascii_lowercase());
            last_was_separator = false;
        } else if (ch.is_whitespace() || matches!(ch, '/' | '\\'))
            && !last_was_separator
            && !sanitized.is_empty()
        {
            sanitized.push('-');
            last_was_separator = true;
        }
    }

    let sanitized = sanitized
        .trim_matches(|ch| matches!(ch, '-' | '_' | '.'))
        .to_string();
    (!sanitized.is_empty()).then_some(sanitized)
}

fn copy_directory_contents(source: &Path, destination: &Path) -> std::io::Result<()> {
    for entry in fs::read_dir(source)? {
        let entry = entry?;
        let entry_type = entry.file_type()?;
        let destination_path = destination.join(entry.file_name());
        if entry_type.is_dir() {
            fs::create_dir_all(&destination_path)?;
            copy_directory_contents(&entry.path(), &destination_path)?;
        } else {
            fs::copy(entry.path(), destination_path)?;
        }
    }
    Ok(())
}

impl SkillInstallSource {
    fn prompt_path(&self) -> &Path {
        match self {
            Self::Directory { prompt_path, .. } => prompt_path,
            Self::MarkdownFile { path } => path,
        }
    }

    fn fallback_name(&self) -> Option<String> {
        match self {
            Self::Directory { root, .. } => root
                .file_name()
                .map(|name| name.to_string_lossy().to_string()),
            Self::MarkdownFile { path } => path
                .file_stem()
                .map(|name| name.to_string_lossy().to_string()),
        }
    }

    fn report_path(&self) -> &Path {
        match self {
            Self::Directory { root, .. } => root,
            Self::MarkdownFile { path } => path,
        }
    }
}

fn push_unique_root(
    roots: &mut Vec<(DefinitionSource, PathBuf)>,
    source: DefinitionSource,
    path: PathBuf,
) {
    if path.is_dir() && !roots.iter().any(|(_, existing)| existing == &path) {
        roots.push((source, path));
    }
}

fn push_unique_skill_root(
    roots: &mut Vec<SkillRoot>,
    source: DefinitionSource,
    path: PathBuf,
    origin: SkillOrigin,
) {
    if path.is_dir() && !roots.iter().any(|existing| existing.path == path) {
        roots.push(SkillRoot {
            source,
            path,
            origin,
        });
    }
}

fn load_agents_from_roots(
    roots: &[(DefinitionSource, PathBuf)],
) -> std::io::Result<Vec<AgentSummary>> {
    let mut agents = Vec::new();
    let mut active_sources = BTreeMap::<String, DefinitionSource>::new();

    for (source, root) in roots {
        let mut root_agents = Vec::new();
        for entry in fs::read_dir(root)? {
            let entry = entry?;
            if entry.path().extension().is_none_or(|ext| ext != "toml") {
                continue;
            }
            let contents = fs::read_to_string(entry.path())?;
            let fallback_name = entry.path().file_stem().map_or_else(
                || entry.file_name().to_string_lossy().to_string(),
                |stem| stem.to_string_lossy().to_string(),
            );
            root_agents.push(AgentSummary {
                name: parse_toml_string(&contents, "name").unwrap_or(fallback_name),
                description: parse_toml_string(&contents, "description"),
                model: parse_toml_string(&contents, "model"),
                reasoning_effort: parse_toml_string(&contents, "model_reasoning_effort"),
                source: *source,
                shadowed_by: None,
            });
        }
        root_agents.sort_by(|left, right| left.name.cmp(&right.name));

        for mut agent in root_agents {
            let key = agent.name.to_ascii_lowercase();
            if let Some(existing) = active_sources.get(&key) {
                agent.shadowed_by = Some(*existing);
            } else {
                active_sources.insert(key, agent.source);
            }
            agents.push(agent);
        }
    }

    Ok(agents)
}

fn load_skills_from_roots(roots: &[SkillRoot]) -> std::io::Result<Vec<SkillSummary>> {
    let mut skills = Vec::new();
    let mut active_sources = BTreeMap::<String, DefinitionSource>::new();

    for root in roots {
        let mut root_skills = Vec::new();
        for entry in fs::read_dir(&root.path)? {
            let entry = entry?;
            match root.origin {
                SkillOrigin::SkillsDir => {
                    if !entry.path().is_dir() {
                        continue;
                    }
                    let skill_path = entry.path().join("SKILL.md");
                    if !skill_path.is_file() {
                        continue;
                    }
                    let contents = fs::read_to_string(skill_path)?;
                    let (name, description) = parse_skill_frontmatter(&contents);
                    root_skills.push(SkillSummary {
                        name: name
                            .unwrap_or_else(|| entry.file_name().to_string_lossy().to_string()),
                        description,
                        source: root.source,
                        shadowed_by: None,
                        origin: root.origin,
                    });
                }
                SkillOrigin::LegacyCommandsDir => {
                    let path = entry.path();
                    let markdown_path = if path.is_dir() {
                        let skill_path = path.join("SKILL.md");
                        if !skill_path.is_file() {
                            continue;
                        }
                        skill_path
                    } else if path
                        .extension()
                        .is_some_and(|ext| ext.to_string_lossy().eq_ignore_ascii_case("md"))
                    {
                        path
                    } else {
                        continue;
                    };

                    let contents = fs::read_to_string(&markdown_path)?;
                    let fallback_name = markdown_path.file_stem().map_or_else(
                        || entry.file_name().to_string_lossy().to_string(),
                        |stem| stem.to_string_lossy().to_string(),
                    );
                    let (name, description) = parse_skill_frontmatter(&contents);
                    root_skills.push(SkillSummary {
                        name: name.unwrap_or(fallback_name),
                        description,
                        source: root.source,
                        shadowed_by: None,
                        origin: root.origin,
                    });
                }
            }
        }
        root_skills.sort_by(|left, right| left.name.cmp(&right.name));

        for mut skill in root_skills {
            let key = skill.name.to_ascii_lowercase();
            if let Some(existing) = active_sources.get(&key) {
                skill.shadowed_by = Some(*existing);
            } else {
                active_sources.insert(key, skill.source);
            }
            skills.push(skill);
        }
    }

    Ok(skills)
}

fn parse_toml_string(contents: &str, key: &str) -> Option<String> {
    let prefix = format!("{key} =");
    for line in contents.lines() {
        let trimmed = line.trim();
        if trimmed.starts_with('#') {
            continue;
        }
        let Some(value) = trimmed.strip_prefix(&prefix) else {
            continue;
        };
        let value = value.trim();
        let Some(value) = value
            .strip_prefix('"')
            .and_then(|value| value.strip_suffix('"'))
        else {
            continue;
        };
        if !value.is_empty() {
            return Some(value.to_string());
        }
    }
    None
}

fn parse_skill_frontmatter(contents: &str) -> (Option<String>, Option<String>) {
    let mut lines = contents.lines();
    if lines.next().map(str::trim) != Some("---") {
        return (None, None);
    }

    let mut name = None;
    let mut description = None;
    for line in lines {
        let trimmed = line.trim();
        if trimmed == "---" {
            break;
        }
        if let Some(value) = trimmed.strip_prefix("name:") {
            let value = unquote_frontmatter_value(value.trim());
            if !value.is_empty() {
                name = Some(value);
            }
            continue;
        }
        if let Some(value) = trimmed.strip_prefix("description:") {
            let value = unquote_frontmatter_value(value.trim());
            if !value.is_empty() {
                description = Some(value);
            }
        }
    }

    (name, description)
}

fn unquote_frontmatter_value(value: &str) -> String {
    value
        .strip_prefix('"')
        .and_then(|trimmed| trimmed.strip_suffix('"'))
        .or_else(|| {
            value
                .strip_prefix('\'')
                .and_then(|trimmed| trimmed.strip_suffix('\''))
        })
        .unwrap_or(value)
        .trim()
        .to_string()
}

fn render_agents_report(agents: &[AgentSummary]) -> String {
    if agents.is_empty() {
        return "No agents found.".to_string();
    }

    let total_active = agents
        .iter()
        .filter(|agent| agent.shadowed_by.is_none())
        .count();
    let mut lines = vec![
        "Agents".to_string(),
        format!("  {total_active} active agents"),
        String::new(),
    ];

    for scope in [
        DefinitionScope::Project,
        DefinitionScope::UserConfigHome,
        DefinitionScope::UserHome,
    ] {
        let group = agents
            .iter()
            .filter(|agent| agent.source.report_scope() == scope)
            .collect::<Vec<_>>();
        if group.is_empty() {
            continue;
        }

        lines.push(format!("{}:", scope.label()));
        for agent in group {
            let detail = agent_detail(agent);
            match agent.shadowed_by {
                Some(winner) => lines.push(format!("  (shadowed by {}) {detail}", winner.label())),
                None => lines.push(format!("  {detail}")),
            }
        }
        lines.push(String::new());
    }

    lines.join("\n").trim_end().to_string()
}

fn render_agents_report_json(cwd: &Path, agents: &[AgentSummary]) -> Value {
    let active = agents
        .iter()
        .filter(|agent| agent.shadowed_by.is_none())
        .count();
    json!({
        "kind": "agents",
        "action": "list",
        "working_directory": cwd.display().to_string(),
        "count": agents.len(),
        "summary": {
            "total": agents.len(),
            "active": active,
            "shadowed": agents.len().saturating_sub(active),
        },
        "agents": agents.iter().map(agent_summary_json).collect::<Vec<_>>(),
    })
}

fn agent_detail(agent: &AgentSummary) -> String {
    let mut parts = vec![agent.name.clone()];
    if let Some(description) = &agent.description {
        parts.push(description.clone());
    }
    if let Some(model) = &agent.model {
        parts.push(model.clone());
    }
    if let Some(reasoning) = &agent.reasoning_effort {
        parts.push(reasoning.clone());
    }
    parts.join(" · ")
}

fn render_skills_report(skills: &[SkillSummary]) -> String {
    if skills.is_empty() {
        return "No skills found.".to_string();
    }

    let total_active = skills
        .iter()
        .filter(|skill| skill.shadowed_by.is_none())
        .count();
    let mut lines = vec![
        "Skills".to_string(),
        format!("  {total_active} available skills"),
        String::new(),
    ];

    for scope in [
        DefinitionScope::Project,
        DefinitionScope::UserConfigHome,
        DefinitionScope::UserHome,
    ] {
        let group = skills
            .iter()
            .filter(|skill| skill.source.report_scope() == scope)
            .collect::<Vec<_>>();
        if group.is_empty() {
            continue;
        }

        lines.push(format!("{}:", scope.label()));
        for skill in group {
            let mut parts = vec![skill.name.clone()];
            if let Some(description) = &skill.description {
                parts.push(description.clone());
            }
            if let Some(detail) = skill.origin.detail_label() {
                parts.push(detail.to_string());
            }
            let detail = parts.join(" · ");
            match skill.shadowed_by {
                Some(winner) => lines.push(format!("  (shadowed by {}) {detail}", winner.label())),
                None => lines.push(format!("  {detail}")),
            }
        }
        lines.push(String::new());
    }

    lines.join("\n").trim_end().to_string()
}

fn render_skills_report_json(skills: &[SkillSummary]) -> Value {
    let active = skills
        .iter()
        .filter(|skill| skill.shadowed_by.is_none())
        .count();
    json!({
        "kind": "skills",
        "action": "list",
        "summary": {
            "total": skills.len(),
            "active": active,
            "shadowed": skills.len().saturating_sub(active),
        },
        "skills": skills.iter().map(skill_summary_json).collect::<Vec<_>>(),
    })
}

fn render_skill_install_report(skill: &InstalledSkill) -> String {
    let mut lines = vec![
        "Skills".to_string(),
        format!("  Result           installed {}", skill.invocation_name),
        format!("  Invoke as        ${}", skill.invocation_name),
    ];
    if let Some(display_name) = &skill.display_name {
        lines.push(format!("  Display name     {display_name}"));
    }
    lines.push(format!("  Source           {}", skill.source.display()));
    lines.push(format!(
        "  Registry         {}",
        skill.registry_root.display()
    ));
    lines.push(format!(
        "  Installed path   {}",
        skill.installed_path.display()
    ));
    lines.join("\n")
}

fn render_skill_install_report_json(skill: &InstalledSkill) -> Value {
    json!({
        "kind": "skills",
        "action": "install",
        "result": "installed",
        "invocation_name": &skill.invocation_name,
        "invoke_as": format!("${}", skill.invocation_name),
        "display_name": &skill.display_name,
        "source": skill.source.display().to_string(),
        "registry_root": skill.registry_root.display().to_string(),
        "installed_path": skill.installed_path.display().to_string(),
    })
}

fn render_mcp_summary_report(
    cwd: &Path,
    servers: &BTreeMap<String, ScopedMcpServerConfig>,
) -> String {
    let mut lines = vec![
        "MCP".to_string(),
        format!("  Working directory {}", cwd.display()),
        format!("  Configured servers {}", servers.len()),
    ];
    if servers.is_empty() {
        lines.push("  No MCP servers configured.".to_string());
        return lines.join("\n");
    }

    lines.push(String::new());
    for (name, server) in servers {
        lines.push(format!(
            "  {name:<16} {transport:<13} {scope:<7} {summary}",
            transport = mcp_transport_label(&server.config),
            scope = config_source_label(server.scope),
            summary = mcp_server_summary(&server.config)
        ));
    }

    lines.join("\n")
}

fn render_mcp_summary_report_json(
    cwd: &Path,
    servers: &BTreeMap<String, ScopedMcpServerConfig>,
) -> Value {
    json!({
        "kind": "mcp",
        "action": "list",
        "working_directory": cwd.display().to_string(),
        "configured_servers": servers.len(),
        "servers": servers
            .iter()
            .map(|(name, server)| mcp_server_json(name, server))
            .collect::<Vec<_>>(),
    })
}

fn render_mcp_server_report(
    cwd: &Path,
    server_name: &str,
    server: Option<&ScopedMcpServerConfig>,
) -> String {
    let Some(server) = server else {
        return format!(
            "MCP\n  Working directory {}\n  Result            server `{server_name}` is not configured",
            cwd.display()
        );
    };

    let mut lines = vec![
        "MCP".to_string(),
        format!("  Working directory {}", cwd.display()),
        format!("  Name              {server_name}"),
        format!("  Scope             {}", config_source_label(server.scope)),
        format!(
            "  Transport         {}",
            mcp_transport_label(&server.config)
        ),
    ];

    match &server.config {
        McpServerConfig::Stdio(config) => {
            lines.push(format!("  Command           {}", config.command));
            lines.push(format!(
                "  Args              {}",
                format_optional_list(&config.args)
            ));
            lines.push(format!(
                "  Env keys          {}",
                format_optional_keys(config.env.keys().cloned().collect())
            ));
            lines.push(format!(
                "  Tool timeout      {}",
                config
                    .tool_call_timeout_ms
                    .map_or_else(|| "<default>".to_string(), |value| format!("{value} ms"))
            ));
        }
        McpServerConfig::Sse(config) | McpServerConfig::Http(config) => {
            lines.push(format!("  URL               {}", config.url));
            lines.push(format!(
                "  Header keys       {}",
                format_optional_keys(config.headers.keys().cloned().collect())
            ));
            lines.push(format!(
                "  Header helper     {}",
                config.headers_helper.as_deref().unwrap_or("<none>")
            ));
            lines.push(format!(
                "  OAuth             {}",
                format_mcp_oauth(config.oauth.as_ref())
            ));
        }
        McpServerConfig::Ws(config) => {
            lines.push(format!("  URL               {}", config.url));
            lines.push(format!(
                "  Header keys       {}",
                format_optional_keys(config.headers.keys().cloned().collect())
            ));
            lines.push(format!(
                "  Header helper     {}",
                config.headers_helper.as_deref().unwrap_or("<none>")
            ));
        }
        McpServerConfig::Sdk(config) => {
            lines.push(format!("  SDK name          {}", config.name));
        }
        McpServerConfig::ManagedProxy(config) => {
            lines.push(format!("  URL               {}", config.url));
            lines.push(format!("  Proxy id          {}", config.id));
        }
    }

    lines.join("\n")
}

fn render_mcp_server_report_json(
    cwd: &Path,
    server_name: &str,
    server: Option<&ScopedMcpServerConfig>,
) -> Value {
    match server {
        Some(server) => json!({
            "kind": "mcp",
            "action": "show",
            "working_directory": cwd.display().to_string(),
            "found": true,
            "server": mcp_server_json(server_name, server),
        }),
        None => json!({
            "kind": "mcp",
            "action": "show",
            "working_directory": cwd.display().to_string(),
            "found": false,
            "server_name": server_name,
            "message": format!("server `{server_name}` is not configured"),
        }),
    }
}

fn normalize_optional_args(args: Option<&str>) -> Option<&str> {
    args.map(str::trim).filter(|value| !value.is_empty())
}

fn is_help_arg(arg: &str) -> bool {
    matches!(arg, "help" | "-h" | "--help")
}

fn help_path_from_args(args: &str) -> Option<Vec<&str>> {
    let parts = args.split_whitespace().collect::<Vec<_>>();
    let help_index = parts.iter().position(|part| is_help_arg(part))?;
    Some(parts[..help_index].to_vec())
}

fn render_agents_usage(unexpected: Option<&str>) -> String {
    let mut lines = vec![
        "Agents".to_string(),
        "  Usage            /agents [list|help]".to_string(),
        "  Direct CLI       forge agents".to_string(),
        "  Sources          .forge/agents, ~/.forge/agents, $FORGE_CONFIG_HOME/agents".to_string(),
    ];
    if let Some(args) = unexpected {
        lines.push(format!("  Unexpected       {args}"));
    }
    lines.join("\n")
}

fn render_agents_usage_json(unexpected: Option<&str>) -> Value {
    json!({
        "kind": "agents",
        "action": "help",
        "usage": {
            "slash_command": "/agents [list|help]",
            "direct_cli": "forge agents [list|help]",
            "sources": [".forge/agents", "~/.forge/agents", "$FORGE_CONFIG_HOME/agents"],
        },
        "unexpected": unexpected,
    })
}

fn render_skills_usage(unexpected: Option<&str>) -> String {
    let mut lines = vec![
        "Skills".to_string(),
        "  Usage            /skills [list|install <path>|help|<skill> [args]]".to_string(),
        "  Alias            /skill".to_string(),
        "  Direct CLI       forge skills [list|install <path>|help|<skill> [args]]".to_string(),
        "  Invoke           /skills help overview -> $help overview".to_string(),
        "  Install root     $FORGE_CONFIG_HOME/skills or ~/.forge/skills".to_string(),
        "  Sources          .forge/skills, .omc/skills, .agents/skills, .codex/skills, .claude/skills, ~/.forge/skills, ~/.omc/skills, ~/.claude/skills/omc-learned, ~/.codex/skills, ~/.claude/skills, legacy /commands".to_string(),
    ];
    if let Some(args) = unexpected {
        lines.push(format!("  Unexpected       {args}"));
    }
    lines.join("\n")
}

fn render_skills_usage_json(unexpected: Option<&str>) -> Value {
    json!({
        "kind": "skills",
        "action": "help",
        "usage": {
            "slash_command": "/skills [list|install <path>|help|<skill> [args]]",
            "aliases": ["/skill"],
            "direct_cli": "forge skills [list|install <path>|help|<skill> [args]]",
            "invoke": "/skills help overview -> $help overview",
            "install_root": "$FORGE_CONFIG_HOME/skills or ~/.forge/skills",
            "sources": [
                ".forge/skills",
                ".omc/skills",
                ".agents/skills",
                ".codex/skills",
                ".claude/skills",
                "~/.forge/skills",
                "~/.omc/skills",
                "~/.claude/skills/omc-learned",
                "~/.codex/skills",
                "~/.claude/skills",
                "legacy /commands",
                "legacy fallback dirs still load automatically"
            ],
        },
        "unexpected": unexpected,
    })
}

fn render_mcp_usage(unexpected: Option<&str>) -> String {
    let mut lines = vec![
        "MCP".to_string(),
        "  Usage            /mcp [list|show <server>|help]".to_string(),
        "  Direct CLI       forge mcp [list|show <server>|help]".to_string(),
        "  Sources          .forge/settings.json, .forge/settings.local.json".to_string(),
    ];
    if let Some(args) = unexpected {
        lines.push(format!("  Unexpected       {args}"));
    }
    lines.join("\n")
}

fn render_mcp_usage_json(unexpected: Option<&str>) -> Value {
    json!({
        "kind": "mcp",
        "action": "help",
        "usage": {
            "slash_command": "/mcp [list|show <server>|help]",
            "direct_cli": "forge mcp [list|show <server>|help]",
            "sources": [".forge/settings.json", ".forge/settings.local.json"],
        },
        "unexpected": unexpected,
    })
}

fn config_source_label(source: ConfigSource) -> &'static str {
    match source {
        ConfigSource::User => "user",
        ConfigSource::Project => "project",
        ConfigSource::Local => "local",
    }
}

fn mcp_transport_label(config: &McpServerConfig) -> &'static str {
    match config {
        McpServerConfig::Stdio(_) => "stdio",
        McpServerConfig::Sse(_) => "sse",
        McpServerConfig::Http(_) => "http",
        McpServerConfig::Ws(_) => "ws",
        McpServerConfig::Sdk(_) => "sdk",
        McpServerConfig::ManagedProxy(_) => "managed-proxy",
    }
}

fn mcp_server_summary(config: &McpServerConfig) -> String {
    match config {
        McpServerConfig::Stdio(config) => {
            if config.args.is_empty() {
                config.command.clone()
            } else {
                format!("{} {}", config.command, config.args.join(" "))
            }
        }
        McpServerConfig::Sse(config) | McpServerConfig::Http(config) => config.url.clone(),
        McpServerConfig::Ws(config) => config.url.clone(),
        McpServerConfig::Sdk(config) => config.name.clone(),
        McpServerConfig::ManagedProxy(config) => format!("{} ({})", config.id, config.url),
    }
}

fn format_optional_list(values: &[String]) -> String {
    if values.is_empty() {
        "<none>".to_string()
    } else {
        values.join(" ")
    }
}

fn format_optional_keys(mut keys: Vec<String>) -> String {
    if keys.is_empty() {
        return "<none>".to_string();
    }
    keys.sort();
    keys.join(", ")
}

fn format_mcp_oauth(oauth: Option<&McpOAuthConfig>) -> String {
    let Some(oauth) = oauth else {
        return "<none>".to_string();
    };

    let mut parts = Vec::new();
    if let Some(client_id) = &oauth.client_id {
        parts.push(format!("client_id={client_id}"));
    }
    if let Some(port) = oauth.callback_port {
        parts.push(format!("callback_port={port}"));
    }
    if let Some(url) = &oauth.auth_server_metadata_url {
        parts.push(format!("metadata_url={url}"));
    }
    if let Some(xaa) = oauth.xaa {
        parts.push(format!("xaa={xaa}"));
    }
    if parts.is_empty() {
        "enabled".to_string()
    } else {
        parts.join(", ")
    }
}

fn definition_source_id(source: DefinitionSource) -> &'static str {
    match source {
        DefinitionSource::ProjectForge
        | DefinitionSource::ProjectCodex
        | DefinitionSource::ProjectClaude => "project_forge",
        DefinitionSource::UserForgeConfigHome | DefinitionSource::UserCodexHome => {
            "user_forge_config_home"
        }
        DefinitionSource::UserForge | DefinitionSource::UserCodex | DefinitionSource::UserClaude => {
            "user_forge"
        }
    }
}

fn definition_source_json(source: DefinitionSource) -> Value {
    json!({
        "id": definition_source_id(source),
        "label": source.label(),
    })
}

fn agent_summary_json(agent: &AgentSummary) -> Value {
    json!({
        "name": &agent.name,
        "description": &agent.description,
        "model": &agent.model,
        "reasoning_effort": &agent.reasoning_effort,
        "source": definition_source_json(agent.source),
        "active": agent.shadowed_by.is_none(),
        "shadowed_by": agent.shadowed_by.map(definition_source_json),
    })
}

fn skill_origin_id(origin: SkillOrigin) -> &'static str {
    match origin {
        SkillOrigin::SkillsDir => "skills_dir",
        SkillOrigin::LegacyCommandsDir => "legacy_commands_dir",
    }
}

fn skill_origin_json(origin: SkillOrigin) -> Value {
    json!({
        "id": skill_origin_id(origin),
        "detail_label": origin.detail_label(),
    })
}

fn skill_summary_json(skill: &SkillSummary) -> Value {
    json!({
        "name": &skill.name,
        "description": &skill.description,
        "source": definition_source_json(skill.source),
        "origin": skill_origin_json(skill.origin),
        "active": skill.shadowed_by.is_none(),
        "shadowed_by": skill.shadowed_by.map(definition_source_json),
    })
}

fn config_source_id(source: ConfigSource) -> &'static str {
    match source {
        ConfigSource::User => "user",
        ConfigSource::Project => "project",
        ConfigSource::Local => "local",
    }
}

fn config_source_json(source: ConfigSource) -> Value {
    json!({
        "id": config_source_id(source),
        "label": config_source_label(source),
    })
}

fn mcp_transport_json(config: &McpServerConfig) -> Value {
    let label = mcp_transport_label(config);
    json!({
        "id": label,
        "label": label,
    })
}

fn mcp_oauth_json(oauth: Option<&McpOAuthConfig>) -> Value {
    let Some(oauth) = oauth else {
        return Value::Null;
    };
    json!({
        "client_id": &oauth.client_id,
        "callback_port": oauth.callback_port,
        "auth_server_metadata_url": &oauth.auth_server_metadata_url,
        "xaa": oauth.xaa,
    })
}

fn mcp_server_details_json(config: &McpServerConfig) -> Value {
    match config {
        McpServerConfig::Stdio(config) => json!({
            "command": &config.command,
            "args": &config.args,
            "env_keys": config.env.keys().cloned().collect::<Vec<_>>(),
            "tool_call_timeout_ms": config.tool_call_timeout_ms,
        }),
        McpServerConfig::Sse(config) | McpServerConfig::Http(config) => json!({
            "url": &config.url,
            "header_keys": config.headers.keys().cloned().collect::<Vec<_>>(),
            "headers_helper": &config.headers_helper,
            "oauth": mcp_oauth_json(config.oauth.as_ref()),
        }),
        McpServerConfig::Ws(config) => json!({
            "url": &config.url,
            "header_keys": config.headers.keys().cloned().collect::<Vec<_>>(),
            "headers_helper": &config.headers_helper,
        }),
        McpServerConfig::Sdk(config) => json!({
            "name": &config.name,
        }),
        McpServerConfig::ManagedProxy(config) => json!({
            "url": &config.url,
            "id": &config.id,
        }),
    }
}

fn mcp_server_json(name: &str, server: &ScopedMcpServerConfig) -> Value {
    json!({
        "name": name,
        "scope": config_source_json(server.scope),
        "transport": mcp_transport_json(&server.config),
        "summary": mcp_server_summary(&server.config),
        "details": mcp_server_details_json(&server.config),
    })
}

#[must_use]
pub fn handle_slash_command(
    input: &str,
    session: &Session,
    compaction: CompactionConfig,
) -> Option<SlashCommandResult> {
    let command = match SlashCommand::parse(input) {
        Ok(Some(command)) => command,
        Ok(None) => return None,
        Err(error) => {
            return Some(SlashCommandResult {
                message: error.to_string(),
                session: session.clone(),
            });
        }
    };

    match command {
        SlashCommand::Compact => {
            let result = compact_session(session, compaction);
            let message = if result.removed_message_count == 0 {
                "Compaction skipped: session is below the compaction threshold.".to_string()
            } else {
                format!(
                    "Compacted {} messages into a resumable system summary.",
                    result.removed_message_count
                )
            };
            Some(SlashCommandResult {
                message,
                session: result.compacted_session,
            })
        }
        SlashCommand::Help => Some(SlashCommandResult {
            message: render_slash_command_help(),
            session: session.clone(),
        }),
        SlashCommand::Status
        | SlashCommand::Bughunter { .. }
        | SlashCommand::Commit
        | SlashCommand::Pr { .. }
        | SlashCommand::Issue { .. }
        | SlashCommand::Ultraplan { .. }
        | SlashCommand::Teleport { .. }
        | SlashCommand::DebugToolCall
        | SlashCommand::Sandbox
        | SlashCommand::Model { .. }
        | SlashCommand::Permissions { .. }
        | SlashCommand::Clear { .. }
        | SlashCommand::Cost
        | SlashCommand::Resume { .. }
        | SlashCommand::Config { .. }
        | SlashCommand::Mcp { .. }
        | SlashCommand::Memory
        | SlashCommand::Init
        | SlashCommand::Diff
        | SlashCommand::Version
        | SlashCommand::Export { .. }
        | SlashCommand::Session { .. }
        | SlashCommand::Plugins { .. }
        | SlashCommand::Agents { .. }
        | SlashCommand::Skills { .. }
        | SlashCommand::Doctor
        | SlashCommand::Login
        | SlashCommand::Logout
        | SlashCommand::Vim
        | SlashCommand::Upgrade
        | SlashCommand::Stats
        | SlashCommand::Share
        | SlashCommand::Feedback
        | SlashCommand::Files
        | SlashCommand::Fast
        | SlashCommand::Exit
        | SlashCommand::Summary
        | SlashCommand::Desktop
        | SlashCommand::Brief
        | SlashCommand::Advisor
        | SlashCommand::Stickers
        | SlashCommand::Insights
        | SlashCommand::Thinkback
        | SlashCommand::ReleaseNotes
        | SlashCommand::SecurityReview
        | SlashCommand::Keybindings
        | SlashCommand::PrivacySettings
        | SlashCommand::Plan { .. }
        | SlashCommand::Review { .. }
        | SlashCommand::Tasks { .. }
        | SlashCommand::Theme { .. }
        | SlashCommand::Voice { .. }
        | SlashCommand::Usage { .. }
        | SlashCommand::Rename { .. }
        | SlashCommand::Copy { .. }
        | SlashCommand::Hooks { .. }
        | SlashCommand::Context { .. }
        | SlashCommand::Color { .. }
        | SlashCommand::Effort { .. }
        | SlashCommand::Branch { .. }
        | SlashCommand::Rewind { .. }
        | SlashCommand::Ide { .. }
        | SlashCommand::Tag { .. }
        | SlashCommand::OutputStyle { .. }
        | SlashCommand::AddDir { .. }
        | SlashCommand::Unknown(_) => None,
    }
}

#[cfg(test)]
mod tests {
    use super::{
        classify_skills_slash_command, handle_agents_slash_command_json,
        handle_plugins_slash_command, handle_skills_slash_command_json, handle_slash_command,
        load_agents_from_roots, load_skills_from_roots, render_agents_report,
        render_agents_report_json, render_mcp_report_json_for, render_plugins_report,
        render_skills_report, render_slash_command_help, render_slash_command_help_detail,
        resolve_skill_path, resume_supported_slash_commands, slash_command_specs,
        suggest_slash_commands, validate_slash_command_input, DefinitionSource, SkillOrigin,
        SkillRoot, SkillSlashDispatch, SlashCommand,
    };
    use plugins::{PluginKind, PluginManager, PluginManagerConfig, PluginMetadata, PluginSummary};
    use runtime::{
        CompactionConfig, ConfigLoader, ContentBlock, ConversationMessage, MessageRole, Session,
    };
    use std::ffi::OsString;
    use std::fs;
    use std::path::{Path, PathBuf};
    use std::sync::{Mutex, OnceLock};
    use std::time::{SystemTime, UNIX_EPOCH};

    fn temp_dir(label: &str) -> PathBuf {
        let nanos = SystemTime::now()
            .duration_since(UNIX_EPOCH)
            .expect("time should be after epoch")
            .as_nanos();
        std::env::temp_dir().join(format!("commands-plugin-{label}-{nanos}"))
    }

    fn env_lock() -> &'static Mutex<()> {
        static LOCK: OnceLock<Mutex<()>> = OnceLock::new();
        LOCK.get_or_init(|| Mutex::new(()))
    }

    fn restore_env_var(key: &str, original: Option<OsString>) {
        match original {
            Some(value) => std::env::set_var(key, value),
            None => std::env::remove_var(key),
        }
    }

    fn write_external_plugin(root: &Path, name: &str, version: &str) {
        fs::create_dir_all(root.join(".claude-plugin")).expect("manifest dir");
        fs::write(
            root.join(".claude-plugin").join("plugin.json"),
            format!(
                "{{\n  \"name\": \"{name}\",\n  \"version\": \"{version}\",\n  \"description\": \"commands plugin\"\n}}"
            ),
        )
        .expect("write manifest");
    }

    fn write_bundled_plugin(root: &Path, name: &str, version: &str, default_enabled: bool) {
        fs::create_dir_all(root.join(".claude-plugin")).expect("manifest dir");
        fs::write(
            root.join(".claude-plugin").join("plugin.json"),
            format!(
                "{{\n  \"name\": \"{name}\",\n  \"version\": \"{version}\",\n  \"description\": \"bundled commands plugin\",\n  \"defaultEnabled\": {}\n}}",
                if default_enabled { "true" } else { "false" }
            ),
        )
        .expect("write bundled manifest");
    }

    fn write_agent(root: &Path, name: &str, description: &str, model: &str, reasoning: &str) {
        fs::create_dir_all(root).expect("agent root");
        fs::write(
            root.join(format!("{name}.toml")),
            format!(
                "name = \"{name}\"\ndescription = \"{description}\"\nmodel = \"{model}\"\nmodel_reasoning_effort = \"{reasoning}\"\n"
            ),
        )
        .expect("write agent");
    }

    fn write_skill(root: &Path, name: &str, description: &str) {
        let skill_root = root.join(name);
        fs::create_dir_all(&skill_root).expect("skill root");
        fs::write(
            skill_root.join("SKILL.md"),
            format!("---\nname: {name}\ndescription: {description}\n---\n\n# {name}\n"),
        )
        .expect("write skill");
    }

    fn write_legacy_command(root: &Path, name: &str, description: &str) {
        fs::create_dir_all(root).expect("commands root");
        fs::write(
            root.join(format!("{name}.md")),
            format!("---\nname: {name}\ndescription: {description}\n---\n\n# {name}\n"),
        )
        .expect("write command");
    }

    fn parse_error_message(input: &str) -> String {
        SlashCommand::parse(input)
            .expect_err("slash command should be rejected")
            .to_string()
    }

    #[allow(clippy::too_many_lines)]
    #[test]
    fn parses_supported_slash_commands() {
        assert_eq!(SlashCommand::parse("/help"), Ok(Some(SlashCommand::Help)));
        assert_eq!(
            SlashCommand::parse(" /status "),
            Ok(Some(SlashCommand::Status))
        );
        assert_eq!(
            SlashCommand::parse("/sandbox"),
            Ok(Some(SlashCommand::Sandbox))
        );
        assert_eq!(
            SlashCommand::parse("/bughunter runtime"),
            Ok(Some(SlashCommand::Bughunter {
                scope: Some("runtime".to_string())
            }))
        );
        assert_eq!(
            SlashCommand::parse("/commit"),
            Ok(Some(SlashCommand::Commit))
        );
        assert_eq!(
            SlashCommand::parse("/pr ready for review"),
            Ok(Some(SlashCommand::Pr {
                context: Some("ready for review".to_string())
            }))
        );
        assert_eq!(
            SlashCommand::parse("/issue flaky test"),
            Ok(Some(SlashCommand::Issue {
                context: Some("flaky test".to_string())
            }))
        );
        assert_eq!(
            SlashCommand::parse("/ultraplan ship both features"),
            Ok(Some(SlashCommand::Ultraplan {
                task: Some("ship both features".to_string())
            }))
        );
        assert_eq!(
            SlashCommand::parse("/teleport conversation.rs"),
            Ok(Some(SlashCommand::Teleport {
                target: Some("conversation.rs".to_string())
            }))
        );
        assert_eq!(
            SlashCommand::parse("/debug-tool-call"),
            Ok(Some(SlashCommand::DebugToolCall))
        );
        assert_eq!(
            SlashCommand::parse("/bughunter runtime"),
            Ok(Some(SlashCommand::Bughunter {
                scope: Some("runtime".to_string())
            }))
        );
        assert_eq!(
            SlashCommand::parse("/commit"),
            Ok(Some(SlashCommand::Commit))
        );
        assert_eq!(
            SlashCommand::parse("/pr ready for review"),
            Ok(Some(SlashCommand::Pr {
                context: Some("ready for review".to_string())
            }))
        );
        assert_eq!(
            SlashCommand::parse("/issue flaky test"),
            Ok(Some(SlashCommand::Issue {
                context: Some("flaky test".to_string())
            }))
        );
        assert_eq!(
            SlashCommand::parse("/ultraplan ship both features"),
            Ok(Some(SlashCommand::Ultraplan {
                task: Some("ship both features".to_string())
            }))
        );
        assert_eq!(
            SlashCommand::parse("/teleport conversation.rs"),
            Ok(Some(SlashCommand::Teleport {
                target: Some("conversation.rs".to_string())
            }))
        );
        assert_eq!(
            SlashCommand::parse("/debug-tool-call"),
            Ok(Some(SlashCommand::DebugToolCall))
        );
        assert_eq!(
            SlashCommand::parse("/model claude-opus"),
            Ok(Some(SlashCommand::Model {
                model: Some("claude-opus".to_string()),
            }))
        );
        assert_eq!(
            SlashCommand::parse("/model"),
            Ok(Some(SlashCommand::Model { model: None }))
        );
        assert_eq!(
            SlashCommand::parse("/permissions read-only"),
            Ok(Some(SlashCommand::Permissions {
                mode: Some("read-only".to_string()),
            }))
        );
        assert_eq!(
            SlashCommand::parse("/clear"),
            Ok(Some(SlashCommand::Clear { confirm: false }))
        );
        assert_eq!(
            SlashCommand::parse("/clear --confirm"),
            Ok(Some(SlashCommand::Clear { confirm: true }))
        );
        assert_eq!(SlashCommand::parse("/cost"), Ok(Some(SlashCommand::Cost)));
        assert_eq!(
            SlashCommand::parse("/resume session.json"),
            Ok(Some(SlashCommand::Resume {
                session_path: Some("session.json".to_string()),
            }))
        );
        assert_eq!(
            SlashCommand::parse("/config"),
            Ok(Some(SlashCommand::Config { section: None }))
        );
        assert_eq!(
            SlashCommand::parse("/config env"),
            Ok(Some(SlashCommand::Config {
                section: Some("env".to_string())
            }))
        );
        assert_eq!(
            SlashCommand::parse("/mcp"),
            Ok(Some(SlashCommand::Mcp {
                action: None,
                target: None
            }))
        );
        assert_eq!(
            SlashCommand::parse("/mcp show remote"),
            Ok(Some(SlashCommand::Mcp {
                action: Some("show".to_string()),
                target: Some("remote".to_string())
            }))
        );
        assert_eq!(
            SlashCommand::parse("/memory"),
            Ok(Some(SlashCommand::Memory))
        );
        assert_eq!(SlashCommand::parse("/init"), Ok(Some(SlashCommand::Init)));
        assert_eq!(SlashCommand::parse("/diff"), Ok(Some(SlashCommand::Diff)));
        assert_eq!(
            SlashCommand::parse("/version"),
            Ok(Some(SlashCommand::Version))
        );
        assert_eq!(
            SlashCommand::parse("/export notes.txt"),
            Ok(Some(SlashCommand::Export {
                path: Some("notes.txt".to_string())
            }))
        );
        assert_eq!(
            SlashCommand::parse("/session switch abc123"),
            Ok(Some(SlashCommand::Session {
                action: Some("switch".to_string()),
                target: Some("abc123".to_string())
            }))
        );
        assert_eq!(
            SlashCommand::parse("/plugins install demo"),
            Ok(Some(SlashCommand::Plugins {
                action: Some("install".to_string()),
                target: Some("demo".to_string())
            }))
        );
        assert_eq!(
            SlashCommand::parse("/plugins list"),
            Ok(Some(SlashCommand::Plugins {
                action: Some("list".to_string()),
                target: None
            }))
        );
        assert_eq!(
            SlashCommand::parse("/plugins enable demo"),
            Ok(Some(SlashCommand::Plugins {
                action: Some("enable".to_string()),
                target: Some("demo".to_string())
            }))
        );
        assert_eq!(
            SlashCommand::parse("/skills install ./fixtures/help-skill"),
            Ok(Some(SlashCommand::Skills {
                args: Some("install ./fixtures/help-skill".to_string())
            }))
        );
        assert_eq!(
            SlashCommand::parse("/plugins disable demo"),
            Ok(Some(SlashCommand::Plugins {
                action: Some("disable".to_string()),
                target: Some("demo".to_string())
            }))
        );
        assert_eq!(
            SlashCommand::parse("/session fork incident-review"),
            Ok(Some(SlashCommand::Session {
                action: Some("fork".to_string()),
                target: Some("incident-review".to_string())
            }))
        );
    }

    #[test]
    fn rejects_unexpected_arguments_for_no_arg_commands() {
        // given
        let input = "/compact now";

        // when
        let error = parse_error_message(input);

        // then
        assert!(error.contains("Unexpected arguments for /compact."));
        assert!(error.contains("  Usage            /compact"));
        assert!(error.contains("  Summary          Compact local session history"));
    }

    #[test]
    fn rejects_invalid_argument_values() {
        // given
        let input = "/permissions admin";

        // when
        let error = parse_error_message(input);

        // then
        assert!(error.contains(
            "Unsupported /permissions mode 'admin'. Use read-only, workspace-write, or danger-full-access."
        ));
        assert!(error.contains(
            "  Usage            /permissions [read-only|workspace-write|danger-full-access]"
        ));
    }

    #[test]
    fn rejects_missing_required_arguments() {
        // given
        let input = "/teleport";

        // when
        let error = parse_error_message(input);

        // then
        assert!(error.contains("Usage: /teleport <symbol-or-path>"));
        assert!(error.contains("  Category         Discovery & debugging"));
    }

    #[test]
    fn rejects_invalid_session_and_plugin_shapes() {
        // given
        let session_input = "/session switch";
        let plugin_input = "/plugins list extra";

        // when
        let session_error = parse_error_message(session_input);
        let plugin_error = parse_error_message(plugin_input);

        // then
        assert!(session_error.contains("Usage: /session switch <session-id>"));
        assert!(session_error.contains("/session"));
        assert!(plugin_error.contains("Usage: /plugin list"));
        assert!(plugin_error.contains("Aliases          /plugins, /marketplace"));
    }

    #[test]
    fn rejects_invalid_agents_arguments() {
        // given
        let agents_input = "/agents show planner";

        // when
        let agents_error = parse_error_message(agents_input);

        // then
        assert!(agents_error.contains(
            "Unexpected arguments for /agents: show planner. Use /agents, /agents list, or /agents help."
        ));
        assert!(agents_error.contains("  Usage            /agents [list|help]"));
    }

    #[test]
    fn accepts_skills_invocation_arguments_for_prompt_dispatch() {
        assert_eq!(
            SlashCommand::parse("/skills help overview"),
            Ok(Some(SlashCommand::Skills {
                args: Some("help overview".to_string()),
            }))
        );
        assert_eq!(
            classify_skills_slash_command(Some("help overview")),
            SkillSlashDispatch::Invoke("$help overview".to_string())
        );
        assert_eq!(
            classify_skills_slash_command(Some("/test")),
            SkillSlashDispatch::Invoke("$test".to_string())
        );
        assert_eq!(
            classify_skills_slash_command(Some("install ./skill-pack")),
            SkillSlashDispatch::Local
        );
    }

    #[test]
    fn rejects_invalid_mcp_arguments() {
        let show_error = parse_error_message("/mcp show alpha beta");
        assert!(show_error.contains("Unexpected arguments for /mcp show."));
        assert!(show_error.contains("  Usage            /mcp show <server>"));

        let action_error = parse_error_message("/mcp inspect alpha");
        assert!(action_error
            .contains("Unknown /mcp action 'inspect'. Use list, show <server>, or help."));
        assert!(action_error.contains("  Usage            /mcp [list|show <server>|help]"));
    }

    #[test]
    fn renders_help_from_shared_specs() {
        let help = render_slash_command_help();
        assert!(help.contains("Start here        /status, /diff, /agents, /skills, /commit"));
        assert!(help.contains("[resume]          also works with --resume SESSION.jsonl"));
        assert!(help.contains("Session & visibility"));
        assert!(help.contains("Workspace & git"));
        assert!(help.contains("Discovery & debugging"));
        assert!(help.contains("Analysis & automation"));
        assert!(help.contains("/help"));
        assert!(help.contains("/status"));
        assert!(help.contains("/sandbox"));
        assert!(help.contains("/compact"));
        assert!(help.contains("/bughunter [scope]"));
        assert!(help.contains("/commit"));
        assert!(help.contains("/pr [context]"));
        assert!(help.contains("/issue [context]"));
        assert!(help.contains("/ultraplan [task]"));
        assert!(help.contains("/teleport <symbol-or-path>"));
        assert!(help.contains("/debug-tool-call"));
        assert!(help.contains("/model [model]"));
        assert!(help.contains("/permissions [read-only|workspace-write|danger-full-access]"));
        assert!(help.contains("/clear [--confirm]"));
        assert!(help.contains("/cost"));
        assert!(help.contains("/resume <session-path>"));
        assert!(help.contains("/config [env|hooks|model|plugins]"));
        assert!(help.contains("/mcp [list|show <server>|help]"));
        assert!(help.contains("/memory"));
        assert!(help.contains("/init"));
        assert!(help.contains("/diff"));
        assert!(help.contains("/version"));
        assert!(help.contains("/export [file]"));
        assert!(help.contains("/session [list|switch <session-id>|fork [branch-name]]"));
        assert!(help.contains("/sandbox"));
        assert!(help.contains(
            "/plugin [list|install <path>|enable <name>|disable <name>|uninstall <id>|update <id>]"
        ));
        assert!(help.contains("aliases: /plugins, /marketplace"));
        assert!(help.contains("/agents [list|help]"));
        assert!(help.contains("/skills [list|install <path>|help|<skill> [args]]"));
        assert!(help.contains("aliases: /skill"));
        assert_eq!(slash_command_specs().len(), 141);
        assert!(resume_supported_slash_commands().len() >= 39);
    }

    #[test]
    fn renders_per_command_help_detail() {
        // given
        let command = "plugins";

        // when
        let help = render_slash_command_help_detail(command).expect("detail help should exist");

        // then
        assert!(help.contains("/plugin"));
        assert!(help.contains("Summary          Manage DreamForge plugins"));
        assert!(help.contains("Aliases          /plugins, /marketplace"));
        assert!(help.contains("Category         Workspace & git"));
    }

    #[test]
    fn renders_per_command_help_detail_for_mcp() {
        let help = render_slash_command_help_detail("mcp").expect("detail help should exist");
        assert!(help.contains("/mcp"));
        assert!(help.contains("Summary          Inspect configured MCP servers"));
        assert!(help.contains("Category         Discovery & debugging"));
        assert!(help.contains("Resume           Supported with --resume SESSION.jsonl"));
    }

    #[test]
    fn validate_slash_command_input_rejects_extra_single_value_arguments() {
        // given
        let session_input = "/session switch current next";
        let plugin_input = "/plugin enable demo extra";

        // when
        let session_error = validate_slash_command_input(session_input)
            .expect_err("session input should be rejected")
            .to_string();
        let plugin_error = validate_slash_command_input(plugin_input)
            .expect_err("plugin input should be rejected")
            .to_string();

        // then
        assert!(session_error.contains("Unexpected arguments for /session switch."));
        assert!(session_error.contains("  Usage            /session switch <session-id>"));
        assert!(plugin_error.contains("Unexpected arguments for /plugin enable."));
        assert!(plugin_error.contains("  Usage            /plugin enable <name>"));
    }

    #[test]
    fn suggests_closest_slash_commands_for_typos_and_aliases() {
        let suggestions = suggest_slash_commands("stats", 3);
        assert!(suggestions.contains(&"/stats".to_string()));
        assert!(suggestions.contains(&"/status".to_string()));
        assert!(suggestions.len() <= 3);
        let plugin_suggestions = suggest_slash_commands("/plugns", 3);
        assert!(plugin_suggestions.contains(&"/plugin".to_string()));
        assert_eq!(suggest_slash_commands("zzz", 3), Vec::<String>::new());
    }

    #[test]
    fn compacts_sessions_via_slash_command() {
        let mut session = Session::new();
        session.messages = vec![
            ConversationMessage::user_text("a ".repeat(200)),
            ConversationMessage::assistant(vec![ContentBlock::Text {
                text: "b ".repeat(200),
            }]),
            ConversationMessage::tool_result("1", "bash", "ok ".repeat(200), false),
            ConversationMessage::assistant(vec![ContentBlock::Text {
                text: "recent".to_string(),
            }]),
        ];

        let result = handle_slash_command(
            "/compact",
            &session,
            CompactionConfig {
                preserve_recent_messages: 2,
                max_estimated_tokens: 1,
            },
        )
        .expect("slash command should be handled");

        assert!(result.message.contains("Compacted 2 messages"));
        assert_eq!(result.session.messages[0].role, MessageRole::System);
    }

    #[test]
    fn help_command_is_non_mutating() {
        let session = Session::new();
        let result = handle_slash_command("/help", &session, CompactionConfig::default())
            .expect("help command should be handled");
        assert_eq!(result.session, session);
        assert!(result.message.contains("Slash commands"));
    }

    #[test]
    fn ignores_unknown_or_runtime_bound_slash_commands() {
        let session = Session::new();
        assert!(handle_slash_command("/unknown", &session, CompactionConfig::default()).is_none());
        assert!(handle_slash_command("/status", &session, CompactionConfig::default()).is_none());
        assert!(handle_slash_command("/sandbox", &session, CompactionConfig::default()).is_none());
        assert!(
            handle_slash_command("/bughunter", &session, CompactionConfig::default()).is_none()
        );
        assert!(handle_slash_command("/commit", &session, CompactionConfig::default()).is_none());
        assert!(handle_slash_command("/pr", &session, CompactionConfig::default()).is_none());
        assert!(handle_slash_command("/issue", &session, CompactionConfig::default()).is_none());
        assert!(
            handle_slash_command("/ultraplan", &session, CompactionConfig::default()).is_none()
        );
        assert!(
            handle_slash_command("/teleport foo", &session, CompactionConfig::default()).is_none()
        );
        assert!(
            handle_slash_command("/debug-tool-call", &session, CompactionConfig::default())
                .is_none()
        );
        assert!(
            handle_slash_command("/model claude", &session, CompactionConfig::default()).is_none()
        );
        assert!(handle_slash_command(
            "/permissions read-only",
            &session,
            CompactionConfig::default()
        )
        .is_none());
        assert!(handle_slash_command("/clear", &session, CompactionConfig::default()).is_none());
        assert!(
            handle_slash_command("/clear --confirm", &session, CompactionConfig::default())
                .is_none()
        );
        assert!(handle_slash_command("/cost", &session, CompactionConfig::default()).is_none());
        assert!(handle_slash_command(
            "/resume session.json",
            &session,
            CompactionConfig::default()
        )
        .is_none());
        assert!(handle_slash_command(
            "/resume session.jsonl",
            &session,
            CompactionConfig::default()
        )
        .is_none());
        assert!(handle_slash_command("/config", &session, CompactionConfig::default()).is_none());
        assert!(
            handle_slash_command("/config env", &session, CompactionConfig::default()).is_none()
        );
        assert!(handle_slash_command("/mcp list", &session, CompactionConfig::default()).is_none());
        assert!(handle_slash_command("/diff", &session, CompactionConfig::default()).is_none());
        assert!(handle_slash_command("/version", &session, CompactionConfig::default()).is_none());
        assert!(
            handle_slash_command("/export note.txt", &session, CompactionConfig::default())
                .is_none()
        );
        assert!(
            handle_slash_command("/session list", &session, CompactionConfig::default()).is_none()
        );
        assert!(
            handle_slash_command("/plugins list", &session, CompactionConfig::default()).is_none()
        );
    }

    #[test]
    fn renders_plugins_report_with_name_version_and_status() {
        let rendered = render_plugins_report(&[
            PluginSummary {
                metadata: PluginMetadata {
                    id: "demo@external".to_string(),
                    name: "demo".to_string(),
                    version: "1.2.3".to_string(),
                    description: "demo plugin".to_string(),
                    kind: PluginKind::External,
                    source: "demo".to_string(),
                    default_enabled: false,
                    root: None,
                },
                enabled: true,
            },
            PluginSummary {
                metadata: PluginMetadata {
                    id: "sample@external".to_string(),
                    name: "sample".to_string(),
                    version: "0.9.0".to_string(),
                    description: "sample plugin".to_string(),
                    kind: PluginKind::External,
                    source: "sample".to_string(),
                    default_enabled: false,
                    root: None,
                },
                enabled: false,
            },
        ]);

        assert!(rendered.contains("demo"));
        assert!(rendered.contains("v1.2.3"));
        assert!(rendered.contains("enabled"));
        assert!(rendered.contains("sample"));
        assert!(rendered.contains("v0.9.0"));
        assert!(rendered.contains("disabled"));
    }

    #[test]
    fn lists_agents_from_project_and_user_roots() {
        let workspace = temp_dir("agents-workspace");
        let project_agents = workspace.join(".codex").join("agents");
        let user_home = temp_dir("agents-home");
        let user_agents = user_home.join(".claude").join("agents");

        write_agent(
            &project_agents,
            "planner",
            "Project planner",
            "gpt-5.4",
            "medium",
        );
        write_agent(
            &user_agents,
            "planner",
            "User planner",
            "gpt-5.4-mini",
            "high",
        );
        write_agent(
            &user_agents,
            "verifier",
            "Verification agent",
            "gpt-5.4-mini",
            "high",
        );

        let roots = vec![
            (DefinitionSource::ProjectCodex, project_agents),
            (DefinitionSource::UserCodex, user_agents),
        ];
        let report =
            render_agents_report(&load_agents_from_roots(&roots).expect("agent roots should load"));

        assert!(report.contains("Agents"));
        assert!(report.contains("2 active agents"));
        assert!(report.contains("Project roots:"));
        assert!(report.contains("planner · Project planner · gpt-5.4 · medium"));
        assert!(report.contains("User home roots:"));
        assert!(report.contains("(shadowed by Project roots) planner · User planner"));
        assert!(report.contains("verifier · Verification agent · gpt-5.4-mini · high"));

        let _ = fs::remove_dir_all(workspace);
        let _ = fs::remove_dir_all(user_home);
    }

    #[test]
    fn renders_agents_reports_as_json() {
        let workspace = temp_dir("agents-json-workspace");
        let project_agents = workspace.join(".codex").join("agents");
        let user_home = temp_dir("agents-json-home");
        let user_agents = user_home.join(".codex").join("agents");

        write_agent(
            &project_agents,
            "planner",
            "Project planner",
            "gpt-5.4",
            "medium",
        );
        write_agent(
            &project_agents,
            "verifier",
            "Verification agent",
            "gpt-5.4-mini",
            "high",
        );
        write_agent(
            &user_agents,
            "planner",
            "User planner",
            "gpt-5.4-mini",
            "high",
        );

        let roots = vec![
            (DefinitionSource::ProjectCodex, project_agents),
            (DefinitionSource::UserCodex, user_agents),
        ];
        let report = render_agents_report_json(
            &workspace,
            &load_agents_from_roots(&roots).expect("agent roots should load"),
        );

        assert_eq!(report["kind"], "agents");
        assert_eq!(report["action"], "list");
        assert_eq!(report["working_directory"], workspace.display().to_string());
        assert_eq!(report["count"], 3);
        assert_eq!(report["summary"]["active"], 2);
        assert_eq!(report["summary"]["shadowed"], 1);
        assert_eq!(report["agents"][0]["name"], "planner");
        assert_eq!(report["agents"][0]["model"], "gpt-5.4");
        assert_eq!(report["agents"][0]["active"], true);
        assert_eq!(report["agents"][1]["name"], "verifier");
        assert_eq!(report["agents"][2]["name"], "planner");
        assert_eq!(report["agents"][2]["active"], false);
        assert_eq!(report["agents"][2]["shadowed_by"]["id"], "project_forge");

        let help = handle_agents_slash_command_json(Some("help"), &workspace).expect("agents help");
        assert_eq!(help["kind"], "agents");
        assert_eq!(help["action"], "help");
        assert_eq!(help["usage"]["direct_cli"], "forge agents [list|help]");

        let unexpected = handle_agents_slash_command_json(Some("show planner"), &workspace)
            .expect("agents usage");
        assert_eq!(unexpected["action"], "help");
        assert_eq!(unexpected["unexpected"], "show planner");

        let _ = fs::remove_dir_all(workspace);
        let _ = fs::remove_dir_all(user_home);
    }

    #[test]
    fn lists_skills_from_project_and_user_roots() {
        let workspace = temp_dir("skills-workspace");
        let project_skills = workspace.join(".codex").join("skills");
        let project_commands = workspace.join(".claude").join("commands");
        let user_home = temp_dir("skills-home");
        let user_skills = user_home.join(".codex").join("skills");

        write_skill(&project_skills, "plan", "Project planning guidance");
        write_legacy_command(&project_commands, "deploy", "Legacy deployment guidance");
        write_skill(&user_skills, "plan", "User planning guidance");
        write_skill(&user_skills, "help", "Help guidance");

        let roots = vec![
            SkillRoot {
                source: DefinitionSource::ProjectCodex,
                path: project_skills,
                origin: SkillOrigin::SkillsDir,
            },
            SkillRoot {
                source: DefinitionSource::ProjectClaude,
                path: project_commands,
                origin: SkillOrigin::LegacyCommandsDir,
            },
            SkillRoot {
                source: DefinitionSource::UserCodex,
                path: user_skills,
                origin: SkillOrigin::SkillsDir,
            },
        ];
        let report =
            render_skills_report(&load_skills_from_roots(&roots).expect("skill roots should load"));

        assert!(report.contains("Skills"));
        assert!(report.contains("3 available skills"));
        assert!(report.contains("Project roots:"));
        assert!(report.contains("plan · Project planning guidance"));
        assert!(report.contains("deploy · Legacy deployment guidance · legacy /commands"));
        assert!(report.contains("User home roots:"));
        assert!(report.contains("(shadowed by Project roots) plan · User planning guidance"));
        assert!(report.contains("help · Help guidance"));

        let _ = fs::remove_dir_all(workspace);
        let _ = fs::remove_dir_all(user_home);
    }

    #[test]
    fn resolves_project_skills_and_legacy_commands_from_shared_registry() {
        let workspace = temp_dir("resolve-project-skills");
        let project_skills = workspace.join(".forge").join("skills");
        let legacy_commands = workspace.join(".forge").join("commands");

        write_skill(&project_skills, "plan", "Project planning guidance");
        write_legacy_command(&legacy_commands, "handoff", "Legacy handoff guidance");

        assert_eq!(
            resolve_skill_path(&workspace, "$plan").expect("project skill should resolve"),
            project_skills.join("plan").join("SKILL.md")
        );
        assert_eq!(
            resolve_skill_path(&workspace, "/handoff").expect("legacy command should resolve"),
            legacy_commands.join("handoff.md")
        );
    }

    #[test]
    fn renders_skills_reports_as_json() {
        let workspace = temp_dir("skills-json-workspace");
        let project_skills = workspace.join(".codex").join("skills");
        let project_commands = workspace.join(".claude").join("commands");
        let user_home = temp_dir("skills-json-home");
        let user_skills = user_home.join(".codex").join("skills");

        write_skill(&project_skills, "plan", "Project planning guidance");
        write_legacy_command(&project_commands, "deploy", "Legacy deployment guidance");
        write_skill(&user_skills, "plan", "User planning guidance");
        write_skill(&user_skills, "help", "Help guidance");

        let roots = vec![
            SkillRoot {
                source: DefinitionSource::ProjectCodex,
                path: project_skills,
                origin: SkillOrigin::SkillsDir,
            },
            SkillRoot {
                source: DefinitionSource::ProjectClaude,
                path: project_commands,
                origin: SkillOrigin::LegacyCommandsDir,
            },
            SkillRoot {
                source: DefinitionSource::UserCodex,
                path: user_skills,
                origin: SkillOrigin::SkillsDir,
            },
        ];
        let report = super::render_skills_report_json(
            &load_skills_from_roots(&roots).expect("skills should load"),
        );
        assert_eq!(report["kind"], "skills");
        assert_eq!(report["action"], "list");
        assert_eq!(report["summary"]["active"], 3);
        assert_eq!(report["summary"]["shadowed"], 1);
        assert_eq!(report["skills"][0]["name"], "plan");
        assert_eq!(report["skills"][0]["source"]["id"], "project_forge");
        assert_eq!(report["skills"][1]["name"], "deploy");
        assert_eq!(report["skills"][1]["origin"]["id"], "legacy_commands_dir");
        assert_eq!(report["skills"][3]["shadowed_by"]["id"], "project_forge");

        let help = handle_skills_slash_command_json(Some("help"), &workspace).expect("skills help");
        assert_eq!(help["kind"], "skills");
        assert_eq!(help["action"], "help");
        assert_eq!(help["usage"]["aliases"][0], "/skill");
        assert_eq!(
            help["usage"]["direct_cli"],
            "forge skills [list|install <path>|help|<skill> [args]]"
        );

        let _ = fs::remove_dir_all(workspace);
        let _ = fs::remove_dir_all(user_home);
    }

    #[test]
    fn agents_and_skills_usage_support_help_and_unexpected_args() {
        let cwd = temp_dir("slash-usage");

        let agents_help =
            super::handle_agents_slash_command(Some("help"), &cwd).expect("agents help");
        assert!(agents_help.contains("Usage            /agents [list|help]"));
        assert!(agents_help.contains("Direct CLI       forge agents"));
        assert!(agents_help
            .contains("Sources          .forge/agents, ~/.forge/agents, $FORGE_CONFIG_HOME/agents"));

        let agents_unexpected =
            super::handle_agents_slash_command(Some("show planner"), &cwd).expect("agents usage");
        assert!(agents_unexpected.contains("Unexpected       show planner"));

        let skills_help =
            super::handle_skills_slash_command(Some("--help"), &cwd).expect("skills help");
        assert!(skills_help
            .contains("Usage            /skills [list|install <path>|help|<skill> [args]]"));
        assert!(skills_help.contains("Alias            /skill"));
        assert!(skills_help.contains("Invoke           /skills help overview -> $help overview"));
        assert!(skills_help.contains("Install root     $FORGE_CONFIG_HOME/skills or ~/.forge/skills"));
        assert!(skills_help.contains(".omc/skills"));
        assert!(skills_help.contains(".agents/skills"));
        assert!(skills_help.contains("~/.claude/skills/omc-learned"));
        assert!(skills_help.contains("legacy /commands"));

        let skills_unexpected =
            super::handle_skills_slash_command(Some("show help"), &cwd).expect("skills usage");
        assert!(skills_unexpected.contains("Unexpected       show"));

        let skills_install_help = super::handle_skills_slash_command(Some("install --help"), &cwd)
            .expect("nested skills help");
        assert!(skills_install_help
            .contains("Usage            /skills [list|install <path>|help|<skill> [args]]"));
        assert!(skills_install_help.contains("Alias            /skill"));
        assert!(skills_install_help.contains("Unexpected       install"));

        let skills_unknown_help =
            super::handle_skills_slash_command(Some("show --help"), &cwd).expect("skills help");
        assert!(skills_unknown_help
            .contains("Usage            /skills [list|install <path>|help|<skill> [args]]"));
        assert!(skills_unknown_help.contains("Unexpected       show"));

        let skills_help_json =
            super::handle_skills_slash_command_json(Some("help"), &cwd).expect("skills help json");
        let sources = skills_help_json["usage"]["sources"]
            .as_array()
            .expect("skills help sources");
        assert_eq!(skills_help_json["usage"]["aliases"][0], "/skill");
        assert!(sources.iter().any(|value| value == ".omc/skills"));
        assert!(sources.iter().any(|value| value == ".agents/skills"));
        assert!(sources.iter().any(|value| value == "~/.omc/skills"));
        assert!(sources
            .iter()
            .any(|value| value == "~/.claude/skills/omc-learned"));

        let _ = fs::remove_dir_all(cwd);
    }

    #[test]
    fn discovers_omc_skills_from_project_and_user_compatibility_roots() {
        let _guard = env_lock().lock().expect("env lock");
        let workspace = temp_dir("skills-omc-workspace");
        let user_home = temp_dir("skills-omc-home");
        let claude_config_dir = temp_dir("skills-omc-claude-config");
        let project_omc_skills = workspace.join(".omc").join("skills");
        let project_agents_skills = workspace.join(".agents").join("skills");
        let user_omc_skills = user_home.join(".omc").join("skills");
        let claude_config_skills = claude_config_dir.join("skills");
        let claude_config_commands = claude_config_dir.join("commands");
        let learned_skills = claude_config_dir.join("skills").join("omc-learned");
        let original_home = std::env::var_os("HOME");
        let original_claude_config_dir = std::env::var_os("CLAUDE_CONFIG_DIR");

        write_skill(&project_omc_skills, "hud", "OMC HUD guidance");
        write_skill(
            &project_agents_skills,
            "trace",
            "Compatibility skill guidance",
        );
        write_skill(&user_omc_skills, "cancel", "OMC cancel guidance");
        write_skill(
            &claude_config_skills,
            "statusline",
            "Claude config skill guidance",
        );
        write_legacy_command(
            &claude_config_commands,
            "doctor-check",
            "Claude config command guidance",
        );
        write_skill(&learned_skills, "learned", "Learned skill guidance");
        std::env::set_var("HOME", &user_home);
        std::env::set_var("CLAUDE_CONFIG_DIR", &claude_config_dir);

        let report = super::handle_skills_slash_command(None, &workspace).expect("skills list");
        assert!(report.contains("available skills"));
        assert!(report.contains("hud · OMC HUD guidance"));
        assert!(report.contains("trace · Compatibility skill guidance"));
        assert!(report.contains("cancel · OMC cancel guidance"));
        assert!(report.contains("statusline · Claude config skill guidance"));
        assert!(report.contains("doctor-check · Claude config command guidance · legacy /commands"));
        assert!(report.contains("learned · Learned skill guidance"));

        let help =
            super::handle_skills_slash_command_json(Some("help"), &workspace).expect("skills help");
        let sources = help["usage"]["sources"]
            .as_array()
            .expect("skills help sources");
        assert_eq!(help["usage"]["aliases"][0], "/skill");
        assert!(sources.iter().any(|value| value == ".omc/skills"));
        assert!(sources.iter().any(|value| value == ".agents/skills"));
        assert!(sources.iter().any(|value| value == "~/.omc/skills"));
        assert!(sources
            .iter()
            .any(|value| value == "~/.claude/skills/omc-learned"));

        restore_env_var("HOME", original_home);
        restore_env_var("CLAUDE_CONFIG_DIR", original_claude_config_dir);
        let _ = fs::remove_dir_all(workspace);
        let _ = fs::remove_dir_all(user_home);
        let _ = fs::remove_dir_all(claude_config_dir);
    }

    #[test]
    fn mcp_usage_supports_help_and_unexpected_args() {
        let cwd = temp_dir("mcp-usage");

        let help = super::handle_mcp_slash_command(Some("help"), &cwd).expect("mcp help");
        assert!(help.contains("Usage            /mcp [list|show <server>|help]"));
        assert!(help.contains("Direct CLI       forge mcp [list|show <server>|help]"));

        let unexpected =
            super::handle_mcp_slash_command(Some("show alpha beta"), &cwd).expect("mcp usage");
        assert!(unexpected.contains("Unexpected       show alpha beta"));

        let nested_help =
            super::handle_mcp_slash_command(Some("show --help"), &cwd).expect("mcp help");
        assert!(nested_help.contains("Usage            /mcp [list|show <server>|help]"));
        assert!(nested_help.contains("Unexpected       show"));

        let unknown_help =
            super::handle_mcp_slash_command(Some("inspect --help"), &cwd).expect("mcp usage");
        assert!(unknown_help.contains("Usage            /mcp [list|show <server>|help]"));
        assert!(unknown_help.contains("Unexpected       inspect"));

        let _ = fs::remove_dir_all(cwd);
    }

    #[test]
    fn renders_mcp_reports_from_loaded_config() {
        let workspace = temp_dir("mcp-config-workspace");
        let config_home = temp_dir("mcp-config-home");
        fs::create_dir_all(workspace.join(".forge")).expect("workspace config dir");
        fs::create_dir_all(&config_home).expect("config home");
        fs::write(
            workspace.join(".forge").join("settings.json"),
            r#"{
              "mcpServers": {
                "alpha": {
                  "command": "uvx",
                  "args": ["alpha-server"],
                  "env": {"ALPHA_TOKEN": "secret"},
                  "toolCallTimeoutMs": 1200
                },
                "remote": {
                  "type": "http",
                  "url": "https://remote.example/mcp",
                  "headers": {"Authorization": "Bearer secret"},
                  "headersHelper": "./bin/headers",
                  "oauth": {
                    "clientId": "remote-client",
                    "callbackPort": 7878
                  }
                }
              }
            }"#,
        )
        .expect("write settings");
        fs::write(
            workspace.join(".forge").join("settings.local.json"),
            r#"{
              "mcpServers": {
                "remote": {
                  "type": "ws",
                  "url": "wss://remote.example/mcp"
                }
              }
            }"#,
        )
        .expect("write local settings");

        let loader = ConfigLoader::new(&workspace, &config_home);
        let list = super::render_mcp_report_for(&loader, &workspace, None)
            .expect("mcp list report should render");
        assert!(list.contains("Configured servers 2"));
        assert!(list.contains("alpha"));
        assert!(list.contains("stdio"));
        assert!(list.contains("project"));
        assert!(list.contains("uvx alpha-server"));
        assert!(list.contains("remote"));
        assert!(list.contains("ws"));
        assert!(list.contains("local"));
        assert!(list.contains("wss://remote.example/mcp"));

        let show = super::render_mcp_report_for(&loader, &workspace, Some("show alpha"))
            .expect("mcp show report should render");
        assert!(show.contains("Name              alpha"));
        assert!(show.contains("Command           uvx"));
        assert!(show.contains("Args              alpha-server"));
        assert!(show.contains("Env keys          ALPHA_TOKEN"));
        assert!(show.contains("Tool timeout      1200 ms"));

        let remote = super::render_mcp_report_for(&loader, &workspace, Some("show remote"))
            .expect("mcp show remote report should render");
        assert!(remote.contains("Transport         ws"));
        assert!(remote.contains("URL               wss://remote.example/mcp"));

        let missing = super::render_mcp_report_for(&loader, &workspace, Some("show missing"))
            .expect("missing report should render");
        assert!(missing.contains("server `missing` is not configured"));

        let _ = fs::remove_dir_all(workspace);
        let _ = fs::remove_dir_all(config_home);
    }

    #[test]
    fn renders_mcp_reports_as_json() {
        let workspace = temp_dir("mcp-json-workspace");
        let config_home = temp_dir("mcp-json-home");
        fs::create_dir_all(workspace.join(".forge")).expect("workspace config dir");
        fs::create_dir_all(&config_home).expect("config home");
        fs::write(
            workspace.join(".forge").join("settings.json"),
            r#"{
              "mcpServers": {
                "alpha": {
                  "command": "uvx",
                  "args": ["alpha-server"],
                  "env": {"ALPHA_TOKEN": "secret"},
                  "toolCallTimeoutMs": 1200
                },
                "remote": {
                  "type": "http",
                  "url": "https://remote.example/mcp",
                  "headers": {"Authorization": "Bearer secret"},
                  "headersHelper": "./bin/headers",
                  "oauth": {
                    "clientId": "remote-client",
                    "callbackPort": 7878
                  }
                }
              }
            }"#,
        )
        .expect("write settings");
        fs::write(
            workspace.join(".forge").join("settings.local.json"),
            r#"{
              "mcpServers": {
                "remote": {
                  "type": "ws",
                  "url": "wss://remote.example/mcp"
                }
              }
            }"#,
        )
        .expect("write local settings");

        let loader = ConfigLoader::new(&workspace, &config_home);
        let list =
            render_mcp_report_json_for(&loader, &workspace, None).expect("mcp list json render");
        assert_eq!(list["kind"], "mcp");
        assert_eq!(list["action"], "list");
        assert_eq!(list["configured_servers"], 2);
        assert_eq!(list["servers"][0]["name"], "alpha");
        assert_eq!(list["servers"][0]["transport"]["id"], "stdio");
        assert_eq!(list["servers"][0]["details"]["command"], "uvx");
        assert_eq!(list["servers"][1]["name"], "remote");
        assert_eq!(list["servers"][1]["scope"]["id"], "local");
        assert_eq!(list["servers"][1]["transport"]["id"], "ws");
        assert_eq!(
            list["servers"][1]["details"]["url"],
            "wss://remote.example/mcp"
        );

        let show = render_mcp_report_json_for(&loader, &workspace, Some("show alpha"))
            .expect("mcp show json render");
        assert_eq!(show["action"], "show");
        assert_eq!(show["found"], true);
        assert_eq!(show["server"]["name"], "alpha");
        assert_eq!(show["server"]["details"]["env_keys"][0], "ALPHA_TOKEN");
        assert_eq!(show["server"]["details"]["tool_call_timeout_ms"], 1200);

        let missing = render_mcp_report_json_for(&loader, &workspace, Some("show missing"))
            .expect("mcp missing json render");
        assert_eq!(missing["found"], false);
        assert_eq!(missing["server_name"], "missing");

        let help =
            render_mcp_report_json_for(&loader, &workspace, Some("help")).expect("mcp help json");
        assert_eq!(help["action"], "help");
        assert_eq!(help["usage"]["sources"][0], ".forge/settings.json");

        let _ = fs::remove_dir_all(workspace);
        let _ = fs::remove_dir_all(config_home);
    }

    #[test]
    fn parses_quoted_skill_frontmatter_values() {
        let contents = "---\nname: \"hud\"\ndescription: 'Quoted description'\n---\n";
        let (name, description) = super::parse_skill_frontmatter(contents);
        assert_eq!(name.as_deref(), Some("hud"));
        assert_eq!(description.as_deref(), Some("Quoted description"));
    }

    #[test]
    fn installs_skill_into_user_registry_and_preserves_nested_files() {
        let workspace = temp_dir("skills-install-workspace");
        let source_root = workspace.join("source").join("help");
        let install_root = temp_dir("skills-install-root");
        write_skill(
            source_root.parent().expect("parent"),
            "help",
            "Helpful skill",
        );
        let script_dir = source_root.join("scripts");
        fs::create_dir_all(&script_dir).expect("script dir");
        fs::write(script_dir.join("run.sh"), "#!/bin/sh\necho help\n").expect("write script");

        let installed = super::install_skill_into(
            source_root.to_str().expect("utf8 skill path"),
            &workspace,
            &install_root,
        )
        .expect("skill should install");

        assert_eq!(installed.invocation_name, "help");
        assert_eq!(installed.display_name.as_deref(), Some("help"));
        assert!(installed.installed_path.ends_with(Path::new("help")));
        assert!(installed.installed_path.join("SKILL.md").is_file());
        assert!(installed
            .installed_path
            .join("scripts")
            .join("run.sh")
            .is_file());

        let report = super::render_skill_install_report(&installed);
        assert!(report.contains("Result           installed help"));
        assert!(report.contains("Invoke as        $help"));
        assert!(report.contains(&install_root.display().to_string()));

        let roots = vec![SkillRoot {
            source: DefinitionSource::UserCodexHome,
            path: install_root.clone(),
            origin: SkillOrigin::SkillsDir,
        }];
        let listed = render_skills_report(
            &load_skills_from_roots(&roots).expect("installed skills should load"),
        );
        assert!(listed.contains("User config roots:"));
        assert!(listed.contains("help · Helpful skill"));

        let _ = fs::remove_dir_all(workspace);
        let _ = fs::remove_dir_all(install_root);
    }

    #[test]
    fn installs_plugin_from_path_and_lists_it() {
        let config_home = temp_dir("home");
        let source_root = temp_dir("source");
        write_external_plugin(&source_root, "demo", "1.0.0");

        let mut manager = PluginManager::new(PluginManagerConfig::new(&config_home));
        let install = handle_plugins_slash_command(
            Some("install"),
            Some(source_root.to_str().expect("utf8 path")),
            &mut manager,
        )
        .expect("install command should succeed");
        assert!(install.reload_runtime);
        assert!(install.message.contains("installed demo@external"));
        assert!(install.message.contains("Name             demo"));
        assert!(install.message.contains("Version          1.0.0"));
        assert!(install.message.contains("Status           enabled"));

        let list = handle_plugins_slash_command(Some("list"), None, &mut manager)
            .expect("list command should succeed");
        assert!(!list.reload_runtime);
        assert!(list.message.contains("demo"));
        assert!(list.message.contains("v1.0.0"));
        assert!(list.message.contains("enabled"));

        let _ = fs::remove_dir_all(config_home);
        let _ = fs::remove_dir_all(source_root);
    }

    #[test]
    fn enables_and_disables_plugin_by_name() {
        let config_home = temp_dir("toggle-home");
        let source_root = temp_dir("toggle-source");
        write_external_plugin(&source_root, "demo", "1.0.0");

        let mut manager = PluginManager::new(PluginManagerConfig::new(&config_home));
        handle_plugins_slash_command(
            Some("install"),
            Some(source_root.to_str().expect("utf8 path")),
            &mut manager,
        )
        .expect("install command should succeed");

        let disable = handle_plugins_slash_command(Some("disable"), Some("demo"), &mut manager)
            .expect("disable command should succeed");
        assert!(disable.reload_runtime);
        assert!(disable.message.contains("disabled demo@external"));
        assert!(disable.message.contains("Name             demo"));
        assert!(disable.message.contains("Status           disabled"));

        let list = handle_plugins_slash_command(Some("list"), None, &mut manager)
            .expect("list command should succeed");
        assert!(list.message.contains("demo"));
        assert!(list.message.contains("disabled"));

        let enable = handle_plugins_slash_command(Some("enable"), Some("demo"), &mut manager)
            .expect("enable command should succeed");
        assert!(enable.reload_runtime);
        assert!(enable.message.contains("enabled demo@external"));
        assert!(enable.message.contains("Name             demo"));
        assert!(enable.message.contains("Status           enabled"));

        let list = handle_plugins_slash_command(Some("list"), None, &mut manager)
            .expect("list command should succeed");
        assert!(list.message.contains("demo"));
        assert!(list.message.contains("enabled"));

        let _ = fs::remove_dir_all(config_home);
        let _ = fs::remove_dir_all(source_root);
    }

    #[test]
    fn lists_auto_installed_bundled_plugins_with_status() {
        let config_home = temp_dir("bundled-home");
        let bundled_root = temp_dir("bundled-root");
        let bundled_plugin = bundled_root.join("starter");
        write_bundled_plugin(&bundled_plugin, "starter", "0.1.0", false);

        let mut config = PluginManagerConfig::new(&config_home);
        config.bundled_root = Some(bundled_root.clone());
        let mut manager = PluginManager::new(config);

        let list = handle_plugins_slash_command(Some("list"), None, &mut manager)
            .expect("list command should succeed");
        assert!(!list.reload_runtime);
        assert!(list.message.contains("starter"));
        assert!(list.message.contains("v0.1.0"));
        assert!(list.message.contains("disabled"));

        let _ = fs::remove_dir_all(config_home);
        let _ = fs::remove_dir_all(bundled_root);
    }
}
