aboutsummaryrefslogtreecommitdiff
path: root/crates/dinggerd/src/watcher.rs
diff options
context:
space:
mode:
authorIgor Soarez <igor@soarez.org>2026-10-26 16:32:18 +0200
committerIgor Soarez <igor@soarez.org>2026-10-26 16:32:18 +0200
commit185af91b4d525db6c192a050b53615772fb5fe11 (patch)
tree6db92db4042dddcc5fee13d02943447ba9df5f2d /crates/dinggerd/src/watcher.rs
parent1eb0ac3705276c6f81f44487c7e99c24a302d4d5 (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.rs65
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);