diff options
Diffstat (limited to 'crates/dinggerd/src/watcher.rs')
| -rw-r--r-- | crates/dinggerd/src/watcher.rs | 107 |
1 files changed, 98 insertions, 9 deletions
diff --git a/crates/dinggerd/src/watcher.rs b/crates/dinggerd/src/watcher.rs index f039456..9fd999d 100644 --- a/crates/dinggerd/src/watcher.rs +++ b/crates/dinggerd/src/watcher.rs @@ -9,14 +9,30 @@ use tracing::{debug, info, warn}; use crate::notify::{play_sound, send_notification, NotificationKind}; use crate::tmux; +/// When a pane became ready for the user, as unix seconds. Ready means the +/// agent is waiting on the user: idle at its prompt or blocked on input. +fn is_ready(state: AgentState) -> bool { + matches!(state, AgentState::Idle | AgentState::Blocked) +} + pub struct Watcher { - /// Last published state per pane id. `status` reads the same values back - /// from tmux's `@agent_state`, so no other state is needed. - states: HashMap<String, AgentState>, + /// Published state plus the unix second it became ready, per pane id. + /// Published state plus the unix second it became ready, per pane id. + /// `status` reads the same values back from tmux's `@agent_state`, so no + /// other durable state is needed. + panes: HashMap<String, PaneTrack>, + /// Last (queue, counts) written to the global options; avoids rewriting + /// them every tick. + last_published: Option<(String, (String, String, String))>, interval_secs: u64, sounds: bool, } +struct PaneTrack { + state: AgentState, + ready_since: Option<u64>, +} + /// herdr's notification policy (`notification_toast_for_state_change`). pub fn notification_kind_for_transition( prev_state: AgentState, @@ -39,7 +55,8 @@ pub fn notification_kind_for_transition( impl Watcher { pub fn new(interval_secs: u64, sounds: bool) -> Self { Self { - states: HashMap::new(), + panes: HashMap::new(), + last_published: None, interval_secs: interval_secs.max(1), sounds, } @@ -74,14 +91,14 @@ impl Watcher { let panes = tmux::list_panes()?; // Forget panes that went away. - self.states + self.panes .retain(|pane_id, _| panes.iter().any(|pane| pane.pane_id == *pane_id)); - for pane in panes { + for pane in &panes { let prev_state = self - .states + .panes .get(&pane.pane_id) - .copied() + .map(|track| track.state) .unwrap_or(AgentState::Unknown); // The detection result is an observation of live agent state. @@ -130,12 +147,22 @@ impl Watcher { if new_state == prev_state { continue; } - self.states.insert(pane.pane_id.clone(), new_state); + let ready_since = is_ready(new_state).then(unix_now); + self.panes.insert( + pane.pane_id.clone(), + PaneTrack { + state: new_state, + ready_since, + }, + ); debug!(target = %pane.target, ?prev_state, ?new_state, "state change"); if let Err(err) = tmux::set_agent_state(&pane.pane_id, new_state) { warn!(target = %pane.target, %err, "failed to write @agent_state"); } + // A fresh transition re-enters the ready queue even if the user + // already jumped to this pane in a previous ready episode. + let _ = tmux::clear_ack(&pane.pane_id); if let Some(kind) = notification_kind_for_transition(prev_state, new_state) { @@ -151,8 +178,70 @@ impl Watcher { } } + self.publish_ready_queue(&panes); Ok(()) } + + /// Publish the FIFO ready queue (oldest first) as a server-global option; + /// `dingger focus` pops its head, and the tmux status line renders counts. + fn publish_ready_queue(&mut self, panes: &[tmux::Pane]) { + let mut ready: Vec<(&str, String, u64)> = Vec::new(); // (pane_id, target, ready_since) + let mut blocked = 0usize; + let mut working = 0usize; + for pane in panes { + let Some(track) = self.panes.get(&pane.pane_id) else { + continue; + }; + match track.state { + AgentState::Working => working += 1, + AgentState::Idle | AgentState::Blocked => { + let acked = pane.written_ack == "1"; + if !acked { + if let Some(since) = track.ready_since { + if track.state == AgentState::Blocked { + blocked += 1; + } + ready.push((pane.pane_id.as_str(), pane.target.clone(), since)); + } + } + } + AgentState::Unknown => {} + } + } + ready.sort_by_key(|(_, _, since)| *since); + + let queue = ready + .iter() + .map(|(pane_id, _, since)| format!("{pane_id}:{since}")) + .collect::<Vec<_>>() + .join(","); + let counts = ( + ready.len().to_string(), + blocked.to_string(), + working.to_string(), + ); + let next = (queue.clone(), counts.clone()); + if self.last_published.as_ref() != Some(&next) { + self.last_published = Some(next); + for (name, value) in [ + ("@dingger_queue", queue.as_str()), + ("@dingger_ready", counts.0.as_str()), + ("@dingger_blocked", counts.1.as_str()), + ("@dingger_working", counts.2.as_str()), + ] { + if let Err(err) = tmux::set_global(name, value) { + warn!(%err, "failed to write {name}"); + } + } + } + } +} + +fn unix_now() -> u64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0) } #[cfg(test)] |
