swimmers 0.1.3

Axum server plus TUI for orchestrating Claude Code and Codex agents across tmux panes
Documentation
pub mod dirs;
pub mod health;
pub mod native;
pub mod selection;
pub mod sessions;
pub mod skills;
pub mod thought_config;
pub mod web_actions;

use axum::middleware;
use axum::Router;
use chrono::{DateTime, Utc};
use std::sync::Arc;
use tokio::sync::{oneshot, RwLock};

use crate::auth;
use crate::config::Config;
use crate::host_actions::RepoActionTracker;
use crate::persistence::file_store::FileStore;
use crate::session::actor::SessionCommand;
use crate::session::supervisor::SessionSupervisor;
use crate::thought::health::BridgeHealthState;
use crate::thought::protocol::SyncRequestSequence;
use crate::thought::runtime_config::{DaemonDefaults, ThoughtConfig};
use crate::types::{GhosttyOpenMode, NativeDesktopApp, SessionSummary};

#[derive(Debug, Clone, Default)]
pub struct PublishedSelectionState {
    pub session_id: Option<String>,
    pub published_at: Option<DateTime<Utc>>,
}

pub struct AppState {
    pub supervisor: Arc<SessionSupervisor>,
    pub config: Arc<Config>,
    pub thought_config: Arc<RwLock<ThoughtConfig>>,
    pub native_desktop_app: Arc<RwLock<NativeDesktopApp>>,
    pub ghostty_open_mode: Arc<RwLock<GhosttyOpenMode>>,
    pub sync_request_sequence: Arc<SyncRequestSequence>,
    pub daemon_defaults: Option<DaemonDefaults>,
    pub file_store: Option<Arc<FileStore>>,
    pub bridge_health: Arc<BridgeHealthState>,
    pub published_selection: Arc<RwLock<PublishedSelectionState>>,
    pub repo_actions: RepoActionTracker,
}

pub(crate) async fn fetch_live_summary(
    state: &Arc<AppState>,
    session_id: &str,
) -> anyhow::Result<Option<SessionSummary>> {
    let handle = match state.supervisor.get_session(session_id).await {
        Some(handle) => handle,
        None => return Ok(None),
    };

    let (tx, rx) = oneshot::channel();
    handle
        .send(SessionCommand::GetSummary(tx))
        .await
        .map_err(|err| anyhow::anyhow!("failed to request session summary: {err}"))?;

    let summary = tokio::time::timeout(std::time::Duration::from_secs(2), rx)
        .await
        .map_err(|_| anyhow::anyhow!("session summary request timed out"))?
        .map_err(|_| anyhow::anyhow!("session summary actor dropped reply"))?;

    Ok(Some(summary))
}

pub fn api_router(config: Arc<Config>) -> Router<Arc<AppState>> {
    let config_for_middleware = config.clone();

    let router = Router::new()
        .merge(native::routes())
        .merge(selection::routes())
        .merge(sessions::routes())
        .merge(thought_config::routes())
        .merge(web_actions::routes());

    #[cfg(feature = "personal-workflows")]
    let router = router.merge(dirs::routes());

    #[cfg(feature = "personal-workflows")]
    let router = router.merge(skills::routes());

    router
        .layer(middleware::from_fn(move |request, next| {
            auth::auth_middleware(config_for_middleware.clone(), request, next)
        }))
        .merge(health::health_router())
}