wsb-daemon 0.258.6441

wsb's daemon: the workbench, the GitHub mirror's schedule and peer continuity, for every workspace on the machine
use wsb::prelude::*;
use axum::{
    body::{boxed, Full},
    extract::{Path, Query, State},
    http::{header, StatusCode},
    response::Response,
    Json,
};

use super::sessions::ErrorResponse;
use wsb::extensions::runtime::{
    DiagnosticEntry, ExtensionMessageEntry, ShowDocumentEntry, StatusBarItemEntry, TreeViewData,
};
use wsb::extensions::{ExtensionHost, VsCodeWorkbenchModel, WorkbenchExecutionContext};
use crate::web::state::WebState;

#[derive(Debug, serde::Serialize)]
pub struct WorkspaceWorkbenchModelResponse {
    pub model: VsCodeWorkbenchModel,
    /// IDs of extensions that are installed but disabled.
    pub disabled_extension_ids: Vec<String>,
    /// Non-fatal issues encountered while loading extensions.
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub warnings: Vec<String>,
}

#[derive(Debug, serde::Deserialize)]
pub struct ExecuteExtensionCommandRequest {
    pub command: String,
    #[serde(default)]
    pub context: Option<WorkbenchExecutionContext>,
}

#[derive(Debug, serde::Serialize)]
pub struct ExecuteExtensionCommandResponse {
    pub command: String,
    pub panels: Vec<ExecutedWebviewPanel>,
    pub diagnostics: Vec<DiagnosticEntry>,
    pub status_bar_items: Vec<StatusBarItemEntry>,
    pub tree_views: Vec<TreeViewData>,
    pub show_documents: Vec<ShowDocumentEntry>,
    pub messages: Vec<ExtensionMessageEntry>,
}

#[derive(Debug, serde::Serialize)]
pub struct ExecutedWebviewPanel {
    pub panel_id: String,
    pub view_type: String,
    pub title: String,
    pub html: String,
    pub messages: Vec<serde_json::Value>,
}

#[derive(Debug, Deserialize)]
pub struct ExtensionAssetQuery {
    pub path: String,
}

pub async fn get_workbench_model(
    State(state): State<WebState>,
) -> Result<Json<WorkspaceWorkbenchModelResponse>, (StatusCode, Json<ErrorResponse>)> {
    // ExtensionHost::discover() and workbench_model() both do filesystem I/O —
    // run them on the blocking thread pool to keep the async executor free.
    tokio::task::spawn_blocking(move || {
        let host = ExtensionHost::new(state.workspace_root.clone());
        let disabled = host.disabled_ids();
        let mut disabled_ids: Vec<String> = disabled.iter().cloned().collect();
        disabled_ids.sort();
        let (mut model, warnings) = host.workbench_model().map_err(internal_error)?;
        // The model only contains enabled extensions' contributions.
        // Append disabled extensions to the extensions list (metadata only) so the
        // frontend sidebar can show all installed extensions.
        let (all, _) = host.discover().map_err(internal_error)?;
        for ext in &all {
            if disabled.contains(&ext.id) {
                model
                    .extensions
                    .push(wsb::extensions::WorkbenchExtension::from_installed(ext));
            }
        }
        model.extensions.sort_by(|a, b| a.id.cmp(&b.id));
        Ok(Json(WorkspaceWorkbenchModelResponse {
            model,
            disabled_extension_ids: disabled_ids,
            warnings,
        }))
    })
    .await
    .map_err(|e| internal_error(&format!("spawn_blocking panicked: {e:?}")))?
}

pub async fn execute_extension_command(
    State(state): State<WebState>,
    Json(request): Json<ExecuteExtensionCommandRequest>,
) -> Result<Json<ExecuteExtensionCommandResponse>, (StatusCode, Json<ErrorResponse>)> {
    let host = ExtensionHost::new(state.workspace_root.clone());
    let server_base_url = format!("http://127.0.0.1:{}", state.port);
    let context = request.context.unwrap_or(WorkbenchExecutionContext {
        groups: Vec::new(),
        active_group_id: None,
        document: None,
    });
    let execution = host
        .execute_command(&request.command, &server_base_url, context)
        .map_err(internal_error)?;
    Ok(Json(ExecuteExtensionCommandResponse {
        command: execution.command,
        panels: execution
            .panels
            .into_iter()
            .map(|panel| ExecutedWebviewPanel {
                panel_id: panel.panel_id,
                view_type: panel.view_type,
                title: panel.title,
                html: panel.html,
                messages: panel.messages,
            })
            .collect(),
        diagnostics: execution.diagnostics,
        status_bar_items: execution.status_bar_items,
        tree_views: execution.tree_views,
        show_documents: execution.show_documents,
        messages: execution.messages,
    }))
}

pub async fn get_extension_asset(
    State(state): State<WebState>,
    Path(extension_id): Path<String>,
    Query(query): Query<ExtensionAssetQuery>,
) -> Result<Response, (StatusCode, Json<ErrorResponse>)> {
    let host = ExtensionHost::new(state.workspace_root.clone());
    let asset = host
        .read_asset(&extension_id, &query.path)
        .map_err(internal_error)?;
    let mime = mime_guess::from_path(&asset.path).first_or_octet_stream();
    Response::builder()
        .status(StatusCode::OK)
        .header(header::CONTENT_TYPE, mime.as_ref())
        .body(boxed(Full::from(asset.bytes)))
        .map_err(|error| internal_error(error.to_string()))
}

pub async fn get_extension_readme(
    State(state): State<WebState>,
    Path(extension_id): Path<String>,
) -> Result<Response, (StatusCode, Json<ErrorResponse>)> {
    let host = ExtensionHost::new(state.workspace_root.clone());
    let readme = host.read_readme(&extension_id).map_err(internal_error)?;
    Response::builder()
        .status(StatusCode::OK)
        .header(header::CONTENT_TYPE, "text/markdown; charset=utf-8")
        .body(boxed(Full::from(readme)))
        .map_err(|error| internal_error(error.to_string()))
}

fn internal_error(error: impl std::fmt::Display) -> (StatusCode, Json<ErrorResponse>) {
    let message = format!("{error:#}");
    tracing::error!("{message}");
    (
        StatusCode::INTERNAL_SERVER_ERROR,
        Json(ErrorResponse { error: message }),
    )
}