Skip to main content

vtcode_config/types/
mod.rs

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