Skip to main content

j_agent/tools/todo/
todo_manager.rs

1use super::entity::TodoItem;
2use crate::permission::JcliConfig;
3use crate::util::safe_lock;
4use std::fs;
5use std::path::PathBuf;
6use std::sync::Mutex;
7use std::sync::atomic::{AtomicU32, Ordering};
8
9/// Todo 管理器:轻量级方向跟踪,单文件持久化
10#[derive(Debug)]
11pub struct TodoManager {
12    items: Mutex<Vec<TodoItem>>,
13    file_path: PathBuf,
14    /// 距离上次 TodoWrite 调用的轮数(用于 nag reminder)
15    turns_without_call: AtomicU32,
16}
17
18impl Default for TodoManager {
19    fn default() -> Self {
20        Self::new()
21    }
22}
23
24impl TodoManager {
25    pub fn new() -> Self {
26        // 优先使用 .jcli/todos.json,找不到则在 cwd 下创建 .jcli/todos.json
27        let config_dir = JcliConfig::find_config_dir().or_else(JcliConfig::ensure_config_dir);
28        let file_path = match config_dir {
29            Some(dir) => {
30                let _ = fs::create_dir_all(&dir);
31                dir.join("todos.json")
32            }
33            None => {
34                // 极端 fallback:使用全局目录
35                let data_dir = crate::constants::data_root();
36                let dir = data_dir.join("agent").join("data");
37                let _ = fs::create_dir_all(&dir);
38                dir.join("todos.json")
39            }
40        };
41
42        // 从磁盘加载已有数据
43        let items = if file_path.exists() {
44            fs::read_to_string(&file_path)
45                .ok()
46                .and_then(|data| serde_json::from_str::<Vec<TodoItem>>(&data).ok())
47                .unwrap_or_default()
48        } else {
49            Vec::new()
50        };
51
52        Self {
53            items: Mutex::new(items),
54            file_path,
55            turns_without_call: AtomicU32::new(0),
56        }
57    }
58
59    /// 使用任意文件路径创建 TodoManager(用于 session / teammate / subagent 独立 todo 文件)
60    pub fn new_with_file_path(file_path: PathBuf) -> Self {
61        if let Some(parent) = file_path.parent() {
62            let _ = fs::create_dir_all(parent);
63        }
64        let items = if file_path.exists() {
65            fs::read_to_string(&file_path)
66                .ok()
67                .and_then(|data| serde_json::from_str::<Vec<TodoItem>>(&data).ok())
68                .unwrap_or_default()
69        } else {
70            Vec::new()
71        };
72        Self {
73            items: Mutex::new(items),
74            file_path,
75            turns_without_call: AtomicU32::new(0),
76        }
77    }
78
79    /// 写入 todos。merge=false 替换全部;merge=true 按 id 合并更新。
80    /// 返回写入后的完整列表。
81    pub fn write_todos(
82        &self,
83        new_items: Vec<TodoItem>,
84        merge: bool,
85    ) -> Result<Vec<TodoItem>, String> {
86        let mut items = safe_lock(&self.items, "TodoManager::write_todos");
87
88        if merge {
89            // 合并模式:按 id 更新已有项,添加新项
90            for new_item in new_items {
91                if let Some(existing) = items.iter_mut().find(|i| i.id == new_item.id) {
92                    if !new_item.content.is_empty() {
93                        existing.content = new_item.content;
94                    }
95                    existing.status = new_item.status;
96                } else {
97                    // 新项,自动分配 id(如果为空)
98                    let item = if new_item.id.is_empty() {
99                        TodoItem {
100                            id: self.next_id_from(&items),
101                            ..new_item
102                        }
103                    } else {
104                        new_item
105                    };
106                    items.push(item);
107                }
108            }
109        } else {
110            // 替换模式:用新列表替换全部
111            let mut final_items = Vec::with_capacity(new_items.len());
112            for (idx, item) in new_items.into_iter().enumerate() {
113                let item = if item.id.is_empty() {
114                    TodoItem {
115                        id: (idx + 1).to_string(),
116                        ..item
117                    }
118                } else {
119                    item
120                };
121                final_items.push(item);
122            }
123            *items = final_items;
124        }
125
126        // 强制只允许一个 in_progress:最后设为 in_progress 的胜出,其余降为 pending
127        self.enforce_single_in_progress(&mut items);
128
129        // 重置 nag 计数器
130        self.turns_without_call.store(0, Ordering::Relaxed);
131
132        // 持久化
133        self.save(&items)?;
134        Ok(items.clone())
135    }
136
137    #[allow(dead_code)]
138    pub fn list_todos(&self) -> Vec<TodoItem> {
139        let items = safe_lock(&self.items, "TodoManager::list_todos");
140        items.clone()
141    }
142
143    pub fn has_todos(&self) -> bool {
144        let items = safe_lock(&self.items, "TodoManager::has_todos");
145        items
146            .iter()
147            .any(|i| i.status == "pending" || i.status == "in_progress")
148    }
149
150    /// 格式化当前 todos 为可读字符串(供 nag reminder 使用)
151    pub fn format_todos_summary(&self) -> String {
152        let items = safe_lock(&self.items, "TodoManager::format_todos_summary");
153        if items.is_empty() {
154            return "No active todos.".to_string();
155        }
156        let mut summary = String::new();
157        for item in items.iter() {
158            let icon = match item.status.as_str() {
159                "completed" => "✅",
160                "in_progress" => "🔄",
161                "cancelled" => "❌",
162                _ => "⬜",
163            };
164            summary.push_str(&format!(
165                "{} [{}] {}: {}\n",
166                icon, item.id, item.status, item.content
167            ));
168        }
169        summary.trim_end().to_string()
170    }
171
172    /// 每轮 agent loop 调用,递增计数器
173    pub fn increment_turn(&self) {
174        self.turns_without_call.fetch_add(1, Ordering::Relaxed);
175    }
176
177    /// 获取距离上次调用的轮数
178    pub fn turns_since_last_call(&self) -> u32 {
179        self.turns_without_call.load(Ordering::Relaxed)
180    }
181
182    // ── 内部方法 ──
183
184    /// 生成下一个 id(基于现有最大数字 id + 1)
185    fn next_id_from(&self, items: &[TodoItem]) -> String {
186        let max_id = items
187            .iter()
188            .filter_map(|i| i.id.parse::<u64>().ok())
189            .max()
190            .unwrap_or(0);
191        (max_id + 1).to_string()
192    }
193
194    /// 确保只有一个 in_progress:保留最后一个,其余降为 pending
195    fn enforce_single_in_progress(&self, items: &mut [TodoItem]) {
196        let in_progress_indices: Vec<usize> = items
197            .iter()
198            .enumerate()
199            .filter(|(_, i)| i.status == "in_progress")
200            .map(|(idx, _)| idx)
201            .collect();
202
203        if in_progress_indices.len() > 1 {
204            // 保留最后一个 in_progress,其余降为 pending
205            for &idx in &in_progress_indices[..in_progress_indices.len() - 1] {
206                items[idx].status = "pending".to_string();
207            }
208        }
209    }
210
211    fn save(&self, items: &[TodoItem]) -> Result<(), String> {
212        let data = serde_json::to_string_pretty(items)
213            .map_err(|e| format!("Failed to serialize todos: {}", e))?;
214        fs::write(&self.file_path, data).map_err(|e| format!("Failed to write todos: {}", e))
215    }
216
217    /// 替换所有 todos(session 恢复时使用)
218    pub fn replace_all(&self, new_items: Vec<TodoItem>) {
219        let mut items = safe_lock(&self.items, "TodoManager::replace_all");
220        *items = new_items;
221        // 重置 nag 计数器
222        self.turns_without_call.store(0, Ordering::Relaxed);
223        // 持久化
224        if let Err(e) = self.save(&items) {
225            crate::util::log::write_error_log("TodoManager::replace_all", &e);
226        }
227    }
228}