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