Skip to main content

atm_core/
beads.rs

1//! Beads task lookup for preview pane integration.
2//!
3//! Scans `.beads/issues.jsonl` in a working directory for in-progress tasks.
4
5use std::path::Path;
6
7/// A beads task summary for display in the preview pane.
8#[derive(Debug, Clone)]
9pub struct BeadsTask {
10    /// Issue ID (e.g., "agent-tmux-manager-6n0")
11    pub id: String,
12    /// Issue title
13    pub title: String,
14    /// Issue description
15    pub description: Option<String>,
16}
17
18/// Finds in-progress beads tasks in the given working directory.
19///
20/// Scans `{working_dir}/.beads/issues.jsonl` for entries with `"status":"in_progress"`.
21/// Returns tasks sorted by most recently updated first.
22pub fn find_in_progress_tasks(working_dir: &str) -> Vec<BeadsTask> {
23    let jsonl_path = Path::new(working_dir).join(".beads/issues.jsonl");
24    let content = match std::fs::read_to_string(&jsonl_path) {
25        Ok(c) => c,
26        Err(_) => return Vec::new(),
27    };
28
29    let mut tasks: Vec<(String, BeadsTask)> = Vec::new(); // (updated_at, task) for sorting
30
31    for line in content.lines() {
32        // Quick pre-filter before parsing JSON
33        if !line.contains("\"in_progress\"") {
34            continue;
35        }
36        if let Ok(val) = serde_json::from_str::<serde_json::Value>(line) {
37            let status = val
38                .get("status")
39                .and_then(|v| v.as_str())
40                .unwrap_or_default();
41            if status != "in_progress" {
42                continue;
43            }
44            let id = val
45                .get("id")
46                .and_then(|v| v.as_str())
47                .unwrap_or_default()
48                .to_string();
49            let title = val
50                .get("title")
51                .and_then(|v| v.as_str())
52                .unwrap_or_default()
53                .to_string();
54            let updated_at = val
55                .get("updated_at")
56                .and_then(|v| v.as_str())
57                .unwrap_or_default()
58                .to_string();
59
60            let description = val
61                .get("description")
62                .and_then(|v| v.as_str())
63                .filter(|s| !s.is_empty())
64                .map(|s| s.to_string());
65
66            if !id.is_empty() && !title.is_empty() {
67                tasks.push((
68                    updated_at,
69                    BeadsTask {
70                        id,
71                        title,
72                        description,
73                    },
74                ));
75            }
76        }
77    }
78
79    // Most recently updated first
80    tasks.sort_by(|a, b| b.0.cmp(&a.0));
81    tasks.into_iter().map(|(_, task)| task).collect()
82}
83
84#[cfg(test)]
85mod tests {
86    use super::*;
87    use std::fs;
88
89    #[test]
90    fn test_find_in_progress_tasks_empty() {
91        let dir = tempfile::tempdir().unwrap();
92        let result = find_in_progress_tasks(dir.path().to_str().unwrap());
93        assert!(result.is_empty());
94    }
95
96    #[test]
97    fn test_find_in_progress_tasks() {
98        let dir = tempfile::tempdir().unwrap();
99        let beads_dir = dir.path().join(".beads");
100        fs::create_dir_all(&beads_dir).unwrap();
101        fs::write(
102            beads_dir.join("issues.jsonl"),
103            r#"{"id":"test-1","title":"Open task","status":"open","updated_at":"2026-01-01T00:00:00Z"}
104{"id":"test-2","title":"Active task","status":"in_progress","updated_at":"2026-01-02T00:00:00Z"}
105{"id":"test-3","title":"Done task","status":"closed","updated_at":"2026-01-03T00:00:00Z"}
106{"id":"test-4","title":"Another active","status":"in_progress","updated_at":"2026-01-04T00:00:00Z"}
107"#,
108        )
109        .unwrap();
110
111        let result = find_in_progress_tasks(dir.path().to_str().unwrap());
112        assert_eq!(result.len(), 2);
113        assert_eq!(result[0].title, "Another active"); // most recent first
114        assert_eq!(result[1].title, "Active task");
115    }
116}