wsb 0.124.3689

Workspace - A tool suite for file operations, version management, and development workflow automation
Documentation
use anyhow::Result;
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use tokio::sync::{broadcast, RwLock};

use super::home_dir;
use super::db::SessionDb;
use super::github::GitHubClient;
use super::handlers::fleet::RepoDetailedStatus;
use super::ops::OpsRegistry;
use super::sync::SyncHandle;
use crate::logging::{LogLine, LogRingBuffer};

/// Shared application state for all request handlers.
///
/// All fields are either cheaply clonable (broadcast senders, `Arc`s) or
/// actor handles (SyncHandle). No long-running operations hold a lock
/// visible to request handlers.
#[derive(Clone)]
pub struct WebState {
    pub db: SessionDb,
    /// Actor handle for sync operations — never holds a lock from handler code.
    pub sync: SyncHandle,
    pub github: Arc<RwLock<Option<GitHubClient>>>,
    pub sync_progress_tx: broadcast::Sender<SyncProgress>,
    pub log_tx: broadcast::Sender<LogLine>,
    /// Ring buffer of recent log lines — replayed to newly-connected SSE clients.
    pub log_ring: LogRingBuffer,
    pub workspace_root: PathBuf,
    pub debug: bool,
    pub port: u16,
    /// Broadcast channel for per-repo git status updates (watcher → SSE clients).
    /// Each message is a single repo's status; the frontend merges by path.
    pub git_status_tx: broadcast::Sender<RepoDetailedStatus>,
    /// Latest per-repo snapshot for new SSE subscribers, keyed by repo path.
    /// RwLock: the watcher writes infrequently; SSE connects only read.
    pub git_status_snapshot: Arc<RwLock<HashMap<String, RepoDetailedStatus>>>,
    /// Channel for focus hints from frontend (which repo the user is looking at)
    pub git_focus_tx: broadcast::Sender<String>,
    /// Channel to nudge the watcher to re-check.
    /// `None` = re-scan all repos (used after mutations where we don't know which changed).
    /// `Some(rel_path)` = re-scan only that repo (used by the FS watcher on working-tree change).
    pub git_nudge_tx: broadcast::Sender<Option<String>>,
    /// Registry of all background operations — streaming activity feed.
    pub ops: OpsRegistry,
}

impl WebState {
    pub async fn new(workspace_root: PathBuf, debug: bool, port: u16, log_tx: broadcast::Sender<LogLine>, log_ring: LogRingBuffer) -> Result<Self> {
        let db = SessionDb::open().await?;
        let sync = super::sync::spawn_sync_actor(db.clone());

        let (sync_progress_tx, _) = broadcast::channel(256);

        let github = match load_github_token() {
            Some(token) => Some(GitHubClient::new(&token)),
            None => None,
        };

        let (git_status_tx, _) = broadcast::channel(64);
        let (git_focus_tx, _) = broadcast::channel(16);
        let (git_nudge_tx, _) = broadcast::channel::<Option<String>>(64);

        Ok(Self {
            db,
            sync,
            github: Arc::new(RwLock::new(github)),
            sync_progress_tx,
            log_tx,
            log_ring,
            workspace_root,
            debug,
            port,
            git_status_tx,
            git_status_snapshot: Arc::new(RwLock::new(HashMap::new())),
            git_focus_tx,
            git_nudge_tx,
            ops: OpsRegistry::new(),
        })
    }
}

/// Progress update emitted during sync operations.
#[derive(Clone, Debug, serde::Serialize)]
pub struct SyncProgress {
    pub phase: SyncPhase,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub current_project: Option<String>,
    pub sessions_total: usize,
    pub sessions_done: usize,
    pub messages_indexed: usize,
}

#[derive(Clone, Debug, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum SyncPhase {
    Idle,
    Discovering,
    Syncing,
    Done,
}

/// Check if a GitHub token is configured.
pub fn load_github_token_exists() -> bool {
    load_github_token().is_some()
}

/// Load GitHub token from ~/.wsb/config.toml
pub fn load_github_token() -> Option<String> {
    let config_path = home_dir().ok()?.join(".wsb").join("config.toml");
    let content = std::fs::read_to_string(&config_path).ok()?;
    let table: toml::Table = content.parse().ok()?;
    let github = table.get("github")?.as_table()?;
    let token = github.get("token")?.as_str()?;
    if token.is_empty() {
        None
    } else {
        Some(token.to_string())
    }
}