Skip to main content

vtcode_skills/
command_skills.rs

1use crate::model::{SkillMetadata, SkillScope};
2use crate::types::{SkillContext, SkillManifest, SkillManifestMetadata, SkillVariety};
3use hashbrown::HashMap;
4use serde_json::json;
5use std::path::PathBuf;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum BuiltInCommandExecutor {
9    SlashAlias,
10}
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum CommandSkillBackend {
14    TraditionalSkill {
15        skill_name: &'static str,
16        skill_path: &'static str,
17    },
18    BuiltInCommand {
19        executor: BuiltInCommandExecutor,
20    },
21}
22
23#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub struct CommandSkillSpec {
25    pub slash_name: &'static str,
26    pub skill_name: &'static str,
27    pub description: &'static str,
28    pub usage: &'static str,
29    category: &'static str,
30    /// Additional slash names that resolve to this command (the canonical name
31    /// stays `slash_name`). Used so legacy commands keep working after a rename.
32    aliases: &'static [&'static str],
33    pub backend: CommandSkillBackend,
34}
35
36impl CommandSkillSpec {
37    const fn is_traditional(self) -> bool {
38        matches!(self.backend, CommandSkillBackend::TraditionalSkill { .. })
39    }
40
41    const fn is_built_in(self) -> bool {
42        matches!(self.backend, CommandSkillBackend::BuiltInCommand { .. })
43    }
44}
45
46#[derive(Debug, Clone)]
47pub struct BuiltInCommandSkill {
48    spec: &'static CommandSkillSpec,
49    manifest: SkillManifest,
50    path: PathBuf,
51}
52
53impl BuiltInCommandSkill {
54    fn from_spec(spec: &'static CommandSkillSpec) -> Self {
55        Self {
56            spec,
57            manifest: built_in_manifest(spec),
58            path: built_in_path(spec.skill_name),
59        }
60    }
61
62    pub fn spec(&self) -> &'static CommandSkillSpec {
63        self.spec
64    }
65
66    pub fn manifest(&self) -> &SkillManifest {
67        &self.manifest
68    }
69
70    pub fn name(&self) -> &str {
71        &self.manifest.name
72    }
73
74    pub fn description(&self) -> &str {
75        &self.manifest.description
76    }
77
78    pub fn usage(&self) -> &'static str {
79        self.spec.usage
80    }
81
82    pub fn category(&self) -> &'static str {
83        self.spec.category
84    }
85
86    pub fn slash_name(&self) -> &'static str {
87        self.spec.slash_name
88    }
89
90    pub fn path(&self) -> &PathBuf {
91        &self.path
92    }
93
94    pub fn scope(&self) -> SkillScope {
95        SkillScope::System
96    }
97
98    pub fn instructions(&self) -> String {
99        format!(
100            "# {}\n\nThis built-in command skill executes the existing `/{}` slash command backend.\n\n- Slash alias: `/{}`
101\n- Usage: `{}`
102\n- Category: `{}`
103\n- Backend: `built_in`\n",
104            self.name(),
105            self.slash_name(),
106            self.slash_name(),
107            self.usage(),
108            self.category()
109        )
110    }
111}
112
113macro_rules! built_in_command_spec {
114    ($slash:literal, $description:literal, $usage:literal, $category:literal) => {
115        CommandSkillSpec {
116            slash_name: $slash,
117            skill_name: concat!("cmd-", $slash),
118            description: $description,
119            usage: $usage,
120            category: $category,
121            aliases: &[],
122            backend: CommandSkillBackend::BuiltInCommand { executor: BuiltInCommandExecutor::SlashAlias },
123        }
124    };
125}
126
127macro_rules! traditional_command_spec {
128    ($slash:literal, $description:literal, $usage:literal, $category:literal, $skill_path:literal) => {
129        CommandSkillSpec {
130            slash_name: $slash,
131            skill_name: concat!("cmd-", $slash),
132            description: $description,
133            usage: $usage,
134            category: $category,
135            aliases: &[],
136            backend: CommandSkillBackend::TraditionalSkill {
137                skill_name: concat!("cmd-", $slash),
138                skill_path: $skill_path,
139            },
140        }
141    };
142}
143
144const COMMAND_SKILL_SPECS: &[CommandSkillSpec] = &[
145    built_in_command_spec!(
146        "init",
147        "Guided workspace setup for vtcode.toml, AGENTS.md, and indexing (usage: /init [--force])",
148        "/init [--force]",
149        "workspace"
150    ),
151    built_in_command_spec!(
152        "config",
153        "Browse settings and run workspace panels: memory, permissions, model, ide, tasks, jobs, log, subprocess, notify, checkup, or reset the active layer (usage: /config [memory|permissions|model|ide|tasks|jobs|log|subprocess|notify|checkup|<path>|reset])",
154        "/config [memory|permissions|model|ide|tasks|jobs|log|subprocess|notify|checkup|<path>|reset]",
155        "configuration"
156    ),
157    CommandSkillSpec {
158        slash_name: "model",
159        skill_name: "cmd-model",
160        description: "Launch the interactive model picker (includes reasoning effort)",
161        usage: "/model (alias: /models)",
162        category: "configuration",
163        aliases: &["models"],
164        backend: CommandSkillBackend::BuiltInCommand { executor: BuiltInCommandExecutor::SlashAlias },
165    },
166    built_in_command_spec!(
167        "mode",
168        "Switch the active agent mode (usage: /mode [agent-name])",
169        "/mode [agent-name]",
170        "configuration"
171    ),
172    built_in_command_spec!("theme", "Switch UI theme (usage: /theme <theme-id>)", "/theme [theme-id]", "configuration"),
173    traditional_command_spec!(
174        "review",
175        "Review the current diff or selected files (usage: /review [instructions | --last-diff | --target <expr> | --file <path> | files...] [--style <style>])",
176        "/review [instructions | --last-diff | --target <expr> | --file <path> | files...] [--style <style>]",
177        "tools",
178        ".system/cmd-review"
179    ),
180    built_in_command_spec!(
181        "files",
182        "Browse and select files from workspace (usage: /files [filter])",
183        "/files [filter]",
184        "tools"
185    ),
186    built_in_command_spec!("copy", "Copy the latest complete assistant reply to clipboard", "/copy", "tools"),
187    built_in_command_spec!(
188        "skills",
189        "Open interactive skills manager (usage: /skills, /skills manager)",
190        "/skills [manager|list|search|create|load|unload|info|use|validate|package|regenerate-index|help]",
191        "tools"
192    ),
193    built_in_command_spec!(
194        "agent",
195        "Manage subagents and delegated child threads (usage: /agent [list|threads|inspect <id>|close <id>|create [project|user] [name]|edit [name]|delete <name>])",
196        "/agent [list|threads|inspect <id>|close <id>|create [project|user] [name]|edit [name]|delete <name>]",
197        "tools"
198    ),
199    built_in_command_spec!("status", "Show model, provider, workspace, and tool status", "/status", "status"),
200    built_in_command_spec!("stop", "Stop the active turn immediately", "/stop", "status"),
201    built_in_command_spec!("pause", "Pause the active turn at the next safe boundary", "/pause", "status"),
202    built_in_command_spec!(
203        "update",
204        "Check for new VT Code releases and install updates (usage: /update [check|install] [--force], or run `vtcode update` from the CLI)",
205        "/update [check|install] [--force]",
206        "status"
207    ),
208    built_in_command_spec!(
209        "mcp",
210        "Open interactive MCP manager (usage: /mcp, optional subcommands still supported)",
211        "/mcp [status|list|tools|refresh|config|config edit|repair|diagnose|login <name>|logout <name>]",
212        "integration"
213    ),
214    built_in_command_spec!(
215        "webmcp",
216        "Manage the opt-in authenticated browser editor bridge (usage: /webmcp [help|status|tools|roots|pair [--replace] <origin>|unpair])",
217        "/webmcp [help|status|tools|roots|pair [--replace] <origin>|unpair]",
218        "integration"
219    ),
220    built_in_command_spec!(
221        "explain",
222        "Explain recorded execution without a model call",
223        "/explain [--scope task|session] [--details|diagram|--web|--export html]",
224        "status"
225    ),
226    built_in_command_spec!(
227        "plugin",
228        "Open interactive Agent Plugins manager (usage: /plugin, /plugin manager)",
229        "/plugin [manager|list|info <name>|add <source> [--name <id>]|remove <name>|validate <path>|refresh|help]",
230        "integration"
231    ),
232    built_in_command_spec!(
233        "local",
234        "Manage local inference servers: Ollama, LM Studio, llama.cpp (usage: /local [action] [provider])",
235        "/local [status|start|stop|configure|troubleshoot] [ollama|lmstudio|llamacpp]",
236        "integration"
237    ),
238    built_in_command_spec!(
239        "resume",
240        "List archived sessions when idle; resume the active turn while it is paused",
241        "/resume [limit|--all]",
242        "session"
243    ),
244    built_in_command_spec!(
245        "fork",
246        "Fork an archived session into a new thread (usage: /fork [limit] [--all])",
247        "/fork [limit] [--all]",
248        "session"
249    ),
250    built_in_command_spec!(
251        "history",
252        "Open command history picker (usage: /history, same as Ctrl+R)",
253        "/history",
254        "session"
255    ),
256    built_in_command_spec!("clear", "Clear visible screen (usage: /clear [new])", "/clear [new]", "session"),
257    built_in_command_spec!(
258        "transcript",
259        "Inspect or manage the bounded TUI transcript snapshot (usage: /transcript [stats|clear|export [path]])",
260        "/transcript [stats|clear|export [path]]",
261        "session"
262    ),
263    built_in_command_spec!(
264        "compact",
265        "Compact the current conversation immediately or manage the saved manual compaction prompt",
266        "/compact [--instructions <text>] [--max-output-tokens <n>] [--reasoning-effort <none|minimal|low|medium|high|xhigh>] [--verbosity <low|medium|high>] [--native-only] | /compact edit-prompt | /compact reset-prompt",
267        "session"
268    ),
269    built_in_command_spec!("new", "Start a new session", "/new", "session"),
270    built_in_command_spec!(
271        "share",
272        "Export the current session as JSON, Markdown, or self-contained HTML timeline (usage: /share [json|markdown|md|html|--format <fmt>])",
273        "/share [json|markdown|md|html|--format <fmt>]",
274        "session"
275    ),
276    built_in_command_spec!(
277        "rewind",
278        "Open the rewind picker or restore a specific checkpoint (usage: /rewind [turn] [conversation|code|both])",
279        "/rewind [turn] [conversation|code|both]",
280        "session"
281    ),
282    built_in_command_spec!("redo", "Restore files and conversation from before the last rewind", "/redo", "session"),
283    built_in_command_spec!(
284        "plan",
285        "Start or continue the planning workflow with an optional task prompt (usage: /plan [task])",
286        "/plan [task]",
287        "session"
288    ),
289    built_in_command_spec!("docs", "Open vtcode documentation in web browser", "/docs", "support"),
290    built_in_command_spec!(
291        "feedback",
292        "Open a new GitHub issue to report a bug or request a feature",
293        "/feedback",
294        "support"
295    ),
296    built_in_command_spec!("help", "Show slash command help", "/help [command]", "support"),
297    built_in_command_spec!("exit", "Exit the session", "/exit", "session"),
298    built_in_command_spec!("donate", "Support the project by buying the author a coffee", "/donate", "support"),
299    built_in_command_spec!(
300        "terminal-setup",
301        "Configure terminal for VT Code (multiline, copy/paste, shell, themes)",
302        "/terminal-setup",
303        "terminal"
304    ),
305    built_in_command_spec!(
306        "statusline",
307        "Set up a custom status line with target selection (usage: /statusline [instructions...])",
308        "/statusline [instructions...]",
309        "terminal"
310    ),
311    built_in_command_spec!("title", "Configure the terminal title items interactively", "/title", "terminal"),
312    built_in_command_spec!(
313        "vim",
314        "Toggle Vim-style prompt editing for this session (usage: /vim [on|off])",
315        "/vim [on|off]",
316        "terminal"
317    ),
318    built_in_command_spec!(
319        "login",
320        "Authenticate with OpenAI, OpenRouter, or GitHub Copilot (usage: /login [provider])",
321        "/login [provider]",
322        "auth"
323    ),
324    built_in_command_spec!(
325        "logout",
326        "Clear stored provider authentication (usage: /logout [provider])",
327        "/logout [provider]",
328        "auth"
329    ),
330    built_in_command_spec!(
331        "auth",
332        "Show authentication status for providers (usage: /auth [provider])",
333        "/auth [provider]",
334        "auth"
335    ),
336    built_in_command_spec!(
337        "refresh-oauth",
338        "Refresh stored provider credentials when supported (usage: /refresh-oauth [provider])",
339        "/refresh-oauth [provider]",
340        "auth"
341    ),
342    built_in_command_spec!(
343        "secret",
344        "Manage provider API keys in your OS keyring (usage: /secret [list|status [provider] [key-name]|add <provider> [key-name]|delete <provider> [key-name]|migrate [provider]|help])",
345        "/secret [list|status [provider] [key-name]|add <provider> [key-name]|delete <provider> [key-name]|migrate [provider]|help]",
346        "auth"
347    ),
348];
349
350pub fn command_skill_specs() -> &'static [CommandSkillSpec] {
351    COMMAND_SKILL_SPECS
352}
353
354pub fn find_command_skill_by_slash_name(name: &str) -> Option<&'static CommandSkillSpec> {
355    COMMAND_SKILL_SPECS
356        .iter()
357        .find(|spec| spec.slash_name == name || spec.aliases.contains(&name))
358}
359
360pub fn find_command_skill_by_skill_name(name: &str) -> Option<&'static CommandSkillSpec> {
361    COMMAND_SKILL_SPECS.iter().find(|spec| spec.skill_name == name)
362}
363
364fn is_command_skill_name(name: &str) -> bool {
365    find_command_skill_by_skill_name(name).is_some()
366}
367
368pub fn is_model_catalog_eligible(skill: &SkillMetadata) -> bool {
369    if skill
370        .manifest
371        .as_ref()
372        .and_then(|manifest| manifest.disable_model_invocation)
373        .unwrap_or(false)
374    {
375        return false;
376    }
377
378    !is_command_skill_name(&skill.name)
379}
380
381pub fn built_in_command_skill_contexts() -> Vec<SkillContext> {
382    COMMAND_SKILL_SPECS
383        .iter()
384        .copied()
385        .filter(|spec| spec.is_built_in())
386        .map(|spec| SkillContext::MetadataOnly(built_in_manifest(&spec), built_in_path(spec.skill_name)))
387        .collect()
388}
389
390pub fn built_in_command_skill(name: &str) -> Option<BuiltInCommandSkill> {
391    find_command_skill_by_skill_name(name)
392        .filter(|spec| spec.is_built_in())
393        .map(BuiltInCommandSkill::from_spec)
394}
395
396pub fn merge_built_in_command_skill_contexts(skills: &mut Vec<SkillContext>) {
397    skills.extend(built_in_command_skill_contexts());
398    skills.sort_by(|left, right| left.manifest().name.cmp(&right.manifest().name));
399    skills.dedup_by(|left, right| left.manifest().name == right.manifest().name);
400}
401
402pub fn merge_built_in_command_skill_metadata(skills: &mut Vec<SkillMetadata>) {
403    skills.extend(built_in_command_skill_contexts().into_iter().map(|skill_ctx| SkillMetadata {
404        name: skill_ctx.manifest().name.clone(),
405        description: skill_ctx.manifest().description.clone(),
406        short_description: None,
407        path: skill_ctx.path().clone(),
408        scope: SkillScope::System,
409        manifest: Some(skill_ctx.manifest().clone().into()),
410    }));
411    skills.sort_by(|left, right| left.name.cmp(&right.name));
412    skills.dedup_by(|left, right| left.name == right.name);
413}
414
415fn built_in_manifest(spec: &CommandSkillSpec) -> SkillManifest {
416    SkillManifest {
417        name: spec.skill_name.to_string(),
418        description: spec.description.to_string(),
419        disable_model_invocation: Some(true),
420        variety: SkillVariety::BuiltIn,
421        metadata: Some(command_skill_metadata(spec, "built_in_command")),
422        ..Default::default()
423    }
424}
425
426fn command_skill_metadata(spec: &CommandSkillSpec, backend: &str) -> SkillManifestMetadata {
427    let mut metadata = HashMap::new();
428    metadata.insert("slash_alias".to_string(), json!(format!("/{}", spec.slash_name)));
429    metadata.insert("usage".to_string(), json!(spec.usage));
430    metadata.insert("category".to_string(), json!(spec.category));
431    metadata.insert("backend".to_string(), json!(backend));
432    metadata
433}
434
435fn built_in_path(skill_name: &str) -> PathBuf {
436    PathBuf::from(format!("<built-in>/{skill_name}"))
437}
438
439#[cfg(test)]
440mod tests {
441    use super::*;
442
443    #[test]
444    fn traditional_and_built_in_commands_are_mapped() {
445        let review = find_command_skill_by_slash_name("review").expect("review spec");
446        assert!(review.is_traditional());
447        assert_eq!(review.skill_name, "cmd-review");
448
449        let status = find_command_skill_by_slash_name("status").expect("status spec");
450        assert!(status.is_built_in());
451        assert_eq!(status.skill_name, "cmd-status");
452    }
453
454    #[test]
455    fn models_alias_opens_the_model_picker() {
456        let model = find_command_skill_by_slash_name("models").expect("models alias");
457        assert_eq!(model.slash_name, "model");
458        assert!(model.is_built_in());
459    }
460
461    #[test]
462    fn built_in_contexts_are_tagged_correctly() {
463        let built_in = built_in_command_skill_contexts();
464        let status = built_in
465            .iter()
466            .find(|ctx| ctx.manifest().name == "cmd-status")
467            .expect("cmd-status context");
468        assert_eq!(status.manifest().variety, SkillVariety::BuiltIn);
469    }
470
471    #[test]
472    fn removed_generate_agent_file_command_is_not_registered() {
473        assert!(find_command_skill_by_slash_name("generate-agent-file").is_none());
474        assert!(find_command_skill_by_skill_name("cmd-generate-agent-file").is_none());
475    }
476
477    #[test]
478    fn mode_command_switches_active_agent_mode() {
479        // The `/mode` command was re-introduced (commit ef1b52b6f) as a live
480        // feature: with no argument it opens the mode palette, with an argument
481        // (build|auto|duck|plan) it selects the primary agent directly. It must
482        // remain registered alongside the dedicated mode commands.
483        let mode = find_command_skill_by_slash_name("mode").expect("mode spec");
484        assert!(mode.description.contains("mode"));
485    }
486
487    #[test]
488    fn plan_command_skill_uses_workflow_copy() {
489        let plan = find_command_skill_by_slash_name("plan").expect("plan spec");
490        assert!(plan.description.contains("planning workflow"));
491        assert!(!plan.description.contains("mode"));
492        assert_eq!(plan.usage, "/plan [task]");
493    }
494
495    #[test]
496    fn removed_config_consolidated_commands_are_not_registered() {
497        for removed in [
498            "checkup",
499            "doctor",
500            "permissions",
501            "ide",
502            "tasks",
503            "jobs",
504            "log",
505            "subprocess",
506            "notify",
507            "memory",
508            "effort",
509            "continue",
510            "edit",
511            "analyze",
512        ] {
513            assert!(
514                find_command_skill_by_slash_name(removed).is_none(),
515                "/{removed} must not be registered; use /config instead"
516            );
517        }
518        let config = find_command_skill_by_slash_name("config").expect("config spec");
519        assert!(config.usage.contains("checkup"));
520    }
521}