use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::RwLock;
use tracing::debug;
use crate::agent::detect::{self, Debounce};
use crate::agent::process;
use crate::agent::state::{AgentKind, AgentState};
use crate::engine::EngineRegistry;
pub const TICK_INTERVAL: Duration = Duration::from_secs(1);
#[derive(Debug, Clone, serde::Serialize)]
pub struct ScreenAgent {
pub kind: AgentKind,
pub state: AgentState,
}
struct Entry {
kind: AgentKind,
debounce: Debounce,
last_activity: String,
}
#[derive(Clone, Default)]
pub struct AgentWatcher {
inner: Arc<RwLock<HashMap<String, Entry>>>,
}
impl AgentWatcher {
pub async fn snapshot(&self) -> HashMap<String, ScreenAgent> {
self.inner
.read()
.await
.iter()
.map(|(name, e)| {
(name.clone(), ScreenAgent { kind: e.kind, state: e.debounce.published() })
})
.collect()
}
}
pub fn spawn(watcher: AgentWatcher, engines: EngineRegistry) {
tokio::spawn(async move {
loop {
tick(&watcher, &engines).await;
tokio::time::sleep(TICK_INTERVAL).await;
}
});
}
fn identify_agent(pane_pid: u32) -> Option<AgentKind> {
if let Some(kind) = process::foreground_pid(pane_pid).and_then(process::read_process_cmdline) {
return Some(kind);
}
process::walk_process_tree(pane_pid)
}
async fn tick(watcher: &AgentWatcher, engines: &EngineRegistry) {
let targets = engines.watch_targets().await;
if targets.is_empty() && watcher.inner.read().await.is_empty() {
return;
}
let mut old: HashMap<String, Entry> = watcher
.inner
.read()
.await
.iter()
.map(|(name, e)| {
(
name.clone(),
Entry {
kind: e.kind,
debounce: e.debounce.clone(),
last_activity: e.last_activity.clone(),
},
)
})
.collect();
let mut new_map: HashMap<String, Entry> = HashMap::new();
for target in targets {
let Some(kind) = identify_agent(target.pane_pid) else { continue };
let prev = old.remove(&target.session).filter(|e| e.kind == kind);
if let Some(prev_entry) = prev {
if prev_entry.last_activity == target.activity
&& prev_entry.debounce.published() == AgentState::Idle
{
new_map.insert(target.session, prev_entry);
continue;
}
old.insert(target.session.clone(), prev_entry);
}
let prev = old.remove(&target.session);
let screen = match engines.capture_screen(target.kind, &target.session).await {
Ok(s) => s,
Err(_) => continue, };
let detection = detect::evaluate(kind, &screen, &target.title);
let mut debounce = prev.map(|e| e.debounce).unwrap_or_default();
let published = debounce.advance(&detection);
debug!(
"agent_watch: {} kind={} rule={} raw={:?} published={}",
target.session,
kind.as_str(),
detection.rule_id,
detection.state,
published.as_str()
);
new_map.insert(target.session, Entry { kind, debounce, last_activity: target.activity });
}
*watcher.inner.write().await = new_map;
}