Skip to main content

vtcode_tui/config/
mod.rs

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