diff options
| author | Igor Soarez <igor@soarez.org> | 2026-10-26 16:32:18 +0200 |
|---|---|---|
| committer | Igor Soarez <igor@soarez.org> | 2026-10-26 16:32:18 +0200 |
| commit | 185af91b4d525db6c192a050b53615772fb5fe11 (patch) | |
| tree | 6db92db4042dddcc5fee13d02943447ba9df5f2d /crates/dinggerd/src/watcher.rs | |
| parent | 1eb0ac3705276c6f81f44487c7e99c24a302d4d5 (diff) | |
importance: mark an agent high, normal, or low
within a status, more important agents are listed first, and so jumped to
first; status stays first. a high agent shows a bold ! before its name, a
low agent's name is lightly dimmed with "low" on its second line, and it
makes no sound (it still notifies and waits in line).
in pi, /importance high|normal|low (completes the levels; none once one is
typed out, so Enter runs it). the extension saves it in the session as a
custom entry (dingger-importance), so /reload, restarts, /resume and
restore bring it back; /new starts at normal. it shows a level other than
normal in pi's footer, emits dingger:importance { level } for other
extensions, and sends it with every report. for other agents, `dingger
importance [level] [--pane]`; it refuses a pi that reports itself, whose
saved level would win.
the daemon stays the one writer of pane state: it writes
@dingger_importance (unset for normal), which goes with the agent and
survives a daemon restart. `dingger status` marks high and low.
tests: e2e step 21 (order, the option, no sound for low, the menu bar,
the command, the extension through a harness that now matches pi's
extension API), a DINGGER_SOUND seam so sounds are observable, and a
status-line check that read the counts once (flaky) now polls.
pi-live: /importance in real pi 1.4, its footer and session entry, and the
level after a restore; every pi there now uses the test's agent directory
(a restored pi read the user's). negative controls fail each part.
Diffstat (limited to 'crates/dinggerd/src/watcher.rs')
| -rw-r--r-- | crates/dinggerd/src/watcher.rs | 65 |
1 files changed, 54 insertions, 11 deletions
diff --git a/crates/dinggerd/src/watcher.rs b/crates/dinggerd/src/watcher.rs index 9198f60..f9c987e 100644 --- a/crates/dinggerd/src/watcher.rs +++ b/crates/dinggerd/src/watcher.rs @@ -31,6 +31,7 @@ use detect_core::{detect_agent_with_osc, AgentState}; use tracing::{debug, info, warn}; use crate::hooks; +use crate::importance::Importance; use crate::notify::{play_sound, send_notification, NotificationKind}; use crate::tmux; @@ -172,15 +173,29 @@ pub fn advance( } /// The order agents are listed in: herdr's status priority; within a -/// status, done and blocked oldest first (the queue: nothing waits -/// forever), working and idle most recent first; then by place. -pub fn list_order(status: Status, since: Option<u64>) -> (std::cmp::Reverse<u8>, i128) { +/// status, the more important first; then done and blocked oldest first +/// (the queue: nothing waits forever), working and idle most recent first; +/// then by place. +pub fn list_order( + status: Status, + importance: Importance, + since: Option<u64>, +) -> (std::cmp::Reverse<u8>, std::cmp::Reverse<Importance>, i128) { let since = since.map(i128::from); let key = match status { Status::Done | Status::Blocked => since.unwrap_or(i128::MAX), _ => since.map(|since| -since).unwrap_or(i128::MAX), }; - (std::cmp::Reverse(status.priority()), key) + (std::cmp::Reverse(status.priority()), std::cmp::Reverse(importance), key) +} + +/// A pane's importance: as its agent last reported it, else as written +/// (it survives a daemon restart); normal without an agent. +pub fn importance_of(pane: &tmux::Pane) -> Importance { + if pane.agent.is_none() { + return Importance::Normal; + } + hooks::importance(&pane.pane_id).unwrap_or_else(|| Importance::from_option(&pane.written_importance)) } pub struct Watcher { @@ -330,6 +345,14 @@ impl Watcher { (None, _) => change("@agent_session", &pane.written_session, String::new()), _ => {} } + // The importance goes with the agent too. + match (pane.agent, hooks::importance(&pane.pane_id)) { + (Some(_), Some(level)) => { + change("@dingger_importance", &pane.written_importance, level.option_value().to_string()) + } + (None, _) => change("@dingger_importance", &pane.written_importance, String::new()), + _ => {} + } if let Err(err) = tmux::set_pane_options(&pane.pane_id, &changes) { warn!(target = %pane.display_target, %err, "failed to write pane options"); } @@ -346,7 +369,8 @@ impl Watcher { if self.notifications && !watching { send_notification(agent, &kind, &format!("{} ({agent})", pane.display_target)); } - if self.sounds { + // A low agent makes no sound; it still notifies and waits in line. + if self.sounds && importance_of(pane) != Importance::Low { play_sound(&kind); } } @@ -355,7 +379,7 @@ impl Watcher { /// status line, menu bar, and focus. The queue is ordered like the menu: /// herdr's priority (blocked before done), oldest first within it. fn publish(&mut self, panes: &[tmux::Pane]) { - let mut queue: Vec<(std::cmp::Reverse<u8>, u64, &str)> = Vec::new(); + let mut queue = Vec::new(); let (mut blocked, mut done, mut working) = (0, 0, 0); for pane in panes { let Some(track) = self.tracks.get(&pane.pane_id) else { @@ -367,16 +391,15 @@ impl Watcher { Status::Working => working += 1, Status::Idle | Status::Unknown => {} } - // The queue: what needs the user, oldest first. + // The queue: what needs the user, in list order. if matches!(track.status(), Status::Blocked | Status::Done) { - let since = track.since.unwrap_or(u64::MAX); - queue.push((std::cmp::Reverse(track.status().priority()), since, pane.pane_id.as_str())); + queue.push((list_order(track.status(), importance_of(pane), track.since), pane.pane_id.as_str())); } } queue.sort(); let queue = queue .iter() - .map(|(_, _, pane_id)| *pane_id) + .map(|(_, pane_id)| *pane_id) .collect::<Vec<_>>() .join(","); @@ -587,7 +610,7 @@ mod tests { ("blocked", Status::Blocked, Some(99)), ("working new", Status::Working, Some(70)), ]; - rows.sort_by_key(|(_, status, since)| list_order(*status, *since)); + rows.sort_by_key(|(_, status, since)| list_order(*status, Importance::Normal, *since)); let names: Vec<_> = rows.iter().map(|(name, ..)| *name).collect(); assert_eq!( names, @@ -596,6 +619,26 @@ mod tests { } #[test] + fn importance_orders_within_a_status_before_time() { + use Importance::*; + let mut rows = [ + ("done old", Status::Done, Normal, Some(10)), + ("done new high", Status::Done, High, Some(90)), + ("done older low", Status::Done, Low, Some(5)), + ("blocked low", Status::Blocked, Low, Some(99)), + ("working high", Status::Working, High, Some(1)), + ("working new", Status::Working, Normal, Some(80)), + ]; + rows.sort_by_key(|(_, status, importance, since)| list_order(*status, *importance, *since)); + let names: Vec<_> = rows.iter().map(|(name, ..)| *name).collect(); + assert_eq!( + names, + ["blocked low", "done new high", "done old", "done older low", "working high", "working new"], + "status first, then importance, then time" + ); + } + + #[test] fn agent_exit_is_unknown() { let done = step(step(PaneTrack::NEW, Working, false), Idle, false); assert_eq!(step(done, Unknown, false).status(), Status::Unknown); |
