From 3b7b7ed091bae3b0609c38b2d445201cc4b7a026 Mon Sep 17 00:00:00 2001 From: Igor Soarez Date: Thu, 5 Nov 2026 13:12:08 +0200 Subject: pi: a dialog during a run is blocked, and a cancelled run is idle, not done --- crates/dinggerd/src/hooks.rs | 48 ++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 44 insertions(+), 4 deletions(-) (limited to 'crates/dinggerd/src/hooks.rs') 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, /// Reports not yet applied by the watcher, oldest first. - pending: Vec, + pending: Vec, /// The agent's latest session reference. session: Option, /// The agent's importance, if reported. @@ -178,6 +181,8 @@ struct HookParams { draft: Option, #[serde(default)] context: Option, + #[serde(default)] + aborted: Option, } 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, draft: Option, context: Option, + /// 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, 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> { +pub fn take_observations(pane_id: &str) -> Option> { 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)) } @@ -466,6 +488,24 @@ mod tests { assert_eq!(extension("%t4").0.as_deref(), Some("v2")); } + #[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() }; -- cgit v1.2.3