Skip to main content

git_paw/mcp/query/
session.rs

1//! Session-state reads.
2//!
3//! Reads the active/most-recent session receipt for the repository and, when a
4//! broker is reachable, enriches per-agent rows with live status from the
5//! broker `/status` endpoint. Returns `None` (null session) when no session is
6//! active.
7
8use rmcp::schemars;
9use serde::Serialize;
10
11use crate::coordination::inventory::fetch_status_agents_over_http;
12use crate::mcp::RepoContext;
13use crate::session::{self, SessionStatus};
14
15/// Per-agent row in a session snapshot.
16#[derive(Debug, Clone, Serialize, schemars::JsonSchema)]
17pub struct AgentRow {
18    /// Branch / agent id.
19    pub branch: String,
20    /// CLI running in the agent's pane.
21    pub cli: String,
22    /// Live status label from the broker (empty when broker unreachable).
23    pub status: String,
24    /// Seconds since the agent was last seen (None when broker unreachable).
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub last_seen_seconds: Option<u64>,
27}
28
29/// A session snapshot.
30#[derive(Debug, Clone, Serialize, schemars::JsonSchema)]
31pub struct SessionSnapshot {
32    /// tmux session name.
33    pub name: String,
34    /// Session mode ("bare" or "supervisor").
35    pub mode: String,
36    /// Session status ("active" / "paused" / "stopped").
37    pub status: String,
38    /// Whether the session is paused.
39    pub paused: bool,
40    /// Number of registered agent worktrees.
41    pub agent_count: usize,
42    /// Broker base URL, when a broker is configured for the session.
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub broker_url: Option<String>,
45    /// Per-agent rows.
46    pub agents: Vec<AgentRow>,
47}
48
49fn status_label(status: &SessionStatus) -> &'static str {
50    match status {
51        SessionStatus::Active => "active",
52        SessionStatus::Paused => "paused",
53        SessionStatus::Stopped => "stopped",
54    }
55}
56
57/// Returns the session snapshot for the repository, or `None` when no session
58/// exists.
59#[must_use]
60pub fn session_status(ctx: &RepoContext) -> Option<SessionSnapshot> {
61    let session = session::find_session_for_repo(&ctx.root).ok().flatten()?;
62
63    // Live per-agent status, if the broker is reachable.
64    let live = ctx
65        .broker_url
66        .as_deref()
67        .and_then(|url| fetch_status_agents_over_http(url).ok())
68        .unwrap_or_default();
69
70    let agents = session
71        .worktrees
72        .iter()
73        .map(|w| {
74            let row = live.iter().find(|a| a.agent_id == w.branch);
75            AgentRow {
76                branch: w.branch.clone(),
77                cli: w.cli.clone(),
78                status: row.map(|r| r.status.clone()).unwrap_or_default(),
79                last_seen_seconds: row.map(|r| r.last_seen_seconds),
80            }
81        })
82        .collect::<Vec<_>>();
83
84    let mode = match session.mode {
85        crate::session::SessionMode::Bare => "bare",
86        crate::session::SessionMode::Supervisor => "supervisor",
87    };
88
89    let paused = session.status == SessionStatus::Paused;
90    Some(SessionSnapshot {
91        name: session.session_name.clone(),
92        mode: mode.to_string(),
93        status: status_label(&session.status).to_string(),
94        paused,
95        agent_count: session.worktrees.len(),
96        broker_url: ctx.broker_url.clone(),
97        agents,
98    })
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104
105    #[test]
106    fn no_session_yields_none() {
107        // A bare temp dir resolves no session receipt.
108        let tmp = tempfile::tempdir().unwrap();
109        let ctx = RepoContext {
110            root: tmp.path().to_path_buf(),
111            git_paw_dir: None,
112            broker_url: None,
113            server_name: "git-paw".to_string(),
114        };
115        assert!(session_status(&ctx).is_none());
116    }
117}