agent-berth 1.2.0

Monitor and resume coding agents
Documentation
use std::io::{self, Write};

use anyhow::Result;

use crate::db;
use crate::paths::{self, Context};
use crate::store::ListedSession;

pub fn run(
    ctx: &Context,
    json: bool,
    resumable: bool,
    idle: Option<std::time::Duration>,
    here: bool,
) -> Result<()> {
    let mut sessions = db::query_sessions(ctx, resumable, idle)?;
    if here {
        sessions.retain(|session| session.cwd.as_deref().is_some_and(paths::is_current_dir));
    }
    if json {
        println!("{}", serde_json::to_string_pretty(&sessions)?);
        return Ok(());
    }
    if sessions.is_empty() {
        if resumable {
            println!("No resumable sessions.");
        } else {
            println!("No active sessions.");
        }
        return Ok(());
    }
    print_table(&sessions)?;
    Ok(())
}

fn print_table(sessions: &[ListedSession]) -> Result<()> {
    let headers = [
        "PROVIDER", "STATUS", "SOURCE", "SESSION", "TITLE", "CWD", "PID", "AGE",
    ];
    let rows: Vec<[String; 8]> = sessions
        .iter()
        .map(|session| {
            [
                session.provider.clone(),
                session.status.as_str().to_string(),
                session.source.as_str().to_string(),
                session.session_id.clone(),
                truncate(session.title.as_deref().unwrap_or("-"), 40),
                session.cwd.clone().unwrap_or_else(|| "-".into()),
                session
                    .pid
                    .map(|pid| pid.to_string())
                    .unwrap_or_else(|| "-".into()),
                age_label(session.last_report_ms),
            ]
        })
        .collect();
    let mut widths: Vec<usize> = headers.iter().map(|h| h.len()).collect();
    for row in &rows {
        for (i, cell) in row.iter().enumerate() {
            widths[i] = widths[i].max(cell.len());
        }
    }
    let mut out = io::stdout().lock();
    for (i, header) in headers.iter().enumerate() {
        if i > 0 {
            write!(out, "  ")?;
        }
        write!(out, "{header:<width$}", width = widths[i])?;
    }
    writeln!(out)?;
    for row in rows {
        for (i, cell) in row.iter().enumerate() {
            if i > 0 {
                write!(out, "  ")?;
            }
            write!(out, "{cell:<width$}", width = widths[i])?;
        }
        writeln!(out)?;
    }
    Ok(())
}

pub(crate) fn truncate(text: &str, max: usize) -> String {
    let count = text.chars().count();
    if count <= max {
        return text.to_string();
    }
    let mut out: String = text.chars().take(max.saturating_sub(1)).collect();
    out.push('');
    out
}

pub(crate) fn age_label(last_report_ms: u64) -> String {
    let now = crate::store::now_ms();
    let secs = now.saturating_sub(last_report_ms) / 1000;
    if secs < 60 {
        format!("{secs}s")
    } else if secs < 3600 {
        format!("{}m", secs / 60)
    } else if secs < 86400 {
        format!("{}h", secs / 3600)
    } else {
        format!("{}d", secs / 86400)
    }
}