147 lines
4.3 KiB
Rust
147 lines
4.3 KiB
Rust
//! 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<String>,
|
|
status: String,
|
|
pid: Option<u32>,
|
|
cwd: Option<String>,
|
|
project: Option<String>,
|
|
started_at: String,
|
|
ended_at: Option<String>,
|
|
duration_s: Option<u64>,
|
|
exit_code: Option<i32>,
|
|
log: Option<String>,
|
|
}
|
|
|
|
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<i32> {
|
|
if let Some(id) = show {
|
|
return show_one(app, id);
|
|
}
|
|
sessions::reconcile(app)?;
|
|
let sf = sessions::load(app)?;
|
|
let mut rows: Vec<SessionRow> = 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 <agent> --background' or run 'am'");
|
|
}
|
|
Ok(0)
|
|
}
|
|
|
|
fn show_one(app: &App, id: &str) -> Result<i32> {
|
|
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)
|
|
}
|
|
}
|