Skip to main content

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