Replace the freeform `metadata.label` session grouping with nesting derived
from each session's working directory. The daemon's activity watcher already
polls each tmux pane every tick, so it now also captures
`#{pane_current_path}` and surfaces it as a new `Session.current_cwd`
(serde-default, back-compat); `cwd` stays the spawn dir for worktree
inference, spawn-similar, and recents.
Web catalog builds the tree from `current_cwd ?? cwd`, collapses the home
dir to `~`, and folds single-child folder chains so `~/git/maestro` reads as
one node. CLI swaps its LABEL column for an abbreviated DIR; the spawn forms
(web + TUI) and `--label` flag are removed.
The pane format stays space-delimited (parser splits the first three tokens
and keeps the remainder as the path) so a cwd containing spaces survives.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
63 lines
1.9 KiB
Rust
63 lines
1.9 KiB
Rust
//! Activity-state inference combine logic (NAP-340).
|
|
//!
|
|
//! Pure decision from three structured signals — PTY quiescence, foreground
|
|
//! process, and `/proc` block-state — so it's unit-testable without tmux or a
|
|
//! live session. The orchestration (gathering the signals each liveness tick
|
|
//! and emitting `SessionUpdated` on change) lives in [`crate::session`].
|
|
|
|
use std::time::Duration;
|
|
|
|
use maestro_protocol::ActivityState;
|
|
|
|
/// PTY output more recent than this keeps a session `Active`. It must exceed the
|
|
/// liveness tick (~2s, [`crate::session::DEFAULT_LIVENESS_INTERVAL`]) so a
|
|
/// session emitting a chunk every tick stays `Active` instead of flapping
|
|
/// Active↔Idle. This window *is* the debounce the issue calls for.
|
|
pub const ACTIVE_WINDOW: Duration = Duration::from_secs(3);
|
|
|
|
/// Combine the observed signals into an [`ActivityState`]:
|
|
/// recent output ⇒ `Active`; else blocked on a terminal read ⇒ `AwaitingInput`;
|
|
/// else `Idle`.
|
|
pub fn decide(idle_for: Duration, awaiting_input: bool) -> ActivityState {
|
|
if idle_for < ACTIVE_WINDOW {
|
|
ActivityState::Active
|
|
} else if awaiting_input {
|
|
ActivityState::AwaitingInput
|
|
} else {
|
|
ActivityState::Idle
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn recent_output_is_active_regardless_of_block_state() {
|
|
assert_eq!(
|
|
decide(Duration::from_millis(100), false),
|
|
ActivityState::Active
|
|
);
|
|
assert_eq!(
|
|
decide(Duration::from_millis(100), true),
|
|
ActivityState::Active
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn quiescent_and_blocked_is_awaiting_input() {
|
|
assert_eq!(
|
|
decide(ACTIVE_WINDOW + Duration::from_secs(1), true),
|
|
ActivityState::AwaitingInput
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn quiescent_and_not_blocked_is_idle() {
|
|
assert_eq!(
|
|
decide(ACTIVE_WINDOW + Duration::from_secs(1), false),
|
|
ActivityState::Idle
|
|
);
|
|
}
|
|
}
|