aboutsummaryrefslogtreecommitdiff
path: root/crates/dinggerd/src/hooks.rs
diff options
context:
space:
mode:
authorIgor Soarez <igor@soarez.org>2026-11-05 19:21:43 +0200
committerIgor Soarez <igor@soarez.org>2026-11-05 19:21:43 +0200
commit5842b093cc502b46076423e4f8efdbbc568f86aa (patch)
tree35b5a2ab971a2b9abb3188e56e0416033b4f13ea /crates/dinggerd/src/hooks.rs
parent3b7b7ed091bae3b0609c38b2d445201cc4b7a026 (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/hooks.rs')
-rw-r--r--crates/dinggerd/src/hooks.rs44
1 files changed, 41 insertions, 3 deletions
diff --git a/crates/dinggerd/src/hooks.rs b/crates/dinggerd/src/hooks.rs
index 709f62b..aeb3285 100644
--- a/crates/dinggerd/src/hooks.rs
+++ b/crates/dinggerd/src/hooks.rs
@@ -29,6 +29,8 @@
//!
//! An idle report may say the run was `aborted` (cancelled, as with
//! Escape): the agent is idle, not done, and nobody is told it finished.
+//! Or that it failed, with the `error` as one readable line: the agent is
+//! done, and the alert says it failed and why.
//!
//! It also carries the `draft` in the agent's input box ("" for none), for
//! marking a draft you left behind; never logged. And `context`: how full
@@ -81,6 +83,8 @@ struct PaneReports {
draft: Option<String>,
/// How full its context window is: whole percent ("" when unknown).
context: Option<String>,
+ /// The error that ended its last failed run, as one line.
+ error: Option<String>,
/// When the last report arrived.
at: Option<Instant>,
}
@@ -183,6 +187,8 @@ struct HookParams {
context: Option<String>,
#[serde(default)]
aborted: Option<bool>,
+ #[serde(default)]
+ error: Option<String>,
}
fn handle_stream(stream: UnixStream) {
@@ -252,6 +258,7 @@ fn handle_stream(stream: UnixStream) {
draft: params.draft,
context: params.context,
aborted: params.aborted == Some(true),
+ error: params.error,
};
record(params.pane_id, state, reported);
wake();
@@ -269,6 +276,8 @@ struct Reported {
context: Option<String>,
/// The run this report ends was cancelled.
aborted: bool,
+ /// The error the run this report ends failed with.
+ error: Option<String>,
}
/// A reported state, and whether it ends a run the user cancelled.
@@ -277,11 +286,13 @@ pub struct Observation {
pub state: AgentState,
/// Idle after a cancelled run: not done, and no "finished" alert.
pub cancelled: bool,
+ /// Idle after a failed run: done, with a "failed" alert (see [`error`]).
+ pub failed: bool,
}
impl Observation {
pub fn plain(state: AgentState) -> Self {
- Observation { state, cancelled: false }
+ Observation { state, cancelled: false, failed: false }
}
}
@@ -311,7 +322,12 @@ fn record(pane_id: String, state: Option<AgentState>, reported: Reported) {
pane.at = Some(Instant::now());
if let Some(state) = state {
pane.last = Some(state);
- pane.pending.push(Observation { state, cancelled: reported.aborted && state == AgentState::Idle });
+ let idle = state == AgentState::Idle;
+ let error = reported.error.as_deref().map(draft_line).filter(|error| idle && !error.is_empty());
+ pane.pending.push(Observation { state, cancelled: reported.aborted && idle, failed: error.is_some() });
+ if error.is_some() {
+ pane.error = error;
+ }
}
if let Some(session) = option_value(reported.session) {
pane.session = Some(session);
@@ -342,6 +358,11 @@ fn context_percent(value: &str) -> String {
}
}
+/// The error the pane's agent's last failed run ended with.
+pub fn error(pane_id: &str) -> Option<String> {
+ reports().get(pane_id).and_then(|pane| pane.error.clone())
+}
+
/// How full the pane's agent's context window is ("" when unknown), if its
/// extension reports it.
pub fn context(pane_id: &str) -> Option<String> {
@@ -497,7 +518,7 @@ mod tests {
take_observations("%t7"),
Some(vec![
Observation::plain(AgentState::Working),
- Observation { state: AgentState::Idle, cancelled: true },
+ Observation { state: AgentState::Idle, cancelled: true, failed: false },
]),
"only an idle report ends a run"
);
@@ -507,6 +528,23 @@ mod tests {
}
#[test]
+ fn a_failed_run_ends_in_a_failed_idle_observation_with_its_error() {
+ let failed = |error: &str| Reported { error: Some(error.into()), ..Default::default() };
+ record("%t8".into(), Some(AgentState::Working), failed("not the end of a run"));
+ record("%t8".into(), Some(AgentState::Idle), failed("401: invalid\nx-api-key"));
+ assert_eq!(
+ take_observations("%t8"),
+ Some(vec![
+ Observation::plain(AgentState::Working),
+ Observation { state: AgentState::Idle, cancelled: false, failed: true },
+ ])
+ );
+ assert_eq!(super::error("%t8").as_deref(), Some("401: invalid x-api-key"), "one line");
+ record("%t8".into(), Some(AgentState::Idle), failed(""));
+ assert_eq!(take_observations("%t8"), Some(vec![Observation::plain(AgentState::Idle)]), "no error, no failure");
+ }
+
+ #[test]
fn context_use_is_kept_as_whole_percent_until_reported_again() {
let context = |text: &str| Reported { context: Some(text.into()), ..Default::default() };
assert_eq!(super::context("%t6"), None, "an extension that does not report it");