#[derive(Debug, Clone)]
pub(crate) struct TaskRow {
pub(crate) id: String,
pub(crate) subject: String,
pub(crate) status: String,
pub(crate) blocked_by: Vec<String>,
}
#[derive(Debug, Clone, Default)]
pub(crate) struct TasksReport {
pub(crate) open: Vec<TaskRow>,
pub(crate) completed: usize,
pub(crate) found: bool,
}
pub(crate) fn tasks_report(owner_id: &str) -> TasksReport {
let mut report = TasksReport::default();
let Ok(home) = crate::path::claude_home() else {
return report;
};
let tasks_root = home.join("tasks");
let mut dirs = vec![tasks_root.join(owner_id)];
if let Some(prefix) = owner_id.get(..8) {
dirs.push(tasks_root.join(format!("session-{prefix}")));
}
for dir in dirs {
let Ok(entries) = std::fs::read_dir(&dir) else {
continue;
};
report.found = true;
for entry in entries.flatten() {
let path = entry.path();
if path.extension().and_then(|e| e.to_str()) != Some("json") {
continue;
}
let Ok(raw) = std::fs::read_to_string(&path) else {
continue;
};
let Ok(v) = serde_json::from_str::<serde_json::Value>(&raw) else {
continue;
};
let status = v
.get("status")
.and_then(serde_json::Value::as_str)
.unwrap_or("(no status)")
.to_string();
if status == "completed" {
report.completed += 1;
continue;
}
report.open.push(TaskRow {
id: json_id(v.get("id")),
subject: v
.get("subject")
.and_then(serde_json::Value::as_str)
.unwrap_or("(no subject)")
.to_string(),
status,
blocked_by: v
.get("blockedBy")
.and_then(serde_json::Value::as_array)
.map(|a| a.iter().map(|x| json_id(Some(x))).collect())
.unwrap_or_default(),
});
}
}
report.open.sort_by(|a, b| {
let rank = |t: &TaskRow| usize::from(t.status != "in_progress");
(rank(a), numeric_id(&a.id)).cmp(&(rank(b), numeric_id(&b.id)))
});
report
}
fn json_id(v: Option<&serde_json::Value>) -> String {
match v {
Some(serde_json::Value::String(s)) => s.clone(),
Some(serde_json::Value::Number(n)) => n.to_string(),
_ => "?".to_string(),
}
}
fn numeric_id(id: &str) -> u64 {
id.parse().unwrap_or(u64::MAX)
}