vtcode_config/types/
mod.rs

1//! Common types and interfaces used throughout the application
2
3use crate::constants::reasoning;
4use crate::core::PromptCachingConfig;
5use serde::{Deserialize, Deserializer, Serialize};
6use serde_json::Value;
7use std::collections::{BTreeMap, HashMap};
8use std::fmt;
9use std::path::PathBuf;
10
11/// Supported reasoning effort levels configured via vtcode.toml
12/// These map to different provider-specific parameters:
13/// - For Gemini 3 Pro: Maps to thinking_level (low, high) - medium coming soon
14/// - For other models: Maps to provider-specific reasoning parameters
15#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
17#[serde(rename_all = "lowercase")]
18#[derive(Default)]
19pub enum ReasoningEffortLevel {
20    /// No reasoning configuration - for models that don't support configurable reasoning
21    None,
22    /// Minimal reasoning effort - maps to low thinking level for Gemini 3 Pro
23    Minimal,
24    /// Low reasoning effort - maps to low thinking level for Gemini 3 Pro
25    Low,
26    /// Medium reasoning effort - Note: Not fully available for Gemini 3 Pro yet, defaults to high
27    #[default]
28    Medium,
29    /// High reasoning effort - maps to high thinking level for Gemini 3 Pro
30    High,
31    /// Extra high reasoning effort - for gpt-5.1-codex-max long-running tasks
32    XHigh,
33}
34
35impl ReasoningEffortLevel {
36    /// Return the textual representation expected by downstream APIs
37    pub fn as_str(self) -> &'static str {
38        match self {
39            Self::None => "none",
40            Self::Minimal => "minimal",
41            Self::Low => reasoning::LOW,
42            Self::Medium => reasoning::MEDIUM,
43            Self::High => reasoning::HIGH,
44            Self::XHigh => "xhigh",
45        }
46    }
47
48    /// Attempt to parse an effort level from user configuration input
49    pub fn parse(value: &str) -> Option<Self> {
50        let normalized = value.trim();
51        if normalized.eq_ignore_ascii_case("none") {
52            Some(Self::None)
53        } else if normalized.eq_ignore_ascii_case("minimal") {
54            Some(Self::Minimal)
55        } else if normalized.eq_ignore_ascii_case(reasoning::LOW) {
56            Some(Self::Low)
57        } else if normalized.eq_ignore_ascii_case(reasoning::MEDIUM) {
58            Some(Self::Medium)
59        } else if normalized.eq_ignore_ascii_case(reasoning::HIGH) {
60            Some(Self::High)
61        } else if normalized.eq_ignore_ascii_case("xhigh") {
62            Some(Self::XHigh)
63        } else {
64            None
65        }
66    }
67
68    /// Enumerate the allowed configuration values for validation and messaging
69    pub fn allowed_values() -> &'static [&'static str] {
70        reasoning::ALLOWED_LEVELS
71    }
72}
73
74/// System prompt mode (inspired by pi-coding-agent philosophy)
75/// Controls verbosity and complexity of system prompts sent to models
76#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
77#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
78#[serde(rename_all = "lowercase")]
79#[derive(Default)]
80pub enum SystemPromptMode {
81    /// Minimal prompt (~500-800 tokens) - Pi-inspired, modern models need less guidance
82    /// Best for: Power users, token-constrained contexts, fast responses
83    Minimal,
84    /// Lightweight prompt (~1-2k tokens) - Essential guidance only
85    /// Best for: Resource-constrained operations, simple tasks
86    Lightweight,
87    /// Default prompt (~6-7k tokens) - Full guidance with all features
88    /// Best for: General usage, comprehensive error handling
89    #[default]
90    Default,
91    /// Specialized prompt (~7-8k tokens) - Complex refactoring and analysis
92    /// Best for: Multi-file changes, sophisticated code analysis
93    Specialized,
94}
95
96impl SystemPromptMode {
97    /// Return the textual representation for configuration
98    pub fn as_str(self) -> &'static str {
99        match self {
100            Self::Minimal => "minimal",
101            Self::Lightweight => "lightweight",
102            Self::Default => "default",
103            Self::Specialized => "specialized",
104        }
105    }
106
107    /// Parse system prompt mode from user configuration
108    pub fn parse(value: &str) -> Option<Self> {
109        let normalized = value.trim();
110        if normalized.eq_ignore_ascii_case("minimal") {
111            Some(Self::Minimal)
112        } else if normalized.eq_ignore_ascii_case("lightweight") {
113            Some(Self::Lightweight)
114        } else if normalized.eq_ignore_ascii_case("default") {
115            Some(Self::Default)
116        } else if normalized.eq_ignore_ascii_case("specialized") {
117            Some(Self::Specialized)
118        } else {
119            None
120        }
121    }
122
123    /// Allowed configuration values for validation
124    pub fn allowed_values() -> &'static [&'static str] {
125        &["minimal", "lightweight", "default", "specialized"]
126    }
127}
128
129impl fmt::Display for SystemPromptMode {
130    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
131        f.write_str(self.as_str())
132    }
133}
134
135impl<'de> Deserialize<'de> for SystemPromptMode {
136    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
137    where
138        D: Deserializer<'de>,
139    {
140        let raw = String::deserialize(deserializer)?;
141        if let Some(parsed) = Self::parse(&raw) {
142            Ok(parsed)
143        } else {
144            Ok(Self::default())
145        }
146    }
147}
148
149/// Tool documentation mode (inspired by pi-coding-agent progressive disclosure)
150/// Controls how much tool documentation is loaded upfront vs on-demand
151#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
152#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
153#[serde(rename_all = "lowercase")]
154#[derive(Default)]
155pub enum ToolDocumentationMode {
156    /// Minimal signatures only (~800 tokens total) - Pi-style, power users
157    /// Best for: Maximum efficiency, experienced users, token-constrained contexts
158    Minimal,
159    /// Signatures + common parameters (~1,200 tokens total) - Smart hints
160    /// Best for: General usage, balances overhead vs guidance (recommended)
161    Progressive,
162    /// Full documentation upfront (~3,000 tokens total) - Current behavior
163    /// Best for: Maximum hand-holding, comprehensive parameter documentation
164    #[default]
165    Full,
166}
167
168impl ToolDocumentationMode {
169    /// Return the textual representation for configuration
170    pub fn as_str(self) -> &'static str {
171        match self {
172            Self::Minimal => "minimal",
173            Self::Progressive => "progressive",
174            Self::Full => "full",
175        }
176    }
177
178    /// Parse tool documentation mode from user configuration
179    pub fn parse(value: &str) -> Option<Self> {
180        let normalized = value.trim();
181        if normalized.eq_ignore_ascii_case("minimal") {
182            Some(Self::Minimal)
183        } else if normalized.eq_ignore_ascii_case("progressive") {
184            Some(Self::Progressive)
185        } else if normalized.eq_ignore_ascii_case("full") {
186            Some(Self::Full)
187        } else {
188            None
189        }
190    }
191
192    /// Allowed configuration values for validation
193    pub fn allowed_values() -> &'static [&'static str] {
194        &["minimal", "progressive", "full"]
195    }
196}
197
198impl fmt::Display for ToolDocumentationMode {
199    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200        f.write_str(self.as_str())
201    }
202}
203
204impl<'de> Deserialize<'de> for ToolDocumentationMode {
205    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
206    where
207        D: Deserializer<'de>,
208    {
209        let raw = String::deserialize(deserializer)?;
210        if let Some(parsed) = Self::parse(&raw) {
211            Ok(parsed)
212        } else {
213            Ok(Self::default())
214        }
215    }
216}
217
218/// Verbosity level for model output (GPT-5.1 and compatible models)
219#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
220#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
221#[serde(rename_all = "lowercase")]
222#[derive(Default)]
223pub enum VerbosityLevel {
224    Low,
225    #[default]
226    Medium,
227    High,
228}
229
230impl VerbosityLevel {
231    /// Return the textual representation expected by downstream APIs
232    pub fn as_str(self) -> &'static str {
233        match self {
234            Self::Low => "low",
235            Self::Medium => "medium",
236            Self::High => "high",
237        }
238    }
239
240    /// Attempt to parse a verbosity level from user configuration input
241    pub fn parse(value: &str) -> Option<Self> {
242        let normalized = value.trim();
243        if normalized.eq_ignore_ascii_case("low") {
244            Some(Self::Low)
245        } else if normalized.eq_ignore_ascii_case("medium") {
246            Some(Self::Medium)
247        } else if normalized.eq_ignore_ascii_case("high") {
248            Some(Self::High)
249        } else {
250            None
251        }
252    }
253
254    /// Enumerate the allowed configuration values
255    pub fn allowed_values() -> &'static [&'static str] {
256        &["low", "medium", "high"]
257    }
258}
259
260impl fmt::Display for VerbosityLevel {
261    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
262        f.write_str(self.as_str())
263    }
264}
265
266impl<'de> Deserialize<'de> for VerbosityLevel {
267    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
268    where
269        D: Deserializer<'de>,
270    {
271        let raw = String::deserialize(deserializer)?;
272        if let Some(parsed) = Self::parse(&raw) {
273            Ok(parsed)
274        } else {
275            Ok(Self::default())
276        }
277    }
278}
279
280impl fmt::Display for ReasoningEffortLevel {
281    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
282        f.write_str(self.as_str())
283    }
284}
285
286impl<'de> Deserialize<'de> for ReasoningEffortLevel {
287    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
288    where
289        D: Deserializer<'de>,
290    {
291        let raw = String::deserialize(deserializer)?;
292        if let Some(parsed) = Self::parse(&raw) {
293            Ok(parsed)
294        } else {
295            Ok(Self::default())
296        }
297    }
298}
299
300/// Preferred rendering surface for the interactive chat UI
301#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
302#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
303#[serde(rename_all = "lowercase")]
304#[derive(Default)]
305pub enum UiSurfacePreference {
306    #[default]
307    Auto,
308    Alternate,
309    Inline,
310}
311
312impl UiSurfacePreference {
313    /// String representation used in configuration and logging
314    pub fn as_str(self) -> &'static str {
315        match self {
316            Self::Auto => "auto",
317            Self::Alternate => "alternate",
318            Self::Inline => "inline",
319        }
320    }
321
322    /// Parse a surface preference from configuration input
323    pub fn parse(value: &str) -> Option<Self> {
324        let normalized = value.trim();
325        if normalized.eq_ignore_ascii_case("auto") {
326            Some(Self::Auto)
327        } else if normalized.eq_ignore_ascii_case("alternate")
328            || normalized.eq_ignore_ascii_case("alt")
329        {
330            Some(Self::Alternate)
331        } else if normalized.eq_ignore_ascii_case("inline") {
332            Some(Self::Inline)
333        } else {
334            None
335        }
336    }
337
338    /// Enumerate the accepted configuration values for validation messaging
339    pub fn allowed_values() -> &'static [&'static str] {
340        &["auto", "alternate", "inline"]
341    }
342}
343
344impl fmt::Display for UiSurfacePreference {
345    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
346        f.write_str(self.as_str())
347    }
348}
349
350impl<'de> Deserialize<'de> for UiSurfacePreference {
351    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
352    where
353        D: Deserializer<'de>,
354    {
355        let raw = String::deserialize(deserializer)?;
356        if let Some(parsed) = Self::parse(&raw) {
357            Ok(parsed)
358        } else {
359            Ok(Self::default())
360        }
361    }
362}
363
364/// Source describing how the active model was selected
365#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
366pub enum ModelSelectionSource {
367    /// Model provided by workspace configuration
368    #[default]
369    WorkspaceConfig,
370    /// Model provided by CLI override
371    CliOverride,
372}
373
374/// Default editing mode for agent startup (Codex-inspired workflow)
375///
376/// Controls the initial mode when a session starts. This is a **configuration**
377/// enum for `default_editing_mode` in vtcode.toml. At runtime, the mode can be
378/// cycled (Edit → Plan → Agent → Edit) via Shift+Tab or /plan, /agent commands.
379///
380/// Inspired by OpenAI Codex's emphasis on structured planning before execution,
381/// but provider-agnostic (works with Gemini, Anthropic, OpenAI, xAI, DeepSeek, etc.)
382#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
383#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
384#[serde(rename_all = "lowercase")]
385#[derive(Default)]
386pub enum EditingMode {
387    /// Full tool access - can read, write, execute commands (default)
388    /// Use for: Implementation, bug fixes, feature development
389    #[default]
390    Edit,
391    /// Read-only exploration - mutating tools blocked
392    /// Use for: Planning, research, architecture analysis
393    /// Agent can write plans to `.vtcode/plans/` but not modify code
394    Plan,
395    /// Autonomous agent mode - full tool access with reduced HITL prompts
396    /// Use for: Long-running autonomous tasks with minimal interruption
397    Agent,
398}
399
400impl EditingMode {
401    /// Return the textual representation for configuration and display
402    pub fn as_str(self) -> &'static str {
403        match self {
404            Self::Edit => "edit",
405            Self::Plan => "plan",
406            Self::Agent => "agent",
407        }
408    }
409
410    /// Parse editing mode from user configuration input
411    pub fn parse(value: &str) -> Option<Self> {
412        let normalized = value.trim();
413        if normalized.eq_ignore_ascii_case("edit") {
414            Some(Self::Edit)
415        } else if normalized.eq_ignore_ascii_case("plan") {
416            Some(Self::Plan)
417        } else if normalized.eq_ignore_ascii_case("agent") {
418            Some(Self::Agent)
419        } else {
420            None
421        }
422    }
423
424    /// Enumerate the allowed configuration values for validation
425    pub fn allowed_values() -> &'static [&'static str] {
426        &["edit", "plan", "agent"]
427    }
428
429    /// Check if this mode allows file modifications
430    pub fn can_modify_files(self) -> bool {
431        matches!(self, Self::Edit | Self::Agent)
432    }
433
434    /// Check if this mode allows command execution
435    pub fn can_execute_commands(self) -> bool {
436        matches!(self, Self::Edit | Self::Agent)
437    }
438
439    /// Check if this is read-only planning mode
440    pub fn is_read_only(self) -> bool {
441        matches!(self, Self::Plan)
442    }
443
444    /// Check if this is autonomous agent mode (reduced HITL)
445    pub fn is_autonomous(self) -> bool {
446        matches!(self, Self::Agent)
447    }
448}
449
450impl fmt::Display for EditingMode {
451    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
452        f.write_str(self.as_str())
453    }
454}
455
456impl<'de> Deserialize<'de> for EditingMode {
457    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
458    where
459        D: Deserializer<'de>,
460    {
461        let raw = String::deserialize(deserializer)?;
462        if let Some(parsed) = Self::parse(&raw) {
463            Ok(parsed)
464        } else {
465            Ok(Self::default())
466        }
467    }
468}
469
470/// Configuration for the agent
471#[derive(Debug, Clone)]
472pub struct AgentConfig {
473    pub model: String,
474    pub api_key: String,
475    pub provider: String,
476    pub api_key_env: String,
477    pub workspace: std::path::PathBuf,
478    pub verbose: bool,
479    pub quiet: bool,
480    pub theme: String,
481    pub reasoning_effort: ReasoningEffortLevel,
482    pub ui_surface: UiSurfacePreference,
483    pub prompt_cache: PromptCachingConfig,
484    pub model_source: ModelSelectionSource,
485    pub custom_api_keys: BTreeMap<String, String>,
486    pub checkpointing_enabled: bool,
487    pub checkpointing_storage_dir: Option<PathBuf>,
488    pub checkpointing_max_snapshots: usize,
489    pub checkpointing_max_age_days: Option<u64>,
490}
491
492/// Workshop agent capability levels
493#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
494pub enum CapabilityLevel {
495    /// Basic chat only
496    Basic,
497    /// Can read files
498    FileReading,
499    /// Can read files and list directories
500    FileListing,
501    /// Can read files, list directories, and run bash commands
502    Bash,
503    /// Can read files, list directories, run bash commands, and edit files
504    Editing,
505    /// Full capabilities including code search
506    CodeSearch,
507}
508
509/// Session information
510#[derive(Debug, Clone, Serialize, Deserialize)]
511pub struct SessionInfo {
512    pub session_id: String,
513    pub start_time: u64,
514    pub total_turns: usize,
515    pub total_decisions: usize,
516    pub error_count: usize,
517}
518
519/// Conversation turn information
520#[derive(Debug, Clone, Serialize, Deserialize)]
521pub struct ConversationTurn {
522    pub turn_number: usize,
523    pub timestamp: u64,
524    pub user_input: Option<String>,
525    pub agent_response: Option<String>,
526    pub tool_calls: Vec<ToolCallInfo>,
527    pub decision: Option<DecisionInfo>,
528}
529
530/// Tool call information
531#[derive(Debug, Clone, Serialize, Deserialize)]
532pub struct ToolCallInfo {
533    pub name: String,
534    pub args: Value,
535    pub result: Option<Value>,
536    pub error: Option<String>,
537    pub execution_time_ms: Option<u64>,
538}
539
540/// Decision information for tracking
541#[derive(Debug, Clone, Serialize, Deserialize)]
542pub struct DecisionInfo {
543    pub turn_number: usize,
544    pub action_type: String,
545    pub description: String,
546    pub reasoning: String,
547    pub outcome: Option<String>,
548    pub confidence_score: Option<f64>,
549    pub timestamp: u64,
550}
551
552/// Error information for tracking
553#[derive(Debug, Clone, Serialize, Deserialize)]
554pub struct ErrorInfo {
555    pub error_type: String,
556    pub message: String,
557    pub turn_number: usize,
558    pub recoverable: bool,
559    pub timestamp: u64,
560}
561
562/// Task information for project workflows
563#[derive(Debug, Clone, Serialize, Deserialize)]
564pub struct TaskInfo {
565    pub task_type: String,
566    pub description: String,
567    pub completed: bool,
568    pub success: bool,
569    pub duration_seconds: Option<u64>,
570    pub tools_used: Vec<String>,
571    pub dependencies: Vec<String>,
572}
573
574/// Project creation specification
575#[derive(Debug, Clone, Serialize, Deserialize)]
576pub struct ProjectSpec {
577    pub name: String,
578    pub features: Vec<String>,
579    pub template: Option<String>,
580    pub dependencies: HashMap<String, String>,
581}
582
583/// Workspace analysis result
584#[derive(Debug, Clone, Serialize, Deserialize)]
585pub struct WorkspaceAnalysis {
586    pub root_path: String,
587    pub project_type: Option<String>,
588    pub languages: Vec<String>,
589    pub frameworks: Vec<String>,
590    pub config_files: Vec<String>,
591    pub source_files: Vec<String>,
592    pub test_files: Vec<String>,
593    pub documentation_files: Vec<String>,
594    pub total_files: usize,
595    pub total_size_bytes: u64,
596}
597
598/// Command execution result
599#[derive(Debug, Clone, Serialize, Deserialize)]
600pub struct CommandResult {
601    pub command: String,
602    pub success: bool,
603    pub stdout: String,
604    pub stderr: String,
605    pub exit_code: Option<i32>,
606    pub execution_time_ms: u64,
607}
608
609/// File operation result
610#[derive(Debug, Clone, Serialize, Deserialize)]
611pub struct FileOperationResult {
612    pub operation: String,
613    pub path: String,
614    pub success: bool,
615    pub details: HashMap<String, Value>,
616    pub error: Option<String>,
617}
618
619/// Performance metrics
620#[derive(Debug, Clone, Serialize, Deserialize)]
621pub struct PerformanceMetrics {
622    pub session_duration_seconds: u64,
623    pub total_api_calls: usize,
624    pub total_tokens_used: Option<usize>,
625    pub average_response_time_ms: f64,
626    pub tool_execution_count: usize,
627    pub error_count: usize,
628    pub recovery_success_rate: f64,
629}
630
631/// Quality metrics for agent actions
632#[derive(Debug, Clone, Serialize, Deserialize)]
633pub struct QualityMetrics {
634    pub decision_confidence_avg: f64,
635    pub tool_success_rate: f64,
636    pub error_recovery_rate: f64,
637    pub context_preservation_rate: f64,
638    pub user_satisfaction_score: Option<f64>,
639}
640
641/// Configuration for tool behavior
642#[derive(Debug, Clone, Serialize, Deserialize)]
643pub struct ToolConfig {
644    pub enable_validation: bool,
645    pub max_execution_time_seconds: u64,
646    pub allow_file_creation: bool,
647    pub allow_file_deletion: bool,
648    pub working_directory: Option<String>,
649}
650
651/// Context management settings
652#[derive(Debug, Clone, Serialize, Deserialize)]
653pub struct ContextConfig {
654    pub max_context_length: usize,
655    pub compression_threshold: usize,
656    pub summarization_interval: usize,
657    pub preservation_priority: Vec<String>,
658}
659
660/// Logging configuration
661#[derive(Debug, Clone, Serialize, Deserialize)]
662pub struct LoggingConfig {
663    pub level: String,
664    pub file_logging: bool,
665    pub log_directory: Option<String>,
666    pub max_log_files: usize,
667    pub max_log_size_mb: usize,
668}
669
670/// Analysis depth for workspace analysis
671#[derive(Debug, Clone, Serialize, Deserialize)]
672pub enum AnalysisDepth {
673    Basic,
674    Standard,
675    Deep,
676}
677
678/// Output format for commands
679#[derive(Debug, Clone, Serialize, Deserialize)]
680pub enum OutputFormat {
681    Text,
682    Json,
683    Html,
684}
685
686/// Compression level for context compression
687#[derive(Debug, Clone, Serialize, Deserialize)]
688pub enum CompressionLevel {
689    Light,
690    Medium,
691    Aggressive,
692}
693
694#[cfg(test)]
695mod tests {
696    use super::*;
697
698    #[test]
699    fn test_editing_mode_parse() {
700        assert_eq!(EditingMode::parse("edit"), Some(EditingMode::Edit));
701        assert_eq!(EditingMode::parse("EDIT"), Some(EditingMode::Edit));
702        assert_eq!(EditingMode::parse("Edit"), Some(EditingMode::Edit));
703        assert_eq!(EditingMode::parse("plan"), Some(EditingMode::Plan));
704        assert_eq!(EditingMode::parse("PLAN"), Some(EditingMode::Plan));
705        assert_eq!(EditingMode::parse("Plan"), Some(EditingMode::Plan));
706        assert_eq!(EditingMode::parse("agent"), Some(EditingMode::Agent));
707        assert_eq!(EditingMode::parse("AGENT"), Some(EditingMode::Agent));
708        assert_eq!(EditingMode::parse("Agent"), Some(EditingMode::Agent));
709        assert_eq!(EditingMode::parse("invalid"), None);
710        assert_eq!(EditingMode::parse(""), None);
711    }
712
713    #[test]
714    fn test_editing_mode_as_str() {
715        assert_eq!(EditingMode::Edit.as_str(), "edit");
716        assert_eq!(EditingMode::Plan.as_str(), "plan");
717        assert_eq!(EditingMode::Agent.as_str(), "agent");
718    }
719
720    #[test]
721    fn test_editing_mode_capabilities() {
722        // Edit mode: full access
723        assert!(EditingMode::Edit.can_modify_files());
724        assert!(EditingMode::Edit.can_execute_commands());
725        assert!(!EditingMode::Edit.is_read_only());
726        assert!(!EditingMode::Edit.is_autonomous());
727
728        // Plan mode: read-only
729        assert!(!EditingMode::Plan.can_modify_files());
730        assert!(!EditingMode::Plan.can_execute_commands());
731        assert!(EditingMode::Plan.is_read_only());
732        assert!(!EditingMode::Plan.is_autonomous());
733
734        // Agent mode: full access + autonomous
735        assert!(EditingMode::Agent.can_modify_files());
736        assert!(EditingMode::Agent.can_execute_commands());
737        assert!(!EditingMode::Agent.is_read_only());
738        assert!(EditingMode::Agent.is_autonomous());
739    }
740
741    #[test]
742    fn test_editing_mode_default() {
743        assert_eq!(EditingMode::default(), EditingMode::Edit);
744    }
745
746    #[test]
747    fn test_editing_mode_display() {
748        assert_eq!(format!("{}", EditingMode::Edit), "edit");
749        assert_eq!(format!("{}", EditingMode::Plan), "plan");
750        assert_eq!(format!("{}", EditingMode::Agent), "agent");
751    }
752
753    #[test]
754    fn test_editing_mode_allowed_values() {
755        let values = EditingMode::allowed_values();
756        assert_eq!(values, &["edit", "plan", "agent"]);
757    }
758}