pub mod claude;
pub mod codex;
pub mod process;
pub mod rate_limit;
pub use claude::ClaudeCollector;
pub use codex::CodexCollector;
pub use rate_limit::read_rate_limits;
use crate::model::{AgentSession, OrphanPort, SessionStatus};
use std::collections::HashMap;
pub struct SharedProcessData {
pub process_info: HashMap<u32, process::ProcInfo>,
pub children_map: HashMap<u32, Vec<u32>>,
pub ports: HashMap<u32, Vec<u16>>,
}
impl SharedProcessData {
pub fn fetch(cached_ports: Option<&HashMap<u32, Vec<u16>>>) -> Self {
let process_info = process::get_process_info();
let children_map = process::get_children_map(&process_info);
let ports = match cached_ports {
Some(p) => p.clone(),
None => process::get_listening_ports(),
};
Self { process_info, children_map, ports }
}
}
#[derive(Clone)]
struct TrackedPortChild {
port: u16,
command: String,
project_name: String,
}
pub struct MultiCollector {
claude: ClaudeCollector,
codex: CodexCollector,
tick_count: u32,
cached_ports: HashMap<u32, Vec<u16>>,
cached_port_pids: Vec<u32>,
cached_git: HashMap<String, (u32, u32)>,
tracked_port_children: HashMap<u32, TrackedPortChild>,
pub orphan_ports: Vec<OrphanPort>,
}
const SLOW_POLL_INTERVAL: u32 = 5;
impl MultiCollector {
pub fn new() -> Self {
Self {
claude: ClaudeCollector::new(),
codex: CodexCollector::new(),
tick_count: SLOW_POLL_INTERVAL, cached_ports: HashMap::new(),
cached_port_pids: Vec::new(),
cached_git: HashMap::new(),
tracked_port_children: HashMap::new(),
orphan_ports: Vec::new(),
}
}
pub fn codex_rate_limit(&self) -> Option<&crate::model::RateLimitInfo> {
self.codex.last_rate_limit.as_ref()
}
pub fn collect(&mut self) -> Vec<AgentSession> {
let slow_tick = self.tick_count >= SLOW_POLL_INTERVAL;
if slow_tick {
self.tick_count = 0;
}
self.tick_count += 1;
let fresh_process = SharedProcessData::fetch(Some(&self.cached_ports));
let mut current_pids: Vec<u32> = fresh_process.process_info.keys().copied().collect();
current_pids.sort_unstable();
let pids_changed = current_pids != self.cached_port_pids;
let shared = if slow_tick || pids_changed {
let s = SharedProcessData::fetch(None);
self.cached_ports = s.ports.clone();
self.cached_port_pids = current_pids;
s
} else {
fresh_process
};
let mut all = Vec::new();
all.extend(self.claude.collect(&shared));
all.extend(self.codex.collect(&shared));
if slow_tick {
self.cached_git.clear();
for s in &mut all {
let stats = process::collect_git_stats(&s.cwd);
self.cached_git.insert(s.cwd.clone(), stats);
s.git_added = stats.0;
s.git_modified = stats.1;
}
} else {
for s in &mut all {
if let Some(&(added, modified)) = self.cached_git.get(&s.cwd) {
s.git_added = added;
s.git_modified = modified;
} else {
let stats = process::collect_git_stats(&s.cwd);
self.cached_git.insert(s.cwd.clone(), stats);
s.git_added = stats.0;
s.git_modified = stats.1;
}
}
}
all.retain(|s| !(matches!(s.status, SessionStatus::Done) && s.pid == 0));
all.sort_by_key(|s| std::cmp::Reverse(s.started_at));
let mut live_child_pids = std::collections::HashSet::new();
for s in &all {
if !matches!(s.status, SessionStatus::Done) {
for child in &s.children {
live_child_pids.insert(child.pid);
if let Some(port) = child.port {
self.tracked_port_children.insert(child.pid, TrackedPortChild {
port,
command: child.command.clone(),
project_name: s.project_name.clone(),
});
}
}
}
}
self.orphan_ports.clear();
let mut stale_pids = Vec::new();
for (pid, tracked) in &self.tracked_port_children {
if live_child_pids.contains(pid) {
continue; }
let still_listening = shared.ports.get(pid)
.is_some_and(|ports| ports.contains(&tracked.port));
let still_alive = shared.process_info.contains_key(pid);
if still_alive && still_listening {
self.orphan_ports.push(OrphanPort {
port: tracked.port,
pid: *pid,
command: tracked.command.clone(),
project_name: tracked.project_name.clone(),
});
} else {
stale_pids.push(*pid);
}
}
for pid in stale_pids {
self.tracked_port_children.remove(&pid);
}
self.orphan_ports.sort_by_key(|o| o.port);
all
}
}