Archive any workspace from the app (#2003)
* feat(app): expose archive for every workspace Workspace actions now archive one workspace by id. The daemon remains responsible for backing-specific cleanup, including removing a managed worktree only after its final workspace is archived. * refactor(app): clarify archive descriptor name * fix(app): preserve workspace archive safety * fix(app): allow archive without upstream * refactor(app): remove stale archive callback * test(app): cover workspace archive ownership
This commit is contained in:
@@ -2,20 +2,30 @@ import { useCallback } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { getHostRuntimeStore } from "@/runtime/host-runtime";
|
||||
import { useToast } from "@/contexts/toast-context";
|
||||
import { useCheckoutGitActionsStore } from "@/git/actions-store";
|
||||
import {
|
||||
confirmRiskyWorktreeArchive,
|
||||
DEFAULT_WORKTREE_ARCHIVE_WARNING_LABELS,
|
||||
type WorktreeArchiveWarningLabels,
|
||||
} from "@/git/worktree-archive-warning";
|
||||
import type { WorkspaceDescriptor } from "@/stores/session-store";
|
||||
import {
|
||||
buildWorkspaceTabPersistenceKey,
|
||||
useWorkspaceLayoutStore,
|
||||
} from "@/stores/workspace-layout-store";
|
||||
import { useWorkspaceTabsStore } from "@/stores/workspace-tabs-store";
|
||||
import { archiveWorkspaceOptimistically } from "@/workspace/workspace-archive";
|
||||
import { requireWorkspaceDirectory } from "@/utils/workspace-directory";
|
||||
|
||||
function purgeArchivedWorkspaceState(input: { serverId: string; workspaceId: string }): void {
|
||||
const workspaceKey = buildWorkspaceTabPersistenceKey(input);
|
||||
if (workspaceKey) {
|
||||
useWorkspaceLayoutStore.getState().purgeWorkspace(workspaceKey);
|
||||
}
|
||||
useWorkspaceTabsStore.getState().purgeWorkspace(input);
|
||||
}
|
||||
|
||||
export interface ArchiveWorkspaceInput {
|
||||
serverId: string;
|
||||
workspaceId: string;
|
||||
workspaceDirectory: string | null | undefined;
|
||||
workspaceKind: WorkspaceDescriptor["workspaceKind"];
|
||||
name: string;
|
||||
isDirty?: boolean | null;
|
||||
@@ -34,7 +44,6 @@ export function useWorkspaceArchive(input: ArchiveWorkspaceInput): WorkspaceArch
|
||||
const {
|
||||
serverId,
|
||||
workspaceId,
|
||||
workspaceDirectory,
|
||||
workspaceKind,
|
||||
name,
|
||||
isDirty,
|
||||
@@ -46,37 +55,8 @@ export function useWorkspaceArchive(input: ArchiveWorkspaceInput): WorkspaceArch
|
||||
} = input;
|
||||
const { t } = useTranslation();
|
||||
const toast = useToast();
|
||||
const archiveWorktree = useCheckoutGitActionsStore((state) => state.archiveWorktree);
|
||||
|
||||
const archiveWorktreeRecord = useCallback(() => {
|
||||
let archiveDirectory: string;
|
||||
try {
|
||||
archiveDirectory = requireWorkspaceDirectory({
|
||||
workspaceId,
|
||||
workspaceDirectory,
|
||||
});
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: t("sidebar.workspace.toasts.workspacePathUnavailable"),
|
||||
);
|
||||
return;
|
||||
}
|
||||
onArchiveStarted();
|
||||
void archiveWorktree({
|
||||
serverId,
|
||||
cwd: archiveDirectory,
|
||||
worktreePath: archiveDirectory,
|
||||
workspaceId,
|
||||
}).catch((error) => {
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : t("sidebar.workspace.toasts.archiveFailed"),
|
||||
);
|
||||
});
|
||||
}, [archiveWorktree, onArchiveStarted, serverId, t, toast, workspaceDirectory, workspaceId]);
|
||||
|
||||
const archiveNonWorktreeRecord = useCallback(async () => {
|
||||
const archiveWorkspaceRecord = useCallback(async () => {
|
||||
const client = getHostRuntimeStore().getClient(serverId);
|
||||
if (!client) {
|
||||
toast.error(t("sidebar.workspace.toasts.hostDisconnected"));
|
||||
@@ -84,17 +64,18 @@ export function useWorkspaceArchive(input: ArchiveWorkspaceInput): WorkspaceArch
|
||||
}
|
||||
onSetHiding?.(true);
|
||||
try {
|
||||
onArchiveStarted();
|
||||
await archiveWorkspaceOptimistically({
|
||||
client,
|
||||
workspace: {
|
||||
serverId,
|
||||
workspaceId,
|
||||
},
|
||||
afterHide: onArchiveStarted,
|
||||
});
|
||||
purgeArchivedWorkspaceState({ serverId, workspaceId });
|
||||
} catch (error) {
|
||||
toast.error(
|
||||
error instanceof Error ? error.message : t("sidebar.workspace.toasts.hideFailed"),
|
||||
error instanceof Error ? error.message : t("sidebar.workspace.toasts.archiveFailed"),
|
||||
);
|
||||
} finally {
|
||||
onSetHiding?.(false);
|
||||
@@ -106,7 +87,7 @@ export function useWorkspaceArchive(input: ArchiveWorkspaceInput): WorkspaceArch
|
||||
if (workspaceKind === "worktree") {
|
||||
const confirmed = await confirmRiskyWorktreeArchive(
|
||||
{
|
||||
worktreeName: name,
|
||||
workspaceName: name,
|
||||
isDirty,
|
||||
aheadOfOrigin,
|
||||
diffStat,
|
||||
@@ -116,15 +97,12 @@ export function useWorkspaceArchive(input: ArchiveWorkspaceInput): WorkspaceArch
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
archiveWorktreeRecord();
|
||||
return;
|
||||
}
|
||||
await archiveNonWorktreeRecord();
|
||||
await archiveWorkspaceRecord();
|
||||
})();
|
||||
}, [
|
||||
aheadOfOrigin,
|
||||
archiveNonWorktreeRecord,
|
||||
archiveWorktreeRecord,
|
||||
archiveWorkspaceRecord,
|
||||
diffStat,
|
||||
isDirty,
|
||||
name,
|
||||
|
||||
Reference in New Issue
Block a user