Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 7 additions & 3 deletions src/browser/components/ArchivedWorkspaces.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -616,7 +616,7 @@ export const ArchivedWorkspaces: React.FC<ArchivedWorkspacesProps> = ({
</div>
{/* 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
Expand Down Expand Up @@ -690,7 +690,11 @@ export const ArchivedWorkspaces: React.FC<ArchivedWorkspacesProps> = ({
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}`}
>
<Trash2 className="h-4 w-4" />
{isProcessing ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Trash2 className="h-4 w-4" />
)}
</button>
</TooltipTrigger>
<TooltipContent>Delete permanently (local branch too)</TooltipContent>
Expand Down
3 changes: 3 additions & 0 deletions src/common/orpc/schemas/workspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
21 changes: 16 additions & 5 deletions src/node/services/workspaceService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>();

/** 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,
Expand Down Expand Up @@ -682,7 +687,10 @@ export class WorkspaceService extends EventEmitter {
}

async remove(workspaceId: string, force = false): Promise<Result<void>> {
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)
Expand Down Expand Up @@ -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<FrontendWorkspaceMetadata[]> {
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 [];
Expand Down
Loading