* 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
117 lines
3.2 KiB
TypeScript
117 lines
3.2 KiB
TypeScript
import { useCallback } from "react";
|
|
import { useTranslation } from "react-i18next";
|
|
import { getHostRuntimeStore } from "@/runtime/host-runtime";
|
|
import { useToast } from "@/contexts/toast-context";
|
|
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";
|
|
|
|
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;
|
|
workspaceKind: WorkspaceDescriptor["workspaceKind"];
|
|
name: string;
|
|
isDirty?: boolean | null;
|
|
aheadOfOrigin?: number | null;
|
|
diffStat?: { additions: number; deletions: number } | null;
|
|
warningLabels?: WorktreeArchiveWarningLabels;
|
|
onArchiveStarted: () => void;
|
|
onSetHiding?: (hiding: boolean) => void;
|
|
}
|
|
|
|
export interface WorkspaceArchiveController {
|
|
archive: () => void;
|
|
}
|
|
|
|
export function useWorkspaceArchive(input: ArchiveWorkspaceInput): WorkspaceArchiveController {
|
|
const {
|
|
serverId,
|
|
workspaceId,
|
|
workspaceKind,
|
|
name,
|
|
isDirty,
|
|
aheadOfOrigin,
|
|
diffStat,
|
|
warningLabels = DEFAULT_WORKTREE_ARCHIVE_WARNING_LABELS,
|
|
onArchiveStarted,
|
|
onSetHiding,
|
|
} = input;
|
|
const { t } = useTranslation();
|
|
const toast = useToast();
|
|
|
|
const archiveWorkspaceRecord = useCallback(async () => {
|
|
const client = getHostRuntimeStore().getClient(serverId);
|
|
if (!client) {
|
|
toast.error(t("sidebar.workspace.toasts.hostDisconnected"));
|
|
return;
|
|
}
|
|
onSetHiding?.(true);
|
|
try {
|
|
onArchiveStarted();
|
|
await archiveWorkspaceOptimistically({
|
|
client,
|
|
workspace: {
|
|
serverId,
|
|
workspaceId,
|
|
},
|
|
});
|
|
purgeArchivedWorkspaceState({ serverId, workspaceId });
|
|
} catch (error) {
|
|
toast.error(
|
|
error instanceof Error ? error.message : t("sidebar.workspace.toasts.archiveFailed"),
|
|
);
|
|
} finally {
|
|
onSetHiding?.(false);
|
|
}
|
|
}, [onArchiveStarted, onSetHiding, serverId, t, toast, workspaceId]);
|
|
|
|
const archive = useCallback(() => {
|
|
void (async () => {
|
|
if (workspaceKind === "worktree") {
|
|
const confirmed = await confirmRiskyWorktreeArchive(
|
|
{
|
|
workspaceName: name,
|
|
isDirty,
|
|
aheadOfOrigin,
|
|
diffStat,
|
|
},
|
|
warningLabels,
|
|
);
|
|
if (!confirmed) {
|
|
return;
|
|
}
|
|
}
|
|
await archiveWorkspaceRecord();
|
|
})();
|
|
}, [
|
|
aheadOfOrigin,
|
|
archiveWorkspaceRecord,
|
|
diffStat,
|
|
isDirty,
|
|
name,
|
|
warningLabels,
|
|
workspaceKind,
|
|
]);
|
|
|
|
return {
|
|
archive,
|
|
};
|
|
}
|