diff --git a/crates/client/src/backend_workspace.rs b/crates/client/src/backend_workspace.rs index a1f093d4..244025ca 100644 --- a/crates/client/src/backend_workspace.rs +++ b/crates/client/src/backend_workspace.rs @@ -2,7 +2,10 @@ use crate::{BackendApiClient, BackendApiClientError}; use reqwest::Method; use serde::{Deserialize, Serialize}; use std::fmt; -use workspace_api::{WorkspaceCreateResponse, WorkspaceRepositoryRecord, WorkspaceSummary}; +use workspace_api::{ + WorkspaceCatalogListResponse, WorkspaceCreateResponse, WorkspaceRepositoryRecord, + WorkspaceSummary, +}; const DEFAULT_WORKSPACE_LIMIT: usize = 200; @@ -88,7 +91,7 @@ async fn list_backend_workspaces_with_client( .send() .await?; client.check_status(response.status())?; - Ok(response.json::>().await?) + Ok(response.json::().await?.0) } pub async fn create_backend_workspace( diff --git a/crates/workspace-api/src/lib.rs b/crates/workspace-api/src/lib.rs index fcbb299a..524e8680 100644 --- a/crates/workspace-api/src/lib.rs +++ b/crates/workspace-api/src/lib.rs @@ -105,6 +105,14 @@ pub struct WorkspaceSummary { pub updated_at: String, } +/// Public response returned by `GET /api/workspaces`. +/// +/// The transparent newtype keeps the established top-level JSON array while making the +/// complete list response a named cross-crate and generated-TypeScript authority. +#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] +#[cfg_attr(feature = "typescript", derive(ts_rs::TS))] +pub struct WorkspaceCatalogListResponse(pub Vec); + /// Public Repository record embedded in Workspace creation responses. #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] #[cfg_attr(feature = "typescript", derive(ts_rs::TS))] @@ -754,6 +762,7 @@ pub fn catalog_typescript() -> String { let config = ts_rs::Config::default(); let declarations = [ WorkspaceSummary::decl(&config), + WorkspaceCatalogListResponse::decl(&config), WorkspaceRepositoryRecord::decl(&config), WorkspaceCreateResponse::decl(&config), WorkspaceAuthConfig::decl(&config), @@ -814,6 +823,17 @@ mod tests { let parsed: WorkspaceResponse = serde_json::from_value(workspace.clone()).unwrap(); assert_eq!(serde_json::to_value(parsed).unwrap(), workspace); + let catalog = serde_json::json!([{ + "workspace_id": "workspace-test", + "owner_account_id": "user-test", + "display_name": "Test", + "state": "active", + "created_at": "2026-01-01T00:00:00Z", + "updated_at": "2026-01-01T00:00:00Z" + }]); + let parsed: WorkspaceCatalogListResponse = serde_json::from_value(catalog.clone()).unwrap(); + assert_eq!(serde_json::to_value(parsed).unwrap(), catalog); + let repositories = serde_json::json!({ "workspace_id": "workspace-test", "items": [{ @@ -860,6 +880,9 @@ mod tests { #[test] fn generated_catalog_typescript_keeps_public_wrappers_and_nullability() { let output = catalog_typescript(); + assert!( + output.contains("export type WorkspaceCatalogListResponse = Array") + ); assert!(output.contains("export type WorkspaceResponse =")); assert!(output.contains("permissions: WorkspacePermissionSummary")); assert!(output.contains("export type RepositoryListResponse =")); diff --git a/crates/workspace-server/src/server.rs b/crates/workspace-server/src/server.rs index a2ae339a..9bdc867b 100644 --- a/crates/workspace-server/src/server.rs +++ b/crates/workspace-server/src/server.rs @@ -67,10 +67,10 @@ use workspace_api::{ RepositoryDetailResponse, RepositoryListResponse, RepositoryLogResponse, RepositorySshCredential, RepositorySshHostTrust, RotateRepositorySshCredentialRequest, RuntimeConnectionTestResponse, RuntimeManagementSummary, TICKET_ORCHESTRATION_PLANS_QUERY_PATH, - TICKET_RELATIONS_QUERY_PATH, WorkspaceCreateResponse, WorkspaceExtensionPointState, - WorkspaceExtensionPoints, WorkspacePermissionSummary, WorkspaceRepositoryRecord, - WorkspaceResponse, WorkspaceRuntimeResource, WorkspaceSummary, WorkspaceWorkerDiscoveryItem, - WorkspaceWorkerDiscoveryPage, WorkspaceWorkerSubject, + TICKET_RELATIONS_QUERY_PATH, WorkspaceCatalogListResponse, WorkspaceCreateResponse, + WorkspaceExtensionPointState, WorkspaceExtensionPoints, WorkspacePermissionSummary, + WorkspaceRepositoryRecord, WorkspaceResponse, WorkspaceRuntimeResource, WorkspaceSummary, + WorkspaceWorkerDiscoveryItem, WorkspaceWorkerDiscoveryPage, WorkspaceWorkerSubject, }; use crate::auth::{ @@ -980,7 +980,9 @@ async fn list_server_workspaces( let owner = match resolve_server_actor(&api, &headers).await { Ok(Some(actor)) => Some(actor.account_id), Ok(None) => match api.catalog.is_empty() { - Ok(true) => return Json(Vec::::new()).into_response(), + Ok(true) => { + return Json(WorkspaceCatalogListResponse(Vec::new())).into_response(); + } Ok(false) => return StatusCode::UNAUTHORIZED.into_response(), Err(error) => return server_error_response(error), }, @@ -990,12 +992,9 @@ async fn list_server_workspaces( .catalog .list(owner.as_deref(), query.limit.unwrap_or(100)) { - Ok(workspaces) => Json( - workspaces - .into_iter() - .map(workspace_summary) - .collect::>(), - ) + Ok(workspaces) => Json(WorkspaceCatalogListResponse( + workspaces.into_iter().map(workspace_summary).collect(), + )) .into_response(), Err(error) => server_error_response(error), } @@ -17784,10 +17783,10 @@ mod tests { let catalog_body = to_bytes(authenticated_catalog.into_body(), usize::MAX) .await .unwrap(); - let typed_catalog: Vec = + let typed_catalog: workspace_api::WorkspaceCatalogListResponse = serde_json::from_slice(&catalog_body).unwrap(); assert_eq!( - typed_catalog[0].workspace_id, + typed_catalog.0[0].workspace_id, workspace.workspace.workspace_id ); diff --git a/web/workspace/src/lib/generated/workspace-api.ts b/web/workspace/src/lib/generated/workspace-api.ts index ebed5766..d753ca65 100644 --- a/web/workspace/src/lib/generated/workspace-api.ts +++ b/web/workspace/src/lib/generated/workspace-api.ts @@ -10,6 +10,8 @@ export type WorkspaceSummary = { updated_at: string; }; +export type WorkspaceCatalogListResponse = Array; + export type WorkspaceRepositoryRecord = { workspace_id: string; repository_id: string; diff --git a/web/workspace/src/lib/workspace/api/workspace-model.ts b/web/workspace/src/lib/workspace/api/workspace-model.ts index a915f17e..11317781 100644 --- a/web/workspace/src/lib/workspace/api/workspace-model.ts +++ b/web/workspace/src/lib/workspace/api/workspace-model.ts @@ -11,6 +11,7 @@ import type { RepositorySourceKind, RepositorySummary, WorkspaceAuthConfig, + WorkspaceCatalogListResponse, WorkspaceCreateResponse, WorkspaceExtensionPoints, WorkspaceExtensionPointState, @@ -28,6 +29,7 @@ export type { RepositoryListResponse, RepositoryLogResponse, RepositorySummary, + WorkspaceCatalogListResponse, WorkspaceCreateResponse, WorkspacePermissionSummary, WorkspaceResponse, @@ -412,7 +414,7 @@ function commitSummary(value: unknown, path: string): GitCommitSummary { export function parseWorkspaceCatalogResponse( value: unknown, -): WorkspaceSummary[] { +): WorkspaceCatalogListResponse { return array(value, "workspaces").map((item, index) => workspaceSummary(item, `workspaces[${index}]`) );