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 "resume",
252 "List archived sessions when idle; resume the active turn while it is paused",
253 "/resume [limit|--all]",
254 "session"
255 ),
256 built_in_command_spec!(
257 "fork",
258 "Fork an archived session into a new thread (usage: /fork [limit] [--all])",
259 "/fork [limit] [--all]",
260 "session"
261 ),
262 built_in_command_spec!(
263 "history",
264 "Open command history picker (usage: /history, same as Ctrl+R)",
265 "/history",
266 "session"
267 ),
268 built_in_command_spec!("clear", "Clear visible screen (usage: /clear [new])", "/clear [new]", "session"),
269 built_in_command_spec!(
270 "transcript",
271 "Inspect or manage the bounded TUI transcript snapshot (usage: /transcript [stats|clear|export [path]])",
272 "/transcript [stats|clear|export [path]]",
273 "session"
274 ),
275 built_in_command_spec!(
276 "compact",
277 "Compact the current conversation immediately or manage the saved manual compaction prompt",
278 "/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",
279 "session"
280 ),
281 built_in_command_spec!("new", "Start a new session", "/new", "session"),
282 built_in_command_spec!(
283 "share",
284 "Export the current session as JSON, Markdown, or self-contained HTML timeline (usage: /share [json|markdown|md|html|--format <fmt>])",
285 "/share [json|markdown|md|html|--format <fmt>]",
286 "session"
287 ),
288 built_in_command_spec!(
289 "rewind",
290 "Open the rewind picker or restore a specific checkpoint (usage: /rewind [turn] [conversation|code|both])",
291 "/rewind [turn] [conversation|code|both]",
292 "session"
293 ),
294 built_in_command_spec!("redo", "Restore files and conversation from before the last rewind", "/redo", "session"),
295 built_in_command_spec!(
296 "plan",
297 "Start or continue the planning workflow with an optional task prompt (usage: /plan [task])",
298 "/plan [task]",
299 "session"
300 ),
301 built_in_command_spec!("docs", "Open vtcode documentation in web browser", "/docs", "support"),
302 built_in_command_spec!(
303 "feedback",
304 "Open a new GitHub issue to report a bug or request a feature",
305 "/feedback",
306 "support"
307 ),
308 built_in_command_spec!("help", "Show slash command help", "/help [command]", "support"),
309 built_in_command_spec!("exit", "Exit the session", "/exit", "session"),
310 built_in_command_spec!("donate", "Support the project by buying the author a coffee", "/donate", "support"),
311 built_in_command_spec!(
312 "terminal-setup",
313 "Configure terminal for VT Code (multiline, copy/paste, shell, themes, tab icon)",
314 "/terminal-setup [install-iterm2-icon|reset-iterm2-icon|icon [light|dark|clear|tinted|dynamic]]",
315 "terminal"
316 ),
317 built_in_command_spec!(
318 "statusline",
319 "Set up a custom status line with target selection (usage: /statusline [instructions...])",
320 "/statusline [instructions...]",
321 "terminal"
322 ),
323 built_in_command_spec!("title", "Configure the terminal title items interactively", "/title", "terminal"),
324 built_in_command_spec!(
325 "vim",
326 "Toggle Vim-style prompt editing for this session (usage: /vim [on|off])",
327 "/vim [on|off]",
328 "terminal"
329 ),
330 built_in_command_spec!(
331 "login",
332 "Authenticate with OpenAI, OpenRouter, or GitHub Copilot (usage: /login [provider])",
333 "/login [provider]",
334 "auth"
335 ),
336 built_in_command_spec!(
337 "logout",
338 "Clear stored provider authentication (usage: /logout [provider])",
339 "/logout [provider]",
340 "auth"
341 ),
342 built_in_command_spec!(
343 "auth",
344 "Show authentication status for providers (usage: /auth [provider])",
345 "/auth [provider]",
346 "auth"
347 ),
348 built_in_command_spec!(
349 "refresh-oauth",
350 "Refresh stored provider credentials when supported (usage: /refresh-oauth [provider])",
351 "/refresh-oauth [provider]",
352 "auth"
353 ),
354 built_in_command_spec!(
355 "secret",
356 "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])",
357 "/secret [list|status [provider] [key-name]|add <provider> [key-name]|delete <provider> [key-name]|migrate [provider]|help]",
358 "auth"
359 ),
360];
361
362pub fn command_skill_specs() -> &'static [CommandSkillSpec] {
363 COMMAND_SKILL_SPECS
364}
365
366pub fn find_command_skill_by_slash_name(name: &str) -> Option<&'static CommandSkillSpec> {
367 COMMAND_SKILL_SPECS
368 .iter()
369 .find(|spec| spec.slash_name == name || spec.aliases.contains(&name))
370}
371
372pub fn find_command_skill_by_skill_name(name: &str) -> Option<&'static CommandSkillSpec> {
373 COMMAND_SKILL_SPECS.iter().find(|spec| spec.skill_name == name)
374}
375
376fn is_command_skill_name(name: &str) -> bool {
377 find_command_skill_by_skill_name(name).is_some()
378}
379
380pub fn is_model_catalog_eligible(skill: &SkillMetadata) -> bool {
381 if skill
382 .manifest
383 .as_ref()
384 .and_then(|manifest| manifest.disable_model_invocation)
385 .unwrap_or(false)
386 {
387 return false;
388 }
389
390 !is_command_skill_name(&skill.name)
391}
392
393pub fn built_in_command_skill_contexts() -> Vec<SkillContext> {
394 COMMAND_SKILL_SPECS
395 .iter()
396 .copied()
397 .filter(|spec| spec.is_built_in())
398 .map(|spec| SkillContext::MetadataOnly(built_in_manifest(&spec), built_in_path(spec.skill_name)))
399 .collect()
400}
401
402pub fn built_in_command_skill(name: &str) -> Option<BuiltInCommandSkill> {
403 find_command_skill_by_skill_name(name)
404 .filter(|spec| spec.is_built_in())
405 .map(BuiltInCommandSkill::from_spec)
406}
407
408pub fn merge_built_in_command_skill_contexts(skills: &mut Vec<SkillContext>) {
409 skills.extend(built_in_command_skill_contexts());
410 skills.sort_by(|left, right| left.manifest().name.cmp(&right.manifest().name));
411 skills.dedup_by(|left, right| left.manifest().name == right.manifest().name);
412}
413
414pub fn merge_built_in_command_skill_metadata(skills: &mut Vec<SkillMetadata>) {
415 skills.extend(built_in_command_skill_contexts().into_iter().map(|skill_ctx| SkillMetadata {
416 name: skill_ctx.manifest().name.clone(),
417 description: skill_ctx.manifest().description.clone(),
418 short_description: None,
419 path: skill_ctx.path().clone(),
420 scope: SkillScope::System,
421 manifest: Some(skill_ctx.manifest().clone().into()),
422 }));
423 skills.sort_by(|left, right| left.name.cmp(&right.name));
424 skills.dedup_by(|left, right| left.name == right.name);
425}
426
427fn built_in_manifest(spec: &CommandSkillSpec) -> SkillManifest {
428 SkillManifest {
429 name: spec.skill_name.to_string(),
430 description: spec.description.to_string(),
431 disable_model_invocation: Some(true),
432 variety: SkillVariety::BuiltIn,
433 metadata: Some(command_skill_metadata(spec, "built_in_command")),
434 ..Default::default()
435 }
436}
437
438fn command_skill_metadata(spec: &CommandSkillSpec, backend: &str) -> SkillManifestMetadata {
439 let mut metadata = HashMap::new();
440 metadata.insert("slash_alias".to_string(), json!(format!("/{}", spec.slash_name)));
441 metadata.insert("usage".to_string(), json!(spec.usage));
442 metadata.insert("category".to_string(), json!(spec.category));
443 metadata.insert("backend".to_string(), json!(backend));
444 metadata
445}
446
447fn built_in_path(skill_name: &str) -> PathBuf {
448 PathBuf::from(format!("<built-in>/{skill_name}"))
449}
450
451#[cfg(test)]
452mod tests {
453 use super::*;
454
455 #[test]
456 fn traditional_and_built_in_commands_are_mapped() {
457 let review = find_command_skill_by_slash_name("review").expect("review spec");
458 assert!(review.is_traditional());
459 assert_eq!(review.skill_name, "cmd-review");
460
461 let status = find_command_skill_by_slash_name("status").expect("status spec");
462 assert!(status.is_built_in());
463 assert_eq!(status.skill_name, "cmd-status");
464
465 let splash = find_command_skill_by_slash_name("splash").expect("splash spec");
466 assert!(splash.is_built_in());
467 assert_eq!(splash.usage, "/splash");
468 }
469
470 #[test]
471 fn models_alias_opens_the_model_picker() {
472 let model = find_command_skill_by_slash_name("models").expect("models alias");
473 assert_eq!(model.slash_name, "model");
474 assert!(model.is_built_in());
475 }
476
477 #[test]
478 fn built_in_contexts_are_tagged_correctly() {
479 let built_in = built_in_command_skill_contexts();
480 let status = built_in
481 .iter()
482 .find(|ctx| ctx.manifest().name == "cmd-status")
483 .expect("cmd-status context");
484 assert_eq!(status.manifest().variety, SkillVariety::BuiltIn);
485 }
486
487 #[test]
488 fn removed_generate_agent_file_command_is_not_registered() {
489 assert!(find_command_skill_by_slash_name("generate-agent-file").is_none());
490 assert!(find_command_skill_by_skill_name("cmd-generate-agent-file").is_none());
491 }
492
493 #[test]
494 fn mode_command_switches_active_agent_mode() {
495 let mode = find_command_skill_by_slash_name("mode").expect("mode spec");
500 assert!(mode.description.contains("mode"));
501 }
502
503 #[test]
504 fn plan_command_skill_uses_workflow_copy() {
505 let plan = find_command_skill_by_slash_name("plan").expect("plan spec");
506 assert!(plan.description.contains("planning workflow"));
507 assert!(!plan.description.contains("mode"));
508 assert_eq!(plan.usage, "/plan [task]");
509 }
510
511 #[test]
512 fn removed_config_consolidated_commands_are_not_registered() {
513 for removed in [
514 "checkup",
515 "doctor",
516 "permissions",
517 "ide",
518 "tasks",
519 "jobs",
520 "log",
521 "subprocess",
522 "notify",
523 "memory",
524 "effort",
525 "continue",
526 "edit",
527 "analyze",
528 ] {
529 assert!(
530 find_command_skill_by_slash_name(removed).is_none(),
531 "/{removed} must not be registered; use /config instead"
532 );
533 }
534 let config = find_command_skill_by_slash_name("config").expect("config spec");
535 assert!(config.usage.contains("checkup"));
536 }
537}