aboutsummaryrefslogtreecommitdiff
path: root/crates/dinggerd/src/watcher.rs
diff options
context:
space:
mode:
Diffstat (limited to 'crates/dinggerd/src/watcher.rs')
-rw-r--r--crates/dinggerd/src/watcher.rs41
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 {