diff --git a/crates/maestro-cli/src/sessions.rs b/crates/maestro-cli/src/sessions.rs index ad43be6..f58fd98 100644 --- a/crates/maestro-cli/src/sessions.rs +++ b/crates/maestro-cli/src/sessions.rs @@ -176,6 +176,7 @@ mod tests { foreground: None, activity: Default::default(), last_activity_at: chrono::Utc::now(), + activity_since: 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 a8150bb..549d1be 100644 --- a/crates/maestro-daemon/src/session.rs +++ b/crates/maestro-daemon/src/session.rs @@ -353,6 +353,7 @@ impl SessionManager { foreground: None, activity: ActivityState::Active, last_activity_at: Utc::now(), + activity_since: chrono::Utc::now(), current_cwd: None, // The watcher fills this in on its first blocked-work pass; a @@ -572,6 +573,7 @@ impl SessionManager { foreground: None, activity: ActivityState::Idle, last_activity_at: Utc::now(), + activity_since: chrono::Utc::now(), current_cwd: None, blocked_work: None, diff --git a/crates/maestro-daemon/tests/sidecar.rs b/crates/maestro-daemon/tests/sidecar.rs index bf864ed..2d88ff4 100644 --- a/crates/maestro-daemon/tests/sidecar.rs +++ b/crates/maestro-daemon/tests/sidecar.rs @@ -27,6 +27,7 @@ fn sample(id: Uuid) -> Session { foreground: None, activity: Default::default(), last_activity_at: chrono::Utc::now(), + activity_since: 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 66fb569..391d0bd 100644 --- a/crates/maestro-protocol/src/lib.rs +++ b/crates/maestro-protocol/src/lib.rs @@ -80,6 +80,13 @@ pub struct Session { /// from daemons that predate the field. #[serde(default = "epoch")] pub last_activity_at: DateTime, + /// Wall-clock time at which the session entered its current + /// [`ActivityState`] (#102) — how long it has been `Active`, `Idle` or + /// `AwaitingInput`. Distinct from [`Self::last_activity_at`], which only + /// tracks the last observed PTY output. Populated by #104; defaults to the + /// Unix epoch on payloads from daemons that predate the field. + #[serde(default = "epoch")] + pub activity_since: DateTime, /// The pane's *current* working directory (`#{pane_current_path}`) — distinct /// from [`Session::cwd`], the directory it was spawned in. The activity /// watcher refreshes it as the user `cd`s around; it drives the web UI's @@ -537,6 +544,9 @@ mod tests { last_activity_at: DateTime::parse_from_rfc3339("2026-05-24T12:05:00Z") .unwrap() .with_timezone(&Utc), + activity_since: DateTime::parse_from_rfc3339("2026-05-24T12:04:30Z") + .unwrap() + .with_timezone(&Utc), current_cwd: Some(PathBuf::from("/home/naps62/projects/maestro/crates")), blocked_work: None, } @@ -588,6 +598,25 @@ mod tests { ); } + #[test] + fn activity_since_defaults_to_epoch_on_legacy_payload() { + // A daemon predating #102 omits `activity_since`; it must deserialize + // to the forward-compat default 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.activity_since, epoch()); + + let full = sample_session(); + assert_eq!(full, round_trip(&full)); + } + #[test] fn activity_state_serializes_pascal_case_and_defaults_idle() { assert_eq!( diff --git a/crates/maestro-testing/src/lib.rs b/crates/maestro-testing/src/lib.rs index 881fd32..d5a6d47 100644 --- a/crates/maestro-testing/src/lib.rs +++ b/crates/maestro-testing/src/lib.rs @@ -42,6 +42,7 @@ pub fn session(id: Uuid, cwd: &str, command: &str) -> Session { foreground: None, activity: Default::default(), last_activity_at: Utc::now(), + activity_since: chrono::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 dff6470..3034aa4 100644 --- a/crates/maestro-tui/src/app.rs +++ b/crates/maestro-tui/src/app.rs @@ -1127,6 +1127,7 @@ mod tests { foreground: None, activity: Default::default(), last_activity_at: Utc::now(), + activity_since: chrono::Utc::now(), current_cwd: None, blocked_work: None, }], @@ -1644,6 +1645,7 @@ mod tests { foreground: None, activity: Default::default(), last_activity_at: chrono::Utc::now(), + activity_since: chrono::Utc::now(), current_cwd: None, blocked_work: None, diff --git a/crates/maestro-tui/src/catalog.rs b/crates/maestro-tui/src/catalog.rs index d2f539b..178a559 100644 --- a/crates/maestro-tui/src/catalog.rs +++ b/crates/maestro-tui/src/catalog.rs @@ -194,6 +194,7 @@ mod tests { foreground: None, activity: Default::default(), last_activity_at: chrono::Utc::now(), + activity_since: 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 724eff0..cd17e98 100644 --- a/crates/maestro-tui/src/sidebar.rs +++ b/crates/maestro-tui/src/sidebar.rs @@ -105,6 +105,7 @@ mod tests { foreground: None, activity: Default::default(), last_activity_at: chrono::Utc::now(), + activity_since: chrono::Utc::now(), current_cwd: None, blocked_work: None, } @@ -228,6 +229,7 @@ mod tests { foreground: None, activity: Default::default(), last_activity_at: chrono::Utc::now(), + activity_since: chrono::Utc::now(), current_cwd: None, blocked_work: None,