collet 0.1.0

Relentless agentic coding orchestrator with zero-drop agent loops
Documentation
//! Sidebar info (MCP/LSP status) and approve-mode REST endpoints.

use std::sync::Arc;

use axum::Router;
use axum::extract::State;
use axum::http::StatusCode;
use axum::response::Json;
use axum::routing::get;
use serde::{Deserialize, Serialize};

use crate::agent::approval::ApproveMode;
use crate::lsp::client::known_servers;

use super::state::AppState;

pub fn router() -> Router<Arc<AppState>> {
    Router::new()
        .route("/api/sidebar", get(sidebar_info))
        .route("/api/approve-mode", get(get_approve_mode))
        .route("/api/approve-mode", axum::routing::patch(set_approve_mode))
}

// ── Sidebar ──────────────────────────────────────────────────────────────────

#[derive(Serialize)]
struct McpServerInfo {
    name: String,
    available: bool,
}

#[derive(Serialize)]
struct LspServerInfo {
    name: String,
    available: bool,
}

#[derive(Serialize)]
struct SidebarResponse {
    mcp_servers: Vec<McpServerInfo>,
    lsp_servers: Vec<LspServerInfo>,
}

async fn sidebar_info(State(state): State<Arc<AppState>>) -> Json<SidebarResponse> {
    let working_dir = state.working_dir().await;

    // MCP servers from config
    let mcp_servers = crate::mcp::config::load_mcp_status(&working_dir)
        .into_iter()
        .map(|s| McpServerInfo {
            available: matches!(s.status, crate::mcp::config::McpStatus::Available),
            name: s.name,
        })
        .collect();

    // LSP servers installed on system
    let lsp_servers = known_servers()
        .into_iter()
        .filter(|s| {
            std::process::Command::new("which")
                .arg(&s.command)
                .output()
                .map(|o| o.status.success())
                .unwrap_or(false)
        })
        .map(|s| LspServerInfo {
            name: s.command.clone(),
            available: true,
        })
        .collect();

    Json(SidebarResponse {
        mcp_servers,
        lsp_servers,
    })
}

// ── Approve mode ─────────────────────────────────────────────────────────────

#[derive(Serialize)]
struct ApproveModeResponse {
    mode: String,
}

async fn get_approve_mode(State(state): State<Arc<AppState>>) -> Json<ApproveModeResponse> {
    Json(ApproveModeResponse {
        mode: state.approve_mode.get().to_string(),
    })
}

#[derive(Deserialize)]
struct SetApproveModeRequest {
    mode: String,
}

async fn set_approve_mode(
    State(state): State<Arc<AppState>>,
    Json(req): Json<SetApproveModeRequest>,
) -> Result<Json<ApproveModeResponse>, (StatusCode, String)> {
    let mode = req.mode.parse::<ApproveMode>().map_err(|_| {
        (
            StatusCode::BAD_REQUEST,
            format!("Invalid mode '{}'. Use: yolo, auto, manual", req.mode),
        )
    })?;
    state.approve_mode.set(mode);
    Ok(Json(ApproveModeResponse {
        mode: state.approve_mode.get().to_string(),
    }))
}