aboutsummaryrefslogtreecommitdiff
path: root/crates/dinggerd/src/hooks.rs
diff options
context:
space:
mode:
authorIgor Soarez <igor@soarez.org>2026-11-05 13:12:08 +0200
committerIgor Soarez <igor@soarez.org>2026-11-05 13:12:08 +0200
commit3b7b7ed091bae3b0609c38b2d445201cc4b7a026 (patch)
tree1d098a7401e08ddb66696d749d0717a2c5bf4102 /crates/dinggerd/src/hooks.rs
parent9518a5442576268a04e7dbfcddeb2457add2f189 (diff)
pi: a dialog during a run is blocked, and a cancelled run is idle, not done
Diffstat (limited to 'crates/dinggerd/src/hooks.rs')
-rw-r--r--crates/dinggerd/src/hooks.rs48
1 files changed, 44 insertions, 4 deletions
diff --git a/crates/dinggerd/src/hooks.rs b/crates/dinggerd/src/hooks.rs
index 82fa7bd..709f62b 100644
--- a/crates/dinggerd/src/hooks.rs
+++ b/crates/dinggerd/src/hooks.rs
@@ -27,6 +27,9 @@
//! `{"method":"agents.reload_outdated"}`, and the watcher reloads each agent
//! on an old extension once it is idle and off screen.
//!
+//! 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.
+//!
//! 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
//! the agent's context window is, in percent, as pi's footer shows it (""
@@ -66,7 +69,7 @@ struct PaneReports {
/// Last reported state: authoritative until the next report.
last: Option<AgentState>,
/// Reports not yet applied by the watcher, oldest first.
- pending: Vec<AgentState>,
+ pending: Vec<Observation>,
/// The agent's latest session reference.
session: Option<String>,
/// The agent's importance, if reported.
@@ -178,6 +181,8 @@ struct HookParams {
draft: Option<String>,
#[serde(default)]
context: Option<String>,
+ #[serde(default)]
+ aborted: Option<bool>,
}
fn handle_stream(stream: UnixStream) {
@@ -246,6 +251,7 @@ fn handle_stream(stream: UnixStream) {
control: params.control,
draft: params.draft,
context: params.context,
+ aborted: params.aborted == Some(true),
};
record(params.pane_id, state, reported);
wake();
@@ -261,6 +267,22 @@ struct Reported {
control: Option<String>,
draft: Option<String>,
context: Option<String>,
+ /// The run this report ends was cancelled.
+ aborted: bool,
+}
+
+/// A reported state, and whether it ends a run the user cancelled.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub struct Observation {
+ pub state: AgentState,
+ /// Idle after a cancelled run: not done, and no "finished" alert.
+ pub cancelled: bool,
+}
+
+impl Observation {
+ pub fn plain(state: AgentState) -> Self {
+ Observation { state, cancelled: false }
+ }
}
/// The longest draft kept: enough to find an agent by it.
@@ -289,7 +311,7 @@ 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(state);
+ pane.pending.push(Observation { state, cancelled: reported.aborted && state == AgentState::Idle });
}
if let Some(session) = option_value(reported.session) {
pane.session = Some(session);
@@ -352,11 +374,11 @@ pub fn spawn_listener() -> Result<(), String> {
/// The pane's hook observations for this tick: every report since the last
/// tick, in order; otherwise the last reported state; `None` if the pane
/// never reported to this process.
-pub fn take_observations(pane_id: &str) -> Option<Vec<AgentState>> {
+pub fn take_observations(pane_id: &str) -> Option<Vec<Observation>> {
let mut reports = reports();
let pane = reports.get_mut(pane_id)?;
if pane.pending.is_empty() {
- return pane.last.map(|state| vec![state]);
+ return pane.last.map(|state| vec![Observation::plain(state)]);
}
Some(std::mem::take(&mut pane.pending))
}
@@ -467,6 +489,24 @@ mod tests {
}
#[test]
+ fn a_cancelled_run_ends_in_a_cancelled_idle_observation() {
+ let aborted = || Reported { aborted: true, ..Default::default() };
+ record("%t7".into(), Some(AgentState::Working), aborted());
+ record("%t7".into(), Some(AgentState::Idle), aborted());
+ assert_eq!(
+ take_observations("%t7"),
+ Some(vec![
+ Observation::plain(AgentState::Working),
+ Observation { state: AgentState::Idle, cancelled: true },
+ ]),
+ "only an idle report ends a run"
+ );
+ assert_eq!(take_observations("%t7"), Some(vec![Observation::plain(AgentState::Idle)]), "seen once");
+ record("%t7".into(), Some(AgentState::Idle), Reported::default());
+ assert_eq!(take_observations("%t7"), Some(vec![Observation::plain(AgentState::Idle)]));
+ }
+
+ #[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");