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