//! sessions: the registry of every agent and REPL session. use super::*; use crate::output::{print_json, status_style, Table}; use crate::sessions::{self, SessionRecord}; use serde::Serialize; #[derive(Serialize)] struct SessionRow { id: String, kind: String, agent: Option, status: String, pid: Option, cwd: Option, project: Option, started_at: String, ended_at: Option, duration_s: Option, exit_code: Option, log: Option, } fn row_of(r: &SessionRecord) -> SessionRow { SessionRow { id: r.id.clone(), kind: r.kind.clone(), agent: r.agent.clone(), status: r.status.clone(), pid: r.pid, cwd: r.cwd.clone(), project: r.project.clone(), started_at: r.started_at.clone(), ended_at: r.ended_at.clone(), duration_s: sessions::duration_s(r), exit_code: r.exit_code, log: r.log.clone(), } } pub fn run( app: &App, agent: Option<&str>, project: Option<&str>, status: Option<&str>, show: Option<&str>, ) -> Result { if let Some(id) = show { return show_one(app, id); } sessions::reconcile(app)?; let sf = sessions::load(app)?; let mut rows: Vec = sf .sessions .values() .filter(|r| agent.map_or(true, |a| r.agent.as_deref() == Some(a))) .filter(|r| project.map_or(true, |p| r.project.as_deref() == Some(p))) .filter(|r| status.map_or(true, |s| r.status == s)) .map(row_of) .collect(); rows.sort_by(|a, b| b.started_at.cmp(&a.started_at)); if app.json() { print_json(&rows); return Ok(0); } let color = app.color(); let theme = app.theme(); let mut table = Table::new(vec!["SESSION", "AGENT", "STATUS", "DURATION", "EXIT"]); for r in &rows { table.row(vec![ short_id(&r.id), r.agent .clone() .unwrap_or_else(|| format!("({})", r.kind)), status_style(&r.status, theme, color), r.duration_s .map(format_duration) .unwrap_or_else(|| "—".to_string()), r.exit_code .map(|c| c.to_string()) .unwrap_or_else(|| "—".to_string()), ]); } print!("{}", table.render()); if rows.is_empty() { app.log .info("no sessions recorded yet — 'am start --background' or run 'am'"); } Ok(0) } fn show_one(app: &App, id: &str) -> Result { sessions::reconcile(app)?; let sf = sessions::load(app)?; let Some(r) = sf.sessions.get(id).cloned() else { bail!("unknown session '{id}' — 'am sessions' lists every session"); }; if app.json() { print_json(&row_of(&r)); return Ok(0); } println!("session {}", r.id); println!(" kind: {}", r.kind); println!(" agent: {}", r.agent.as_deref().unwrap_or("—")); println!(" status: {}", r.status); println!(" pid: {}", r.pid.map(|p| p.to_string()).unwrap_or_else(|| "—".to_string())); println!(" cwd: {}", r.cwd.as_deref().unwrap_or("—")); println!(" project: {}", r.project.as_deref().unwrap_or("—")); println!(" started: {}", r.started_at); println!(" ended: {}", r.ended_at.as_deref().unwrap_or("—")); if let Some(d) = sessions::duration_s(&r) { println!(" duration: {}", format_duration(d)); } if let Some(c) = r.exit_code { println!(" exit code: {c}"); } if let Some(l) = &r.log { let p = std::path::PathBuf::from(l); if p.exists() { println!(" log: {l}"); let tail = crate::process::last_lines(&p, 8); if !tail.is_empty() { println!(" --- log excerpt ---"); println!("{tail}"); } } else { println!(" log: {l} (missing)"); } } Ok(0) } /// "20260815_143926_a1b2c3" -> "20260815_143926" fn short_id(id: &str) -> String { id.chars().take(15).collect() } fn format_duration(secs: u64) -> String { if secs < 60 { format!("{secs}s") } else if secs < 3600 { format!("{}m{:02}s", secs / 60, secs % 60) } else { format!("{}h{:02}m", secs / 3600, (secs % 3600) / 60) } }