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