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