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        "fast",
168        "Toggle OpenAI ultrafast processing at higher cost (usage: /fast [on|off])",
169        "/fast [on|off]",
170        "configuration"
171    ),
172    built_in_command_spec!(
173        "mode",
174        "Switch the active agent mode (usage: /mode [agent-name])",
175        "/mode [agent-name]",
176        "configuration"
177    ),
178    built_in_command_spec!("theme", "Switch UI theme (usage: /theme <theme-id>)", "/theme [theme-id]", "configuration"),
179    traditional_command_spec!(
180        "review",
181        "Review the current diff or selected files (usage: /review [instructions | --last-diff | --target <expr> | --file <path> | files...] [--style <style>])",
182        "/review [instructions | --last-diff | --target <expr> | --file <path> | files...] [--style <style>]",
183        "tools",
184        ".system/cmd-review"
185    ),
186    built_in_command_spec!(
187        "files",
188        "Browse and select files from workspace (usage: /files [filter])",
189        "/files [filter]",
190        "tools"
191    ),
192    built_in_command_spec!("copy", "Copy the latest complete assistant reply to clipboard", "/copy", "tools"),
193    built_in_command_spec!(
194        "skills",
195        "Open interactive skills manager (usage: /skills, /skills manager)",
196        "/skills [manager|list|search|create|load|unload|info|use|validate|package|regenerate-index|help]",
197        "tools"
198    ),
199    built_in_command_spec!(
200        "agent",
201        "Manage subagents and delegated child threads (usage: /agent [list|threads|inspect <id>|close <id>|create [project|user] [name]|edit [name]|delete <name>])",
202        "/agent [list|threads|inspect <id>|close <id>|create [project|user] [name]|edit [name]|delete <name>]",
203        "tools"
204    ),
205    built_in_command_spec!("status", "Show model, provider, workspace, and tool status", "/status", "status"),
206    built_in_command_spec!("stop", "Stop the active turn immediately", "/stop", "status"),
207    built_in_command_spec!("pause", "Pause the active turn at the next safe boundary", "/pause", "status"),
208    built_in_command_spec!(
209        "update",
210        "Check for new VT Code releases and install updates (usage: /update [check|install] [--force], or run `vtcode update` from the CLI)",
211        "/update [check|install] [--force]",
212        "status"
213    ),
214    built_in_command_spec!(
215        "mcp",
216        "Open interactive MCP manager (usage: /mcp, optional subcommands still supported)",
217        "/mcp [status|list|tools|refresh|config|config edit|repair|diagnose|login <name>|logout <name>]",
218        "integration"
219    ),
220    built_in_command_spec!(
221        "webmcp",
222        "Manage the opt-in authenticated browser editor bridge (usage: /webmcp [help|status|tools|roots|pair [--replace] <origin>|unpair])",
223        "/webmcp [help|status|tools|roots|pair [--replace] <origin>|unpair]",
224        "integration"
225    ),
226    built_in_command_spec!(
227        "explain",
228        "Explain recorded execution without a model call",
229        "/explain [--scope task|session] [--details|diagram|--web|--export html]",
230        "status"
231    ),
232    built_in_command_spec!(
233        "plugin",
234        "Open interactive Agent Plugins manager (usage: /plugin, /plugin manager)",
235        "/plugin [manager|list|info <name>|add <source> [--name <id>]|remove <name>|validate <path>|refresh|help]",
236        "integration"
237    ),
238    built_in_command_spec!(
239        "local",
240        "Manage local inference servers: Ollama, LM Studio, llama.cpp (usage: /local [action] [provider])",
241        "/local [status|start|stop|configure|troubleshoot] [ollama|lmstudio|llamacpp]",
242        "integration"
243    ),
244    built_in_command_spec!(
245        "resume",
246        "List archived sessions when idle; resume the active turn while it is paused",
247        "/resume [limit|--all]",
248        "session"
249    ),
250    built_in_command_spec!(
251        "fork",
252        "Fork an archived session into a new thread (usage: /fork [limit] [--all])",
253        "/fork [limit] [--all]",
254        "session"
255    ),
256    built_in_command_spec!(
257        "history",
258        "Open command history picker (usage: /history, same as Ctrl+R)",
259        "/history",
260        "session"
261    ),
262    built_in_command_spec!("clear", "Clear visible screen (usage: /clear [new])", "/clear [new]", "session"),
263    built_in_command_spec!(
264        "transcript",
265        "Inspect or manage the bounded TUI transcript snapshot (usage: /transcript [stats|clear|export [path]])",
266        "/transcript [stats|clear|export [path]]",
267        "session"
268    ),
269    built_in_command_spec!(
270        "compact",
271        "Compact the current conversation immediately or manage the saved manual compaction prompt",
272        "/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",
273        "session"
274    ),
275    built_in_command_spec!("new", "Start a new session", "/new", "session"),
276    built_in_command_spec!(
277        "share",
278        "Export the current session as JSON, Markdown, or self-contained HTML timeline (usage: /share [json|markdown|md|html|--format <fmt>])",
279        "/share [json|markdown|md|html|--format <fmt>]",
280        "session"
281    ),
282    built_in_command_spec!(
283        "rewind",
284        "Open the rewind picker or restore a specific checkpoint (usage: /rewind [turn] [conversation|code|both])",
285        "/rewind [turn] [conversation|code|both]",
286        "session"
287    ),
288    built_in_command_spec!("redo", "Restore files and conversation from before the last rewind", "/redo", "session"),
289    built_in_command_spec!(
290        "plan",
291        "Start or continue the planning workflow with an optional task prompt (usage: /plan [task])",
292        "/plan [task]",
293        "session"
294    ),
295    built_in_command_spec!("docs", "Open vtcode documentation in web browser", "/docs", "support"),
296    built_in_command_spec!(
297        "feedback",
298        "Open a new GitHub issue to report a bug or request a feature",
299        "/feedback",
300        "support"
301    ),
302    built_in_command_spec!("help", "Show slash command help", "/help [command]", "support"),
303    built_in_command_spec!("exit", "Exit the session", "/exit", "session"),
304    built_in_command_spec!("donate", "Support the project by buying the author a coffee", "/donate", "support"),
305    built_in_command_spec!(
306        "terminal-setup",
307        "Configure terminal for VT Code (multiline, copy/paste, shell, themes)",
308        "/terminal-setup",
309        "terminal"
310    ),
311    built_in_command_spec!(
312        "statusline",
313        "Set up a custom status line with target selection (usage: /statusline [instructions...])",
314        "/statusline [instructions...]",
315        "terminal"
316    ),
317    built_in_command_spec!("title", "Configure the terminal title items interactively", "/title", "terminal"),
318    built_in_command_spec!(
319        "vim",
320        "Toggle Vim-style prompt editing for this session (usage: /vim [on|off])",
321        "/vim [on|off]",
322        "terminal"
323    ),
324    built_in_command_spec!(
325        "login",
326        "Authenticate with OpenAI, OpenRouter, or GitHub Copilot (usage: /login [provider])",
327        "/login [provider]",
328        "auth"
329    ),
330    built_in_command_spec!(
331        "logout",
332        "Clear stored provider authentication (usage: /logout [provider])",
333        "/logout [provider]",
334        "auth"
335    ),
336    built_in_command_spec!(
337        "auth",
338        "Show authentication status for providers (usage: /auth [provider])",
339        "/auth [provider]",
340        "auth"
341    ),
342    built_in_command_spec!(
343        "refresh-oauth",
344        "Refresh stored provider credentials when supported (usage: /refresh-oauth [provider])",
345        "/refresh-oauth [provider]",
346        "auth"
347    ),
348    built_in_command_spec!(
349        "secret",
350        "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])",
351        "/secret [list|status [provider] [key-name]|add <provider> [key-name]|delete <provider> [key-name]|migrate [provider]|help]",
352        "auth"
353    ),
354];
355
356pub fn command_skill_specs() -> &'static [CommandSkillSpec] {
357    COMMAND_SKILL_SPECS
358}
359
360pub fn find_command_skill_by_slash_name(name: &str) -> Option<&'static CommandSkillSpec> {
361    COMMAND_SKILL_SPECS
362        .iter()
363        .find(|spec| spec.slash_name == name || spec.aliases.contains(&name))
364}
365
366pub fn find_command_skill_by_skill_name(name: &str) -> Option<&'static CommandSkillSpec> {
367    COMMAND_SKILL_SPECS.iter().find(|spec| spec.skill_name == name)
368}
369
370fn is_command_skill_name(name: &str) -> bool {
371    find_command_skill_by_skill_name(name).is_some()
372}
373
374pub fn is_model_catalog_eligible(skill: &SkillMetadata) -> bool {
375    if skill
376        .manifest
377        .as_ref()
378        .and_then(|manifest| manifest.disable_model_invocation)
379        .unwrap_or(false)
380    {
381        return false;
382    }
383
384    !is_command_skill_name(&skill.name)
385}
386
387pub fn built_in_command_skill_contexts() -> Vec<SkillContext> {
388    COMMAND_SKILL_SPECS
389        .iter()
390        .copied()
391        .filter(|spec| spec.is_built_in())
392        .map(|spec| SkillContext::MetadataOnly(built_in_manifest(&spec), built_in_path(spec.skill_name)))
393        .collect()
394}
395
396pub fn built_in_command_skill(name: &str) -> Option<BuiltInCommandSkill> {
397    find_command_skill_by_skill_name(name)
398        .filter(|spec| spec.is_built_in())
399        .map(BuiltInCommandSkill::from_spec)
400}
401
402pub fn merge_built_in_command_skill_contexts(skills: &mut Vec<SkillContext>) {
403    skills.extend(built_in_command_skill_contexts());
404    skills.sort_by(|left, right| left.manifest().name.cmp(&right.manifest().name));
405    skills.dedup_by(|left, right| left.manifest().name == right.manifest().name);
406}
407
408pub fn merge_built_in_command_skill_metadata(skills: &mut Vec<SkillMetadata>) {
409    skills.extend(built_in_command_skill_contexts().into_iter().map(|skill_ctx| SkillMetadata {
410        name: skill_ctx.manifest().name.clone(),
411        description: skill_ctx.manifest().description.clone(),
412        short_description: None,
413        path: skill_ctx.path().clone(),
414        scope: SkillScope::System,
415        manifest: Some(skill_ctx.manifest().clone().into()),
416    }));
417    skills.sort_by(|left, right| left.name.cmp(&right.name));
418    skills.dedup_by(|left, right| left.name == right.name);
419}
420
421fn built_in_manifest(spec: &CommandSkillSpec) -> SkillManifest {
422    SkillManifest {
423        name: spec.skill_name.to_string(),
424        description: spec.description.to_string(),
425        disable_model_invocation: Some(true),
426        variety: SkillVariety::BuiltIn,
427        metadata: Some(command_skill_metadata(spec, "built_in_command")),
428        ..Default::default()
429    }
430}
431
432fn command_skill_metadata(spec: &CommandSkillSpec, backend: &str) -> SkillManifestMetadata {
433    let mut metadata = HashMap::new();
434    metadata.insert("slash_alias".to_string(), json!(format!("/{}", spec.slash_name)));
435    metadata.insert("usage".to_string(), json!(spec.usage));
436    metadata.insert("category".to_string(), json!(spec.category));
437    metadata.insert("backend".to_string(), json!(backend));
438    metadata
439}
440
441fn built_in_path(skill_name: &str) -> PathBuf {
442    PathBuf::from(format!("<built-in>/{skill_name}"))
443}
444
445#[cfg(test)]
446mod tests {
447    use super::*;
448
449    #[test]
450    fn traditional_and_built_in_commands_are_mapped() {
451        let review = find_command_skill_by_slash_name("review").expect("review spec");
452        assert!(review.is_traditional());
453        assert_eq!(review.skill_name, "cmd-review");
454
455        let status = find_command_skill_by_slash_name("status").expect("status spec");
456        assert!(status.is_built_in());
457        assert_eq!(status.skill_name, "cmd-status");
458    }
459
460    #[test]
461    fn models_alias_opens_the_model_picker() {
462        let model = find_command_skill_by_slash_name("models").expect("models alias");
463        assert_eq!(model.slash_name, "model");
464        assert!(model.is_built_in());
465    }
466
467    #[test]
468    fn built_in_contexts_are_tagged_correctly() {
469        let built_in = built_in_command_skill_contexts();
470        let status = built_in
471            .iter()
472            .find(|ctx| ctx.manifest().name == "cmd-status")
473            .expect("cmd-status context");
474        assert_eq!(status.manifest().variety, SkillVariety::BuiltIn);
475    }
476
477    #[test]
478    fn removed_generate_agent_file_command_is_not_registered() {
479        assert!(find_command_skill_by_slash_name("generate-agent-file").is_none());
480        assert!(find_command_skill_by_skill_name("cmd-generate-agent-file").is_none());
481    }
482
483    #[test]
484    fn mode_command_switches_active_agent_mode() {
485        // The `/mode` command was re-introduced (commit ef1b52b6f) as a live
486        // feature: with no argument it opens the mode palette, with an argument
487        // (build|auto|duck|plan) it selects the primary agent directly. It must
488        // remain registered alongside the dedicated mode commands.
489        let mode = find_command_skill_by_slash_name("mode").expect("mode spec");
490        assert!(mode.description.contains("mode"));
491    }
492
493    #[test]
494    fn plan_command_skill_uses_workflow_copy() {
495        let plan = find_command_skill_by_slash_name("plan").expect("plan spec");
496        assert!(plan.description.contains("planning workflow"));
497        assert!(!plan.description.contains("mode"));
498        assert_eq!(plan.usage, "/plan [task]");
499    }
500
501    #[test]
502    fn removed_config_consolidated_commands_are_not_registered() {
503        for removed in [
504            "checkup",
505            "doctor",
506            "permissions",
507            "ide",
508            "tasks",
509            "jobs",
510            "log",
511            "subprocess",
512            "notify",
513            "memory",
514            "effort",
515            "continue",
516            "edit",
517            "analyze",
518        ] {
519            assert!(
520                find_command_skill_by_slash_name(removed).is_none(),
521                "/{removed} must not be registered; use /config instead"
522            );
523        }
524        let config = find_command_skill_by_slash_name("config").expect("config spec");
525        assert!(config.usage.contains("checkup"));
526    }
527}