bosun-tmux 2.0.5

Tmux-native orchestrator for AI agent sessions
Documentation
use std::sync::Arc;
use std::time::SystemTime;

use crate::tmux::detector::Status;

/// A tmux session as observed by Bosun. This is NOT our authoritative
/// source of truth for persistence — tmux itself is. We rebuild these
/// structs on every poll from `tmux list-sessions -F ...`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TmuxSession {
    /// Actual tmux session name — includes prefix + uniq suffix for
    /// bosun-managed sessions, e.g. `bosun-rasterfox-a1b2c3d4`. This
    /// is what we pass to `tmux attach-session -t`, `capture-pane -t`,
    /// etc.
    pub name: String,
    /// Pretty name shown in bosun's UI. Populated from the tmux user
    /// option `@bosun_display` that we set at create time. `None` for
    /// sessions bosun didn't create (or ones set by an older bosun).
    pub display_name: Option<String>,
    pub windows: u32,
    pub attached: bool,
    pub created: Option<SystemTime>,
    pub last_activity: Option<SystemTime>,
    pub current_path: Option<String>,
    /// Agent name stored in `@bosun_agent` at create time
    /// (`claude` / `codex` / `terminal`). `None` for non-bosun
    /// sessions or sessions from an older bosun that didn't persist
    /// the user option.
    pub agent: Option<String>,
    /// Original spec path stored in `@bosun_path` at create time.
    /// This is the path the user typed into the new-session modal,
    /// which is more stable than `current_path` (which tracks the
    /// shell's cwd and can drift). `None` for non-bosun sessions.
    pub spec_path: Option<String>,
    /// Container ID stored in `@bosun_container_id` at create time.
    /// Identifies which sidebar container this tmux session belongs
    /// to ("tab" semantics — multiple sessions sharing one sidebar
    /// row). `None` for non-bosun sessions and for older bosun
    /// sessions from before the container feature shipped; those
    /// reconcile into their own fresh single-tab containers.
    pub container_id: Option<String>,
}

impl TmuxSession {
    /// Pretty name for the UI. Falls back to the internal session name
    /// if no display name was set.
    pub fn display(&self) -> &str {
        self.display_name.as_deref().unwrap_or(&self.name)
    }

    /// Best-available path for the UI: the user's declared spec path
    /// if the session is bosun-managed, otherwise the shell's current
    /// working directory. `None` if neither is known.
    pub fn best_path(&self) -> Option<&str> {
        self.spec_path.as_deref().or(self.current_path.as_deref())
    }
}

/// The session as the UI wants to see it: the raw tmux view plus the
/// smoothed status and (optionally) the latest pane capture for preview.
/// The actor produces these and hands them to the app.
#[derive(Debug, Clone)]
pub struct SessionView {
    pub session: TmuxSession,
    pub status: Status,
    /// Raw ANSI capture of the session's pane. `Arc<[u8]>` so the UI
    /// can render without cloning the whole buffer on every frame.
    /// `None` for sessions we skipped capturing on this tick.
    pub preview: Option<Arc<[u8]>>,
}

impl SessionView {
    pub fn new(session: TmuxSession, status: Status, preview: Option<Arc<[u8]>>) -> Self {
        Self {
            session,
            status,
            preview,
        }
    }

    /// The internal tmux name — use this when talking to tmux.
    pub fn name(&self) -> &str {
        &self.session.name
    }

    /// The pretty display name — use this in the UI.
    pub fn display(&self) -> &str {
        self.session.display()
    }
}