Skip to main content

matrixcode_core/tools/
todo_write.rs

1use anyhow::Result;
2use async_trait::async_trait;
3use serde_json::{Value, json};
4
5use super::{Tool, ToolDefinition};
6use crate::approval::RiskLevel;
7
8pub struct TodoWriteTool;
9
10#[async_trait]
11impl Tool for TodoWriteTool {
12    fn definition(&self) -> ToolDefinition {
13        ToolDefinition {
14            name: "todo_write".to_string(),
15            description: "维护结构化待办列表,用于规划和跟踪多步骤工作。\
16                 每次调用会替换整个列表。用于非 trivial 任务(3 步以上)以展示进度。\
17                 同时只保持一个任务 'in_progress',完成后立即标记为 'completed'。"
18                .to_string(),
19            parameters: json!({
20                "type": "object",
21                "properties": {
22                    "todos": {
23                        "type": "array",
24                        "description": "待办项的完整替换列表",
25                        "items": {
26                            "type": "object",
27                            "properties": {
28                                "content": {
29                                    "type": "string",
30                                    "description": "命令式描述(如 '运行测试')"
31                                },
32                                "activeForm": {
33                                    "type": "string",
34                                    "description": "进行式描述(如 '正在运行测试')"
35                                },
36                                "status": {
37                                    "type": "string",
38                                    "enum": ["pending", "in_progress", "completed"]
39                                }
40                            },
41                            "required": ["content", "activeForm", "status"]
42                        }
43                    }
44                },
45                "required": ["todos"]
46            }),
47        }
48    }
49
50    async fn execute(&self, params: Value) -> Result<String> {
51        let todos = params["todos"]
52            .as_array()
53            .ok_or_else(|| anyhow::anyhow!("missing 'todos' array"))?;
54
55        // Show spinner while updating todo list - RAII guard ensures cleanup on error
56        // let mut spinner = ToolSpinner::new(&format!("updating todos ({} items)", todos.len()));
57
58        let mut in_progress_count = 0;
59        let _completed_count = 0;
60        let _pending_count = 0;
61        let mut lines = Vec::with_capacity(todos.len() + 1);
62
63        for (i, todo) in todos.iter().enumerate() {
64            let content = todo["content"]
65                .as_str()
66                .ok_or_else(|| anyhow::anyhow!("todo {}: missing 'content'", i))?;
67            let active = todo["activeForm"]
68                .as_str()
69                .ok_or_else(|| anyhow::anyhow!("todo {}: missing 'activeForm'", i))?;
70            let status = todo["status"]
71                .as_str()
72                .ok_or_else(|| anyhow::anyhow!("todo {}: missing 'status'", i))?;
73
74            let (marker, display) = match status {
75                "pending" => ("[ ]", content),
76                "in_progress" => {
77                    in_progress_count += 1;
78                    ("[~]", active)
79                }
80                "completed" => ("[x]", content),
81                other => anyhow::bail!("todo {}: invalid status '{}'", i, other),
82            };
83
84            lines.push(format!("  {} {}", marker, display));
85        }
86
87        if in_progress_count > 1 {
88            // spinner.finish_error("multiple in_progress");
89            anyhow::bail!(
90                "at most one task may be 'in_progress' at a time (found {})",
91                in_progress_count
92            );
93        }
94
95        if lines.is_empty() {
96            // spinner.finish_success("cleared");
97            return Ok("Todo list cleared.".to_string());
98        }
99
100        let header = format!("Todos ({}):", lines.len());
101        let mut out =
102            String::with_capacity(header.len() + lines.iter().map(|l| l.len() + 1).sum::<usize>());
103        out.push_str(&header);
104        for l in &lines {
105            out.push('\n');
106            out.push_str(l);
107        }
108
109        // spinner.finish_success(&format!(
110        //     "{} pending, {} in_progress, {} done",
111        //     pending_count, in_progress_count, completed_count
112        // ));
113        Ok(out)
114    }
115
116    fn risk_level(&self) -> RiskLevel {
117        RiskLevel::Mutating
118    }
119}