Skip to main content

hematite/agent/
tasks.rs

1use serde::{Deserialize, Serialize};
2use std::path::PathBuf;
3
4#[derive(Debug, Clone, Serialize, Deserialize)]
5pub struct HematiteTask {
6    pub id: usize,
7    pub text: String,
8    pub done: bool,
9}
10
11fn store_path() -> PathBuf {
12    crate::tools::file_ops::hematite_dir().join("tasks.json")
13}
14
15pub fn load() -> Vec<HematiteTask> {
16    let path = store_path();
17    let Ok(raw) = std::fs::read_to_string(&path) else {
18        return Vec::new();
19    };
20    serde_json::from_str(&raw).unwrap_or_default()
21}
22
23fn save(tasks: &[HematiteTask]) {
24    let path = store_path();
25    if let Some(parent) = path.parent() {
26        let _ = std::fs::create_dir_all(parent);
27    }
28    if let Ok(json) = serde_json::to_string_pretty(tasks) {
29        let _ = std::fs::write(&path, json);
30    }
31}
32
33pub fn add(text: &str) -> Vec<HematiteTask> {
34    let mut tasks = load();
35    let next_id = tasks.iter().map(|t| t.id).max().unwrap_or(0) + 1;
36    tasks.push(HematiteTask {
37        id: next_id,
38        text: text.trim().to_string(),
39        done: false,
40    });
41    save(&tasks);
42    tasks
43}
44
45pub fn mark_done(n: usize) -> Result<Vec<HematiteTask>, String> {
46    let mut tasks = load();
47    let task = tasks
48        .iter_mut()
49        .find(|t| t.id == n)
50        .ok_or_else(|| format!("No task with id {}.", n))?;
51    task.done = true;
52    save(&tasks);
53    Ok(tasks)
54}
55
56pub fn remove(n: usize) -> Result<Vec<HematiteTask>, String> {
57    let mut tasks = load();
58    let before = tasks.len();
59    tasks.retain(|t| t.id != n);
60    if tasks.len() == before {
61        return Err(format!("No task with id {}.", n));
62    }
63    save(&tasks);
64    Ok(tasks)
65}
66
67pub fn clear() {
68    save(&[]);
69}
70
71/// Compact block injected into the system prompt every turn when tasks exist.
72pub fn render_prompt_block(tasks: &[HematiteTask]) -> Option<String> {
73    if tasks.is_empty() {
74        return None;
75    }
76    let mut out = String::from("## Active Task List\n");
77    for t in tasks {
78        let mark = if t.done { "[x]" } else { "[ ]" };
79        out.push_str(&format!("{} {}. {}\n", mark, t.id, t.text));
80    }
81    out.push_str(
82        "\nWhen you complete a task, let the user know and suggest running `/task done <N>`.",
83    );
84    Some(out)
85}
86
87/// Human-readable list for `/task list` output.
88pub fn render_list(tasks: &[HematiteTask]) -> String {
89    if tasks.is_empty() {
90        return "No tasks. Use `/task add <text>` to add one.".to_string();
91    }
92    let mut out = String::from("## Task List\n\n");
93    for t in tasks {
94        let mark = if t.done { "[x]" } else { "[ ]" };
95        out.push_str(&format!("{} **{}**. {}\n", mark, t.id, t.text));
96    }
97    out
98}