1use std::fmt::Display;
2use std::process::Command;
3use std::process::Stdio;
4
5use agm_core::agent::Agent;
6use agm_core::agent::session::Session;
7use owo_colors::OwoColorize as _;
8use rootcause::prelude::ResultExt as _;
9use strum::EnumIter;
10use strum::IntoEnumIterator as _;
11
12pub fn run() -> rootcause::Result<()> {
13 let mut sessions = Vec::new();
14
15 sessions.extend(agm_core::agent::session_loader::claude::load_sessions()?);
16 sessions.extend(agm_core::agent::session_loader::codex::load_sessions()?);
17 sessions.extend(agm_core::agent::session_loader::cursor::load_sessions()?);
18
19 sessions.sort_by(|a, b| {
20 b.updated_at
21 .cmp(&a.updated_at)
22 .then_with(|| b.created_at.cmp(&a.created_at))
23 .then_with(|| a.name.cmp(&b.name))
24 .then_with(|| a.id.cmp(&b.id))
25 });
26
27 if sessions.is_empty() {
28 println!("No sessions");
29 return Ok(());
30 }
31
32 let Some(selected) = ytil_tui::minimal_multi_select(sessions.into_iter().map(RenderableSession).collect())? else {
33 println!("No sessions selected");
34 return Ok(());
35 };
36
37 let Some(op) = ytil_tui::minimal_select::<Op>(Op::iter().collect())? else {
38 println!("No action selected");
39 return Ok(());
40 };
41
42 match op {
43 Op::Resume => ytil_tui::require_single(&selected, "sessions").and_then(launch_session),
44 Op::Delete => {
45 for session in &selected {
46 delete_session(session)?;
47 }
48 Ok(())
49 }
50 }
51}
52
53struct RenderableSession(Session);
54
55impl Display for RenderableSession {
56 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57 let agent_name = match self.0.agent {
58 Agent::Claude => "CLAUDE".red().bold().to_string(),
59 Agent::Codex => "CODEX".green().bold().to_string(),
60 Agent::Cursor => "CURSOR".bright_black().bold().to_string(),
61 Agent::Gemini | Agent::Opencode => self.0.agent.to_string(),
62 };
63
64 let path_label = agm_core::short_path(
65 &self.0.workspace,
66 std::env::var_os("HOME")
67 .as_deref()
68 .map_or_else(|| std::path::Path::new("/"), std::path::Path::new),
69 );
70 let session_name = ytil_tui::display_fixed_width(&self.0.name, 42);
71 let updated_label = self.0.updated_at.format("%d/%m/%Y-%H:%M").to_string();
72 let created_label = self.0.created_at.format("%d/%m/%Y-%H:%M").to_string();
73
74 write!(
75 f,
76 "{agent_name} {} {} {} {}",
77 path_label.blue(),
78 session_name.white().bold(),
79 updated_label.dimmed(),
80 created_label.dimmed(),
81 )
82 }
83}
84
85#[derive(Debug, EnumIter)]
86enum Op {
87 Resume,
88 Delete,
89}
90
91impl Display for Op {
92 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
93 match self {
94 Self::Resume => write!(f, "{}", "Resume".green().bold()),
95 Self::Delete => write!(f, "{}", "Delete".red().bold()),
96 }
97 }
98}
99
100fn launch_session(RenderableSession(session): &RenderableSession) -> rootcause::Result<()> {
101 let (program, args) = session.build_resume_command()?;
102
103 let mut cmd = Command::new(program);
104 cmd.args(args);
105 let status = cmd
106 .current_dir(&session.workspace)
107 .stdin(Stdio::inherit())
108 .stdout(Stdio::inherit())
109 .stderr(Stdio::inherit())
110 .status()
111 .context("failed to launch agent CLI")
112 .attach_with(|| format!("agent={}", session.agent.name()))
113 .attach_with(|| format!("workspace={}", session.workspace.display()))
114 .attach_with(|| format!("session_id={}", session.id))?;
115
116 status
117 .exit_ok()
118 .context("agent CLI exited with non-zero status")
119 .attach_with(|| format!("agent={}", session.agent.name()))
120 .attach_with(|| format!("workspace={}", session.workspace.display()))
121 .attach_with(|| format!("session_id={}", session.id))?;
122
123 Ok(())
124}
125
126fn delete_session(session: &RenderableSession) -> rootcause::Result<()> {
127 let delete_path = &session.0.path;
128 if delete_path.is_dir() {
129 std::fs::remove_dir_all(delete_path)
130 .context("failed to delete session directory")
131 .attach_with(|| format!("path={}", delete_path.display()))
132 .attach_with(|| format!("session_id={}", session.0.id))?;
133 } else {
134 std::fs::remove_file(delete_path)
135 .context("failed to delete session file")
136 .attach_with(|| format!("path={}", delete_path.display()))
137 .attach_with(|| format!("session_id={}", session.0.id))?;
138 }
139 println!("{} {session}", "Deleted".red().bold());
140 Ok(())
141}