use super::entity::AgentTask;
use crate::command::chat::permission::JcliConfig;
use crate::util::safe_lock;
use serde_json::Value;
use std::fs;
use std::path::PathBuf;
use std::sync::Mutex;
#[derive(Debug)]
pub struct TaskManager {
tasks_dir: PathBuf,
write_lock: Mutex<()>,
}
impl Default for TaskManager {
fn default() -> Self {
Self::new()
}
}
impl TaskManager {
pub fn new() -> Self {
let config_dir = JcliConfig::find_config_dir().or_else(JcliConfig::ensure_config_dir);
let tasks_dir = match config_dir {
Some(dir) => dir.join("tasks"),
None => {
let data_dir = crate::config::YamlConfig::data_dir();
data_dir.join("agent").join("data").join("tasks")
}
};
let _ = fs::create_dir_all(&tasks_dir);
Self {
tasks_dir,
write_lock: Mutex::new(()),
}
}
pub fn new_with_session(session_id: &str) -> Self {
let sessions_dir = crate::command::chat::storage::sessions_dir();
let session_dir = sessions_dir.join(session_id);
let tasks_dir = session_dir.join("tasks_session");
let _ = fs::create_dir_all(&tasks_dir);
let mgr = Self {
tasks_dir,
write_lock: Mutex::new(()),
};
if let Some(tasks) = crate::command::chat::storage::load_tasks_state(session_id) {
mgr.replace_all(tasks);
}
mgr
}
fn next_id(&self) -> u64 {
let mut max_id: u64 = 0;
if let Ok(entries) = fs::read_dir(&self.tasks_dir) {
for entry in entries.flatten() {
let name = entry.file_name();
let name = name.to_string_lossy();
if let Some(rest) = name.strip_prefix("task_")
&& let Some(id_str) = rest.strip_suffix(".json")
&& let Ok(id) = id_str.parse::<u64>()
{
max_id = max_id.max(id);
}
}
}
max_id + 1
}
fn task_path(&self, id: u64) -> PathBuf {
self.tasks_dir.join(format!("task_{}.json", id))
}
pub fn create_task(
&self,
title: &str,
description: &str,
blocked_by: Vec<u64>,
task_doc_paths: Vec<String>,
) -> Result<AgentTask, String> {
let _lock = safe_lock(&self.write_lock, "TaskManager::create_task");
let task_id = self.next_id();
let task = AgentTask {
task_id,
title: title.to_string(),
description: description.to_string(),
status: "pending".to_string(),
blocked_by,
owner: String::new(),
task_doc_paths,
};
self.save_task(&task)?;
Ok(task)
}
pub fn list_ready_tasks(&self) -> Vec<AgentTask> {
self.list_tasks()
.into_iter()
.filter(|t| t.status == "pending" && t.blocked_by.is_empty())
.collect()
}
pub fn get_task(&self, id: u64) -> Result<AgentTask, String> {
let path = self.task_path(id);
if !path.exists() {
return Err(format!("Task {} does not exist", id));
}
let data = fs::read_to_string(&path).map_err(|e| format!("Failed to read task: {}", e))?;
serde_json::from_str(&data).map_err(|e| format!("Failed to parse task: {}", e))
}
pub fn list_tasks(&self) -> Vec<AgentTask> {
let mut tasks = Vec::new();
if let Ok(entries) = fs::read_dir(&self.tasks_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().is_some_and(|e| e == "json")
&& let Ok(data) = fs::read_to_string(&path)
&& let Ok(task) = serde_json::from_str::<AgentTask>(&data)
{
tasks.push(task);
}
}
}
tasks.sort_by_key(|t| t.task_id);
tasks
}
pub fn update_task(&self, id: u64, updates: &Value) -> Result<AgentTask, String> {
let _lock = safe_lock(&self.write_lock, "TaskManager::update_task");
let mut task = self.get_task(id)?;
if let Some(status) = updates.get("status").and_then(|s| s.as_str()) {
match status {
"deleted" => {
let path = self.task_path(id);
let _ = fs::remove_file(&path);
self.clean_references(id);
task.status = "deleted".to_string();
return Ok(task);
}
"pending" | "in_progress" | "completed" => {
task.status = status.to_string();
if status == "completed" {
self.clean_references(id);
}
}
_ => return Err(format!("Invalid status: {}", status)),
}
}
if let Some(title) = updates.get("title").and_then(|s| s.as_str()) {
task.title = title.to_string();
}
if let Some(description) = updates.get("description").and_then(|s| s.as_str()) {
task.description = description.to_string();
}
if let Some(owner) = updates.get("owner").and_then(|s| s.as_str()) {
task.owner = owner.to_string();
}
if let Some(add_blocked_by) = updates.get("addBlockedBy").and_then(|v| v.as_array()) {
for id_val in add_blocked_by {
if let Some(dep_id) = id_val.as_u64()
&& !task.blocked_by.contains(&dep_id)
{
task.blocked_by.push(dep_id);
}
}
}
self.save_task(&task)?;
Ok(task)
}
fn save_task(&self, task: &AgentTask) -> Result<(), String> {
let path = self.task_path(task.task_id);
let data = serde_json::to_string_pretty(task)
.map_err(|e| format!("Failed to serialize task: {}", e))?;
fs::write(&path, data).map_err(|e| format!("Failed to write task: {}", e))
}
fn clean_references(&self, completed_id: u64) {
let tasks = self.list_tasks();
for mut task in tasks {
if task.blocked_by.contains(&completed_id) {
task.blocked_by.retain(|&id| id != completed_id);
let _ = self.save_task(&task);
}
}
}
pub fn replace_all(&self, new_tasks: Vec<AgentTask>) {
let _lock = safe_lock(&self.write_lock, "TaskManager::replace_all");
if let Ok(entries) = fs::read_dir(&self.tasks_dir) {
for entry in entries.flatten() {
let path = entry.path();
if path.extension().is_some_and(|e| e == "json") {
let _ = fs::remove_file(&path);
}
}
}
for task in new_tasks {
let _ = self.save_task(&task);
}
}
}