From 185af91b4d525db6c192a050b53615772fb5fe11 Mon Sep 17 00:00:00 2001 From: Igor Soarez Date: Mon, 26 Oct 2026 16:32:18 +0200 Subject: 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. --- crates/dinggerd/src/hooks.rs | 46 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 6 deletions(-) (limited to 'crates/dinggerd/src/hooks.rs') 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, /// The agent's latest session reference. session: Option, + /// The agent's importance, if reported. + importance: Option, /// When the last report arrived. at: Option, } @@ -111,6 +118,8 @@ struct HookParams { agent_session_path: Option, #[serde(default)] agent_session_id: Option, + #[serde(default)] + importance: Option, } 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, session: Option) { +fn record(pane_id: String, state: Option, session: Option, importance: Option) { 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, session: Option) { 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 { reports().get(pane_id).and_then(|pane| pane.session.clone()) } +/// The pane's agent importance, if reported. +pub fn importance(pane_id: &str) -> Option { + 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"); + } } -- cgit v1.2.3