1use anyhow::{Result, anyhow, bail};
2use serde::{Deserialize, Serialize};
3
4use crate::status_line::StatusLineConfig;
5
6#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
7#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
8#[serde(rename_all = "snake_case")]
9#[derive(Default)]
10pub enum ToolOutputMode {
11 #[default]
12 Compact,
13 Full,
14}
15
16#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
17#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq)]
18#[serde(rename_all = "snake_case")]
19#[derive(Default)]
20pub enum ReasoningDisplayMode {
21 Always,
22 #[default]
23 Toggle,
24 Hidden,
25}
26
27#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
29#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Default)]
30#[serde(rename_all = "snake_case")]
31pub enum LayoutModeOverride {
32 #[default]
34 Auto,
35 Compact,
37 Standard,
39 Wide,
41}
42
43#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
45#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Default)]
46#[serde(rename_all = "snake_case")]
47pub enum UiDisplayMode {
48 Full,
50 #[default]
52 Minimal,
53 Focused,
55}
56
57#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
59#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Default)]
60#[serde(rename_all = "snake_case")]
61pub enum NotificationDeliveryMode {
62 Terminal,
64 #[default]
66 Hybrid,
67 Desktop,
69}
70
71#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
73#[derive(Debug, Clone, Deserialize, Serialize)]
74pub struct UiNotificationsConfig {
75 #[serde(default = "default_notifications_enabled")]
77 pub enabled: bool,
78
79 #[serde(default)]
81 pub delivery_mode: NotificationDeliveryMode,
82
83 #[serde(default = "default_notifications_suppress_when_focused")]
85 pub suppress_when_focused: bool,
86
87 #[serde(default)]
90 pub command_failure: Option<bool>,
91
92 #[serde(default = "default_notifications_tool_failure")]
94 pub tool_failure: bool,
95
96 #[serde(default = "default_notifications_error")]
98 pub error: bool,
99
100 #[serde(default = "default_notifications_completion")]
103 pub completion: bool,
104
105 #[serde(default)]
108 pub completion_success: Option<bool>,
109
110 #[serde(default)]
113 pub completion_failure: Option<bool>,
114
115 #[serde(default = "default_notifications_hitl")]
117 pub hitl: bool,
118
119 #[serde(default)]
122 pub policy_approval: Option<bool>,
123
124 #[serde(default)]
127 pub request: Option<bool>,
128
129 #[serde(default = "default_notifications_tool_success")]
131 pub tool_success: bool,
132
133 #[serde(default = "default_notifications_repeat_window_seconds")]
135 pub repeat_window_seconds: u64,
136
137 #[serde(default = "default_notifications_max_identical_in_window")]
139 pub max_identical_in_window: u32,
140}
141
142impl Default for UiNotificationsConfig {
143 fn default() -> Self {
144 Self {
145 enabled: default_notifications_enabled(),
146 delivery_mode: NotificationDeliveryMode::default(),
147 suppress_when_focused: default_notifications_suppress_when_focused(),
148 command_failure: Some(default_notifications_command_failure()),
149 tool_failure: default_notifications_tool_failure(),
150 error: default_notifications_error(),
151 completion: default_notifications_completion(),
152 completion_success: Some(default_notifications_completion_success()),
153 completion_failure: Some(default_notifications_completion_failure()),
154 hitl: default_notifications_hitl(),
155 policy_approval: Some(default_notifications_policy_approval()),
156 request: Some(default_notifications_request()),
157 tool_success: default_notifications_tool_success(),
158 repeat_window_seconds: default_notifications_repeat_window_seconds(),
159 max_identical_in_window: default_notifications_max_identical_in_window(),
160 }
161 }
162}
163
164#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
165#[derive(Debug, Clone, Deserialize, Serialize)]
166pub struct UiConfig {
167 #[serde(default = "default_tool_output_mode")]
169 pub tool_output_mode: ToolOutputMode,
170
171 #[serde(default = "default_tool_output_max_lines")]
173 pub tool_output_max_lines: usize,
174
175 #[serde(default = "default_tool_output_spool_bytes")]
177 pub tool_output_spool_bytes: usize,
178
179 #[serde(default)]
181 pub tool_output_spool_dir: Option<String>,
182
183 #[serde(default = "default_allow_tool_ansi")]
185 pub allow_tool_ansi: bool,
186
187 #[serde(default = "default_inline_viewport_rows")]
189 pub inline_viewport_rows: u16,
190
191 #[serde(default = "default_reasoning_display_mode")]
193 pub reasoning_display_mode: ReasoningDisplayMode,
194
195 #[serde(default = "default_reasoning_visible_default")]
197 pub reasoning_visible_default: bool,
198
199 #[serde(default)]
201 pub status_line: StatusLineConfig,
202
203 #[serde(default)]
205 pub keyboard_protocol: KeyboardProtocolConfig,
206
207 #[serde(default)]
209 pub layout_mode: LayoutModeOverride,
210
211 #[serde(default)]
213 pub display_mode: UiDisplayMode,
214
215 #[serde(default = "default_show_sidebar")]
217 pub show_sidebar: bool,
218
219 #[serde(default = "default_dim_completed_todos")]
221 pub dim_completed_todos: bool,
222
223 #[serde(default = "default_message_block_spacing")]
225 pub message_block_spacing: bool,
226
227 #[serde(default = "default_show_turn_timer")]
229 pub show_turn_timer: bool,
230
231 #[serde(default = "default_show_diagnostics_in_transcript")]
235 pub show_diagnostics_in_transcript: bool,
236
237 #[serde(default = "default_minimum_contrast")]
246 pub minimum_contrast: f64,
247
248 #[serde(default = "default_bold_is_bright")]
252 pub bold_is_bright: bool,
253
254 #[serde(default = "default_safe_colors_only")]
260 pub safe_colors_only: bool,
261
262 #[serde(default = "default_color_scheme_mode")]
267 pub color_scheme_mode: ColorSchemeMode,
268
269 #[serde(default)]
271 pub notifications: UiNotificationsConfig,
272
273 #[serde(default = "default_screen_reader_mode")]
277 pub screen_reader_mode: bool,
278
279 #[serde(default = "default_reduce_motion_mode")]
282 pub reduce_motion_mode: bool,
283
284 #[serde(default = "default_reduce_motion_keep_progress_animation")]
286 pub reduce_motion_keep_progress_animation: bool,
287}
288
289#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
291#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, Default)]
292#[serde(rename_all = "snake_case")]
293pub enum ColorSchemeMode {
294 #[default]
296 Auto,
297 Light,
299 Dark,
301}
302
303fn default_minimum_contrast() -> f64 {
304 crate::constants::ui::THEME_MIN_CONTRAST_RATIO
305}
306
307fn default_bold_is_bright() -> bool {
308 false
309}
310
311fn default_safe_colors_only() -> bool {
312 false
313}
314
315fn default_color_scheme_mode() -> ColorSchemeMode {
316 ColorSchemeMode::Auto
317}
318
319fn default_show_sidebar() -> bool {
320 true
321}
322
323fn default_dim_completed_todos() -> bool {
324 true
325}
326
327fn default_message_block_spacing() -> bool {
328 true
329}
330
331fn default_show_turn_timer() -> bool {
332 false
333}
334
335fn default_show_diagnostics_in_transcript() -> bool {
336 false
337}
338
339fn default_notifications_enabled() -> bool {
340 true
341}
342
343fn default_notifications_suppress_when_focused() -> bool {
344 true
345}
346
347fn default_notifications_command_failure() -> bool {
348 false
349}
350
351fn default_notifications_tool_failure() -> bool {
352 false
353}
354
355fn default_notifications_error() -> bool {
356 true
357}
358
359fn default_notifications_completion() -> bool {
360 true
361}
362
363fn default_notifications_completion_success() -> bool {
364 false
365}
366
367fn default_notifications_completion_failure() -> bool {
368 true
369}
370
371fn default_notifications_hitl() -> bool {
372 true
373}
374
375fn default_notifications_policy_approval() -> bool {
376 true
377}
378
379fn default_notifications_request() -> bool {
380 false
381}
382
383fn default_notifications_tool_success() -> bool {
384 false
385}
386
387fn default_notifications_repeat_window_seconds() -> u64 {
388 30
389}
390
391fn default_notifications_max_identical_in_window() -> u32 {
392 1
393}
394
395fn env_bool_var(name: &str) -> Option<bool> {
396 std::env::var(name).ok().and_then(|v| {
397 let normalized = v.trim().to_ascii_lowercase();
398 match normalized.as_str() {
399 "1" | "true" | "yes" | "on" => Some(true),
400 "0" | "false" | "no" | "off" => Some(false),
401 _ => None,
402 }
403 })
404}
405
406fn default_screen_reader_mode() -> bool {
407 env_bool_var("VTCODE_SCREEN_READER").unwrap_or(false)
408}
409
410fn default_reduce_motion_mode() -> bool {
411 env_bool_var("VTCODE_REDUCE_MOTION").unwrap_or(false)
412}
413
414fn default_reduce_motion_keep_progress_animation() -> bool {
415 false
416}
417
418fn default_ask_questions_enabled() -> bool {
419 true
420}
421
422impl Default for UiConfig {
423 fn default() -> Self {
424 Self {
425 tool_output_mode: default_tool_output_mode(),
426 tool_output_max_lines: default_tool_output_max_lines(),
427 tool_output_spool_bytes: default_tool_output_spool_bytes(),
428 tool_output_spool_dir: None,
429 allow_tool_ansi: default_allow_tool_ansi(),
430 inline_viewport_rows: default_inline_viewport_rows(),
431 reasoning_display_mode: default_reasoning_display_mode(),
432 reasoning_visible_default: default_reasoning_visible_default(),
433 status_line: StatusLineConfig::default(),
434 keyboard_protocol: KeyboardProtocolConfig::default(),
435 layout_mode: LayoutModeOverride::default(),
436 display_mode: UiDisplayMode::default(),
437 show_sidebar: default_show_sidebar(),
438 dim_completed_todos: default_dim_completed_todos(),
439 message_block_spacing: default_message_block_spacing(),
440 show_turn_timer: default_show_turn_timer(),
441 show_diagnostics_in_transcript: default_show_diagnostics_in_transcript(),
442 minimum_contrast: default_minimum_contrast(),
444 bold_is_bright: default_bold_is_bright(),
445 safe_colors_only: default_safe_colors_only(),
446 color_scheme_mode: default_color_scheme_mode(),
447 notifications: UiNotificationsConfig::default(),
448 screen_reader_mode: default_screen_reader_mode(),
449 reduce_motion_mode: default_reduce_motion_mode(),
450 reduce_motion_keep_progress_animation: default_reduce_motion_keep_progress_animation(),
451 }
452 }
453}
454
455#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
457#[derive(Debug, Clone, Deserialize, Serialize, Default)]
458pub struct ChatConfig {
459 #[serde(default, rename = "askQuestions", alias = "ask_questions")]
461 pub ask_questions: AskQuestionsConfig,
462}
463
464#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
466#[derive(Debug, Clone, Deserialize, Serialize)]
467pub struct AskQuestionsConfig {
468 #[serde(default = "default_ask_questions_enabled")]
470 pub enabled: bool,
471}
472
473impl Default for AskQuestionsConfig {
474 fn default() -> Self {
475 Self {
476 enabled: default_ask_questions_enabled(),
477 }
478 }
479}
480
481#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
483#[derive(Debug, Clone, Deserialize, Serialize)]
484pub struct PtyConfig {
485 #[serde(default = "default_pty_enabled")]
487 pub enabled: bool,
488
489 #[serde(default = "default_pty_rows")]
491 pub default_rows: u16,
492
493 #[serde(default = "default_pty_cols")]
495 pub default_cols: u16,
496
497 #[serde(default = "default_max_pty_sessions")]
499 pub max_sessions: usize,
500
501 #[serde(default = "default_pty_timeout")]
503 pub command_timeout_seconds: u64,
504
505 #[serde(default = "default_stdout_tail_lines")]
507 pub stdout_tail_lines: usize,
508
509 #[serde(default = "default_scrollback_lines")]
511 pub scrollback_lines: usize,
512
513 #[serde(default = "default_max_scrollback_bytes")]
515 pub max_scrollback_bytes: usize,
516
517 #[serde(default = "default_large_output_threshold_kb")]
519 pub large_output_threshold_kb: usize,
520
521 #[serde(default)]
523 pub preferred_shell: Option<String>,
524
525 #[serde(default = "default_shell_zsh_fork")]
527 pub shell_zsh_fork: bool,
528
529 #[serde(default)]
531 pub zsh_path: Option<String>,
532}
533
534impl Default for PtyConfig {
535 fn default() -> Self {
536 Self {
537 enabled: default_pty_enabled(),
538 default_rows: default_pty_rows(),
539 default_cols: default_pty_cols(),
540 max_sessions: default_max_pty_sessions(),
541 command_timeout_seconds: default_pty_timeout(),
542 stdout_tail_lines: default_stdout_tail_lines(),
543 scrollback_lines: default_scrollback_lines(),
544 max_scrollback_bytes: default_max_scrollback_bytes(),
545 large_output_threshold_kb: default_large_output_threshold_kb(),
546 preferred_shell: None,
547 shell_zsh_fork: default_shell_zsh_fork(),
548 zsh_path: None,
549 }
550 }
551}
552
553impl PtyConfig {
554 pub fn validate(&self) -> Result<()> {
555 self.zsh_fork_shell_path()?;
556 Ok(())
557 }
558
559 pub fn zsh_fork_shell_path(&self) -> Result<Option<&str>> {
560 if !self.shell_zsh_fork {
561 return Ok(None);
562 }
563
564 let zsh_path = self
565 .zsh_path
566 .as_deref()
567 .map(str::trim)
568 .filter(|path| !path.is_empty())
569 .ok_or_else(|| {
570 anyhow!(
571 "pty.shell_zsh_fork is enabled, but pty.zsh_path is not configured. \
572 Set pty.zsh_path to an absolute path to patched zsh."
573 )
574 })?;
575
576 #[cfg(not(unix))]
577 {
578 let _ = zsh_path;
579 bail!("pty.shell_zsh_fork is only supported on Unix platforms");
580 }
581
582 #[cfg(unix)]
583 {
584 let path = std::path::Path::new(zsh_path);
585 if !path.is_absolute() {
586 bail!(
587 "pty.zsh_path '{}' must be an absolute path when pty.shell_zsh_fork is enabled",
588 zsh_path
589 );
590 }
591 if !path.exists() {
592 bail!(
593 "pty.zsh_path '{}' does not exist (required when pty.shell_zsh_fork is enabled)",
594 zsh_path
595 );
596 }
597 if !path.is_file() {
598 bail!(
599 "pty.zsh_path '{}' is not a file (required when pty.shell_zsh_fork is enabled)",
600 zsh_path
601 );
602 }
603 }
604
605 Ok(Some(zsh_path))
606 }
607}
608
609fn default_pty_enabled() -> bool {
610 true
611}
612
613fn default_pty_rows() -> u16 {
614 24
615}
616
617fn default_pty_cols() -> u16 {
618 80
619}
620
621fn default_max_pty_sessions() -> usize {
622 10
623}
624
625fn default_pty_timeout() -> u64 {
626 300
627}
628
629fn default_shell_zsh_fork() -> bool {
630 false
631}
632
633fn default_stdout_tail_lines() -> usize {
634 crate::constants::defaults::DEFAULT_PTY_STDOUT_TAIL_LINES
635}
636
637fn default_scrollback_lines() -> usize {
638 crate::constants::defaults::DEFAULT_PTY_SCROLLBACK_LINES
639}
640
641fn default_max_scrollback_bytes() -> usize {
642 25_000_000 }
646
647fn default_large_output_threshold_kb() -> usize {
648 5_000 }
650
651fn default_tool_output_mode() -> ToolOutputMode {
652 ToolOutputMode::Compact
653}
654
655fn default_tool_output_max_lines() -> usize {
656 600
657}
658
659fn default_tool_output_spool_bytes() -> usize {
660 200_000
661}
662
663fn default_allow_tool_ansi() -> bool {
664 false
665}
666
667fn default_inline_viewport_rows() -> u16 {
668 crate::constants::ui::DEFAULT_INLINE_VIEWPORT_ROWS
669}
670
671fn default_reasoning_display_mode() -> ReasoningDisplayMode {
672 ReasoningDisplayMode::Toggle
673}
674
675fn default_reasoning_visible_default() -> bool {
676 crate::constants::ui::DEFAULT_REASONING_VISIBLE
677}
678
679#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
682#[derive(Debug, Clone, Deserialize, Serialize)]
683pub struct KeyboardProtocolConfig {
684 #[serde(default = "default_keyboard_protocol_enabled")]
686 pub enabled: bool,
687
688 #[serde(default = "default_keyboard_protocol_mode")]
690 pub mode: String,
691
692 #[serde(default = "default_disambiguate_escape_codes")]
694 pub disambiguate_escape_codes: bool,
695
696 #[serde(default = "default_report_event_types")]
698 pub report_event_types: bool,
699
700 #[serde(default = "default_report_alternate_keys")]
702 pub report_alternate_keys: bool,
703
704 #[serde(default = "default_report_all_keys")]
706 pub report_all_keys: bool,
707}
708
709impl Default for KeyboardProtocolConfig {
710 fn default() -> Self {
711 Self {
712 enabled: default_keyboard_protocol_enabled(),
713 mode: default_keyboard_protocol_mode(),
714 disambiguate_escape_codes: default_disambiguate_escape_codes(),
715 report_event_types: default_report_event_types(),
716 report_alternate_keys: default_report_alternate_keys(),
717 report_all_keys: default_report_all_keys(),
718 }
719 }
720}
721
722impl KeyboardProtocolConfig {
723 pub fn validate(&self) -> Result<()> {
724 match self.mode.as_str() {
725 "default" | "full" | "minimal" | "custom" => Ok(()),
726 _ => anyhow::bail!(
727 "Invalid keyboard protocol mode '{}'. Must be: default, full, minimal, or custom",
728 self.mode
729 ),
730 }
731 }
732}
733
734fn default_keyboard_protocol_enabled() -> bool {
735 std::env::var("VTCODE_KEYBOARD_PROTOCOL_ENABLED")
736 .ok()
737 .and_then(|v| v.parse().ok())
738 .unwrap_or(true)
739}
740
741fn default_keyboard_protocol_mode() -> String {
742 std::env::var("VTCODE_KEYBOARD_PROTOCOL_MODE").unwrap_or_else(|_| "default".to_string())
743}
744
745fn default_disambiguate_escape_codes() -> bool {
746 true
747}
748
749fn default_report_event_types() -> bool {
750 true
751}
752
753fn default_report_alternate_keys() -> bool {
754 true
755}
756
757fn default_report_all_keys() -> bool {
758 false
759}