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/// Notification delivery mode for terminal attention events.
57#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
58#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Default)]
59#[serde(rename_all = "snake_case")]
60pub enum NotificationDeliveryMode {
61    /// Terminal-native alerts only (bell/OSC).
62    Terminal,
63    /// Terminal alerts with desktop notifications when supported.
64    #[default]
65    Hybrid,
66    /// Desktop notifications first; fall back to terminal alerts when unavailable.
67    Desktop,
68}
69
70/// Notification preferences for terminal and desktop alerts.
71#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
72#[derive(Debug, Clone, Deserialize, Serialize)]
73pub struct UiNotificationsConfig {
74    /// Master toggle for all runtime notifications.
75    #[serde(default = "default_notifications_enabled")]
76    pub enabled: bool,
77
78    /// Notification transport strategy.
79    #[serde(default)]
80    pub delivery_mode: NotificationDeliveryMode,
81
82    /// Suppress notifications while terminal focus is active.
83    #[serde(default = "default_notifications_suppress_when_focused")]
84    pub suppress_when_focused: bool,
85
86    /// Notify when a tool call fails.
87    #[serde(default = "default_notifications_tool_failure")]
88    pub tool_failure: bool,
89
90    /// Notify on runtime/system errors.
91    #[serde(default = "default_notifications_error")]
92    pub error: bool,
93
94    /// Notify on turn/session completion events.
95    #[serde(default = "default_notifications_completion")]
96    pub completion: bool,
97
98    /// Notify when human input/approval is required.
99    #[serde(default = "default_notifications_hitl")]
100    pub hitl: bool,
101
102    /// Notify on successful tool calls.
103    #[serde(default = "default_notifications_tool_success")]
104    pub tool_success: bool,
105}
106
107impl Default for UiNotificationsConfig {
108    fn default() -> Self {
109        Self {
110            enabled: default_notifications_enabled(),
111            delivery_mode: NotificationDeliveryMode::default(),
112            suppress_when_focused: default_notifications_suppress_when_focused(),
113            tool_failure: default_notifications_tool_failure(),
114            error: default_notifications_error(),
115            completion: default_notifications_completion(),
116            hitl: default_notifications_hitl(),
117            tool_success: default_notifications_tool_success(),
118        }
119    }
120}
121
122#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
123#[derive(Debug, Clone, Deserialize, Serialize)]
124pub struct UiConfig {
125    /// Tool output display mode ("compact" or "full")
126    #[serde(default = "default_tool_output_mode")]
127    pub tool_output_mode: ToolOutputMode,
128
129    /// Maximum number of lines to display in tool output (prevents transcript flooding)
130    #[serde(default = "default_tool_output_max_lines")]
131    pub tool_output_max_lines: usize,
132
133    /// Maximum bytes of output to display before auto-spooling to disk
134    #[serde(default = "default_tool_output_spool_bytes")]
135    pub tool_output_spool_bytes: usize,
136
137    /// Optional custom directory for spooled tool output logs
138    #[serde(default)]
139    pub tool_output_spool_dir: Option<String>,
140
141    /// Allow ANSI escape sequences in tool output (enables colors but may cause layout issues)
142    #[serde(default = "default_allow_tool_ansi")]
143    pub allow_tool_ansi: bool,
144
145    /// Number of rows to allocate for inline UI viewport
146    #[serde(default = "default_inline_viewport_rows")]
147    pub inline_viewport_rows: u16,
148
149    /// Reasoning display mode for chat UI ("always", "toggle", or "hidden")
150    #[serde(default = "default_reasoning_display_mode")]
151    pub reasoning_display_mode: ReasoningDisplayMode,
152
153    /// Default visibility for reasoning when display mode is "toggle"
154    #[serde(default = "default_reasoning_visible_default")]
155    pub reasoning_visible_default: bool,
156
157    /// Status line configuration settings
158    #[serde(default)]
159    pub status_line: StatusLineConfig,
160
161    /// Keyboard protocol enhancements for modern terminals (e.g. Kitty protocol)
162    #[serde(default)]
163    pub keyboard_protocol: KeyboardProtocolConfig,
164
165    /// Override the responsive layout mode
166    #[serde(default)]
167    pub layout_mode: LayoutModeOverride,
168
169    /// UI display mode preset (full, minimal, focused)
170    #[serde(default)]
171    pub display_mode: UiDisplayMode,
172
173    /// Show the right sidebar (queue, context, tools)
174    #[serde(default = "default_show_sidebar")]
175    pub show_sidebar: bool,
176
177    /// Dim completed todo items (- [x]) in agent output
178    #[serde(default = "default_dim_completed_todos")]
179    pub dim_completed_todos: bool,
180
181    /// Add spacing between message blocks
182    #[serde(default = "default_message_block_spacing")]
183    pub message_block_spacing: bool,
184
185    /// Show per-turn elapsed timer line after completed turns
186    #[serde(default = "default_show_turn_timer")]
187    pub show_turn_timer: bool,
188
189    /// Show warning/error/fatal diagnostic lines in the TUI transcript and log panel.
190    /// Also controls whether ERROR-level tracing logs appear in the TUI session log.
191    /// Errors are always captured in the session archive JSON regardless of this setting.
192    #[serde(default = "default_show_diagnostics_in_transcript")]
193    pub show_diagnostics_in_transcript: bool,
194
195    // === Color Accessibility Configuration ===
196    // Based on NO_COLOR standard, Ghostty minimum-contrast, and terminal color portability research
197    // See: https://no-color.org/, https://ghostty.org/docs/config/reference#minimum-contrast
198    /// Minimum contrast ratio for text against background (WCAG 2.1 standard)
199    /// - 4.5: WCAG AA (default, suitable for most users)
200    /// - 7.0: WCAG AAA (enhanced, for low-vision users)
201    /// - 3.0: Large text minimum
202    /// - 1.0: Disable contrast enforcement
203    #[serde(default = "default_minimum_contrast")]
204    pub minimum_contrast: f64,
205
206    /// Compatibility mode for legacy terminals that map bold to bright colors.
207    /// When enabled, avoids using bold styling on text that would become bright colors,
208    /// preventing visibility issues in terminals with "bold is bright" behavior.
209    #[serde(default = "default_bold_is_bright")]
210    pub bold_is_bright: bool,
211
212    /// Restrict color palette to the 11 "safe" ANSI colors portable across common themes.
213    /// Safe colors: red, green, yellow, blue, magenta, cyan + brred, brgreen, brmagenta, brcyan
214    /// Problematic colors avoided: brblack (invisible in Solarized Dark), bryellow (light themes),
215    /// white/brwhite (light themes), brblue (Basic Dark).
216    /// See: https://blog.xoria.org/terminal-colors/
217    #[serde(default = "default_safe_colors_only")]
218    pub safe_colors_only: bool,
219
220    /// Color scheme mode for automatic light/dark theme switching.
221    /// - "auto": Detect from terminal (via OSC 11 or COLORFGBG env var)
222    /// - "light": Force light mode theme selection
223    /// - "dark": Force dark mode theme selection
224    #[serde(default = "default_color_scheme_mode")]
225    pub color_scheme_mode: ColorSchemeMode,
226
227    /// Notification preferences for attention events.
228    #[serde(default)]
229    pub notifications: UiNotificationsConfig,
230
231    /// Screen reader mode: disables animations, uses plain text indicators,
232    /// and optimizes output for assistive technology compatibility.
233    /// Can also be enabled via VTCODE_SCREEN_READER=1 environment variable.
234    #[serde(default = "default_screen_reader_mode")]
235    pub screen_reader_mode: bool,
236
237    /// Reduce motion mode: minimizes shimmer/flashing animations.
238    /// Can also be enabled via VTCODE_REDUCE_MOTION=1 environment variable.
239    #[serde(default = "default_reduce_motion_mode")]
240    pub reduce_motion_mode: bool,
241
242    /// Keep animated progress indicators while reduce_motion_mode is enabled.
243    #[serde(default = "default_reduce_motion_keep_progress_animation")]
244    pub reduce_motion_keep_progress_animation: bool,
245}
246
247/// Color scheme mode for theme selection
248#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
249#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Default)]
250#[serde(rename_all = "snake_case")]
251pub enum ColorSchemeMode {
252    /// Detect from terminal environment (OSC 11 query or COLORFGBG)
253    #[default]
254    Auto,
255    /// Force light color scheme
256    Light,
257    /// Force dark color scheme
258    Dark,
259}
260
261fn default_minimum_contrast() -> f64 {
262    crate::constants::ui::THEME_MIN_CONTRAST_RATIO
263}
264
265fn default_bold_is_bright() -> bool {
266    false
267}
268
269fn default_safe_colors_only() -> bool {
270    false
271}
272
273fn default_color_scheme_mode() -> ColorSchemeMode {
274    ColorSchemeMode::Auto
275}
276
277fn default_show_sidebar() -> bool {
278    true
279}
280
281fn default_dim_completed_todos() -> bool {
282    true
283}
284
285fn default_message_block_spacing() -> bool {
286    true
287}
288
289fn default_show_turn_timer() -> bool {
290    false
291}
292
293fn default_show_diagnostics_in_transcript() -> bool {
294    false
295}
296
297fn default_notifications_enabled() -> bool {
298    true
299}
300
301fn default_notifications_suppress_when_focused() -> bool {
302    true
303}
304
305fn default_notifications_tool_failure() -> bool {
306    true
307}
308
309fn default_notifications_error() -> bool {
310    true
311}
312
313fn default_notifications_completion() -> bool {
314    true
315}
316
317fn default_notifications_hitl() -> bool {
318    true
319}
320
321fn default_notifications_tool_success() -> bool {
322    false
323}
324
325fn env_bool_var(name: &str) -> Option<bool> {
326    std::env::var(name).ok().and_then(|v| {
327        let normalized = v.trim().to_ascii_lowercase();
328        match normalized.as_str() {
329            "1" | "true" | "yes" | "on" => Some(true),
330            "0" | "false" | "no" | "off" => Some(false),
331            _ => None,
332        }
333    })
334}
335
336fn default_screen_reader_mode() -> bool {
337    env_bool_var("VTCODE_SCREEN_READER").unwrap_or(false)
338}
339
340fn default_reduce_motion_mode() -> bool {
341    env_bool_var("VTCODE_REDUCE_MOTION").unwrap_or(false)
342}
343
344fn default_reduce_motion_keep_progress_animation() -> bool {
345    false
346}
347
348fn default_ask_questions_enabled() -> bool {
349    true
350}
351
352impl Default for UiConfig {
353    fn default() -> Self {
354        Self {
355            tool_output_mode: default_tool_output_mode(),
356            tool_output_max_lines: default_tool_output_max_lines(),
357            tool_output_spool_bytes: default_tool_output_spool_bytes(),
358            tool_output_spool_dir: None,
359            allow_tool_ansi: default_allow_tool_ansi(),
360            inline_viewport_rows: default_inline_viewport_rows(),
361            reasoning_display_mode: default_reasoning_display_mode(),
362            reasoning_visible_default: default_reasoning_visible_default(),
363            status_line: StatusLineConfig::default(),
364            keyboard_protocol: KeyboardProtocolConfig::default(),
365            layout_mode: LayoutModeOverride::default(),
366            display_mode: UiDisplayMode::default(),
367            show_sidebar: default_show_sidebar(),
368            dim_completed_todos: default_dim_completed_todos(),
369            message_block_spacing: default_message_block_spacing(),
370            show_turn_timer: default_show_turn_timer(),
371            show_diagnostics_in_transcript: default_show_diagnostics_in_transcript(),
372            // Color accessibility defaults
373            minimum_contrast: default_minimum_contrast(),
374            bold_is_bright: default_bold_is_bright(),
375            safe_colors_only: default_safe_colors_only(),
376            color_scheme_mode: default_color_scheme_mode(),
377            notifications: UiNotificationsConfig::default(),
378            screen_reader_mode: default_screen_reader_mode(),
379            reduce_motion_mode: default_reduce_motion_mode(),
380            reduce_motion_keep_progress_animation: default_reduce_motion_keep_progress_animation(),
381        }
382    }
383}
384
385/// Chat configuration
386#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
387#[derive(Debug, Clone, Deserialize, Serialize, Default)]
388pub struct ChatConfig {
389    /// Ask Questions tool configuration (chat.askQuestions.*)
390    #[serde(default, rename = "askQuestions", alias = "ask_questions")]
391    pub ask_questions: AskQuestionsConfig,
392}
393
394/// Ask Questions tool configuration
395#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
396#[derive(Debug, Clone, Deserialize, Serialize)]
397pub struct AskQuestionsConfig {
398    /// Enable the Ask Questions tool in interactive chat
399    #[serde(default = "default_ask_questions_enabled")]
400    pub enabled: bool,
401}
402
403impl Default for AskQuestionsConfig {
404    fn default() -> Self {
405        Self {
406            enabled: default_ask_questions_enabled(),
407        }
408    }
409}
410
411/// PTY configuration
412#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
413#[derive(Debug, Clone, Deserialize, Serialize)]
414pub struct PtyConfig {
415    /// Enable PTY support for interactive commands
416    #[serde(default = "default_pty_enabled")]
417    pub enabled: bool,
418
419    /// Default terminal rows for PTY sessions
420    #[serde(default = "default_pty_rows")]
421    pub default_rows: u16,
422
423    /// Default terminal columns for PTY sessions
424    #[serde(default = "default_pty_cols")]
425    pub default_cols: u16,
426
427    /// Maximum number of concurrent PTY sessions
428    #[serde(default = "default_max_pty_sessions")]
429    pub max_sessions: usize,
430
431    /// Command timeout in seconds (prevents hanging commands)
432    #[serde(default = "default_pty_timeout")]
433    pub command_timeout_seconds: u64,
434
435    /// Number of recent PTY output lines to display in the chat transcript
436    #[serde(default = "default_stdout_tail_lines")]
437    pub stdout_tail_lines: usize,
438
439    /// Total scrollback buffer size (lines) retained per PTY session
440    #[serde(default = "default_scrollback_lines")]
441    pub scrollback_lines: usize,
442
443    /// Maximum bytes of output to retain per PTY session (prevents memory explosion)
444    #[serde(default = "default_max_scrollback_bytes")]
445    pub max_scrollback_bytes: usize,
446
447    /// Threshold (KB) at which to auto-spool large outputs to disk instead of memory
448    #[serde(default = "default_large_output_threshold_kb")]
449    pub large_output_threshold_kb: usize,
450
451    /// Preferred shell program for PTY sessions (e.g. "zsh", "bash"); falls back to $SHELL
452    #[serde(default)]
453    pub preferred_shell: Option<String>,
454}
455
456impl Default for PtyConfig {
457    fn default() -> Self {
458        Self {
459            enabled: default_pty_enabled(),
460            default_rows: default_pty_rows(),
461            default_cols: default_pty_cols(),
462            max_sessions: default_max_pty_sessions(),
463            command_timeout_seconds: default_pty_timeout(),
464            stdout_tail_lines: default_stdout_tail_lines(),
465            scrollback_lines: default_scrollback_lines(),
466            max_scrollback_bytes: default_max_scrollback_bytes(),
467            large_output_threshold_kb: default_large_output_threshold_kb(),
468            preferred_shell: None,
469        }
470    }
471}
472
473fn default_pty_enabled() -> bool {
474    true
475}
476
477fn default_pty_rows() -> u16 {
478    24
479}
480
481fn default_pty_cols() -> u16 {
482    80
483}
484
485fn default_max_pty_sessions() -> usize {
486    10
487}
488
489fn default_pty_timeout() -> u64 {
490    300
491}
492
493fn default_stdout_tail_lines() -> usize {
494    crate::constants::defaults::DEFAULT_PTY_STDOUT_TAIL_LINES
495}
496
497fn default_scrollback_lines() -> usize {
498    crate::constants::defaults::DEFAULT_PTY_SCROLLBACK_LINES
499}
500
501fn default_max_scrollback_bytes() -> usize {
502    // Reduced from 50MB to 25MB for memory-constrained development environments
503    // Can be overridden in vtcode.toml with: pty.max_scrollback_bytes = 52428800
504    25_000_000 // 25MB max to prevent memory explosion
505}
506
507fn default_large_output_threshold_kb() -> usize {
508    5_000 // 5MB threshold for auto-spooling
509}
510
511fn default_tool_output_mode() -> ToolOutputMode {
512    ToolOutputMode::Compact
513}
514
515fn default_tool_output_max_lines() -> usize {
516    600
517}
518
519fn default_tool_output_spool_bytes() -> usize {
520    200_000
521}
522
523fn default_allow_tool_ansi() -> bool {
524    false
525}
526
527fn default_inline_viewport_rows() -> u16 {
528    crate::constants::ui::DEFAULT_INLINE_VIEWPORT_ROWS
529}
530
531fn default_reasoning_display_mode() -> ReasoningDisplayMode {
532    ReasoningDisplayMode::Toggle
533}
534
535fn default_reasoning_visible_default() -> bool {
536    crate::constants::ui::DEFAULT_REASONING_VISIBLE
537}
538
539/// Kitty keyboard protocol configuration
540/// Reference: https://sw.kovidgoyal.net/kitty/keyboard-protocol/
541#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
542#[derive(Debug, Clone, Deserialize, Serialize)]
543pub struct KeyboardProtocolConfig {
544    /// Enable keyboard protocol enhancements (master toggle)
545    #[serde(default = "default_keyboard_protocol_enabled")]
546    pub enabled: bool,
547
548    /// Preset mode: "default", "full", "minimal", or "custom"
549    #[serde(default = "default_keyboard_protocol_mode")]
550    pub mode: String,
551
552    /// Resolve Esc key ambiguity (recommended for performance)
553    #[serde(default = "default_disambiguate_escape_codes")]
554    pub disambiguate_escape_codes: bool,
555
556    /// Report press, release, and repeat events
557    #[serde(default = "default_report_event_types")]
558    pub report_event_types: bool,
559
560    /// Report alternate key layouts (e.g. for non-US keyboards)
561    #[serde(default = "default_report_alternate_keys")]
562    pub report_alternate_keys: bool,
563
564    /// Report all keys, including modifier-only keys (Shift, Ctrl)
565    #[serde(default = "default_report_all_keys")]
566    pub report_all_keys: bool,
567}
568
569impl Default for KeyboardProtocolConfig {
570    fn default() -> Self {
571        Self {
572            enabled: default_keyboard_protocol_enabled(),
573            mode: default_keyboard_protocol_mode(),
574            disambiguate_escape_codes: default_disambiguate_escape_codes(),
575            report_event_types: default_report_event_types(),
576            report_alternate_keys: default_report_alternate_keys(),
577            report_all_keys: default_report_all_keys(),
578        }
579    }
580}
581
582impl KeyboardProtocolConfig {
583    pub fn validate(&self) -> anyhow::Result<()> {
584        match self.mode.as_str() {
585            "default" | "full" | "minimal" | "custom" => Ok(()),
586            _ => anyhow::bail!(
587                "Invalid keyboard protocol mode '{}'. Must be: default, full, minimal, or custom",
588                self.mode
589            ),
590        }
591    }
592}
593
594fn default_keyboard_protocol_enabled() -> bool {
595    std::env::var("VTCODE_KEYBOARD_PROTOCOL_ENABLED")
596        .ok()
597        .and_then(|v| v.parse().ok())
598        .unwrap_or(true)
599}
600
601fn default_keyboard_protocol_mode() -> String {
602    std::env::var("VTCODE_KEYBOARD_PROTOCOL_MODE").unwrap_or_else(|_| "default".to_string())
603}
604
605fn default_disambiguate_escape_codes() -> bool {
606    true
607}
608
609fn default_report_event_types() -> bool {
610    true
611}
612
613fn default_report_alternate_keys() -> bool {
614    true
615}
616
617fn default_report_all_keys() -> bool {
618    false
619}