diff --git a/server/src/__tests__/execution-workspaces-service.test.ts b/server/src/__tests__/execution-workspaces-service.test.ts index c031ad58..d4a50bdc 100644 --- a/server/src/__tests__/execution-workspaces-service.test.ts +++ b/server/src/__tests__/execution-workspaces-service.test.ts @@ -148,7 +148,7 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => { await tempDb?.cleanup(); }); - it("blocks close for shared workspaces that still have open linked issues", async () => { + it("allows archiving shared workspace sessions with warnings even when issues are still open", async () => { const companyId = randomUUID(); const projectId = randomUUID(); const projectWorkspaceId = randomUUID(); @@ -209,14 +209,15 @@ describeEmbeddedPostgres("executionWorkspaceService.getCloseReadiness", () => { expect(readiness).toMatchObject({ workspaceId: executionWorkspaceId, - state: "blocked", + state: "ready_with_warnings", isSharedWorkspace: true, isProjectPrimaryWorkspace: true, - isDestructiveCloseAllowed: false, + isDestructiveCloseAllowed: true, }); - expect(readiness?.blockingReasons).toEqual(expect.arrayContaining([ - "This workspace is still linked to an open issue.", - "Shared execution workspaces are project infrastructure and cannot be destructively closed.", + expect(readiness?.blockingReasons).toEqual([]); + expect(readiness?.warnings).toEqual(expect.arrayContaining([ + "This workspace is still linked to an open issue. Archiving it will detach this shared workspace session from those issues, but keep the underlying project workspace available.", + "This shared workspace session points at project workspace infrastructure. Archiving it only removes the session record.", ])); }); diff --git a/server/src/routes/execution-workspaces.ts b/server/src/routes/execution-workspaces.ts index d4291967..4fa20425 100644 --- a/server/src/routes/execution-workspaces.ts +++ b/server/src/routes/execution-workspaces.ts @@ -1,7 +1,7 @@ import { and, eq } from "drizzle-orm"; import { Router } from "express"; import type { Db } from "@paperclipai/db"; -import { projects, projectWorkspaces } from "@paperclipai/db"; +import { issues, projects, projectWorkspaces } from "@paperclipai/db"; import { updateExecutionWorkspaceSchema } from "@paperclipai/shared"; import { validate } from "../middleware/validate.js"; import { executionWorkspaceService, logActivity, workspaceOperationService } from "../services/index.js"; @@ -303,6 +303,21 @@ export function executionWorkspaceRoutes(db: Db) { } workspace = archivedWorkspace; + if (existing.mode === "shared_workspace") { + await db + .update(issues) + .set({ + executionWorkspaceId: null, + updatedAt: new Date(), + }) + .where( + and( + eq(issues.companyId, existing.companyId), + eq(issues.executionWorkspaceId, existing.id), + ), + ); + } + try { await stopRuntimeServicesForExecutionWorkspace({ db, diff --git a/server/src/services/execution-workspaces.ts b/server/src/services/execution-workspaces.ts index 8b0cebde..a1d3b41d 100644 --- a/server/src/services/execution-workspaces.ts +++ b/server/src/services/execution-workspaces.ts @@ -456,15 +456,19 @@ export function executionWorkspaceService(db: Db) { const blockingIssues = linkedIssueSummaries.filter((issue) => !issue.isTerminal); if (blockingIssues.length > 0) { - blockingReasons.push( + const linkedIssueMessage = blockingIssues.length === 1 ? "This workspace is still linked to an open issue." - : `This workspace is still linked to ${blockingIssues.length} open issues.`, - ); + : `This workspace is still linked to ${blockingIssues.length} open issues.`; + if (isSharedWorkspace) { + warnings.push(`${linkedIssueMessage} Archiving it will detach this shared workspace session from those issues, but keep the underlying project workspace available.`); + } else { + blockingReasons.push(linkedIssueMessage); + } } if (isSharedWorkspace) { - blockingReasons.push("Shared execution workspaces are project infrastructure and cannot be destructively closed."); + warnings.push("This shared workspace session points at project workspace infrastructure. Archiving it only removes the session record."); } if (runtimeServices.some((service) => service.status !== "stopped")) { diff --git a/ui/src/components/ExecutionWorkspaceCloseDialog.tsx b/ui/src/components/ExecutionWorkspaceCloseDialog.tsx index 4ad9a5bb..3b6aef8a 100644 --- a/ui/src/components/ExecutionWorkspaceCloseDialog.tsx +++ b/ui/src/components/ExecutionWorkspaceCloseDialog.tsx @@ -118,7 +118,7 @@ export function ExecutionWorkspaceCloseDialog({