Skip to main content

vtcode_config/loader/
config.rs

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