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)
This commit is contained in:
2026-06-01 20:03:24 +02:00
parent 9a036f1968
commit 95efa5d029
6 changed files with 46 additions and 24 deletions
+35
View File
@@ -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("/") @all_workspaces_router.post("/")
async def create_workspace_top_level( async def create_workspace_top_level(
data: dict, data: dict,
+3 -1
View File
@@ -147,7 +147,9 @@ class GitService:
os.unlink(key_path) os.unlink(key_path)
@staticmethod @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. """Check if a branch exists on the remote.
Args: Args:
+1 -3
View File
@@ -71,13 +71,11 @@ export async function updateWorkspace(
} }
export async function deleteWorkspace( export async function deleteWorkspace(
projectId: string,
repoId: string,
workspaceId: string, workspaceId: string,
force = false, force = false,
): Promise<{ status: string }> { ): Promise<{ status: string }> {
const response = await apiClient.delete<{ status: string }>( const response = await apiClient.delete<{ status: string }>(
`${workspaceUrl(projectId, repoId, workspaceId)}?force=${force}`, `/workspaces/${workspaceId}/?force=${force}`,
); );
return response.data; return response.data;
} }
+3 -7
View File
@@ -17,8 +17,6 @@ export interface UseWorkspaceActionsResult {
data: CreateWorkspaceRequest, data: CreateWorkspaceRequest,
) => Promise<Workspace>; ) => Promise<Workspace>;
delete: ( delete: (
projectId: string,
repoId: string,
workspace: Workspace, workspace: Workspace,
onRefresh: () => Promise<void>, onRefresh: () => Promise<void>,
) => Promise<void>; ) => Promise<void>;
@@ -61,14 +59,12 @@ export function useWorkspaceActions(): UseWorkspaceActionsResult {
const deleteAction = useCallback( const deleteAction = useCallback(
async ( async (
projectId: string,
repoId: string,
workspace: Workspace, workspace: Workspace,
onRefresh: () => Promise<void>, onRefresh: () => Promise<void>,
) => { ) => {
setLoadingId(workspace.id); setLoadingId(workspace.id);
try { try {
await deleteWorkspace(projectId, repoId, workspace.id); await deleteWorkspace(workspace.id);
await onRefresh(); await onRefresh();
} catch (err) { } catch (err) {
const error = err as ApiError; const error = err as ApiError;
@@ -81,7 +77,7 @@ export function useWorkspaceActions(): UseWorkspaceActionsResult {
`\n\nDelete workspace and all instances?`, `\n\nDelete workspace and all instances?`,
); );
if (confirmed) { if (confirmed) {
await deleteWorkspace(projectId, repoId, workspace.id, true); await deleteWorkspace(workspace.id, true);
await onRefresh(); await onRefresh();
} }
} else { } else {
@@ -118,7 +114,7 @@ export function useWorkspaceActions(): UseWorkspaceActionsResult {
`${message}\n\nDelete this workspace?`, `${message}\n\nDelete this workspace?`,
); );
if (confirmed) { if (confirmed) {
await deleteWorkspace(projectId, repoId, workspace.id, true); await deleteWorkspace(workspace.id, true);
await onRefresh(); await onRefresh();
} }
} else { } else {
+3 -7
View File
@@ -124,15 +124,11 @@ export const ProjectsPage = () => {
} }
}; };
const handleDeleteWorkspace = async ( const handleDeleteWorkspace = async (workspace: WorkspaceSummary) => {
projectId: string,
repoId: string,
workspace: WorkspaceSummary,
) => {
if (!confirm(`Delete workspace "${workspace.name}"?`)) return; if (!confirm(`Delete workspace "${workspace.name}"?`)) return;
setWorkspaceLoading(workspace.id); setWorkspaceLoading(workspace.id);
try { try {
await deleteWorkspace(projectId, repoId, workspace.id); await deleteWorkspace(workspace.id);
reload(); reload();
} catch (err) { } catch (err) {
alert(err instanceof Error ? err.message : "Failed to delete workspace"); alert(err instanceof Error ? err.message : "Failed to delete workspace");
@@ -187,7 +183,7 @@ export const ProjectsPage = () => {
if (action === "sync") { if (action === "sync") {
void handleSyncWorkspace(project.id, repoId, workspace); void handleSyncWorkspace(project.id, repoId, workspace);
} else if (action === "delete") { } else if (action === "delete") {
void handleDeleteWorkspace(project.id, repoId, workspace); void handleDeleteWorkspace(workspace);
} }
}} }}
workspaceLoading={workspaceLoading} workspaceLoading={workspaceLoading}
+1 -6
View File
@@ -18,12 +18,7 @@ export function WorkspacesPage() {
const actions = useWorkspaceActions(); const actions = useWorkspaceActions();
const handleDelete = async (workspace: Workspace) => { const handleDelete = async (workspace: Workspace) => {
await actions.delete( await actions.delete(workspace, refresh);
workspace.project_id,
workspace.repo_id,
workspace,
refresh,
);
}; };
const handleSync = async (workspace: Workspace) => { const handleSync = async (workspace: Workspace) => {