1use crate::db::models::{TaskSortBy, WorkspaceStats};
2use crate::error::Result;
3use crate::events::EventManager;
4use crate::tasks::TaskManager;
5use crate::workspace::WorkspaceManager;
6use serde::{Deserialize, Serialize};
7use sqlx::SqlitePool;
8
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
11#[serde(rename_all = "lowercase")]
12pub enum SessionStatus {
13 Success,
15 NoFocus,
17 Error,
19}
20
21#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
23#[serde(rename_all = "snake_case")]
24pub enum ErrorType {
25 WorkspaceNotFound,
26 DatabaseCorrupted,
27 PermissionDenied,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
32pub struct TaskInfo {
33 pub id: i64,
34 pub name: String,
35 #[serde(skip_serializing_if = "Option::is_none")]
36 pub status: Option<String>,
37}
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct CurrentTaskInfo {
42 pub id: i64,
43 pub name: String,
44 pub status: String,
45 pub spec: Option<String>,
47 pub spec_preview: Option<String>,
49 #[serde(skip_serializing_if = "Option::is_none")]
50 pub created_at: Option<String>,
51 #[serde(skip_serializing_if = "Option::is_none")]
52 pub first_doing_at: Option<String>,
53}
54
55#[derive(Debug, Clone, Serialize, Deserialize)]
57pub struct SiblingsInfo {
58 pub total: usize,
59 pub done: usize,
60 pub doing: usize,
61 pub todo: usize,
62 pub done_list: Vec<TaskInfo>,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct ChildrenInfo {
68 pub total: usize,
69 pub todo: usize,
70 pub list: Vec<TaskInfo>,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct EventInfo {
76 #[serde(rename = "type")]
77 pub event_type: String,
78 pub data: String,
79 pub timestamp: String,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
86pub struct SessionRestoreResult {
87 pub status: SessionStatus,
88 #[serde(skip_serializing_if = "Option::is_none")]
89 pub workspace_path: Option<String>,
90 #[serde(skip_serializing_if = "Option::is_none")]
91 pub current_task: Option<CurrentTaskInfo>,
92 #[serde(skip_serializing_if = "Option::is_none")]
93 pub parent_task: Option<TaskInfo>,
94 #[serde(skip_serializing_if = "Option::is_none")]
95 pub siblings: Option<SiblingsInfo>,
96 #[serde(skip_serializing_if = "Option::is_none")]
97 pub children: Option<ChildrenInfo>,
98 #[serde(skip_serializing_if = "Option::is_none")]
99 pub recent_events: Option<Vec<EventInfo>>,
100 #[serde(skip_serializing_if = "Option::is_none")]
101 pub suggested_commands: Option<Vec<String>>,
102 #[serde(skip_serializing_if = "Option::is_none")]
103 pub stats: Option<WorkspaceStats>,
104 #[serde(skip_serializing_if = "Option::is_none")]
106 pub recommended_task: Option<TaskInfo>,
107 #[serde(skip_serializing_if = "Option::is_none")]
109 pub top_pending_tasks: Option<Vec<TaskInfo>>,
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(None).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 result = task_mgr
199 .find_tasks(None, Some(Some(parent_id)), None, None, None)
200 .await?;
201 Self::build_siblings_info(&result.tasks)
202 } else {
203 None
204 };
205
206 let children = {
208 let result = task_mgr
209 .find_tasks(None, Some(Some(current_task_id)), None, None, None)
210 .await?;
211 Self::build_children_info(&result.tasks)
212 };
213
214 let events = event_mgr
216 .list_events(Some(current_task_id), None, None, None)
217 .await?;
218 let recent_events: Vec<EventInfo> = events
219 .into_iter()
220 .take(include_events)
221 .map(|e| EventInfo {
222 event_type: e.log_type,
223 data: e.discussion_data,
224 timestamp: e.timestamp.to_rfc3339(),
225 })
226 .collect();
227
228 let suggested_commands = Self::suggest_commands(¤t_task, children.as_ref());
230
231 Ok(SessionRestoreResult {
232 status: SessionStatus::Success,
233 workspace_path,
234 current_task: Some(current_task),
235 parent_task,
236 siblings,
237 children,
238 recent_events: Some(recent_events),
239 suggested_commands: Some(suggested_commands),
240 stats: None,
241 recommended_task: None,
242 top_pending_tasks: None,
243 error_type: None,
244 message: None,
245 recovery_suggestion: None,
246 })
247 }
248
249 async fn restore_no_focus(
254 &self,
255 workspace_path: Option<String>,
256 ) -> Result<SessionRestoreResult> {
257 let task_mgr = TaskManager::new(self.pool);
258
259 let stats = task_mgr.get_stats().await?;
261
262 let recommendation = task_mgr.pick_next().await?;
264 let recommended_task = recommendation.task.map(|t| TaskInfo {
265 id: t.id,
266 name: t.name,
267 status: Some(t.status),
268 });
269
270 let top_pending_result = task_mgr
272 .find_tasks(
273 Some("todo".to_string()),
274 None,
275 Some(TaskSortBy::Priority),
276 Some(5),
277 None,
278 )
279 .await?;
280 let top_pending_tasks: Vec<TaskInfo> = top_pending_result
281 .tasks
282 .into_iter()
283 .map(|t| TaskInfo {
284 id: t.id,
285 name: t.name,
286 status: Some(t.status),
287 })
288 .collect();
289
290 let suggested_commands = vec![
291 "ie pick-next".to_string(),
292 "ie task list --status todo".to_string(),
293 ];
294
295 Ok(SessionRestoreResult {
296 status: SessionStatus::NoFocus,
297 workspace_path,
298 current_task: None,
299 parent_task: None,
300 siblings: None,
301 children: None,
302 recent_events: None,
303 suggested_commands: Some(suggested_commands),
304 stats: Some(stats),
305 recommended_task,
306 top_pending_tasks: if top_pending_tasks.is_empty() {
307 None
308 } else {
309 Some(top_pending_tasks)
310 },
311 error_type: None,
312 message: None,
313 recovery_suggestion: None,
314 })
315 }
316
317 fn build_siblings_info(siblings: &[crate::db::models::Task]) -> Option<SiblingsInfo> {
319 if siblings.is_empty() {
320 return None;
321 }
322
323 let done_list: Vec<TaskInfo> = siblings
324 .iter()
325 .filter(|s| s.status == "done")
326 .map(|s| TaskInfo {
327 id: s.id,
328 name: s.name.clone(),
329 status: Some(s.status.clone()),
330 })
331 .collect();
332
333 Some(SiblingsInfo {
334 total: siblings.len(),
335 done: siblings.iter().filter(|s| s.status == "done").count(),
336 doing: siblings.iter().filter(|s| s.status == "doing").count(),
337 todo: siblings.iter().filter(|s| s.status == "todo").count(),
338 done_list,
339 })
340 }
341
342 fn build_children_info(children: &[crate::db::models::Task]) -> Option<ChildrenInfo> {
344 if children.is_empty() {
345 return None;
346 }
347
348 let list: Vec<TaskInfo> = children
349 .iter()
350 .map(|c| TaskInfo {
351 id: c.id,
352 name: c.name.clone(),
353 status: Some(c.status.clone()),
354 })
355 .collect();
356
357 Some(ChildrenInfo {
358 total: children.len(),
359 todo: children.iter().filter(|c| c.status == "todo").count(),
360 list,
361 })
362 }
363
364 fn suggest_commands(
366 _current_task: &CurrentTaskInfo,
367 children: Option<&ChildrenInfo>,
368 ) -> Vec<String> {
369 let mut commands = vec![];
370
371 commands.push("ie event list --type blocker".to_string());
373
374 if let Some(children) = children {
376 if children.todo > 0 {
377 commands.push("ie task done".to_string());
378 }
379 } else {
380 commands.push("ie task done".to_string());
381 }
382
383 commands.push("ie task spawn-subtask".to_string());
384
385 commands
386 }
387
388 fn truncate_spec(spec: &str, max_len: usize) -> String {
390 if spec.len() <= max_len {
391 spec.to_string()
392 } else {
393 format!("{}...", &spec[..max_len])
394 }
395 }
396
397 fn error_result(
399 workspace_path: Option<String>,
400 error_type: ErrorType,
401 message: &str,
402 recovery: &str,
403 ) -> SessionRestoreResult {
404 let suggested_commands = match error_type {
405 ErrorType::WorkspaceNotFound => {
406 vec!["ie workspace init".to_string(), "ie help".to_string()]
407 },
408 ErrorType::DatabaseCorrupted => {
409 vec!["ie workspace init".to_string(), "ie doctor".to_string()]
410 },
411 ErrorType::PermissionDenied => {
412 vec!["chmod 644 .intent-engine/workspace.db".to_string()]
413 },
414 };
415
416 SessionRestoreResult {
417 status: SessionStatus::Error,
418 workspace_path,
419 current_task: None,
420 parent_task: None,
421 siblings: None,
422 children: None,
423 recent_events: None,
424 suggested_commands: Some(suggested_commands),
425 stats: None,
426 recommended_task: None,
427 top_pending_tasks: None,
428 error_type: Some(error_type),
429 message: Some(message.to_string()),
430 recovery_suggestion: Some(recovery.to_string()),
431 }
432 }
433}
434
435#[cfg(test)]
436mod tests {
437 use super::*;
438 use crate::events::EventManager;
439 use crate::tasks::{TaskManager, TaskUpdate};
440 use crate::test_utils::test_helpers::TestContext;
441 use crate::workspace::WorkspaceManager;
442
443 #[test]
444 fn test_truncate_spec() {
445 let spec = "a".repeat(200);
446 let truncated = SessionRestoreManager::truncate_spec(&spec, 100);
447 assert_eq!(truncated.len(), 103); assert!(truncated.ends_with("..."));
449 }
450
451 #[test]
452 fn test_truncate_spec_short() {
453 let spec = "Short spec";
454 let truncated = SessionRestoreManager::truncate_spec(spec, 100);
455 assert_eq!(truncated, spec);
456 assert!(!truncated.ends_with("..."));
457 }
458
459 #[tokio::test]
460 async fn test_restore_with_focus_minimal() {
461 let ctx = TestContext::new().await;
462 let pool = ctx.pool();
463
464 let task_mgr = TaskManager::new(pool);
466 let task = task_mgr
467 .add_task("Test task".to_string(), None, None, None, None, None)
468 .await
469 .unwrap();
470
471 let workspace_mgr = WorkspaceManager::new(pool);
472 workspace_mgr.set_current_task(task.id, None).await.unwrap();
473
474 let restore_mgr = SessionRestoreManager::new(pool);
476 let result = restore_mgr.restore(3).await.unwrap();
477
478 assert_eq!(result.status, SessionStatus::Success);
480 assert!(result.current_task.is_some());
481 let current_task = result.current_task.unwrap();
482 assert_eq!(current_task.id, task.id);
483 assert_eq!(current_task.name, "Test task");
484 }
485
486 #[tokio::test]
487 async fn test_restore_with_focus_rich_context() {
488 let ctx = TestContext::new().await;
489 let pool = ctx.pool();
490
491 let task_mgr = TaskManager::new(pool);
492 let event_mgr = EventManager::new(pool);
493 let workspace_mgr = WorkspaceManager::new(pool);
494
495 let parent = task_mgr
497 .add_task(
498 "Parent task".to_string(),
499 Some("Parent spec".to_string()),
500 None,
501 None,
502 None,
503 None,
504 )
505 .await
506 .unwrap();
507
508 let sibling1 = task_mgr
510 .add_task(
511 "Sibling 1".to_string(),
512 None,
513 Some(parent.id),
514 None,
515 None,
516 None,
517 )
518 .await
519 .unwrap();
520 task_mgr
521 .update_task(
522 sibling1.id,
523 TaskUpdate {
524 status: Some("done"),
525 ..Default::default()
526 },
527 )
528 .await
529 .unwrap();
530
531 let current = task_mgr
532 .add_task(
533 "Current task".to_string(),
534 Some("Current spec".to_string()),
535 Some(parent.id),
536 None,
537 None,
538 None,
539 )
540 .await
541 .unwrap();
542 task_mgr
543 .update_task(
544 current.id,
545 TaskUpdate {
546 status: Some("doing"),
547 ..Default::default()
548 },
549 )
550 .await
551 .unwrap();
552 workspace_mgr
553 .set_current_task(current.id, None)
554 .await
555 .unwrap();
556
557 let _sibling3 = task_mgr
558 .add_task(
559 "Sibling 3".to_string(),
560 None,
561 Some(parent.id),
562 None,
563 None,
564 None,
565 )
566 .await
567 .unwrap();
568
569 let _child1 = task_mgr
571 .add_task(
572 "Child 1".to_string(),
573 None,
574 Some(current.id),
575 None,
576 None,
577 None,
578 )
579 .await
580 .unwrap();
581 let _child2 = task_mgr
582 .add_task(
583 "Child 2".to_string(),
584 None,
585 Some(current.id),
586 None,
587 None,
588 None,
589 )
590 .await
591 .unwrap();
592
593 event_mgr
595 .add_event(current.id, "decision".to_string(), "Decision 1".to_string())
596 .await
597 .unwrap();
598 event_mgr
599 .add_event(current.id, "blocker".to_string(), "Blocker 1".to_string())
600 .await
601 .unwrap();
602 event_mgr
603 .add_event(current.id, "note".to_string(), "Note 1".to_string())
604 .await
605 .unwrap();
606
607 let restore_mgr = SessionRestoreManager::new(pool);
609 let result = restore_mgr.restore(3).await.unwrap();
610
611 assert_eq!(result.status, SessionStatus::Success);
613
614 let task = result.current_task.unwrap();
615 assert_eq!(task.id, current.id);
616 assert_eq!(task.spec, Some("Current spec".to_string()));
617
618 assert!(result.parent_task.is_some());
620 let parent_info = result.parent_task.unwrap();
621 assert_eq!(parent_info.id, parent.id);
622
623 assert!(result.siblings.is_some());
625 let siblings = result.siblings.unwrap();
626 assert_eq!(siblings.total, 3);
627 assert_eq!(siblings.done, 1);
628 assert_eq!(siblings.doing, 1);
629 assert_eq!(siblings.todo, 1);
630 assert_eq!(siblings.done_list.len(), 1);
631
632 assert!(result.children.is_some());
634 let children = result.children.unwrap();
635 assert_eq!(children.total, 2);
636 assert_eq!(children.todo, 2);
637
638 assert!(result.recent_events.is_some());
640 let events = result.recent_events.unwrap();
641 assert_eq!(events.len(), 3);
642
643 let event_types: Vec<&str> = events.iter().map(|e| e.event_type.as_str()).collect();
645 assert!(event_types.contains(&"decision"));
646 assert!(event_types.contains(&"blocker"));
647 assert!(event_types.contains(&"note"));
648 }
649
650 #[tokio::test]
651 async fn test_restore_with_spec_preview() {
652 let ctx = TestContext::new().await;
653 let pool = ctx.pool();
654
655 let task_mgr = TaskManager::new(pool);
656 let workspace_mgr = WorkspaceManager::new(pool);
657
658 let long_spec = "a".repeat(200);
660 let task = task_mgr
661 .add_task(
662 "Test task".to_string(),
663 Some(long_spec.to_string()),
664 None,
665 None,
666 None,
667 None,
668 )
669 .await
670 .unwrap();
671 workspace_mgr.set_current_task(task.id, None).await.unwrap();
672
673 let restore_mgr = SessionRestoreManager::new(pool);
675 let result = restore_mgr.restore(3).await.unwrap();
676
677 let current_task = result.current_task.unwrap();
679 assert_eq!(current_task.spec, Some(long_spec));
680 assert!(current_task.spec_preview.is_some());
681 let preview = current_task.spec_preview.unwrap();
682 assert_eq!(preview.len(), 103); assert!(preview.ends_with("..."));
684 }
685
686 #[tokio::test]
687 async fn test_restore_no_focus() {
688 let ctx = TestContext::new().await;
689 let pool = ctx.pool();
690
691 let task_mgr = TaskManager::new(pool);
692
693 task_mgr
695 .add_task("Task 1".to_string(), None, None, None, None, None)
696 .await
697 .unwrap();
698 task_mgr
699 .add_task("Task 2".to_string(), None, None, None, None, None)
700 .await
701 .unwrap();
702
703 let restore_mgr = SessionRestoreManager::new(pool);
705 let result = restore_mgr.restore(3).await.unwrap();
706
707 assert_eq!(result.status, SessionStatus::NoFocus);
709 assert!(result.current_task.is_none());
710 assert!(result.stats.is_some());
711
712 let stats = result.stats.unwrap();
713 assert_eq!(stats.total_tasks, 2);
714 assert_eq!(stats.todo, 2);
715
716 assert!(result.recommended_task.is_some());
719
720 assert!(result.top_pending_tasks.is_some());
722 let top_pending = result.top_pending_tasks.unwrap();
723 assert_eq!(top_pending.len(), 2);
724 }
725
726 #[tokio::test]
727 async fn test_restore_recent_events_limit() {
728 let ctx = TestContext::new().await;
729 let pool = ctx.pool();
730
731 let task_mgr = TaskManager::new(pool);
732 let event_mgr = EventManager::new(pool);
733 let workspace_mgr = WorkspaceManager::new(pool);
734
735 let task = task_mgr
737 .add_task("Test task".to_string(), None, None, None, None, None)
738 .await
739 .unwrap();
740 workspace_mgr.set_current_task(task.id, None).await.unwrap();
741
742 for i in 0..10 {
744 event_mgr
745 .add_event(task.id, "note".to_string(), format!("Event {}", i))
746 .await
747 .unwrap();
748 }
749
750 let restore_mgr = SessionRestoreManager::new(pool);
752 let result = restore_mgr.restore(3).await.unwrap();
753
754 let events = result.recent_events.unwrap();
756 assert_eq!(events.len(), 3);
757 }
758
759 #[tokio::test]
760 async fn test_restore_custom_events_limit() {
761 let ctx = TestContext::new().await;
762 let pool = ctx.pool();
763
764 let task_mgr = TaskManager::new(pool);
765 let event_mgr = EventManager::new(pool);
766 let workspace_mgr = WorkspaceManager::new(pool);
767
768 let task = task_mgr
770 .add_task("Test task".to_string(), None, None, None, None, None)
771 .await
772 .unwrap();
773 workspace_mgr.set_current_task(task.id, None).await.unwrap();
774
775 for i in 0..10 {
777 event_mgr
778 .add_event(task.id, "note".to_string(), format!("Event {}", i))
779 .await
780 .unwrap();
781 }
782
783 let restore_mgr = SessionRestoreManager::new(pool);
785 let result = restore_mgr.restore(5).await.unwrap();
786
787 let events = result.recent_events.unwrap();
789 assert_eq!(events.len(), 5);
790 }
791
792 #[test]
793 fn test_suggest_commands_with_children() {
794 let current_task = CurrentTaskInfo {
795 id: 1,
796 name: "Test".to_string(),
797 status: "doing".to_string(),
798 spec: None,
799 spec_preview: None,
800 created_at: None,
801 first_doing_at: None,
802 };
803
804 let children = Some(ChildrenInfo {
805 total: 2,
806 todo: 1,
807 list: vec![],
808 });
809
810 let commands = SessionRestoreManager::suggest_commands(¤t_task, children.as_ref());
811
812 assert!(!commands.is_empty());
813 assert!(commands.iter().any(|c| c.contains("blocker")));
814 }
815
816 #[test]
817 fn test_error_result_workspace_not_found() {
818 let result = SessionRestoreManager::error_result(
819 Some("/test/path".to_string()),
820 ErrorType::WorkspaceNotFound,
821 "Test message",
822 "Test recovery",
823 );
824
825 assert_eq!(result.status, SessionStatus::Error);
826 assert_eq!(result.error_type, Some(ErrorType::WorkspaceNotFound));
827 assert_eq!(result.message, Some("Test message".to_string()));
828 assert_eq!(
829 result.recovery_suggestion,
830 Some("Test recovery".to_string())
831 );
832 assert!(result.suggested_commands.is_some());
833
834 let commands = result.suggested_commands.unwrap();
835 assert!(commands.iter().any(|c| c.contains("init")));
836 }
837
838 #[test]
839 fn test_build_siblings_info_empty() {
840 let siblings: Vec<crate::db::models::Task> = vec![];
841 let result = SessionRestoreManager::build_siblings_info(&siblings);
842 assert!(result.is_none());
843 }
844
845 #[test]
846 fn test_build_children_info_empty() {
847 let children: Vec<crate::db::models::Task> = vec![];
848 let result = SessionRestoreManager::build_children_info(&children);
849 assert!(result.is_none());
850 }
851}