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