From 95efa5d0291845cfe23c22dfe057ec208716bfdf Mon Sep 17 00:00:00 2001 From: Alex Blank Date: Mon, 1 Jun 2026 20:03:24 +0200 Subject: [PATCH] fix: workspace delete mixed-content error via top-level endpoint - Add top-level DELETE /workspaces/{workspace_id} endpoint (avoids nested path) - Frontend deleteWorkspace now uses /workspaces/{id}/?force=... (no project/repo needed) - Update useWorkspaceActions, ProjectsPage, WorkspacesPage to match new signature Quality gates: ruff clean, tsc --noEmit clean, pytest workspaces API (9 passed, 1 skipped) --- apps/api/src/api/workspaces.py | 35 +++++++++++++++++++++ apps/api/src/services/git_service.py | 4 ++- apps/web/src/api/workspaces.ts | 4 +-- apps/web/src/hooks/use-workspace-actions.ts | 10 ++---- apps/web/src/pages/projects.tsx | 10 ++---- apps/web/src/pages/workspaces.tsx | 7 +---- 6 files changed, 46 insertions(+), 24 deletions(-) diff --git a/apps/api/src/api/workspaces.py b/apps/api/src/api/workspaces.py index 61477ed..5bdda47 100644 --- a/apps/api/src/api/workspaces.py +++ b/apps/api/src/api/workspaces.py @@ -67,6 +67,41 @@ async def list_all_workspaces( ] +@all_workspaces_router.delete("/{workspace_id}") +async def delete_workspace_top_level( + workspace_id: uuid.UUID, + force: bool = Query(False), + user_id: uuid.UUID = Depends(get_current_user_id), + session: AsyncSession = Depends(get_db_session), +) -> dict: + """Delete a workspace via top-level path.""" + workspace = await session.get(Workspace, workspace_id) + if not workspace or workspace.user_id != user_id: + raise HTTPException(status_code=404, detail="Workspace not found") + + manager = WorkspaceManager() + try: + await manager.delete(workspace, force=force, session=session) + await session.commit() + except WorkspaceHasInstancesError as exc: + await session.rollback() + raise HTTPException( + status_code=409, + detail={ + "message": "Workspace has running tool instances", + "instances": [{"id": str(i.id), "name": i.name} for i in exc.instances], + }, + ) from exc + except Exception as exc: + await session.rollback() + logger.error("Failed to delete workspace: %s", exc) + raise HTTPException( + status_code=500, detail="Failed to delete workspace" + ) from exc + + return {"status": "deleted"} + + @all_workspaces_router.post("/") async def create_workspace_top_level( data: dict, diff --git a/apps/api/src/services/git_service.py b/apps/api/src/services/git_service.py index b076ede..8e81f10 100644 --- a/apps/api/src/services/git_service.py +++ b/apps/api/src/services/git_service.py @@ -147,7 +147,9 @@ class GitService: os.unlink(key_path) @staticmethod - def branch_exists_remotely(path: str, branch: str, ssh_key: str | None = None) -> bool: + def branch_exists_remotely( + path: str, branch: str, ssh_key: str | None = None + ) -> bool: """Check if a branch exists on the remote. Args: diff --git a/apps/web/src/api/workspaces.ts b/apps/web/src/api/workspaces.ts index 9636b7c..c032f54 100644 --- a/apps/web/src/api/workspaces.ts +++ b/apps/web/src/api/workspaces.ts @@ -71,13 +71,11 @@ export async function updateWorkspace( } export async function deleteWorkspace( - projectId: string, - repoId: string, workspaceId: string, force = false, ): Promise<{ status: string }> { const response = await apiClient.delete<{ status: string }>( - `${workspaceUrl(projectId, repoId, workspaceId)}?force=${force}`, + `/workspaces/${workspaceId}/?force=${force}`, ); return response.data; } diff --git a/apps/web/src/hooks/use-workspace-actions.ts b/apps/web/src/hooks/use-workspace-actions.ts index 52b5d99..968c163 100644 --- a/apps/web/src/hooks/use-workspace-actions.ts +++ b/apps/web/src/hooks/use-workspace-actions.ts @@ -17,8 +17,6 @@ export interface UseWorkspaceActionsResult { data: CreateWorkspaceRequest, ) => Promise; delete: ( - projectId: string, - repoId: string, workspace: Workspace, onRefresh: () => Promise, ) => Promise; @@ -61,14 +59,12 @@ export function useWorkspaceActions(): UseWorkspaceActionsResult { const deleteAction = useCallback( async ( - projectId: string, - repoId: string, workspace: Workspace, onRefresh: () => Promise, ) => { setLoadingId(workspace.id); try { - await deleteWorkspace(projectId, repoId, workspace.id); + await deleteWorkspace(workspace.id); await onRefresh(); } catch (err) { const error = err as ApiError; @@ -81,7 +77,7 @@ export function useWorkspaceActions(): UseWorkspaceActionsResult { `\n\nDelete workspace and all instances?`, ); if (confirmed) { - await deleteWorkspace(projectId, repoId, workspace.id, true); + await deleteWorkspace(workspace.id, true); await onRefresh(); } } else { @@ -118,7 +114,7 @@ export function useWorkspaceActions(): UseWorkspaceActionsResult { `${message}\n\nDelete this workspace?`, ); if (confirmed) { - await deleteWorkspace(projectId, repoId, workspace.id, true); + await deleteWorkspace(workspace.id, true); await onRefresh(); } } else { diff --git a/apps/web/src/pages/projects.tsx b/apps/web/src/pages/projects.tsx index 103d510..9f71b29 100644 --- a/apps/web/src/pages/projects.tsx +++ b/apps/web/src/pages/projects.tsx @@ -124,15 +124,11 @@ export const ProjectsPage = () => { } }; - const handleDeleteWorkspace = async ( - projectId: string, - repoId: string, - workspace: WorkspaceSummary, - ) => { + const handleDeleteWorkspace = async (workspace: WorkspaceSummary) => { if (!confirm(`Delete workspace "${workspace.name}"?`)) return; setWorkspaceLoading(workspace.id); try { - await deleteWorkspace(projectId, repoId, workspace.id); + await deleteWorkspace(workspace.id); reload(); } catch (err) { alert(err instanceof Error ? err.message : "Failed to delete workspace"); @@ -187,7 +183,7 @@ export const ProjectsPage = () => { if (action === "sync") { void handleSyncWorkspace(project.id, repoId, workspace); } else if (action === "delete") { - void handleDeleteWorkspace(project.id, repoId, workspace); + void handleDeleteWorkspace(workspace); } }} workspaceLoading={workspaceLoading} diff --git a/apps/web/src/pages/workspaces.tsx b/apps/web/src/pages/workspaces.tsx index 9802f17..f688a2e 100644 --- a/apps/web/src/pages/workspaces.tsx +++ b/apps/web/src/pages/workspaces.tsx @@ -18,12 +18,7 @@ export function WorkspacesPage() { const actions = useWorkspaceActions(); const handleDelete = async (workspace: Workspace) => { - await actions.delete( - workspace.project_id, - workspace.repo_id, - workspace, - refresh, - ); + await actions.delete(workspace, refresh); }; const handleSync = async (workspace: Workspace) => {