agm_core/agent/
session.rs1use std::path::PathBuf;
2
3use chrono::DateTime;
4use chrono::Utc;
5use rootcause::option_ext::OptionExt as _;
6use rootcause::report;
7
8use crate::agent::Agent;
9
10#[derive(Clone, Debug, Eq, PartialEq)]
11pub struct Session {
12 pub id: String,
13 pub agent: Agent,
14 pub name: String,
15 pub workspace: PathBuf,
16 pub path: PathBuf,
17 pub created_at: DateTime<Utc>,
18 pub updated_at: DateTime<Utc>,
19}
20
21impl Session {
22 pub fn new(
23 agent: Agent,
24 session_id: String,
25 workspace_dir: PathBuf,
26 name: Option<String>,
27 created_at: DateTime<Utc>,
28 ) -> Self {
29 let name = name.filter(|name| !name.trim().is_empty()).unwrap_or_else(|| {
30 workspace_dir
31 .file_name()
32 .and_then(|name| name.to_str())
33 .filter(|name| !name.is_empty())
34 .map_or_else(|| session_id.clone(), str::to_owned)
35 });
36
37 Self {
38 id: session_id,
39 agent,
40 name,
41 workspace: workspace_dir,
42 path: PathBuf::new(),
43 created_at,
44 updated_at: created_at,
45 }
46 }
47
48 pub fn build_resume_command(&self) -> rootcause::Result<(&'static str, Vec<String>)> {
49 let workspace = self.workspace.to_str().context("non-utf8 workspace dir".to_owned())?;
50 match self.agent {
51 Agent::Claude => Ok(("claude", vec!["--resume".into(), self.id.clone()])),
52 Agent::Codex => Ok((
53 "codex",
54 vec![
55 "resume".into(),
56 self.id.clone(),
57 "--no-alt-screen".into(),
58 "--cd".into(),
59 workspace.into(),
60 ],
61 )),
62 Agent::Cursor => Ok((
63 "cursor-agent",
64 vec![
65 "--resume".into(),
66 self.id.clone(),
67 "--workspace".into(),
68 workspace.into(),
69 ],
70 )),
71 Agent::Gemini | Agent::Opencode => {
72 Err(report!("resume is not supported for this agent").attach(format!("agent={}", self.agent)))
73 }
74 }
75 }
76}
77
78#[cfg(test)]
79mod tests {
80 use chrono::DateTime;
81 use tempfile::tempdir;
82
83 use super::*;
84
85 #[test]
86 fn test_build_resume_command_matches_agent() {
87 let tempdir = tempdir().unwrap();
88 let workspace = tempdir.path().join("workspace");
89 std::fs::create_dir_all(&workspace).unwrap();
90
91 let claude = Session {
92 agent: Agent::Claude,
93 id: "session-id".into(),
94 workspace: workspace.clone(),
95 name: "session-name".into(),
96 path: PathBuf::new(),
97 created_at: DateTime::from_timestamp_millis(1).unwrap().to_utc(),
98 updated_at: DateTime::from_timestamp_millis(1).unwrap().to_utc(),
99 };
100 let codex = Session {
101 agent: Agent::Codex,
102 ..claude.clone()
103 };
104 let cursor = Session {
105 agent: Agent::Cursor,
106 ..claude.clone()
107 };
108
109 assert2::assert!(let Ok((_, claude_args)) = claude.build_resume_command());
110 pretty_assertions::assert_eq!(claude_args, vec!["--resume".to_owned(), "session-id".to_owned()]);
111 assert2::assert!(let Some(workspace_str) = workspace.to_str());
112 assert2::assert!(let Ok((_, codex_args)) = codex.build_resume_command());
113 pretty_assertions::assert_eq!(
114 codex_args,
115 vec![
116 "resume".to_owned(),
117 "session-id".to_owned(),
118 "--no-alt-screen".to_owned(),
119 "--cd".to_owned(),
120 workspace_str.to_owned(),
121 ]
122 );
123 assert2::assert!(let Ok((_, cursor_args)) = cursor.build_resume_command());
124 pretty_assertions::assert_eq!(
125 cursor_args,
126 vec![
127 "--resume".to_owned(),
128 "session-id".to_owned(),
129 "--workspace".to_owned(),
130 workspace_str.to_owned(),
131 ]
132 );
133 }
134}