diff --git a/src/browser/components/ArchivedWorkspaces.tsx b/src/browser/components/ArchivedWorkspaces.tsx index d1f77658de..f36d1fcbed 100644 --- a/src/browser/components/ArchivedWorkspaces.tsx +++ b/src/browser/components/ArchivedWorkspaces.tsx @@ -3,7 +3,7 @@ import { cn } from "@/common/lib/utils"; import type { FrontendWorkspaceMetadata } from "@/common/types/workspace"; import { useWorkspaceContext } from "@/browser/contexts/WorkspaceContext"; import { useAPI } from "@/browser/contexts/API"; -import { Trash2, Search } from "lucide-react"; +import { Trash2, Search, Loader2 } from "lucide-react"; import { ArchiveIcon, ArchiveRestoreIcon } from "./icons/ArchiveIcon"; import { Tooltip, TooltipTrigger, TooltipContent } from "./ui/tooltip"; import { RuntimeBadge } from "./RuntimeBadge"; @@ -616,7 +616,7 @@ export const ArchivedWorkspaces: React.FC = ({ {/* Workspaces in this period */} {periodWorkspaces.map((workspace) => { - const isProcessing = processingIds.has(workspace.id); + const isProcessing = processingIds.has(workspace.id) || workspace.isRemoving; const isSelected = selectedIds.has(workspace.id); const branchForTooltip = workspace.title && workspace.title !== workspace.name @@ -690,7 +690,11 @@ export const ArchivedWorkspaces: React.FC = ({ className="text-muted rounded p-1.5 transition-colors hover:bg-white/10 hover:text-red-400 disabled:opacity-50" aria-label={`Delete workspace ${displayTitle}`} > - + {isProcessing ? ( + + ) : ( + + )} Delete permanently (local branch too) diff --git a/src/common/orpc/schemas/workspace.ts b/src/common/orpc/schemas/workspace.ts index eaa3cafd4a..c7f6571f3f 100644 --- a/src/common/orpc/schemas/workspace.ts +++ b/src/common/orpc/schemas/workspace.ts @@ -92,6 +92,9 @@ export const FrontendWorkspaceMetadataSchema = WorkspaceMetadataSchema.extend({ description: "If set, this workspace has an incompatible runtime configuration (e.g., from a newer version of mux). The workspace should be displayed but interactions should show this error message.", }), + isRemoving: z.boolean().optional().meta({ + description: "True if this workspace is currently being deleted (deletion in progress).", + }), }); export const WorkspaceActivitySnapshotSchema = z.object({ diff --git a/src/node/services/workspaceService.ts b/src/node/services/workspaceService.ts index 9b795f5bb0..dd52e23eb0 100644 --- a/src/node/services/workspaceService.ts +++ b/src/node/services/workspaceService.ts @@ -138,6 +138,11 @@ export class WorkspaceService extends EventEmitter { // Tracks workspaces currently being removed to prevent new sessions/streams during deletion. private readonly removingWorkspaces = new Set(); + /** Check if a workspace is currently being removed. */ + isRemoving(workspaceId: string): boolean { + return this.removingWorkspaces.has(workspaceId); + } + constructor( private readonly config: Config, private readonly historyService: HistoryService, @@ -682,7 +687,10 @@ export class WorkspaceService extends EventEmitter { } async remove(workspaceId: string, force = false): Promise> { - const wasRemoving = this.removingWorkspaces.has(workspaceId); + // Idempotent: if already removing, return success to prevent race conditions + if (this.removingWorkspaces.has(workspaceId)) { + return Ok(undefined); + } this.removingWorkspaces.add(workspaceId); // Try to remove from runtime (filesystem) @@ -793,15 +801,18 @@ export class WorkspaceService extends EventEmitter { const message = error instanceof Error ? error.message : String(error); return Err(`Failed to remove workspace: ${message}`); } finally { - if (!wasRemoving) { - this.removingWorkspaces.delete(workspaceId); - } + this.removingWorkspaces.delete(workspaceId); } } async list(): Promise { try { - return await this.config.getAllWorkspaceMetadata(); + const workspaces = await this.config.getAllWorkspaceMetadata(); + // Enrich with isRemoving status for UI to show deletion spinners + return workspaces.map((w) => ({ + ...w, + isRemoving: this.removingWorkspaces.has(w.id) || undefined, + })); } catch (error) { log.error("Failed to list workspaces:", error); return [];