use wsb::prelude::*;
use axum::{extract::State, http::StatusCode, Json};
use rusqlite::OptionalExtension;
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,
}
pub const SELECT_LAYOUT: &str =
"SELECT state_json FROM workbench_state WHERE workspace_root = ?1";
pub const UPSERT_LAYOUT: &str = "INSERT INTO workbench_state (workspace_root, state_json, updated_at)
VALUES (?1, ?2, datetime('now'))
ON CONFLICT(workspace_root) DO UPDATE SET
state_json = excluded.state_json,
updated_at = excluded.updated_at";
pub async fn get_workbench_state(
State(state): State<WebState>,
) -> Result<Json<PersistedWorkbenchState>, (StatusCode, Json<ErrorResponse>)> {
let service = state.db_service.clone();
let key = state.workspace_root.display().to_string();
let held: Option<String> = tokio::task::spawn_blocking(move || {
let conn = service
.get_connection()
.map_err(|e| format!("cannot open the workspace database: {e}"))?;
conn.query_row(
SELECT_LAYOUT,
[&key],
|row| row.get::<_, String>(0),
)
.optional()
.map_err(|e| format!("cannot read the saved workbench layout: {e}"))
})
.await
.map_err(|e| internal_error(format!("get_workbench_state task panicked: {e}")))?
.map_err(internal_error)?;
let Some(raw) = held else {
return Err((
StatusCode::NOT_FOUND,
Json(ErrorResponse {
error: "no saved workbench state".to_string(),
}),
));
};
let persisted: PersistedWorkbenchState = serde_json::from_str(&raw).map_err(|e| {
internal_error(format!(
"the saved workbench layout for {} is not readable: {e}",
state.workspace_root.display()
))
})?;
Ok(Json(persisted))
}
pub async fn put_workbench_state(
State(state): State<WebState>,
Json(body): Json<PersistedWorkbenchState>,
) -> Result<Json<PersistedWorkbenchState>, (StatusCode, Json<ErrorResponse>)> {
let service = state.db_service.clone();
let key = state.workspace_root.display().to_string();
let json = serde_json::to_string(&body)
.map_err(|e| internal_error(format!("failed to serialize workbench state: {e}")))?;
tokio::task::spawn_blocking(move || {
let conn = service
.get_connection()
.map_err(|e| format!("cannot open the workspace database: {e}"))?;
conn.execute(
UPSERT_LAYOUT,
rusqlite::params![&key, &json],
)
.map_err(|e| format!("cannot save the workbench layout: {e}"))
})
.await
.map_err(|e| internal_error(format!("put_workbench_state task panicked: {e}")))?
.map_err(internal_error)?;
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 }),
)
}
#[cfg(test)]
#[path = "workbench_state_tests.rs"]
mod tests;