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