1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use sqlx::FromRow;
4
5#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
6pub struct Task {
7 pub id: i64,
8 pub parent_id: Option<i64>,
9 pub name: String,
10 #[serde(skip_serializing_if = "Option::is_none")]
11 pub spec: Option<String>,
12 pub status: String,
13 #[serde(skip_serializing_if = "Option::is_none")]
14 pub complexity: Option<i32>,
15 #[serde(skip_serializing_if = "Option::is_none")]
16 pub priority: Option<i32>,
17 pub first_todo_at: Option<DateTime<Utc>>,
18 pub first_doing_at: Option<DateTime<Utc>>,
19 pub first_done_at: Option<DateTime<Utc>>,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct TaskWithEvents {
24 #[serde(flatten)]
25 pub task: Task,
26 #[serde(skip_serializing_if = "Option::is_none")]
27 pub events_summary: Option<EventsSummary>,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct EventsSummary {
32 pub total_count: i64,
33 pub recent_events: Vec<Event>,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
37pub struct Event {
38 pub id: i64,
39 pub task_id: i64,
40 pub timestamp: DateTime<Utc>,
41 pub log_type: String,
42 pub discussion_data: String,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize, FromRow)]
46pub struct WorkspaceState {
47 pub key: String,
48 pub value: String,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct Report {
53 pub summary: ReportSummary,
54 #[serde(skip_serializing_if = "Option::is_none")]
55 pub tasks: Option<Vec<Task>>,
56 #[serde(skip_serializing_if = "Option::is_none")]
57 pub events: Option<Vec<Event>>,
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct ReportSummary {
62 pub total_tasks: i64,
63 pub tasks_by_status: StatusBreakdown,
64 pub total_events: i64,
65 pub date_range: Option<DateRange>,
66}
67
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct StatusBreakdown {
70 pub todo: i64,
71 pub doing: i64,
72 pub done: i64,
73}
74
75#[derive(Debug, Clone, Serialize, Deserialize)]
76pub struct DateRange {
77 pub from: DateTime<Utc>,
78 pub to: DateTime<Utc>,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct DoneTaskResponse {
83 pub completed_task: Task,
84 pub workspace_status: WorkspaceStatus,
85 pub next_step_suggestion: NextStepSuggestion,
86}
87
88#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct WorkspaceStatus {
90 pub current_task_id: Option<i64>,
91}
92
93#[derive(Debug, Clone, Serialize, Deserialize)]
94#[serde(tag = "type")]
95pub enum NextStepSuggestion {
96 #[serde(rename = "PARENT_IS_READY")]
97 ParentIsReady {
98 message: String,
99 parent_task_id: i64,
100 parent_task_name: String,
101 },
102 #[serde(rename = "SIBLING_TASKS_REMAIN")]
103 SiblingTasksRemain {
104 message: String,
105 parent_task_id: i64,
106 parent_task_name: String,
107 remaining_siblings_count: i64,
108 },
109 #[serde(rename = "TOP_LEVEL_TASK_COMPLETED")]
110 TopLevelTaskCompleted {
111 message: String,
112 completed_task_id: i64,
113 completed_task_name: String,
114 },
115 #[serde(rename = "NO_PARENT_CONTEXT")]
116 NoParentContext {
117 message: String,
118 completed_task_id: i64,
119 completed_task_name: String,
120 },
121 #[serde(rename = "WORKSPACE_IS_CLEAR")]
122 WorkspaceIsClear {
123 message: String,
124 completed_task_id: i64,
125 },
126}
127
128#[derive(Debug, Clone, Serialize, Deserialize)]
129pub struct TaskSearchResult {
130 #[serde(flatten)]
131 pub task: Task,
132 pub match_snippet: String,
133}
134
135#[derive(Debug, Clone, Serialize, Deserialize)]
136pub struct PickNextResponse {
137 pub suggestion_type: String,
138 #[serde(skip_serializing_if = "Option::is_none")]
139 pub task: Option<Task>,
140 #[serde(skip_serializing_if = "Option::is_none")]
141 pub reason_code: Option<String>,
142 #[serde(skip_serializing_if = "Option::is_none")]
143 pub message: Option<String>,
144}
145
146impl PickNextResponse {
147 pub fn focused_subtask(task: Task) -> Self {
149 Self {
150 suggestion_type: "FOCUSED_SUB_TASK".to_string(),
151 task: Some(task),
152 reason_code: None,
153 message: None,
154 }
155 }
156
157 pub fn top_level_task(task: Task) -> Self {
159 Self {
160 suggestion_type: "TOP_LEVEL_TASK".to_string(),
161 task: Some(task),
162 reason_code: None,
163 message: None,
164 }
165 }
166
167 pub fn no_tasks_in_project() -> Self {
169 Self {
170 suggestion_type: "NONE".to_string(),
171 task: None,
172 reason_code: Some("NO_TASKS_IN_PROJECT".to_string()),
173 message: Some(
174 "No tasks found in this project. Your intent backlog is empty.".to_string(),
175 ),
176 }
177 }
178
179 pub fn all_tasks_completed() -> Self {
181 Self {
182 suggestion_type: "NONE".to_string(),
183 task: None,
184 reason_code: Some("ALL_TASKS_COMPLETED".to_string()),
185 message: Some("Project Complete! All intents have been realized.".to_string()),
186 }
187 }
188
189 pub fn no_available_todos() -> Self {
191 Self {
192 suggestion_type: "NONE".to_string(),
193 task: None,
194 reason_code: Some("NO_AVAILABLE_TODOS".to_string()),
195 message: Some("No immediate next task found based on the current context.".to_string()),
196 }
197 }
198
199 pub fn format_as_text(&self) -> String {
201 match self.suggestion_type.as_str() {
202 "FOCUSED_SUB_TASK" | "TOP_LEVEL_TASK" => {
203 if let Some(task) = &self.task {
204 format!(
205 "Based on your current focus, the recommended next task is:\n\n\
206 [ID: {}] [Priority: {}] [Status: {}]\n\
207 Name: {}\n\n\
208 To start working on it, run:\n ie task start {}",
209 task.id,
210 task.priority.unwrap_or(0),
211 task.status,
212 task.name,
213 task.id
214 )
215 } else {
216 "[ERROR] Invalid response: task is missing".to_string()
217 }
218 }
219 "NONE" => {
220 let reason_code = self.reason_code.as_deref().unwrap_or("UNKNOWN");
221 let message = self.message.as_deref().unwrap_or("No tasks found");
222
223 match reason_code {
224 "NO_TASKS_IN_PROJECT" => {
225 format!(
226 "[INFO] {}\n\n\
227 To get started, capture your first high-level intent:\n \
228 ie task add --name \"Setup initial project structure\" --priority 1",
229 message
230 )
231 }
232 "ALL_TASKS_COMPLETED" => {
233 format!(
234 "[SUCCESS] {}\n\n\
235 You can review the accomplishments of the last 30 days with:\n \
236 ie report --since 30d",
237 message
238 )
239 }
240 "NO_AVAILABLE_TODOS" => {
241 format!(
242 "[INFO] {}\n\n\
243 Possible reasons:\n\
244 - All available 'todo' tasks are part of larger epics that have not been started yet.\n\
245 - You are not currently focused on a task that has 'todo' sub-tasks.\n\n\
246 To see all available top-level tasks you can start, run:\n \
247 ie task find --parent NULL --status todo",
248 message
249 )
250 }
251 _ => format!("[INFO] {}", message),
252 }
253 }
254 _ => "[ERROR] Unknown suggestion type".to_string(),
255 }
256 }
257}