1use crate::models::{
2 build_git_context, build_jj_context, build_workflow_context, oldest_pending_todo,
3 workspace_lifecycle, AgentGuardrails, GitAgentContext, JjAgentContext, Task, TaskRepo, Todo,
4 TodoAction, TodoAgentView, TodoStatus, VcsMode, WorkflowContext, WorkspaceAgentView, Worktree,
5};
6use crate::services::WorktreeService;
7use serde::Serialize;
8
9#[derive(Debug, Clone, Serialize)]
11pub struct AgentStatusExtensions {
12 pub vcs_mode: VcsMode,
13 pub workflow: WorkflowContext,
14 #[serde(skip_serializing_if = "Option::is_none")]
15 pub jj: Option<JjAgentContext>,
16 #[serde(skip_serializing_if = "Option::is_none")]
17 pub git: Option<GitAgentContext>,
18 pub todos_agent: Vec<TodoAgentView>,
19 pub guardrails: AgentGuardrails,
20}
21
22pub fn build_agent_extensions(
24 vcs_mode: VcsMode,
25 task: &Task,
26 todos: &[Todo],
27 worktrees: &[Worktree],
28 repos: &[TaskRepo],
29 worktree_service: &WorktreeService<'_>,
30) -> AgentStatusExtensions {
31 let workflow = build_workflow_context(vcs_mode, task, todos, worktrees, repos);
32 let next_todo_id = oldest_pending_todo(todos).map(|todo| todo.id);
33 let legacy_merge_required = vcs_mode == VcsMode::Jj
34 && todos
35 .iter()
36 .any(|todo| todo.status == TodoStatus::Pending && todo.worktree_requested);
37
38 let git_ctx = build_git_context(task, repos);
39 let git_worktree_path = git_ctx.workspace_path.clone();
40
41 let todos_agent: Vec<TodoAgentView> = todos
42 .iter()
43 .map(|todo| {
44 let lifecycle = workspace_lifecycle(todo, worktrees);
45 let bookmark = if todo.worktree_requested {
46 worktree_service
47 .get_todo_branch_name(task.id, task.ticket_id.as_deref(), todo.task_index)
48 .ok()
49 } else {
50 None
51 };
52 let path = worktrees
53 .iter()
54 .find(|wt| wt.todo_id == Some(todo.id))
55 .map(|wt| wt.path.clone())
56 .or_else(|| {
57 if vcs_mode == VcsMode::Git && todo.status == TodoStatus::Pending {
58 git_worktree_path.clone()
59 } else {
60 None
61 }
62 });
63
64 TodoAgentView {
65 todo_id: todo.task_index,
66 content: todo.content.clone(),
67 status: todo.status,
68 is_next: next_todo_id == Some(todo.id),
69 allowed_actions: TodoAction::allowed_for(todo)
70 .into_iter()
71 .map(|action| action.as_str().to_string())
72 .chain(if todo.status == TodoStatus::Pending {
73 vec!["delete".to_string()]
74 } else {
75 Vec::new()
76 })
77 .collect(),
78 workspace: WorkspaceAgentView {
79 lifecycle,
80 path,
81 bookmark,
82 },
83 }
84 })
85 .collect();
86
87 let guardrails = AgentGuardrails::for_mode(vcs_mode, legacy_merge_required);
88
89 let (jj, git) = match vcs_mode {
90 VcsMode::Jj => (Some(build_jj_context(task, repos)), None),
91 VcsMode::Git => (None, Some(git_ctx)),
92 };
93
94 AgentStatusExtensions {
95 vcs_mode,
96 workflow,
97 jj,
98 git,
99 todos_agent,
100 guardrails,
101 }
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107 use crate::db::Database;
108 use crate::models::WorkflowPhase;
109 use crate::services::{TaskService, TodoService};
110
111 #[test]
112 fn agent_extensions_include_workflow_and_jj() {
113 let db = Database::new_in_memory().unwrap();
114 let task = TaskService::new(&db)
115 .create_task("Task", None, None, None)
116 .unwrap();
117 let todo_service = TodoService::new(&db);
118 todo_service.add_todo(task.id, "Work", false).unwrap();
119
120 let todos = todo_service.list_todos(task.id).unwrap();
121 let worktree_service = WorktreeService::new(&db);
122 let extensions =
123 build_agent_extensions(VcsMode::Jj, &task, &todos, &[], &[], &worktree_service);
124
125 assert_eq!(extensions.workflow.phase, WorkflowPhase::Setup);
126 assert_eq!(extensions.vcs_mode, VcsMode::Jj);
127 assert_eq!(extensions.jj.as_ref().unwrap().skill, "jj");
128 assert!(extensions.git.is_none());
129 assert!(extensions.guardrails.must_use_jj_skill);
130 assert!(extensions.guardrails.reopen_forbidden);
131 }
132
133 #[test]
134 fn agent_extensions_use_git_context_in_git_mode() {
135 let db = Database::new_in_memory().unwrap();
136 db.set_vcs_mode(VcsMode::Git).unwrap();
137 let task = TaskService::new(&db)
138 .create_task("Task", None, None, None)
139 .unwrap();
140 let todo_service = TodoService::new(&db);
141 todo_service.add_todo(task.id, "Work", false).unwrap();
142
143 let todos = todo_service.list_todos(task.id).unwrap();
144 let worktree_service = WorktreeService::new(&db);
145 let extensions = build_agent_extensions(
146 db.get_vcs_mode().unwrap(),
147 &task,
148 &todos,
149 &[],
150 &[],
151 &worktree_service,
152 );
153
154 assert_eq!(extensions.vcs_mode, VcsMode::Git);
155 assert!(extensions.jj.is_none());
156 assert_eq!(extensions.git.as_ref().unwrap().branch, "track/task-1");
157 assert!(!extensions.guardrails.must_use_jj_skill);
158 }
159}