agtop 2.3.1

Terminal UI for monitoring AI coding agents (Claude Code, Codex, Aider, Cursor, Gemini, Goose, ...) — like top, but for agents.
// Data shapes shared by the collector, the TUI, and the JSON output path.

use serde::Serialize;
use std::collections::HashMap;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum Status {
    Busy,
    Spawning,
    Active,
    Idle,
    Waiting,
    Completed,
    Stale,
}

impl Status {
    pub fn rank(self) -> u8 {
        match self {
            Status::Busy => 0,
            Status::Spawning => 1,
            Status::Active => 2,
            Status::Idle => 3,
            Status::Waiting => 4,
            Status::Completed => 5,
            Status::Stale => 6,
        }
    }
    pub fn label(self) -> &'static str {
        match self {
            Status::Busy => "BUSY",
            Status::Spawning => "SPWN",
            Status::Active => "ACTV",
            Status::Idle => "idle",
            Status::Waiting => "WAIT",
            Status::Completed => "DONE",
            Status::Stale => "stale",
        }
    }
    pub fn glyph(self) -> &'static str {
        match self {
            Status::Busy | Status::Active => "",
            Status::Spawning => "",
            Status::Idle => "",
            Status::Waiting => "",
            Status::Completed => "",
            Status::Stale => "·",
        }
    }
}

#[derive(Debug, Clone, Serialize)]
pub struct Agent {
    pub pid: u32,
    pub label: String,
    pub status: Status,
    pub project: String,
    pub current_tool: Option<String>,
    pub current_task: Option<String>,
    pub subagents: u32,
    /// Human-readable descriptions of in-flight Task / Agent tool calls
    /// (e.g. "code-reviewer: review the auth refactor").  Populated by the
    /// session reader; one entry per element in `subagents`.
    pub in_flight_subagents: Vec<String>,
    /// Last ~6–10 readable events from the session transcript, oldest →
    /// newest.  Each line is already prefixed with one of:
    ///   `› `  user / assistant prose
    ///   `→ `  tool call
    ///   `← `  tool result
    /// Surfaced in the detail popup as a live-preview box.
    pub recent_activity: Vec<String>,
    pub session_id: Option<String>,
    pub session_age_ms: Option<u64>,
    /// Sum of input + output (+ cache) tokens charged to this agent's session.
    pub tokens_total: u64,
    /// Total input bucket: standard_input + cache_read + cache_creation.
    /// (Use `tokens_input - tokens_cache_read - tokens_cache_write` if
    /// you specifically want the *uncached* portion.)
    pub tokens_input: u64,
    pub tokens_output: u64,
    /// Cache-read tokens, charged at ~10% of the standard input rate
    /// under Anthropic prompt-caching pricing.  Tracked separately so
    /// `cost_usd` applies the discount instead of over-billing cache
    /// hits at the full input rate.
    #[serde(default)]
    pub tokens_cache_read: u64,
    /// Cache-creation / cache-write tokens, charged at ~125% of the
    /// standard input rate.
    #[serde(default)]
    pub tokens_cache_write: u64,
    pub cost_usd: f64,
    /// How `cost_usd` was computed.  One of:
    ///   - "api"     — known per-token rate looked up in the price table
    ///   - "local"   — model runs on user's hardware (Ollama / vLLM / llama.cpp); $0
    ///   - "unknown" — no model name, or no price-table match (treated as $0 but flagged)
    pub cost_basis: String,
    pub model: Option<String>,
    /// Latest-turn input window size in tokens (input_tokens +
    /// cache_read_input_tokens for Anthropic, prompt_tokens for
    /// OpenAI / Codex).  Drives the per-agent context-fill bar in the
    /// detail popup so the user can see how close they are to
    /// auto-compaction.  0 if unknown.
    pub context_used: u64,
    /// Maximum input window in tokens for this agent's model, looked
    /// up via the bundled price table (LiteLLM-derived).  0 if unknown.
    pub context_limit: u64,
    /// Names of Claude Code agent skills loaded for this session.
    /// Populated by scanning `~/.claude/skills/<name>/SKILL.md` plus
    /// `<cwd>/.claude/skills/<name>/SKILL.md`.  Empty for non-Claude
    /// vendors.
    pub loaded_skills: Vec<String>,
    /// Process is running with elevated / unsafe permissions
    /// (e.g. `claude --dangerously-skip-permissions`, `--yolo`, `--no-permissions`).
    /// The TUI surfaces this as a pulsating "GOD" tag on the row.
    pub dangerous: bool,
    /// Recent CPU% samples for the inline sparkline (oldest → newest).
    pub cpu_history: Vec<f64>,
    pub cpu: f64,
    pub cpu_raw: f64,
    pub rss: u64,
    pub vsize: u64,
    pub threads: u64,
    pub state: String,
    pub ppid: u32,
    pub uptime_sec: u64,
    pub cwd: String,
    pub exe: String,
    pub cmdline: String,
    pub read_bytes: u64,
    pub write_bytes: u64,
    pub writing_files: Vec<String>,
    pub writing_dirs: Vec<String>,
}

