syncable_cli/agent/session/
plan_mode.rs

1//! Plan mode utilities and incomplete plan tracking
2//!
3//! This module provides:
4//! - `PlanMode` enum for toggling between standard and planning modes
5//! - `IncompletePlan` struct for tracking plan progress
6//! - `find_incomplete_plans` function to discover incomplete plans
7
8use regex::Regex;
9
10/// Information about an incomplete plan
11#[derive(Debug, Clone)]
12pub struct IncompletePlan {
13    pub path: String,
14    pub filename: String,
15    pub done: usize,
16    pub pending: usize,
17    pub total: usize,
18}
19
20/// Find incomplete plans in the plans/ directory
21pub fn find_incomplete_plans(project_path: &std::path::Path) -> Vec<IncompletePlan> {
22    let plans_dir = project_path.join("plans");
23    if !plans_dir.exists() {
24        return Vec::new();
25    }
26
27    let task_regex = Regex::new(r"^\s*-\s*\[([ x~!])\]").unwrap();
28    let mut incomplete = Vec::new();
29
30    if let Ok(entries) = std::fs::read_dir(&plans_dir) {
31        for entry in entries.flatten() {
32            let path = entry.path();
33            if path.extension().map(|e| e == "md").unwrap_or(false)
34                && let Ok(content) = std::fs::read_to_string(&path)
35            {
36                let mut done = 0;
37                let mut pending = 0;
38                let mut in_progress = 0;
39
40                for line in content.lines() {
41                    if let Some(caps) = task_regex.captures(line) {
42                        match caps.get(1).map(|m| m.as_str()) {
43                            Some("x") => done += 1,
44                            Some(" ") => pending += 1,
45                            Some("~") => in_progress += 1,
46                            Some("!") => done += 1, // Failed counts as "attempted"
47                            _ => {}
48                        }
49                    }
50                }
51
52                let total = done + pending + in_progress;
53                if total > 0 && (pending > 0 || in_progress > 0) {
54                    let rel_path = path
55                        .strip_prefix(project_path)
56                        .map(|p| p.display().to_string())
57                        .unwrap_or_else(|_| path.display().to_string());
58
59                    incomplete.push(IncompletePlan {
60                        path: rel_path,
61                        filename: path
62                            .file_name()
63                            .map(|n| n.to_string_lossy().to_string())
64                            .unwrap_or_default(),
65                        done,
66                        pending: pending + in_progress,
67                        total,
68                    });
69                }
70            }
71        }
72    }
73
74    // Sort by most recently modified (newest first)
75    incomplete.sort_by(|a, b| b.filename.cmp(&a.filename));
76    incomplete
77}
78
79/// Planning mode state - toggles between standard and plan mode
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
81pub enum PlanMode {
82    /// Standard mode - all tools available, normal operation
83    #[default]
84    Standard,
85    /// Planning mode - read-only exploration, no file modifications
86    Planning,
87}
88
89impl PlanMode {
90    /// Toggle between Standard and Planning mode
91    pub fn toggle(&self) -> Self {
92        match self {
93            PlanMode::Standard => PlanMode::Planning,
94            PlanMode::Planning => PlanMode::Standard,
95        }
96    }
97
98    /// Check if in planning mode
99    pub fn is_planning(&self) -> bool {
100        matches!(self, PlanMode::Planning)
101    }
102
103    /// Get display name for the mode
104    pub fn display_name(&self) -> &'static str {
105        match self {
106            PlanMode::Standard => "standard mode",
107            PlanMode::Planning => "plan mode",
108        }
109    }
110}