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