use std::sync::Arc;
use anyhow::Result;
use async_trait::async_trait;
use serde::Deserialize;
use serde_json::json;
use tokio::sync::Mutex;
use super::{ApprovalRequirement, Tool, ToolContext, ToolDef, ToolResult};
#[derive(Debug, Clone)]
struct TodoItem {
id: usize,
content: String,
status: String,
}
pub struct TodoTool {
items: Arc<Mutex<Vec<TodoItem>>>,
next_id: Arc<Mutex<usize>>,
}
impl TodoTool {
pub fn new() -> Self {
Self {
items: Arc::new(Mutex::new(Vec::new())),
next_id: Arc::new(Mutex::new(1)),
}
}
async fn format_list(items: &Arc<Mutex<Vec<TodoItem>>>) -> String {
let items = items.lock().await;
if items.is_empty() {
return "No tasks.".to_string();
}
let mut out = String::new();
for item in items.iter() {
let icon = match item.status.as_str() {
"completed" => "[x]",
"in_progress" => "[>]",
_ => "[ ]",
};
out.push_str(&format!("{} {}. {}\n", icon, item.id, item.content));
}
out
}
}
#[derive(Deserialize)]
struct TodoArgs {
action: String,
#[serde(default)]
content: Option<String>,
#[serde(default)]
id: Option<usize>,
#[serde(default)]
status: Option<String>,
}
#[async_trait]
impl Tool for TodoTool {
fn definition(&self) -> ToolDef {
ToolDef {
name: "todo",
description: "Manage a task list to track progress on multi-step work. Use 'add' to create tasks, 'update' to change status, and 'list' to show all tasks.".to_string(),
parameters: json!({
"type": "object",
"properties": {
"action": {
"type": "string",
"enum": ["add", "update", "list"],
"description": "Action: 'add' a new task, 'update' a task's status, or 'list' all tasks"
},
"content": {
"type": "string",
"description": "Task description (required for 'add')"
},
"id": {
"type": "integer",
"description": "Task ID (required for 'update')"
},
"status": {
"type": "string",
"enum": ["pending", "in_progress", "completed"],
"description": "New status (required for 'update')"
}
},
"required": ["action"]
}),
}
}
fn approval(&self, _args: &str) -> ApprovalRequirement {
ApprovalRequirement::AutoApprove
}
async fn execute(&self, args: &str, _ctx: &ToolContext) -> Result<ToolResult> {
let parsed: TodoArgs = serde_json::from_str(args)?;
match parsed.action.as_str() {
"add" => {
let content = parsed.content.unwrap_or_else(|| "Untitled task".to_string());
let mut id_guard = self.next_id.lock().await;
let id = *id_guard;
*id_guard += 1;
drop(id_guard);
let item = TodoItem {
id,
content: content.clone(),
status: "pending".to_string(),
};
self.items.lock().await.push(item);
Ok(ToolResult {
call_id: String::new(),
output: format!("Added task #{}: {}", id, content),
success: true,
})
}
"update" => {
let id = parsed.id.ok_or_else(|| anyhow::anyhow!("'id' is required for update"))?;
let status = parsed.status.unwrap_or_else(|| "in_progress".to_string());
let mut items = self.items.lock().await;
if let Some(item) = items.iter_mut().find(|i| i.id == id) {
item.status = status.clone();
Ok(ToolResult {
call_id: String::new(),
output: format!("Task #{} updated to '{}'", id, status),
success: true,
})
} else {
Ok(ToolResult {
call_id: String::new(),
output: format!("Task #{} not found", id),
success: false,
})
}
}
"list" => {
let list = Self::format_list(&self.items).await;
Ok(ToolResult {
call_id: String::new(),
output: list,
success: true,
})
}
other => Ok(ToolResult {
call_id: String::new(),
output: format!("Unknown action: {}. Use 'add', 'update', or 'list'.", other),
success: false,
}),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn add_and_list_tasks() {
let tool = TodoTool::new();
let ctx = ToolContext::new(std::path::PathBuf::from("/tmp"));
let r = tool.execute(r#"{"action":"add","content":"Write tests"}"#, &ctx).await.unwrap();
assert!(r.success);
assert!(r.output.contains("#1"));
let r = tool.execute(r#"{"action":"add","content":"Fix bug"}"#, &ctx).await.unwrap();
assert!(r.output.contains("#2"));
let r = tool.execute(r#"{"action":"list"}"#, &ctx).await.unwrap();
assert!(r.output.contains("Write tests"));
assert!(r.output.contains("Fix bug"));
assert!(r.output.contains("[ ]"));
}
#[tokio::test]
async fn update_task_status() {
let tool = TodoTool::new();
let ctx = ToolContext::new(std::path::PathBuf::from("/tmp"));
tool.execute(r#"{"action":"add","content":"Task 1"}"#, &ctx).await.unwrap();
let r = tool.execute(r#"{"action":"update","id":1,"status":"completed"}"#, &ctx).await.unwrap();
assert!(r.success);
let r = tool.execute(r#"{"action":"list"}"#, &ctx).await.unwrap();
assert!(r.output.contains("[x]"));
}
#[tokio::test]
async fn update_nonexistent_task_fails() {
let tool = TodoTool::new();
let ctx = ToolContext::new(std::path::PathBuf::from("/tmp"));
let r = tool.execute(r#"{"action":"update","id":99,"status":"completed"}"#, &ctx).await.unwrap();
assert!(!r.success);
assert!(r.output.contains("not found"));
}
#[tokio::test]
async fn list_empty_shows_no_tasks() {
let tool = TodoTool::new();
let ctx = ToolContext::new(std::path::PathBuf::from("/tmp"));
let r = tool.execute(r#"{"action":"list"}"#, &ctx).await.unwrap();
assert!(r.output.contains("No tasks"));
}
}