aboutsummaryrefslogtreecommitdiff
path: root/crates/dinggerd/src/hooks.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/hooks.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/hooks.rs')
-rw-r--r--crates/dinggerd/src/hooks.rs46
1 files changed, 40 insertions, 6 deletions
diff --git a/crates/dinggerd/src/hooks.rs b/crates/dinggerd/src/hooks.rs
index c467c98..8b24659 100644
--- a/crates/dinggerd/src/hooks.rs
+++ b/crates/dinggerd/src/hooks.rs
@@ -10,6 +10,9 @@
//! or `agent_session_id`, herdr's names), which restore uses to resume it.
//! A report without a state only records the session (Claude's
//! SessionStart hook, `dingger hook claude`); state stays with the screen.
+//! A report may also carry the agent's `importance` (`low`, `normal`,
+//! `high`): pi's extension sends it with every report, `dingger importance`
+//! alone.
//!
//! Reports are state-change *events*: the watcher applies every one in
//! order, so a turn that starts and finishes between two polls is still a
@@ -29,6 +32,8 @@ use std::sync::{Condvar, Mutex, MutexGuard, OnceLock};
use std::time::{Duration, Instant};
use detect_core::AgentState;
+
+use crate::importance::Importance;
use serde::Deserialize;
use tracing::{debug, info, warn};
@@ -47,6 +52,8 @@ struct PaneReports {
pending: Vec<AgentState>,
/// The agent's latest session reference.
session: Option<String>,
+ /// The agent's importance, if reported.
+ importance: Option<Importance>,
/// When the last report arrived.
at: Option<Instant>,
}
@@ -111,6 +118,8 @@ struct HookParams {
agent_session_path: Option<String>,
#[serde(default)]
agent_session_id: Option<String>,
+ #[serde(default)]
+ importance: Option<String>,
}
fn handle_stream(stream: UnixStream) {
@@ -137,16 +146,23 @@ fn handle_stream(stream: UnixStream) {
continue;
}
};
+ let importance = params.importance.as_deref().and_then(|value| {
+ let level = Importance::parse(value);
+ if level.is_none() {
+ debug!(importance = value, "unknown importance");
+ }
+ level
+ });
match state {
- Some(state) => info!(pane = %params.pane_id, ?state, "hook report"),
- None => info!(pane = %params.pane_id, "hook session report"),
+ Some(state) => info!(pane = %params.pane_id, ?state, ?importance, "hook report"),
+ None => info!(pane = %params.pane_id, ?importance, "hook report without state"),
}
- record(params.pane_id, state, params.agent_session_path.or(params.agent_session_id));
+ record(params.pane_id, state, params.agent_session_path.or(params.agent_session_id), importance);
wake();
}
}
-fn record(pane_id: String, state: Option<AgentState>, session: Option<String>) {
+fn record(pane_id: String, state: Option<AgentState>, session: Option<String>, importance: Option<Importance>) {
let mut reports = reports();
let pane = reports.entry(pane_id).or_default();
pane.at = Some(Instant::now());
@@ -157,6 +173,9 @@ fn record(pane_id: String, state: Option<AgentState>, session: Option<String>) {
if let Some(session) = session.filter(|s| !s.is_empty() && !s.chars().any(char::is_control)) {
pane.session = Some(session);
}
+ if importance.is_some() {
+ pane.importance = importance;
+ }
}
/// Start listening; reports accumulate for the watcher to consume.
@@ -204,6 +223,11 @@ pub fn session(pane_id: &str) -> Option<String> {
reports().get(pane_id).and_then(|pane| pane.session.clone())
}
+/// The pane's agent importance, if reported.
+pub fn importance(pane_id: &str) -> Option<Importance> {
+ reports().get(pane_id).and_then(|pane| pane.importance)
+}
+
/// Drop a pane's reports: the agent was not running when the panes were
/// listed at `listed`. A report since then is kept: the agent started
/// after the listing (Claude reports its session once, as it starts).
@@ -225,7 +249,7 @@ mod tests {
#[test]
fn a_session_report_records_only_the_session() {
- record("%t1".into(), None, Some("/p/s.jsonl".into()));
+ record("%t1".into(), None, Some("/p/s.jsonl".into()), None);
assert_eq!(session("%t1").as_deref(), Some("/p/s.jsonl"));
assert_eq!(take_observations("%t1"), None, "state stays with the screen");
assert_eq!(last_state("%t1"), None);
@@ -234,10 +258,20 @@ mod tests {
#[test]
fn a_report_after_the_pane_listing_is_not_dropped() {
let listed = Instant::now();
- record("%t2".into(), None, Some("/p/s.jsonl".into()));
+ record("%t2".into(), None, Some("/p/s.jsonl".into()), None);
drop_reports("%t2", listed);
assert!(session("%t2").is_some(), "the agent started after the listing");
drop_reports("%t2", Instant::now());
assert_eq!(session("%t2"), None, "listed since, without the agent: dropped");
}
+
+ #[test]
+ fn importance_is_kept_until_reported_again() {
+ record("%t3".into(), Some(AgentState::Working), None, Some(Importance::High));
+ record("%t3".into(), Some(AgentState::Idle), None, None);
+ assert_eq!(importance("%t3"), Some(Importance::High), "a report without it keeps it");
+ record("%t3".into(), None, None, Some(Importance::Normal));
+ assert_eq!(importance("%t3"), Some(Importance::Normal));
+ assert_eq!(last_state("%t3"), Some(AgentState::Idle), "a report without state keeps the state");
+ }
}