Skip to main content

hematite/agent/
tasks.rs

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