Skip to main content

vtcode_config/root/
mod.rs

1use anyhow::{Result, anyhow, bail};
2use hashbrown::HashMap;
3use serde::{Deserialize, Serialize};
4
5use crate::accessibility::reduce_motion_preference;
6use crate::status_line::StatusLineConfig;
7use crate::terminal_title::TerminalTitleConfig;
8use vtcode_commons::ui_protocol::AppIconPreference;
9use vtcode_commons::ui_protocol::DiffPreviewMode;
10
11mod modes;
12
13pub use modes::{
14    LayoutModeOverride, NotificationBackend, NotificationDeliveryMode, ReasoningDisplayMode, ToolDisplayMode,
15    ToolOutputMode, UiDisplayMode,
16};
17
18/// Notification preferences for terminal and desktop alerts.
19#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
20#[derive(Debug, Clone, Deserialize, Serialize)]
21pub struct UiNotificationsConfig {
22    /// Master toggle for all runtime notifications.
23    #[serde(default = "default_notifications_enabled")]
24    pub enabled: bool,
25
26    /// Notification transport strategy.
27    #[serde(default)]
28    pub delivery_mode: NotificationDeliveryMode,
29
30    /// Preferred backend for desktop notification delivery.
31    #[serde(default)]
32    pub backend: NotificationBackend,
33
34    /// Suppress notifications while terminal focus is active.
35    #[serde(default = "default_notifications_suppress_when_focused")]
36    pub suppress_when_focused: bool,
37
38    /// Notify when a shell/command execution fails.
39    /// If omitted, falls back to `tool_failure` for backward compatibility.
40    #[serde(default)]
41    pub command_failure: Option<bool>,
42
43    /// Notify when a tool call fails.
44    #[serde(default = "default_notifications_tool_failure")]
45    pub tool_failure: bool,
46
47    /// Notify on runtime/system errors.
48    #[serde(default = "default_notifications_error")]
49    pub error: bool,
50
51    /// Legacy master toggle for completion notifications.
52    /// New installs should prefer `completion_success` and `completion_failure`.
53    #[serde(default = "default_notifications_completion")]
54    pub completion: bool,
55
56    /// Notify when a turn/session completes successfully.
57    /// If omitted, falls back to `completion`.
58    #[serde(default)]
59    pub completion_success: Option<bool>,
60
61    /// Notify when a turn/session is partial, failed, or cancelled.
62    /// If omitted, falls back to `completion`.
63    #[serde(default)]
64    pub completion_failure: Option<bool>,
65
66    /// Notify when human input/approval is required.
67    #[serde(default = "default_notifications_hitl")]
68    pub hitl: bool,
69
70    /// Notify when policy approval is required.
71    /// If omitted, falls back to `hitl` for backward compatibility.
72    #[serde(default)]
73    pub policy_approval: Option<bool>,
74
75    /// Notify on generic request events.
76    /// If omitted, falls back to `hitl` for backward compatibility.
77    #[serde(default)]
78    pub request: Option<bool>,
79
80    /// Notify on successful tool calls.
81    #[serde(default = "default_notifications_tool_success")]
82    pub tool_success: bool,
83
84    /// Suppression window for repeated identical notifications.
85    #[serde(default = "default_notifications_repeat_window_seconds")]
86    pub repeat_window_seconds: u64,
87
88    /// Maximum identical notifications allowed within the suppression window.
89    #[serde(default = "default_notifications_max_identical_in_window")]
90    pub max_identical_in_window: u32,
91}
92
93impl Default for UiNotificationsConfig {
94    fn default() -> Self {
95        Self {
96            enabled: default_notifications_enabled(),
97            delivery_mode: NotificationDeliveryMode::default(),
98            backend: NotificationBackend::default(),
99            suppress_when_focused: default_notifications_suppress_when_focused(),
100            command_failure: Some(default_notifications_command_failure()),
101            tool_failure: default_notifications_tool_failure(),
102            error: default_notifications_error(),
103            completion: default_notifications_completion(),
104            completion_success: Some(default_notifications_completion_success()),
105            completion_failure: Some(default_notifications_completion_failure()),
106            hitl: default_notifications_hitl(),
107            policy_approval: Some(default_notifications_policy_approval()),
108            request: Some(default_notifications_request()),
109            tool_success: default_notifications_tool_success(),
110            repeat_window_seconds: default_notifications_repeat_window_seconds(),
111            max_identical_in_window: default_notifications_max_identical_in_window(),
112        }
113    }
114}
115
116#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
117#[derive(Debug, Clone, Deserialize, Serialize)]
118pub struct UiFullscreenConfig {
119    /// Capture mouse events inside the fullscreen UI.
120    /// Can also be controlled via VTCODE_FULLSCREEN_MOUSE_CAPTURE=0/1.
121    #[serde(default = "default_fullscreen_mouse_capture")]
122    pub mouse_capture: bool,
123
124    /// Copy selected transcript text immediately when the mouse selection ends
125    /// (click-drag or double-click word select).
126    /// When disabled, copy manually with Ctrl+C (transcript/input selection)
127    /// or Ctrl+O (last agent response).
128    /// Can also be controlled via VTCODE_FULLSCREEN_COPY_ON_SELECT=0/1.
129    #[serde(default = "default_fullscreen_copy_on_select")]
130    pub copy_on_select: bool,
131
132    /// Multiplier applied to mouse wheel transcript scrolling in fullscreen mode.
133    /// Values are clamped to the range 1..=20.
134    /// Can also be controlled via VTCODE_FULLSCREEN_SCROLL_SPEED.
135    #[serde(default = "default_fullscreen_scroll_speed")]
136    pub scroll_speed: u8,
137}
138
139impl Default for UiFullscreenConfig {
140    fn default() -> Self {
141        Self {
142            mouse_capture: default_fullscreen_mouse_capture(),
143            copy_on_select: default_fullscreen_copy_on_select(),
144            scroll_speed: default_fullscreen_scroll_speed(),
145        }
146    }
147}
148
149/// Presentation controls for the session-local Transcript Review overlay.
150/// These settings affect only the interactive UI; captured transcript content
151/// and exported output remain complete and independent of the controls.
152#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
153#[derive(Debug, Clone, Deserialize, Serialize)]
154pub struct UiTranscriptReviewConfig {
155    /// Show the click/keyboard affordance on compact command rows.
156    #[serde(default = "default_transcript_review_control")]
157    pub show_hints: bool,
158
159    /// Show the keyboard guide footer inside Transcript Review.
160    #[serde(default = "default_transcript_review_control")]
161    pub show_shortcut_guide: bool,
162
163    /// Show the mouse-clickable close control in the Transcript Review title.
164    #[serde(default = "default_transcript_review_control")]
165    pub show_close_button: bool,
166}
167
168impl Default for UiTranscriptReviewConfig {
169    fn default() -> Self {
170        Self {
171            show_hints: default_transcript_review_control(),
172            show_shortcut_guide: default_transcript_review_control(),
173            show_close_button: default_transcript_review_control(),
174        }
175    }
176}
177
178#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
179#[derive(Debug, Clone, Deserialize, Serialize)]
180pub struct UiConfig {
181    /// Tool output display mode ("compact" or "full")
182    #[serde(default = "default_tool_output_mode")]
183    pub tool_output_mode: ToolOutputMode,
184
185    /// Tool transition summary display mode
186    /// Options: "expanded" (expanded summary layout) or "compact" (one compact summary per tool call)
187    #[serde(default = "default_tool_display_mode")]
188    pub tool_display_mode: ToolDisplayMode,
189
190    /// Session action bindings. Each action maps to one or more key specs;
191    /// an empty list explicitly unbinds the built-in action.
192    #[serde(default, alias = "key_bindings")]
193    #[cfg_attr(
194        feature = "schema",
195        schemars(with = "std::collections::BTreeMap<String, Vec<String>>")
196    )]
197    pub keybindings: HashMap<String, Vec<String>>,
198
199    /// Maximum number of lines to display in tool output (prevents transcript flooding)
200    #[serde(default = "default_tool_output_max_lines")]
201    pub tool_output_max_lines: usize,
202
203    /// Maximum bytes of output to display before auto-spooling to disk
204    #[serde(default = "default_tool_output_spool_bytes")]
205    pub tool_output_spool_bytes: usize,
206
207    /// Optional custom directory for spooled tool output logs
208    #[serde(default)]
209    pub tool_output_spool_dir: Option<String>,
210
211    /// Allow ANSI escape sequences in tool output (enables colors but may cause layout issues)
212    #[serde(default = "default_allow_tool_ansi")]
213    pub allow_tool_ansi: bool,
214
215    /// Number of rows to allocate for inline UI viewport
216    #[serde(default = "default_inline_viewport_rows")]
217    pub inline_viewport_rows: u16,
218
219    /// Reasoning display mode for chat UI ("always", "toggle", or "hidden"). Options: "always", "toggle", "hidden".
220    #[serde(default = "default_reasoning_display_mode")]
221    pub reasoning_display_mode: ReasoningDisplayMode,
222
223    /// Default visibility for reasoning when display mode is "toggle"
224    #[serde(default = "default_reasoning_visible_default")]
225    pub reasoning_visible_default: bool,
226
227    /// Default collapse state of agent thinking/reasoning blocks ("collapsed" or "extended")
228    #[serde(default = "default_thinking_display")]
229    pub thinking_display: vtcode_commons::ui_protocol::ThinkingBlockState,
230
231    /// Enable Vim-style prompt editing in the interactive terminal UI.
232    #[serde(default = "default_vim_mode")]
233    pub vim_mode: bool,
234
235    /// Status line configuration settings
236    #[serde(default)]
237    pub status_line: StatusLineConfig,
238
239    /// Terminal title configuration settings
240    #[serde(default)]
241    pub terminal_title: TerminalTitleConfig,
242
243    /// Keyboard protocol enhancements for modern terminals (e.g. Kitty protocol)
244    #[serde(default)]
245    pub keyboard_protocol: KeyboardProtocolConfig,
246
247    /// Override the responsive layout mode
248    #[serde(default)]
249    pub layout_mode: LayoutModeOverride,
250
251    /// UI display mode preset (full, minimal, focused)
252    #[serde(default)]
253    pub display_mode: UiDisplayMode,
254
255    /// Show the right sidebar (queue, context, tools)
256    #[serde(default = "default_show_sidebar")]
257    pub show_sidebar: bool,
258
259    /// Dim completed todo items (- \[x\]) in agent output
260    #[serde(default = "default_dim_completed_todos")]
261    pub dim_completed_todos: bool,
262
263    /// Automatically show the plan-mode TODO task tracking panel when a plan
264    /// is approved or the task tracker is updated. When disabled, the panel
265    /// can still be toggled with the `toggle_task_panel` keybinding.
266    #[serde(default = "default_show_task_panel")]
267    pub show_task_panel: bool,
268
269    /// Add spacing between message blocks
270    #[serde(default = "default_message_block_spacing")]
271    pub message_block_spacing: bool,
272
273    /// Show per-turn elapsed timer line after completed turns
274    #[serde(default = "default_show_turn_timer")]
275    pub show_turn_timer: bool,
276
277    /// Show warning/error/fatal diagnostic lines in the TUI transcript and log panel.
278    /// Also controls whether ERROR-level tracing logs appear in the TUI session log.
279    /// Errors are always captured in the session archive JSON regardless of this setting.
280    #[serde(default = "default_show_diagnostics_in_transcript")]
281    pub show_diagnostics_in_transcript: bool,
282
283    // === Color Accessibility Configuration ===
284    // Based on NO_COLOR standard, Ghostty minimum-contrast, and terminal color portability research.
285    /// Minimum contrast ratio for text against background (WCAG 2.1 standard)
286    /// - 4.5: WCAG AA (default, suitable for most users)
287    /// - 7.0: WCAG AAA (enhanced, for low-vision users)
288    /// - 3.0: Large text minimum
289    /// - 1.0: Disable contrast enforcement
290    #[serde(default = "default_minimum_contrast")]
291    pub minimum_contrast: f32,
292
293    /// Compatibility mode for legacy terminals that map bold to bright colors.
294    /// When enabled, avoids using bold styling on text that would become bright colors,
295    /// preventing visibility issues in terminals with "bold is bright" behavior.
296    #[serde(default = "default_bold_is_bright")]
297    pub bold_is_bright: bool,
298
299    /// Restrict color palette to the 11 "safe" ANSI colors portable across common themes.
300    /// Safe colors: red, green, yellow, blue, magenta, cyan + brred, brgreen, brmagenta, brcyan
301    /// Problematic colors avoided: brblack (invisible in Solarized Dark), bryellow (light themes),
302    /// white/brwhite (light themes), brblue (Basic Dark).
303    /// See: <https://blog.xoria.org/terminal-colors/>
304    #[serde(default = "default_safe_colors_only")]
305    pub safe_colors_only: bool,
306
307    /// Color scheme mode for automatic light/dark theme switching.
308    /// - "auto": Detect from terminal (via the Contour dark/light query where supported, else OSC 11, or the COLORFGBG env var) and follow live palette changes
309    /// - "light": Force light mode theme selection
310    /// - "dark": Force dark mode theme selection
311    #[serde(default = "default_color_scheme_mode")]
312    pub color_scheme_mode: ColorSchemeMode,
313
314    /// Application icon artwork variant for terminal tab icons and the desktop picker.
315    /// - "dynamic": Follow the active light/dark mode (default)
316    /// - "light": Always use the light-mode artwork
317    /// - "dark": Always use the dark-mode artwork
318    /// - "clear": Always use the clear (transparent glass V) artwork
319    /// - "tinted": Always use the tinted (monochrome slate) artwork
320    #[serde(default = "default_app_icon")]
321    pub app_icon: AppIconPreference,
322
323    /// Terminal program status reports for the interactive TUI (on by default).
324    #[serde(default)]
325    pub program_status: UiProgramStatusConfig,
326
327    /// Notification preferences for attention events.
328    #[serde(default)]
329    pub notifications: UiNotificationsConfig,
330
331    /// Fullscreen interaction settings for alternate-screen rendering.
332    #[serde(default)]
333    pub fullscreen: UiFullscreenConfig,
334
335    /// Screen reader mode: disables animations, uses plain text thinking indicators,
336    /// and opens Transcript Review in raw mode. Alternate-screen compatibility
337    /// depends on the terminal and assistive technology.
338    /// Can also be enabled via VTCODE_SCREEN_READER=1 environment variable.
339    #[serde(default = "default_screen_reader_mode")]
340    pub screen_reader_mode: bool,
341
342    /// Reduce motion mode: keeps progress labels visible without animated effects.
343    /// If omitted, defaults from VTCODE_REDUCE_MOTION, then a supported OS
344    /// accessibility preference; unknown or unavailable preferences default to false.
345    #[serde(default = "default_reduce_motion_mode")]
346    pub reduce_motion_mode: bool,
347
348    /// Keep animated progress indicators while reduce_motion_mode is enabled.
349    /// Screen reader mode still disables progress animation.
350    #[serde(default = "default_reduce_motion_keep_progress_animation")]
351    pub reduce_motion_keep_progress_animation: bool,
352
353    /// Hide the full TUI header, showing only version info in a compact line.
354    #[serde(default = "default_hide_header")]
355    pub hide_header: bool,
356
357    /// Diff preview layout for file-edit approval overlays.
358    /// Options: "inline" (default) or "side-by-side".
359    #[serde(default = "default_diff_preview_mode")]
360    pub diff_preview_mode: DiffPreviewMode,
361
362    /// Transcript Review presentation controls.
363    #[serde(default)]
364    pub transcript_review: UiTranscriptReviewConfig,
365}
366
367/// Color scheme mode for theme selection
368#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
369#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Default)]
370#[serde(rename_all = "snake_case")]
371pub enum ColorSchemeMode {
372    /// Detect from terminal environment (OSC 11 query or COLORFGBG)
373    #[default]
374    Auto,
375    /// Force light color scheme
376    Light,
377    /// Force dark color scheme
378    Dark,
379    /// Catch-all for unknown modes added by future versions.
380    #[serde(other)]
381    Unknown,
382}
383
384fn default_minimum_contrast() -> f32 {
385    crate::constants::ui::THEME_MIN_CONTRAST_RATIO
386}
387
388fn default_bold_is_bright() -> bool {
389    false
390}
391
392fn default_safe_colors_only() -> bool {
393    false
394}
395
396fn default_color_scheme_mode() -> ColorSchemeMode {
397    ColorSchemeMode::Auto
398}
399
400fn default_app_icon() -> AppIconPreference {
401    AppIconPreference::Dynamic
402}
403
404fn default_show_sidebar() -> bool {
405    true
406}
407
408fn default_dim_completed_todos() -> bool {
409    true
410}
411
412fn default_message_block_spacing() -> bool {
413    true
414}
415
416fn default_program_status_enabled() -> bool {
417    true
418}
419
420fn default_show_turn_timer() -> bool {
421    false
422}
423
424fn default_show_diagnostics_in_transcript() -> bool {
425    false
426}
427
428fn default_vim_mode() -> bool {
429    false
430}
431
432fn default_notifications_enabled() -> bool {
433    true
434}
435
436fn default_notifications_suppress_when_focused() -> bool {
437    true
438}
439
440fn default_notifications_command_failure() -> bool {
441    false
442}
443
444fn default_notifications_tool_failure() -> bool {
445    false
446}
447
448fn default_notifications_error() -> bool {
449    true
450}
451
452fn default_notifications_completion() -> bool {
453    true
454}
455
456fn default_notifications_completion_success() -> bool {
457    false
458}
459
460fn default_notifications_completion_failure() -> bool {
461    true
462}
463
464fn default_notifications_hitl() -> bool {
465    true
466}
467
468fn default_notifications_policy_approval() -> bool {
469    true
470}
471
472fn default_notifications_request() -> bool {
473    false
474}
475
476fn default_notifications_tool_success() -> bool {
477    false
478}
479
480fn default_notifications_repeat_window_seconds() -> u64 {
481    30
482}
483
484fn default_notifications_max_identical_in_window() -> u32 {
485    1
486}
487
488fn env_bool_var(name: &str) -> Option<bool> {
489    crate::env_helpers::read_env_var(name).and_then(|v| vtcode_commons::utils::parse_bool_env_value(&v))
490}
491
492fn env_u8_var(name: &str) -> Option<u8> {
493    crate::env_helpers::read_env_var(name)
494        .and_then(|value| value.trim().parse::<u8>().ok())
495        .map(clamp_fullscreen_scroll_speed)
496}
497
498fn clamp_fullscreen_scroll_speed(value: u8) -> u8 {
499    value.clamp(1, 20)
500}
501
502fn default_fullscreen_mouse_capture() -> bool {
503    env_bool_var("VTCODE_FULLSCREEN_MOUSE_CAPTURE").unwrap_or(true)
504}
505
506fn default_fullscreen_copy_on_select() -> bool {
507    env_bool_var("VTCODE_FULLSCREEN_COPY_ON_SELECT").unwrap_or(true)
508}
509
510fn default_fullscreen_scroll_speed() -> u8 {
511    env_u8_var("VTCODE_FULLSCREEN_SCROLL_SPEED").unwrap_or(3)
512}
513
514fn default_screen_reader_mode() -> bool {
515    env_bool_var("VTCODE_SCREEN_READER").unwrap_or(false)
516}
517
518fn default_reduce_motion_mode() -> bool {
519    env_bool_var("VTCODE_REDUCE_MOTION")
520        .or_else(reduce_motion_preference)
521        .unwrap_or(false)
522}
523
524fn default_reduce_motion_keep_progress_animation() -> bool {
525    false
526}
527
528fn default_hide_header() -> bool {
529    true
530}
531
532fn default_diff_preview_mode() -> DiffPreviewMode {
533    DiffPreviewMode::Inline
534}
535
536fn default_transcript_review_control() -> bool {
537    true
538}
539
540fn default_show_task_panel() -> bool {
541    false
542}
543
544fn default_ask_questions_enabled() -> bool {
545    true
546}
547
548impl Default for UiConfig {
549    fn default() -> Self {
550        Self {
551            tool_output_mode: default_tool_output_mode(),
552            tool_display_mode: default_tool_display_mode(),
553            keybindings: HashMap::new(),
554            tool_output_max_lines: default_tool_output_max_lines(),
555            tool_output_spool_bytes: default_tool_output_spool_bytes(),
556            tool_output_spool_dir: None,
557            allow_tool_ansi: default_allow_tool_ansi(),
558            inline_viewport_rows: default_inline_viewport_rows(),
559            reasoning_display_mode: default_reasoning_display_mode(),
560            reasoning_visible_default: default_reasoning_visible_default(),
561            thinking_display: default_thinking_display(),
562            vim_mode: default_vim_mode(),
563            status_line: StatusLineConfig::default(),
564            terminal_title: TerminalTitleConfig::default(),
565            keyboard_protocol: KeyboardProtocolConfig::default(),
566            layout_mode: LayoutModeOverride::default(),
567            display_mode: UiDisplayMode::default(),
568            show_sidebar: default_show_sidebar(),
569            dim_completed_todos: default_dim_completed_todos(),
570            show_task_panel: default_show_task_panel(),
571            message_block_spacing: default_message_block_spacing(),
572            show_turn_timer: default_show_turn_timer(),
573            show_diagnostics_in_transcript: default_show_diagnostics_in_transcript(),
574            // Color accessibility defaults
575            minimum_contrast: default_minimum_contrast(),
576            bold_is_bright: default_bold_is_bright(),
577            safe_colors_only: default_safe_colors_only(),
578            color_scheme_mode: default_color_scheme_mode(),
579            app_icon: default_app_icon(),
580            program_status: UiProgramStatusConfig::default(),
581            notifications: UiNotificationsConfig::default(),
582            fullscreen: UiFullscreenConfig::default(),
583            screen_reader_mode: default_screen_reader_mode(),
584            reduce_motion_mode: default_reduce_motion_mode(),
585            reduce_motion_keep_progress_animation: default_reduce_motion_keep_progress_animation(),
586            hide_header: default_hide_header(),
587            diff_preview_mode: default_diff_preview_mode(),
588            transcript_review: UiTranscriptReviewConfig::default(),
589        }
590    }
591}
592
593/// Terminal Program Status Protocol preferences.
594#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
595#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
596#[serde(default)]
597pub struct UiProgramStatusConfig {
598    /// Emit OSC 7501 status reports on the interactive TUI terminal stream.
599    #[serde(default = "default_program_status_enabled")]
600    pub enabled: bool,
601}
602
603impl Default for UiProgramStatusConfig {
604    fn default() -> Self {
605        Self { enabled: default_program_status_enabled() }
606    }
607}
608
609/// Chat configuration
610#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
611#[derive(Debug, Clone, Deserialize, Serialize, Default)]
612pub struct ChatConfig {
613    /// Ask Questions tool configuration (chat.askQuestions.*)
614    #[serde(default, rename = "askQuestions", alias = "ask_questions")]
615    pub ask_questions: AskQuestionsConfig,
616}
617
618/// Ask Questions tool configuration
619#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
620#[derive(Debug, Clone, Deserialize, Serialize)]
621pub struct AskQuestionsConfig {
622    /// Enable the Ask Questions tool in interactive chat
623    #[serde(default = "default_ask_questions_enabled")]
624    pub enabled: bool,
625}
626
627impl Default for AskQuestionsConfig {
628    fn default() -> Self {
629        Self { enabled: default_ask_questions_enabled() }
630    }
631}
632
633/// PTY configuration
634#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
635#[derive(Debug, Clone, Deserialize, Serialize)]
636pub struct PtyConfig {
637    /// Enable PTY support for interactive commands
638    #[serde(default = "default_pty_enabled")]
639    pub enabled: bool,
640
641    /// Default terminal rows for PTY sessions
642    #[serde(default = "default_pty_rows")]
643    pub default_rows: u16,
644
645    /// Default terminal columns for PTY sessions
646    #[serde(default = "default_pty_cols")]
647    pub default_cols: u16,
648
649    /// Maximum number of concurrent PTY sessions
650    #[serde(default = "default_max_pty_sessions")]
651    pub max_sessions: usize,
652
653    /// Command timeout in seconds (prevents hanging commands)
654    #[serde(default = "default_pty_timeout")]
655    pub command_timeout_seconds: u64,
656
657    /// Number of recent PTY output lines to display in the chat transcript
658    #[serde(default = "default_stdout_tail_lines")]
659    pub stdout_tail_lines: usize,
660
661    /// Total scrollback buffer size (lines) retained per PTY session
662    #[serde(default = "default_scrollback_lines")]
663    pub scrollback_lines: usize,
664
665    /// Maximum bytes of output to retain per PTY session (prevents memory explosion)
666    #[serde(default = "default_max_scrollback_bytes")]
667    pub max_scrollback_bytes: usize,
668
669    /// Threshold (KB) at which to auto-spool large outputs to disk instead of memory
670    #[serde(default = "default_large_output_threshold_kb")]
671    pub large_output_threshold_kb: usize,
672
673    /// Preferred shell program for PTY sessions (e.g. "zsh", "bash"); falls back to $SHELL
674    #[serde(default)]
675    pub preferred_shell: Option<String>,
676
677    /// Feature-gated shell runtime path that routes shell execution through zsh EXEC_WRAPPER hooks.
678    #[serde(default = "default_shell_zsh_fork")]
679    pub shell_zsh_fork: bool,
680
681    /// Optional absolute path to patched zsh used when shell_zsh_fork is enabled.
682    #[serde(default)]
683    pub zsh_path: Option<String>,
684}
685
686impl Default for PtyConfig {
687    fn default() -> Self {
688        Self {
689            enabled: default_pty_enabled(),
690            default_rows: default_pty_rows(),
691            default_cols: default_pty_cols(),
692            max_sessions: default_max_pty_sessions(),
693            command_timeout_seconds: default_pty_timeout(),
694            stdout_tail_lines: default_stdout_tail_lines(),
695            scrollback_lines: default_scrollback_lines(),
696            max_scrollback_bytes: default_max_scrollback_bytes(),
697            large_output_threshold_kb: default_large_output_threshold_kb(),
698            preferred_shell: None,
699            shell_zsh_fork: default_shell_zsh_fork(),
700            zsh_path: None,
701        }
702    }
703}
704
705impl PtyConfig {
706    pub fn validate(&self) -> Result<()> {
707        self.zsh_fork_shell_path()?;
708        Ok(())
709    }
710
711    pub fn zsh_fork_shell_path(&self) -> Result<Option<&str>> {
712        if !self.shell_zsh_fork {
713            return Ok(None);
714        }
715
716        let zsh_path = self
717            .zsh_path
718            .as_deref()
719            .map(str::trim)
720            .filter(|path| !path.is_empty())
721            .ok_or_else(|| {
722                anyhow!(
723                    "pty.shell_zsh_fork is enabled, but pty.zsh_path is not configured. \
724                     Set pty.zsh_path to an absolute path to patched zsh."
725                )
726            })?;
727
728        #[cfg(not(unix))]
729        {
730            let _ = zsh_path;
731            bail!("pty.shell_zsh_fork is only supported on Unix platforms");
732        }
733
734        #[cfg(unix)]
735        {
736            let path = std::path::Path::new(zsh_path);
737            if !path.is_absolute() {
738                bail!("pty.zsh_path '{zsh_path}' must be an absolute path when pty.shell_zsh_fork is enabled");
739            }
740            if !path.exists() {
741                bail!("pty.zsh_path '{zsh_path}' does not exist (required when pty.shell_zsh_fork is enabled)");
742            }
743            if !path.is_file() {
744                bail!("pty.zsh_path '{zsh_path}' is not a file (required when pty.shell_zsh_fork is enabled)");
745            }
746
747            Ok(Some(zsh_path))
748        }
749    }
750}
751
752fn default_pty_enabled() -> bool {
753    true
754}
755
756fn default_pty_rows() -> u16 {
757    24
758}
759
760fn default_pty_cols() -> u16 {
761    80
762}
763
764fn default_max_pty_sessions() -> usize {
765    10
766}
767
768fn default_pty_timeout() -> u64 {
769    300
770}
771
772fn default_shell_zsh_fork() -> bool {
773    false
774}
775
776fn default_stdout_tail_lines() -> usize {
777    crate::constants::defaults::DEFAULT_PTY_STDOUT_TAIL_LINES
778}
779
780fn default_scrollback_lines() -> usize {
781    crate::constants::defaults::DEFAULT_PTY_SCROLLBACK_LINES
782}
783
784fn default_max_scrollback_bytes() -> usize {
785    // Reduced from 50MB to 25MB for memory-constrained development environments
786    // Can be overridden in vtcode.toml with: pty.max_scrollback_bytes = 52428800
787    25_000_000 // 25MB max to prevent memory explosion
788}
789
790fn default_large_output_threshold_kb() -> usize {
791    5_000 // 5MB threshold for auto-spooling
792}
793
794fn default_tool_output_mode() -> ToolOutputMode {
795    ToolOutputMode::Compact
796}
797
798fn default_tool_display_mode() -> ToolDisplayMode {
799    ToolDisplayMode::Compact
800}
801
802fn default_tool_output_max_lines() -> usize {
803    30
804}
805
806fn default_tool_output_spool_bytes() -> usize {
807    80_000
808}
809
810fn default_allow_tool_ansi() -> bool {
811    false
812}
813
814fn default_inline_viewport_rows() -> u16 {
815    crate::constants::ui::DEFAULT_INLINE_VIEWPORT_ROWS
816}
817
818fn default_reasoning_display_mode() -> ReasoningDisplayMode {
819    ReasoningDisplayMode::Toggle
820}
821
822fn default_reasoning_visible_default() -> bool {
823    crate::constants::ui::DEFAULT_REASONING_VISIBLE
824}
825
826fn default_thinking_display() -> vtcode_commons::ui_protocol::ThinkingBlockState {
827    vtcode_commons::ui_protocol::ThinkingBlockState::Collapsed
828}
829
830/// Kitty keyboard protocol configuration
831/// Reference: <https://sw.kovidgoyal.net/kitty/keyboard-protocol/>
832/// Keyboard protocol preset mode
833#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
834#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Deserialize, Serialize)]
835#[serde(rename_all = "snake_case")]
836pub enum KeyboardProtocolMode {
837    /// Standard enhancements (disambiguate + event types + alternate keys)
838    #[default]
839    Default,
840    /// All enhancements including report-all-keys
841    Full,
842    /// Minimal: disambiguate escape codes only
843    Minimal,
844    /// Custom: use individual toggle fields
845    Custom,
846}
847
848impl KeyboardProtocolMode {
849    /// Returns the snake_case string representation.
850    pub fn as_str(&self) -> &'static str {
851        match self {
852            Self::Default => "default",
853            Self::Full => "full",
854            Self::Minimal => "minimal",
855            Self::Custom => "custom",
856        }
857    }
858}
859
860#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
861#[derive(Debug, Clone, Deserialize, Serialize)]
862pub struct KeyboardProtocolConfig {
863    /// Enable keyboard protocol enhancements (master toggle)
864    #[serde(default = "default_keyboard_protocol_enabled")]
865    pub enabled: bool,
866
867    /// Preset mode: default, full, minimal, or custom
868    #[serde(default = "default_keyboard_protocol_mode")]
869    pub mode: KeyboardProtocolMode,
870
871    /// Resolve Esc key ambiguity (recommended for performance)
872    #[serde(default = "default_disambiguate_escape_codes")]
873    pub disambiguate_escape_codes: bool,
874
875    /// Report press, release, and repeat events
876    #[serde(default = "default_report_event_types")]
877    pub report_event_types: bool,
878
879    /// Report alternate key layouts (e.g. for non-US keyboards)
880    #[serde(default = "default_report_alternate_keys")]
881    pub report_alternate_keys: bool,
882
883    /// Report all keys, including modifier-only keys (Shift, Ctrl)
884    #[serde(default = "default_report_all_keys")]
885    pub report_all_keys: bool,
886}
887
888impl Default for KeyboardProtocolConfig {
889    fn default() -> Self {
890        Self {
891            enabled: default_keyboard_protocol_enabled(),
892            mode: default_keyboard_protocol_mode(),
893            disambiguate_escape_codes: default_disambiguate_escape_codes(),
894            report_event_types: default_report_event_types(),
895            report_alternate_keys: default_report_alternate_keys(),
896            report_all_keys: default_report_all_keys(),
897        }
898    }
899}
900
901impl KeyboardProtocolConfig {
902    pub fn validate(&self) -> Result<()> {
903        // All enum variants are valid; nothing to validate.
904        let _ = self.mode;
905        Ok(())
906    }
907}
908
909fn default_keyboard_protocol_enabled() -> bool {
910    std::env::var("VTCODE_KEYBOARD_PROTOCOL_ENABLED")
911        .ok()
912        .and_then(|v| v.parse().ok())
913        .unwrap_or(true)
914}
915
916fn default_keyboard_protocol_mode() -> KeyboardProtocolMode {
917    std::env::var("VTCODE_KEYBOARD_PROTOCOL_MODE")
918        .ok()
919        .and_then(|v| match v.to_ascii_lowercase().as_str() {
920            "default" => Some(KeyboardProtocolMode::Default),
921            "full" => Some(KeyboardProtocolMode::Full),
922            "minimal" => Some(KeyboardProtocolMode::Minimal),
923            "custom" => Some(KeyboardProtocolMode::Custom),
924            _ => None,
925        })
926        .unwrap_or_default()
927}
928
929fn default_disambiguate_escape_codes() -> bool {
930    true
931}
932
933fn default_report_event_types() -> bool {
934    true
935}
936
937fn default_report_alternate_keys() -> bool {
938    true
939}
940
941fn default_report_all_keys() -> bool {
942    false
943}
944#[cfg(test)]
945mod tests;