1use anyhow::{Context, Result};
2use serde::{Deserialize, Serialize};
3use std::fs;
4use std::path::Path;
5
6use crate::acp::AgentClientProtocolConfig;
7use crate::context::ContextFeaturesConfig;
8use crate::core::{
9 AgentConfig, AnthropicConfig, AuthConfig, AutomationConfig, CommandsConfig,
10 DotfileProtectionConfig, ModelConfig, OpenAIConfig, PermissionsConfig, PromptCachingConfig,
11 SandboxConfig, SecurityConfig, SkillsConfig, ToolsConfig,
12};
13use crate::debug::DebugConfig;
14use crate::defaults::{self, ConfigDefaultsProvider};
15use crate::hooks::HooksConfig;
16use crate::ide_context::IdeContextConfig;
17use crate::mcp::McpClientConfig;
18use crate::optimization::OptimizationConfig;
19use crate::output_styles::OutputStyleConfig;
20use crate::root::{ChatConfig, PtyConfig, UiConfig};
21use crate::telemetry::TelemetryConfig;
22use crate::timeouts::TimeoutsConfig;
23
24use crate::loader::syntax_highlighting::SyntaxHighlightingConfig;
25
26#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
28#[derive(Debug, Clone, Deserialize, Serialize, Default)]
29pub struct ProviderConfig {
30 #[serde(default)]
32 pub openai: OpenAIConfig,
33
34 #[serde(default)]
36 pub anthropic: AnthropicConfig,
37}
38
39#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
41#[derive(Debug, Clone, Deserialize, Serialize, Default)]
42pub struct VTCodeConfig {
43 #[serde(default)]
45 pub agent: AgentConfig,
46
47 #[serde(default)]
49 pub auth: AuthConfig,
50
51 #[serde(default)]
53 pub tools: ToolsConfig,
54
55 #[serde(default)]
57 pub commands: CommandsConfig,
58
59 #[serde(default)]
61 pub permissions: PermissionsConfig,
62
63 #[serde(default)]
65 pub security: SecurityConfig,
66
67 #[serde(default)]
69 pub sandbox: SandboxConfig,
70
71 #[serde(default)]
73 pub ui: UiConfig,
74
75 #[serde(default)]
77 pub chat: ChatConfig,
78
79 #[serde(default)]
81 pub pty: PtyConfig,
82
83 #[serde(default)]
85 pub debug: DebugConfig,
86
87 #[serde(default)]
89 pub context: ContextFeaturesConfig,
90
91 #[serde(default)]
93 pub telemetry: TelemetryConfig,
94
95 #[serde(default)]
97 pub optimization: OptimizationConfig,
98
99 #[serde(default)]
101 pub syntax_highlighting: SyntaxHighlightingConfig,
102
103 #[serde(default)]
105 pub timeouts: TimeoutsConfig,
106
107 #[serde(default)]
109 pub automation: AutomationConfig,
110
111 #[serde(default)]
113 pub prompt_cache: PromptCachingConfig,
114
115 #[serde(default)]
117 pub mcp: McpClientConfig,
118
119 #[serde(default)]
121 pub acp: AgentClientProtocolConfig,
122
123 #[serde(default)]
125 pub ide_context: IdeContextConfig,
126
127 #[serde(default)]
129 pub hooks: HooksConfig,
130
131 #[serde(default)]
133 pub model: ModelConfig,
134
135 #[serde(default)]
137 pub provider: ProviderConfig,
138
139 #[serde(default)]
141 pub skills: SkillsConfig,
142
143 #[serde(default)]
145 pub output_style: OutputStyleConfig,
146
147 #[serde(default)]
149 pub dotfile_protection: DotfileProtectionConfig,
150}
151
152impl VTCodeConfig {
153 pub fn validate(&self) -> Result<()> {
154 self.syntax_highlighting
155 .validate()
156 .context("Invalid syntax_highlighting configuration")?;
157
158 self.context
159 .validate()
160 .context("Invalid context configuration")?;
161
162 self.hooks
163 .validate()
164 .context("Invalid hooks configuration")?;
165
166 self.timeouts
167 .validate()
168 .context("Invalid timeouts configuration")?;
169
170 self.prompt_cache
171 .validate()
172 .context("Invalid prompt_cache configuration")?;
173
174 self.ui
175 .keyboard_protocol
176 .validate()
177 .context("Invalid keyboard_protocol configuration")?;
178
179 self.pty.validate().context("Invalid pty configuration")?;
180
181 Ok(())
182 }
183
184 #[cfg(feature = "bootstrap")]
185 pub fn bootstrap_project<P: AsRef<Path>>(workspace: P, force: bool) -> Result<Vec<String>> {
187 Self::bootstrap_project_with_options(workspace, force, false)
188 }
189
190 #[cfg(feature = "bootstrap")]
191 pub fn bootstrap_project_with_options<P: AsRef<Path>>(
193 workspace: P,
194 force: bool,
195 use_home_dir: bool,
196 ) -> Result<Vec<String>> {
197 let workspace = workspace.as_ref().to_path_buf();
198 defaults::with_config_defaults(|provider| {
199 Self::bootstrap_project_with_provider(&workspace, force, use_home_dir, provider)
200 })
201 }
202
203 #[cfg(feature = "bootstrap")]
204 pub fn bootstrap_project_with_provider<P: AsRef<Path>>(
206 workspace: P,
207 force: bool,
208 use_home_dir: bool,
209 defaults_provider: &dyn ConfigDefaultsProvider,
210 ) -> Result<Vec<String>> {
211 let workspace = workspace.as_ref();
212 let config_file_name = defaults_provider.config_file_name().to_string();
213 let (config_path, gitignore_path) = crate::loader::bootstrap::determine_bootstrap_targets(
214 workspace,
215 use_home_dir,
216 &config_file_name,
217 defaults_provider,
218 )?;
219
220 crate::loader::bootstrap::ensure_parent_dir(&config_path)?;
221 crate::loader::bootstrap::ensure_parent_dir(&gitignore_path)?;
222
223 let mut created_files = Vec::new();
224
225 if !config_path.exists() || force {
226 let config_content = Self::default_vtcode_toml_template();
227
228 fs::write(&config_path, config_content).with_context(|| {
229 format!("Failed to write config file: {}", config_path.display())
230 })?;
231
232 if let Some(file_name) = config_path.file_name().and_then(|name| name.to_str()) {
233 created_files.push(file_name.to_string());
234 }
235 }
236
237 if !gitignore_path.exists() || force {
238 let gitignore_content = Self::default_vtcode_gitignore();
239 fs::write(&gitignore_path, gitignore_content).with_context(|| {
240 format!(
241 "Failed to write gitignore file: {}",
242 gitignore_path.display()
243 )
244 })?;
245
246 if let Some(file_name) = gitignore_path.file_name().and_then(|name| name.to_str()) {
247 created_files.push(file_name.to_string());
248 }
249 }
250
251 Ok(created_files)
252 }
253
254 #[cfg(feature = "bootstrap")]
255 fn default_vtcode_toml_template() -> String {
257 r#"# VT Code Configuration File (Example)
258# Getting-started reference; see docs/config/CONFIGURATION_PRECEDENCE.md for override order.
259# Copy this file to vtcode.toml and customize as needed.
260
261# Core agent behavior; see docs/config/CONFIGURATION_PRECEDENCE.md.
262[agent]
263# Primary LLM provider to use (e.g., "openai", "gemini", "anthropic", "openrouter")
264provider = "openai"
265
266# Environment variable containing the API key for the provider
267api_key_env = "OPENAI_API_KEY"
268
269# Default model to use when no specific model is specified
270default_model = "gpt-5.4"
271
272# Visual theme for the terminal interface
273theme = "ciapre-dark"
274
275# Enable TODO planning helper mode for structured task management
276todo_planning_mode = true
277
278# UI surface to use ("auto", "alternate", "inline")
279ui_surface = "auto"
280
281# Maximum number of conversation turns before rotating context (affects memory usage)
282# Lower values reduce memory footprint but may lose context; higher values preserve context but use more memory
283max_conversation_turns = 150
284
285# Reasoning effort level ("none", "minimal", "low", "medium", "high", "xhigh") - affects model usage and response speed
286reasoning_effort = "none"
287
288# Temperature for main model responses (0.0-1.0)
289temperature = 0.7
290
291# Enable self-review loop to check and improve responses (increases API calls)
292enable_self_review = false
293
294# Maximum number of review passes when self-review is enabled
295max_review_passes = 1
296
297# Enable prompt refinement loop for improved prompt quality (increases processing time)
298refine_prompts_enabled = false
299
300# Maximum passes for prompt refinement when enabled
301refine_prompts_max_passes = 1
302
303# Optional alternate model for refinement (leave empty to use default)
304refine_prompts_model = ""
305
306# Maximum size of project documentation to include in context (in bytes)
307project_doc_max_bytes = 16384
308
309# Maximum size of instruction files to process (in bytes)
310instruction_max_bytes = 16384
311
312# List of additional instruction files to include in context
313instruction_files = []
314
315# Default editing mode on startup: "edit" or "plan"
316# "edit" - Full tool access for file modifications and command execution (default)
317# "plan" - Read-only mode that produces implementation plans without making changes
318# Toggle during session with Shift+Tab or /plan command
319default_editing_mode = "edit"
320
321# Onboarding configuration - Customize the startup experience
322[agent.onboarding]
323# Enable the onboarding welcome message on startup
324enabled = true
325
326# Custom introduction text shown on startup
327intro_text = "Let's get oriented. I preloaded workspace context so we can move fast."
328
329# Include project overview information in welcome
330include_project_overview = true
331
332# Include language summary information in welcome
333include_language_summary = false
334
335# Include key guideline highlights from AGENTS.md
336include_guideline_highlights = true
337
338# Include usage tips in the welcome message
339include_usage_tips_in_welcome = false
340
341# Include recommended actions in the welcome message
342include_recommended_actions_in_welcome = false
343
344# Maximum number of guideline highlights to show
345guideline_highlight_limit = 3
346
347# List of usage tips shown during onboarding
348usage_tips = [
349 "Describe your current coding goal or ask for a quick status overview.",
350 "Reference AGENTS.md guidelines when proposing changes.",
351 "Prefer asking for targeted file reads or diffs before editing.",
352]
353
354# List of recommended actions shown during onboarding
355recommended_actions = [
356 "Review the highlighted guidelines and share the task you want to tackle.",
357 "Ask for a workspace tour if you need more context.",
358]
359
360# Checkpointing configuration for session persistence
361[agent.checkpointing]
362# Enable automatic session checkpointing
363enabled = false
364
365# Maximum number of checkpoints to keep on disk
366max_snapshots = 50
367
368# Maximum age of checkpoints to keep (in days)
369max_age_days = 30
370
371# Tool security configuration
372[tools]
373# Default policy when no specific policy is defined ("allow", "prompt", "deny")
374# "allow" - Execute without confirmation
375# "prompt" - Ask for confirmation
376# "deny" - Block the tool
377default_policy = "prompt"
378
379# Maximum number of tool loops allowed per turn
380# Set to 0 to disable the limit and let other turn safeguards govern termination.
381max_tool_loops = 0
382
383# Maximum number of repeated identical tool calls (prevents stuck loops)
384max_repeated_tool_calls = 2
385
386# Maximum consecutive blocked tool calls before force-breaking the turn
387# Helps prevent high-CPU churn when calls are repeatedly denied/blocked
388max_consecutive_blocked_tool_calls_per_turn = 8
389
390# Maximum sequential spool-chunk reads per turn before nudging targeted extraction/summarization
391max_sequential_spool_chunk_reads = 6
392
393# Specific tool policies - Override default policy for individual tools
394[tools.policies]
395apply_patch = "prompt" # Apply code patches (requires confirmation)
396request_user_input = "allow" # Ask focused user questions when the task requires it
397task_tracker = "prompt" # Create or update explicit task plans
398unified_exec = "prompt" # Run commands; pipe-first by default, set tty=true for PTY/interactive sessions
399unified_file = "allow" # Canonical file read/write/edit/move/copy/delete surface
400unified_search = "allow" # Canonical search/list/intelligence/error surface
401
402# Command security - Define safe and dangerous command patterns
403[commands]
404# Commands that are always allowed without confirmation
405allow_list = [
406 "ls", # List directory contents
407 "pwd", # Print working directory
408 "git status", # Show git status
409 "git diff", # Show git differences
410 "cargo check", # Check Rust code
411 "echo", # Print text
412]
413
414# Commands that are never allowed
415deny_list = [
416 "rm -rf /", # Delete root directory (dangerous)
417 "rm -rf ~", # Delete home directory (dangerous)
418 "shutdown", # Shut down system (dangerous)
419 "reboot", # Reboot system (dangerous)
420 "sudo *", # Any sudo command (dangerous)
421 ":(){ :|:& };:", # Fork bomb (dangerous)
422]
423
424# Command patterns that are allowed (supports glob patterns)
425allow_glob = [
426 "git *", # All git commands
427 "cargo *", # All cargo commands
428 "python -m *", # Python module commands
429]
430
431# Command patterns that are denied (supports glob patterns)
432deny_glob = [
433 "rm *", # All rm commands
434 "sudo *", # All sudo commands
435 "chmod *", # All chmod commands
436 "chown *", # All chown commands
437 "kubectl *", # All kubectl commands (admin access)
438]
439
440# Regular expression patterns for allowed commands (if needed)
441allow_regex = []
442
443# Regular expression patterns for denied commands (if needed)
444deny_regex = []
445
446# Security configuration - Safety settings for automated operations
447[security]
448# Require human confirmation for potentially dangerous actions
449human_in_the_loop = true
450
451# Require explicit write tool usage for claims about file modifications
452require_write_tool_for_claims = true
453
454# Auto-apply patches without prompting (DANGEROUS - disable for safety)
455auto_apply_detected_patches = false
456
457# UI configuration - Terminal and display settings
458[ui]
459# Tool output display mode
460# "compact" - Concise tool output
461# "full" - Detailed tool output
462tool_output_mode = "compact"
463
464# Maximum number of lines to display in tool output (prevents transcript flooding)
465# Lines beyond this limit are truncated to a tail preview
466tool_output_max_lines = 600
467
468# Maximum bytes threshold for spooling tool output to disk
469# Output exceeding this size is written to .vtcode/tool-output/*.log
470tool_output_spool_bytes = 200000
471
472# Optional custom directory for spooled tool output logs
473# If not set, defaults to .vtcode/tool-output/
474# tool_output_spool_dir = "/path/to/custom/spool/dir"
475
476# Allow ANSI escape sequences in tool output (enables colors but may cause layout issues)
477allow_tool_ansi = false
478
479# Number of rows to allocate for inline UI viewport
480inline_viewport_rows = 16
481
482# Show elapsed time divider after each completed turn
483show_turn_timer = false
484
485# Show warning/error/fatal diagnostic lines directly in transcript
486# Effective in debug/development builds only
487show_diagnostics_in_transcript = false
488
489# Show timeline navigation panel
490show_timeline_pane = false
491
492# Runtime notification preferences
493[ui.notifications]
494# Master toggle for terminal/desktop notifications
495enabled = true
496
497# Delivery mode: "terminal", "hybrid", or "desktop"
498delivery_mode = "hybrid"
499
500# Suppress notifications while terminal is focused
501suppress_when_focused = true
502
503# Failure/error notifications
504command_failure = false
505tool_failure = false
506error = true
507
508# Completion notifications
509# Legacy master toggle (fallback for split settings when unset)
510completion = true
511completion_success = false
512completion_failure = true
513
514# Human approval/interaction notifications
515hitl = true
516policy_approval = true
517request = false
518
519# Success notifications for tool call results
520tool_success = false
521
522# Repeated notification suppression
523repeat_window_seconds = 30
524max_identical_in_window = 1
525
526# Status line configuration
527[ui.status_line]
528# Status line mode ("auto", "command", "hidden")
529mode = "auto"
530
531# How often to refresh status line (milliseconds)
532refresh_interval_ms = 2000
533
534# Timeout for command execution in status line (milliseconds)
535command_timeout_ms = 200
536
537# PTY (Pseudo Terminal) configuration - For interactive command execution
538[pty]
539# Enable PTY support for interactive commands
540enabled = true
541
542# Default number of terminal rows for PTY sessions
543default_rows = 24
544
545# Default number of terminal columns for PTY sessions
546default_cols = 80
547
548# Maximum number of concurrent PTY sessions
549max_sessions = 10
550
551# Command timeout in seconds (prevents hanging commands)
552command_timeout_seconds = 300
553
554# Number of recent lines to show in PTY output
555stdout_tail_lines = 20
556
557# Total lines to keep in PTY scrollback buffer
558scrollback_lines = 400
559
560# Optional preferred shell for PTY sessions (falls back to $SHELL when unset)
561# preferred_shell = "/bin/zsh"
562
563# Route shell execution through zsh EXEC_WRAPPER intercept hooks (feature-gated)
564shell_zsh_fork = false
565
566# Absolute path to patched zsh used when shell_zsh_fork is enabled
567# zsh_path = "/usr/local/bin/zsh"
568
569# Context management configuration - Controls conversation memory
570[context]
571# Maximum number of tokens to keep in context (affects model cost and performance)
572# Higher values preserve more context but cost more and may hit token limits
573max_context_tokens = 90000
574
575# Percentage to trim context to when it gets too large
576trim_to_percent = 60
577
578# Number of recent conversation turns to always preserve
579preserve_recent_turns = 6
580
581# Decision ledger configuration - Track important decisions
582[context.ledger]
583# Enable decision tracking and persistence
584enabled = true
585
586# Maximum number of decisions to keep in ledger
587max_entries = 12
588
589# Include ledger summary in model prompts
590include_in_prompt = true
591
592# Preserve ledger during context compression
593preserve_in_compression = true
594
595# AI model routing - Intelligent model selection
596# Telemetry and analytics
597[telemetry]
598# Enable trajectory logging for usage analysis
599trajectory_enabled = true
600
601# Syntax highlighting configuration
602[syntax_highlighting]
603# Enable syntax highlighting for code in tool output
604enabled = true
605
606# Theme for syntax highlighting
607theme = "base16-ocean.dark"
608
609# Cache syntax highlighting themes for performance
610cache_themes = true
611
612# Maximum file size for syntax highlighting (in MB)
613max_file_size_mb = 10
614
615# Programming languages to enable syntax highlighting for
616enabled_languages = [
617 "rust",
618 "python",
619 "javascript",
620 "typescript",
621 "go",
622 "java",
623 "bash",
624 "sh",
625 "shell",
626 "zsh",
627 "markdown",
628 "md",
629]
630
631# Timeout for syntax highlighting operations (milliseconds)
632highlight_timeout_ms = 1000
633
634# Automation features - Full-auto mode settings
635[automation.full_auto]
636# Enable full automation mode (DANGEROUS - requires careful oversight)
637enabled = false
638
639# Maximum number of turns before asking for human input
640max_turns = 30
641
642# Tools allowed in full automation mode
643allowed_tools = [
644 "write_file",
645 "read_file",
646 "list_files",
647 "grep_file",
648]
649
650# Require profile acknowledgment before using full auto
651require_profile_ack = true
652
653# Path to full auto profile configuration
654profile_path = "automation/full_auto_profile.toml"
655
656# Prompt caching - Cache model responses for efficiency
657[prompt_cache]
658# Enable prompt caching (reduces API calls for repeated prompts)
659enabled = false
660
661# Directory for cache storage
662cache_dir = "~/.vtcode/cache/prompts"
663
664# Maximum number of cache entries to keep
665max_entries = 1000
666
667# Maximum age of cache entries (in days)
668max_age_days = 30
669
670# Enable automatic cache cleanup
671enable_auto_cleanup = true
672
673# Minimum quality threshold to keep cache entries
674min_quality_threshold = 0.7
675
676# Keep volatile runtime counters at the end of system prompts to improve provider-side prefix cache reuse
677# (disabled by default; opt in per workspace)
678cache_friendly_prompt_shaping = false
679
680# Prompt cache configuration for OpenAI
681 [prompt_cache.providers.openai]
682 enabled = true
683 min_prefix_tokens = 1024
684 idle_expiration_seconds = 3600
685 surface_metrics = true
686 # Routing key strategy for OpenAI prompt cache locality.
687 # "session" creates one stable key per VT Code conversation.
688 prompt_cache_key_mode = "session"
689 # Optional: server-side prompt cache retention for OpenAI Responses API
690 # Example: "24h" (leave commented out for default behavior)
691 # prompt_cache_retention = "24h"
692
693# Prompt cache configuration for Anthropic
694[prompt_cache.providers.anthropic]
695enabled = true
696default_ttl_seconds = 300
697extended_ttl_seconds = 3600
698max_breakpoints = 4
699cache_system_messages = true
700cache_user_messages = true
701
702# Prompt cache configuration for Gemini
703[prompt_cache.providers.gemini]
704enabled = true
705mode = "implicit"
706min_prefix_tokens = 1024
707explicit_ttl_seconds = 3600
708
709# Prompt cache configuration for OpenRouter
710[prompt_cache.providers.openrouter]
711enabled = true
712propagate_provider_capabilities = true
713report_savings = true
714
715# Prompt cache configuration for Moonshot
716[prompt_cache.providers.moonshot]
717enabled = true
718
719# Prompt cache configuration for DeepSeek
720[prompt_cache.providers.deepseek]
721enabled = true
722surface_metrics = true
723
724# Prompt cache configuration for Z.AI
725[prompt_cache.providers.zai]
726enabled = false
727
728# Model Context Protocol (MCP) - Connect external tools and services
729[mcp]
730# Enable Model Context Protocol (may impact startup time if services unavailable)
731enabled = true
732max_concurrent_connections = 5
733request_timeout_seconds = 30
734retry_attempts = 3
735
736# MCP UI configuration
737[mcp.ui]
738mode = "compact"
739max_events = 50
740show_provider_names = true
741
742# MCP renderer profiles for different services
743[mcp.ui.renderers]
744sequential-thinking = "sequential-thinking"
745context7 = "context7"
746
747# MCP provider configuration - External services that connect via MCP
748[[mcp.providers]]
749name = "time"
750command = "uvx"
751args = ["mcp-server-time"]
752enabled = true
753max_concurrent_requests = 3
754[mcp.providers.env]
755
756# Agent Client Protocol (ACP) - IDE integration
757[acp]
758enabled = true
759
760[acp.zed]
761enabled = true
762transport = "stdio"
763# workspace_trust controls ACP trust mode: "tools_policy" (prompts) or "full_auto" (no prompts)
764workspace_trust = "full_auto"
765
766[acp.zed.tools]
767read_file = true
768list_files = true
769
770# Cross-IDE editor context bridge
771[ide_context]
772enabled = true
773inject_into_prompt = true
774show_in_tui = true
775include_selection_text = true
776provider_mode = "auto"
777
778[ide_context.providers.vscode_compatible]
779enabled = true
780
781[ide_context.providers.zed]
782enabled = true
783
784[ide_context.providers.generic]
785enabled = true"#.to_string()
786 }
787
788 #[cfg(feature = "bootstrap")]
789 fn default_vtcode_gitignore() -> String {
790 r#"# Security-focused exclusions
791.env, .env.local, secrets/, .aws/, .ssh/
792
793# Development artifacts
794target/, build/, dist/, node_modules/, vendor/
795
796# Database files
797*.db, *.sqlite, *.sqlite3
798
799# Binary files
800*.exe, *.dll, *.so, *.dylib, *.bin
801
802# IDE files (comprehensive)
803.vscode/, .idea/, *.swp, *.swo
804"#
805 .to_string()
806 }
807
808 #[cfg(feature = "bootstrap")]
809 pub fn create_sample_config<P: AsRef<Path>>(output: P) -> Result<()> {
811 let output = output.as_ref();
812 let config_content = Self::default_vtcode_toml_template();
813
814 fs::write(output, config_content)
815 .with_context(|| format!("Failed to write config file: {}", output.display()))?;
816
817 Ok(())
818 }
819}