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 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 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}