fix: keep archived worktrees optimistically hidden (#640)
This commit is contained in:
@@ -83,6 +83,7 @@ function workspace(input: {
|
||||
workspaceKind: input.name === "main" ? "local_checkout" : "worktree",
|
||||
name: input.name,
|
||||
status: input.status ?? "done",
|
||||
archivingAt: null,
|
||||
diffStat: null,
|
||||
scripts: input.scripts ?? [],
|
||||
};
|
||||
|
||||
@@ -1462,7 +1462,7 @@ function WorkspaceRowWithMenu({
|
||||
: "idle",
|
||||
);
|
||||
const isWorktree = workspace.workspaceKind === "worktree";
|
||||
const isArchiving = isWorktree ? archiveStatus === "pending" : isArchivingWorkspace;
|
||||
const isArchiving = isWorktree ? workspace.archivingAt !== null : isArchivingWorkspace;
|
||||
const redirectAfterArchive = useCallback(() => {
|
||||
redirectIfArchivingActiveWorkspace({
|
||||
serverId: workspace.serverId,
|
||||
|
||||
@@ -31,6 +31,7 @@ function workspaceDescriptor(input: {
|
||||
workspaceKind: "worktree",
|
||||
name: input.name ?? input.id,
|
||||
status: "done",
|
||||
archivingAt: null,
|
||||
diffStat: null,
|
||||
scripts: [],
|
||||
};
|
||||
|
||||
@@ -18,6 +18,7 @@ function workspace(input: {
|
||||
workspaceKind: "checkout",
|
||||
name: "main",
|
||||
status: "running",
|
||||
archivingAt: null,
|
||||
diffStat: null,
|
||||
scripts: input.scripts ?? [],
|
||||
};
|
||||
|
||||
@@ -41,6 +41,7 @@ import {
|
||||
normalizeWorkspaceDescriptor,
|
||||
} from "@/stores/session-store";
|
||||
import { useDraftStore } from "@/stores/draft-store";
|
||||
import { isLocalWorktreeArchivePending } from "@/stores/checkout-git-actions-store";
|
||||
import { useWorkspaceSetupStore } from "@/stores/workspace-setup-store";
|
||||
import { sendOsNotification } from "@/utils/os-notifications";
|
||||
import { getIsAppActivelyVisible } from "@/utils/app-visibility";
|
||||
@@ -58,6 +59,7 @@ import type { AttachmentMetadata } from "@/attachments/types";
|
||||
import { splitComposerAttachmentsForSubmit } from "@/components/composer-attachments";
|
||||
import { reconcilePreviousAgentStatuses } from "@/contexts/session-status-tracking";
|
||||
import { patchWorkspaceScripts } from "@/contexts/session-workspace-scripts";
|
||||
import { shouldSuppressWorkspaceUpsertForLocalArchive } from "@/contexts/session-workspace-upserts";
|
||||
import { isNative } from "@/constants/platform";
|
||||
import { useToast } from "@/contexts/toast-context";
|
||||
import { toErrorMessage } from "@/utils/error-messages";
|
||||
@@ -1232,6 +1234,15 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
|
||||
return;
|
||||
}
|
||||
const workspace = normalizeWorkspaceDescriptor(message.payload.workspace);
|
||||
if (
|
||||
shouldSuppressWorkspaceUpsertForLocalArchive({
|
||||
serverId,
|
||||
workspace,
|
||||
isArchivePending: isLocalWorktreeArchivePending,
|
||||
})
|
||||
) {
|
||||
return;
|
||||
}
|
||||
mergeWorkspaces(serverId, [workspace]);
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { WorkspaceDescriptor } from "@/stores/session-store";
|
||||
import { shouldSuppressWorkspaceUpsertForLocalArchive } from "@/contexts/session-workspace-upserts";
|
||||
|
||||
const baseWorkspace: WorkspaceDescriptor = {
|
||||
id: "/repo/worktree",
|
||||
projectId: "/repo",
|
||||
projectDisplayName: "Repo",
|
||||
projectRootPath: "/repo",
|
||||
workspaceDirectory: "/repo/worktree",
|
||||
projectKind: "git",
|
||||
workspaceKind: "worktree",
|
||||
name: "feature",
|
||||
status: "done",
|
||||
archivingAt: "2026-04-30T00:00:00.000Z",
|
||||
diffStat: null,
|
||||
scripts: [],
|
||||
};
|
||||
|
||||
function workspace(input?: Partial<WorkspaceDescriptor>): WorkspaceDescriptor {
|
||||
return { ...baseWorkspace, ...input };
|
||||
}
|
||||
|
||||
describe("shouldSuppressWorkspaceUpsertForLocalArchive", () => {
|
||||
it("suppresses archiving upserts for a locally pending archive", () => {
|
||||
const isArchivePending = vi.fn(() => true);
|
||||
|
||||
expect(
|
||||
shouldSuppressWorkspaceUpsertForLocalArchive({
|
||||
serverId: "server-1",
|
||||
workspace: workspace({ workspaceDirectory: "/repo/worktree" }),
|
||||
isArchivePending,
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(isArchivePending).toHaveBeenCalledWith({
|
||||
serverId: "server-1",
|
||||
cwd: "/repo/worktree",
|
||||
});
|
||||
});
|
||||
|
||||
it("allows archiving upserts when this client did not start the archive", () => {
|
||||
expect(
|
||||
shouldSuppressWorkspaceUpsertForLocalArchive({
|
||||
serverId: "server-1",
|
||||
workspace: workspace(),
|
||||
isArchivePending: () => false,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("allows normal upserts while a local archive is pending", () => {
|
||||
expect(
|
||||
shouldSuppressWorkspaceUpsertForLocalArchive({
|
||||
serverId: "server-1",
|
||||
workspace: workspace({ archivingAt: null }),
|
||||
isArchivePending: () => true,
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,15 @@
|
||||
import type { WorkspaceDescriptor } from "@/stores/session-store";
|
||||
|
||||
export function shouldSuppressWorkspaceUpsertForLocalArchive(input: {
|
||||
serverId: string;
|
||||
workspace: WorkspaceDescriptor;
|
||||
isArchivePending: (params: { serverId: string; cwd: string }) => boolean;
|
||||
}): boolean {
|
||||
return (
|
||||
input.workspace.archivingAt !== null &&
|
||||
input.isArchivePending({
|
||||
serverId: input.serverId,
|
||||
cwd: input.workspace.workspaceDirectory,
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -78,6 +78,7 @@ describe("openProjectDirectly", () => {
|
||||
projectKind: "git" as const,
|
||||
workspaceKind: "checkout" as const,
|
||||
name: "project",
|
||||
archivingAt: null,
|
||||
status: "done" as const,
|
||||
activityAt: null,
|
||||
diffStat: null,
|
||||
|
||||
@@ -87,6 +87,7 @@ function workspace(input: {
|
||||
projectKind: "git",
|
||||
workspaceKind: "local_checkout",
|
||||
name: input.id,
|
||||
archivingAt: null,
|
||||
status: "done",
|
||||
activityAt: null,
|
||||
diffStat: null,
|
||||
|
||||
@@ -57,6 +57,7 @@ function workspaceDescriptor(id: string): WorkspaceDescriptor {
|
||||
workspaceKind: "worktree",
|
||||
name: id,
|
||||
status: "done",
|
||||
archivingAt: null,
|
||||
diffStat: null,
|
||||
scripts: [],
|
||||
};
|
||||
|
||||
@@ -28,6 +28,7 @@ export interface SidebarWorkspaceEntry {
|
||||
workspaceKind: WorkspaceDescriptor["workspaceKind"];
|
||||
name: string;
|
||||
statusBucket: SidebarStateBucket;
|
||||
archivingAt: string | null;
|
||||
diffStat: { additions: number; deletions: number } | null;
|
||||
scripts: WorkspaceDescriptor["scripts"];
|
||||
hasRunningScripts: boolean;
|
||||
@@ -65,6 +66,7 @@ function createStructuralWorkspaceEntry(input: {
|
||||
workspaceKind: "checkout",
|
||||
name: input.workspaceId,
|
||||
statusBucket: "done",
|
||||
archivingAt: null,
|
||||
diffStat: null,
|
||||
scripts: [],
|
||||
hasRunningScripts: false,
|
||||
@@ -86,6 +88,7 @@ export function createSidebarWorkspaceEntry(input: {
|
||||
workspaceKind: input.workspace.workspaceKind,
|
||||
name: input.workspace.name,
|
||||
statusBucket: input.workspace.status,
|
||||
archivingAt: input.workspace.archivingAt,
|
||||
diffStat: input.workspace.diffStat,
|
||||
scripts: input.workspace.scripts,
|
||||
hasRunningScripts: input.workspace.scripts.some((script) => script.lifecycle === "running"),
|
||||
|
||||
@@ -26,6 +26,7 @@ function createWorkspaceDescriptor(input: Partial<WorkspaceDescriptor> = {}): Wo
|
||||
diffStat: null,
|
||||
scripts: [],
|
||||
...input,
|
||||
archivingAt: input.archivingAt ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ import type { WorkspaceDescriptor } from "@/stores/session-store";
|
||||
import {
|
||||
__resetCheckoutGitActionsStoreForTests,
|
||||
invalidateCheckoutGitQueriesForClient,
|
||||
isLocalWorktreeArchivePending,
|
||||
useCheckoutGitActionsStore,
|
||||
} from "@/stores/checkout-git-actions-store";
|
||||
|
||||
@@ -39,6 +40,7 @@ function workspace(input: Partial<WorkspaceDescriptor> & Pick<WorkspaceDescripto
|
||||
workspaceKind: input.workspaceKind ?? "worktree",
|
||||
name: input.name ?? input.id,
|
||||
status: input.status ?? "done",
|
||||
archivingAt: input.archivingAt ?? null,
|
||||
diffStat: input.diffStat ?? null,
|
||||
scripts: input.scripts ?? [],
|
||||
} satisfies WorkspaceDescriptor;
|
||||
@@ -207,6 +209,7 @@ describe("checkout-git-actions-store", () => {
|
||||
expect(appQueryClient.getQueryData(["sidebarPaseoWorktreeList", serverId, "/tmp"])).toEqual([
|
||||
{ worktreePath: "/tmp/other" },
|
||||
]);
|
||||
expect(isLocalWorktreeArchivePending({ serverId, cwd })).toBe(true);
|
||||
|
||||
deferred.resolve({});
|
||||
await archive;
|
||||
@@ -233,4 +236,25 @@ describe("checkout-git-actions-store", () => {
|
||||
listSnapshot,
|
||||
);
|
||||
});
|
||||
|
||||
it("reports local archive pending only while the archive action is in flight", async () => {
|
||||
const deferred = createDeferred<Record<string, never>>();
|
||||
const client = {
|
||||
archivePaseoWorktree: vi.fn(() => deferred.promise),
|
||||
};
|
||||
const featureWorkspace = workspace({ id: cwd, name: "feature" });
|
||||
useSessionStore.getState().initializeSession(serverId, client as unknown as DaemonClient);
|
||||
useSessionStore.getState().setWorkspaces(serverId, new Map([[cwd, featureWorkspace]]));
|
||||
|
||||
const archive = useCheckoutGitActionsStore
|
||||
.getState()
|
||||
.archiveWorktree({ serverId, cwd, worktreePath: cwd });
|
||||
|
||||
expect(isLocalWorktreeArchivePending({ serverId, cwd })).toBe(true);
|
||||
|
||||
deferred.resolve({});
|
||||
await archive;
|
||||
|
||||
expect(isLocalWorktreeArchivePending({ serverId, cwd })).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -191,7 +191,6 @@ function removeWorktreeFromSessionStore(input: { serverId: string; worktreePath:
|
||||
|
||||
function restoreWorktreeArchiveState(input: {
|
||||
serverId: string;
|
||||
worktreePath: string;
|
||||
snapshot: WorktreeArchiveSnapshot;
|
||||
}): void {
|
||||
if (input.snapshot.workspace) {
|
||||
@@ -223,6 +222,16 @@ function inFlightKey(key: CheckoutKey, actionId: CheckoutGitAsyncActionId): stri
|
||||
return `${key}::${actionId}`;
|
||||
}
|
||||
|
||||
export function isLocalWorktreeArchivePending(input: { serverId: string; cwd: string }): boolean {
|
||||
return (
|
||||
useCheckoutGitActionsStore.getState().getStatus({
|
||||
serverId: input.serverId,
|
||||
cwd: input.cwd,
|
||||
actionId: "archive-worktree",
|
||||
}) === "pending"
|
||||
);
|
||||
}
|
||||
|
||||
interface CheckoutGitActionsStoreState {
|
||||
statusByCheckout: Record<CheckoutKey, StatusMap>;
|
||||
|
||||
@@ -436,7 +445,7 @@ export const useCheckoutGitActionsStore = create<CheckoutGitActionsStoreState>()
|
||||
throw new Error(payload.error.message);
|
||||
}
|
||||
} catch (error) {
|
||||
restoreWorktreeArchiveState({ serverId, worktreePath, snapshot });
|
||||
restoreWorktreeArchiveState({ serverId, snapshot });
|
||||
throw error;
|
||||
}
|
||||
invalidateWorktreeList();
|
||||
|
||||
@@ -34,6 +34,7 @@ function createWorkspace(
|
||||
workspaceKind: input.workspaceKind ?? "local_checkout",
|
||||
name: input.name ?? "main",
|
||||
status: input.status ?? "done",
|
||||
archivingAt: input.archivingAt ?? null,
|
||||
diffStat: input.diffStat ?? null,
|
||||
scripts: input.scripts ?? [],
|
||||
};
|
||||
|
||||
@@ -23,6 +23,7 @@ function createWorkspace(
|
||||
workspaceKind: input.workspaceKind ?? "local_checkout",
|
||||
name: input.name ?? "main",
|
||||
status: input.status ?? "done",
|
||||
archivingAt: input.archivingAt ?? null,
|
||||
diffStat: input.diffStat ?? null,
|
||||
scripts: input.scripts ?? [],
|
||||
};
|
||||
@@ -73,6 +74,7 @@ describe("normalizeWorkspaceDescriptor", () => {
|
||||
projectKind: "git",
|
||||
workspaceKind: "checkout",
|
||||
name: "main",
|
||||
archivingAt: null,
|
||||
status: "running",
|
||||
activityAt: "not-a-date",
|
||||
diffStat: null,
|
||||
@@ -105,6 +107,7 @@ describe("normalizeWorkspaceDescriptor", () => {
|
||||
projectKind: "git",
|
||||
workspaceKind: "checkout",
|
||||
name: "main",
|
||||
archivingAt: null,
|
||||
status: "done",
|
||||
activityAt: null,
|
||||
diffStat: null,
|
||||
@@ -116,6 +119,27 @@ describe("normalizeWorkspaceDescriptor", () => {
|
||||
expect(workspace.scripts).toEqual([]);
|
||||
});
|
||||
|
||||
it("defaults missing archivingAt to null", () => {
|
||||
const payload = {
|
||||
id: "1",
|
||||
projectId: "1",
|
||||
projectDisplayName: "Project 1",
|
||||
projectRootPath: "/repo",
|
||||
workspaceDirectory: "/repo",
|
||||
projectKind: "git",
|
||||
workspaceKind: "checkout",
|
||||
name: "main",
|
||||
status: "done",
|
||||
activityAt: null,
|
||||
diffStat: null,
|
||||
scripts: [],
|
||||
} as unknown as WorkspaceDescriptorPayload;
|
||||
|
||||
const workspace = normalizeWorkspaceDescriptor(payload);
|
||||
|
||||
expect(workspace.archivingAt).toBeNull();
|
||||
});
|
||||
|
||||
it("preserves project placement from workspace descriptor payloads", () => {
|
||||
const workspace = normalizeWorkspaceDescriptor({
|
||||
id: "1",
|
||||
@@ -126,6 +150,7 @@ describe("normalizeWorkspaceDescriptor", () => {
|
||||
projectKind: "git",
|
||||
workspaceKind: "local_checkout",
|
||||
name: "main",
|
||||
archivingAt: null,
|
||||
status: "done",
|
||||
activityAt: null,
|
||||
diffStat: null,
|
||||
|
||||
@@ -117,6 +117,7 @@ export interface WorkspaceDescriptor {
|
||||
workspaceKind: WorkspaceDescriptorPayload["workspaceKind"];
|
||||
name: string;
|
||||
status: WorkspaceDescriptorPayload["status"];
|
||||
archivingAt: string | null;
|
||||
diffStat: { additions: number; deletions: number } | null;
|
||||
scripts: WorkspaceDescriptorPayload["scripts"];
|
||||
gitRuntime?: WorkspaceDescriptorPayload["gitRuntime"];
|
||||
@@ -137,6 +138,7 @@ export function normalizeWorkspaceDescriptor(
|
||||
workspaceKind: payload.workspaceKind,
|
||||
name: payload.name,
|
||||
status: payload.status,
|
||||
archivingAt: payload.archivingAt ?? null,
|
||||
diffStat: payload.diffStat ?? null,
|
||||
scripts: (payload.scripts ?? []).map((s) => Object.assign({}, s)),
|
||||
gitRuntime: payload.gitRuntime,
|
||||
|
||||
@@ -43,6 +43,7 @@ function workspace(input: {
|
||||
workspaceKind: "local_checkout",
|
||||
name: input.id,
|
||||
status: "done",
|
||||
archivingAt: null,
|
||||
diffStat: null,
|
||||
scripts: [],
|
||||
gitRuntime: {
|
||||
|
||||
@@ -23,6 +23,7 @@ function workspace(overrides: Partial<SidebarWorkspaceEntry> = {}): SidebarWorks
|
||||
scripts: [],
|
||||
hasRunningScripts: false,
|
||||
...overrides,
|
||||
archivingAt: overrides.archivingAt ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ function workspace(input: {
|
||||
workspaceKind: "checkout",
|
||||
name: input.name,
|
||||
statusBucket: "done",
|
||||
archivingAt: null,
|
||||
diffStat: null,
|
||||
scripts: [],
|
||||
hasRunningScripts: false,
|
||||
|
||||
@@ -32,6 +32,7 @@ function workspace(
|
||||
workspaceKind: input.workspaceKind ?? "worktree",
|
||||
name: input.name ?? input.id,
|
||||
status: input.status ?? "done",
|
||||
archivingAt: input.archivingAt ?? null,
|
||||
diffStat: input.diffStat ?? null,
|
||||
scripts: input.scripts ?? [],
|
||||
};
|
||||
|
||||
@@ -21,6 +21,7 @@ function createWorkspace(
|
||||
workspaceKind: input.workspaceKind ?? "checkout",
|
||||
name: input.name ?? "main",
|
||||
status: input.status ?? "running",
|
||||
archivingAt: input.archivingAt ?? null,
|
||||
diffStat: input.diffStat ?? null,
|
||||
scripts: input.scripts ?? [],
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user