Skip to main content

vtcode_ui/tui/config/
mod.rs

1/// TUI configuration constants split into sub-modules for defaults and UI tuning.
2pub mod constants {
3    pub mod defaults;
4    pub mod ui;
5}
6
7/// Config file loading and deserialization.
8pub mod loader;
9/// Configuration type definitions re-exported for convenience.
10pub mod types;
11
12use anyhow::Result;
13use serde::{Deserialize, Serialize};
14use vtcode_commons::terminal_detection::is_ghostty_terminal;
15
16pub use types::{ReasoningEffortLevel, SystemPromptMode, ToolDocumentationMode, UiSurfacePreference, VerbosityLevel};
17
18/// Controls how tool call output is displayed in the TUI.
19#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Default)]
20#[serde(rename_all = "snake_case")]
21pub enum ToolOutputMode {
22    /// Show a condensed summary of tool output.
23    #[default]
24    Compact,
25    /// Show the full, untruncated tool output.
26    Full,
27}
28
29/// Controls the overall density of the terminal UI layout.
30#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Default)]
31#[serde(rename_all = "snake_case")]
32pub enum UiDisplayMode {
33    /// Full layout with all panels visible.
34    Full,
35    /// Compact layout that minimizes chrome.
36    #[default]
37    Minimal,
38    /// Focuses on the active input area with reduced side panels.
39    Focused,
40}
41
42/// Determines how desktop notifications are delivered.
43#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Default)]
44#[serde(rename_all = "snake_case")]
45pub enum NotificationDeliveryMode {
46    /// Show notifications only in the terminal.
47    Terminal,
48    /// Use both terminal and desktop notifications.
49    #[default]
50    Hybrid,
51    /// Show notifications via the OS desktop notification system.
52    Desktop,
53}
54
55/// Workspace trust mode for the Agent Client Protocol (Zed integration).
56#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Default)]
57#[serde(rename_all = "snake_case")]
58pub enum AgentClientProtocolZedWorkspaceTrustMode {
59    /// Grant full automatic trust to all workspace operations.
60    FullAuto,
61    /// Use per-tool policy checks for untrusted operations.
62    #[default]
63    ToolsPolicy,
64}
65
66pub use vtcode_config::core::tools::ToolPolicy;
67
68/// Configuration for the terminal keyboard protocol (Kitty progressive enhancement).
69#[derive(Debug, Clone, Deserialize, Serialize)]
70pub struct KeyboardProtocolConfig {
71    pub(crate) enabled: bool,
72    pub(crate) mode: String,
73    pub(crate) disambiguate_escape_codes: bool,
74    pub(crate) report_event_types: bool,
75    pub(crate) report_alternate_keys: bool,
76    pub(crate) report_all_keys: bool,
77}
78
79impl Default for KeyboardProtocolConfig {
80    fn default() -> Self {
81        Self {
82            enabled: true,
83            mode: "default".to_string(),
84            disambiguate_escape_codes: true,
85            report_event_types: true,
86            report_alternate_keys: true,
87            report_all_keys: false,
88        }
89    }
90}
91
92impl KeyboardProtocolConfig {
93    /// Validate the keyboard protocol configuration.
94    pub fn validate(&self) -> Result<()> {
95        match self.mode.as_str() {
96            "default" | "full" | "minimal" | "custom" => Ok(()),
97            _ => anyhow::bail!(
98                "Invalid keyboard protocol mode '{}'. Must be: default, full, minimal, or custom",
99                self.mode
100            ),
101        }
102    }
103}
104
105/// Configuration for UI notification behavior.
106#[derive(Debug, Clone, Deserialize, Serialize)]
107pub struct UiNotificationsConfig {
108    pub enabled: bool,
109    pub delivery_mode: NotificationDeliveryMode,
110    pub suppress_when_focused: bool,
111    pub tool_failure: bool,
112    pub error: bool,
113    pub completion: bool,
114    pub hitl: bool,
115    pub tool_success: bool,
116}
117
118impl Default for UiNotificationsConfig {
119    fn default() -> Self {
120        Self {
121            enabled: true,
122            delivery_mode: NotificationDeliveryMode::Hybrid,
123            suppress_when_focused: true,
124            tool_failure: true,
125            error: true,
126            completion: true,
127            hitl: true,
128            tool_success: false,
129        }
130    }
131}
132
133/// Top-level UI configuration controlling display modes and input behavior.
134#[derive(Debug, Clone, Deserialize, Serialize)]
135pub struct UiConfig {
136    pub tool_output_mode: ToolOutputMode,
137    pub allow_tool_ansi: bool,
138    pub inline_viewport_rows: u16,
139    pub keyboard_protocol: KeyboardProtocolConfig,
140    pub display_mode: UiDisplayMode,
141    pub show_sidebar: bool,
142    pub dim_completed_todos: bool,
143    pub message_block_spacing: bool,
144    pub show_turn_timer: bool,
145    pub notifications: UiNotificationsConfig,
146}
147
148impl Default for UiConfig {
149    fn default() -> Self {
150        Self {
151            tool_output_mode: ToolOutputMode::Compact,
152            allow_tool_ansi: false,
153            inline_viewport_rows: constants::ui::DEFAULT_INLINE_VIEWPORT_ROWS,
154            keyboard_protocol: KeyboardProtocolConfig::default(),
155            display_mode: UiDisplayMode::Minimal,
156            show_sidebar: true,
157            dim_completed_todos: true,
158            message_block_spacing: false,
159            show_turn_timer: false,
160            notifications: UiNotificationsConfig::default(),
161        }
162    }
163}
164
165/// Configuration for agent checkpoint persistence.
166#[derive(Debug, Clone, Deserialize, Serialize, Default)]
167pub struct AgentCheckpointingConfig {
168    pub enabled: bool,
169}
170
171/// Configuration for the small (fast/cheap) model used for lightweight tasks.
172#[derive(Debug, Clone, Deserialize, Serialize, Default)]
173pub struct AgentSmallModelConfig {
174    pub enabled: bool,
175}
176
177#[derive(Debug, Clone, Deserialize, Serialize, Default)]
178pub struct AgentVibeCodingConfig {
179    pub enabled: bool,
180}
181
182/// Top-level agent configuration covering model selection, prompt style, and limits.
183#[derive(Debug, Clone, Deserialize, Serialize)]
184pub struct AgentConfig {
185    pub default_model: String,
186    pub theme: String,
187    pub reasoning_effort: ReasoningEffortLevel,
188    pub system_prompt_mode: SystemPromptMode,
189    pub tool_documentation_mode: ToolDocumentationMode,
190    pub verbosity: VerbosityLevel,
191    pub todo_planning_mode: bool,
192    pub checkpointing: AgentCheckpointingConfig,
193    pub small_model: AgentSmallModelConfig,
194    pub vibe_coding: AgentVibeCodingConfig,
195    pub max_conversation_turns: usize,
196}
197
198impl Default for AgentConfig {
199    fn default() -> Self {
200        Self {
201            default_model: "gpt-5-mini".to_string(),
202            theme: "default".to_string(),
203            reasoning_effort: ReasoningEffortLevel::Medium,
204            system_prompt_mode: SystemPromptMode::Default,
205            tool_documentation_mode: ToolDocumentationMode::Progressive,
206            verbosity: VerbosityLevel::Medium,
207            todo_planning_mode: false,
208            checkpointing: AgentCheckpointingConfig::default(),
209            small_model: AgentSmallModelConfig::default(),
210            vibe_coding: AgentVibeCodingConfig::default(),
211            max_conversation_turns: 100,
212        }
213    }
214}
215
216/// Configuration for LLM prompt caching.
217#[derive(Debug, Clone, Deserialize, Serialize)]
218pub struct PromptCacheConfig {
219    pub enabled: bool,
220}
221
222impl Default for PromptCacheConfig {
223    fn default() -> Self {
224        Self { enabled: true }
225    }
226}
227
228/// Configuration for Model Context Protocol connections.
229#[derive(Debug, Clone, Deserialize, Serialize)]
230pub struct McpConfig {
231    pub enabled: bool,
232}
233
234impl Default for McpConfig {
235    fn default() -> Self {
236        Self { enabled: true }
237    }
238}
239
240/// Zed-specific Agent Client Protocol configuration.
241#[derive(Debug, Clone, Deserialize, Serialize)]
242pub struct AcpZedConfig {
243    pub workspace_trust: AgentClientProtocolZedWorkspaceTrustMode,
244}
245
246impl Default for AcpZedConfig {
247    fn default() -> Self {
248        Self {
249            workspace_trust: AgentClientProtocolZedWorkspaceTrustMode::ToolsPolicy,
250        }
251    }
252}
253
254/// Agent Client Protocol configuration.
255#[derive(Debug, Clone, Deserialize, Serialize, Default)]
256pub struct AcpConfig {
257    pub zed: AcpZedConfig,
258}
259
260/// Configuration for full-automation mode.
261#[derive(Debug, Clone, Deserialize, Serialize, Default)]
262pub struct FullAutoConfig {
263    pub enabled: bool,
264}
265
266/// Automation settings controlling automatic execution behavior.
267#[derive(Debug, Clone, Deserialize, Serialize, Default)]
268pub struct AutomationConfig {
269    pub full_auto: FullAutoConfig,
270}
271
272/// Tool execution policy configuration.
273#[derive(Debug, Clone, Deserialize, Serialize)]
274pub struct ToolsConfig {
275    pub default_policy: ToolPolicy,
276}
277
278impl Default for ToolsConfig {
279    fn default() -> Self {
280        Self { default_policy: ToolPolicy::Prompt }
281    }
282}
283
284/// Security-related settings.
285#[derive(Debug, Clone, Deserialize, Serialize)]
286pub struct SecurityConfig {
287    pub human_in_the_loop: bool,
288}
289
290impl Default for SecurityConfig {
291    fn default() -> Self {
292        Self { human_in_the_loop: true }
293    }
294}
295
296/// Context window management settings.
297#[derive(Debug, Clone, Deserialize, Serialize)]
298pub struct ContextConfig {
299    pub max_context_tokens: usize,
300    pub trim_to_percent: u8,
301}
302
303impl Default for ContextConfig {
304    fn default() -> Self {
305        Self { max_context_tokens: 128_000, trim_to_percent: 80 }
306    }
307}
308
309/// Configuration for syntax highlighting in code blocks.
310#[derive(Debug, Clone, Deserialize, Serialize)]
311pub struct SyntaxHighlightingConfig {
312    pub enabled: bool,
313    pub theme: String,
314    pub cache_themes: bool,
315    pub max_file_size_mb: usize,
316    pub enabled_languages: Vec<String>,
317    pub highlight_timeout_ms: u64,
318}
319
320impl Default for SyntaxHighlightingConfig {
321    fn default() -> Self {
322        Self {
323            enabled: true,
324            theme: "base16-ocean.dark".to_string(),
325            cache_themes: true,
326            max_file_size_mb: 5,
327            enabled_languages: vec![
328                "rust".to_string(),
329                "python".to_string(),
330                "javascript".to_string(),
331                "typescript".to_string(),
332                "go".to_string(),
333                "bash".to_string(),
334                "json".to_string(),
335                "yaml".to_string(),
336                "toml".to_string(),
337                "markdown".to_string(),
338            ],
339            highlight_timeout_ms: 300,
340        }
341    }
342}
343
344/// Pseudo-terminal (PTY) configuration for shell sessions.
345#[derive(Debug, Clone, Deserialize, Serialize)]
346pub struct PtyConfig {
347    pub enabled: bool,
348    pub default_rows: u16,
349    pub default_cols: u16,
350    pub command_timeout_seconds: u64,
351}
352
353impl Default for PtyConfig {
354    fn default() -> Self {
355        Self {
356            enabled: true,
357            default_rows: 24,
358            default_cols: 80,
359            command_timeout_seconds: 300,
360        }
361    }
362}
363
364/// Convert KeyboardProtocolConfig to crossterm keyboard enhancement flags.
365pub fn keyboard_protocol_to_flags(config: &KeyboardProtocolConfig) -> crossterm::event::KeyboardEnhancementFlags {
366    keyboard_protocol_to_flags_for_terminal(
367        config,
368        cfg!(target_os = "macos"),
369        std::env::var("TERM_PROGRAM").ok().as_deref(),
370        std::env::var("TERM").ok().as_deref(),
371    )
372}
373
374fn keyboard_protocol_to_flags_for_terminal(
375    config: &KeyboardProtocolConfig,
376    is_macos: bool,
377    term_program: Option<&str>,
378    term: Option<&str>,
379) -> crossterm::event::KeyboardEnhancementFlags {
380    use ratatui::crossterm::event::KeyboardEnhancementFlags;
381
382    if !config.enabled {
383        return KeyboardEnhancementFlags::empty();
384    }
385
386    let mut flags = match config.mode.as_str() {
387        "default" => {
388            KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
389                | KeyboardEnhancementFlags::REPORT_EVENT_TYPES
390                | KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS
391        }
392        "full" => {
393            KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
394                | KeyboardEnhancementFlags::REPORT_EVENT_TYPES
395                | KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS
396                | KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES
397        }
398        "minimal" => KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES,
399        "custom" => {
400            let mut flags = KeyboardEnhancementFlags::empty();
401            if config.disambiguate_escape_codes {
402                flags |= KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES;
403            }
404            if config.report_event_types {
405                flags |= KeyboardEnhancementFlags::REPORT_EVENT_TYPES;
406            }
407            if config.report_alternate_keys {
408                flags |= KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS;
409            }
410            if config.report_all_keys {
411                flags |= KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES;
412            }
413            flags
414        }
415        _ => {
416            KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES
417                | KeyboardEnhancementFlags::REPORT_EVENT_TYPES
418                | KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS
419        }
420    };
421
422    if should_force_report_all_keys(config.mode.as_str(), is_macos, term_program, term) {
423        flags |= KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES;
424    }
425
426    flags
427}
428
429fn should_force_report_all_keys(mode: &str, is_macos: bool, term_program: Option<&str>, term: Option<&str>) -> bool {
430    if !is_macos || !matches!(mode, "default") {
431        return false;
432    }
433
434    // Ghostty on macOS needs "report all keys" enabled so bare Command presses
435    // surface as modifier-key events that transcript link clicks can merge in.
436    is_ghostty_terminal(term_program, term)
437}
438
439#[cfg(test)]
440mod keyboard_protocol_tests {
441    use super::*;
442    use ratatui::crossterm::event::KeyboardEnhancementFlags;
443
444    fn default_keyboard_protocol_config() -> KeyboardProtocolConfig {
445        KeyboardProtocolConfig {
446            enabled: true,
447            mode: "default".to_string(),
448            disambiguate_escape_codes: true,
449            report_event_types: true,
450            report_alternate_keys: true,
451            report_all_keys: false,
452        }
453    }
454
455    #[test]
456    fn keyboard_protocol_defaults_keep_standard_flags() {
457        let flags = keyboard_protocol_to_flags_for_terminal(
458            &default_keyboard_protocol_config(),
459            false,
460            Some("Ghostty"),
461            Some("xterm-ghostty"),
462        );
463
464        assert!(flags.contains(KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES));
465        assert!(flags.contains(KeyboardEnhancementFlags::REPORT_EVENT_TYPES));
466        assert!(flags.contains(KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS));
467        assert!(!flags.contains(KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES));
468    }
469
470    #[test]
471    fn keyboard_protocol_defaults_enable_all_keys_for_ghostty_on_macos() {
472        let flags = keyboard_protocol_to_flags_for_terminal(
473            &default_keyboard_protocol_config(),
474            true,
475            Some("Ghostty"),
476            Some("xterm-ghostty"),
477        );
478
479        assert!(flags.contains(KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES));
480    }
481
482    #[test]
483    fn keyboard_protocol_custom_mode_respects_explicit_report_all_keys_setting() {
484        let flags = keyboard_protocol_to_flags_for_terminal(
485            &KeyboardProtocolConfig {
486                enabled: true,
487                mode: "custom".to_string(),
488                disambiguate_escape_codes: true,
489                report_event_types: true,
490                report_alternate_keys: true,
491                report_all_keys: false,
492            },
493            true,
494            Some("Ghostty"),
495            Some("xterm-ghostty"),
496        );
497
498        assert!(!flags.contains(KeyboardEnhancementFlags::REPORT_ALL_KEYS_AS_ESCAPE_CODES));
499    }
500}