agcodex_core/
modes.rs

1//! Operating modes for AGCodex (Plan, Build, Review).
2//! Minimal scaffolding to start the refactor without impacting existing flows.
3
4use std::sync::Arc;
5use std::sync::Mutex;
6use std::time::SystemTime;
7
8/// Color representation for mode indicators
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum ModeColor {
11    Blue,
12    Green,
13    Yellow,
14}
15
16/// Visual properties for each mode
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct ModeVisuals {
19    pub indicator: &'static str,
20    pub color: ModeColor,
21    pub description: &'static str,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
25pub enum OperatingMode {
26    Plan,
27    Build,
28    Review,
29}
30
31impl OperatingMode {
32    /// Get the visual properties for this mode
33    pub const fn visuals(&self) -> ModeVisuals {
34        match self {
35            Self::Plan => ModeVisuals {
36                indicator: "📋 PLAN",
37                color: ModeColor::Blue,
38                description: "Read-only analysis mode",
39            },
40            Self::Build => ModeVisuals {
41                indicator: "🔨 BUILD",
42                color: ModeColor::Green,
43                description: "Full access development mode",
44            },
45            Self::Review => ModeVisuals {
46                indicator: "🔍 REVIEW",
47                color: ModeColor::Yellow,
48                description: "Quality-focused review mode",
49            },
50        }
51    }
52
53    /// Cycle to the next mode
54    pub const fn cycle(&self) -> Self {
55        match self {
56            Self::Plan => Self::Build,
57            Self::Build => Self::Review,
58            Self::Review => Self::Plan,
59        }
60    }
61}
62
63#[derive(Debug, Clone, PartialEq, Eq, Default)]
64pub struct ModeRestrictions {
65    pub allow_file_write: bool,
66    pub allow_command_exec: bool,
67    pub allow_network_access: bool,
68    pub allow_git_operations: bool,
69    pub max_file_size: Option<usize>,
70}
71
72#[derive(Debug, Clone)]
73pub struct ModeManager {
74    pub current_mode: OperatingMode,
75    pub mode_history: Vec<(OperatingMode, SystemTime)>,
76    pub restrictions: ModeRestrictions,
77}
78
79impl ModeManager {
80    pub fn new(initial: OperatingMode) -> Self {
81        let mut mgr = Self {
82            current_mode: initial,
83            mode_history: Vec::new(),
84            restrictions: ModeRestrictions::default(),
85        };
86        mgr.apply_restrictions(initial);
87        mgr
88    }
89
90    pub fn switch_mode(&mut self, new_mode: OperatingMode) {
91        self.mode_history
92            .push((self.current_mode, SystemTime::now()));
93        self.current_mode = new_mode;
94        self.apply_restrictions(new_mode);
95    }
96
97    /// Cycle to the next mode (Plan → Build → Review → Plan)
98    pub fn cycle(&mut self) -> OperatingMode {
99        let new_mode = self.current_mode.cycle();
100        self.switch_mode(new_mode);
101        new_mode
102    }
103
104    /// Get the current mode
105    pub const fn current_mode(&self) -> OperatingMode {
106        self.current_mode
107    }
108
109    /// Get visual properties for the current mode
110    pub const fn current_visuals(&self) -> ModeVisuals {
111        self.current_mode.visuals()
112    }
113
114    /// Create a new thread-safe ModeManager
115    pub fn new_shared(initial: OperatingMode) -> Arc<Mutex<Self>> {
116        Arc::new(Mutex::new(Self::new(initial)))
117    }
118
119    const fn apply_restrictions(&mut self, mode: OperatingMode) {
120        self.restrictions = match mode {
121            OperatingMode::Plan => ModeRestrictions {
122                allow_file_write: false,
123                allow_command_exec: false,
124                allow_network_access: true, // Allow research
125                allow_git_operations: false,
126                max_file_size: None,
127            },
128            OperatingMode::Build => ModeRestrictions {
129                allow_file_write: true,
130                allow_command_exec: true,
131                allow_network_access: true,
132                allow_git_operations: true,
133                max_file_size: None,
134            },
135            OperatingMode::Review => ModeRestrictions {
136                allow_file_write: true, // Limited edits allowed
137                allow_command_exec: false,
138                allow_network_access: true,
139                allow_git_operations: false,
140                max_file_size: Some(10_000),
141            },
142        };
143    }
144
145    /// Check if the current mode is read-only
146    pub const fn is_read_only(&self) -> bool {
147        matches!(self.current_mode, OperatingMode::Plan)
148    }
149
150    /// Check if file writes are allowed in the current mode
151    pub const fn allows_write(&self) -> bool {
152        self.restrictions.allow_file_write
153    }
154
155    /// Check if command execution is allowed in the current mode
156    pub const fn allows_execution(&self) -> bool {
157        self.restrictions.allow_command_exec
158    }
159
160    /// Check if a file operation is allowed in the current mode
161    pub const fn can_write_file(&self, file_size: Option<usize>) -> bool {
162        if !self.restrictions.allow_file_write {
163            return false;
164        }
165
166        if let (Some(max_size), Some(size)) = (self.restrictions.max_file_size, file_size) {
167            return size <= max_size;
168        }
169
170        true
171    }
172
173    /// Check if command execution is allowed in the current mode
174    pub const fn can_execute_command(&self) -> bool {
175        self.restrictions.allow_command_exec
176    }
177
178    /// Check if git operations are allowed in the current mode
179    pub const fn can_perform_git_operations(&self) -> bool {
180        self.restrictions.allow_git_operations
181    }
182
183    /// Check if network access is allowed in the current mode
184    pub const fn can_access_network(&self) -> bool {
185        self.restrictions.allow_network_access
186    }
187
188    /// Get a user-friendly error message for disallowed operations
189    pub fn restriction_message(&self, operation: &str) -> String {
190        match self.current_mode {
191            OperatingMode::Plan => format!(
192                "⛔ Operation '{}' not allowed in Plan mode (read-only). Switch to Build mode (Shift+Tab) for full access.",
193                operation
194            ),
195            OperatingMode::Review => {
196                // Check if it's a size restriction issue
197                if operation.contains("file") || operation.contains("edit") {
198                    if let Some(max_size) = self.restrictions.max_file_size {
199                        format!(
200                            "⚠️ Operation '{}' is limited in Review mode. File edits must be under {} bytes. Switch to Build mode (Shift+Tab) for unlimited access.",
201                            operation, max_size
202                        )
203                    } else {
204                        format!(
205                            "⚠️ Operation '{}' not allowed in Review mode (quality focus). Switch to Build mode (Shift+Tab) for full access.",
206                            operation
207                        )
208                    }
209                } else {
210                    format!(
211                        "⚠️ Operation '{}' not allowed in Review mode (quality focus). Switch to Build mode (Shift+Tab) for full access.",
212                        operation
213                    )
214                }
215            }
216            OperatingMode::Build => format!(
217                "✓ Operation '{}' should be allowed in Build mode. This may be a bug.",
218                operation
219            ),
220        }
221    }
222
223    /// Validate an operation and return a Result with appropriate error message
224    pub fn validate_file_write(&self, path: &str, size: Option<usize>) -> Result<(), String> {
225        if !self.allows_write() {
226            return Err(self.restriction_message(&format!("write to {}", path)));
227        }
228
229        if let Some(file_size) = size
230            && !self.can_write_file(Some(file_size))
231            && let Some(max_size) = self.restrictions.max_file_size
232        {
233            return Err(format!(
234                "⚠️ File '{}' exceeds the {} byte limit in Review mode. File size: {} bytes. Switch to Build mode (Shift+Tab) for unlimited file editing.",
235                path, max_size, file_size
236            ));
237        }
238
239        Ok(())
240    }
241
242    /// Validate command execution and return a Result with appropriate error message
243    pub fn validate_command_execution(&self, command: &str) -> Result<(), String> {
244        if !self.allows_execution() {
245            return Err(self.restriction_message(&format!("execute command '{}'", command)));
246        }
247        Ok(())
248    }
249
250    /// Validate git operations and return a Result with appropriate error message
251    pub fn validate_git_operation(&self, operation: &str) -> Result<(), String> {
252        if !self.can_perform_git_operations() {
253            return Err(self.restriction_message(&format!("git operation '{}'", operation)));
254        }
255        Ok(())
256    }
257
258    pub const fn prompt_suffix(&self) -> &'static str {
259        match self.current_mode {
260            OperatingMode::Plan => {
261                r#"
262<mode>PLAN MODE - Read Only</mode>
263You are analyzing and planning. You CAN:
264✓ Read any file
265✓ Search code using AST-powered semantic search
266✓ Analyze AST structure with tree-sitter
267✓ Create detailed implementation plans
268
269You CANNOT:
270✗ Edit or write files
271✗ Execute commands that modify state
272"#
273            }
274            OperatingMode::Build => {
275                r#"
276<mode>BUILD MODE - Full Access</mode>
277You have complete development capabilities:
278✓ Read, write, edit files
279✓ Execute commands
280✓ Use all tools
281✓ Full AST-based editing
282"#
283            }
284            OperatingMode::Review => {
285                r#"
286<mode>REVIEW MODE - Quality Focus</mode>
287You are reviewing code quality. You CAN:
288✓ Read and analyze code
289✓ Suggest improvements
290✓ Make small fixes (< 100 lines)
291Focus on: bugs, performance, best practices, security
292"#
293            }
294        }
295    }
296}
297
298#[cfg(test)]
299mod tests {
300    use super::*;
301
302    #[test]
303    fn test_mode_cycling() {
304        let mut manager = ModeManager::new(OperatingMode::Plan);
305        assert_eq!(manager.current_mode(), OperatingMode::Plan);
306
307        // Cycle from Plan to Build
308        manager.cycle();
309        assert_eq!(manager.current_mode(), OperatingMode::Build);
310
311        // Cycle from Build to Review
312        manager.cycle();
313        assert_eq!(manager.current_mode(), OperatingMode::Review);
314
315        // Cycle from Review back to Plan
316        manager.cycle();
317        assert_eq!(manager.current_mode(), OperatingMode::Plan);
318    }
319
320    #[test]
321    fn test_plan_mode_restrictions() {
322        let manager = ModeManager::new(OperatingMode::Plan);
323
324        assert!(manager.is_read_only());
325        assert!(!manager.allows_write());
326        assert!(!manager.allows_execution());
327        assert!(!manager.can_write_file(Some(100)));
328        assert!(!manager.can_execute_command());
329        assert!(!manager.can_perform_git_operations());
330        assert!(manager.can_access_network()); // Research is allowed
331    }
332
333    #[test]
334    fn test_build_mode_restrictions() {
335        let manager = ModeManager::new(OperatingMode::Build);
336
337        assert!(!manager.is_read_only());
338        assert!(manager.allows_write());
339        assert!(manager.allows_execution());
340        assert!(manager.can_write_file(Some(1_000_000))); // Any size allowed
341        assert!(manager.can_execute_command());
342        assert!(manager.can_perform_git_operations());
343        assert!(manager.can_access_network());
344    }
345
346    #[test]
347    fn test_review_mode_restrictions() {
348        let manager = ModeManager::new(OperatingMode::Review);
349
350        assert!(!manager.is_read_only());
351        assert!(manager.allows_write()); // Limited writes allowed
352        assert!(!manager.allows_execution());
353
354        // Test file size restrictions (10KB limit)
355        assert!(manager.can_write_file(Some(5_000))); // Under limit
356        assert!(manager.can_write_file(Some(10_000))); // At limit
357        assert!(!manager.can_write_file(Some(10_001))); // Over limit
358
359        assert!(!manager.can_execute_command());
360        assert!(!manager.can_perform_git_operations());
361        assert!(manager.can_access_network());
362    }
363
364    #[test]
365    fn test_validate_file_write() {
366        let mut manager = ModeManager::new(OperatingMode::Plan);
367
368        // Plan mode - no writes allowed
369        assert!(manager.validate_file_write("test.txt", Some(100)).is_err());
370
371        // Build mode - all writes allowed
372        manager.switch_mode(OperatingMode::Build);
373        assert!(
374            manager
375                .validate_file_write("test.txt", Some(1_000_000))
376                .is_ok()
377        );
378
379        // Review mode - limited writes
380        manager.switch_mode(OperatingMode::Review);
381        assert!(manager.validate_file_write("test.txt", Some(5_000)).is_ok());
382        assert!(
383            manager
384                .validate_file_write("test.txt", Some(20_000))
385                .is_err()
386        );
387    }
388
389    #[test]
390    fn test_validate_command_execution() {
391        let mut manager = ModeManager::new(OperatingMode::Plan);
392
393        // Plan mode - no execution
394        assert!(manager.validate_command_execution("ls").is_err());
395
396        // Build mode - execution allowed
397        manager.switch_mode(OperatingMode::Build);
398        assert!(manager.validate_command_execution("ls").is_ok());
399
400        // Review mode - no execution
401        manager.switch_mode(OperatingMode::Review);
402        assert!(manager.validate_command_execution("rm -rf /").is_err());
403    }
404
405    #[test]
406    fn test_mode_history() {
407        let mut manager = ModeManager::new(OperatingMode::Plan);
408        assert_eq!(manager.mode_history.len(), 0);
409
410        manager.switch_mode(OperatingMode::Build);
411        assert_eq!(manager.mode_history.len(), 1);
412        assert_eq!(manager.mode_history[0].0, OperatingMode::Plan);
413
414        manager.switch_mode(OperatingMode::Review);
415        assert_eq!(manager.mode_history.len(), 2);
416        assert_eq!(manager.mode_history[1].0, OperatingMode::Build);
417    }
418
419    #[test]
420    fn test_restriction_messages() {
421        let manager = ModeManager::new(OperatingMode::Plan);
422        let msg = manager.restriction_message("write file");
423        assert!(msg.contains("Plan mode"));
424        assert!(msg.contains("read-only"));
425        assert!(msg.contains("Shift+Tab"));
426
427        let manager = ModeManager::new(OperatingMode::Review);
428        let msg = manager.restriction_message("edit large file");
429        assert!(msg.contains("Review mode"));
430        assert!(msg.contains("10000 bytes")); // Should mention the size limit
431
432        let manager = ModeManager::new(OperatingMode::Build);
433        let msg = manager.restriction_message("anything");
434        assert!(msg.contains("should be allowed"));
435        assert!(msg.contains("Build mode"));
436    }
437
438    #[test]
439    fn test_mode_visuals() {
440        assert_eq!(OperatingMode::Plan.visuals().indicator, "📋 PLAN");
441        assert_eq!(OperatingMode::Build.visuals().indicator, "🔨 BUILD");
442        assert_eq!(OperatingMode::Review.visuals().indicator, "🔍 REVIEW");
443
444        assert_eq!(OperatingMode::Plan.visuals().color, ModeColor::Blue);
445        assert_eq!(OperatingMode::Build.visuals().color, ModeColor::Green);
446        assert_eq!(OperatingMode::Review.visuals().color, ModeColor::Yellow);
447    }
448}