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/hooks.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/hooks.rs')
| -rw-r--r-- | crates/dinggerd/src/hooks.rs | 46 |
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"); + } } |
