diff options
| author | Igor Soarez <igor@soarez.org> | 2026-11-05 19:21:43 +0200 |
|---|---|---|
| committer | Igor Soarez <igor@soarez.org> | 2026-11-05 19:21:43 +0200 |
| commit | 5842b093cc502b46076423e4f8efdbbc568f86aa (patch) | |
| tree | 35b5a2ab971a2b9abb3188e56e0416033b4f13ea /crates/dinggerd/src/watcher.rs | |
| parent | 3b7b7ed091bae3b0609c38b2d445201cc4b7a026 (diff) | |
pi: a failed run says so: a 'failed' alert with the error, and the error in red until seen
Diffstat (limited to 'crates/dinggerd/src/watcher.rs')
| -rw-r--r-- | crates/dinggerd/src/watcher.rs | 41 |
1 files changed, 33 insertions, 8 deletions
diff --git a/crates/dinggerd/src/watcher.rs b/crates/dinggerd/src/watcher.rs index aee8d5e..9bad1fe 100644 --- a/crates/dinggerd/src/watcher.rs +++ b/crates/dinggerd/src/watcher.rs @@ -29,6 +29,7 @@ //! - `@dingger_draft` the draft in the agent's input box, as one line //! - `@dingger_cost` what the agent's session spent today (see `cost`) //! - `@dingger_context` how full its context window is, in whole percent +//! - `@dingger_error` why its run failed, while it is done (unseen) use std::collections::HashMap; @@ -375,7 +376,15 @@ impl Watcher { // Apply every observation in order: a turn that started and // finished between two ticks is still a completion. let mut alerts = Vec::new(); + // Why its last run failed: kept while it is done. + let mut error = pane.written_error.clone(); for observed in observations { + if observed.state != AgentState::Idle { + error.clear(); + } + if observed.failed { + error = hooks::error(&pane.pane_id).unwrap_or_else(|| "error".into()); + } let mut next = advance(track, observed.state, pane.attended, acked, now); // A cancelled run did not finish: idle, as if seen. if observed.cancelled && next.state == AgentState::Idle { @@ -392,7 +401,12 @@ impl Watcher { // Notify on transitions observed by this process, not // when resuming from options written before a restart. if !restored && !observed.cancelled { - alerts.extend(notification_kind_for_transition(track.state, next.state)); + alerts.extend(notification_kind_for_transition(track.state, next.state).map(|kind| { + match kind { + NotificationKind::Finished if observed.failed => NotificationKind::Failed, + kind => kind, + } + })); } } track = next; @@ -408,14 +422,18 @@ impl Watcher { track = next; } self.tracks.insert(pane.pane_id.clone(), track); + // Seen, a new run, or gone: the failure is no longer news. + if track.status() != Status::Done { + error.clear(); + } // Update what the user sees first, then alert. if acked { let _ = tmux::unset_pane_option(&pane.pane_id, "@dingger_ack"); } - self.write_pane_options(pane, track); + self.write_pane_options(pane, track, &error); for kind in alerts { - self.notify(pane, kind); + self.notify(pane, kind, &error); } } @@ -465,7 +483,7 @@ impl Watcher { /// Write the pane's options that changed, in one tmux command list: /// tmux applies it as a unit, so a daemon killed mid-write never leaves /// a display state that disagrees with the hook state it came from. - fn write_pane_options(&self, pane: &tmux::Pane, track: PaneTrack) { + fn write_pane_options(&self, pane: &tmux::Pane, track: PaneTrack, error: &str) { let mut changes: Vec<(&str, Option<String>)> = Vec::new(); let mut change = |name: &'static str, written: &str, value: String| { if written != value { @@ -483,6 +501,7 @@ impl Watcher { } change("@agent_state", &pane.written_state, track.status().label().to_string()); change("@dingger_since", &pane.written_since, track.since.map(|s| s.to_string()).unwrap_or_default()); + change("@dingger_error", &pane.written_error, error.to_string()); change("@agent", &pane.written_agent, pane.agent.map(detect_core::agent_label).unwrap_or("").to_string()); let name = crate::names::display_name(pane.agent, &pane.pane_title, &pane.cwd, host_name()) .unwrap_or_default(); @@ -544,16 +563,22 @@ impl Watcher { } } - fn notify(&self, pane: &tmux::Pane, kind: NotificationKind) { + fn notify(&self, pane: &tmux::Pane, kind: NotificationKind, error: &str) { // herdr: a blocked pane plays the alert even when the user is looking - // at it; a finished pane stays silent when they already are. + // at it; a finished (or failed) pane stays silent when they already + // are. let watching = pane.attended; - if matches!(kind, NotificationKind::Finished) && watching { + if matches!(kind, NotificationKind::Finished | NotificationKind::Failed) && watching { return; } let agent = pane.agent.map(detect_core::agent_label).unwrap_or("agent"); if self.notifications && !watching { - send_notification(agent, &kind, &format!("{} ({agent})", pane.display_target)); + let mut context = format!("{} ({agent})", pane.display_target); + if matches!(kind, NotificationKind::Failed) && !error.is_empty() { + context.push_str(": "); + context.push_str(error); + } + send_notification(agent, &kind, &context); } // A low agent makes no sound; it still notifies and waits in line. if self.sounds && importance_of(pane) != Importance::Low { |
