diff --git a/packages/app/src/workspace/archive-undo/internal/undo-window.test.ts b/packages/app/src/workspace/archive-undo/internal/undo-window.test.ts index 1e36395f2a..efa9a539ac 100644 --- a/packages/app/src/workspace/archive-undo/internal/undo-window.test.ts +++ b/packages/app/src/workspace/archive-undo/internal/undo-window.test.ts @@ -164,6 +164,24 @@ describe("archiveWorkspaceWithUndo", () => { }); }); + it("keeps the layout when another client restores the workspace before the toast closes", async () => { + const host = createClient(); + const toast = createToastHost(); + const committed: string[] = []; + + const archive = startArchive({ host, toast, workspaceId: "a", committed }); + host.finishArchive("a"); + await archive; + clearWorkspaceArchivePending({ serverId: SERVER_ID, workspaceId: "a" }); + getHostRuntimeStore().acceptWorkspaceSnapshots(SERVER_ID, [workspace("a")]); + toast.timeOut(); + + expect({ committed, row: storedWorkspace("a") }).toEqual({ + committed: [], + row: workspace("a"), + }); + }); + it("commits when the archive finishes after its toast already timed out", async () => { const host = createClient(); const toast = createToastHost(); diff --git a/packages/app/src/workspace/archive-undo/internal/undo-window.tsx b/packages/app/src/workspace/archive-undo/internal/undo-window.tsx index 65b5d8419e..5dc968124e 100644 --- a/packages/app/src/workspace/archive-undo/internal/undo-window.tsx +++ b/packages/app/src/workspace/archive-undo/internal/undo-window.tsx @@ -3,6 +3,7 @@ import { i18n } from "@/i18n/i18next"; import type { WorkspaceDescriptor } from "@/stores/session-store"; import { beginOptimisticWorkspaceArchive, + isWorkspaceListed, undoWorkspaceArchiveOptimistically, type WorkspaceArchiveTarget, } from "@/workspace/workspace-archive"; @@ -50,7 +51,7 @@ class UndoWindow { /** The host finished the archive. It commits now if undo is no longer offered. */ archiveFinished(): void { this.archived = true; - if (this.phase === "closed") this.input.onArchiveCommitted(); + if (this.phase === "closed") this.commit(); } /** The archive failed on the host, so there is nothing left to undo. */ @@ -63,9 +64,15 @@ class UndoWindow { if (this.phase !== "open") return; this.phase = "closed"; this.release(); - if (this.archived) this.input.onArchiveCommitted(); + if (this.archived) this.commit(); }; + /** Commits unless the workspace is back by now, for example restored from another client. */ + private commit(): void { + if (isWorkspaceListed(this.input.workspace)) return; + this.input.onArchiveCommitted(); + } + readonly undo = (): boolean => { if (this.phase !== "open") return false; this.phase = "undone"; @@ -87,7 +94,8 @@ class UndoWindow { * Archives a workspace and offers to undo it until the toast goes away. * * `onArchiveCommitted` runs once the archive both finished on the host and can no longer be - * undone, so local state the undo would want back (tabs, layout) survives the window. + * undone, and only if the workspace is still gone then. Local state the undo would want back + * (tabs, layout) survives the window, and survives a restore from another client. * Rejects when the archive fails and was not undone; the row is back by then. */ export async function archiveWorkspaceWithUndo(input: { diff --git a/packages/app/src/workspace/workspace-archive.ts b/packages/app/src/workspace/workspace-archive.ts index cc2a24db97..fd757934ef 100644 --- a/packages/app/src/workspace/workspace-archive.ts +++ b/packages/app/src/workspace/workspace-archive.ts @@ -55,15 +55,24 @@ function isWorkspaceArchiveFailure(error: unknown): error is WorkspaceArchiveFai ); } -function hideWorkspaceOptimistically( - workspace: WorkspaceArchiveTarget, -): OptimisticWorkspaceArchiveSnapshot { +function findListedWorkspace(workspace: WorkspaceArchiveTarget): WorkspaceDescriptor | null { const workspaces = useSessionStore.getState().sessions[workspace.serverId]?.workspaces; const workspaceKey = resolveWorkspaceMapKeyByIdentity({ workspaces, workspaceId: workspace.workspaceId, }); - const snapshot = workspaceKey ? (workspaces?.get(workspaceKey) ?? null) : null; + return workspaceKey ? (workspaces?.get(workspaceKey) ?? null) : null; +} + +/** True when the workspace is in this client's list, for example after another client restored it. */ +export function isWorkspaceListed(workspace: WorkspaceArchiveTarget): boolean { + return findListedWorkspace(workspace) !== null; +} + +function hideWorkspaceOptimistically( + workspace: WorkspaceArchiveTarget, +): OptimisticWorkspaceArchiveSnapshot { + const snapshot = findListedWorkspace(workspace); const intent = markWorkspaceArchivePending({ serverId: workspace.serverId, workspaceId: workspace.workspaceId,