Skip to main content

vtcode_config/
root.rs

1use serde::{Deserialize, Serialize};
2
3use crate::status_line::StatusLineConfig;
4
5#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
6#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
7#[serde(rename_all = "snake_case")]
8#[derive(Default)]
9pub enum ToolOutputMode {
10    #[default]
11    Compact,
12    Full,
13}
14
15#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
16#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
17#[serde(rename_all = "snake_case")]
18#[derive(Default)]
19pub enum ReasoningDisplayMode {
20    Always,
21    #[default]
22    Toggle,
23    Hidden,
24}
25
26/// Layout mode override for responsive UI
27#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
28#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Default)]
29#[serde(rename_all = "snake_case")]
30pub enum LayoutModeOverride {
31    /// Auto-detect based on terminal size
32    #[default]
33    Auto,
34    /// Force compact mode (no borders)
35    Compact,
36    /// Force standard mode (borders, no sidebar/footer)
37    Standard,
38    /// Force wide mode (sidebar + footer)
39    Wide,
40}
41
42/// UI display mode variants for quick presets
43#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
44#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Default)]
45#[serde(rename_all = "snake_case")]
46pub enum UiDisplayMode {
47    /// Full UI with all features (sidebar, footer)
48    Full,
49    /// Minimal UI - no sidebar, no footer
50    #[default]
51    Minimal,
52    /// Focused mode - transcript only, maximum content space
53    Focused,
54}
55
56#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
57#[derive(Debug, Clone, Deserialize, Serialize)]
58pub struct UiConfig {
59    /// Tool output display mode ("compact" or "full")
60    #[serde(default = "default_tool_output_mode")]
61    pub tool_output_mode: ToolOutputMode,
62
63    /// Maximum number of lines to display in tool output (prevents transcript flooding)
64    #[serde(default = "default_tool_output_max_lines")]
65    pub tool_output_max_lines: usize,
66
67    /// Maximum bytes of output to display before auto-spooling to disk
68    #[serde(default = "default_tool_output_spool_bytes")]
69    pub tool_output_spool_bytes: usize,
70
71    /// Optional custom directory for spooled tool output logs
72    #[serde(default)]
73    pub tool_output_spool_dir: Option<String>,
74
75    /// Allow ANSI escape sequences in tool output (enables colors but may cause layout issues)
76    #[serde(default = "default_allow_tool_ansi")]
77    pub allow_tool_ansi: bool,
78
79    /// Number of rows to allocate for inline UI viewport
80    #[serde(default = "default_inline_viewport_rows")]
81    pub inline_viewport_rows: u16,
82
83    /// Reasoning display mode for chat UI ("always", "toggle", or "hidden")
84    #[serde(default = "default_reasoning_display_mode")]
85    pub reasoning_display_mode: ReasoningDisplayMode,
86
87    /// Default visibility for reasoning when display mode is "toggle"
88    #[serde(default = "default_reasoning_visible_default")]
89    pub reasoning_visible_default: bool,
90
91    /// Status line configuration settings
92    #[serde(default)]
93    pub status_line: StatusLineConfig,
94
95    /// Keyboard protocol enhancements for modern terminals (e.g. Kitty protocol)
96    #[serde(default)]
97    pub keyboard_protocol: KeyboardProtocolConfig,
98
99    /// Override the responsive layout mode
100    #[serde(default)]
101    pub layout_mode: LayoutModeOverride,
102
103    /// UI display mode preset (full, minimal, focused)
104    #[serde(default)]
105    pub display_mode: UiDisplayMode,
106
107    /// Show the right sidebar (queue, context, tools)
108    #[serde(default = "default_show_sidebar")]
109    pub show_sidebar: bool,
110
111    /// Dim completed todo items (- [x]) in agent output
112    #[serde(default = "default_dim_completed_todos")]
113    pub dim_completed_todos: bool,
114
115    /// Add spacing between message blocks
116    #[serde(default = "default_message_block_spacing")]
117    pub message_block_spacing: bool,
118
119    // === Color Accessibility Configuration ===
120    // Based on NO_COLOR standard, Ghostty minimum-contrast, and terminal color portability research
121    // See: https://no-color.org/, https://ghostty.org/docs/config/reference#minimum-contrast
122    /// Minimum contrast ratio for text against background (WCAG 2.1 standard)
123    /// - 4.5: WCAG AA (default, suitable for most users)
124    /// - 7.0: WCAG AAA (enhanced, for low-vision users)
125    /// - 3.0: Large text minimum
126    /// - 1.0: Disable contrast enforcement
127    #[serde(default = "default_minimum_contrast")]
128    pub minimum_contrast: f64,
129
130    /// Compatibility mode for legacy terminals that map bold to bright colors.
131    /// When enabled, avoids using bold styling on text that would become bright colors,
132    /// preventing visibility issues in terminals with "bold is bright" behavior.
133    #[serde(default = "default_bold_is_bright")]
134    pub bold_is_bright: bool,
135
136    /// Restrict color palette to the 11 "safe" ANSI colors portable across common themes.
137    /// Safe colors: red, green, yellow, blue, magenta, cyan + brred, brgreen, brmagenta, brcyan
138    /// Problematic colors avoided: brblack (invisible in Solarized Dark), bryellow (light themes),
139    /// white/brwhite (light themes), brblue (Basic Dark).
140    /// See: https://blog.xoria.org/terminal-colors/
141    #[serde(default = "default_safe_colors_only")]
142    pub safe_colors_only: bool,
143
144    /// Color scheme mode for automatic light/dark theme switching.
145    /// - "auto": Detect from terminal (via OSC 11 or COLORFGBG env var)
146    /// - "light": Force light mode theme selection
147    /// - "dark": Force dark mode theme selection
148    #[serde(default = "default_color_scheme_mode")]
149    pub color_scheme_mode: ColorSchemeMode,
150}
151
152/// Color scheme mode for theme selection
153#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
154#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Default)]
155#[serde(rename_all = "snake_case")]
156pub enum ColorSchemeMode {
157    /// Detect from terminal environment (OSC 11 query or COLORFGBG)
158    #[default]
159    Auto,
160    /// Force light color scheme
161    Light,
162    /// Force dark color scheme
163    Dark,
164}
165
166fn default_minimum_contrast() -> f64 {
167    crate::constants::ui::THEME_MIN_CONTRAST_RATIO
168}
169
170fn default_bold_is_bright() -> bool {
171    false
172}
173
174fn default_safe_colors_only() -> bool {
175    false
176}
177
178fn default_color_scheme_mode() -> ColorSchemeMode {
179    ColorSchemeMode::Auto
180}
181
182fn default_show_sidebar() -> bool {
183    true
184}
185
186fn default_dim_completed_todos() -> bool {
187    true
188}
189
190fn default_message_block_spacing() -> bool {
191    true
192}
193
194fn default_ask_questions_enabled() -> bool {
195    true
196}
197
198impl Default for UiConfig {
199    fn default() -> Self {
200        Self {
201            tool_output_mode: default_tool_output_mode(),
202            tool_output_max_lines: default_tool_output_max_lines(),
203            tool_output_spool_bytes: default_tool_output_spool_bytes(),
204            tool_output_spool_dir: None,
205            allow_tool_ansi: default_allow_tool_ansi(),
206            inline_viewport_rows: default_inline_viewport_rows(),
207            reasoning_display_mode: default_reasoning_display_mode(),
208            reasoning_visible_default: default_reasoning_visible_default(),
209            status_line: StatusLineConfig::default(),
210            keyboard_protocol: KeyboardProtocolConfig::default(),
211            layout_mode: LayoutModeOverride::default(),
212            display_mode: UiDisplayMode::default(),
213            show_sidebar: default_show_sidebar(),
214            dim_completed_todos: default_dim_completed_todos(),
215            message_block_spacing: default_message_block_spacing(),
216            // Color accessibility defaults
217            minimum_contrast: default_minimum_contrast(),
218            bold_is_bright: default_bold_is_bright(),
219            safe_colors_only: default_safe_colors_only(),
220            color_scheme_mode: default_color_scheme_mode(),
221        }
222    }
223}
224
225/// Chat configuration
226#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
227#[derive(Debug, Clone, Deserialize, Serialize, Default)]
228pub struct ChatConfig {
229    /// Ask Questions tool configuration (chat.askQuestions.*)
230    #[serde(default, rename = "askQuestions", alias = "ask_questions")]
231    pub ask_questions: AskQuestionsConfig,
232}
233
234/// Ask Questions tool configuration
235#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
236#[derive(Debug, Clone, Deserialize, Serialize)]
237pub struct AskQuestionsConfig {
238    /// Enable the Ask Questions tool in interactive chat
239    #[serde(default = "default_ask_questions_enabled")]
240    pub enabled: bool,
241}
242
243impl Default for AskQuestionsConfig {
244    fn default() -> Self {
245        Self {
246            enabled: default_ask_questions_enabled(),
247        }
248    }
249}
250
251/// PTY configuration
252#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
253#[derive(Debug, Clone, Deserialize, Serialize)]
254pub struct PtyConfig {
255    /// Enable PTY support for interactive commands
256    #[serde(default = "default_pty_enabled")]
257    pub enabled: bool,
258
259    /// Default terminal rows for PTY sessions
260    #[serde(default = "default_pty_rows")]
261    pub default_rows: u16,
262
263    /// Default terminal columns for PTY sessions
264    #[serde(default = "default_pty_cols")]
265    pub default_cols: u16,
266
267    /// Maximum number of concurrent PTY sessions
268    #[serde(default = "default_max_pty_sessions")]
269    pub max_sessions: usize,
270
271    /// Command timeout in seconds (prevents hanging commands)
272    #[serde(default = "default_pty_timeout")]
273    pub command_timeout_seconds: u64,
274
275    /// Number of recent PTY output lines to display in the chat transcript
276    #[serde(default = "default_stdout_tail_lines")]
277    pub stdout_tail_lines: usize,
278
279    /// Total scrollback buffer size (lines) retained per PTY session
280    #[serde(default = "default_scrollback_lines")]
281    pub scrollback_lines: usize,
282
283    /// Maximum bytes of output to retain per PTY session (prevents memory explosion)
284    #[serde(default = "default_max_scrollback_bytes")]
285    pub max_scrollback_bytes: usize,
286
287    /// Threshold (KB) at which to auto-spool large outputs to disk instead of memory
288    #[serde(default = "default_large_output_threshold_kb")]
289    pub large_output_threshold_kb: usize,
290
291    /// Preferred shell program for PTY sessions (e.g. "zsh", "bash"); falls back to $SHELL
292    #[serde(default)]
293    pub preferred_shell: Option<String>,
294}
295
296impl Default for PtyConfig {
297    fn default() -> Self {
298        Self {
299            enabled: default_pty_enabled(),
300            default_rows: default_pty_rows(),
301            default_cols: default_pty_cols(),
302            max_sessions: default_max_pty_sessions(),
303            command_timeout_seconds: default_pty_timeout(),
304            stdout_tail_lines: default_stdout_tail_lines(),
305            scrollback_lines: default_scrollback_lines(),
306            max_scrollback_bytes: default_max_scrollback_bytes(),
307            large_output_threshold_kb: default_large_output_threshold_kb(),
308            preferred_shell: None,
309        }
310    }
311}
312
313fn default_pty_enabled() -> bool {
314    true
315}
316
317fn default_pty_rows() -> u16 {
318    24
319}
320
321fn default_pty_cols() -> u16 {
322    80
323}
324
325fn default_max_pty_sessions() -> usize {
326    10
327}
328
329fn default_pty_timeout() -> u64 {
330    300
331}
332
333fn default_stdout_tail_lines() -> usize {
334    crate::constants::defaults::DEFAULT_PTY_STDOUT_TAIL_LINES
335}
336
337fn default_scrollback_lines() -> usize {
338    crate::constants::defaults::DEFAULT_PTY_SCROLLBACK_LINES
339}
340
341fn default_max_scrollback_bytes() -> usize {
342    // Reduced from 50MB to 25MB for memory-constrained development environments
343    // Can be overridden in vtcode.toml with: pty.max_scrollback_bytes = 52428800
344    25_000_000 // 25MB max to prevent memory explosion
345}
346
347fn default_large_output_threshold_kb() -> usize {
348    5_000 // 5MB threshold for auto-spooling
349}
350
351fn default_tool_output_mode() -> ToolOutputMode {
352    ToolOutputMode::Compact
353}
354
355fn default_tool_output_max_lines() -> usize {
356    600
357}
358
359fn default_tool_output_spool_bytes() -> usize {
360    200_000
361}
362
363fn default_allow_tool_ansi() -> bool {
364    false
365}
366
367fn default_inline_viewport_rows() -> u16 {
368    crate::constants::ui::DEFAULT_INLINE_VIEWPORT_ROWS
369}
370
371fn default_reasoning_display_mode() -> ReasoningDisplayMode {
372    ReasoningDisplayMode::Toggle
373}
374
375fn default_reasoning_visible_default() -> bool {
376    crate::constants::ui::DEFAULT_REASONING_VISIBLE
377}
378
379/// Kitty keyboard protocol configuration
380/// Reference: https://sw.kovidgoyal.net/kitty/keyboard-protocol/
381#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
382#[derive(Debug, Clone, Deserialize, Serialize)]
383pub struct KeyboardProtocolConfig {
384    /// Enable keyboard protocol enhancements (master toggle)
385    #[serde(default = "default_keyboard_protocol_enabled")]
386    pub enabled: bool,
387
388    /// Preset mode: "default", "full", "minimal", or "custom"
389    #[serde(default = "default_keyboard_protocol_mode")]
390    pub mode: String,
391
392    /// Resolve Esc key ambiguity (recommended for performance)
393    #[serde(default = "default_disambiguate_escape_codes")]
394    pub disambiguate_escape_codes: bool,
395
396    /// Report press, release, and repeat events
397    #[serde(default = "default_report_event_types")]
398    pub report_event_types: bool,
399
400    /// Report alternate key layouts (e.g. for non-US keyboards)
401    #[serde(default = "default_report_alternate_keys")]
402    pub report_alternate_keys: bool,
403
404    /// Report all keys, including modifier-only keys (Shift, Ctrl)
405    #[serde(default = "default_report_all_keys")]
406    pub report_all_keys: bool,
407}
408
409impl Default for KeyboardProtocolConfig {
410    fn default() -> Self {
411        Self {
412            enabled: default_keyboard_protocol_enabled(),
413            mode: default_keyboard_protocol_mode(),
414            disambiguate_escape_codes: default_disambiguate_escape_codes(),
415            report_event_types: default_report_event_types(),
416            report_alternate_keys: default_report_alternate_keys(),
417            report_all_keys: default_report_all_keys(),
418        }
419    }
420}
421
422impl KeyboardProtocolConfig {
423    pub fn validate(&self) -> anyhow::Result<()> {
424        match self.mode.as_str() {
425            "default" | "full" | "minimal" | "custom" => Ok(()),
426            _ => anyhow::bail!(
427                "Invalid keyboard protocol mode '{}'. Must be: default, full, minimal, or custom",
428                self.mode
429            ),
430        }
431    }
432}
433
434fn default_keyboard_protocol_enabled() -> bool {
435    std::env::var("VTCODE_KEYBOARD_PROTOCOL_ENABLED")
436        .ok()
437        .and_then(|v| v.parse().ok())
438        .unwrap_or(true)
439}
440
441fn default_keyboard_protocol_mode() -> String {
442    std::env::var("VTCODE_KEYBOARD_PROTOCOL_MODE").unwrap_or_else(|_| "default".to_string())
443}
444
445fn default_disambiguate_escape_codes() -> bool {
446    true
447}
448
449fn default_report_event_types() -> bool {
450    true
451}
452
453fn default_report_alternate_keys() -> bool {
454    true
455}
456
457fn default_report_all_keys() -> bool {
458    false
459}