Skip to main content

agm_core/agent/
session.rs

1use 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}