1use std::sync::Arc;
5use std::sync::Mutex;
6use std::time::SystemTime;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum ModeColor {
11 Blue,
12 Green,
13 Yellow,
14}
15
16#[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 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 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 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 pub const fn current_mode(&self) -> OperatingMode {
106 self.current_mode
107 }
108
109 pub const fn current_visuals(&self) -> ModeVisuals {
111 self.current_mode.visuals()
112 }
113
114 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_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, 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 pub const fn is_read_only(&self) -> bool {
147 matches!(self.current_mode, OperatingMode::Plan)
148 }
149
150 pub const fn allows_write(&self) -> bool {
152 self.restrictions.allow_file_write
153 }
154
155 pub const fn allows_execution(&self) -> bool {
157 self.restrictions.allow_command_exec
158 }
159
160 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 pub const fn can_execute_command(&self) -> bool {
175 self.restrictions.allow_command_exec
176 }
177
178 pub const fn can_perform_git_operations(&self) -> bool {
180 self.restrictions.allow_git_operations
181 }
182
183 pub const fn can_access_network(&self) -> bool {
185 self.restrictions.allow_network_access
186 }
187
188 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 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 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 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 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 manager.cycle();
309 assert_eq!(manager.current_mode(), OperatingMode::Build);
310
311 manager.cycle();
313 assert_eq!(manager.current_mode(), OperatingMode::Review);
314
315 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()); }
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))); 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()); assert!(!manager.allows_execution());
353
354 assert!(manager.can_write_file(Some(5_000))); assert!(manager.can_write_file(Some(10_000))); assert!(!manager.can_write_file(Some(10_001))); 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 assert!(manager.validate_file_write("test.txt", Some(100)).is_err());
370
371 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 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 assert!(manager.validate_command_execution("ls").is_err());
395
396 manager.switch_mode(OperatingMode::Build);
398 assert!(manager.validate_command_execution("ls").is_ok());
399
400 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")); 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}