use crate::prelude::*;
use axum::{extract::State, http::StatusCode, Json};
use super::sessions::ErrorResponse;
use crate::web::state::WebState;
#[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 }),
)
}