Skip to main content

track/use_cases/
get_task_info.rs

1use crate::db::Database;
2use crate::models::{Link, Scrap, Task, TaskRepo, Todo, VcsMode, Worktree};
3use crate::services::agent_context::build_agent_extensions;
4use crate::services::{
5    LinkService, RepoService, ScrapService, TaskService, TodoService, WorktreeService,
6};
7use crate::utils::{Result, TrackError};
8
9/// Aggregated task detail for CLI status and agent tooling.
10#[derive(Debug, Clone)]
11pub struct TaskInfoSnapshot {
12    pub task: Task,
13    pub todos: Vec<Todo>,
14    pub links: Vec<Link>,
15    pub scraps: Vec<Scrap>,
16    pub worktrees: Vec<Worktree>,
17    pub repos: Vec<TaskRepo>,
18    pub vcs_mode: VcsMode,
19}
20
21/// Loads task detail and builds CLI/JSON status views.
22pub struct GetTaskInfoUseCase<'a> {
23    db: &'a Database,
24}
25
26impl<'a> GetTaskInfoUseCase<'a> {
27    pub fn new(db: &'a Database) -> Self {
28        Self { db }
29    }
30
31    pub fn load(&self, task_id: i64) -> Result<TaskInfoSnapshot> {
32        let task_service = TaskService::new(self.db);
33        let task = task_service.get_task(task_id)?;
34
35        let todo_service = TodoService::new(self.db);
36        let todos = todo_service.list_todos(task_id)?;
37
38        let link_service = LinkService::new(self.db);
39        let links = link_service.list_links(task_id)?;
40
41        let scrap_service = ScrapService::new(self.db);
42        let scraps = scrap_service.list_scraps(task_id)?;
43
44        let worktree_service = WorktreeService::new(self.db);
45        let worktrees = worktree_service.list_worktrees(task_id)?;
46
47        let repo_service = RepoService::new(self.db);
48        let repos = repo_service.list_repos(task_id)?;
49        let vcs_mode = self.db.get_vcs_mode()?;
50
51        Ok(TaskInfoSnapshot {
52            task,
53            todos,
54            links,
55            scraps,
56            worktrees,
57            repos,
58            vcs_mode,
59        })
60    }
61
62    pub fn base_bookmark(snapshot: &TaskInfoSnapshot) -> String {
63        if let Some(base_wt) = snapshot.worktrees.iter().find(|wt| wt.is_base) {
64            return base_wt.branch.clone();
65        }
66
67        if let Some(ticket_id) = &snapshot.task.ticket_id {
68            format!("task/{ticket_id}")
69        } else {
70            format!("task/task-{}", snapshot.task.id)
71        }
72    }
73
74    pub fn todo_worktree_branch(&self, snapshot: &TaskInfoSnapshot, todo: &Todo) -> Option<String> {
75        if let Some(worktree) = snapshot
76            .worktrees
77            .iter()
78            .find(|wt| wt.todo_id == Some(todo.id))
79        {
80            return Some(worktree.branch.clone());
81        }
82
83        if !todo.worktree_requested {
84            return None;
85        }
86
87        WorktreeService::new(self.db)
88            .get_todo_branch_name(
89                snapshot.task.id,
90                snapshot.task.ticket_id.as_deref(),
91                todo.task_index,
92            )
93            .ok()
94    }
95
96    pub fn orphan_worktrees(snapshot: &TaskInfoSnapshot) -> Vec<&Worktree> {
97        snapshot
98            .worktrees
99            .iter()
100            .filter(|wt| wt.todo_id.is_none())
101            .collect()
102    }
103
104    pub fn to_cli_json(&self, snapshot: &TaskInfoSnapshot) -> Result<serde_json::Value> {
105        let worktree_service = WorktreeService::new(self.db);
106
107        let mut todos_json = Vec::with_capacity(snapshot.todos.len());
108        for todo in &snapshot.todos {
109            let mut todo_val = serde_json::to_value(todo)
110                .map_err(|e| TrackError::SerializationFailed(e.to_string()))?;
111            if let Some(obj) = todo_val.as_object_mut() {
112                let worktree_branch = self.todo_worktree_branch(snapshot, todo);
113                obj.insert(
114                    "worktree_branch".to_string(),
115                    serde_json::to_value(worktree_branch)
116                        .map_err(|e| TrackError::SerializationFailed(e.to_string()))?,
117                );
118            }
119            todos_json.push(todo_val);
120        }
121
122        let mut worktrees_json = Vec::with_capacity(snapshot.worktrees.len());
123        for worktree in &snapshot.worktrees {
124            let mut wt_val = serde_json::to_value(worktree)
125                .map_err(|e| TrackError::SerializationFailed(e.to_string()))?;
126            if let Some(obj) = wt_val.as_object_mut() {
127                obj.remove("id");
128                obj.remove("is_base");
129                obj.remove("task_id");
130
131                let task_scoped_id = worktree.todo_id.and_then(|id| {
132                    snapshot
133                        .todos
134                        .iter()
135                        .find(|todo| todo.id == id)
136                        .map(|todo| todo.task_index)
137                });
138                obj.insert(
139                    "todo_id".to_string(),
140                    serde_json::to_value(task_scoped_id)
141                        .map_err(|e| TrackError::SerializationFailed(e.to_string()))?,
142                );
143            }
144            worktrees_json.push(wt_val);
145        }
146
147        for todo in &snapshot.todos {
148            if todo.worktree_requested
149                && !snapshot
150                    .worktrees
151                    .iter()
152                    .any(|wt| wt.todo_id == Some(todo.id))
153            {
154                let branch_name = self.todo_worktree_branch(snapshot, todo);
155                worktrees_json.push(serde_json::json!({
156                    "todo_id": todo.task_index,
157                    "branch": branch_name,
158                    "status": "requested",
159                    "path": null,
160                    "created_at": null,
161                    "base_repo": null
162                }));
163            }
164        }
165
166        let mut output = serde_json::json!({
167            "task": snapshot.task,
168            "todos": todos_json,
169            "links": snapshot.links,
170            "scraps": snapshot.scraps,
171            "worktrees": worktrees_json,
172            "repos": snapshot.repos,
173        });
174
175        let agent = build_agent_extensions(
176            snapshot.vcs_mode,
177            &snapshot.task,
178            &snapshot.todos,
179            &snapshot.worktrees,
180            &snapshot.repos,
181            &worktree_service,
182        );
183        let agent_val = serde_json::to_value(&agent)
184            .map_err(|e| TrackError::SerializationFailed(e.to_string()))?;
185        if let Some(obj) = output.as_object_mut() {
186            if let Some(agent_obj) = agent_val.as_object() {
187                for (key, value) in agent_obj {
188                    obj.insert(key.clone(), value.clone());
189                }
190            }
191        }
192
193        Ok(output)
194    }
195}
196
197#[cfg(test)]
198mod tests {
199    use super::*;
200    use crate::services::TaskService;
201
202    #[test]
203    fn base_bookmark_uses_ticket_when_no_base_worktree() {
204        let db = Database::new_in_memory().unwrap();
205        let task = TaskService::new(&db)
206            .create_task("Task", None, Some("ABC-1"), None)
207            .unwrap();
208        let snapshot = GetTaskInfoUseCase::new(&db).load(task.id).unwrap();
209
210        assert_eq!(GetTaskInfoUseCase::base_bookmark(&snapshot), "task/ABC-1");
211    }
212
213    #[test]
214    fn cli_json_includes_task_and_todos() {
215        let db = Database::new_in_memory().unwrap();
216        let task = TaskService::new(&db)
217            .create_task("Task", None, None, None)
218            .unwrap();
219        TodoService::new(&db)
220            .add_todo(task.id, "Do thing", false)
221            .unwrap();
222
223        let use_case = GetTaskInfoUseCase::new(&db);
224        let snapshot = use_case.load(task.id).unwrap();
225        let json = use_case.to_cli_json(&snapshot).unwrap();
226
227        assert_eq!(json["task"]["name"], "Task");
228        assert_eq!(json["todos"].as_array().map(|t| t.len()), Some(1));
229    }
230}