vtcode_config/loader/
mod.rs

1#[cfg(feature = "bootstrap")]
2pub mod bootstrap;
3
4use crate::acp::AgentClientProtocolConfig;
5use crate::context::ContextFeaturesConfig;
6use crate::core::{
7    AgentConfig, AutomationConfig, CommandsConfig, PromptCachingConfig, SecurityConfig, ToolsConfig,
8};
9use crate::defaults::{self, ConfigDefaultsProvider, SyntaxHighlightingDefaults};
10use crate::hooks::HooksConfig;
11use crate::mcp::McpClientConfig;
12use crate::root::{PtyConfig, UiConfig};
13use crate::router::RouterConfig;
14use crate::telemetry::TelemetryConfig;
15use anyhow::{Context, Result, ensure};
16use serde::{Deserialize, Serialize};
17use std::fs;
18use std::path::{Path, PathBuf};
19
20/// Syntax highlighting configuration
21#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
22#[derive(Debug, Clone, Deserialize, Serialize)]
23pub struct SyntaxHighlightingConfig {
24    /// Enable syntax highlighting for tool output
25    #[serde(default = "defaults::syntax_highlighting::enabled")]
26    pub enabled: bool,
27
28    /// Theme to use for syntax highlighting
29    #[serde(default = "defaults::syntax_highlighting::theme")]
30    pub theme: String,
31
32    /// Enable theme caching for better performance
33    #[serde(default = "defaults::syntax_highlighting::cache_themes")]
34    pub cache_themes: bool,
35
36    /// Maximum file size for syntax highlighting (in MB)
37    #[serde(default = "defaults::syntax_highlighting::max_file_size_mb")]
38    pub max_file_size_mb: usize,
39
40    /// Languages to enable syntax highlighting for
41    #[serde(default = "defaults::syntax_highlighting::enabled_languages")]
42    pub enabled_languages: Vec<String>,
43
44    /// Performance settings - highlight timeout in milliseconds
45    #[serde(default = "defaults::syntax_highlighting::highlight_timeout_ms")]
46    pub highlight_timeout_ms: u64,
47}
48
49impl Default for SyntaxHighlightingConfig {
50    fn default() -> Self {
51        Self {
52            enabled: defaults::syntax_highlighting::enabled(),
53            theme: defaults::syntax_highlighting::theme(),
54            cache_themes: defaults::syntax_highlighting::cache_themes(),
55            max_file_size_mb: defaults::syntax_highlighting::max_file_size_mb(),
56            enabled_languages: defaults::syntax_highlighting::enabled_languages(),
57            highlight_timeout_ms: defaults::syntax_highlighting::highlight_timeout_ms(),
58        }
59    }
60}
61
62impl SyntaxHighlightingConfig {
63    pub fn validate(&self) -> Result<()> {
64        if !self.enabled {
65            return Ok(());
66        }
67
68        ensure!(
69            self.max_file_size_mb >= SyntaxHighlightingDefaults::min_file_size_mb(),
70            "Syntax highlighting max_file_size_mb must be at least {} MB",
71            SyntaxHighlightingDefaults::min_file_size_mb()
72        );
73
74        ensure!(
75            self.highlight_timeout_ms >= SyntaxHighlightingDefaults::min_highlight_timeout_ms(),
76            "Syntax highlighting highlight_timeout_ms must be at least {} ms",
77            SyntaxHighlightingDefaults::min_highlight_timeout_ms()
78        );
79
80        ensure!(
81            !self.theme.trim().is_empty(),
82            "Syntax highlighting theme must not be empty"
83        );
84
85        ensure!(
86            self.enabled_languages
87                .iter()
88                .all(|lang| !lang.trim().is_empty()),
89            "Syntax highlighting languages must not contain empty entries"
90        );
91
92        Ok(())
93    }
94}
95
96/// Main configuration structure for VTCode
97#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
98#[derive(Debug, Clone, Deserialize, Serialize, Default)]
99pub struct VTCodeConfig {
100    /// Agent-wide settings
101    #[serde(default)]
102    pub agent: AgentConfig,
103
104    /// Tool execution policies
105    #[serde(default)]
106    pub tools: ToolsConfig,
107
108    /// Unix command permissions
109    #[serde(default)]
110    pub commands: CommandsConfig,
111
112    /// Security settings
113    #[serde(default)]
114    pub security: SecurityConfig,
115
116    /// UI settings
117    #[serde(default)]
118    pub ui: UiConfig,
119
120    /// PTY settings
121    #[serde(default)]
122    pub pty: PtyConfig,
123
124    /// Context features (e.g., Decision Ledger)
125    #[serde(default)]
126    pub context: ContextFeaturesConfig,
127
128    /// Router configuration (dynamic model + engine selection)
129    #[serde(default)]
130    pub router: RouterConfig,
131
132    /// Telemetry configuration (logging, trajectory)
133    #[serde(default)]
134    pub telemetry: TelemetryConfig,
135
136    /// Syntax highlighting configuration
137    #[serde(default)]
138    pub syntax_highlighting: SyntaxHighlightingConfig,
139
140    /// Automation configuration
141    #[serde(default)]
142    pub automation: AutomationConfig,
143
144    /// Prompt cache configuration (local + provider integration)
145    #[serde(default)]
146    pub prompt_cache: PromptCachingConfig,
147
148    /// Model Context Protocol configuration
149    #[serde(default)]
150    pub mcp: McpClientConfig,
151
152    /// Agent Client Protocol configuration
153    #[serde(default)]
154    pub acp: AgentClientProtocolConfig,
155
156    /// Lifecycle hooks configuration
157    #[serde(default)]
158    pub hooks: HooksConfig,
159}
160
161impl VTCodeConfig {
162    pub fn validate(&self) -> Result<()> {
163        self.syntax_highlighting
164            .validate()
165            .context("Invalid syntax_highlighting configuration")?;
166
167        self.context
168            .validate()
169            .context("Invalid context configuration")?;
170
171        self.router
172            .validate()
173            .context("Invalid router configuration")?;
174
175        self.hooks
176            .validate()
177            .context("Invalid hooks configuration")?;
178
179        Ok(())
180    }
181
182    #[cfg(feature = "bootstrap")]
183    /// Bootstrap project with config + gitignore
184    pub fn bootstrap_project<P: AsRef<Path>>(workspace: P, force: bool) -> Result<Vec<String>> {
185        Self::bootstrap_project_with_options(workspace, force, false)
186    }
187
188    #[cfg(feature = "bootstrap")]
189    /// Bootstrap project with config + gitignore, with option to create in home directory
190    pub fn bootstrap_project_with_options<P: AsRef<Path>>(
191        workspace: P,
192        force: bool,
193        use_home_dir: bool,
194    ) -> Result<Vec<String>> {
195        let workspace = workspace.as_ref().to_path_buf();
196        defaults::with_config_defaults(|provider| {
197            Self::bootstrap_project_with_provider(&workspace, force, use_home_dir, provider)
198        })
199    }
200
201    #[cfg(feature = "bootstrap")]
202    /// Bootstrap project files using the supplied [`ConfigDefaultsProvider`].
203    pub fn bootstrap_project_with_provider<P: AsRef<Path>>(
204        workspace: P,
205        force: bool,
206        use_home_dir: bool,
207        defaults_provider: &dyn ConfigDefaultsProvider,
208    ) -> Result<Vec<String>> {
209        let workspace = workspace.as_ref();
210        let config_file_name = defaults_provider.config_file_name().to_string();
211        let (config_path, gitignore_path) = bootstrap::determine_bootstrap_targets(
212            workspace,
213            use_home_dir,
214            &config_file_name,
215            defaults_provider,
216        )?;
217
218        bootstrap::ensure_parent_dir(&config_path)?;
219        bootstrap::ensure_parent_dir(&gitignore_path)?;
220
221        let mut created_files = Vec::new();
222
223        if !config_path.exists() || force {
224            let config_content = Self::default_vtcode_toml_template();
225
226            fs::write(&config_path, config_content).with_context(|| {
227                format!("Failed to write config file: {}", config_path.display())
228            })?;
229
230            if let Some(file_name) = config_path.file_name().and_then(|name| name.to_str()) {
231                created_files.push(file_name.to_string());
232            }
233        }
234
235        if !gitignore_path.exists() || force {
236            let gitignore_content = Self::default_vtcode_gitignore();
237            fs::write(&gitignore_path, gitignore_content).with_context(|| {
238                format!(
239                    "Failed to write gitignore file: {}",
240                    gitignore_path.display()
241                )
242            })?;
243
244            if let Some(file_name) = gitignore_path.file_name().and_then(|name| name.to_str()) {
245                created_files.push(file_name.to_string());
246            }
247        }
248
249        Ok(created_files)
250    }
251
252    #[cfg(feature = "bootstrap")]
253    /// Generate the default `vtcode.toml` template used by bootstrap helpers.
254    fn default_vtcode_toml_template() -> String {
255        r#"# VTCode Configuration File (Example)
256# Getting-started reference; see docs/config/CONFIGURATION_PRECEDENCE.md for override order.
257# Copy this file to vtcode.toml and customize as needed.
258
259# Core agent behavior; see docs/config/CONFIGURATION_PRECEDENCE.md.
260[agent]
261# Primary LLM provider to use (e.g., "openai", "gemini", "anthropic", "openrouter")
262provider = "openai"
263
264# Environment variable containing the API key for the provider
265api_key_env = "OPENAI_API_KEY"
266
267# Default model to use when no specific model is specified
268default_model = "gpt-5-nano"
269
270# Visual theme for the terminal interface
271theme = "ciapre-dark"
272
273# Enable TODO planning helper mode for structured task management
274todo_planning_mode = true
275
276# UI surface to use ("auto", "alternate", "inline")
277ui_surface = "auto"
278
279# Maximum number of conversation turns before rotating context (affects memory usage)
280# Lower values reduce memory footprint but may lose context; higher values preserve context but use more memory
281max_conversation_turns = 50
282
283# Reasoning effort level ("low", "medium", "high") - affects model usage and response speed
284reasoning_effort = "low"
285
286# Enable self-review loop to check and improve responses (increases API calls)
287enable_self_review = false
288
289# Maximum number of review passes when self-review is enabled
290max_review_passes = 1
291
292# Enable prompt refinement loop for improved prompt quality (increases processing time)
293refine_prompts_enabled = false
294
295# Maximum passes for prompt refinement when enabled
296refine_prompts_max_passes = 1
297
298# Optional alternate model for refinement (leave empty to use default)
299refine_prompts_model = ""
300
301# Maximum size of project documentation to include in context (in bytes)
302project_doc_max_bytes = 16384
303
304# Maximum size of instruction files to process (in bytes)
305instruction_max_bytes = 16384
306
307# List of additional instruction files to include in context
308instruction_files = []
309
310# Onboarding configuration - Customize the startup experience
311[agent.onboarding]
312# Enable the onboarding welcome message on startup
313enabled = true
314
315# Custom introduction text shown on startup
316intro_text = "Let's get oriented. I preloaded workspace context so we can move fast."
317
318# Include project overview information in welcome
319include_project_overview = true
320
321# Include language summary information in welcome
322include_language_summary = false
323
324# Include key guideline highlights from AGENTS.md
325include_guideline_highlights = true
326
327# Include usage tips in the welcome message
328include_usage_tips_in_welcome = false
329
330# Include recommended actions in the welcome message
331include_recommended_actions_in_welcome = false
332
333# Maximum number of guideline highlights to show
334guideline_highlight_limit = 3
335
336# List of usage tips shown during onboarding
337usage_tips = [
338    "Describe your current coding goal or ask for a quick status overview.",
339    "Reference AGENTS.md guidelines when proposing changes.",
340    "Draft or refresh your TODO list with update_plan before coding.",
341    "Prefer asking for targeted file reads or diffs before editing.",
342]
343
344# List of recommended actions shown during onboarding
345recommended_actions = [
346    "Start the session by outlining a 3–6 step TODO plan via update_plan.",
347    "Review the highlighted guidelines and share the task you want to tackle.",
348    "Ask for a workspace tour if you need more context.",
349]
350
351# Custom prompts configuration - Define personal assistant commands
352[agent.custom_prompts]
353# Enable the custom prompts feature with /prompt:<name> syntax
354enabled = true
355
356# Directory where custom prompt files are stored
357directory = "~/.vtcode/prompts"
358
359# Additional directories to search for custom prompts
360extra_directories = []
361
362# Maximum file size for custom prompts (in kilobytes)
363max_file_size_kb = 64
364
365# Custom API keys for specific providers
366[agent.custom_api_keys]
367# Moonshot AI API key (for specific provider access)
368moonshot = "sk-sDj3JUXDbfARCYKNL4q7iGWRtWuhL1M4O6zzgtDpN3Yxt9EA"
369
370# Checkpointing configuration for session persistence
371[agent.checkpointing]
372# Enable automatic session checkpointing
373enabled = false
374
375# Maximum number of checkpoints to keep on disk
376max_snapshots = 50
377
378# Maximum age of checkpoints to keep (in days)
379max_age_days = 30
380
381# Tool security configuration
382[tools]
383# Default policy when no specific policy is defined ("allow", "prompt", "deny")
384# "allow" - Execute without confirmation
385# "prompt" - Ask for confirmation
386# "deny" - Block the tool
387default_policy = "prompt"
388
389# Maximum number of tool loops allowed per turn (prevents infinite loops)
390# Higher values allow more complex operations but risk performance issues
391# Recommended: 20 for most tasks, 50 for complex multi-step workflows
392max_tool_loops = 20
393
394# Maximum number of repeated identical tool calls (prevents stuck loops)
395max_repeated_tool_calls = 2
396
397# Specific tool policies - Override default policy for individual tools
398[tools.policies]
399apply_patch = "prompt"            # Apply code patches (requires confirmation)
400ast_grep_search = "allow"         # AST-based code search (no confirmation needed)
401bash = "prompt"                   # Execute bash commands (requires confirmation)
402close_pty_session = "allow"        # Close PTY sessions (no confirmation needed)
403create_pty_session = "allow"       # Create PTY sessions (no confirmation needed)
404curl = "prompt"                   # HTTP requests (requires confirmation)
405edit_file = "allow"               # Edit files directly (no confirmation needed)
406git_diff = "allow"                # Git diff operations (no confirmation needed)
407grep_file = "allow"               # Sole content-search tool (ripgrep-backed)
408list_files = "allow"              # List directory contents (no confirmation needed)
409list_pty_sessions = "allow"       # List PTY sessions (no confirmation needed)
410read_file = "allow"               # Read files (no confirmation needed)
411read_pty_session = "allow"        # Read PTY session output (no no confirmation needed)
412resize_pty_session = "allow"      # Resize PTY sessions (no confirmation needed)
413run_pty_cmd = "prompt"            # Run commands in PTY (requires confirmation)
414run_terminal_cmd = "prompt"       # Run terminal commands (requires confirmation)
415send_pty_input = "prompt"         # Send input to PTY (requires confirmation)
416update_plan = "allow"             # Update task plans (no confirmation needed)
417write_file = "allow"              # Write files (no confirmation needed)
418
419# Command security - Define safe and dangerous command patterns
420[commands]
421# Commands that are always allowed without confirmation
422allow_list = [
423    "ls",           # List directory contents
424    "pwd",          # Print working directory
425    "git status",   # Show git status
426    "git diff",     # Show git differences
427    "cargo check",  # Check Rust code
428    "echo",         # Print text
429]
430
431# Commands that are never allowed
432deny_list = [
433    "rm -rf /",        # Delete root directory (dangerous)
434    "rm -rf ~",        # Delete home directory (dangerous)
435    "shutdown",        # Shut down system (dangerous)
436    "reboot",          # Reboot system (dangerous)
437    "sudo *",          # Any sudo command (dangerous)
438    ":(){ :|:& };:",   # Fork bomb (dangerous)
439]
440
441# Command patterns that are allowed (supports glob patterns)
442allow_glob = [
443    "git *",        # All git commands
444    "cargo *",      # All cargo commands
445    "python -m *",  # Python module commands
446]
447
448# Command patterns that are denied (supports glob patterns)
449deny_glob = [
450    "rm *",         # All rm commands
451    "sudo *",       # All sudo commands
452    "chmod *",      # All chmod commands
453    "chown *",      # All chown commands
454    "kubectl *",    # All kubectl commands (admin access)
455]
456
457# Regular expression patterns for allowed commands (if needed)
458allow_regex = []
459
460# Regular expression patterns for denied commands (if needed)
461deny_regex = []
462
463# Security configuration - Safety settings for automated operations
464[security]
465# Require human confirmation for potentially dangerous actions
466human_in_the_loop = true
467
468# Require explicit write tool usage for claims about file modifications
469require_write_tool_for_claims = true
470
471# Auto-apply patches without prompting (DANGEROUS - disable for safety)
472auto_apply_detected_patches = false
473
474# UI configuration - Terminal and display settings
475[ui]
476# Tool output display mode
477# "compact" - Concise tool output
478# "full" - Detailed tool output
479tool_output_mode = "compact"
480
481# Maximum number of lines to display in tool output (prevents transcript flooding)
482# Lines beyond this limit are truncated to a tail preview
483tool_output_max_lines = 600
484
485# Maximum bytes threshold for spooling tool output to disk
486# Output exceeding this size is written to .vtcode/tool-output/*.log
487tool_output_spool_bytes = 200000
488
489# Optional custom directory for spooled tool output logs
490# If not set, defaults to .vtcode/tool-output/
491# tool_output_spool_dir = "/path/to/custom/spool/dir"
492
493# Allow ANSI escape sequences in tool output (enables colors but may cause layout issues)
494allow_tool_ansi = false
495
496# Number of rows to allocate for inline UI viewport
497inline_viewport_rows = 16
498
499# Show timeline navigation panel
500show_timeline_pane = false
501
502# Status line configuration
503[ui.status_line]
504# Status line mode ("auto", "command", "hidden")
505mode = "auto"
506
507# How often to refresh status line (milliseconds)
508refresh_interval_ms = 2000
509
510# Timeout for command execution in status line (milliseconds)
511command_timeout_ms = 200
512
513# PTY (Pseudo Terminal) configuration - For interactive command execution
514[pty]
515# Enable PTY support for interactive commands
516enabled = true
517
518# Default number of terminal rows for PTY sessions
519default_rows = 24
520
521# Default number of terminal columns for PTY sessions
522default_cols = 80
523
524# Maximum number of concurrent PTY sessions
525max_sessions = 10
526
527# Command timeout in seconds (prevents hanging commands)
528command_timeout_seconds = 300
529
530# Number of recent lines to show in PTY output
531stdout_tail_lines = 20
532
533# Total lines to keep in PTY scrollback buffer
534scrollback_lines = 400
535
536# Context management configuration - Controls conversation memory
537[context]
538# Maximum number of tokens to keep in context (affects model cost and performance)
539# Higher values preserve more context but cost more and may hit token limits
540max_context_tokens = 90000
541
542# Percentage to trim context to when it gets too large
543trim_to_percent = 60
544
545# Number of recent conversation turns to always preserve
546preserve_recent_turns = 6
547
548# Decision ledger configuration - Track important decisions
549[context.ledger]
550# Enable decision tracking and persistence
551enabled = true
552
553# Maximum number of decisions to keep in ledger
554max_entries = 12
555
556# Include ledger summary in model prompts
557include_in_prompt = true
558
559# Preserve ledger during context compression
560preserve_in_compression = true
561
562# Token budget management - Track and limit token usage
563[context.token_budget]
564# Enable token usage tracking and budget enforcement
565enabled = false
566
567# Model to use for token counting (must match your actual model)
568model = "gpt-5-nano"
569
570# Percentage threshold to warn about token usage (0.75 = 75%)
571warning_threshold = 0.75
572
573# Percentage threshold to trigger context compaction (0.85 = 85%)
574compaction_threshold = 0.85
575
576# Enable detailed component-level token tracking (increases overhead)
577detailed_tracking = false
578
579# Context curation - Intelligent context management
580[context.curation]
581# Enable automatic context curation (filters and optimizes context)
582enabled = false
583
584# Maximum tokens to allow per turn after curation
585max_tokens_per_turn = 50000
586
587# Number of recent messages to always preserve
588preserve_recent_messages = 5
589
590# Maximum number of tool descriptions to keep in context
591max_tool_descriptions = 10
592
593# Include decision ledger in curation
594include_ledger = true
595
596# Maximum ledger entries to include in curation
597ledger_max_entries = 12
598
599# Include recent error messages in context
600include_recent_errors = true
601
602# Maximum recent errors to include
603max_recent_errors = 3
604
605# AI model routing - Intelligent model selection
606[router]
607# Enable intelligent model routing
608enabled = true
609
610# Enable heuristic-based model selection
611heuristic_classification = true
612
613# Optional override model for routing decisions (empty = use default)
614llm_router_model = ""
615
616# Model mapping for different task types
617[router.models]
618# Model for simple queries
619simple = "gpt-5-nano"
620# Model for standard tasks
621standard = "gpt-5-nano"
622# Model for complex tasks
623complex = "gpt-5-nano"
624# Model for code generation heavy tasks
625codegen_heavy = "gpt-5-nano"
626# Model for information retrieval heavy tasks
627retrieval_heavy = "gpt-5-nano"
628
629# Router budget settings (if applicable)
630[router.budgets]
631
632# Router heuristic patterns for task classification
633[router.heuristics]
634# Maximum characters for short requests
635short_request_max_chars = 120
636# Minimum characters for long requests
637long_request_min_chars = 1200
638
639# Text patterns that indicate code patch operations
640code_patch_markers = [
641    "```",
642    "diff --git",
643    "apply_patch",
644    "unified diff",
645    "patch",
646    "edit_file",
647    "create_file",
648]
649
650# Text patterns that indicate information retrieval
651retrieval_markers = [
652    "search",
653    "web",
654    "google",
655    "docs",
656    "cite",
657    "source",
658    "up-to-date",
659]
660
661# Text patterns that indicate complex multi-step tasks
662complex_markers = [
663    "plan",
664    "multi-step",
665    "decompose",
666    "orchestrate",
667    "architecture",
668    "benchmark",
669    "implement end-to-end",
670    "design api",
671    "refactor module",
672    "evaluate",
673    "tests suite",
674]
675
676# Telemetry and analytics
677[telemetry]
678# Enable trajectory logging for usage analysis
679trajectory_enabled = true
680
681# Syntax highlighting configuration
682[syntax_highlighting]
683# Enable syntax highlighting for code in tool output
684enabled = true
685
686# Theme for syntax highlighting
687theme = "base16-ocean.dark"
688
689# Cache syntax highlighting themes for performance
690cache_themes = true
691
692# Maximum file size for syntax highlighting (in MB)
693max_file_size_mb = 10
694
695# Programming languages to enable syntax highlighting for
696enabled_languages = [
697    "rust",
698    "python",
699    "javascript",
700    "typescript",
701    "go",
702    "java",
703]
704
705# Timeout for syntax highlighting operations (milliseconds)
706highlight_timeout_ms = 1000
707
708# Automation features - Full-auto mode settings
709[automation.full_auto]
710# Enable full automation mode (DANGEROUS - requires careful oversight)
711enabled = false
712
713# Maximum number of turns before asking for human input
714max_turns = 30
715
716# Tools allowed in full automation mode
717allowed_tools = [
718    "write_file",
719    "read_file",
720    "list_files",
721    "grep_file",
722]
723
724# Require profile acknowledgment before using full auto
725require_profile_ack = true
726
727# Path to full auto profile configuration
728profile_path = "automation/full_auto_profile.toml"
729
730# Prompt caching - Cache model responses for efficiency
731[prompt_cache]
732# Enable prompt caching (reduces API calls for repeated prompts)
733enabled = false
734
735# Directory for cache storage
736cache_dir = "~/.vtcode/cache/prompts"
737
738# Maximum number of cache entries to keep
739max_entries = 1000
740
741# Maximum age of cache entries (in days)
742max_age_days = 30
743
744# Enable automatic cache cleanup
745enable_auto_cleanup = true
746
747# Minimum quality threshold to keep cache entries
748min_quality_threshold = 0.7
749
750# Prompt cache configuration for OpenAI
751[prompt_cache.providers.openai]
752enabled = true
753min_prefix_tokens = 1024
754idle_expiration_seconds = 3600
755surface_metrics = true
756
757# Prompt cache configuration for Anthropic
758[prompt_cache.providers.anthropic]
759enabled = true
760default_ttl_seconds = 300
761extended_ttl_seconds = 3600
762max_breakpoints = 4
763cache_system_messages = true
764cache_user_messages = true
765
766# Prompt cache configuration for Gemini
767[prompt_cache.providers.gemini]
768enabled = true
769mode = "implicit"
770min_prefix_tokens = 1024
771explicit_ttl_seconds = 3600
772
773# Prompt cache configuration for OpenRouter
774[prompt_cache.providers.openrouter]
775enabled = true
776propagate_provider_capabilities = true
777report_savings = true
778
779# Prompt cache configuration for Moonshot
780[prompt_cache.providers.moonshot]
781enabled = true
782
783# Prompt cache configuration for xAI
784[prompt_cache.providers.xai]
785enabled = true
786
787# Prompt cache configuration for DeepSeek
788[prompt_cache.providers.deepseek]
789enabled = true
790surface_metrics = true
791
792# Prompt cache configuration for Z.AI
793[prompt_cache.providers.zai]
794enabled = false
795
796# Model Context Protocol (MCP) - Connect external tools and services
797[mcp]
798# Enable Model Context Protocol (may impact startup time if services unavailable)
799enabled = true
800max_concurrent_connections = 5
801request_timeout_seconds = 30
802retry_attempts = 3
803
804# MCP UI configuration
805[mcp.ui]
806mode = "compact"
807max_events = 50
808show_provider_names = true
809
810# MCP renderer profiles for different services
811[mcp.ui.renderers]
812sequential-thinking = "sequential-thinking"
813context7 = "context7"
814
815# MCP provider configuration - External services that connect via MCP
816[[mcp.providers]]
817name = "time"
818command = "uvx"
819args = ["mcp-server-time"]
820enabled = true
821max_concurrent_requests = 3
822[mcp.providers.env]
823
824# Agent Client Protocol (ACP) - IDE integration
825[acp]
826enabled = true
827
828[acp.zed]
829enabled = true
830transport = "stdio"
831workspace_trust = "full_auto"
832
833[acp.zed.tools]
834read_file = true
835list_files = true"#.to_string()
836    }
837
838    #[cfg(feature = "bootstrap")]
839    fn default_vtcode_gitignore() -> String {
840        r#"# Security-focused exclusions
841.env, .env.local, secrets/, .aws/, .ssh/
842
843# Development artifacts
844target/, build/, dist/, node_modules/, vendor/
845
846# Database files
847*.db, *.sqlite, *.sqlite3
848
849# Binary files
850*.exe, *.dll, *.so, *.dylib, *.bin
851
852# IDE files (comprehensive)
853.vscode/, .idea/, *.swp, *.swo
854"#
855        .to_string()
856    }
857
858    #[cfg(feature = "bootstrap")]
859    /// Create sample configuration file
860    pub fn create_sample_config<P: AsRef<Path>>(output: P) -> Result<()> {
861        let output = output.as_ref();
862        let config_content = Self::default_vtcode_toml_template();
863
864        fs::write(output, config_content)
865            .with_context(|| format!("Failed to write config file: {}", output.display()))?;
866
867        Ok(())
868    }
869}
870
871/// Configuration manager for loading and validating configurations
872#[derive(Clone)]
873pub struct ConfigManager {
874    config: VTCodeConfig,
875    config_path: Option<PathBuf>,
876    workspace_root: Option<PathBuf>,
877    config_file_name: String,
878}
879
880impl ConfigManager {
881    /// Load configuration from the default locations
882    pub fn load() -> Result<Self> {
883        Self::load_from_workspace(std::env::current_dir()?)
884    }
885
886    /// Load configuration from a specific workspace
887    pub fn load_from_workspace(workspace: impl AsRef<Path>) -> Result<Self> {
888        let workspace = workspace.as_ref();
889        let defaults_provider = defaults::current_config_defaults();
890        let workspace_paths = defaults_provider.workspace_paths_for(workspace);
891        let workspace_root = workspace_paths.workspace_root().to_path_buf();
892        let config_dir = workspace_paths.config_dir();
893        let config_file_name = defaults_provider.config_file_name().to_string();
894
895        // Try configuration file in workspace root first
896        let config_path = workspace_root.join(&config_file_name);
897        if config_path.exists() {
898            let mut manager = Self::load_from_file(&config_path)?;
899            manager.workspace_root = Some(workspace_root.clone());
900            manager.config_file_name = config_file_name.clone();
901            return Ok(manager);
902        }
903
904        // Try config directory fallback (e.g., .vtcode/vtcode.toml)
905        let fallback_path = config_dir.join(&config_file_name);
906        if fallback_path.exists() {
907            let mut manager = Self::load_from_file(&fallback_path)?;
908            manager.workspace_root = Some(workspace_root.clone());
909            manager.config_file_name = config_file_name.clone();
910            return Ok(manager);
911        }
912
913        // Try ~/.vtcode/vtcode.toml in user home directory
914        for home_config_path in defaults_provider.home_config_paths(&config_file_name) {
915            if home_config_path.exists() {
916                let mut manager = Self::load_from_file(&home_config_path)?;
917                manager.workspace_root = Some(workspace_root.clone());
918                manager.config_file_name = config_file_name.clone();
919                return Ok(manager);
920            }
921        }
922
923        // Try project-specific configuration within the workspace config directory
924        if let Some(project_config_path) =
925            Self::project_config_path(&config_dir, &workspace_root, &config_file_name)
926        {
927            let mut manager = Self::load_from_file(&project_config_path)?;
928            manager.workspace_root = Some(workspace_root.clone());
929            manager.config_file_name = config_file_name.clone();
930            return Ok(manager);
931        }
932
933        // Use default configuration if no file found
934        let config = VTCodeConfig::default();
935        config
936            .validate()
937            .context("Default configuration failed validation")?;
938
939        Ok(Self {
940            config,
941            config_path: None,
942            workspace_root: Some(workspace_root),
943            config_file_name,
944        })
945    }
946
947    /// Load configuration from a specific file
948    pub fn load_from_file(path: impl AsRef<Path>) -> Result<Self> {
949        let path = path.as_ref();
950        let content = std::fs::read_to_string(path)
951            .with_context(|| format!("Failed to read config file: {}", path.display()))?;
952
953        let config: VTCodeConfig = toml::from_str(&content)
954            .with_context(|| format!("Failed to parse config file: {}", path.display()))?;
955
956        config
957            .validate()
958            .with_context(|| format!("Failed to validate config file: {}", path.display()))?;
959
960        let config_file_name = path
961            .file_name()
962            .and_then(|name| name.to_str().map(ToOwned::to_owned))
963            .unwrap_or_else(|| {
964                defaults::current_config_defaults()
965                    .config_file_name()
966                    .to_string()
967            });
968
969        Ok(Self {
970            config,
971            config_path: Some(path.to_path_buf()),
972            workspace_root: path.parent().map(Path::to_path_buf),
973            config_file_name,
974        })
975    }
976
977    /// Get the loaded configuration
978    pub fn config(&self) -> &VTCodeConfig {
979        &self.config
980    }
981
982    /// Get the configuration file path (if loaded from file)
983    pub fn config_path(&self) -> Option<&Path> {
984        self.config_path.as_deref()
985    }
986
987    /// Get session duration from agent config
988    pub fn session_duration(&self) -> std::time::Duration {
989        std::time::Duration::from_secs(60 * 60) // Default 1 hour
990    }
991
992    /// Persist configuration to a specific path, preserving comments
993    pub fn save_config_to_path(path: impl AsRef<Path>, config: &VTCodeConfig) -> Result<()> {
994        let path = path.as_ref();
995
996        // If file exists, preserve comments by using toml_edit
997        if path.exists() {
998            let original_content = fs::read_to_string(path)
999                .with_context(|| format!("Failed to read existing config: {}", path.display()))?;
1000
1001            let mut doc = original_content
1002                .parse::<toml_edit::DocumentMut>()
1003                .with_context(|| format!("Failed to parse existing config: {}", path.display()))?;
1004
1005            // Serialize new config to TOML value
1006            let new_value =
1007                toml::to_string_pretty(config).context("Failed to serialize configuration")?;
1008            let new_doc: toml_edit::DocumentMut = new_value
1009                .parse()
1010                .context("Failed to parse serialized configuration")?;
1011
1012            // Update values while preserving structure and comments
1013            Self::merge_toml_documents(&mut doc, &new_doc);
1014
1015            fs::write(path, doc.to_string())
1016                .with_context(|| format!("Failed to write config file: {}", path.display()))?;
1017        } else {
1018            // New file, just write normally
1019            let content =
1020                toml::to_string_pretty(config).context("Failed to serialize configuration")?;
1021            fs::write(path, content)
1022                .with_context(|| format!("Failed to write config file: {}", path.display()))?;
1023        }
1024
1025        Ok(())
1026    }
1027
1028    /// Merge TOML documents, preserving comments and structure from original
1029    fn merge_toml_documents(original: &mut toml_edit::DocumentMut, new: &toml_edit::DocumentMut) {
1030        for (key, new_value) in new.iter() {
1031            if let Some(original_value) = original.get_mut(key) {
1032                Self::merge_toml_items(original_value, new_value);
1033            } else {
1034                original[key] = new_value.clone();
1035            }
1036        }
1037    }
1038
1039    /// Recursively merge TOML items
1040    fn merge_toml_items(original: &mut toml_edit::Item, new: &toml_edit::Item) {
1041        match (original, new) {
1042            (toml_edit::Item::Table(orig_table), toml_edit::Item::Table(new_table)) => {
1043                for (key, new_value) in new_table.iter() {
1044                    if let Some(orig_value) = orig_table.get_mut(key) {
1045                        Self::merge_toml_items(orig_value, new_value);
1046                    } else {
1047                        orig_table[key] = new_value.clone();
1048                    }
1049                }
1050            }
1051            (orig, new) => {
1052                *orig = new.clone();
1053            }
1054        }
1055    }
1056
1057    fn project_config_path(
1058        config_dir: &Path,
1059        workspace_root: &Path,
1060        config_file_name: &str,
1061    ) -> Option<PathBuf> {
1062        let project_name = Self::identify_current_project(workspace_root)?;
1063        let project_config_path = config_dir
1064            .join("projects")
1065            .join(project_name)
1066            .join("config")
1067            .join(config_file_name);
1068
1069        if project_config_path.exists() {
1070            Some(project_config_path)
1071        } else {
1072            None
1073        }
1074    }
1075
1076    fn identify_current_project(workspace_root: &Path) -> Option<String> {
1077        let project_file = workspace_root.join(".vtcode-project");
1078        if let Ok(contents) = fs::read_to_string(&project_file) {
1079            let name = contents.trim();
1080            if !name.is_empty() {
1081                return Some(name.to_string());
1082            }
1083        }
1084
1085        workspace_root
1086            .file_name()
1087            .and_then(|name| name.to_str())
1088            .map(|name| name.to_string())
1089    }
1090
1091    /// Persist configuration to the manager's associated path or workspace
1092    pub fn save_config(&self, config: &VTCodeConfig) -> Result<()> {
1093        if let Some(path) = &self.config_path {
1094            return Self::save_config_to_path(path, config);
1095        }
1096
1097        if let Some(workspace_root) = &self.workspace_root {
1098            let path = workspace_root.join(&self.config_file_name);
1099            return Self::save_config_to_path(path, config);
1100        }
1101
1102        let cwd = std::env::current_dir().context("Failed to resolve current directory")?;
1103        let path = cwd.join(&self.config_file_name);
1104        Self::save_config_to_path(path, config)
1105    }
1106}
1107
1108#[cfg(test)]
1109mod tests {
1110    use super::*;
1111    use crate::defaults::WorkspacePathsDefaults;
1112    use std::io::Write;
1113    use std::path::PathBuf;
1114    use std::sync::Arc;
1115    use tempfile::NamedTempFile;
1116    use vtcode_commons::reference::StaticWorkspacePaths;
1117
1118    #[test]
1119    fn syntax_highlighting_defaults_are_valid() {
1120        let config = SyntaxHighlightingConfig::default();
1121        config
1122            .validate()
1123            .expect("default syntax highlighting config should be valid");
1124    }
1125
1126    #[test]
1127    fn vtcode_config_validation_fails_for_invalid_highlight_timeout() {
1128        let mut config = VTCodeConfig::default();
1129        config.syntax_highlighting.highlight_timeout_ms = 0;
1130        let error = config
1131            .validate()
1132            .expect_err("validation should fail for zero highlight timeout");
1133        assert!(
1134            error.to_string().contains("highlight timeout"),
1135            "expected error to mention highlight timeout, got: {}",
1136            error
1137        );
1138    }
1139
1140    #[test]
1141    fn load_from_file_rejects_invalid_syntax_highlighting() {
1142        let mut temp_file = NamedTempFile::new().expect("failed to create temp file");
1143        writeln!(
1144            temp_file,
1145            "[syntax_highlighting]\nhighlight_timeout_ms = 0\n"
1146        )
1147        .expect("failed to write temp config");
1148
1149        let result = ConfigManager::load_from_file(temp_file.path());
1150        assert!(result.is_err(), "expected validation error");
1151        let error = result.unwrap_err();
1152        assert!(
1153            error.to_string().contains("validate"),
1154            "expected validation context in error, got: {}",
1155            error
1156        );
1157    }
1158
1159    #[test]
1160    fn save_config_preserves_comments() {
1161        use std::io::Write;
1162
1163        let mut temp_file = NamedTempFile::new().expect("failed to create temp file");
1164        let config_with_comments = r#"# This is a test comment
1165[agent]
1166# Provider comment
1167provider = "openai"
1168default_model = "gpt-5-nano"
1169
1170# Tools section comment
1171[tools]
1172default_policy = "prompt"
1173"#;
1174
1175        write!(temp_file, "{}", config_with_comments).expect("failed to write temp config");
1176        temp_file.flush().expect("failed to flush");
1177
1178        // Load config
1179        let manager =
1180            ConfigManager::load_from_file(temp_file.path()).expect("failed to load config");
1181
1182        // Modify and save
1183        let mut modified_config = manager.config().clone();
1184        modified_config.agent.default_model = "gpt-5".to_string();
1185
1186        ConfigManager::save_config_to_path(temp_file.path(), &modified_config)
1187            .expect("failed to save config");
1188
1189        // Read back and verify comments are preserved
1190        let saved_content =
1191            fs::read_to_string(temp_file.path()).expect("failed to read saved config");
1192
1193        assert!(
1194            saved_content.contains("# This is a test comment"),
1195            "top-level comment should be preserved"
1196        );
1197        assert!(
1198            saved_content.contains("# Provider comment"),
1199            "inline comment should be preserved"
1200        );
1201        assert!(
1202            saved_content.contains("# Tools section comment"),
1203            "section comment should be preserved"
1204        );
1205        assert!(
1206            saved_content.contains("gpt-5"),
1207            "modified value should be present"
1208        );
1209    }
1210
1211    #[test]
1212    fn config_defaults_provider_overrides_paths_and_theme() {
1213        let workspace = tempfile::tempdir().expect("failed to create workspace");
1214        let workspace_root = workspace.path();
1215        let config_dir = workspace_root.join("config-root");
1216        fs::create_dir_all(&config_dir).expect("failed to create config directory");
1217
1218        let config_file_name = "custom-config.toml";
1219        let config_path = config_dir.join(config_file_name);
1220        let serialized =
1221            toml::to_string(&VTCodeConfig::default()).expect("failed to serialize default config");
1222        fs::write(&config_path, serialized).expect("failed to write config file");
1223
1224        let static_paths = StaticWorkspacePaths::new(workspace_root, &config_dir);
1225        let provider = WorkspacePathsDefaults::new(Arc::new(static_paths))
1226            .with_config_file_name(config_file_name)
1227            .with_home_paths::<Vec<PathBuf>, _>(Vec::new())
1228            .with_syntax_theme("custom-theme")
1229            .with_syntax_languages(["zig"]);
1230
1231        defaults::provider::with_config_defaults_provider_for_test(Arc::new(provider), || {
1232            let manager = ConfigManager::load_from_workspace(workspace_root)
1233                .expect("failed to load workspace config");
1234
1235            let resolved_path = manager
1236                .config_path()
1237                .expect("config path should be resolved");
1238            assert_eq!(resolved_path, config_path);
1239
1240            assert_eq!(SyntaxHighlightingDefaults::theme(), "custom-theme");
1241            assert_eq!(
1242                SyntaxHighlightingDefaults::enabled_languages(),
1243                vec!["zig".to_string()]
1244            );
1245        });
1246    }
1247}