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