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                "Maintain a structured todo list to plan and track multi-step work. \
17                 Each call REPLACES the full list. Use for non-trivial tasks (3+ \
18                 steps) to show progress. Keep exactly one task 'in_progress' at \
19                 a time and mark tasks 'completed' as soon as they finish."
20                    .to_string(),
21            parameters: json!({
22                "type": "object",
23                "properties": {
24                    "todos": {
25                        "type": "array",
26                        "description": "Full replacement list of todo items",
27                        "items": {
28                            "type": "object",
29                            "properties": {
30                                "content": {
31                                    "type": "string",
32                                    "description": "Imperative form (e.g. 'Run tests')"
33                                },
34                                "activeForm": {
35                                    "type": "string",
36                                    "description": "Present-continuous form (e.g. 'Running tests')"
37                                },
38                                "status": {
39                                    "type": "string",
40                                    "enum": ["pending", "in_progress", "completed"]
41                                }
42                            },
43                            "required": ["content", "activeForm", "status"]
44                        }
45                    }
46                },
47                "required": ["todos"]
48            }),
49        }
50    }
51
52    async fn execute(&self, params: Value) -> Result<String> {
53        let todos = params["todos"]
54            .as_array()
55            .ok_or_else(|| anyhow::anyhow!("missing 'todos' array"))?;
56
57        // Show spinner while updating todo list - RAII guard ensures cleanup on error
58        // let mut spinner = ToolSpinner::new(&format!("updating todos ({} items)", todos.len()));
59
60        let mut in_progress_count = 0;
61        let _completed_count = 0;
62        let _pending_count = 0;
63        let mut lines = Vec::with_capacity(todos.len() + 1);
64
65        for (i, todo) in todos.iter().enumerate() {
66            let content = todo["content"]
67                .as_str()
68                .ok_or_else(|| anyhow::anyhow!("todo {}: missing 'content'", i))?;
69            let active = todo["activeForm"]
70                .as_str()
71                .ok_or_else(|| anyhow::anyhow!("todo {}: missing 'activeForm'", i))?;
72            let status = todo["status"]
73                .as_str()
74                .ok_or_else(|| anyhow::anyhow!("todo {}: missing 'status'", i))?;
75
76            let (marker, display) = match status {
77                "pending" => ("[ ]", content),
78                "in_progress" => {
79                    in_progress_count += 1;
80                    ("[~]", active)
81                }
82                "completed" => ("[x]", content),
83                other => anyhow::bail!("todo {}: invalid status '{}'", i, other),
84            };
85
86            lines.push(format!("  {} {}", marker, display));
87        }
88
89        if in_progress_count > 1 {
90            // spinner.finish_error("multiple in_progress");
91            anyhow::bail!(
92                "at most one task may be 'in_progress' at a time (found {})",
93                in_progress_count
94            );
95        }
96
97        if lines.is_empty() {
98            // spinner.finish_success("cleared");
99            return Ok("Todo list cleared.".to_string());
100        }
101
102        let header = format!("Todos ({}):", lines.len());
103        let mut out = 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}