wsb 0.164.718

Workspace - A tool suite for file operations, version management, and development workflow automation
use crate::prelude::*;
use axum::{extract::State, http::StatusCode, Json};

use super::sessions::ErrorResponse;
use crate::web::state::WebState;

/// The full persisted workbench layout state.
/// The frontend owns the schema for `input` on each tab — we store it
/// as opaque JSON so the backend never needs to understand editor input types.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PersistedWorkbenchState {
    pub groups: Vec<PersistedEditorGroup>,
    pub active_group_id: String,
    #[serde(default = "default_group_sizes")]
    pub editor_group_sizes: Vec<f64>,
    pub sidebar_open: bool,
    #[serde(default = "default_sidebar_width")]
    pub sidebar_width: u32,
    pub sidebar_active_container_id: String,
    pub activity_bar_order: Vec<String>,
    pub bottom_panel_open: bool,
    pub bottom_panel_height: u32,
    pub bottom_panel_active_id: String,
    pub bottom_panel_order: Vec<String>,
    pub expanded_paths: Vec<String>,
    #[serde(default)]
    pub sort_newest_first: bool,
    #[serde(default = "default_message_layout")]
    pub message_layout: String,
    #[serde(default = "default_transcript_mode")]
    pub transcript_mode: String,
}

fn default_group_sizes() -> Vec<f64> {
    vec![1.0]
}

fn default_sidebar_width() -> u32 {
    320
}

fn default_message_layout() -> String {
    "default".to_string()
}

fn default_transcript_mode() -> String {
    "normal".to_string()
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PersistedEditorGroup {
    pub id: String,
    pub tabs: Vec<PersistedEditorTab>,
    pub active_tab_id: Option<String>,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PersistedEditorTab {
    pub id: String,
    pub label: String,
    pub description: String,
    pub input: serde_json::Value,
    pub pinned: bool,
}

fn workbench_state_path(workspace_root: &std::path::Path) -> std::path::PathBuf {
    workspace_root.join(".wsb").join("workbench.json")
}

pub async fn get_workbench_state(
    State(state): State<WebState>,
) -> Result<Json<PersistedWorkbenchState>, (StatusCode, Json<ErrorResponse>)> {
    let path = workbench_state_path(&state.workspace_root);
    tokio::task::spawn_blocking(move || match std::fs::read(&path) {
        Ok(bytes) => {
            let persisted: PersistedWorkbenchState =
                serde_json::from_slice(&bytes).map_err(|e| {
                    internal_error(format!("failed to parse {}: {e}", path.display()))
                })?;
            Ok(Json(persisted))
        }
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => Err((
            StatusCode::NOT_FOUND,
            Json(ErrorResponse {
                error: "no saved workbench state".to_string(),
            }),
        )),
        Err(e) => Err(internal_error(format!(
            "failed to read {}: {e}",
            path.display()
        ))),
    })
    .await
    .map_err(|e| internal_error(format!("get_workbench_state task panicked: {e}")))?
}

pub async fn put_workbench_state(
    State(state): State<WebState>,
    Json(body): Json<PersistedWorkbenchState>,
) -> Result<Json<PersistedWorkbenchState>, (StatusCode, Json<ErrorResponse>)> {
    let path = workbench_state_path(&state.workspace_root);
    let json = serde_json::to_string_pretty(&body)
        .map_err(|e| internal_error(format!("failed to serialize workbench state: {e}")))?;
    tokio::task::spawn_blocking(move || {
        if let Some(parent) = path.parent() {
            std::fs::create_dir_all(parent).map_err(|e| {
                internal_error(format!("failed to create {}: {e}", parent.display()))
            })?;
        }
        std::fs::write(&path, json)
            .map_err(|e| internal_error(format!("failed to write {}: {e}", path.display())))
    })
    .await
    .map_err(|e| internal_error(format!("put_workbench_state task panicked: {e}")))??;
    Ok(Json(body))
}

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