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