diff --git a/packages/core/workspace/mutations.test.tsx b/packages/core/workspace/mutations.test.tsx
new file mode 100644
index 0000000000..d7e7965e8c
--- /dev/null
+++ b/packages/core/workspace/mutations.test.tsx
@@ -0,0 +1,121 @@
+/**
+ * @vitest-environment jsdom
+ */
+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
+import { act, renderHook, waitFor } from "@testing-library/react";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import type { ReactNode } from "react";
+import { setApiInstance } from "../api";
+import type { ApiClient } from "../api/client";
+import type { Workspace } from "../types";
+import { useDeleteWorkspace } from "./mutations";
+import { workspaceKeys } from "./queries";
+
+function createWrapper(qc: QueryClient) {
+ return function Wrapper({ children }: { children: ReactNode }) {
+ return {children};
+ };
+}
+
+const makeWorkspace = (id: string, slug: string): Workspace => ({
+ id,
+ name: slug,
+ slug,
+ description: null,
+ context: null,
+ settings: {},
+ repos: [],
+ issue_prefix: "MUL",
+ avatar_url: null,
+ created_at: "2026-01-01T00:00:00Z",
+ updated_at: "2026-01-01T00:00:00Z",
+});
+
+describe("useDeleteWorkspace", () => {
+ let qc: QueryClient;
+ let deleteWorkspace: ReturnType Promise>>;
+
+ const seedList = () => {
+ qc.setQueryData(workspaceKeys.list(), [
+ makeWorkspace("ws-1", "keep-me"),
+ makeWorkspace("ws-2", "delete-me"),
+ ]);
+ };
+
+ const cachedList = () =>
+ qc.getQueryData(workspaceKeys.list()) ?? [];
+
+ beforeEach(() => {
+ qc = new QueryClient({ defaultOptions: { queries: { retry: false } } });
+ deleteWorkspace = vi.fn().mockResolvedValue(undefined);
+ setApiInstance({ deleteWorkspace } as unknown as ApiClient);
+ });
+
+ afterEach(() => {
+ qc.clear();
+ vi.restoreAllMocks();
+ });
+
+ it("removes the workspace from the list cache while the DELETE is pending", async () => {
+ seedList();
+ // Hold the DELETE open so we can observe the pending window — the bug
+ // was that during this window the list cache still contained the
+ // workspace, so any consumer re-presented it as selectable/current.
+ let resolveDelete!: () => void;
+ deleteWorkspace.mockReturnValue(
+ new Promise((resolve) => {
+ resolveDelete = resolve;
+ }),
+ );
+
+ const { result } = renderHook(() => useDeleteWorkspace(), {
+ wrapper: createWrapper(qc),
+ });
+
+ let mutationDone: Promise;
+ await act(async () => {
+ mutationDone = result.current.mutateAsync("ws-2");
+ // Let onMutate (cancelQueries + optimistic removal) run.
+ await Promise.resolve();
+ });
+
+ expect(deleteWorkspace).toHaveBeenCalledWith("ws-2");
+ expect(cachedList().map((w) => w.id)).toEqual(["ws-1"]);
+
+ await act(async () => {
+ resolveDelete();
+ await mutationDone;
+ });
+ expect(cachedList().map((w) => w.id)).toEqual(["ws-1"]);
+ });
+
+ it("invalidates the workspace list after a successful delete", async () => {
+ seedList();
+ const { result } = renderHook(() => useDeleteWorkspace(), {
+ wrapper: createWrapper(qc),
+ });
+
+ await act(async () => {
+ await result.current.mutateAsync("ws-2");
+ });
+
+ expect(qc.getQueryState(workspaceKeys.list())?.isInvalidated).toBe(true);
+ });
+
+ it("rolls the list back when the DELETE fails", async () => {
+ seedList();
+ deleteWorkspace.mockRejectedValue(new Error("boom"));
+
+ const { result } = renderHook(() => useDeleteWorkspace(), {
+ wrapper: createWrapper(qc),
+ });
+
+ await act(async () => {
+ await expect(result.current.mutateAsync("ws-2")).rejects.toThrow("boom");
+ });
+
+ await waitFor(() => {
+ expect(cachedList().map((w) => w.id)).toEqual(["ws-1", "ws-2"]);
+ });
+ });
+});
diff --git a/packages/core/workspace/mutations.ts b/packages/core/workspace/mutations.ts
index f23bb7f964..32e9c70571 100644
--- a/packages/core/workspace/mutations.ts
+++ b/packages/core/workspace/mutations.ts
@@ -39,6 +39,30 @@ export function useDeleteWorkspace() {
const qc = useQueryClient();
return useMutation({
mutationFn: (workspaceId: string) => api.deleteWorkspace(workspaceId),
+ // Optimistically drop the workspace from the list cache while the
+ // DELETE is in flight. The delete flow navigates away BEFORE awaiting
+ // the mutation (see workspace-tab.tsx's navigateAwayFromCurrentWorkspace
+ // for the CancelledError race that forces that ordering), so during the
+ // pending window every list consumer — sidebar switcher, by-slug route
+ // resolution, post-auth destination — must already see the workspace as
+ // gone, or a concurrent list refetch re-presents it as selectable and
+ // it can be re-entered mid-delete.
+ onMutate: async (workspaceId) => {
+ // Cancel in-flight list fetches so a response that started before the
+ // delete can't land after the optimistic update and resurrect the row.
+ await qc.cancelQueries({ queryKey: workspaceKeys.list() });
+ const previous = qc.getQueryData(workspaceKeys.list());
+ qc.setQueryData(workspaceKeys.list(), (old) =>
+ old?.filter((w) => w.id !== workspaceId),
+ );
+ return { previous };
+ },
+ // Rollback: the server still has the workspace, so put it back in the
+ // list (the caller surfaces the error toast). onSettled's invalidate
+ // then reconciles against server truth either way.
+ onError: (_err, _workspaceId, ctx) => {
+ if (ctx?.previous) qc.setQueryData(workspaceKeys.list(), ctx.previous);
+ },
onSettled: () => {
qc.invalidateQueries({ queryKey: workspaceKeys.list() });
},