1#[cfg(feature = "bootstrap")]
2pub mod bootstrap;
3
4use crate::acp::AgentClientProtocolConfig;
5use crate::context::ContextFeaturesConfig;
6use crate::core::{
7 AgentConfig, AutomationConfig, CommandsConfig, ModelConfig, PermissionsConfig,
8 PromptCachingConfig, SecurityConfig, ToolsConfig,
9};
10use crate::debug::DebugConfig;
11use crate::defaults::{self, ConfigDefaultsProvider, SyntaxHighlightingDefaults};
12use crate::hooks::HooksConfig;
13use crate::mcp::McpClientConfig;
14use crate::root::{PtyConfig, UiConfig};
15use crate::router::RouterConfig;
16use crate::telemetry::TelemetryConfig;
17use crate::timeouts::TimeoutsConfig;
18use anyhow::{Context, Result, ensure};
19use serde::{Deserialize, Serialize};
20use std::fs;
21use std::path::{Path, PathBuf};
22
23#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
25#[derive(Debug, Clone, Deserialize, Serialize)]
26pub struct SyntaxHighlightingConfig {
27 #[serde(default = "defaults::syntax_highlighting::enabled")]
29 pub enabled: bool,
30
31 #[serde(default = "defaults::syntax_highlighting::theme")]
33 pub theme: String,
34
35 #[serde(default = "defaults::syntax_highlighting::cache_themes")]
37 pub cache_themes: bool,
38
39 #[serde(default = "defaults::syntax_highlighting::max_file_size_mb")]
41 pub max_file_size_mb: usize,
42
43 #[serde(default = "defaults::syntax_highlighting::enabled_languages")]
45 pub enabled_languages: Vec<String>,
46
47 #[serde(default = "defaults::syntax_highlighting::highlight_timeout_ms")]
49 pub highlight_timeout_ms: u64,
50}
51
52impl Default for SyntaxHighlightingConfig {
53 fn default() -> Self {
54 Self {
55 enabled: defaults::syntax_highlighting::enabled(),
56 theme: defaults::syntax_highlighting::theme(),
57 cache_themes: defaults::syntax_highlighting::cache_themes(),
58 max_file_size_mb: defaults::syntax_highlighting::max_file_size_mb(),
59 enabled_languages: defaults::syntax_highlighting::enabled_languages(),
60 highlight_timeout_ms: defaults::syntax_highlighting::highlight_timeout_ms(),
61 }
62 }
63}
64
65impl SyntaxHighlightingConfig {
66 pub fn validate(&self) -> Result<()> {
67 if !self.enabled {
68 return Ok(());
69 }
70
71 ensure!(
72 self.max_file_size_mb >= SyntaxHighlightingDefaults::min_file_size_mb(),
73 "Syntax highlighting max_file_size_mb must be at least {} MB",
74 SyntaxHighlightingDefaults::min_file_size_mb()
75 );
76
77 ensure!(
78 self.highlight_timeout_ms >= SyntaxHighlightingDefaults::min_highlight_timeout_ms(),
79 "Syntax highlighting highlight_timeout_ms must be at least {} ms",
80 SyntaxHighlightingDefaults::min_highlight_timeout_ms()
81 );
82
83 ensure!(
84 !self.theme.trim().is_empty(),
85 "Syntax highlighting theme must not be empty"
86 );
87
88 ensure!(
89 self.enabled_languages
90 .iter()
91 .all(|lang| !lang.trim().is_empty()),
92 "Syntax highlighting languages must not contain empty entries"
93 );
94
95 Ok(())
96 }
97}
98
99#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
101#[derive(Debug, Clone, Deserialize, Serialize, Default)]
102pub struct VTCodeConfig {
103 #[serde(default)]
105 pub agent: AgentConfig,
106
107 #[serde(default)]
109 pub tools: ToolsConfig,
110
111 #[serde(default)]
113 pub commands: CommandsConfig,
114
115 #[serde(default)]
117 pub permissions: PermissionsConfig,
118
119 #[serde(default)]
121 pub security: SecurityConfig,
122
123 #[serde(default)]
125 pub ui: UiConfig,
126
127 #[serde(default)]
129 pub pty: PtyConfig,
130
131 #[serde(default)]
133 pub debug: DebugConfig,
134
135 #[serde(default)]
137 pub context: ContextFeaturesConfig,
138
139 #[serde(default)]
141 pub router: RouterConfig,
142
143 #[serde(default)]
145 pub telemetry: TelemetryConfig,
146
147 #[serde(default)]
149 pub syntax_highlighting: SyntaxHighlightingConfig,
150
151 #[serde(default)]
153 pub timeouts: TimeoutsConfig,
154
155 #[serde(default)]
157 pub automation: AutomationConfig,
158
159 #[serde(default)]
161 pub prompt_cache: PromptCachingConfig,
162
163 #[serde(default)]
165 pub mcp: McpClientConfig,
166
167 #[serde(default)]
169 pub acp: AgentClientProtocolConfig,
170
171 #[serde(default)]
173 pub hooks: HooksConfig,
174
175 #[serde(default)]
177 pub model: ModelConfig,
178}
179
180impl VTCodeConfig {
181 pub fn validate(&self) -> Result<()> {
182 self.syntax_highlighting
183 .validate()
184 .context("Invalid syntax_highlighting configuration")?;
185
186 self.context
187 .validate()
188 .context("Invalid context configuration")?;
189
190 self.router
191 .validate()
192 .context("Invalid router configuration")?;
193
194 self.hooks
195 .validate()
196 .context("Invalid hooks configuration")?;
197
198 self.timeouts
199 .validate()
200 .context("Invalid timeouts configuration")?;
201
202 self.prompt_cache
203 .validate()
204 .context("Invalid prompt_cache configuration")?;
205
206 Ok(())
207 }
208
209 #[cfg(feature = "bootstrap")]
210 pub fn bootstrap_project<P: AsRef<Path>>(workspace: P, force: bool) -> Result<Vec<String>> {
212 Self::bootstrap_project_with_options(workspace, force, false)
213 }
214
215 #[cfg(feature = "bootstrap")]
216 pub fn bootstrap_project_with_options<P: AsRef<Path>>(
218 workspace: P,
219 force: bool,
220 use_home_dir: bool,
221 ) -> Result<Vec<String>> {
222 let workspace = workspace.as_ref().to_path_buf();
223 defaults::with_config_defaults(|provider| {
224 Self::bootstrap_project_with_provider(&workspace, force, use_home_dir, provider)
225 })
226 }
227
228 #[cfg(feature = "bootstrap")]
229 pub fn bootstrap_project_with_provider<P: AsRef<Path>>(
231 workspace: P,
232 force: bool,
233 use_home_dir: bool,
234 defaults_provider: &dyn ConfigDefaultsProvider,
235 ) -> Result<Vec<String>> {
236 let workspace = workspace.as_ref();
237 let config_file_name = defaults_provider.config_file_name().to_string();
238 let (config_path, gitignore_path) = bootstrap::determine_bootstrap_targets(
239 workspace,
240 use_home_dir,
241 &config_file_name,
242 defaults_provider,
243 )?;
244
245 bootstrap::ensure_parent_dir(&config_path)?;
246 bootstrap::ensure_parent_dir(&gitignore_path)?;
247
248 let mut created_files = Vec::new();
249
250 if !config_path.exists() || force {
251 let config_content = Self::default_vtcode_toml_template();
252
253 fs::write(&config_path, config_content).with_context(|| {
254 format!("Failed to write config file: {}", config_path.display())
255 })?;
256
257 if let Some(file_name) = config_path.file_name().and_then(|name| name.to_str()) {
258 created_files.push(file_name.to_string());
259 }
260 }
261
262 if !gitignore_path.exists() || force {
263 let gitignore_content = Self::default_vtcode_gitignore();
264 fs::write(&gitignore_path, gitignore_content).with_context(|| {
265 format!(
266 "Failed to write gitignore file: {}",
267 gitignore_path.display()
268 )
269 })?;
270
271 if let Some(file_name) = gitignore_path.file_name().and_then(|name| name.to_str()) {
272 created_files.push(file_name.to_string());
273 }
274 }
275
276 Ok(created_files)
277 }
278
279 #[cfg(feature = "bootstrap")]
280 fn default_vtcode_toml_template() -> String {
282 r#"# VTCode Configuration File (Example)
283# Getting-started reference; see docs/config/CONFIGURATION_PRECEDENCE.md for override order.
284# Copy this file to vtcode.toml and customize as needed.
285
286# Core agent behavior; see docs/config/CONFIGURATION_PRECEDENCE.md.
287[agent]
288# Primary LLM provider to use (e.g., "openai", "gemini", "anthropic", "openrouter")
289provider = "openai"
290
291# Environment variable containing the API key for the provider
292api_key_env = "OPENAI_API_KEY"
293
294# Default model to use when no specific model is specified
295default_model = "gpt-5-nano"
296
297# Visual theme for the terminal interface
298theme = "ciapre-dark"
299
300# Enable TODO planning helper mode for structured task management
301todo_planning_mode = true
302
303# UI surface to use ("auto", "alternate", "inline")
304ui_surface = "auto"
305
306# Maximum number of conversation turns before rotating context (affects memory usage)
307# Lower values reduce memory footprint but may lose context; higher values preserve context but use more memory
308max_conversation_turns = 50
309
310# Reasoning effort level ("low", "medium", "high") - affects model usage and response speed
311reasoning_effort = "low"
312
313# Enable self-review loop to check and improve responses (increases API calls)
314enable_self_review = false
315
316# Maximum number of review passes when self-review is enabled
317max_review_passes = 1
318
319# Enable prompt refinement loop for improved prompt quality (increases processing time)
320refine_prompts_enabled = false
321
322# Maximum passes for prompt refinement when enabled
323refine_prompts_max_passes = 1
324
325# Optional alternate model for refinement (leave empty to use default)
326refine_prompts_model = ""
327
328# Maximum size of project documentation to include in context (in bytes)
329project_doc_max_bytes = 16384
330
331# Maximum size of instruction files to process (in bytes)
332instruction_max_bytes = 16384
333
334# List of additional instruction files to include in context
335instruction_files = []
336
337# Onboarding configuration - Customize the startup experience
338[agent.onboarding]
339# Enable the onboarding welcome message on startup
340enabled = true
341
342# Custom introduction text shown on startup
343intro_text = "Let's get oriented. I preloaded workspace context so we can move fast."
344
345# Include project overview information in welcome
346include_project_overview = true
347
348# Include language summary information in welcome
349include_language_summary = false
350
351# Include key guideline highlights from AGENTS.md
352include_guideline_highlights = true
353
354# Include usage tips in the welcome message
355include_usage_tips_in_welcome = false
356
357# Include recommended actions in the welcome message
358include_recommended_actions_in_welcome = false
359
360# Maximum number of guideline highlights to show
361guideline_highlight_limit = 3
362
363# List of usage tips shown during onboarding
364usage_tips = [
365 "Describe your current coding goal or ask for a quick status overview.",
366 "Reference AGENTS.md guidelines when proposing changes.",
367 "Draft or refresh your TODO list with update_plan before coding.",
368 "Prefer asking for targeted file reads or diffs before editing.",
369]
370
371# List of recommended actions shown during onboarding
372recommended_actions = [
373 "Start the session by outlining a 3–6 step TODO plan via update_plan.",
374 "Review the highlighted guidelines and share the task you want to tackle.",
375 "Ask for a workspace tour if you need more context.",
376]
377
378# Custom prompts configuration - Define personal assistant commands
379[agent.custom_prompts]
380# Enable the custom prompts feature with /prompt:<name> syntax
381enabled = true
382
383# Directory where custom prompt files are stored
384directory = "~/.vtcode/prompts"
385
386# Additional directories to search for custom prompts
387extra_directories = []
388
389# Maximum file size for custom prompts (in kilobytes)
390max_file_size_kb = 64
391
392# Custom API keys for specific providers
393[agent.custom_api_keys]
394# Moonshot AI API key (for specific provider access)
395moonshot = "sk-sDj3JUXDbfARCYKNL4q7iGWRtWuhL1M4O6zzgtDpN3Yxt9EA"
396
397# Checkpointing configuration for session persistence
398[agent.checkpointing]
399# Enable automatic session checkpointing
400enabled = false
401
402# Maximum number of checkpoints to keep on disk
403max_snapshots = 50
404
405# Maximum age of checkpoints to keep (in days)
406max_age_days = 30
407
408# Tool security configuration
409[tools]
410# Default policy when no specific policy is defined ("allow", "prompt", "deny")
411# "allow" - Execute without confirmation
412# "prompt" - Ask for confirmation
413# "deny" - Block the tool
414default_policy = "prompt"
415
416# Maximum number of tool loops allowed per turn (prevents infinite loops)
417# Higher values allow more complex operations but risk performance issues
418# Recommended: 20 for most tasks, 50 for complex multi-step workflows
419max_tool_loops = 20
420
421# Maximum number of repeated identical tool calls (prevents stuck loops)
422max_repeated_tool_calls = 2
423
424# Specific tool policies - Override default policy for individual tools
425[tools.policies]
426apply_patch = "prompt" # Apply code patches (requires confirmation)
427close_pty_session = "allow" # Close PTY sessions (no confirmation needed)
428create_pty_session = "allow" # Create PTY sessions (no confirmation needed)
429edit_file = "allow" # Edit files directly (no confirmation needed)
430grep_file = "allow" # Sole content-search tool (ripgrep-backed)
431list_files = "allow" # List directory contents (no confirmation needed)
432list_pty_sessions = "allow" # List PTY sessions (no confirmation needed)
433read_file = "allow" # Read files (no confirmation needed)
434read_pty_session = "allow" # Read PTY session output (no no confirmation needed)
435resize_pty_session = "allow" # Resize PTY sessions (no confirmation needed)
436run_pty_cmd = "prompt" # Run commands in PTY (requires confirmation)
437
438send_pty_input = "prompt" # Send input to PTY (requires confirmation)
439update_plan = "allow" # Update task plans (no confirmation needed)
440write_file = "allow" # Write files (no confirmation needed)
441
442# Command security - Define safe and dangerous command patterns
443[commands]
444# Commands that are always allowed without confirmation
445allow_list = [
446 "ls", # List directory contents
447 "pwd", # Print working directory
448 "git status", # Show git status
449 "git diff", # Show git differences
450 "cargo check", # Check Rust code
451 "echo", # Print text
452]
453
454# Commands that are never allowed
455deny_list = [
456 "rm -rf /", # Delete root directory (dangerous)
457 "rm -rf ~", # Delete home directory (dangerous)
458 "shutdown", # Shut down system (dangerous)
459 "reboot", # Reboot system (dangerous)
460 "sudo *", # Any sudo command (dangerous)
461 ":(){ :|:& };:", # Fork bomb (dangerous)
462]
463
464# Command patterns that are allowed (supports glob patterns)
465allow_glob = [
466 "git *", # All git commands
467 "cargo *", # All cargo commands
468 "python -m *", # Python module commands
469]
470
471# Command patterns that are denied (supports glob patterns)
472deny_glob = [
473 "rm *", # All rm commands
474 "sudo *", # All sudo commands
475 "chmod *", # All chmod commands
476 "chown *", # All chown commands
477 "kubectl *", # All kubectl commands (admin access)
478]
479
480# Regular expression patterns for allowed commands (if needed)
481allow_regex = []
482
483# Regular expression patterns for denied commands (if needed)
484deny_regex = []
485
486# Security configuration - Safety settings for automated operations
487[security]
488# Require human confirmation for potentially dangerous actions
489human_in_the_loop = true
490
491# Require explicit write tool usage for claims about file modifications
492require_write_tool_for_claims = true
493
494# Auto-apply patches without prompting (DANGEROUS - disable for safety)
495auto_apply_detected_patches = false
496
497# UI configuration - Terminal and display settings
498[ui]
499# Tool output display mode
500# "compact" - Concise tool output
501# "full" - Detailed tool output
502tool_output_mode = "compact"
503
504# Maximum number of lines to display in tool output (prevents transcript flooding)
505# Lines beyond this limit are truncated to a tail preview
506tool_output_max_lines = 600
507
508# Maximum bytes threshold for spooling tool output to disk
509# Output exceeding this size is written to .vtcode/tool-output/*.log
510tool_output_spool_bytes = 200000
511
512# Optional custom directory for spooled tool output logs
513# If not set, defaults to .vtcode/tool-output/
514# tool_output_spool_dir = "/path/to/custom/spool/dir"
515
516# Allow ANSI escape sequences in tool output (enables colors but may cause layout issues)
517allow_tool_ansi = false
518
519# Number of rows to allocate for inline UI viewport
520inline_viewport_rows = 16
521
522# Show timeline navigation panel
523show_timeline_pane = false
524
525# Status line configuration
526[ui.status_line]
527# Status line mode ("auto", "command", "hidden")
528mode = "auto"
529
530# How often to refresh status line (milliseconds)
531refresh_interval_ms = 2000
532
533# Timeout for command execution in status line (milliseconds)
534command_timeout_ms = 200
535
536# PTY (Pseudo Terminal) configuration - For interactive command execution
537[pty]
538# Enable PTY support for interactive commands
539enabled = true
540
541# Default number of terminal rows for PTY sessions
542default_rows = 24
543
544# Default number of terminal columns for PTY sessions
545default_cols = 80
546
547# Maximum number of concurrent PTY sessions
548max_sessions = 10
549
550# Command timeout in seconds (prevents hanging commands)
551command_timeout_seconds = 300
552
553# Number of recent lines to show in PTY output
554stdout_tail_lines = 20
555
556# Total lines to keep in PTY scrollback buffer
557scrollback_lines = 400
558
559# Context management configuration - Controls conversation memory
560[context]
561# Maximum number of tokens to keep in context (affects model cost and performance)
562# Higher values preserve more context but cost more and may hit token limits
563max_context_tokens = 90000
564
565# Percentage to trim context to when it gets too large
566trim_to_percent = 60
567
568# Number of recent conversation turns to always preserve
569preserve_recent_turns = 6
570
571# Decision ledger configuration - Track important decisions
572[context.ledger]
573# Enable decision tracking and persistence
574enabled = true
575
576# Maximum number of decisions to keep in ledger
577max_entries = 12
578
579# Include ledger summary in model prompts
580include_in_prompt = true
581
582# Preserve ledger during context compression
583preserve_in_compression = true
584
585# Token budget management - Track and limit token usage
586[context.token_budget]
587# Enable token usage tracking and budget enforcement
588enabled = false
589
590# Model to use for token counting (must match your actual model)
591model = "gpt-5-nano"
592
593# Percentage threshold to warn about token usage (0.75 = 75%)
594warning_threshold = 0.75
595
596# Percentage threshold to trigger context alerts (0.85 = 85%)
597alert_threshold = 0.85
598
599# Enable detailed component-level token tracking (increases overhead)
600detailed_tracking = false
601
602# AI model routing - Intelligent model selection
603[router]
604# Enable intelligent model routing
605enabled = true
606
607# Enable heuristic-based model selection
608heuristic_classification = true
609
610# Optional override model for routing decisions (empty = use default)
611llm_router_model = ""
612
613# Model mapping for different task types
614[router.models]
615# Model for simple queries
616simple = "gpt-5-nano"
617# Model for standard tasks
618standard = "gpt-5-nano"
619# Model for complex tasks
620complex = "gpt-5-nano"
621# Model for code generation heavy tasks
622codegen_heavy = "gpt-5-nano"
623# Model for information retrieval heavy tasks
624retrieval_heavy = "gpt-5-nano"
625
626# Router budget settings (if applicable)
627[router.budgets]
628
629# Router heuristic patterns for task classification
630[router.heuristics]
631# Maximum characters for short requests
632short_request_max_chars = 120
633# Minimum characters for long requests
634long_request_min_chars = 1200
635
636# Text patterns that indicate code patch operations
637code_patch_markers = [
638 "```",
639 "diff --git",
640 "apply_patch",
641 "unified diff",
642 "patch",
643 "edit_file",
644 "create_file",
645]
646
647# Text patterns that indicate information retrieval
648retrieval_markers = [
649 "search",
650 "web",
651 "google",
652 "docs",
653 "cite",
654 "source",
655 "up-to-date",
656]
657
658# Text patterns that indicate complex multi-step tasks
659complex_markers = [
660 "plan",
661 "multi-step",
662 "decompose",
663 "orchestrate",
664 "architecture",
665 "benchmark",
666 "implement end-to-end",
667 "design api",
668 "refactor module",
669 "evaluate",
670 "tests suite",
671]
672
673# Telemetry and analytics
674[telemetry]
675# Enable trajectory logging for usage analysis
676trajectory_enabled = true
677
678# Syntax highlighting configuration
679[syntax_highlighting]
680# Enable syntax highlighting for code in tool output
681enabled = true
682
683# Theme for syntax highlighting
684theme = "base16-ocean.dark"
685
686# Cache syntax highlighting themes for performance
687cache_themes = true
688
689# Maximum file size for syntax highlighting (in MB)
690max_file_size_mb = 10
691
692# Programming languages to enable syntax highlighting for
693enabled_languages = [
694 "rust",
695 "python",
696 "javascript",
697 "typescript",
698 "go",
699 "java",
700]
701
702# Timeout for syntax highlighting operations (milliseconds)
703highlight_timeout_ms = 1000
704
705# Automation features - Full-auto mode settings
706[automation.full_auto]
707# Enable full automation mode (DANGEROUS - requires careful oversight)
708enabled = false
709
710# Maximum number of turns before asking for human input
711max_turns = 30
712
713# Tools allowed in full automation mode
714allowed_tools = [
715 "write_file",
716 "read_file",
717 "list_files",
718 "grep_file",
719]
720
721# Require profile acknowledgment before using full auto
722require_profile_ack = true
723
724# Path to full auto profile configuration
725profile_path = "automation/full_auto_profile.toml"
726
727# Prompt caching - Cache model responses for efficiency
728[prompt_cache]
729# Enable prompt caching (reduces API calls for repeated prompts)
730enabled = false
731
732# Directory for cache storage
733cache_dir = "~/.vtcode/cache/prompts"
734
735# Maximum number of cache entries to keep
736max_entries = 1000
737
738# Maximum age of cache entries (in days)
739max_age_days = 30
740
741# Enable automatic cache cleanup
742enable_auto_cleanup = true
743
744# Minimum quality threshold to keep cache entries
745min_quality_threshold = 0.7
746
747# Prompt cache configuration for OpenAI
748 [prompt_cache.providers.openai]
749 enabled = true
750 min_prefix_tokens = 1024
751 idle_expiration_seconds = 3600
752 surface_metrics = true
753 # Optional: server-side prompt cache retention for OpenAI Responses API
754 # Example: "24h" (leave commented out for default behavior)
755 # prompt_cache_retention = "24h"
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 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#[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 pub fn load() -> Result<Self> {
883 Self::load_from_workspace(std::env::current_dir()?)
884 }
885
886 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 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 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 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 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 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 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 pub fn config(&self) -> &VTCodeConfig {
979 &self.config
980 }
981
982 pub fn config_path(&self) -> Option<&Path> {
984 self.config_path.as_deref()
985 }
986
987 pub fn session_duration(&self) -> std::time::Duration {
989 std::time::Duration::from_secs(60 * 60) }
991
992 pub fn save_config_to_path(path: impl AsRef<Path>, config: &VTCodeConfig) -> Result<()> {
994 let path = path.as_ref();
995
996 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 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 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 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 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 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 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::sync::Arc;
1114 use tempfile::NamedTempFile;
1115 use vtcode_commons::reference::StaticWorkspacePaths;
1116
1117 #[test]
1118 fn syntax_highlighting_defaults_are_valid() {
1119 let config = SyntaxHighlightingConfig::default();
1120 config
1121 .validate()
1122 .expect("default syntax highlighting config should be valid");
1123 }
1124
1125 #[test]
1126 fn vtcode_config_validation_fails_for_invalid_highlight_timeout() {
1127 let mut config = VTCodeConfig::default();
1128 config.syntax_highlighting.highlight_timeout_ms = 0;
1129 let error = config
1130 .validate()
1131 .expect_err("validation should fail for zero highlight timeout");
1132 assert!(
1133 error.to_string().contains("highlight timeout"),
1134 "expected error to mention highlight timeout, got: {}",
1135 error
1136 );
1137 }
1138
1139 #[test]
1140 fn load_from_file_rejects_invalid_syntax_highlighting() {
1141 let mut temp_file = NamedTempFile::new().expect("failed to create temp file");
1142 writeln!(
1143 temp_file,
1144 "[syntax_highlighting]\nhighlight_timeout_ms = 0\n"
1145 )
1146 .expect("failed to write temp config");
1147
1148 let result = ConfigManager::load_from_file(temp_file.path());
1149 assert!(result.is_err(), "expected validation error");
1150 let error = format!("{:?}", result.err().unwrap());
1151 assert!(
1152 error.contains("validate"),
1153 "expected validation context in error, got: {}",
1154 error
1155 );
1156 }
1157
1158 #[test]
1159 fn loader_loads_prompt_cache_retention_from_toml() {
1160 use std::fs::File;
1161 use std::io::Write;
1162
1163 let temp = tempfile::tempdir().unwrap();
1164 let path = temp.path().join("vtcode.toml");
1165 let mut file = File::create(&path).unwrap();
1166 let contents = r#"
1167[prompt_cache]
1168enabled = true
1169[prompt_cache.providers.openai]
1170prompt_cache_retention = "24h"
1171"#;
1172 file.write_all(contents.as_bytes()).unwrap();
1173
1174 let manager = ConfigManager::load_from_file(&path).unwrap();
1175 let config = manager.config();
1176 assert_eq!(
1177 config.prompt_cache.providers.openai.prompt_cache_retention,
1178 Some("24h".to_string())
1179 );
1180 }
1181
1182 #[test]
1183 fn save_config_preserves_comments() {
1184 use std::io::Write;
1185
1186 let mut temp_file = NamedTempFile::new().expect("failed to create temp file");
1187 let config_with_comments = r#"# This is a test comment
1188[agent]
1189# Provider comment
1190provider = "openai"
1191default_model = "gpt-5-nano"
1192
1193# Tools section comment
1194[tools]
1195default_policy = "prompt"
1196"#;
1197
1198 write!(temp_file, "{}", config_with_comments).expect("failed to write temp config");
1199 temp_file.flush().expect("failed to flush");
1200
1201 let manager =
1203 ConfigManager::load_from_file(temp_file.path()).expect("failed to load config");
1204
1205 let mut modified_config = manager.config().clone();
1207 modified_config.agent.default_model = "gpt-5".to_string();
1208
1209 ConfigManager::save_config_to_path(temp_file.path(), &modified_config)
1210 .expect("failed to save config");
1211
1212 let saved_content =
1214 fs::read_to_string(temp_file.path()).expect("failed to read saved config");
1215
1216 assert!(
1217 saved_content.contains("# This is a test comment"),
1218 "top-level comment should be preserved"
1219 );
1220 assert!(
1221 saved_content.contains("# Provider comment"),
1222 "inline comment should be preserved"
1223 );
1224 assert!(
1225 saved_content.contains("# Tools section comment"),
1226 "section comment should be preserved"
1227 );
1228 assert!(
1229 saved_content.contains("gpt-5"),
1230 "modified value should be present"
1231 );
1232 }
1233
1234 #[test]
1235 fn config_defaults_provider_overrides_paths_and_theme() {
1236 let workspace = assert_fs::TempDir::new().expect("failed to create workspace");
1237 let workspace_root = workspace.path();
1238 let config_dir = workspace_root.join("config-root");
1239 fs::create_dir_all(&config_dir).expect("failed to create config directory");
1240
1241 let config_file_name = "custom-config.toml";
1242 let config_path = config_dir.join(config_file_name);
1243 let serialized =
1244 toml::to_string(&VTCodeConfig::default()).expect("failed to serialize default config");
1245 fs::write(&config_path, serialized).expect("failed to write config file");
1246
1247 let static_paths = StaticWorkspacePaths::new(workspace_root, &config_dir);
1248 let provider = WorkspacePathsDefaults::new(Arc::new(static_paths))
1249 .with_config_file_name(config_file_name)
1250 .with_home_paths(Vec::new())
1251 .with_syntax_theme("custom-theme")
1252 .with_syntax_languages(vec!["zig".to_string()]);
1253
1254 defaults::provider::with_config_defaults_provider_for_test(Arc::new(provider), || {
1255 let manager = ConfigManager::load_from_workspace(workspace_root)
1256 .expect("failed to load workspace config");
1257
1258 let resolved_path = manager
1259 .config_path()
1260 .expect("config path should be resolved");
1261 assert_eq!(resolved_path, config_path);
1262
1263 assert_eq!(SyntaxHighlightingDefaults::theme(), "custom-theme");
1264 assert_eq!(
1265 SyntaxHighlightingDefaults::enabled_languages(),
1266 vec!["zig".to_string()]
1267 );
1268 });
1269 }
1270}