ai_agent/utils/task_list/
mod.rs1use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6use std::path::PathBuf;
7use std::sync::{Mutex, OnceLock};
8
9pub const TASK_STATUSES: [&str; 3] = ["pending", "in_progress", "completed"];
11
12#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
14#[serde(rename_all = "snake_case")]
15pub enum TaskStatus {
16 Pending,
17 InProgress,
18 Completed,
19}
20
21impl std::fmt::Display for TaskStatus {
22 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23 match self {
24 TaskStatus::Pending => write!(f, "pending"),
25 TaskStatus::InProgress => write!(f, "in_progress"),
26 TaskStatus::Completed => write!(f, "completed"),
27 }
28 }
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct Task {
34 pub id: String,
35 pub subject: String,
36 pub description: String,
37 #[serde(skip_serializing_if = "Option::is_none")]
38 pub active_form: Option<String>,
39 #[serde(skip_serializing_if = "Option::is_none")]
40 pub owner: Option<String>,
41 pub status: TaskStatus,
42 #[serde(default)]
43 pub blocks: Vec<String>,
44 #[serde(default)]
45 pub blocked_by: Vec<String>,
46 #[serde(skip_serializing_if = "Option::is_none")]
47 pub metadata: Option<HashMap<String, serde_json::Value>>,
48}
49
50pub fn is_todo_v2_enabled() -> bool {
52 let env_enabled = std::env::var("AI_CODE_ENABLE_TASKS")
55 .map(|v| v == "1" || v == "true" || v == "yes")
56 .unwrap_or(false);
57
58 if env_enabled {
59 return true;
60 }
61
62 true
65}
66
67pub fn get_task_list_id() -> String {
69 std::env::var("AI_CODE_SESSION_ID").ok().unwrap_or_else(|| {
71 uuid::Uuid::new_v4().to_string()
73 })
74}
75
76fn get_tasks_dir(task_list_id: &str) -> PathBuf {
78 let config_dir = dirs::home_dir()
79 .map(|d| d.join(".ai").join("tasks"))
80 .unwrap_or_else(|| PathBuf::from("/tmp/.ai/tasks"));
81
82 config_dir.join(task_list_id)
83}
84
85static TASK_STORE: OnceLock<Mutex<TaskStore>> = OnceLock::new();
87
88struct TaskStore {
89 tasks: HashMap<String, Task>,
90 high_water_mark: u64,
91}
92
93impl TaskStore {
94 fn new() -> Self {
95 Self {
96 tasks: HashMap::new(),
97 high_water_mark: 0,
98 }
99 }
100}
101
102fn get_store() -> &'static Mutex<TaskStore> {
103 TASK_STORE.get_or_init(|| Mutex::new(TaskStore::new()))
104}
105
106pub fn reset_task_store() {
107 let mut store = get_store().lock().unwrap();
108 store.tasks.clear();
109 store.high_water_mark = 0;
110}
111
112fn next_task_id() -> String {
114 let mut store = get_store().lock().unwrap();
115 store.high_water_mark += 1;
116 store.high_water_mark.to_string()
117}
118
119pub async fn create_task(_task_list_id: &str, task: Task) -> Result<String, String> {
121 let id = next_task_id();
122 let mut new_task = task.clone();
123 new_task.id = id.clone();
124
125 let mut store = get_store().lock().unwrap();
126 store.tasks.insert(id.clone(), new_task);
127 Ok(id)
128}
129
130pub async fn get_task(_task_list_id: &str, task_id: &str) -> Result<Option<Task>, String> {
132 let store = get_store().lock().unwrap();
133 Ok(store.tasks.get(task_id).cloned())
134}
135
136pub async fn list_tasks(_task_list_id: &str) -> Result<Vec<Task>, String> {
138 let store = get_store().lock().unwrap();
139 Ok(store.tasks.values().cloned().collect())
140}
141
142pub fn get_unfinished_tasks() -> Vec<Task> {
144 let store = get_store().lock().unwrap();
145 store
146 .tasks
147 .values()
148 .filter(|t| t.status != TaskStatus::Completed)
149 .cloned()
150 .collect()
151}
152
153pub async fn update_task(
155 _task_list_id: &str,
156 task_id: &str,
157 updates: TaskUpdate,
158) -> Result<(), String> {
159 let mut store = get_store().lock().unwrap();
160 if let Some(task) = store.tasks.get_mut(task_id) {
161 if let Some(subject) = updates.subject {
162 task.subject = subject;
163 }
164 if let Some(description) = updates.description {
165 task.description = description;
166 }
167 if let Some(status) = updates.status {
168 task.status = status;
169 }
170 if let Some(owner) = updates.owner {
171 task.owner = Some(owner);
172 }
173 if let Some(active_form) = updates.active_form {
174 task.active_form = Some(active_form);
175 }
176 if let Some(blocks) = updates.blocks {
177 task.blocks = blocks;
178 }
179 if let Some(blocked_by) = updates.blocked_by {
180 task.blocked_by = blocked_by;
181 }
182 Ok(())
183 } else {
184 Err(format!("Task {} not found", task_id))
185 }
186}
187
188pub async fn delete_task(_task_list_id: &str, task_id: &str) -> Result<(), String> {
190 let mut store = get_store().lock().unwrap();
191 if store.tasks.remove(task_id).is_some() {
192 Ok(())
193 } else {
194 Err(format!("Task {} not found", task_id))
195 }
196}
197
198pub struct TaskUpdate {
200 pub subject: Option<String>,
201 pub description: Option<String>,
202 pub status: Option<TaskStatus>,
203 pub owner: Option<String>,
204 pub active_form: Option<String>,
205 pub blocks: Option<Vec<String>>,
206 pub blocked_by: Option<Vec<String>>,
207}
208
209#[cfg(test)]
210mod tests {
211 use super::*;
212 use crate::tests::common::clear_all_test_state;
213
214 #[test]
215 fn test_is_todo_v2_enabled() {
216 clear_all_test_state();
217 assert!(is_todo_v2_enabled());
218 }
219
220 #[test]
221 fn test_task_status_display() {
222 clear_all_test_state();
223 assert_eq!(TaskStatus::Pending.to_string(), "pending");
224 assert_eq!(TaskStatus::InProgress.to_string(), "in_progress");
225 assert_eq!(TaskStatus::Completed.to_string(), "completed");
226 }
227
228 #[tokio::test]
229 async fn test_create_and_get_task() {
230 clear_all_test_state();
231 reset_task_store();
232 let task_list_id = get_task_list_id();
233 let task = Task {
234 id: String::new(),
235 subject: "Test task".to_string(),
236 description: "Test description".to_string(),
237 active_form: None,
238 owner: None,
239 status: TaskStatus::Pending,
240 blocks: vec![],
241 blocked_by: vec![],
242 metadata: None,
243 };
244 let id = create_task(&task_list_id, task).await.unwrap();
245 assert_eq!(id, "1");
246
247 let retrieved = get_task(&task_list_id, &id).await.unwrap().unwrap();
248 assert_eq!(retrieved.subject, "Test task");
249 assert_eq!(retrieved.status, TaskStatus::Pending);
250 }
251
252 #[tokio::test]
253 async fn test_list_tasks() {
254 clear_all_test_state();
255 reset_task_store();
256 let task_list_id = get_task_list_id();
257 let task = Task {
259 id: String::new(),
260 subject: "Test task".to_string(),
261 description: "Test description".to_string(),
262 active_form: None,
263 owner: None,
264 status: TaskStatus::Pending,
265 blocks: vec![],
266 blocked_by: vec![],
267 metadata: None,
268 };
269 create_task(&task_list_id, task).await.unwrap();
270 let tasks = list_tasks(&task_list_id).await.unwrap();
271 assert!(!tasks.is_empty());
272 }
273
274 #[tokio::test]
275 async fn test_delete_task() {
276 clear_all_test_state();
277 reset_task_store();
278 let task_list_id = get_task_list_id();
279 let task = Task {
280 id: String::new(),
281 subject: "To delete".to_string(),
282 description: "Will be deleted".to_string(),
283 active_form: None,
284 owner: None,
285 status: TaskStatus::Pending,
286 blocks: vec![],
287 blocked_by: vec![],
288 metadata: None,
289 };
290 let id = create_task(&task_list_id, task).await.unwrap();
291 delete_task(&task_list_id, &id).await.unwrap();
292 let retrieved = get_task(&task_list_id, &id).await.unwrap();
293 assert!(retrieved.is_none());
294 }
295}