From 003e3839bc31e7e4f33090026da156d2522332c1 Mon Sep 17 00:00:00 2001 From: Miguel Palhas Date: Mon, 24 Aug 2026 12:53:17 +0100 Subject: [PATCH] feat(maestro): carry a blocked-work indicator on Session (#83) Session.blocked_work is Option, refreshed by the daemon's watcher; None for worktree-less sessions and until first probed. Defaults on the wire so older payloads stay forward-compatible. --- crates/maestro-cli/src/sessions.rs | 1 + crates/maestro-daemon/src/session.rs | 4 +++ crates/maestro-daemon/tests/sidecar.rs | 1 + crates/maestro-protocol/src/lib.rs | 36 ++++++++++++++++++++++++++ crates/maestro-testing/src/lib.rs | 1 + crates/maestro-tui/src/app.rs | 2 ++ crates/maestro-tui/src/catalog.rs | 1 + crates/maestro-tui/src/sidebar.rs | 2 ++ 8 files changed, 48 insertions(+) diff --git a/crates/maestro-cli/src/sessions.rs b/crates/maestro-cli/src/sessions.rs index 7423ddd..c88d0e7 100644 --- a/crates/maestro-cli/src/sessions.rs +++ b/crates/maestro-cli/src/sessions.rs @@ -164,6 +164,7 @@ mod tests { activity: Default::default(), last_activity_at: chrono::Utc::now(), current_cwd: None, + blocked_work: None, } } diff --git a/crates/maestro-daemon/src/session.rs b/crates/maestro-daemon/src/session.rs index 910894b..7d0c44e 100644 --- a/crates/maestro-daemon/src/session.rs +++ b/crates/maestro-daemon/src/session.rs @@ -344,6 +344,9 @@ impl SessionManager { last_activity_at: Utc::now(), current_cwd: None, + // The watcher fills this in on its first blocked-work pass; a + // fresh tree has nothing in the way anyway. + blocked_work: None, }; if let Err(e) = self.inner.sidecar.write(&session) { @@ -541,6 +544,7 @@ impl SessionManager { last_activity_at: Utc::now(), current_cwd: None, + blocked_work: None, }, }; let fifo_path = self.inner.fifo_dir.join(format!("{id}.fifo")); diff --git a/crates/maestro-daemon/tests/sidecar.rs b/crates/maestro-daemon/tests/sidecar.rs index 4997f1a..bf864ed 100644 --- a/crates/maestro-daemon/tests/sidecar.rs +++ b/crates/maestro-daemon/tests/sidecar.rs @@ -28,6 +28,7 @@ fn sample(id: Uuid) -> Session { activity: Default::default(), last_activity_at: chrono::Utc::now(), current_cwd: Some(PathBuf::from("/tmp/work/crates")), + blocked_work: None, } } diff --git a/crates/maestro-protocol/src/lib.rs b/crates/maestro-protocol/src/lib.rs index 8a24179..66fb569 100644 --- a/crates/maestro-protocol/src/lib.rs +++ b/crates/maestro-protocol/src/lib.rs @@ -87,6 +87,13 @@ pub struct Session { /// from daemons that predate the field. #[serde(default)] pub current_cwd: Option, + /// Whether the session's worktree currently holds anything that would + /// block an unforced kill (#83), summarised by [`InTheWay`]'s `Display`. + /// Refreshed by the daemon's watcher; `None` for sessions without a + /// worktree and until the first probe. Defaults on the wire so payloads + /// from daemons that predate the field stay forward-compatible. + #[serde(default)] + pub blocked_work: Option, } /// Default for [`Session::last_activity_at`] when an older payload omits it. @@ -531,6 +538,7 @@ mod tests { .unwrap() .with_timezone(&Utc), current_cwd: Some(PathBuf::from("/home/naps62/projects/maestro/crates")), + blocked_work: None, } } @@ -550,6 +558,34 @@ mod tests { assert_eq!(s.activity, ActivityState::Idle); assert_eq!(s.last_activity_at, epoch()); assert!(s.current_cwd.is_none()); + assert!(s.blocked_work.is_none()); + } + + #[test] + fn blocked_work_defaults_to_none_on_legacy_payload() { + // A pre-#83 daemon omits the indicator; it must deserialize to `None` + // rather than failing, and a present value round-trips. + let json = serde_json::json!({ + "id": Uuid::from_u128(1), + "cwd": "/tmp", + "command": "bash", + "status": "Running", + "created_at": "2026-05-24T12:00:00Z" + }); + let s: Session = serde_json::from_value(json).unwrap(); + assert_eq!(s.blocked_work, None); + + let mut with_blocker = sample_session(); + with_blocker.blocked_work = Some(InTheWay { + uncommitted_files: 3, + ..InTheWay::default() + }); + assert_eq!( + with_blocker.clone().blocked_work, + serde_json::from_value::(serde_json::to_value(&with_blocker).unwrap()) + .unwrap() + .blocked_work + ); } #[test] diff --git a/crates/maestro-testing/src/lib.rs b/crates/maestro-testing/src/lib.rs index 2706e83..881fd32 100644 --- a/crates/maestro-testing/src/lib.rs +++ b/crates/maestro-testing/src/lib.rs @@ -43,6 +43,7 @@ pub fn session(id: Uuid, cwd: &str, command: &str) -> Session { activity: Default::default(), last_activity_at: Utc::now(), current_cwd: None, + blocked_work: None, } } diff --git a/crates/maestro-tui/src/app.rs b/crates/maestro-tui/src/app.rs index b329821..cecf43e 100644 --- a/crates/maestro-tui/src/app.rs +++ b/crates/maestro-tui/src/app.rs @@ -1107,6 +1107,7 @@ mod tests { activity: Default::default(), last_activity_at: Utc::now(), current_cwd: None, + blocked_work: None, }], }); app.dispatch(Action::SidebarNext); @@ -1614,6 +1615,7 @@ mod tests { last_activity_at: chrono::Utc::now(), current_cwd: None, + blocked_work: None, }], }); app.dispatch(Action::KillHighlighted); diff --git a/crates/maestro-tui/src/catalog.rs b/crates/maestro-tui/src/catalog.rs index 32115d6..d2f539b 100644 --- a/crates/maestro-tui/src/catalog.rs +++ b/crates/maestro-tui/src/catalog.rs @@ -195,6 +195,7 @@ mod tests { activity: Default::default(), last_activity_at: chrono::Utc::now(), current_cwd: None, + blocked_work: None, } } diff --git a/crates/maestro-tui/src/sidebar.rs b/crates/maestro-tui/src/sidebar.rs index fa60e90..ea9b566 100644 --- a/crates/maestro-tui/src/sidebar.rs +++ b/crates/maestro-tui/src/sidebar.rs @@ -99,6 +99,7 @@ mod tests { activity: Default::default(), last_activity_at: chrono::Utc::now(), current_cwd: None, + blocked_work: None, } } @@ -188,6 +189,7 @@ mod tests { last_activity_at: chrono::Utc::now(), current_cwd: None, + blocked_work: None, }; let sb = Sidebar { sessions: vec![s],