Skip to main content

tmx/agents/
mod.rs

1//! Agent dashboard — shows which Claude/Codex agents are running in which
2//! tmux panes, with cwd, task, and review/intent status.
3//!
4//! Data sources are all local: tmux itself + `~/.claude/state/`. No daemon.
5//! Identity anchor is the tmux pane PID; cwd is for state-marker matching
6//! and display, never for identity. See `state.rs` for the matching logic.
7
8pub mod attention;
9pub mod classify;
10pub mod collector;
11pub mod panes;
12pub mod proc;
13pub mod session_meta;
14pub mod state;
15pub mod ui;
16
17use std::path::PathBuf;
18use std::time::SystemTime;
19
20#[derive(Debug, Clone)]
21pub struct Agent {
22    /// Stable identity used as the cursor anchor across snapshot refreshes.
23    /// `pane:<target>` for tmux panes, `codex:<job_id>` for background jobs.
24    pub id: String,
25    pub pane: Option<PaneLocator>,
26    pub kind: AgentKind,
27    pub status: Status,
28    pub cwd: PathBuf,
29    pub repo_name: String,
30    pub flags: Flags,
31    pub extra: String,
32}
33
34#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct PaneLocator {
36    pub session: String,
37    pub window: u32,
38    pub pane: u32,
39    pub pane_pid: u32,
40}
41
42impl PaneLocator {
43    pub fn target(&self) -> String {
44        format!("{}:{}.{}", self.session, self.window, self.pane)
45    }
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq)]
49pub enum AgentKind {
50    Claude,
51    Codex,
52    Shell,
53    Other,
54}
55
56impl AgentKind {
57    pub fn from_command(cmd: &str) -> Self {
58        match cmd {
59            "claude" => Self::Claude,
60            "codex" => Self::Codex,
61            "zsh" | "bash" | "fish" | "sh" => Self::Shell,
62            _ => Self::Other,
63        }
64    }
65
66    pub fn label(self) -> &'static str {
67        match self {
68            Self::Claude => "claude",
69            Self::Codex => "codex",
70            Self::Shell => "shell",
71            Self::Other => "other",
72        }
73    }
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub enum Status {
78    /// Claude/Codex is mid-turn (running tools or composing). No input
79    /// from the user is needed.
80    Working,
81    /// Claude/Codex is parked at the chat prompt. User can type a new
82    /// request whenever.
83    Ready,
84    /// Claude/Codex is blocking on a selection/permission dialog and
85    /// will not progress until the user acts.
86    AwaitingDecision,
87    /// Pane has no recognised Claude/Codex UI (plain shell, nvim, etc.).
88    Idle,
89    /// Codex-companion background job (no associated tmux pane).
90    Background,
91}
92
93impl Status {
94    pub fn glyph(self) -> &'static str {
95        match self {
96            Self::Working => "●",
97            Self::Ready => "◐",
98            Self::AwaitingDecision => "⚠",
99            Self::Idle => "○",
100            Self::Background => "▷",
101        }
102    }
103
104    pub fn label(self) -> &'static str {
105        match self {
106            Self::Working => "working",
107            Self::Ready => "ready",
108            Self::AwaitingDecision => "decision",
109            Self::Idle => "idle",
110            Self::Background => "bg",
111        }
112    }
113}
114
115#[derive(Debug, Clone, Default, PartialEq, Eq)]
116pub struct Flags {
117    pub has_intent: bool,
118    pub blocked: bool,
119    pub reviewed_fresh: bool,
120}
121
122#[derive(Debug, Clone)]
123pub struct Snapshot {
124    pub agents: Vec<Agent>,
125    pub captured_at: SystemTime,
126    /// Total blocked sessions in `~/.claude/state/` — not always pane-mappable.
127    pub global_blocked: usize,
128    /// Non-fatal error from the most recent `tmux list-panes` call, if any.
129    /// Surfaced as a banner so the user doesn't mistake "tmux is broken" for
130    /// "nothing is running."
131    pub panes_error: Option<String>,
132    /// Cross-tool attention queue (Claude stop / notification / codex turn
133    /// hooks). Newest first. Pre-filtered to the same 1h cutoff
134    /// `attention-picker.sh` uses so the two surfaces agree on what's pending.
135    pub attention: Vec<attention::AttentionEntry>,
136}
137
138impl Snapshot {
139    pub fn empty() -> Self {
140        Self {
141            agents: Vec::new(),
142            captured_at: SystemTime::now(),
143            global_blocked: 0,
144            panes_error: None,
145            attention: Vec::new(),
146        }
147    }
148}
149
150pub fn run() -> anyhow::Result<()> {
151    ui::run()
152}