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 aliases: &'static [&'static str],
33 pub backend: CommandSkillBackend,
34}
35
36impl CommandSkillSpec {
37 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 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}