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            ..Default::default()
48        }
49    }
50
51    async fn execute(&self, params: Value) -> Result<String> {
52        let todos = params["todos"]
53            .as_array()
54            .ok_or_else(|| anyhow::anyhow!("missing 'todos' array"))?;
55
56        // Show spinner while updating todo list - RAII guard ensures cleanup on error
57        // let mut spinner = ToolSpinner::new(&format!("updating todos ({} items)", todos.len()));
58
59        let mut in_progress_count = 0;
60        let _completed_count = 0;
61        let _pending_count = 0;
62        let mut lines = Vec::with_capacity(todos.len() + 1);
63
64        for (i, todo) in todos.iter().enumerate() {
65            let content = todo["content"]
66                .as_str()
67                .ok_or_else(|| anyhow::anyhow!("todo {}: missing 'content'", i))?;
68            let active = todo["activeForm"]
69                .as_str()
70                .ok_or_else(|| anyhow::anyhow!("todo {}: missing 'activeForm'", i))?;
71            let status = todo["status"]
72                .as_str()
73                .ok_or_else(|| anyhow::anyhow!("todo {}: missing 'status'", i))?;
74
75            let (marker, display) = match status {
76                "pending" => ("[ ]", content),
77                "in_progress" => {
78                    in_progress_count += 1;
79                    ("[~]", active)
80                }
81                "completed" => ("[x]", content),
82                other => anyhow::bail!("todo {}: invalid status '{}'", i, other),
83            };
84
85            lines.push(format!("  {} {}", marker, display));
86        }
87
88        if in_progress_count > 1 {
89            // spinner.finish_error("multiple in_progress");
90            anyhow::bail!(
91                "at most one task may be 'in_progress' at a time (found {})",
92                in_progress_count
93            );
94        }
95
96        if lines.is_empty() {
97            // spinner.finish_success("cleared");
98            return Ok("Todo list cleared.".to_string());
99        }
100
101        let header = format!("Todos ({}):", lines.len());
102        let mut out =
103            String::with_capacity(header.len() + lines.iter().map(|l| l.len() + 1).sum::<usize>());
104        out.push_str(&header);
105        for l in &lines {
106            out.push('\n');
107            out.push_str(l);
108        }
109
110        // spinner.finish_success(&format!(
111        //     "{} pending, {} in_progress, {} done",
112        //     pending_count, in_progress_count, completed_count
113        // ));
114        Ok(out)
115    }
116
117    fn risk_level(&self) -> RiskLevel {
118        RiskLevel::Mutating
119    }
120}