use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use super::common::TaskSummary;
use crate::tasks::task_store::TaskStore;
use crate::tasks::types::{TaskId, TaskStatus};
use mcp_utils::display_meta::{ToolDisplayMeta, ToolResultMeta};
#[derive(Debug, Clone, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct TaskListInput {
#[serde(default)]
pub assignee: Option<String>,
#[serde(default)]
pub status: Option<TaskStatusFilter>,
#[serde(default, alias = "tree_id")]
pub tree_id: Option<String>,
#[serde(default, alias = "ready_only")]
pub ready_only: Option<bool>,
}
#[derive(Debug, Clone, Copy, Deserialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub enum TaskStatusFilter {
Pending,
#[serde(alias = "in_progress")]
InProgress,
Completed,
Blocked,
}
impl From<TaskStatusFilter> for TaskStatus {
fn from(status: TaskStatusFilter) -> Self {
match status {
TaskStatusFilter::Pending => TaskStatus::Pending,
TaskStatusFilter::InProgress => TaskStatus::InProgress,
TaskStatusFilter::Completed => TaskStatus::Completed,
TaskStatusFilter::Blocked => TaskStatus::Blocked,
}
}
}
#[derive(Debug, Clone, Serialize, JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct TaskListOutput {
pub status: String,
pub tasks: Vec<TaskSummary>,
pub count: usize,
pub message: String,
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
#[schemars(skip)]
pub meta: Option<ToolResultMeta>,
}
pub fn execute_task_list(input: &TaskListInput, store: &TaskStore) -> TaskListOutput {
let tasks: Vec<TaskSummary> = if input.ready_only.unwrap_or(false) {
store
.get_ready()
.into_iter()
.map(TaskSummary::from)
.collect()
} else if let Some(tree_id) = &input.tree_id {
let tree_id = TaskId::from(tree_id.as_str());
store
.get_tree(&tree_id)
.map(|tasks| tasks.into_iter().map(TaskSummary::from).collect())
.unwrap_or_default()
} else if let Some(assignee) = &input.assignee {
store
.list_by_assignee(assignee)
.into_iter()
.map(TaskSummary::from)
.collect()
} else if let Some(status) = input.status {
store
.list_by_status(status.into())
.into_iter()
.map(TaskSummary::from)
.collect()
} else {
store
.list_trees()
.iter()
.flat_map(|root_id| {
store
.get_tree(root_id)
.unwrap_or_default()
.into_iter()
.map(TaskSummary::from)
})
.collect()
};
let count = tasks.len();
let filter_desc = build_filter_description(input);
let message = if count == 0 {
format!("No tasks found{filter_desc}")
} else if count == 1 {
format!("Found 1 task{filter_desc}")
} else {
format!("Found {count} tasks{filter_desc}")
};
let done = tasks.iter().filter(|t| t.status == "completed").count();
let display_meta = ToolDisplayMeta::new("Todo", format!("{done}/{count} tasks"));
TaskListOutput {
status: "success".to_string(),
tasks,
count,
message,
meta: Some(display_meta.into()),
}
}
fn build_filter_description(input: &TaskListInput) -> String {
let mut parts = Vec::new();
if input.ready_only.unwrap_or(false) {
parts.push("ready to start".to_string());
}
if let Some(tree_id) = &input.tree_id {
parts.push(format!("in tree {tree_id}"));
}
if let Some(assignee) = &input.assignee {
parts.push(format!("assigned to {assignee}"));
}
if let Some(status) = input.status {
let status_str = match status {
TaskStatusFilter::Pending => "pending",
TaskStatusFilter::InProgress => "inProgress",
TaskStatusFilter::Completed => "completed",
TaskStatusFilter::Blocked => "blocked",
};
parts.push(format!("with status {status_str}"));
}
if parts.is_empty() {
String::new()
} else {
format!(" ({})", parts.join(", "))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tasks::TaskUpdate;
use tempfile::TempDir;
fn setup() -> (TempDir, TaskStore) {
let temp_dir = TempDir::new().unwrap();
let root = temp_dir.path().join(".aether-tasks");
let mut store = TaskStore::new(root);
store.init().unwrap();
(temp_dir, store)
}
#[test]
fn test_list_all_tasks() {
let (_temp, mut store) = setup();
let root = store.create_tree("Root", None).unwrap();
store.add_subtask(&root.id, "Subtask 1").unwrap();
store.add_subtask(&root.id, "Subtask 2").unwrap();
let output = execute_task_list(
&TaskListInput {
assignee: None,
status: None,
tree_id: None,
ready_only: None,
},
&store,
);
assert_eq!(output.count, 3);
assert!(output.message.contains("Found 3 tasks"));
}
#[test]
fn test_list_by_tree() {
let (_temp, mut store) = setup();
let root1 = store.create_tree("Root 1", None).unwrap();
store.add_subtask(&root1.id, "Subtask 1.1").unwrap();
let root2 = store.create_tree("Root 2", None).unwrap();
store.add_subtask(&root2.id, "Subtask 2.1").unwrap();
store.add_subtask(&root2.id, "Subtask 2.2").unwrap();
let output = execute_task_list(
&TaskListInput {
assignee: None,
status: None,
tree_id: Some(root2.id.to_string()),
ready_only: None,
},
&store,
);
assert_eq!(output.count, 3);
assert!(output.message.contains("in tree"));
}
#[test]
fn test_list_by_assignee() {
let (_temp, mut store) = setup();
let root = store.create_tree("Root", None).unwrap();
let sub1 = store.add_subtask(&root.id, "Task 1").unwrap();
let sub2 = store.add_subtask(&root.id, "Task 2").unwrap();
store
.update(
&sub1.id,
TaskUpdate {
assignee: Some("worker-1".to_string()),
..Default::default()
},
)
.unwrap();
store
.update(
&sub2.id,
TaskUpdate {
assignee: Some("worker-2".to_string()),
..Default::default()
},
)
.unwrap();
let output = execute_task_list(
&TaskListInput {
assignee: Some("worker-1".to_string()),
status: None,
tree_id: None,
ready_only: None,
},
&store,
);
assert_eq!(output.count, 1);
assert_eq!(output.tasks[0].assignee, Some("worker-1".to_string()));
}
#[test]
fn test_list_by_status() {
let (_temp, mut store) = setup();
let root = store.create_tree("Root", None).unwrap();
let sub = store.add_subtask(&root.id, "Subtask").unwrap();
store
.update(
&sub.id,
TaskUpdate {
status: Some(TaskStatus::InProgress),
..Default::default()
},
)
.unwrap();
let output = execute_task_list(
&TaskListInput {
assignee: None,
status: Some(TaskStatusFilter::InProgress),
tree_id: None,
ready_only: None,
},
&store,
);
assert_eq!(output.count, 1);
assert_eq!(output.tasks[0].status, "inProgress");
}
#[test]
fn test_list_ready_only() {
let (_temp, mut store) = setup();
let root = store.create_tree("Root", None).unwrap();
let sub1 = store.add_subtask(&root.id, "Subtask 1").unwrap();
let sub2 = store.add_subtask(&root.id, "Subtask 2").unwrap();
store
.update(
&sub2.id,
TaskUpdate {
deps: Some(vec![sub1.id.clone()]),
..Default::default()
},
)
.unwrap();
let output = execute_task_list(
&TaskListInput {
assignee: None,
status: None,
tree_id: None,
ready_only: Some(true),
},
&store,
);
assert_eq!(output.count, 2);
store
.update(
&sub1.id,
TaskUpdate {
status: Some(TaskStatus::Completed),
summary: Some("done".to_string()),
..Default::default()
},
)
.unwrap();
let output = execute_task_list(
&TaskListInput {
assignee: None,
status: None,
tree_id: None,
ready_only: Some(true),
},
&store,
);
assert_eq!(output.count, 2);
let ready_ids: Vec<_> = output.tasks.iter().map(|t| t.id.as_str()).collect();
assert!(ready_ids.contains(&sub2.id.as_str()));
}
#[test]
fn test_list_empty() {
let (_temp, store) = setup();
let output = execute_task_list(
&TaskListInput {
assignee: None,
status: None,
tree_id: None,
ready_only: None,
},
&store,
);
assert_eq!(output.count, 0);
assert!(output.message.contains("No tasks found"));
}
#[test]
fn test_list_nonexistent_tree() {
let (_temp, store) = setup();
let output = execute_task_list(
&TaskListInput {
assignee: None,
status: None,
tree_id: Some("at-nonexistent".to_string()),
ready_only: None,
},
&store,
);
assert_eq!(output.count, 0);
}
}