#[derive(Debug, Clone, Default, Serialize)]
pub struct ProjectAgg {
    pub project: String,
    pub agents: u32,
    pub cpu: f64,
    pub rss: u64,
    pub subagents: u32,
    pub tokens_total: u64,
    pub cost_usd: f64,
    pub statuses: HashMap<&'static str, u32>,
    pub cwd: String,
}

#[derive(Debug, Clone, Serialize)]
pub struct Session {
    pub id: String,
    pub project: String,
    pub project_short: String,
    pub file: String,
    pub size_bytes: u64,
    pub mtime_ms: u64,
    pub age_ms: u64,
    pub status: Status,
    pub stop_reason: Option<String>,
    pub last_task: Option<String>,
    pub last_tool: Option<String>,
    pub current_tool: Option<String>,
    pub in_flight_tasks: u32,
    pub in_flight_subagents: Vec<String>,
    pub recent_activity: Vec<String>,
    pub live_pid: Option<u32>,
    pub is_most_recent: bool,
    pub tokens_total: u64,
    pub tokens_input: u64,
    pub tokens_output: u64,
    /// See `Agent::tokens_cache_read`.
    #[serde(default)]
    pub tokens_cache_read: u64,
    /// See `Agent::tokens_cache_write`.
    #[serde(default)]
    pub tokens_cache_write: u64,
    pub cost_usd: f64,
    pub model: Option<String>,
    /// Latest-turn input window size (see `Agent::context_used`).
    pub context_used: u64,
}

#[derive(Debug, Clone, Default, Serialize)]
pub struct Sessions {
    pub sessions: Vec<Session>,
    pub recent_tasks: Vec<RecentTask>,
    pub active: u32,
    pub busy: u32,
    pub waiting: u32,
    pub completed: u32,
}

#[derive(Debug, Clone, Serialize)]
pub struct RecentTask {
    pub project: String,
    pub project_short: String,
    pub task: String,
    pub mtime_ms: u64,
    pub status: Status,
}

#[derive(Debug, Clone, Serialize)]
pub struct ActivityEvent {
    pub t: u64,
    pub kind: ActivityKind,
    pub label: String,
    pub pid: u32,
    pub cwd: Option<String>,
}

#[derive(Debug, Clone, Copy, Serialize)]
#[serde(rename_all = "lowercase")]
pub enum ActivityKind {
    Spawn,
    Exit,
}

#[derive(Debug, Clone, Default, Serialize)]
pub struct Aggregates {
    pub cpu: f64,
    pub mem_bytes: u64,
    pub active: u32,
    pub busy: u32,
    pub waiting: u32,
    pub completed: u32,
    pub subagents: u32,
    pub project_count: u32,
    pub tokens_total: u64,
    pub tokens_input: u64,
    pub tokens_output: u64,
    pub cost_usd: f64,
}

#[derive(Debug, Clone, Default, Serialize)]
pub struct History {
    pub total: Vec<f64>,
    pub active: Vec<f64>,
    pub busy: Vec<f64>,
    pub cpu: Vec<f64>,
    pub mem: Vec<f64>,
    /// Per-tick *delta* in cumulative tokens — gives a true activity pulse.
    pub tokens_rate: Vec<f64>,
}

#[derive(Debug, Clone, Default, Serialize)]
pub struct Snapshot {
    pub now: u64,
    pub platform: String,
    pub note: Option<String>,
    pub sys_cpus: u32,
    pub mem_total: u64,
    pub mem_available: u64,
    pub aggregates: Aggregates,
    pub agents: Vec<Agent>,
    pub projects: Vec<ProjectAgg>,
    pub sessions: Sessions,
    pub history: History,
    pub activity: Vec<ActivityEvent>,
}