1use anyhow::{Context, Result};
2use serde::{Deserialize, Serialize};
3use std::fs;
4use std::path::Path;
5
6use crate::acp::AgentClientProtocolConfig;
7use crate::codex::{FileOpener, HistoryConfig, TuiConfig};
8use crate::context::ContextFeaturesConfig;
9use crate::core::{
10 AgentConfig, AnthropicConfig, AuthConfig, AutomationConfig, CommandsConfig,
11 CustomProviderConfig, DotfileProtectionConfig, ModelConfig, OpenAIConfig, PermissionsConfig,
12 PromptCachingConfig, SandboxConfig, SecurityConfig, SkillsConfig, ToolsConfig,
13};
14use crate::debug::DebugConfig;
15use crate::defaults::{self, ConfigDefaultsProvider};
16use crate::hooks::HooksConfig;
17use crate::ide_context::IdeContextConfig;
18use crate::mcp::McpClientConfig;
19use crate::optimization::OptimizationConfig;
20use crate::output_styles::OutputStyleConfig;
21use crate::root::{ChatConfig, PtyConfig, UiConfig};
22use crate::subagents::SubagentRuntimeLimits;
23use crate::telemetry::TelemetryConfig;
24use crate::timeouts::TimeoutsConfig;
25
26use crate::loader::syntax_highlighting::SyntaxHighlightingConfig;
27
28#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
30#[derive(Debug, Clone, Deserialize, Serialize, Default)]
31pub struct ProviderConfig {
32 #[serde(default)]
34 pub openai: OpenAIConfig,
35
36 #[serde(default)]
38 pub anthropic: AnthropicConfig,
39}
40
41#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
43#[derive(Debug, Clone, Deserialize, Serialize, Default)]
44pub struct VTCodeConfig {
45 #[serde(default)]
47 pub file_opener: FileOpener,
48
49 #[serde(default)]
51 pub project_doc_max_bytes: Option<usize>,
52
53 #[serde(default)]
55 pub project_doc_fallback_filenames: Vec<String>,
56
57 #[serde(default)]
59 pub notify: Vec<String>,
60
61 #[serde(default)]
63 pub history: HistoryConfig,
64
65 #[serde(default)]
67 pub tui: TuiConfig,
68
69 #[serde(default)]
71 pub agent: AgentConfig,
72
73 #[serde(default)]
75 pub auth: AuthConfig,
76
77 #[serde(default)]
79 pub tools: ToolsConfig,
80
81 #[serde(default)]
83 pub commands: CommandsConfig,
84
85 #[serde(default)]
87 pub permissions: PermissionsConfig,
88
89 #[serde(default)]
91 pub security: SecurityConfig,
92
93 #[serde(default)]
95 pub sandbox: SandboxConfig,
96
97 #[serde(default)]
99 pub ui: UiConfig,
100
101 #[serde(default)]
103 pub chat: ChatConfig,
104
105 #[serde(default)]
107 pub pty: PtyConfig,
108
109 #[serde(default)]
111 pub debug: DebugConfig,
112
113 #[serde(default)]
115 pub context: ContextFeaturesConfig,
116
117 #[serde(default)]
119 pub telemetry: TelemetryConfig,
120
121 #[serde(default)]
123 pub optimization: OptimizationConfig,
124
125 #[serde(default)]
127 pub syntax_highlighting: SyntaxHighlightingConfig,
128
129 #[serde(default)]
131 pub timeouts: TimeoutsConfig,
132
133 #[serde(default)]
135 pub automation: AutomationConfig,
136
137 #[serde(default)]
139 pub subagents: SubagentRuntimeLimits,
140
141 #[serde(default)]
143 pub prompt_cache: PromptCachingConfig,
144
145 #[serde(default)]
147 pub mcp: McpClientConfig,
148
149 #[serde(default)]
151 pub acp: AgentClientProtocolConfig,
152
153 #[serde(default)]
155 pub ide_context: IdeContextConfig,
156
157 #[serde(default)]
159 pub hooks: HooksConfig,
160
161 #[serde(default)]
163 pub model: ModelConfig,
164
165 #[serde(default)]
167 pub provider: ProviderConfig,
168
169 #[serde(default)]
171 pub skills: SkillsConfig,
172
173 #[serde(default)]
177 pub custom_providers: Vec<CustomProviderConfig>,
178
179 #[serde(default)]
181 pub output_style: OutputStyleConfig,
182
183 #[serde(default)]
185 pub dotfile_protection: DotfileProtectionConfig,
186}
187
188impl VTCodeConfig {
189 pub fn apply_compat_defaults(&mut self) {
190 if let Some(max_bytes) = self.project_doc_max_bytes {
191 self.agent.project_doc_max_bytes = max_bytes;
192 }
193
194 if !self.project_doc_fallback_filenames.is_empty() {
195 self.agent.project_doc_fallback_filenames = self.project_doc_fallback_filenames.clone();
196 }
197 }
198
199 pub fn validate(&self) -> Result<()> {
200 self.syntax_highlighting
201 .validate()
202 .context("Invalid syntax_highlighting configuration")?;
203
204 self.context
205 .validate()
206 .context("Invalid context configuration")?;
207
208 self.hooks
209 .validate()
210 .context("Invalid hooks configuration")?;
211
212 self.timeouts
213 .validate()
214 .context("Invalid timeouts configuration")?;
215
216 self.prompt_cache
217 .validate()
218 .context("Invalid prompt_cache configuration")?;
219
220 self.agent
221 .validate_llm_params()
222 .map_err(anyhow::Error::msg)
223 .context("Invalid agent configuration")?;
224
225 self.ui
226 .keyboard_protocol
227 .validate()
228 .context("Invalid keyboard_protocol configuration")?;
229
230 self.pty.validate().context("Invalid pty configuration")?;
231
232 let mut seen_names = std::collections::HashSet::new();
234 for cp in &self.custom_providers {
235 cp.validate()
236 .map_err(|msg| anyhow::anyhow!(msg))
237 .context("Invalid custom_providers configuration")?;
238 if !seen_names.insert(cp.name.to_lowercase()) {
239 anyhow::bail!("custom_providers: duplicate name `{}`", cp.name);
240 }
241 }
242
243 Ok(())
244 }
245
246 pub fn custom_provider(&self, name: &str) -> Option<&CustomProviderConfig> {
248 let lower = name.to_lowercase();
249 self.custom_providers
250 .iter()
251 .find(|cp| cp.name.to_lowercase() == lower)
252 }
253
254 pub fn provider_display_name(&self, provider_key: &str) -> String {
257 if let Some(cp) = self.custom_provider(provider_key) {
258 cp.display_name.clone()
259 } else if let Ok(p) = std::str::FromStr::from_str(provider_key) {
260 let p: crate::models::Provider = p;
261 p.label().to_string()
262 } else {
263 provider_key.to_string()
264 }
265 }
266
267 #[cfg(feature = "bootstrap")]
268 pub fn bootstrap_project<P: AsRef<Path>>(workspace: P, force: bool) -> Result<Vec<String>> {
270 Self::bootstrap_project_with_options(workspace, force, false)
271 }
272
273 #[cfg(feature = "bootstrap")]
274 pub fn bootstrap_project_with_options<P: AsRef<Path>>(
276 workspace: P,
277 force: bool,
278 use_home_dir: bool,
279 ) -> Result<Vec<String>> {
280 let workspace = workspace.as_ref().to_path_buf();
281 defaults::with_config_defaults(|provider| {
282 Self::bootstrap_project_with_provider(&workspace, force, use_home_dir, provider)
283 })
284 }
285
286 #[cfg(feature = "bootstrap")]
287 pub fn bootstrap_project_with_provider<P: AsRef<Path>>(
289 workspace: P,
290 force: bool,
291 use_home_dir: bool,
292 defaults_provider: &dyn ConfigDefaultsProvider,
293 ) -> Result<Vec<String>> {
294 let workspace = workspace.as_ref();
295 let config_file_name = defaults_provider.config_file_name().to_string();
296 let (config_path, gitignore_path) = crate::loader::bootstrap::determine_bootstrap_targets(
297 workspace,
298 use_home_dir,
299 &config_file_name,
300 defaults_provider,
301 )?;
302 let vtcode_readme_path = workspace.join(".vtcode").join("README.md");
303 let ast_grep_config_path = workspace.join("sgconfig.yml");
304 let ast_grep_rule_path = workspace
305 .join("rules")
306 .join("examples")
307 .join("no-console-log.yml");
308 let ast_grep_test_path = workspace
309 .join("rule-tests")
310 .join("examples")
311 .join("no-console-log-test.yml");
312
313 crate::loader::bootstrap::ensure_parent_dir(&config_path)?;
314 crate::loader::bootstrap::ensure_parent_dir(&gitignore_path)?;
315 crate::loader::bootstrap::ensure_parent_dir(&vtcode_readme_path)?;
316 crate::loader::bootstrap::ensure_parent_dir(&ast_grep_config_path)?;
317 crate::loader::bootstrap::ensure_parent_dir(&ast_grep_rule_path)?;
318 crate::loader::bootstrap::ensure_parent_dir(&ast_grep_test_path)?;
319
320 let mut created_files = Vec::new();
321
322 if !config_path.exists() || force {
323 let config_content = Self::default_vtcode_toml_template();
324
325 fs::write(&config_path, config_content).with_context(|| {
326 format!("Failed to write config file: {}", config_path.display())
327 })?;
328
329 if let Some(file_name) = config_path.file_name().and_then(|name| name.to_str()) {
330 created_files.push(file_name.to_string());
331 }
332 }
333
334 if !gitignore_path.exists() || force {
335 let gitignore_content = Self::default_vtcode_gitignore();
336 fs::write(&gitignore_path, gitignore_content).with_context(|| {
337 format!(
338 "Failed to write gitignore file: {}",
339 gitignore_path.display()
340 )
341 })?;
342
343 if let Some(file_name) = gitignore_path.file_name().and_then(|name| name.to_str()) {
344 created_files.push(file_name.to_string());
345 }
346 }
347
348 if !vtcode_readme_path.exists() || force {
349 let vtcode_readme = Self::default_vtcode_readme_template();
350 fs::write(&vtcode_readme_path, vtcode_readme).with_context(|| {
351 format!(
352 "Failed to write VT Code README: {}",
353 vtcode_readme_path.display()
354 )
355 })?;
356 created_files.push(".vtcode/README.md".to_string());
357 }
358
359 let ast_grep_files = [
360 (
361 &ast_grep_config_path,
362 Self::default_ast_grep_config_template(),
363 "sgconfig.yml",
364 ),
365 (
366 &ast_grep_rule_path,
367 Self::default_ast_grep_example_rule_template(),
368 "rules/examples/no-console-log.yml",
369 ),
370 (
371 &ast_grep_test_path,
372 Self::default_ast_grep_example_test_template(),
373 "rule-tests/examples/no-console-log-test.yml",
374 ),
375 ];
376
377 for (path, contents, label) in ast_grep_files {
378 if !path.exists() || force {
379 fs::write(path, contents).with_context(|| {
380 format!("Failed to write ast-grep scaffold file: {}", path.display())
381 })?;
382 created_files.push(label.to_string());
383 }
384 }
385
386 Ok(created_files)
387 }
388
389 #[cfg(feature = "bootstrap")]
390 fn default_vtcode_toml_template() -> String {
392 r#"# VT Code Configuration File (Example)
393# Getting-started reference; see docs/config/CONFIGURATION_PRECEDENCE.md for override order.
394# Copy this file to vtcode.toml and customize as needed.
395
396# Clickable file citation URI scheme ("vscode", "cursor", "windsurf", "vscode-insiders", "none")
397file_opener = "none"
398
399# Additional fallback filenames to use when AGENTS.md is absent
400project_doc_fallback_filenames = []
401
402# Optional external command invoked after each completed agent turn
403notify = []
404
405# User-defined OpenAI-compatible providers
406custom_providers = []
407
408# [[custom_providers]]
409# name = "mycorp"
410# display_name = "MyCorporateName"
411# base_url = "https://llm.corp.example/v1"
412# api_key_env = "MYCORP_API_KEY"
413# model = "gpt-5-mini"
414
415[history]
416# Persist local session transcripts to disk
417persistence = "file"
418
419# Optional max size budget for each persisted session snapshot
420# max_bytes = 104857600
421
422[tui]
423# Enable all built-in TUI notifications, disable them, or restrict to specific event types.
424# notifications = true
425# notifications = ["agent-turn-complete", "approval-requested"]
426
427# Notification transport: "auto", "osc9", or "bel"
428# notification_method = "auto"
429
430# Set to false to reduce shimmer/animation effects
431# animations = true
432
433# Alternate-screen override: "always" or "never"
434# alternate_screen = "never"
435
436# Show onboarding hints on the welcome screen
437# show_tooltips = true
438
439# Core agent behavior; see docs/config/CONFIGURATION_PRECEDENCE.md.
440[agent]
441# Primary LLM provider to use (e.g., "openai", "gemini", "anthropic", "openrouter")
442provider = "openai"
443
444# Environment variable containing the API key for the provider
445api_key_env = "OPENAI_API_KEY"
446
447# Default model to use when no specific model is specified
448default_model = "gpt-5.4"
449
450# Visual theme for the terminal interface
451theme = "ciapre-dark"
452
453# Enable TODO planning helper mode for structured task management
454todo_planning_mode = true
455
456# UI surface to use ("auto", "alternate", "inline")
457ui_surface = "auto"
458
459# Maximum number of conversation turns before rotating context (affects memory usage)
460# Lower values reduce memory footprint but may lose context; higher values preserve context but use more memory
461max_conversation_turns = 150
462
463# Reasoning effort level ("none", "minimal", "low", "medium", "high", "xhigh") - affects model usage and response speed
464reasoning_effort = "none"
465
466# Temperature for main model responses (0.0-1.0)
467temperature = 0.7
468
469# Enable self-review loop to check and improve responses (increases API calls)
470enable_self_review = false
471
472# Maximum number of review passes when self-review is enabled
473max_review_passes = 1
474
475# Enable prompt refinement loop for improved prompt quality (increases processing time)
476refine_prompts_enabled = false
477
478# Maximum passes for prompt refinement when enabled
479refine_prompts_max_passes = 1
480
481# Optional alternate model for refinement (leave empty to use default)
482refine_prompts_model = ""
483
484# Maximum size of project documentation to include in context (in bytes)
485project_doc_max_bytes = 16384
486
487# Maximum size of instruction files to process (in bytes)
488instruction_max_bytes = 16384
489
490# List of additional instruction files to include in context
491instruction_files = []
492
493# Instruction files or globs to exclude from AGENTS/rules discovery
494instruction_excludes = []
495
496# Maximum recursive @import depth for instruction and rule files
497instruction_import_max_depth = 5
498
499# Durable per-repository memory for main sessions
500[agent.persistent_memory]
501enabled = false
502auto_write = true
503# directory_override = "/absolute/user-local/path"
504# Startup scan budget for the compact memory summary
505startup_line_limit = 200
506startup_byte_limit = 25600
507
508# Lightweight model helpers for lower-cost side tasks
509[agent.small_model]
510enabled = true
511model = ""
512temperature = 0.3
513use_for_large_reads = true
514use_for_web_summary = true
515use_for_git_history = true
516use_for_memory = true
517
518# Default editing mode on startup: "edit" or "plan"
519# "edit" - Full tool access for file modifications and command execution (default)
520# "plan" - Read-only mode that produces implementation plans without making changes
521# Toggle during session with Shift+Tab or /plan command
522default_editing_mode = "edit"
523
524# Inline prompt suggestions for the chat composer
525[agent.prompt_suggestions]
526# Enable Alt+P ghost-text suggestions in the composer
527enabled = true
528
529# Lightweight model for prompt suggestions (leave empty to auto-pick)
530model = ""
531
532# Lower values keep suggestions stable and completion-like
533temperature = 0.3
534
535# Show a one-time note that LLM-backed suggestions can consume tokens
536show_cost_notice = true
537
538# Onboarding configuration - Customize the startup experience
539[agent.onboarding]
540# Enable the onboarding welcome message on startup
541enabled = true
542
543# Custom introduction text shown on startup
544intro_text = "Let's get oriented. I preloaded workspace context so we can move fast."
545
546# Include project overview information in welcome
547include_project_overview = true
548
549# Include language summary information in welcome
550include_language_summary = false
551
552# Include key guideline highlights from AGENTS.md
553include_guideline_highlights = true
554
555# Include usage tips in the welcome message
556include_usage_tips_in_welcome = false
557
558# Include recommended actions in the welcome message
559include_recommended_actions_in_welcome = false
560
561# Maximum number of guideline highlights to show
562guideline_highlight_limit = 3
563
564# List of usage tips shown during onboarding
565usage_tips = [
566 "Describe your current coding goal or ask for a quick status overview.",
567 "Reference AGENTS.md guidelines when proposing changes.",
568 "Prefer asking for targeted file reads or diffs before editing.",
569]
570
571# List of recommended actions shown during onboarding
572recommended_actions = [
573 "Review the highlighted guidelines and share the task you want to tackle.",
574 "Ask for a workspace tour if you need more context.",
575]
576
577# Checkpointing configuration for session persistence
578[agent.checkpointing]
579# Enable automatic session checkpointing
580enabled = true
581
582# Maximum number of checkpoints to keep on disk
583max_snapshots = 50
584
585# Maximum age of checkpoints to keep (in days)
586max_age_days = 30
587
588# Tool security configuration
589[tools]
590# Default policy when no specific policy is defined ("allow", "prompt", "deny")
591# "allow" - Execute without confirmation
592# "prompt" - Ask for confirmation
593# "deny" - Block the tool
594default_policy = "prompt"
595
596# Maximum number of tool loops allowed per turn
597# Set to 0 to disable the limit and let other turn safeguards govern termination.
598max_tool_loops = 0
599
600# Maximum number of repeated identical tool calls (prevents stuck loops)
601max_repeated_tool_calls = 2
602
603# Maximum consecutive blocked tool calls before force-breaking the turn
604# Helps prevent high-CPU churn when calls are repeatedly denied/blocked
605max_consecutive_blocked_tool_calls_per_turn = 8
606
607# Maximum sequential spool-chunk reads per turn before nudging targeted extraction/summarization
608max_sequential_spool_chunk_reads = 6
609
610# Specific tool policies - Override default policy for individual tools
611[tools.policies]
612apply_patch = "prompt" # Apply code patches (requires confirmation)
613request_user_input = "allow" # Ask focused user questions when the task requires it
614task_tracker = "prompt" # Create or update explicit task plans
615unified_exec = "prompt" # Run commands; pipe-first by default, set tty=true for PTY/interactive sessions
616unified_file = "allow" # Canonical file read/write/edit/move/copy/delete surface
617unified_search = "allow" # Canonical search/list/intelligence/error surface
618
619# Command security - Define safe and dangerous command patterns
620[commands]
621# Commands that are always allowed without confirmation
622allow_list = [
623 "ls", # List directory contents
624 "pwd", # Print working directory
625 "git status", # Show git status
626 "git diff", # Show git differences
627 "cargo check", # Check Rust code
628 "echo", # Print text
629]
630
631# Commands that are never allowed
632deny_list = [
633 "rm -rf /", # Delete root directory (dangerous)
634 "rm -rf ~", # Delete home directory (dangerous)
635 "shutdown", # Shut down system (dangerous)
636 "reboot", # Reboot system (dangerous)
637 "sudo *", # Any sudo command (dangerous)
638 ":(){ :|:& };:", # Fork bomb (dangerous)
639]
640
641# Command patterns that are allowed (supports glob patterns)
642allow_glob = [
643 "git *", # All git commands
644 "cargo *", # All cargo commands
645 "python -m *", # Python module commands
646]
647
648# Command patterns that are denied (supports glob patterns)
649deny_glob = [
650 "rm *", # All rm commands
651 "sudo *", # All sudo commands
652 "chmod *", # All chmod commands
653 "chown *", # All chown commands
654 "kubectl *", # All kubectl commands (admin access)
655]
656
657# Regular expression patterns for allowed commands (if needed)
658allow_regex = []
659
660# Regular expression patterns for denied commands (if needed)
661deny_regex = []
662
663# Security configuration - Safety settings for automated operations
664[security]
665# Require human confirmation for potentially dangerous actions
666human_in_the_loop = true
667
668# Require explicit write tool usage for claims about file modifications
669require_write_tool_for_claims = true
670
671# Auto-apply patches without prompting (DANGEROUS - disable for safety)
672auto_apply_detected_patches = false
673
674# UI configuration - Terminal and display settings
675[ui]
676# Tool output display mode
677# "compact" - Concise tool output
678# "full" - Detailed tool output
679tool_output_mode = "compact"
680
681# Maximum number of lines to display in tool output (prevents transcript flooding)
682# Lines beyond this limit are truncated to a tail preview
683tool_output_max_lines = 600
684
685# Maximum bytes threshold for spooling tool output to disk
686# Output exceeding this size is written to .vtcode/tool-output/*.log
687tool_output_spool_bytes = 200000
688
689# Optional custom directory for spooled tool output logs
690# If not set, defaults to .vtcode/tool-output/
691# tool_output_spool_dir = "/path/to/custom/spool/dir"
692
693# Allow ANSI escape sequences in tool output (enables colors but may cause layout issues)
694allow_tool_ansi = false
695
696# Number of rows to allocate for inline UI viewport
697inline_viewport_rows = 16
698
699# Show elapsed time divider after each completed turn
700show_turn_timer = false
701
702# Show warning/error/fatal diagnostic lines directly in transcript
703# Effective in debug/development builds only
704show_diagnostics_in_transcript = false
705
706# Show timeline navigation panel
707show_timeline_pane = false
708
709# Runtime notification preferences
710[ui.notifications]
711# Master toggle for terminal/desktop notifications
712enabled = true
713
714# Delivery mode: "terminal", "hybrid", or "desktop"
715delivery_mode = "desktop"
716
717# Preferred desktop backend: "auto", "osascript", "notify_rust", or "terminal"
718backend = "auto"
719
720# Suppress notifications while terminal is focused
721suppress_when_focused = true
722
723# Failure/error notifications
724command_failure = false
725tool_failure = false
726error = true
727
728# Completion notifications
729# Legacy master toggle (fallback for split settings when unset)
730completion = true
731completion_success = false
732completion_failure = true
733
734# Human approval/interaction notifications
735hitl = true
736policy_approval = true
737request = false
738
739# Success notifications for tool call results
740tool_success = false
741
742# Repeated notification suppression
743repeat_window_seconds = 30
744max_identical_in_window = 1
745
746# Status line configuration
747[ui.status_line]
748# Status line mode ("auto", "command", "hidden")
749mode = "auto"
750
751# How often to refresh status line (milliseconds)
752refresh_interval_ms = 2000
753
754# Timeout for command execution in status line (milliseconds)
755command_timeout_ms = 200
756
757# Terminal title configuration
758[ui.terminal_title]
759# Ordered terminal title items (empty list disables VT Code-managed titles)
760items = ["spinner", "project"]
761
762# Enable Vim-style prompt editing in interactive mode
763vim_mode = false
764
765# PTY (Pseudo Terminal) configuration - For interactive command execution
766[pty]
767# Enable PTY support for interactive commands
768enabled = true
769
770# Default number of terminal rows for PTY sessions
771default_rows = 24
772
773# Default number of terminal columns for PTY sessions
774default_cols = 80
775
776# Maximum number of concurrent PTY sessions
777max_sessions = 10
778
779# Command timeout in seconds (prevents hanging commands)
780command_timeout_seconds = 300
781
782# Number of recent lines to show in PTY output
783stdout_tail_lines = 20
784
785# Total lines to keep in PTY scrollback buffer
786scrollback_lines = 400
787
788# Terminal emulation backend for PTY snapshots
789emulation_backend = "ghostty"
790
791# Optional preferred shell for PTY sessions (falls back to $SHELL when unset)
792# preferred_shell = "/bin/zsh"
793
794# Route shell execution through zsh EXEC_WRAPPER intercept hooks (feature-gated)
795shell_zsh_fork = false
796
797# Absolute path to patched zsh used when shell_zsh_fork is enabled
798# zsh_path = "/usr/local/bin/zsh"
799
800# Context management configuration - Controls conversation memory
801[context]
802# Maximum number of tokens to keep in context (affects model cost and performance)
803# Higher values preserve more context but cost more and may hit token limits
804max_context_tokens = 90000
805
806# Percentage to trim context to when it gets too large
807trim_to_percent = 60
808
809# Number of recent conversation turns to always preserve
810preserve_recent_turns = 6
811
812# Decision ledger configuration - Track important decisions
813[context.ledger]
814# Enable decision tracking and persistence
815enabled = true
816
817# Maximum number of decisions to keep in ledger
818max_entries = 12
819
820# Include ledger summary in model prompts
821include_in_prompt = true
822
823# Preserve ledger during context compression
824preserve_in_compression = true
825
826# AI model routing - Intelligent model selection
827# Telemetry and analytics
828[telemetry]
829# Enable trajectory logging for usage analysis
830trajectory_enabled = true
831
832# Syntax highlighting configuration
833[syntax_highlighting]
834# Enable syntax highlighting for code in tool output
835enabled = true
836
837# Theme for syntax highlighting
838theme = "base16-ocean.dark"
839
840# Cache syntax highlighting themes for performance
841cache_themes = true
842
843# Maximum file size for syntax highlighting (in MB)
844max_file_size_mb = 10
845
846# Programming languages to enable syntax highlighting for
847enabled_languages = [
848 "rust",
849 "python",
850 "javascript",
851 "typescript",
852 "go",
853 "java",
854 "bash",
855 "sh",
856 "shell",
857 "zsh",
858 "markdown",
859 "md",
860]
861
862# Timeout for syntax highlighting operations (milliseconds)
863highlight_timeout_ms = 1000
864
865# Automation features - Full-auto mode settings
866[automation.full_auto]
867# Enable full automation mode (DANGEROUS - requires careful oversight)
868enabled = false
869
870# Maximum number of turns before asking for human input
871max_turns = 30
872
873# Tools allowed in full automation mode
874allowed_tools = [
875 "write_file",
876 "read_file",
877 "list_files",
878 "grep_file",
879]
880
881# Require profile acknowledgment before using full auto
882require_profile_ack = true
883
884# Path to full auto profile configuration
885profile_path = "automation/full_auto_profile.toml"
886
887[automation.scheduled_tasks]
888# Enable /loop, cron tools, and durable `vtcode schedule` jobs
889enabled = false
890
891# Prompt caching - Cache model responses for efficiency
892[prompt_cache]
893# Enable prompt caching (reduces API calls for repeated prompts)
894enabled = true
895
896# Directory for cache storage
897cache_dir = "~/.vtcode/cache/prompts"
898
899# Maximum number of cache entries to keep
900max_entries = 1000
901
902# Maximum age of cache entries (in days)
903max_age_days = 30
904
905# Enable automatic cache cleanup
906enable_auto_cleanup = true
907
908# Minimum quality threshold to keep cache entries
909min_quality_threshold = 0.7
910
911# Keep volatile runtime counters at the end of system prompts to improve provider-side prefix cache reuse
912# (enabled by default; disable only if you need legacy prompt layout)
913cache_friendly_prompt_shaping = true
914
915# Prompt cache configuration for OpenAI
916 [prompt_cache.providers.openai]
917 enabled = true
918 min_prefix_tokens = 1024
919 idle_expiration_seconds = 3600
920 surface_metrics = true
921 # Routing key strategy for OpenAI prompt cache locality.
922 # "session" creates one stable key per VT Code conversation.
923 prompt_cache_key_mode = "session"
924 # Optional: server-side prompt cache retention for OpenAI Responses API
925 # Supported values: "in_memory" or "24h" (leave commented out for default behavior)
926 # prompt_cache_retention = "24h"
927
928# Prompt cache configuration for Anthropic
929[prompt_cache.providers.anthropic]
930enabled = true
931default_ttl_seconds = 300
932extended_ttl_seconds = 3600
933max_breakpoints = 4
934cache_system_messages = true
935cache_user_messages = true
936
937# Prompt cache configuration for Gemini
938[prompt_cache.providers.gemini]
939enabled = true
940mode = "implicit"
941min_prefix_tokens = 1024
942explicit_ttl_seconds = 3600
943
944# Prompt cache configuration for OpenRouter
945[prompt_cache.providers.openrouter]
946enabled = true
947propagate_provider_capabilities = true
948report_savings = true
949
950# Prompt cache configuration for Moonshot
951[prompt_cache.providers.moonshot]
952enabled = true
953
954# Prompt cache configuration for DeepSeek
955[prompt_cache.providers.deepseek]
956enabled = true
957surface_metrics = true
958
959# Prompt cache configuration for Z.AI
960[prompt_cache.providers.zai]
961enabled = false
962
963# Model Context Protocol (MCP) - Connect external tools and services
964[mcp]
965# Enable Model Context Protocol (may impact startup time if services unavailable)
966enabled = true
967max_concurrent_connections = 5
968request_timeout_seconds = 30
969retry_attempts = 3
970
971# MCP UI configuration
972[mcp.ui]
973mode = "compact"
974max_events = 50
975show_provider_names = true
976
977# MCP renderer profiles for different services
978[mcp.ui.renderers]
979sequential-thinking = "sequential-thinking"
980context7 = "context7"
981
982# MCP provider configuration - External services that connect via MCP
983[[mcp.providers]]
984name = "time"
985command = "uvx"
986args = ["mcp-server-time"]
987enabled = true
988max_concurrent_requests = 3
989[mcp.providers.env]
990
991# Agent Client Protocol (ACP) - IDE integration
992[acp]
993enabled = true
994
995[acp.zed]
996enabled = true
997transport = "stdio"
998# workspace_trust controls ACP trust mode: "tools_policy" (prompts) or "full_auto" (no prompts)
999workspace_trust = "full_auto"
1000
1001[acp.zed.tools]
1002read_file = true
1003list_files = true
1004
1005# Cross-IDE editor context bridge
1006[ide_context]
1007enabled = true
1008inject_into_prompt = true
1009show_in_tui = true
1010include_selection_text = true
1011provider_mode = "auto"
1012
1013[ide_context.providers.vscode_compatible]
1014enabled = true
1015
1016[ide_context.providers.zed]
1017enabled = true
1018
1019[ide_context.providers.generic]
1020enabled = true"#.to_string()
1021 }
1022
1023 #[cfg(feature = "bootstrap")]
1024 fn default_vtcode_gitignore() -> String {
1025 r#"# Security-focused exclusions
1026.env, .env.local, secrets/, .aws/, .ssh/
1027
1028# Development artifacts
1029target/, build/, dist/, node_modules/, vendor/
1030
1031# Database files
1032*.db, *.sqlite, *.sqlite3
1033
1034# Binary files
1035*.exe, *.dll, *.so, *.dylib, *.bin
1036
1037# IDE files (comprehensive)
1038.vscode/, .idea/, *.swp, *.swo
1039"#
1040 .to_string()
1041 }
1042
1043 #[cfg(feature = "bootstrap")]
1044 fn default_vtcode_readme_template() -> &'static str {
1045 "# VT Code Workspace Files\n\n- Put always-on repository guidance in `AGENTS.md`.\n- Put path-scoped prompt rules in `.vtcode/rules/*.md` using YAML frontmatter.\n- Keep authoring notes and other workspace docs outside `.vtcode/rules/` so they are not loaded into prompt memory.\n"
1046 }
1047
1048 #[cfg(feature = "bootstrap")]
1049 fn default_ast_grep_config_template() -> &'static str {
1050 "ruleDirs:\n - rules\ntestConfigs:\n - testDir: rule-tests\n snapshotDir: __snapshots__\n"
1051 }
1052
1053 #[cfg(feature = "bootstrap")]
1054 fn default_ast_grep_example_rule_template() -> &'static str {
1055 "id: no-console-log\nlanguage: JavaScript\nseverity: error\nmessage: Avoid `console.log` in checked JavaScript files.\nnote: |\n This starter rule is scoped to `__ast_grep_examples__/` so fresh repositories can\n validate the scaffold without scanning unrelated project files.\nrule:\n pattern: console.log($$$ARGS)\nfiles:\n - __ast_grep_examples__/**/*.js\n"
1056 }
1057
1058 #[cfg(feature = "bootstrap")]
1059 fn default_ast_grep_example_test_template() -> &'static str {
1060 "id: no-console-log\nvalid:\n - |\n const logger = {\n info(message) {\n return message;\n },\n };\ninvalid:\n - |\n function greet(name) {\n console.log(name);\n }\n"
1061 }
1062
1063 #[cfg(feature = "bootstrap")]
1064 pub fn create_sample_config<P: AsRef<Path>>(output: P) -> Result<()> {
1066 let output = output.as_ref();
1067 let config_content = Self::default_vtcode_toml_template();
1068
1069 fs::write(output, config_content)
1070 .with_context(|| format!("Failed to write config file: {}", output.display()))?;
1071
1072 Ok(())
1073 }
1074}
1075
1076#[cfg(test)]
1077mod tests {
1078 use super::VTCodeConfig;
1079 use tempfile::tempdir;
1080
1081 #[cfg(feature = "bootstrap")]
1082 #[test]
1083 fn bootstrap_project_creates_vtcode_readme() {
1084 let workspace = tempdir().expect("workspace");
1085 let created = VTCodeConfig::bootstrap_project(workspace.path(), false)
1086 .expect("bootstrap project should succeed");
1087
1088 assert!(
1089 created.iter().any(|entry| entry == ".vtcode/README.md"),
1090 "created files: {:?}",
1091 created
1092 );
1093 assert!(workspace.path().join(".vtcode/README.md").exists());
1094 }
1095
1096 #[cfg(feature = "bootstrap")]
1097 #[test]
1098 fn bootstrap_project_creates_ast_grep_scaffold() {
1099 let workspace = tempdir().expect("workspace");
1100 let created = VTCodeConfig::bootstrap_project(workspace.path(), false)
1101 .expect("bootstrap project should succeed");
1102
1103 assert!(created.iter().any(|entry| entry == "sgconfig.yml"));
1104 assert!(
1105 created
1106 .iter()
1107 .any(|entry| entry == "rules/examples/no-console-log.yml")
1108 );
1109 assert!(
1110 created
1111 .iter()
1112 .any(|entry| entry == "rule-tests/examples/no-console-log-test.yml")
1113 );
1114
1115 assert!(workspace.path().join("sgconfig.yml").exists());
1116 assert!(
1117 workspace
1118 .path()
1119 .join("rules/examples/no-console-log.yml")
1120 .exists()
1121 );
1122 assert!(
1123 workspace
1124 .path()
1125 .join("rule-tests/examples/no-console-log-test.yml")
1126 .exists()
1127 );
1128 }
1129
1130 #[cfg(feature = "bootstrap")]
1131 #[test]
1132 fn bootstrap_project_preserves_existing_ast_grep_files_without_force() {
1133 let workspace = tempdir().expect("workspace");
1134 let sgconfig_path = workspace.path().join("sgconfig.yml");
1135 let rule_path = workspace.path().join("rules/examples/no-console-log.yml");
1136
1137 std::fs::create_dir_all(workspace.path().join("rules/examples")).expect("create rules dir");
1138 std::fs::write(&sgconfig_path, "ruleDirs:\n - custom-rules\n").expect("write sgconfig");
1139 std::fs::write(&rule_path, "id: custom-rule\n").expect("write rule");
1140
1141 let created = VTCodeConfig::bootstrap_project(workspace.path(), false)
1142 .expect("bootstrap project should succeed");
1143
1144 assert!(
1145 !created.iter().any(|entry| entry == "sgconfig.yml"),
1146 "created files: {created:?}"
1147 );
1148 assert!(
1149 !created
1150 .iter()
1151 .any(|entry| entry == "rules/examples/no-console-log.yml"),
1152 "created files: {created:?}"
1153 );
1154 assert_eq!(
1155 std::fs::read_to_string(&sgconfig_path).expect("read sgconfig"),
1156 "ruleDirs:\n - custom-rules\n"
1157 );
1158 assert_eq!(
1159 std::fs::read_to_string(&rule_path).expect("read rule"),
1160 "id: custom-rule\n"
1161 );
1162 }
1163}