1use crate::error::Result;
2use crate::events::EventManager;
3use crate::tasks::TaskManager;
4use crate::workspace::WorkspaceManager;
5use serde::{Deserialize, Serialize};
6use sqlx::SqlitePool;
7
8#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
10#[serde(rename_all = "lowercase")]
11pub enum SessionStatus {
12 Success,
14 NoFocus,
16 Error,
18}
19
20#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
22#[serde(rename_all = "snake_case")]
23pub enum ErrorType {
24 WorkspaceNotFound,
25 DatabaseCorrupted,
26 PermissionDenied,
27}
28
29#[derive(Debug, Clone, Serialize, Deserialize)]
31pub struct TaskInfo {
32 pub id: i64,
33 pub name: String,
34 #[serde(skip_serializing_if = "Option::is_none")]
35 pub status: Option<String>,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct CurrentTaskInfo {
41 pub id: i64,
42 pub name: String,
43 pub status: String,
44 pub spec: Option<String>,
46 pub spec_preview: Option<String>,
48 #[serde(skip_serializing_if = "Option::is_none")]
49 pub created_at: Option<String>,
50 #[serde(skip_serializing_if = "Option::is_none")]
51 pub first_doing_at: Option<String>,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct SiblingsInfo {
57 pub total: usize,
58 pub done: usize,
59 pub doing: usize,
60 pub todo: usize,
61 pub done_list: Vec<TaskInfo>,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct ChildrenInfo {
67 pub total: usize,
68 pub todo: usize,
69 pub list: Vec<TaskInfo>,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct EventInfo {
75 #[serde(rename = "type")]
76 pub event_type: String,
77 pub data: String,
78 pub timestamp: String,
79}
80
81#[derive(Debug, Clone, Serialize, Deserialize)]
83pub struct WorkspaceStats {
84 pub total_tasks: usize,
85 pub todo: usize,
86 pub doing: usize,
87 pub done: usize,
88}
89
90#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct SessionRestoreResult {
93 pub status: SessionStatus,
94 #[serde(skip_serializing_if = "Option::is_none")]
95 pub workspace_path: Option<String>,
96 #[serde(skip_serializing_if = "Option::is_none")]
97 pub current_task: Option<CurrentTaskInfo>,
98 #[serde(skip_serializing_if = "Option::is_none")]
99 pub parent_task: Option<TaskInfo>,
100 #[serde(skip_serializing_if = "Option::is_none")]
101 pub siblings: Option<SiblingsInfo>,
102 #[serde(skip_serializing_if = "Option::is_none")]
103 pub children: Option<ChildrenInfo>,
104 #[serde(skip_serializing_if = "Option::is_none")]
105 pub recent_events: Option<Vec<EventInfo>>,
106 #[serde(skip_serializing_if = "Option::is_none")]
107 pub suggested_commands: Option<Vec<String>>,
108 #[serde(skip_serializing_if = "Option::is_none")]
109 pub stats: Option<WorkspaceStats>,
110 #[serde(skip_serializing_if = "Option::is_none")]
111 pub error_type: Option<ErrorType>,
112 #[serde(skip_serializing_if = "Option::is_none")]
113 pub message: Option<String>,
114 #[serde(skip_serializing_if = "Option::is_none")]
115 pub recovery_suggestion: Option<String>,
116}
117
118pub struct SessionRestoreManager<'a> {
120 pool: &'a SqlitePool,
121}
122
123impl<'a> SessionRestoreManager<'a> {
124 pub fn new(pool: &'a SqlitePool) -> Self {
125 Self { pool }
126 }
127
128 pub async fn restore(&self, include_events: usize) -> Result<SessionRestoreResult> {
130 let workspace_mgr = WorkspaceManager::new(self.pool);
131 let task_mgr = TaskManager::new(self.pool);
132 let event_mgr = EventManager::new(self.pool);
133
134 let workspace_path = std::env::current_dir()
136 .ok()
137 .and_then(|p| p.to_str().map(String::from));
138
139 let current_task_id = match workspace_mgr.get_current_task().await {
141 Ok(response) => {
142 if let Some(id) = response.current_task_id {
143 id
144 } else {
145 return self.restore_no_focus(workspace_path).await;
147 }
148 },
149 Err(_) => {
150 return Ok(Self::error_result(
152 workspace_path,
153 ErrorType::DatabaseCorrupted,
154 "Unable to query workspace state",
155 "Run 'ie workspace init' or check database integrity",
156 ));
157 },
158 };
159
160 let task = match task_mgr.get_task(current_task_id).await {
162 Ok(t) => t,
163 Err(_) => {
164 return Ok(Self::error_result(
165 workspace_path,
166 ErrorType::DatabaseCorrupted,
167 "Current task not found in database",
168 "Run 'ie current --set <task_id>' to set a valid task",
169 ));
170 },
171 };
172
173 let spec_preview = task.spec.as_ref().map(|s| Self::truncate_spec(s, 100));
174
175 let current_task = CurrentTaskInfo {
176 id: task.id,
177 name: task.name.clone(),
178 status: task.status.clone(),
179 spec: task.spec.clone(),
180 spec_preview,
181 created_at: task.first_todo_at.map(|dt| dt.to_rfc3339()),
182 first_doing_at: task.first_doing_at.map(|dt| dt.to_rfc3339()),
183 };
184
185 let parent_task = if let Some(parent_id) = task.parent_id {
187 task_mgr.get_task(parent_id).await.ok().map(|p| TaskInfo {
188 id: p.id,
189 name: p.name,
190 status: Some(p.status),
191 })
192 } else {
193 None
194 };
195
196 let siblings = if let Some(parent_id) = task.parent_id {
198 let all_siblings = task_mgr.find_tasks(None, Some(Some(parent_id))).await?;
199 Self::build_siblings_info(&all_siblings)
200 } else {
201 None
202 };
203
204 let children = {
206 let all_children = task_mgr
207 .find_tasks(None, Some(Some(current_task_id)))
208 .await?;
209 Self::build_children_info(&all_children)
210 };
211
212 let events = event_mgr
214 .list_events(current_task_id, None, None, None)
215 .await?;
216 let recent_events: Vec<EventInfo> = events
217 .into_iter()
218 .take(include_events)
219 .map(|e| EventInfo {
220 event_type: e.log_type,
221 data: e.discussion_data,
222 timestamp: e.timestamp.to_rfc3339(),
223 })
224 .collect();
225
226 let suggested_commands = Self::suggest_commands(¤t_task, children.as_ref());
228
229 Ok(SessionRestoreResult {
230 status: SessionStatus::Success,
231 workspace_path,
232 current_task: Some(current_task),
233 parent_task,
234 siblings,
235 children,
236 recent_events: Some(recent_events),
237 suggested_commands: Some(suggested_commands),
238 stats: None,
239 error_type: None,
240 message: None,
241 recovery_suggestion: None,
242 })
243 }
244
245 async fn restore_no_focus(
247 &self,
248 workspace_path: Option<String>,
249 ) -> Result<SessionRestoreResult> {
250 let task_mgr = TaskManager::new(self.pool);
251
252 let all_tasks = task_mgr.find_tasks(None, None).await?;
254
255 let stats = WorkspaceStats {
256 total_tasks: all_tasks.len(),
257 todo: all_tasks.iter().filter(|t| t.status == "todo").count(),
258 doing: all_tasks.iter().filter(|t| t.status == "doing").count(),
259 done: all_tasks.iter().filter(|t| t.status == "done").count(),
260 };
261
262 let suggested_commands = vec![
263 "ie pick-next".to_string(),
264 "ie task list --status todo".to_string(),
265 ];
266
267 Ok(SessionRestoreResult {
268 status: SessionStatus::NoFocus,
269 workspace_path,
270 current_task: None,
271 parent_task: None,
272 siblings: None,
273 children: None,
274 recent_events: None,
275 suggested_commands: Some(suggested_commands),
276 stats: Some(stats),
277 error_type: None,
278 message: None,
279 recovery_suggestion: None,
280 })
281 }
282
283 fn build_siblings_info(siblings: &[crate::db::models::Task]) -> Option<SiblingsInfo> {
285 if siblings.is_empty() {
286 return None;
287 }
288
289 let done_list: Vec<TaskInfo> = siblings
290 .iter()
291 .filter(|s| s.status == "done")
292 .map(|s| TaskInfo {
293 id: s.id,
294 name: s.name.clone(),
295 status: Some(s.status.clone()),
296 })
297 .collect();
298
299 Some(SiblingsInfo {
300 total: siblings.len(),
301 done: siblings.iter().filter(|s| s.status == "done").count(),
302 doing: siblings.iter().filter(|s| s.status == "doing").count(),
303 todo: siblings.iter().filter(|s| s.status == "todo").count(),
304 done_list,
305 })
306 }
307
308 fn build_children_info(children: &[crate::db::models::Task]) -> Option<ChildrenInfo> {
310 if children.is_empty() {
311 return None;
312 }
313
314 let list: Vec<TaskInfo> = children
315 .iter()
316 .map(|c| TaskInfo {
317 id: c.id,
318 name: c.name.clone(),
319 status: Some(c.status.clone()),
320 })
321 .collect();
322
323 Some(ChildrenInfo {
324 total: children.len(),
325 todo: children.iter().filter(|c| c.status == "todo").count(),
326 list,
327 })
328 }
329
330 fn suggest_commands(
332 _current_task: &CurrentTaskInfo,
333 children: Option<&ChildrenInfo>,
334 ) -> Vec<String> {
335 let mut commands = vec![];
336
337 commands.push("ie event list --type blocker".to_string());
339
340 if let Some(children) = children {
342 if children.todo > 0 {
343 commands.push("ie task done".to_string());
344 }
345 } else {
346 commands.push("ie task done".to_string());
347 }
348
349 commands.push("ie task spawn-subtask".to_string());
350
351 commands
352 }
353
354 fn truncate_spec(spec: &str, max_len: usize) -> String {
356 if spec.len() <= max_len {
357 spec.to_string()
358 } else {
359 format!("{}...", &spec[..max_len])
360 }
361 }
362
363 fn error_result(
365 workspace_path: Option<String>,
366 error_type: ErrorType,
367 message: &str,
368 recovery: &str,
369 ) -> SessionRestoreResult {
370 let suggested_commands = match error_type {
371 ErrorType::WorkspaceNotFound => {
372 vec!["ie workspace init".to_string(), "ie help".to_string()]
373 },
374 ErrorType::DatabaseCorrupted => {
375 vec!["ie workspace init".to_string(), "ie doctor".to_string()]
376 },
377 ErrorType::PermissionDenied => {
378 vec!["chmod 644 .intent-engine/workspace.db".to_string()]
379 },
380 };
381
382 SessionRestoreResult {
383 status: SessionStatus::Error,
384 workspace_path,
385 current_task: None,
386 parent_task: None,
387 siblings: None,
388 children: None,
389 recent_events: None,
390 suggested_commands: Some(suggested_commands),
391 stats: None,
392 error_type: Some(error_type),
393 message: Some(message.to_string()),
394 recovery_suggestion: Some(recovery.to_string()),
395 }
396 }
397}
398
399#[cfg(test)]
400mod tests {
401 use super::*;
402 use crate::events::EventManager;
403 use crate::tasks::TaskManager;
404 use crate::test_utils::test_helpers::TestContext;
405 use crate::workspace::WorkspaceManager;
406
407 #[test]
408 fn test_truncate_spec() {
409 let spec = "a".repeat(200);
410 let truncated = SessionRestoreManager::truncate_spec(&spec, 100);
411 assert_eq!(truncated.len(), 103); assert!(truncated.ends_with("..."));
413 }
414
415 #[test]
416 fn test_truncate_spec_short() {
417 let spec = "Short spec";
418 let truncated = SessionRestoreManager::truncate_spec(spec, 100);
419 assert_eq!(truncated, spec);
420 assert!(!truncated.ends_with("..."));
421 }
422
423 #[tokio::test]
424 async fn test_restore_with_focus_minimal() {
425 let ctx = TestContext::new().await;
426 let pool = ctx.pool();
427
428 let task_mgr = TaskManager::new(pool);
430 let task = task_mgr.add_task("Test task", None, None).await.unwrap();
431
432 let workspace_mgr = WorkspaceManager::new(pool);
433 workspace_mgr.set_current_task(task.id).await.unwrap();
434
435 let restore_mgr = SessionRestoreManager::new(pool);
437 let result = restore_mgr.restore(3).await.unwrap();
438
439 assert_eq!(result.status, SessionStatus::Success);
441 assert!(result.current_task.is_some());
442 let current_task = result.current_task.unwrap();
443 assert_eq!(current_task.id, task.id);
444 assert_eq!(current_task.name, "Test task");
445 }
446
447 #[tokio::test]
448 async fn test_restore_with_focus_rich_context() {
449 let ctx = TestContext::new().await;
450 let pool = ctx.pool();
451
452 let task_mgr = TaskManager::new(pool);
453 let event_mgr = EventManager::new(pool);
454 let workspace_mgr = WorkspaceManager::new(pool);
455
456 let parent = task_mgr
458 .add_task("Parent task", Some("Parent spec"), None)
459 .await
460 .unwrap();
461
462 let sibling1 = task_mgr
464 .add_task("Sibling 1", None, Some(parent.id))
465 .await
466 .unwrap();
467 task_mgr
468 .update_task(sibling1.id, None, None, None, Some("done"), None, None)
469 .await
470 .unwrap();
471
472 let current = task_mgr
473 .add_task("Current task", Some("Current spec"), Some(parent.id))
474 .await
475 .unwrap();
476 task_mgr
477 .update_task(current.id, None, None, None, Some("doing"), None, None)
478 .await
479 .unwrap();
480 workspace_mgr.set_current_task(current.id).await.unwrap();
481
482 let _sibling3 = task_mgr
483 .add_task("Sibling 3", None, Some(parent.id))
484 .await
485 .unwrap();
486
487 let _child1 = task_mgr
489 .add_task("Child 1", None, Some(current.id))
490 .await
491 .unwrap();
492 let _child2 = task_mgr
493 .add_task("Child 2", None, Some(current.id))
494 .await
495 .unwrap();
496
497 event_mgr
499 .add_event(current.id, "decision", "Decision 1")
500 .await
501 .unwrap();
502 event_mgr
503 .add_event(current.id, "blocker", "Blocker 1")
504 .await
505 .unwrap();
506 event_mgr
507 .add_event(current.id, "note", "Note 1")
508 .await
509 .unwrap();
510
511 let restore_mgr = SessionRestoreManager::new(pool);
513 let result = restore_mgr.restore(3).await.unwrap();
514
515 assert_eq!(result.status, SessionStatus::Success);
517
518 let task = result.current_task.unwrap();
519 assert_eq!(task.id, current.id);
520 assert_eq!(task.spec, Some("Current spec".to_string()));
521
522 assert!(result.parent_task.is_some());
524 let parent_info = result.parent_task.unwrap();
525 assert_eq!(parent_info.id, parent.id);
526
527 assert!(result.siblings.is_some());
529 let siblings = result.siblings.unwrap();
530 assert_eq!(siblings.total, 3);
531 assert_eq!(siblings.done, 1);
532 assert_eq!(siblings.doing, 1);
533 assert_eq!(siblings.todo, 1);
534 assert_eq!(siblings.done_list.len(), 1);
535
536 assert!(result.children.is_some());
538 let children = result.children.unwrap();
539 assert_eq!(children.total, 2);
540 assert_eq!(children.todo, 2);
541
542 assert!(result.recent_events.is_some());
544 let events = result.recent_events.unwrap();
545 assert_eq!(events.len(), 3);
546
547 let event_types: Vec<&str> = events.iter().map(|e| e.event_type.as_str()).collect();
549 assert!(event_types.contains(&"decision"));
550 assert!(event_types.contains(&"blocker"));
551 assert!(event_types.contains(&"note"));
552 }
553
554 #[tokio::test]
555 async fn test_restore_with_spec_preview() {
556 let ctx = TestContext::new().await;
557 let pool = ctx.pool();
558
559 let task_mgr = TaskManager::new(pool);
560 let workspace_mgr = WorkspaceManager::new(pool);
561
562 let long_spec = "a".repeat(200);
564 let task = task_mgr
565 .add_task("Test task", Some(&long_spec), None)
566 .await
567 .unwrap();
568 workspace_mgr.set_current_task(task.id).await.unwrap();
569
570 let restore_mgr = SessionRestoreManager::new(pool);
572 let result = restore_mgr.restore(3).await.unwrap();
573
574 let current_task = result.current_task.unwrap();
576 assert_eq!(current_task.spec, Some(long_spec));
577 assert!(current_task.spec_preview.is_some());
578 let preview = current_task.spec_preview.unwrap();
579 assert_eq!(preview.len(), 103); assert!(preview.ends_with("..."));
581 }
582
583 #[tokio::test]
584 async fn test_restore_no_focus() {
585 let ctx = TestContext::new().await;
586 let pool = ctx.pool();
587
588 let task_mgr = TaskManager::new(pool);
589
590 task_mgr.add_task("Task 1", None, None).await.unwrap();
592 task_mgr.add_task("Task 2", None, None).await.unwrap();
593
594 let restore_mgr = SessionRestoreManager::new(pool);
596 let result = restore_mgr.restore(3).await.unwrap();
597
598 assert_eq!(result.status, SessionStatus::NoFocus);
600 assert!(result.current_task.is_none());
601 assert!(result.stats.is_some());
602
603 let stats = result.stats.unwrap();
604 assert_eq!(stats.total_tasks, 2);
605 assert_eq!(stats.todo, 2);
606 }
607
608 #[tokio::test]
609 async fn test_restore_recent_events_limit() {
610 let ctx = TestContext::new().await;
611 let pool = ctx.pool();
612
613 let task_mgr = TaskManager::new(pool);
614 let event_mgr = EventManager::new(pool);
615 let workspace_mgr = WorkspaceManager::new(pool);
616
617 let task = task_mgr.add_task("Test task", None, None).await.unwrap();
619 workspace_mgr.set_current_task(task.id).await.unwrap();
620
621 for i in 0..10 {
623 event_mgr
624 .add_event(task.id, "note", &format!("Event {}", i))
625 .await
626 .unwrap();
627 }
628
629 let restore_mgr = SessionRestoreManager::new(pool);
631 let result = restore_mgr.restore(3).await.unwrap();
632
633 let events = result.recent_events.unwrap();
635 assert_eq!(events.len(), 3);
636 }
637
638 #[tokio::test]
639 async fn test_restore_custom_events_limit() {
640 let ctx = TestContext::new().await;
641 let pool = ctx.pool();
642
643 let task_mgr = TaskManager::new(pool);
644 let event_mgr = EventManager::new(pool);
645 let workspace_mgr = WorkspaceManager::new(pool);
646
647 let task = task_mgr.add_task("Test task", None, None).await.unwrap();
649 workspace_mgr.set_current_task(task.id).await.unwrap();
650
651 for i in 0..10 {
653 event_mgr
654 .add_event(task.id, "note", &format!("Event {}", i))
655 .await
656 .unwrap();
657 }
658
659 let restore_mgr = SessionRestoreManager::new(pool);
661 let result = restore_mgr.restore(5).await.unwrap();
662
663 let events = result.recent_events.unwrap();
665 assert_eq!(events.len(), 5);
666 }
667
668 #[test]
669 fn test_suggest_commands_with_children() {
670 let current_task = CurrentTaskInfo {
671 id: 1,
672 name: "Test".to_string(),
673 status: "doing".to_string(),
674 spec: None,
675 spec_preview: None,
676 created_at: None,
677 first_doing_at: None,
678 };
679
680 let children = Some(ChildrenInfo {
681 total: 2,
682 todo: 1,
683 list: vec![],
684 });
685
686 let commands = SessionRestoreManager::suggest_commands(¤t_task, children.as_ref());
687
688 assert!(!commands.is_empty());
689 assert!(commands.iter().any(|c| c.contains("blocker")));
690 }
691
692 #[test]
693 fn test_error_result_workspace_not_found() {
694 let result = SessionRestoreManager::error_result(
695 Some("/test/path".to_string()),
696 ErrorType::WorkspaceNotFound,
697 "Test message",
698 "Test recovery",
699 );
700
701 assert_eq!(result.status, SessionStatus::Error);
702 assert_eq!(result.error_type, Some(ErrorType::WorkspaceNotFound));
703 assert_eq!(result.message, Some("Test message".to_string()));
704 assert_eq!(
705 result.recovery_suggestion,
706 Some("Test recovery".to_string())
707 );
708 assert!(result.suggested_commands.is_some());
709
710 let commands = result.suggested_commands.unwrap();
711 assert!(commands.iter().any(|c| c.contains("init")));
712 }
713
714 #[test]
715 fn test_build_siblings_info_empty() {
716 let siblings: Vec<crate::db::models::Task> = vec![];
717 let result = SessionRestoreManager::build_siblings_info(&siblings);
718 assert!(result.is_none());
719 }
720
721 #[test]
722 fn test_build_children_info_empty() {
723 let children: Vec<crate::db::models::Task> = vec![];
724 let result = SessionRestoreManager::build_children_info(&children);
725 assert!(result.is_none());
726 }
727}