diff --git a/docs/design.md b/docs/design.md index 1b9aac6e3..172416984 100644 --- a/docs/design.md +++ b/docs/design.md @@ -218,7 +218,7 @@ The bespoke pills in `packages/app/src/screens/settings/host-page.tsx:97-116`, ` - Raw DOM APIs without an `isWeb` guard. - Spacing values outside the scale. `padding: 20` and `gap: 10` are wrong. - Color changes for disabled state. Opacity only. -- Destructive actions without `confirmDialog`. Restart, remove, and future destructive actions are confirmed. Worktree archive is confirmed only when git runtime reports uncommitted changes or unpushed commits; clean pushed worktrees archive immediately. +- Destructive actions without `confirmDialog`. Restart, remove, and future destructive actions are confirmed. Archive workspace is confirmed only when its worktree backing reports uncommitted changes or unpushed commits; otherwise it archives immediately. - Bespoke status pills. `` is the pill primitive. - Raw `Modal` for a focused task. `` is the modal primitive. - Importing `ActivityIndicator` directly. `` is the loading primitive. diff --git a/docs/glossary.md b/docs/glossary.md index 5a9d14b99..e4c39d826 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -4,6 +4,7 @@ Authoritative terminology. UI label wins. Don't invent synonyms; use what's here - **Project** — Logical grouping of workspaces sharing a git remote (or main repo root). UI: "Project" / "Add project". Code: `ProjectSummary` (`packages/app/src/utils/projects.ts:22`), `projectKey` (`packages/server/src/server/workspace-registry-model.ts:16`). Forbidden: "Repo", "Repository" as UI label. - **Workspace** — One concrete `cwd` on one daemon, with git state; belongs to exactly one project. Its `id` is opaque workspace identity; its `cwd` is the filesystem directory. UI: "Workspace". Code: `WorkspaceDescriptorPayload` (`packages/protocol/src/messages.ts:2178`). Don't confuse with: Branch (one branch can back many workspaces via worktrees). Forbidden: "Folder", "Directory" as UI label. +- **Archive workspace** — Removes one workspace from active use and archives everything it owns. UI and app shortcuts always say "Archive workspace", regardless of backing. The daemon leaves ordinary directories intact and removes a Paseo-owned worktree only when no active workspace still references it. CLI/MCP **archive worktree** is a separate lower-level operation that archives every workspace on that worktree. - **Workspace kind** — `"directory" | "local_checkout" | "worktree"`. The git-derived, persisted property of a workspace, used across its lifetime (archive safety, sidebar, grouping). Derived from the cwd's git reality (`deriveWorkspaceKind`, `packages/server/src/server/workspace-registry-model.ts:158`), not stored from a user choice. Code: `PersistedWorkspaceKind` (`packages/server/src/server/workspace-registry-model.ts:8`). Don't confuse with **Isolation** (the create-time intent). - **Isolation** — Create-time choice for a new workspace: reuse the existing checkout (**Local**) or cut a dedicated git worktree (**New worktree**). A transient setup input, also remembered as a create-form preference; it is not a workspace property. UI: "Isolation" control on the New Workspace screen. Code: `isolation` (`"local" | "worktree"`), `useWorkspaceIsolation` (`packages/app/src/screens/new-workspace-screen.tsx`); persisted as `FormPreferences.isolation` (`packages/app/src/create-agent-preferences/preferences.ts`). Distinct from **Workspace kind**, which is the git-derived property the intent produces (Local → `local_checkout` or `directory` by git-ness; New worktree → `worktree`). On the wire it is the create request's `source.kind` (`directory | worktree`, `packages/protocol/src/messages.ts:1693`). - **Agent** — See **Agent session**. UI still says "Agent" / "New Agent" in places, but moving toward **Agent session** as the canonical term. Code: `AgentSnapshotPayload` (`packages/protocol/src/messages.ts:608`). Forbidden: "Task", "Job", "Run". diff --git a/packages/app/e2e/empty-project-persists.spec.ts b/packages/app/e2e/empty-project-persists.spec.ts index 470df195c..43433b210 100644 --- a/packages/app/e2e/empty-project-persists.spec.ts +++ b/packages/app/e2e/empty-project-persists.spec.ts @@ -1,4 +1,5 @@ import path from "node:path"; +import { existsSync } from "node:fs"; import { test, expect, type Page } from "./fixtures"; import { gotoAppShell } from "./helpers/app"; import { expectOpenedProject } from "./helpers/project-picker-ui"; @@ -11,7 +12,7 @@ function workspaceRowTestId(workspaceId: string): string { return `sidebar-workspace-row-${getServerId()}:${workspaceId}`; } -async function hideWorkspaceFromSidebar(page: Page, workspaceId: string): Promise { +async function archiveWorkspaceFromSidebar(page: Page, workspaceId: string): Promise { const serverId = getServerId(); const row = page.getByTestId(workspaceRowTestId(workspaceId)); await expect(row).toBeVisible({ timeout: 30_000 }); @@ -21,10 +22,6 @@ async function hideWorkspaceFromSidebar(page: Page, workspaceId: string): Promis await expect(kebab).toBeVisible({ timeout: 10_000 }); await kebab.click(); - // Hiding a checkout from the sidebar raises a browser confirm; accept it so the - // user-confirmed archive proceeds deterministically. - page.once("dialog", (dialog) => void dialog.accept()); - const archiveItem = page.getByTestId(`sidebar-workspace-menu-archive-${serverId}:${workspaceId}`); await expect(archiveItem).toBeVisible({ timeout: 10_000 }); await archiveItem.click(); @@ -159,17 +156,24 @@ test.describe("Project with no workspaces persists", () => { await gotoAppShell(page); await waitForSidebarHydration(page); await expect(projectRow).toBeVisible({ timeout: 30_000 }); - await expect(page.getByTestId(workspaceRowTestId(workspace.workspaceId))).toBeVisible({ + const workspaceRow = page.getByTestId(workspaceRowTestId(workspace.workspaceId)); + await expect(workspaceRow).toBeVisible({ timeout: 30_000, }); + await workspaceRow.click(); + await expect(page.getByTestId("changes-primary-cta")).toHaveAttribute( + "aria-label", + "Archive workspace", + ); - await hideWorkspaceFromSidebar(page, workspace.workspaceId); + await archiveWorkspaceFromSidebar(page, workspace.workspaceId); // The workspace row goes away, but its project parent stays and exposes a // child row for creating the next workspace. await expect(page.getByTestId(workspaceRowTestId(workspace.workspaceId))).toHaveCount(0, { timeout: 30_000, }); + expect(existsSync(workspace.repoPath)).toBe(true); await expect(projectRow).toBeVisible({ timeout: 30_000 }); await expect(newWorkspaceRow).toBeVisible({ timeout: 30_000 }); await expect(newWorkspaceRow).toContainText("New workspace"); diff --git a/packages/app/e2e/helpers/sidebar.ts b/packages/app/e2e/helpers/sidebar.ts index 6d6afc900..63a6e0cec 100644 --- a/packages/app/e2e/helpers/sidebar.ts +++ b/packages/app/e2e/helpers/sidebar.ts @@ -39,10 +39,9 @@ export async function clickArchiveWorkspaceMenuItem( await archiveItem.click(); } -export async function archiveWorktreeFromSidebar(page: Page, workspaceId: string): Promise { - // A clean worktree archives with no prompt; if the host reports unsynced work the app - // raises a browser confirm. Accept it so the user-confirmed archive stays deterministic - // either way. +export async function archiveWorkspaceFromSidebar(page: Page, workspaceId: string): Promise { + // A clean workspace archives with no prompt. Managed worktree backing may raise + // a browser confirm for unsynced work, so accept it when present. page.once("dialog", (dialog) => void dialog.accept()); await clickArchiveWorkspaceMenuItem(page, workspaceId); } diff --git a/packages/app/e2e/workspace-archive-shortcut.spec.ts b/packages/app/e2e/workspace-archive-shortcut.spec.ts new file mode 100644 index 000000000..8e6881bed --- /dev/null +++ b/packages/app/e2e/workspace-archive-shortcut.spec.ts @@ -0,0 +1,26 @@ +import { existsSync } from "node:fs"; +import { expect, test } from "./fixtures"; +import { gotoAppShell } from "./helpers/app"; +import { seedWorkspace } from "./helpers/seed-client"; +import { expectWorkspaceAbsentFromSidebar, selectWorkspaceInSidebar } from "./helpers/sidebar"; +import { waitForSidebarHydration } from "./helpers/workspace-ui"; + +test.describe("Workspace archive shortcut", () => { + test("archives the selected workspace without removing its local checkout", async ({ page }) => { + const workspace = await seedWorkspace({ repoPrefix: "archive-shortcut-" }); + + try { + await gotoAppShell(page); + await waitForSidebarHydration(page); + await selectWorkspaceInSidebar(page, workspace.workspaceId); + + const modifier = process.platform === "darwin" ? "Meta" : "Control"; + await page.keyboard.press(`${modifier}+Shift+Backspace`); + + await expectWorkspaceAbsentFromSidebar(page, workspace.workspaceId); + expect(existsSync(workspace.repoPath)).toBe(true); + } finally { + await workspace.cleanup(); + } + }); +}); diff --git a/packages/app/e2e/worktree-archive-risk-warning.spec.ts b/packages/app/e2e/worktree-archive-risk-warning.spec.ts index fce68f548..3fe198430 100644 --- a/packages/app/e2e/worktree-archive-risk-warning.spec.ts +++ b/packages/app/e2e/worktree-archive-risk-warning.spec.ts @@ -76,7 +76,7 @@ async function clickArchiveAndAnswerWarning( return warning; } -test.describe("Worktree archive risk warning", () => { +test.describe("Workspace archive risk warning for worktree backing", () => { let client: Awaited>; let tempRepo: { path: string; cleanup: () => Promise }; const createdWorktreeDirectories = new Set(); @@ -97,7 +97,7 @@ test.describe("Worktree archive risk warning", () => { await tempRepo?.cleanup().catch(() => undefined); }); - test("a risky worktree archive is gated by confirmation and removes the directory after acceptance", async ({ + test("a risky workspace archive is gated by confirmation and removes its worktree after acceptance", async ({ page, }) => { const serverId = getServerId(); diff --git a/packages/app/e2e/worktree-archive.spec.ts b/packages/app/e2e/worktree-archive.spec.ts index b61e90cfe..ed874f18d 100644 --- a/packages/app/e2e/worktree-archive.spec.ts +++ b/packages/app/e2e/worktree-archive.spec.ts @@ -8,11 +8,11 @@ import { openProjectViaDaemon, } from "./helpers/new-workspace"; import { getServerId } from "./helpers/server-id"; -import { archiveWorktreeFromSidebar, expectWorkspaceAbsentFromSidebar } from "./helpers/sidebar"; +import { archiveWorkspaceFromSidebar, expectWorkspaceAbsentFromSidebar } from "./helpers/sidebar"; import { createTempGitRepo } from "./helpers/workspace"; import { waitForSidebarHydration, waitForWorkspaceInSidebar } from "./helpers/workspace-ui"; -test.describe("Worktree archive", () => { +test.describe("Workspace archive with worktree backing", () => { let client: Awaited>; let tempRepo: { path: string; cleanup: () => Promise }; const createdWorktreeDirectories = new Set(); @@ -33,9 +33,7 @@ test.describe("Worktree archive", () => { await tempRepo?.cleanup().catch(() => undefined); }); - test("archiving a worktree from the sidebar removes its row and worktree directory", async ({ - page, - }) => { + test("archiving the final workspace removes its managed worktree directory", async ({ page }) => { const serverId = getServerId(); await openProjectViaDaemon(client, tempRepo.path); const worktree = await createWorktreeViaDaemon(client, { @@ -49,11 +47,51 @@ test.describe("Worktree archive", () => { await waitForSidebarHydration(page); await waitForWorkspaceInSidebar(page, { serverId, workspaceId: worktree.workspaceId }); - await archiveWorktreeFromSidebar(page, worktree.workspaceId); + await archiveWorkspaceFromSidebar(page, worktree.workspaceId); await expectWorkspaceAbsentFromSidebar(page, worktree.workspaceId); await expect .poll(() => existsSync(worktree.workspaceDirectory), { timeout: 30_000 }) .toBe(false); }); + + test("a managed worktree remains until its last workspace is archived", async ({ page }) => { + const serverId = getServerId(); + await openProjectViaDaemon(client, tempRepo.path); + const first = await createWorktreeViaDaemon(client, { + cwd: tempRepo.path, + slug: `shared-archive-${Date.now()}`, + }); + createdWorktreeDirectories.add(first.workspaceDirectory); + + const siblingPayload = await client.createWorkspace({ + source: { + kind: "directory", + path: first.workspaceDirectory, + }, + title: "Second workspace", + }); + if (!siblingPayload.workspace) { + throw new Error(siblingPayload.error ?? "Failed to create a workspace on the worktree"); + } + const sibling = siblingPayload.workspace; + expect(sibling.workspaceKind).toBe("worktree"); + expect(sibling.workspaceDirectory).toBe(first.workspaceDirectory); + + await gotoAppShell(page); + await waitForSidebarHydration(page); + await waitForWorkspaceInSidebar(page, { serverId, workspaceId: first.workspaceId }); + await waitForWorkspaceInSidebar(page, { serverId, workspaceId: sibling.id }); + + await archiveWorkspaceFromSidebar(page, first.workspaceId); + + await expectWorkspaceAbsentFromSidebar(page, first.workspaceId); + expect(existsSync(first.workspaceDirectory)).toBe(true); + await waitForWorkspaceInSidebar(page, { serverId, workspaceId: sibling.id }); + + await archiveWorkspaceFromSidebar(page, sibling.id); + + await expectWorkspaceAbsentFromSidebar(page, sibling.id); + await expect.poll(() => existsSync(first.workspaceDirectory), { timeout: 30_000 }).toBe(false); + }); }); diff --git a/packages/app/src/components/sidebar-workspace-list.tsx b/packages/app/src/components/sidebar-workspace-list.tsx index 3ac3b5d88..59b947afe 100644 --- a/packages/app/src/components/sidebar-workspace-list.tsx +++ b/packages/app/src/components/sidebar-workspace-list.tsx @@ -77,7 +77,6 @@ import { } from "@/components/ui/dropdown-menu"; import { SyncedLoader } from "@/components/synced-loader"; import { useToast } from "@/contexts/toast-context"; -import { useCheckoutGitActionsStore } from "@/git/actions-store"; import { toWorktreeArchiveRisk } from "@/git/worktree-archive-warning"; import { hasVisibleOrderChanged, mergeWithRemainder } from "@/utils/sidebar-reorder"; import { decideLongPressMove } from "@/utils/sidebar-gesture-arbitration"; @@ -112,7 +111,7 @@ import { } from "@/utils/sidebar-project-row-model"; import { redirectIfArchivingActiveWorkspace } from "@/utils/sidebar-workspace-archive-redirect"; import { openExternalUrl } from "@/utils/open-external-url"; -import { requireWorkspaceDirectory, resolveWorkspaceDirectory } from "@/utils/workspace-directory"; +import { requireWorkspaceDirectory } from "@/utils/workspace-directory"; import { useWorkspaceArchive } from "@/workspace/use-workspace-archive"; import { getCurrentProjectRemoveReadiness, @@ -284,16 +283,6 @@ interface WorkspaceRowInnerProps { archiveShortcutKeys?: ShortcutKey[][] | null; } -function getWorkspaceArchiveStatus( - isWorktree: boolean, - archiveStatus: "idle" | "pending" | "success", - isArchivingWorkspace: boolean, -): "idle" | "pending" | "success" { - if (isWorktree) return archiveStatus; - if (isArchivingWorkspace) return "pending"; - return "idle"; -} - export function PrBadge({ hint }: { hint: PrHint }) { const { t } = useTranslation(); const [isHovered, setIsHovered] = useState(false); @@ -1454,20 +1443,7 @@ function WorkspaceRowWithMenu({ const toast = useToast(); const [isHidingWorkspace, setIsHidingWorkspace] = useState(false); const [isRenameOpen, setIsRenameOpen] = useState(false); - const workspaceDirectory = resolveWorkspaceDirectory({ - workspaceDirectory: workspace.workspaceDirectory, - }); - const worktreeArchiveStatus = useCheckoutGitActionsStore((state) => - workspaceDirectory - ? state.getStatus({ - serverId: workspace.serverId, - cwd: workspaceDirectory, - actionId: "archive-worktree", - }) - : "idle", - ); - const isWorktree = workspace.workspaceKind === "worktree"; - const isArchiving = isWorktree ? workspace.archivingAt !== null : isHidingWorkspace; + const isArchiving = workspace.archivingAt !== null || isHidingWorkspace; const redirectAfterArchive = useCallback(() => { redirectIfArchivingActiveWorkspace({ serverId: workspace.serverId, @@ -1479,7 +1455,6 @@ function WorkspaceRowWithMenu({ const archiveController = useWorkspaceArchive({ serverId: workspace.serverId, workspaceId: workspace.workspaceId, - workspaceDirectory: workspace.workspaceDirectory, workspaceKind: workspace.workspaceKind, name: workspace.name, ...toWorktreeArchiveRisk(workspace), @@ -1546,7 +1521,7 @@ function WorkspaceRowWithMenu({ [renameMutation], ); - const archiveShortcutKeys = useShortcutKeys("archive-worktree"); + const archiveShortcutKeys = useShortcutKeys("archive-workspace"); const { hasClearableAttention, clearAttention } = useClearWorkspaceAttention({ serverId: workspace.serverId, workspaceId: workspace.workspaceId, @@ -1558,8 +1533,8 @@ function WorkspaceRowWithMenu({ }, [clearAttention, toast]); useKeyboardActionHandler({ - handlerId: `worktree-archive-${workspace.workspaceKey}`, - actions: ["worktree.archive"], + handlerId: `workspace-archive-${workspace.workspaceKey}`, + actions: ["workspace.archive"], enabled: selected && !isArchiving, priority: 0, handle: () => { @@ -1584,11 +1559,7 @@ function WorkspaceRowWithMenu({ dragHandleProps={dragHandleProps} menuController={null} archiveLabel={t("sidebar.workspace.actions.archive")} - archiveStatus={getWorkspaceArchiveStatus( - isWorktree, - worktreeArchiveStatus, - isHidingWorkspace, - )} + archiveStatus={isArchiving ? "pending" : "idle"} archivePendingLabel={t("sidebar.workspace.actions.archiving")} onArchive={handleArchive} onCopyBranchName={canCopyBranchName ? handleCopyBranchName : undefined} diff --git a/packages/app/src/components/sidebar/sidebar-status-list.tsx b/packages/app/src/components/sidebar/sidebar-status-list.tsx index 77f5ea26f..dd677aa00 100644 --- a/packages/app/src/components/sidebar/sidebar-status-list.tsx +++ b/packages/app/src/components/sidebar/sidebar-status-list.tsx @@ -23,10 +23,9 @@ import { useToast } from "@/contexts/toast-context"; import { useMutation } from "@tanstack/react-query"; import { getHostRuntimeStore } from "@/runtime/host-runtime"; import { AdaptiveRenameModal } from "@/components/rename-modal"; -import { requireWorkspaceDirectory, resolveWorkspaceDirectory } from "@/utils/workspace-directory"; +import { requireWorkspaceDirectory } from "@/utils/workspace-directory"; import { redirectIfArchivingActiveWorkspace } from "@/utils/sidebar-workspace-archive-redirect"; import { useWorkspaceArchive } from "@/workspace/use-workspace-archive"; -import { useCheckoutGitActionsStore } from "@/git/actions-store"; import { toWorktreeArchiveRisk } from "@/git/worktree-archive-warning"; import * as Clipboard from "expo-clipboard"; import type { ShortcutKey } from "@/utils/format-shortcut"; @@ -330,20 +329,7 @@ function StatusWorkspaceRowWithMenu({ const toast = useToast(); const [isHidingWorkspace, setIsHidingWorkspace] = useState(false); const [isRenameOpen, setIsRenameOpen] = useState(false); - const workspaceDirectory = resolveWorkspaceDirectory({ - workspaceDirectory: workspace.workspaceDirectory, - }); - const worktreeArchiveStatus = useCheckoutGitActionsStore((state) => - workspaceDirectory - ? state.getStatus({ - serverId: workspace.serverId, - cwd: workspaceDirectory, - actionId: "archive-worktree", - }) - : "idle", - ); - const isWorktree = workspace.workspaceKind === "worktree"; - const isArchiving = isWorktree ? workspace.archivingAt !== null : isHidingWorkspace; + const isArchiving = workspace.archivingAt !== null || isHidingWorkspace; const redirectAfterArchive = useCallback(() => { redirectIfArchivingActiveWorkspace({ @@ -358,7 +344,6 @@ function StatusWorkspaceRowWithMenu({ const archiveController = useWorkspaceArchive({ serverId: workspace.serverId, workspaceId: workspace.workspaceId, - workspaceDirectory: workspace.workspaceDirectory, workspaceKind: workspace.workspaceKind, name: workspace.name, ...toWorktreeArchiveRisk(workspace), @@ -408,7 +393,7 @@ function StatusWorkspaceRowWithMenu({ [renameMutation], ); - const archiveShortcutKeys = useShortcutKeys("archive-worktree"); + const archiveShortcutKeys = useShortcutKeys("archive-workspace"); const { hasClearableAttention, clearAttention } = useClearWorkspaceAttention({ serverId: workspace.serverId, workspaceId: workspace.workspaceId, @@ -420,8 +405,8 @@ function StatusWorkspaceRowWithMenu({ }, [clearAttention, toast]); useKeyboardActionHandler({ - handlerId: `worktree-archive-${workspace.workspaceKey}`, - actions: ["worktree.archive"], + handlerId: `workspace-archive-${workspace.workspaceKey}`, + actions: ["workspace.archive"], enabled: selected && !isArchiving, priority: 0, handle: () => { @@ -430,13 +415,6 @@ function StatusWorkspaceRowWithMenu({ }, }); - let computedArchiveStatus: "idle" | "pending" | "success" = "idle"; - if (isWorktree) { - computedArchiveStatus = worktreeArchiveStatus; - } else if (isHidingWorkspace) { - computedArchiveStatus = "pending"; - } - return ( <> - workspaceDirectory - ? state.getStatus({ - serverId: workspace.serverId, - cwd: workspaceDirectory, - actionId: "archive-worktree", - }) - : "idle", - ); - const isWorktree = workspace.workspaceKind === "worktree"; - const isArchiving = isWorktree ? workspace.archivingAt !== null : isHidingWorkspace; + const isArchiving = workspace.archivingAt !== null || isHidingWorkspace; const redirectAfterArchive = useCallback(() => { redirectIfArchivingActiveWorkspace({ @@ -94,7 +80,6 @@ export function SidebarWorkspaceRow({ const archiveController = useWorkspaceArchive({ serverId: workspace.serverId, workspaceId: workspace.workspaceId, - workspaceDirectory: workspace.workspaceDirectory, workspaceKind: workspace.workspaceKind, name: workspace.name, ...toWorktreeArchiveRisk(workspace), @@ -161,7 +146,7 @@ export function SidebarWorkspaceRow({ [renameMutation], ); - const archiveShortcutKeys = useShortcutKeys("archive-worktree"); + const archiveShortcutKeys = useShortcutKeys("archive-workspace"); const { hasClearableAttention, clearAttention } = useClearWorkspaceAttention({ serverId: workspace.serverId, workspaceId: workspace.workspaceId, @@ -173,8 +158,8 @@ export function SidebarWorkspaceRow({ }, [clearAttention, toast]); useKeyboardActionHandler({ - handlerId: `worktree-archive-${workspace.workspaceKey}`, - actions: ["worktree.archive"], + handlerId: `workspace-archive-${workspace.workspaceKey}`, + actions: ["workspace.archive"], enabled: selected && !isArchiving, priority: 0, handle: () => { @@ -183,13 +168,6 @@ export function SidebarWorkspaceRow({ }, }); - let archiveStatus: "idle" | "pending" | "success" = "idle"; - if (isWorktree) { - archiveStatus = worktreeArchiveStatus; - } else if (isHidingWorkspace) { - archiveStatus = "pending"; - } - return ( <> >(); @@ -15,7 +14,6 @@ function pendingArchiveKey(input: { serverId: string; workspaceId: string }): st export function markWorkspaceArchivePending(input: { serverId: string; workspaceId: string; - workspaceDirectory?: string | null; }): void { const serverId = input.serverId.trim(); const workspaceId = normalizeWorkspaceOpaqueId(input.workspaceId); @@ -26,7 +24,6 @@ export function markWorkspaceArchivePending(input: { const archives = pendingWorkspaceArchivesByServer.get(serverId) ?? new Map(); archives.set(pendingArchiveKey({ serverId, workspaceId }), { workspaceId, - workspaceDirectory: normalizeWorkspacePath(input.workspaceDirectory), }); pendingWorkspaceArchivesByServer.set(serverId, archives); } @@ -54,7 +51,6 @@ export function clearWorkspaceArchivePending(input: { export function isWorkspaceArchivePending(input: { serverId: string; workspaceId?: string | null; - workspaceDirectory?: string | null; }): boolean { const serverId = input.serverId.trim(); if (!serverId) { @@ -67,21 +63,7 @@ export function isWorkspaceArchivePending(input: { } const workspaceId = normalizeWorkspaceOpaqueId(input.workspaceId); - if (workspaceId && archives.has(pendingArchiveKey({ serverId, workspaceId }))) { - return true; - } - - const workspaceDirectory = normalizeWorkspacePath(input.workspaceDirectory); - if (!workspaceDirectory) { - return false; - } - - for (const archive of archives.values()) { - if (archive.workspaceDirectory === workspaceDirectory) { - return true; - } - } - return false; + return Boolean(workspaceId && archives.has(pendingArchiveKey({ serverId, workspaceId }))); } export function shouldSuppressWorkspaceForLocalArchive(input: { @@ -91,6 +73,5 @@ export function shouldSuppressWorkspaceForLocalArchive(input: { return isWorkspaceArchivePending({ serverId: input.serverId, workspaceId: input.workspace.id, - workspaceDirectory: input.workspace.workspaceDirectory, }); } diff --git a/packages/app/src/git/actions-split-button.tsx b/packages/app/src/git/actions-split-button.tsx index 41a99510d..2551f1e9e 100644 --- a/packages/app/src/git/actions-split-button.tsx +++ b/packages/app/src/git/actions-split-button.tsx @@ -48,7 +48,7 @@ function GitActionMenuItem({ const handleSelect = useCallback(() => onSelect(action), [onSelect, action]); const trailing = useMemo( () => - action.id === "archive-worktree" && archiveShortcutKeys ? ( + action.id === "archive-workspace" && archiveShortcutKeys ? ( ) : undefined, [action.id, archiveShortcutKeys], @@ -58,7 +58,7 @@ function GitActionMenuItem({ {needsSeparator && showSeparator ? : null} { if (action.status === "pending") return action.pendingLabel; diff --git a/packages/app/src/git/actions-store.test.ts b/packages/app/src/git/actions-store.test.ts index a2717344a..111547163 100644 --- a/packages/app/src/git/actions-store.test.ts +++ b/packages/app/src/git/actions-store.test.ts @@ -2,16 +2,10 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import type { DaemonClient } from "@getpaseo/client/internal/daemon-client"; import { queryClient as appQueryClient } from "@/data/query-client"; import { useSessionStore } from "@/stores/session-store"; -import type { WorkspaceDescriptor } from "@/stores/session-store"; import { __resetCheckoutGitActionsStoreForTests, - isLocalWorktreeArchivePending, useCheckoutGitActionsStore, } from "@/git/actions-store"; -import { - clearWorkspaceArchivePending, - isWorkspaceArchivePending, -} from "@/contexts/session-workspace-upserts"; vi.mock("@react-native-async-storage/async-storage", () => ({ default: { @@ -31,33 +25,13 @@ function createDeferred() { return { promise, resolve, reject }; } -function workspace(input: Partial & Pick) { - return { - id: input.id, - projectId: input.projectId ?? "project-1", - projectDisplayName: input.projectDisplayName ?? "Project", - projectRootPath: input.projectRootPath ?? "/tmp/repo", - workspaceDirectory: input.workspaceDirectory ?? "/tmp/repo/worktrees/feature", - projectKind: input.projectKind ?? "git", - workspaceKind: input.workspaceKind ?? "worktree", - name: input.name ?? input.id, - status: input.status ?? "done", - archivingAt: input.archivingAt ?? null, - statusEnteredAt: null, - diffStat: input.diffStat ?? null, - scripts: input.scripts ?? [], - } satisfies WorkspaceDescriptor; -} - describe("checkout-git-actions-store", () => { const serverId = "server-1"; const cwd = "/tmp/repo/worktrees/feature"; - const workspaceId = "ws-feature"; beforeEach(() => { vi.useFakeTimers(); __resetCheckoutGitActionsStoreForTests(); - clearWorkspaceArchivePending({ serverId, workspaceId }); appQueryClient.clear(); useSessionStore.setState((state) => ({ ...state, sessions: {} })); }); @@ -65,7 +39,6 @@ describe("checkout-git-actions-store", () => { afterEach(() => { vi.useRealTimers(); __resetCheckoutGitActionsStoreForTests(); - clearWorkspaceArchivePending({ serverId, workspaceId }); appQueryClient.clear(); useSessionStore.setState((state) => ({ ...state, sessions: {} })); }); @@ -294,118 +267,4 @@ describe("checkout-git-actions-store", () => { .getStatus({ serverId, cwd, actionId: "enable-pr-auto-merge-merge" }), ).toBe("idle"); }); - - it("hides an archived worktree optimistically while the archive RPC is in flight", async () => { - const deferred = createDeferred>(); - const client = { - archivePaseoWorktree: vi.fn(() => deferred.promise), - }; - const featureWorkspace = workspace({ - id: workspaceId, - name: "feature", - workspaceDirectory: cwd, - }); - useSessionStore.getState().initializeSession(serverId, client as unknown as DaemonClient); - useSessionStore.getState().setWorkspaces(serverId, new Map([[workspaceId, featureWorkspace]])); - appQueryClient.setQueryData( - ["sidebarPaseoWorktreeList", serverId, "/tmp"], - [{ worktreePath: cwd }, { worktreePath: "/tmp/other" }], - ); - - const archive = useCheckoutGitActionsStore - .getState() - .archiveWorktree({ serverId, cwd, worktreePath: cwd, workspaceId }); - - expect(useSessionStore.getState().sessions[serverId]?.workspaces.has(workspaceId)).toBe(false); - expect(useSessionStore.getState().sessions[serverId]?.workspaces.has(cwd)).toBe(false); - expect(appQueryClient.getQueryData(["sidebarPaseoWorktreeList", serverId, "/tmp"])).toEqual([ - { worktreePath: "/tmp/other" }, - ]); - expect(isLocalWorktreeArchivePending({ serverId, cwd })).toBe(true); - - deferred.resolve({}); - await archive; - - expect( - isWorkspaceArchivePending({ - serverId, - workspaceId, - }), - ).toBe(true); - expect( - isWorkspaceArchivePending({ - serverId, - workspaceId: cwd, - }), - ).toBe(false); - }); - - it("archives on the server even when its workspace cannot be resolved", async () => { - const client = { - archivePaseoWorktree: vi.fn(async () => ({})), - }; - useSessionStore.getState().initializeSession(serverId, client as unknown as DaemonClient); - - await useCheckoutGitActionsStore - .getState() - .archiveWorktree({ serverId, cwd, worktreePath: cwd }); - - // The server archive is keyed by worktreePath and must run regardless. - expect(client.archivePaseoWorktree).toHaveBeenCalledWith({ worktreePath: cwd }); - // The optimistic client-side mark is never keyed by the path. - expect(isWorkspaceArchivePending({ serverId, workspaceId: cwd })).toBe(false); - }); - - it("restores an optimistically hidden worktree when archive fails", async () => { - const client = { - archivePaseoWorktree: vi.fn(async () => ({ error: { message: "archive failed" } })), - }; - const featureWorkspace = workspace({ - id: workspaceId, - name: "feature", - workspaceDirectory: cwd, - }); - const listSnapshot = [{ worktreePath: cwd }, { worktreePath: "/tmp/other" }]; - useSessionStore.getState().initializeSession(serverId, client as unknown as DaemonClient); - useSessionStore.getState().setWorkspaces(serverId, new Map([[workspaceId, featureWorkspace]])); - appQueryClient.setQueryData(["sidebarPaseoWorktreeList", serverId, "/tmp"], listSnapshot); - - await expect( - useCheckoutGitActionsStore - .getState() - .archiveWorktree({ serverId, cwd, worktreePath: cwd, workspaceId }), - ).rejects.toThrow("archive failed"); - - expect(useSessionStore.getState().sessions[serverId]?.workspaces.get(workspaceId)).toEqual( - featureWorkspace, - ); - expect(appQueryClient.getQueryData(["sidebarPaseoWorktreeList", serverId, "/tmp"])).toEqual( - listSnapshot, - ); - }); - - it("reports local archive pending only while the archive action is in flight", async () => { - const deferred = createDeferred>(); - const client = { - archivePaseoWorktree: vi.fn(() => deferred.promise), - }; - const featureWorkspace = workspace({ - id: workspaceId, - name: "feature", - workspaceDirectory: cwd, - }); - useSessionStore.getState().initializeSession(serverId, client as unknown as DaemonClient); - useSessionStore.getState().setWorkspaces(serverId, new Map([[workspaceId, 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); - }); }); diff --git a/packages/app/src/git/actions-store.ts b/packages/app/src/git/actions-store.ts index 63e40bc8d..bb735d954 100644 --- a/packages/app/src/git/actions-store.ts +++ b/packages/app/src/git/actions-store.ts @@ -1,19 +1,7 @@ -import type { QueryKey } from "@tanstack/react-query"; import type { CheckoutPrMergeMethod } from "@getpaseo/protocol/messages"; import { create } from "zustand"; import { queryClient as appQueryClient } from "@/data/query-client"; -import { - buildWorkspaceTabPersistenceKey, - useWorkspaceLayoutStore, -} from "@/stores/workspace-layout-store"; import { useSessionStore } from "@/stores/session-store"; -import type { WorkspaceDescriptor } from "@/stores/session-store"; -import { useWorkspaceTabsStore } from "@/stores/workspace-tabs-store"; -import { - clearWorkspaceArchivePending, - markWorkspaceArchivePending, -} from "@/contexts/session-workspace-upserts"; -import { resolveWorkspaceMapKeyByIdentity } from "@/utils/workspace-identity"; import { invalidateCheckoutGitQueriesForClient } from "@/git/query-keys"; import { i18n } from "@/i18n/i18next"; @@ -36,8 +24,7 @@ export type CheckoutGitAsyncActionId = | "enable-pr-auto-merge-rebase" | "disable-pr-auto-merge" | "merge-branch" - | "merge-from-base" - | "archive-worktree"; + | "merge-from-base"; type CheckoutKey = string; type StatusMap = Partial>; @@ -89,119 +76,6 @@ function invalidateCheckoutGitQueries(serverId: string, cwd: string) { return invalidateCheckoutGitQueriesForClient(appQueryClient, { serverId, cwd }); } -function invalidateWorktreeList() { - void appQueryClient.invalidateQueries({ - predicate: (query) => - Array.isArray(query.queryKey) && query.queryKey[0] === "paseoWorktreeList", - }); - void appQueryClient.invalidateQueries({ - predicate: (query) => - Array.isArray(query.queryKey) && query.queryKey[0] === "sidebarPaseoWorktreeList", - }); -} - -function removeWorktreeFromCachedLists(input: { serverId: string; worktreePath: string }): void { - const serverId = input.serverId.trim(); - const worktreePath = input.worktreePath.trim(); - if (!serverId || !worktreePath) { - return; - } - - const removeFromList = (current: unknown) => { - if (!Array.isArray(current)) { - return current; - } - const filtered = current.filter((entry) => entry?.worktreePath !== worktreePath); - return filtered.length === current.length ? current : filtered; - }; - - appQueryClient.setQueriesData( - { - predicate: (query) => - Array.isArray(query.queryKey) && - query.queryKey[0] === "paseoWorktreeList" && - query.queryKey[1] === serverId, - }, - removeFromList, - ); - - appQueryClient.setQueriesData( - { - predicate: (query) => - Array.isArray(query.queryKey) && - query.queryKey[0] === "sidebarPaseoWorktreeList" && - query.queryKey[1] === serverId, - }, - removeFromList, - ); -} - -interface WorktreeArchiveSnapshot { - workspace: WorkspaceDescriptor | null; - worktreeLists: Array<[QueryKey, unknown]>; -} - -function isWorktreeListQuery(input: { queryKey: QueryKey; serverId: string }): boolean { - return ( - Array.isArray(input.queryKey) && - (input.queryKey[0] === "paseoWorktreeList" || - input.queryKey[0] === "sidebarPaseoWorktreeList") && - input.queryKey[1] === input.serverId - ); -} - -function snapshotWorktreeArchiveState(input: { - serverId: string; - workspaceId: string | undefined; -}): WorktreeArchiveSnapshot { - const workspaces = useSessionStore.getState().sessions[input.serverId]?.workspaces; - const workspaceKey = input.workspaceId - ? resolveWorkspaceMapKeyByIdentity({ workspaces, workspaceId: input.workspaceId }) - : null; - return { - workspace: workspaceKey ? (workspaces?.get(workspaceKey) ?? null) : null, - worktreeLists: appQueryClient.getQueriesData({ - predicate: (query) => - isWorktreeListQuery({ queryKey: query.queryKey, serverId: input.serverId }), - }), - }; -} - -function removeWorktreeFromSessionStore(input: { serverId: string; workspaceId: string }): void { - const serverId = input.serverId.trim(); - const workspaceId = input.workspaceId.trim(); - if (!serverId || !workspaceId) { - return; - } - useSessionStore.getState().removeWorkspace(serverId, workspaceId); -} - -function restoreWorktreeArchiveState(input: { - serverId: string; - snapshot: WorktreeArchiveSnapshot; -}): void { - if (input.snapshot.workspace) { - useSessionStore.getState().mergeWorkspaces(input.serverId, [input.snapshot.workspace]); - } - - for (const [queryKey, data] of input.snapshot.worktreeLists) { - appQueryClient.setQueryData(queryKey, data); - } -} - -function purgeArchivedWorkspaceState(input: { serverId: string; workspaceId: string }): void { - const serverId = input.serverId.trim(); - const workspaceId = input.workspaceId.trim(); - if (!serverId || !workspaceId) { - return; - } - const workspaceKey = buildWorkspaceTabPersistenceKey({ serverId, workspaceId }); - if (workspaceKey) { - useWorkspaceLayoutStore.getState().purgeWorkspace(workspaceKey); - } - useWorkspaceTabsStore.getState().purgeWorkspace({ serverId, workspaceId }); -} - const successTimers = new Map>(); const inFlight = new Map>(); @@ -209,16 +83,6 @@ 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; @@ -247,12 +111,6 @@ interface CheckoutGitActionsStoreState { disablePrAutoMerge: (params: { serverId: string; cwd: string }) => Promise; mergeBranch: (params: { serverId: string; cwd: string; baseRef: string }) => Promise; mergeFromBase: (params: { serverId: string; cwd: string; baseRef: string }) => Promise; - archiveWorktree: (params: { - serverId: string; - cwd: string; - worktreePath: string; - workspaceId?: string; - }) => Promise; } async function runCheckoutAction({ @@ -490,50 +348,6 @@ export const useCheckoutGitActionsStore = create() }, }); }, - - archiveWorktree: async ({ serverId, cwd, worktreePath, workspaceId }) => { - await runCheckoutAction({ - serverId, - cwd, - actionId: "archive-worktree", - run: async () => { - const client = resolveClient(serverId); - const snapshot = snapshotWorktreeArchiveState({ serverId, workspaceId }); - // The server archive is keyed by worktreePath and must always run. The - // optimistic client-side updates are keyed by workspace id, so they only - // apply when the caller passes the workspace id and it resolves in the - // local store. - const workspace = snapshot.workspace; - if (workspace) { - markWorkspaceArchivePending({ - serverId, - workspaceId: workspace.id, - }); - removeWorktreeFromSessionStore({ serverId, workspaceId: workspace.id }); - } - removeWorktreeFromCachedLists({ serverId, worktreePath }); - try { - const payload = await client.archivePaseoWorktree({ - worktreePath, - ...(workspaceId !== undefined ? { workspaceId } : {}), - }); - if (payload.error) { - throw new Error(payload.error.message); - } - } catch (error) { - if (workspace) { - clearWorkspaceArchivePending({ serverId, workspaceId: workspace.id }); - } - restoreWorktreeArchiveState({ serverId, snapshot }); - throw error; - } - invalidateWorktreeList(); - if (workspace) { - purgeArchivedWorkspaceState({ serverId, workspaceId: workspace.id }); - } - }, - }); - }, })); export function __resetCheckoutGitActionsStoreForTests() { diff --git a/packages/app/src/git/policy.test.ts b/packages/app/src/git/policy.test.ts index 4e21c27a7..3a71121ec 100644 --- a/packages/app/src/git/policy.test.ts +++ b/packages/app/src/git/policy.test.ts @@ -122,7 +122,7 @@ function createInput(overrides: Partial = {}): BuildGitAct status: "idle", handler: () => undefined, }, - "archive-worktree": { + "archive-workspace": { disabled: false, status: "idle", handler: () => undefined, @@ -272,6 +272,7 @@ describe("git-actions-policy", () => { "merge-pr-squash", "merge-pr-merge", "merge-pr-rebase", + "archive-workspace", ]); expect( actions.secondary.some((action) => action.id === "pr" && action.label === "View PR"), @@ -352,12 +353,16 @@ describe("git-actions-policy", () => { ); }); - it("only shows archive worktree for paseo worktrees", () => { - const hidden = buildGitActions(createInput()); - const shown = buildGitActions(createInput({ isPaseoOwnedWorktree: true })); + it("offers archive workspace regardless of its backing", () => { + const directory = buildGitActions(createInput({ isGit: false })); + const localCheckout = buildGitActions(createInput({ hasUncommittedChanges: true })); + const worktree = buildGitActions( + createInput({ hasUncommittedChanges: true, isPaseoOwnedWorktree: true }), + ); - expect(hidden.secondary.some((action) => action.id === "archive-worktree")).toBe(false); - expect(shown.secondary.some((action) => action.id === "archive-worktree")).toBe(true); + expect(directory.primary?.id).toBe("archive-workspace"); + expect(localCheckout.secondary.some((action) => action.id === "archive-workspace")).toBe(true); + expect(worktree.secondary.some((action) => action.id === "archive-workspace")).toBe(true); }); it("promotes squash-and-merge when an open PR is mergeable and the branch is in sync", () => { @@ -541,6 +546,7 @@ describe("git-actions-policy", () => { "merge-pr-squash", "merge-pr-merge", "merge-pr-rebase", + "archive-workspace", ]); }); @@ -717,6 +723,7 @@ describe("git-actions-policy", () => { "merge-pr-squash", "merge-pr-merge", "merge-pr-rebase", + "archive-workspace", ]); }); @@ -757,6 +764,7 @@ describe("git-actions-policy", () => { "merge-branch", "pr", "enable-pr-auto-merge-squash", + "archive-workspace", ]); expect( actions.secondary.some((action) => @@ -899,6 +907,7 @@ describe("git-actions-policy", () => { "merge-branch", "pr", "merge-pr-merge", + "archive-workspace", ]); }); @@ -952,7 +961,7 @@ describe("git-actions-policy", () => { .filter((action) => !action.startsGroup) .map((action) => action.id); - expect(groupStarters).toEqual(["merge-from-base", "merge-pr-squash", "archive-worktree"]); + expect(groupStarters).toEqual(["merge-from-base", "merge-pr-squash", "archive-workspace"]); expect(nonGroupStarters).toEqual([ "pull", "push", diff --git a/packages/app/src/git/policy.ts b/packages/app/src/git/policy.ts index 63a5d74af..50a3c8933 100644 --- a/packages/app/src/git/policy.ts +++ b/packages/app/src/git/policy.ts @@ -23,7 +23,7 @@ export type GitActionId = | "disable-pr-auto-merge" | "merge-branch" | "merge-from-base" - | "archive-worktree"; + | "archive-workspace"; export interface GitAction { id: GitActionId; @@ -189,10 +189,6 @@ export function narrowPullRequestState(state: string | null | undefined): "open" } export function buildGitActions(input: BuildGitActionsInput): GitActions { - if (!input.isGit) { - return { primary: null, secondary: [], menu: [] }; - } - const allActions = new Map(); allActions.set("commit", { @@ -282,43 +278,39 @@ export function buildGitActions(input: BuildGitActionsInput): GitActions { handler: input.runtime["merge-from-base"].handler, }); - allActions.set("archive-worktree", { - id: "archive-worktree", + allActions.set("archive-workspace", { + id: "archive-workspace", label: i18n.t("workspace.git.actions.archive.label"), pendingLabel: i18n.t("workspace.git.actions.archive.pending"), successLabel: i18n.t("workspace.git.actions.archive.success"), - disabled: input.runtime["archive-worktree"].disabled, - status: input.runtime["archive-worktree"].status, - unavailableMessage: - input.runtime["archive-worktree"].disabled || input.isPaseoOwnedWorktree - ? undefined - : i18n.t("workspace.git.actions.unavailable.archiveNotWorktree"), - icon: input.runtime["archive-worktree"].icon, + disabled: input.runtime["archive-workspace"].disabled, + status: input.runtime["archive-workspace"].status, + icon: input.runtime["archive-workspace"].icon, startsGroup: true, - handler: input.runtime["archive-worktree"].handler, + handler: input.runtime["archive-workspace"].handler, }); const primaryActionId = getPrimaryActionId(input); const primary = primaryActionId ? (allActions.get(primaryActionId) ?? null) : null; - const secondaryIds = [...REMOTE_ACTION_IDS]; - if (!input.isOnBaseBranch) { + const secondaryIds = input.isGit ? [...REMOTE_ACTION_IDS] : []; + if (input.isGit && !input.isOnBaseBranch) { secondaryIds.push(...getFeatureActionIds(input)); } - if (input.isPaseoOwnedWorktree) { - secondaryIds.push("archive-worktree"); - } + secondaryIds.push("archive-workspace"); return { primary, - secondary: secondaryIds.map((id) => allActions.get(id)!), + secondary: secondaryIds + .filter((id) => id !== "archive-workspace" || primaryActionId !== "archive-workspace") + .map((id) => allActions.get(id)!), menu: [], }; } function getPrimaryActionId(input: BuildGitActionsInput): GitActionId | null { - if (input.shouldPromoteArchive && input.isPaseoOwnedWorktree) { - return "archive-worktree"; + if (input.shouldPromoteArchive) { + return "archive-workspace"; } if (input.hasUncommittedChanges) { return "commit"; @@ -350,7 +342,7 @@ function getPrimaryActionId(input: BuildGitActionsInput): GitActionId | null { if (input.githubFeaturesEnabled && input.hasPullRequest && input.pullRequestUrl) { return "pr"; } - return null; + return "archive-workspace"; } function getPullRequestActionIds(filter: { diff --git a/packages/app/src/git/use-actions.tsx b/packages/app/src/git/use-actions.tsx index 22491d60f..5f36092f8 100644 --- a/packages/app/src/git/use-actions.tsx +++ b/packages/app/src/git/use-actions.tsx @@ -14,7 +14,7 @@ import { import type { CheckoutPrMergeMethod } from "@getpaseo/protocol/messages"; import { openExternalUrl } from "@/utils/open-external-url"; import { useToast } from "@/contexts/toast-context"; -import { useSessionStore } from "@/stores/session-store"; +import { useSessionStore, type WorkspaceDescriptor } from "@/stores/session-store"; import { useActiveWorkspaceSelection, type ActiveWorkspaceSelection, @@ -22,6 +22,7 @@ import { import { redirectIfArchivingActiveWorkspace } from "@/utils/sidebar-workspace-archive-redirect"; import { type WorktreeArchiveWarningLabels } from "@/git/worktree-archive-warning"; import { useWorkspaceArchive } from "@/workspace/use-workspace-archive"; +import { resolveWorkspaceMapKeyByIdentity } from "@/utils/workspace-identity"; export type { GitActionId, GitAction, GitActions } from "@/git/policy"; @@ -97,15 +98,12 @@ function extractGitCommitCounts(gitStatus: CheckoutStatusPayload | null): GitCom } function computeShouldPromoteArchive(input: { - isPaseoOwnedWorktree: boolean; hasUncommittedChanges: boolean; postShipArchiveSuggested: boolean; isMergedPullRequest: boolean; }): boolean { return ( - input.isPaseoOwnedWorktree && - !input.hasUncommittedChanges && - (input.postShipArchiveSuggested || input.isMergedPullRequest) + !input.hasUncommittedChanges && (input.postShipArchiveSuggested || input.isMergedPullRequest) ); } @@ -131,7 +129,6 @@ function deriveGitActionsState(args: DeriveGitActionsStateArgs): DerivedGitActio isPaseoOwnedWorktree, isOnBaseBranch: gitStatus?.currentBranch === baseRefLabel, shouldPromoteArchive: computeShouldPromoteArchive({ - isPaseoOwnedWorktree, hasUncommittedChanges, postShipArchiveSuggested, isMergedPullRequest, @@ -173,6 +170,52 @@ interface UseWorkspaceScreenArchiveControllerInput { t: (key: string, options?: Record) => string; } +function resolveArchiveWorkspaceDescriptor(input: { + workspaces: Map | undefined; + activeWorkspaceSelection: ActiveWorkspaceSelection | null; + workspaceDirectory: string | null | undefined; +}): WorkspaceDescriptor | null { + const activeWorkspaceKey = input.activeWorkspaceSelection + ? resolveWorkspaceMapKeyByIdentity({ + workspaces: input.workspaces, + workspaceId: input.activeWorkspaceSelection.workspaceId, + }) + : null; + if (activeWorkspaceKey) { + return input.workspaces?.get(activeWorkspaceKey) ?? null; + } + if (!input.workspaceDirectory) { + return null; + } + for (const candidate of input.workspaces?.values() ?? []) { + if (candidate.workspaceDirectory === input.workspaceDirectory) { + return candidate; + } + } + return null; +} + +function resolveWorkspaceArchiveRisk( + workspace: WorkspaceDescriptor | null, + gitStatus: CheckoutStatusPayload | null, +): { isDirty: boolean | null | undefined; aheadOfOrigin: number | null | undefined } { + return { + isDirty: gitStatus?.isDirty ?? workspace?.gitRuntime?.isDirty, + aheadOfOrigin: gitStatus?.aheadOfOrigin ?? workspace?.gitRuntime?.aheadOfOrigin, + }; +} + +function canArchiveWorkspace( + workspace: WorkspaceDescriptor | null, + risk: ReturnType, +): boolean { + return ( + workspace !== null && + (workspace.workspaceKind !== "worktree" || + (risk.isDirty !== undefined && risk.aheadOfOrigin !== undefined)) + ); +} + function useWorkspaceScreenArchiveController({ serverId, activeWorkspaceSelection, @@ -182,28 +225,28 @@ function useWorkspaceScreenArchiveController({ t, }: UseWorkspaceScreenArchiveControllerInput) { const sessionWorkspaces = useSessionStore((state) => state.sessions[serverId]?.workspaces); - const archiveWorkspaceRecord = useMemo(() => { - if (!workspaceDirectory) { - return null; - } - for (const candidate of sessionWorkspaces?.values() ?? []) { - if (candidate.workspaceDirectory === workspaceDirectory) { - return candidate; - } - } - return null; - }, [sessionWorkspaces, workspaceDirectory]); + const [isHidingWorkspace, setIsHidingWorkspace] = useState(false); + const workspaceDescriptor = useMemo( + () => + resolveArchiveWorkspaceDescriptor({ + workspaces: sessionWorkspaces, + activeWorkspaceSelection, + workspaceDirectory, + }), + [activeWorkspaceSelection, sessionWorkspaces, workspaceDirectory], + ); + const archiveRisk = resolveWorkspaceArchiveRisk(workspaceDescriptor, gitStatus); - return useWorkspaceArchive({ + const controller = useWorkspaceArchive({ serverId, - workspaceId: activeWorkspaceSelection?.workspaceId ?? archiveWorkspaceRecord?.id ?? "", - workspaceDirectory, - workspaceKind: gitStatus?.isPaseoOwnedWorktree ? "worktree" : "local_checkout", - name: archiveWorkspaceRecord?.name ?? branchLabel, - isDirty: gitStatus?.isDirty, - aheadOfOrigin: gitStatus?.aheadOfOrigin, - diffStat: archiveWorkspaceRecord?.diffStat ?? null, + workspaceId: workspaceDescriptor?.id ?? "", + workspaceKind: workspaceDescriptor?.workspaceKind ?? "directory", + name: workspaceDescriptor?.name ?? branchLabel, + isDirty: archiveRisk.isDirty, + aheadOfOrigin: archiveRisk.aheadOfOrigin, + diffStat: workspaceDescriptor?.diffStat ?? null, warningLabels: getWorktreeArchiveWarningLabels(t), + onSetHiding: setIsHidingWorkspace, onArchiveStarted: () => { if (!activeWorkspaceSelection) { return; @@ -215,6 +258,12 @@ function useWorkspaceScreenArchiveController({ }); }, }); + + return { + ...controller, + isArchiving: workspaceDescriptor?.archivingAt != null || isHidingWorkspace, + canArchive: canArchiveWorkspace(workspaceDescriptor, archiveRisk), + }; } export function useGitActions({ serverId, cwd, icons }: UseGitActionsInput): UseGitActionsResult { @@ -342,9 +391,6 @@ export function useGitActions({ serverId, cwd, icons }: UseGitActionsInput): Use const mergeFromBaseStatus = useCheckoutGitActionsStore((s) => s.getStatus({ serverId, cwd, actionId: "merge-from-base" }), ); - const archiveStatus = useCheckoutGitActionsStore((s) => - s.getStatus({ serverId, cwd, actionId: "archive-worktree" }), - ); const runCommit = useCheckoutGitActionsStore((s) => s.commit); const runPull = useCheckoutGitActionsStore((s) => s.pull); @@ -533,7 +579,7 @@ export function useGitActions({ serverId, cwd, icons }: UseGitActionsInput): Use t, }); - const handleArchiveWorktree = useCallback(() => { + const handleArchiveWorkspace = useCallback(() => { archiveController.archive(); }, [archiveController]); @@ -678,11 +724,11 @@ export function useGitActions({ serverId, cwd, icons }: UseGitActionsInput): Use icon: icons.mergeFromBase, handler: handleMergeFromBase, }, - "archive-worktree": { - disabled: isActionDisabled(actionsDisabled, archiveStatus), - status: archiveStatus, + "archive-workspace": { + disabled: !archiveController.canArchive || archiveController.isArchiving, + status: archiveController.isArchiving ? "pending" : "idle", icon: icons.archive, - handler: handleArchiveWorktree, + handler: handleArchiveWorkspace, }, }, }), @@ -723,7 +769,8 @@ export function useGitActions({ serverId, cwd, icons }: UseGitActionsInput): Use disablePrAutoMergeStatus, mergeStatus, mergeFromBaseStatus, - archiveStatus, + archiveController.canArchive, + archiveController.isArchiving, handleCommit, handlePull, handlePush, @@ -734,7 +781,7 @@ export function useGitActions({ serverId, cwd, icons }: UseGitActionsInput): Use handleDisablePrAutoMerge, handleMergeBranch, handleMergeFromBase, - handleArchiveWorktree, + handleArchiveWorkspace, icons, baseRef, ], @@ -890,7 +937,7 @@ function getTranslatedGitActionLabels( pendingLabel: t("workspace.git.actions.mergeFromBase.pending"), successLabel: t("workspace.git.actions.mergeFromBase.success"), }; - case "archive-worktree": + case "archive-workspace": return { label: t("workspace.git.actions.archive.label"), pendingLabel: t("workspace.git.actions.archive.pending"), @@ -945,8 +992,6 @@ function translateGitActionUnavailableMessage( "workspace.git.actions.unavailable.updateNoBase", "Update isn't available while you have local changes so commit or stash them first": "workspace.git.actions.unavailable.updateDirty", - "Archive isn't available here because this workspace was not created as a Paseo worktree": - "workspace.git.actions.unavailable.archiveNotWorktree", "Merge PR isn't available right now because GitHub isn't connected": "workspace.git.actions.unavailable.mergePrNoGithub", "Merge PR isn't available because there isn't a pull request yet": @@ -979,7 +1024,7 @@ function getWorktreeArchiveWarningLabels( t: (key: string, options?: Record) => string, ): WorktreeArchiveWarningLabels { return { - title: (worktreeName) => t("workspace.git.actions.archiveWarning.title", { worktreeName }), + title: (workspaceName) => t("workspace.git.actions.archiveWarning.title", { workspaceName }), confirm: t("workspace.git.actions.archiveWarning.confirm"), cancel: t("workspace.git.actions.archiveWarning.cancel"), uncommittedChanges: t("workspace.git.actions.archiveWarning.uncommittedChanges"), diff --git a/packages/app/src/git/workspace-actions.tsx b/packages/app/src/git/workspace-actions.tsx index e1921f9d5..9149ba591 100644 --- a/packages/app/src/git/workspace-actions.tsx +++ b/packages/app/src/git/workspace-actions.tsx @@ -13,7 +13,7 @@ import { GitActionsSplitButton } from "@/git/actions-split-button"; import { useGitActions } from "@/git/use-actions"; import type { Theme } from "@/styles/theme"; -interface WorkspaceGitActionsProps { +interface WorkspaceActionsProps { serverId: string; cwd: string; hideLabels?: boolean; @@ -47,16 +47,12 @@ const ICONS = { archive: , }; -export function WorkspaceGitActions({ serverId, cwd, hideLabels }: WorkspaceGitActionsProps) { - const { gitActions, isGit } = useGitActions({ +export function WorkspaceActions({ serverId, cwd, hideLabels }: WorkspaceActionsProps) { + const { gitActions } = useGitActions({ serverId, cwd, icons: ICONS, }); - if (!isGit) { - return null; - } - return ; } diff --git a/packages/app/src/git/worktree-archive-warning.test.ts b/packages/app/src/git/worktree-archive-warning.test.ts index 362afade0..ddf60c774 100644 --- a/packages/app/src/git/worktree-archive-warning.test.ts +++ b/packages/app/src/git/worktree-archive-warning.test.ts @@ -6,11 +6,11 @@ import { toWorktreeArchiveRisk, } from "@/git/worktree-archive-warning"; -describe("worktree archive warning", () => { +describe("workspace archive warning for worktree backing", () => { it("does not require a confirmation for clean and pushed worktrees", () => { expect( buildWorktreeArchiveConfirmationMessage({ - worktreeName: "feature", + workspaceName: "feature", isDirty: false, aheadOfOrigin: 0, diffStat: null, @@ -51,7 +51,7 @@ describe("worktree archive warning", () => { it("includes every archive risk in the confirmation copy", () => { expect( buildWorktreeArchiveConfirmationMessage({ - worktreeName: "risky-feature", + workspaceName: "risky-feature", isDirty: true, aheadOfOrigin: 1, diffStat: { additions: 1, deletions: 3 }, diff --git a/packages/app/src/git/worktree-archive-warning.ts b/packages/app/src/git/worktree-archive-warning.ts index 51e882eaa..f7405cd2b 100644 --- a/packages/app/src/git/worktree-archive-warning.ts +++ b/packages/app/src/git/worktree-archive-warning.ts @@ -14,11 +14,11 @@ export interface WorktreeArchiveRiskInput { } export interface WorktreeArchiveConfirmationInput extends WorktreeArchiveRisk { - worktreeName: string; + workspaceName: string; } export interface WorktreeArchiveWarningLabels { - title: (worktreeName: string) => string; + title: (workspaceName: string) => string; confirm: string; cancel: string; uncommittedChanges: string; @@ -29,7 +29,7 @@ export interface WorktreeArchiveWarningLabels { } export const DEFAULT_WORKTREE_ARCHIVE_WARNING_LABELS: WorktreeArchiveWarningLabels = { - title: (worktreeName) => i18n.t("workspace.git.actions.archiveWarning.title", { worktreeName }), + title: (workspaceName) => i18n.t("workspace.git.actions.archiveWarning.title", { workspaceName }), confirm: i18n.t("workspace.git.actions.archiveWarning.confirm"), cancel: i18n.t("workspace.git.actions.archiveWarning.cancel"), uncommittedChanges: i18n.t("workspace.git.actions.archiveWarning.uncommittedChanges"), @@ -123,7 +123,7 @@ export async function confirmRiskyWorktreeArchive( } return await confirmDialog({ - title: labels.title(input.worktreeName), + title: labels.title(input.workspaceName), message, confirmLabel: labels.confirm, cancelLabel: labels.cancel, diff --git a/packages/app/src/i18n/resources/ar.ts b/packages/app/src/i18n/resources/ar.ts index abf0703f9..09ccba2f4 100644 --- a/packages/app/src/i18n/resources/ar.ts +++ b/packages/app/src/i18n/resources/ar.ts @@ -619,7 +619,7 @@ export const ar: TranslationResources = { success: "تم التحديث", }, archive: { - label: "أرشفة شجرة العمل", + label: "أرشفة مساحة العمل", pending: "أرشفة...", success: "مؤرشف", }, @@ -661,8 +661,6 @@ export const ar: TranslationResources = { updateNoBase: "التحديث غير متاح لأننا لم نتمكن من تحديد الفرع الأساسي", updateDirty: "التحديث غير متاح أثناء وجود تغييرات محلية، لذا قم بتنفيذها أو تخزينها أولاً", updateCurrent: "التحديث غير متاح لأن هذا الفرع محدث بالفعل باستخدام{{baseRef}}", - archiveNotWorktree: - "الأرشيف غير متاح هنا لأنه لم يتم إنشاء مساحة العمل هذه كشجرة عمل Paseo", mergePrNoGithub: "دمج PR غير متاح الآن لأن GitHub غير متصل", mergePrMissing: "دمج PR غير متاح لأنه لا يوجد طلب سحب حتى الآن", mergePrDraft: "دمج PR غير متاح لأن طلب السحب لا يزال مسودة", @@ -685,11 +683,9 @@ export const ar: TranslationResources = { baseRefUnavailable: "المرجع الأساسي غير متاح", failedMerge: "فشل الدمج", failedMergeFromBase: "فشل الدمج من القاعدة", - worktreePathUnavailable: "مسار شجرة العمل غير متوفر", - failedArchive: "فشل في أرشفة شجرة العمل", }, archiveWarning: { - title: 'الأرشيف "{{worktreeName}}"؟', + title: 'الأرشيف "{{workspaceName}}"؟', confirm: "أرشيف", cancel: "يلغي", uncommittedChanges: "تغييرات غير ملتزم بها", @@ -839,7 +835,7 @@ export const ar: TranslationResources = { copyBranchName: "انسخ اسم الفرع", rename: "إعادة تسمية مساحة العمل", archive: "أرشيف", - archiveWorktree: "أرشفة شجرة العمل", + archiveWorkspace: "أرشفة مساحة العمل", hideFromSidebar: "إخفاء من الشريط الجانبي", archiving: "أرشفة...", hiding: "إخفاء...", @@ -862,7 +858,7 @@ export const ar: TranslationResources = { branchNameCopied: "تم نسخ اسم الفرع", hostDisconnected: "Host غير متصل", hideFailed: "فشل في إخفاء مساحة العمل", - archiveFailed: "فشل في أرشفة شجرة العمل", + archiveFailed: "فشل في أرشفة مساحة العمل", }, }, }, @@ -1605,7 +1601,7 @@ export const ar: TranslationResources = { openProject: "مشروع مفتوح", newWorkspace: "مساحة عمل جديدة", newWorktree: "شجرة عمل جديدة", - archiveWorktree: "أرشفة شجرة العمل", + archiveWorkspace: "أرشفة مساحة العمل", newTab: "علامة تبويب جديدة", closeCurrentTab: "إغلاق علامة التبويب الحالية", jumpToWorkspace: "انتقل إلى مساحة العمل", diff --git a/packages/app/src/i18n/resources/en.ts b/packages/app/src/i18n/resources/en.ts index 782c697ad..66ae84aaf 100644 --- a/packages/app/src/i18n/resources/en.ts +++ b/packages/app/src/i18n/resources/en.ts @@ -618,7 +618,7 @@ export const en = { success: "Updated", }, archive: { - label: "Archive worktree", + label: "Archive workspace", pending: "Archiving...", success: "Archived", }, @@ -667,8 +667,6 @@ export const en = { "Update isn't available while you have local changes so commit or stash them first", updateCurrent: "Update isn't available because this branch is already up to date with {{baseRef}}", - archiveNotWorktree: - "Archive isn't available here because this workspace was not created as a Paseo worktree", mergePrNoGithub: "Merge PR isn't available right now because GitHub isn't connected", mergePrMissing: "Merge PR isn't available because there isn't a pull request yet", mergePrDraft: "Merge PR isn't available because the pull request is still a draft", @@ -692,11 +690,9 @@ export const en = { baseRefUnavailable: "Base ref unavailable", failedMerge: "Failed to merge", failedMergeFromBase: "Failed to merge from base", - worktreePathUnavailable: "Worktree path unavailable", - failedArchive: "Failed to archive worktree", }, archiveWarning: { - title: 'Archive "{{worktreeName}}"?', + title: 'Archive "{{workspaceName}}"?', confirm: "Archive", cancel: "Cancel", uncommittedChanges: "Uncommitted changes", @@ -846,7 +842,7 @@ export const en = { copyBranchName: "Copy branch name", rename: "Rename workspace", archive: "Archive", - archiveWorktree: "Archive worktree", + archiveWorkspace: "Archive workspace", hideFromSidebar: "Hide from sidebar", archiving: "Archiving...", hiding: "Hiding...", @@ -869,7 +865,7 @@ export const en = { branchNameCopied: "Branch name copied", hostDisconnected: "Host is not connected", hideFailed: "Failed to hide workspace", - archiveFailed: "Failed to archive worktree", + archiveFailed: "Failed to archive workspace", }, }, }, @@ -1612,7 +1608,7 @@ export const en = { openProject: "Open project", newWorkspace: "New workspace", newWorktree: "New worktree", - archiveWorktree: "Archive worktree", + archiveWorkspace: "Archive workspace", newTab: "New tab", closeCurrentTab: "Close current tab", jumpToWorkspace: "Jump to workspace", diff --git a/packages/app/src/i18n/resources/es.ts b/packages/app/src/i18n/resources/es.ts index e4344d972..1c1efbb12 100644 --- a/packages/app/src/i18n/resources/es.ts +++ b/packages/app/src/i18n/resources/es.ts @@ -625,7 +625,7 @@ export const es: TranslationResources = { success: "Actualizado", }, archive: { - label: "Árbol de trabajo de archivo", + label: "Archivar espacio de trabajo", pending: "Archivando...", success: "Archivado", }, @@ -679,8 +679,6 @@ export const es: TranslationResources = { "La actualización no está disponible mientras tenga cambios locales, así que confírmelos o guárdelos primero", updateCurrent: "La actualización no está disponible porque esta rama ya está actualizada con{{baseRef}}", - archiveNotWorktree: - "El archivo no está disponible aquí porque este espacio de trabajo no se creó como un árbol de trabajoPaseo", mergePrNoGithub: "FusionarPRno está disponible en este momento porqueGitHubno está conectado", mergePrMissing: @@ -712,11 +710,9 @@ export const es: TranslationResources = { baseRefUnavailable: "Referencia base no disponible", failedMerge: "No se pudo fusionar", failedMergeFromBase: "No se pudo fusionar desde la base", - worktreePathUnavailable: "Ruta del árbol de trabajo no disponible", - failedArchive: "No se pudo archivar el árbol de trabajo", }, archiveWarning: { - title: '¿Archivo "{{worktreeName}}"?', + title: '¿Archivo "{{workspaceName}}"?', confirm: "Archivo", cancel: "Cancelar", uncommittedChanges: "Cambios no confirmados", @@ -866,7 +862,7 @@ export const es: TranslationResources = { copyBranchName: "Copiar nombre de sucursal", rename: "Cambiar nombre del espacio de trabajo", archive: "Archivo", - archiveWorktree: "Árbol de trabajo de archivo", + archiveWorkspace: "Archivar espacio de trabajo", hideFromSidebar: "Ocultar de la barra lateral", archiving: "Archivando...", hiding: "Ocultación...", @@ -889,7 +885,7 @@ export const es: TranslationResources = { branchNameCopied: "Nombre de la sucursal copiado", hostDisconnected: "Hostno está conectado", hideFailed: "No se pudo ocultar el espacio de trabajo", - archiveFailed: "No se pudo archivar el árbol de trabajo", + archiveFailed: "No se pudo archivar el espacio de trabajo", }, }, }, @@ -1647,7 +1643,7 @@ export const es: TranslationResources = { openProject: "Abrir proyecto", newWorkspace: "Nuevo espacio de trabajo", newWorktree: "Nuevo árbol de trabajo", - archiveWorktree: "Árbol de trabajo de archivo", + archiveWorkspace: "Archivar espacio de trabajo", newTab: "Nueva pestaña", closeCurrentTab: "Cerrar pestaña actual", jumpToWorkspace: "Saltar al espacio de trabajo", diff --git a/packages/app/src/i18n/resources/fr.ts b/packages/app/src/i18n/resources/fr.ts index 7cc502f2d..5a42a8858 100644 --- a/packages/app/src/i18n/resources/fr.ts +++ b/packages/app/src/i18n/resources/fr.ts @@ -625,7 +625,7 @@ export const fr: TranslationResources = { success: "Mis à jour", }, archive: { - label: "Arbre de travail d'archivage", + label: "Archiver l’espace de travail", pending: "Archivage...", success: "Archivé", }, @@ -679,8 +679,6 @@ export const fr: TranslationResources = { "La mise à jour n'est pas disponible tant que vous avez des modifications locales, alors validez-les ou cachez-les d'abord", updateCurrent: "La mise à jour n'est pas disponible car cette branche est déjà à jour avec{{baseRef}}", - archiveNotWorktree: - "L'archive n'est pas disponible ici car cet espace de travail n'a pas été créé en tant qu'arbre de travailPaseo", mergePrNoGithub: "La fusionPRn'est pas disponible pour le moment carGitHubn'est pas connecté", mergePrMissing: @@ -711,11 +709,9 @@ export const fr: TranslationResources = { baseRefUnavailable: "Réf de base indisponible", failedMerge: "Échec de la fusion", failedMergeFromBase: "Échec de la fusion à partir de la base", - worktreePathUnavailable: "Chemin d'accès à l'arbre de travail indisponible", - failedArchive: "Échec de l'archivage de l'arbre de travail", }, archiveWarning: { - title: "Archiver «{{worktreeName}}»?", + title: "Archiver «{{workspaceName}}»?", confirm: "Archive", cancel: "Annuler", uncommittedChanges: "Modifications non validées", @@ -865,7 +861,7 @@ export const fr: TranslationResources = { copyBranchName: "Copier le nom de la branche", rename: "Renommer l'espace de travail", archive: "Archive", - archiveWorktree: "Arbre de travail d'archivage", + archiveWorkspace: "Archiver l’espace de travail", hideFromSidebar: "Masquer de la barre latérale", archiving: "Archivage...", hiding: "Dissimulation...", @@ -888,7 +884,7 @@ export const fr: TranslationResources = { branchNameCopied: "Nom de la succursale copié", hostDisconnected: "Hostn'est pas connecté", hideFailed: "Échec du masquage de l'espace de travail", - archiveFailed: "Échec de l'archivage de l'arbre de travail", + archiveFailed: "Échec de l'archivage de l'espace de travail", }, }, }, @@ -1650,7 +1646,7 @@ export const fr: TranslationResources = { openProject: "Projet ouvert", newWorkspace: "Nouvel espace de travail", newWorktree: "Nouvel arbre de travail", - archiveWorktree: "Arbre de travail d'archivage", + archiveWorkspace: "Archiver l’espace de travail", newTab: "Nouvel onglet", closeCurrentTab: "Fermer l'onglet actuel", jumpToWorkspace: "Accéder à l'espace de travail", diff --git a/packages/app/src/i18n/resources/ja.ts b/packages/app/src/i18n/resources/ja.ts index 7a809d74a..14612aa4e 100644 --- a/packages/app/src/i18n/resources/ja.ts +++ b/packages/app/src/i18n/resources/ja.ts @@ -623,7 +623,7 @@ export const ja: TranslationResources = { success: "更新しました", }, archive: { - label: "ワークツリーをアーカイブ", + label: "ワークスペースをアーカイブ", pending: "アーカイブ中...", success: "アーカイブしました", }, @@ -670,8 +670,6 @@ export const ja: TranslationResources = { updateDirty: "ローカルに変更があるため更新は利用できません。先にコミットまたはスタッシュしてください", updateCurrent: "このブランチはすでに{{baseRef}}と最新の状態のため、更新は利用できません", - archiveNotWorktree: - "このワークスペースはPaseoワークツリーとして作成されていないため、アーカイブはここでは利用できません", mergePrNoGithub: "GitHubが接続されていないため、PRのマージは現在利用できません", mergePrMissing: "プルリクエストがまだないため、PRのマージは利用できません", mergePrDraft: "プルリクエストがまだドラフトのため、PRのマージは利用できません", @@ -697,11 +695,9 @@ export const ja: TranslationResources = { baseRefUnavailable: "ベースRefが利用できません", failedMerge: "マージに失敗しました", failedMergeFromBase: "ベースからのマージに失敗しました", - worktreePathUnavailable: "ワークツリーパスが利用できません", - failedArchive: "ワークツリーのアーカイブに失敗しました", }, archiveWarning: { - title: '"{{worktreeName}}"をアーカイブしますか?', + title: '"{{workspaceName}}"をアーカイブしますか?', confirm: "アーカイブ", cancel: "キャンセル", uncommittedChanges: "未コミットの変更", @@ -851,7 +847,7 @@ export const ja: TranslationResources = { copyBranchName: "ブランチ名をコピー", rename: "ワークスペースの名前を変更", archive: "アーカイブ", - archiveWorktree: "ワークツリーをアーカイブ", + archiveWorkspace: "ワークスペースをアーカイブ", hideFromSidebar: "サイドバーから非表示", archiving: "アーカイブ中...", hiding: "非表示にしています...", @@ -874,7 +870,7 @@ export const ja: TranslationResources = { branchNameCopied: "ブランチ名をコピーしました", hostDisconnected: "ホストが接続されていません", hideFailed: "ワークスペースの非表示に失敗しました", - archiveFailed: "ワークツリーのアーカイブに失敗しました", + archiveFailed: "ワークスペースのアーカイブに失敗しました", }, }, }, @@ -1622,7 +1618,7 @@ export const ja: TranslationResources = { openProject: "プロジェクトを開く", newWorkspace: "新しいワークスペース", newWorktree: "新しいワークツリー", - archiveWorktree: "ワークツリーをアーカイブ", + archiveWorkspace: "ワークスペースをアーカイブ", newTab: "新しいタブ", closeCurrentTab: "現在のタブを閉じる", jumpToWorkspace: "ワークスペースにジャンプ", diff --git a/packages/app/src/i18n/resources/pt-BR.ts b/packages/app/src/i18n/resources/pt-BR.ts index 9960dcc8c..a7a124be8 100644 --- a/packages/app/src/i18n/resources/pt-BR.ts +++ b/packages/app/src/i18n/resources/pt-BR.ts @@ -623,7 +623,7 @@ export const ptBR: TranslationResources = { success: "Atualizado", }, archive: { - label: "Arquivar worktree", + label: "Arquivar workspace", pending: "Arquivando...", success: "Arquivado", }, @@ -675,8 +675,6 @@ export const ptBR: TranslationResources = { "Atualizar não está disponível enquanto há alterações locais. Faça commit ou stash primeiro", updateCurrent: "Atualizar não está disponível porque esta branch já está atualizada com {{baseRef}}", - archiveNotWorktree: - "Arquivar não está disponível aqui porque este workspace não foi criado como um worktree do Paseo", mergePrNoGithub: "Merge da PR não está disponível agora porque o GitHub não está conectado", mergePrMissing: "Merge da PR não está disponível porque ainda não há uma pull request", @@ -703,11 +701,9 @@ export const ptBR: TranslationResources = { baseRefUnavailable: "Ref base indisponível", failedMerge: "Falha ao fazer merge", failedMergeFromBase: "Falha ao fazer merge da base", - worktreePathUnavailable: "Caminho do worktree indisponível", - failedArchive: "Falha ao arquivar worktree", }, archiveWarning: { - title: 'Arquivar "{{worktreeName}}"?', + title: 'Arquivar "{{workspaceName}}"?', confirm: "Arquivar", cancel: "Cancelar", uncommittedChanges: "Alterações sem commit", @@ -857,7 +853,7 @@ export const ptBR: TranslationResources = { copyBranchName: "Copiar nome da branch", rename: "Renomear workspace", archive: "Arquivar", - archiveWorktree: "Arquivar worktree", + archiveWorkspace: "Arquivar workspace", hideFromSidebar: "Ocultar da barra lateral", archiving: "Arquivando...", hiding: "Ocultando...", @@ -880,7 +876,7 @@ export const ptBR: TranslationResources = { branchNameCopied: "Nome da branch copiado", hostDisconnected: "Host não está conectado", hideFailed: "Falha ao ocultar workspace", - archiveFailed: "Falha ao arquivar worktree", + archiveFailed: "Falha ao arquivar workspace", }, }, }, @@ -1631,7 +1627,7 @@ export const ptBR: TranslationResources = { openProject: "Abrir projeto", newWorkspace: "Novo workspace", newWorktree: "Novo worktree", - archiveWorktree: "Arquivar worktree", + archiveWorkspace: "Arquivar workspace", newTab: "Nova aba", closeCurrentTab: "Fechar aba atual", jumpToWorkspace: "Ir para workspace", diff --git a/packages/app/src/i18n/resources/ru.ts b/packages/app/src/i18n/resources/ru.ts index a36804969..7fed4d9a7 100644 --- a/packages/app/src/i18n/resources/ru.ts +++ b/packages/app/src/i18n/resources/ru.ts @@ -624,7 +624,7 @@ export const ru: TranslationResources = { success: "Обновлено", }, archive: { - label: "Архив рабочего дерева", + label: "Архивировать рабочее пространство", pending: "Архивирование...", success: "В архиве", }, @@ -675,8 +675,6 @@ export const ru: TranslationResources = { "Обновление недоступно, пока у вас есть локальные изменения, поэтому сначала зафиксируйте или сохраните их.", updateCurrent: "Обновление недоступно, поскольку эта ветка уже обновлена ​​до версии{{baseRef}}.", - archiveNotWorktree: - "Архив здесь недоступен, поскольку это рабочее пространство не было создано как рабочее дерево Paseo.", mergePrNoGithub: "Объединение PR сейчас недоступно, поскольку GitHub не подключен.", mergePrMissing: "Объединение PR недоступно, поскольку еще нет запроса на включение", mergePrDraft: @@ -704,11 +702,9 @@ export const ru: TranslationResources = { baseRefUnavailable: "Базовый номер недоступен.", failedMerge: "Не удалось объединиться", failedMergeFromBase: "Не удалось объединиться с базой.", - worktreePathUnavailable: "Путь к рабочему дереву недоступен.", - failedArchive: "Не удалось заархивировать рабочее дерево.", }, archiveWarning: { - title: 'Архив "{{worktreeName}}"?', + title: 'Архив "{{workspaceName}}"?', confirm: "Архив", cancel: "Отмена", uncommittedChanges: "Незафиксированные изменения", @@ -858,7 +854,7 @@ export const ru: TranslationResources = { copyBranchName: "Скопировать название ветки", rename: "Переименовать рабочую область", archive: "Архив", - archiveWorktree: "Архив рабочего дерева", + archiveWorkspace: "Архивировать рабочее пространство", hideFromSidebar: "Скрыть с боковой панели", archiving: "Архивирование...", hiding: "Скрытие...", @@ -881,7 +877,7 @@ export const ru: TranslationResources = { branchNameCopied: "Название филиала скопировано.", hostDisconnected: "Host не подключен", hideFailed: "Не удалось скрыть рабочую область.", - archiveFailed: "Не удалось заархивировать рабочее дерево.", + archiveFailed: "Не удалось заархивировать рабочее пространство.", }, }, }, @@ -1639,7 +1635,7 @@ export const ru: TranslationResources = { openProject: "Открыть проект", newWorkspace: "Новое рабочее пространство", newWorktree: "Новое рабочее дерево", - archiveWorktree: "Архив рабочего дерева", + archiveWorkspace: "Архивировать рабочее пространство", newTab: "Новая вкладка", closeCurrentTab: "Закрыть текущую вкладку", jumpToWorkspace: "Перейти в рабочую область", diff --git a/packages/app/src/i18n/resources/zh-CN.ts b/packages/app/src/i18n/resources/zh-CN.ts index 172b8610b..6eb6adefb 100644 --- a/packages/app/src/i18n/resources/zh-CN.ts +++ b/packages/app/src/i18n/resources/zh-CN.ts @@ -617,7 +617,7 @@ export const zhCN: TranslationResources = { success: "已更新", }, archive: { - label: "归档 worktree", + label: "归档工作区", pending: "正在归档...", success: "已归档", }, @@ -656,7 +656,6 @@ export const zhCN: TranslationResources = { updateNoBase: "无法更新,因为无法确定 base branch", updateDirty: "有本地变更时无法更新,请先 commit 或 stash", updateCurrent: "无法更新,因为此分支已与 {{baseRef}} 保持最新", - archiveNotWorktree: "此处无法归档,因为此 workspace 不是作为 Paseo worktree 创建的", mergePrNoGithub: "当前无法 merge PR,因为 GitHub 未连接", mergePrMissing: "无法 merge PR,因为还没有 pull request", mergePrDraft: "无法 merge PR,因为 pull request 仍是 draft", @@ -679,11 +678,9 @@ export const zhCN: TranslationResources = { baseRefUnavailable: "Base ref 不可用", failedMerge: "Merge 失败", failedMergeFromBase: "从 base merge 失败", - worktreePathUnavailable: "Worktree 路径不可用", - failedArchive: "归档 worktree 失败", }, archiveWarning: { - title: "归档「{{worktreeName}}」?", + title: "归档「{{workspaceName}}」?", confirm: "归档", cancel: "取消", uncommittedChanges: "未 commit 的变更", @@ -831,7 +828,7 @@ export const zhCN: TranslationResources = { copyBranchName: "复制分支名称", rename: "重命名 workspace", archive: "归档", - archiveWorktree: "归档 worktree", + archiveWorkspace: "归档工作区", hideFromSidebar: "从侧边栏隐藏", archiving: "正在归档...", hiding: "正在隐藏...", @@ -853,7 +850,7 @@ export const zhCN: TranslationResources = { branchNameCopied: "分支名称已复制", hostDisconnected: "Host 未连接", hideFailed: "隐藏 workspace 失败", - archiveFailed: "归档 worktree 失败", + archiveFailed: "归档工作区失败", }, }, }, @@ -1586,7 +1583,7 @@ export const zhCN: TranslationResources = { openProject: "打开项目", newWorkspace: "新建 workspace", newWorktree: "新建 worktree", - archiveWorktree: "归档 worktree", + archiveWorkspace: "归档工作区", newTab: "新建标签", closeCurrentTab: "关闭当前标签", jumpToWorkspace: "跳转到 workspace", diff --git a/packages/app/src/keyboard/actions.ts b/packages/app/src/keyboard/actions.ts index 7b9afd11c..2a7889ad6 100644 --- a/packages/app/src/keyboard/actions.ts +++ b/packages/app/src/keyboard/actions.ts @@ -44,7 +44,7 @@ export type KeyboardActionId = | "workspace.terminal.new" | "workspace.new" | "worktree.new" - | "worktree.archive" + | "workspace.archive" | "view.toggle.focus" | "theme.cycle" | "message-input.action"; diff --git a/packages/app/src/keyboard/keyboard-action-dispatcher.ts b/packages/app/src/keyboard/keyboard-action-dispatcher.ts index 021285536..61abf5389 100644 --- a/packages/app/src/keyboard/keyboard-action-dispatcher.ts +++ b/packages/app/src/keyboard/keyboard-action-dispatcher.ts @@ -29,7 +29,7 @@ export type KeyboardActionId = | "sidebar.toggle.right" | "workspace.new" | "worktree.new" - | "worktree.archive"; + | "workspace.archive"; export type KeyboardActionDefinition = | { id: "agent.interrupt"; scope: KeyboardActionScope } @@ -60,7 +60,7 @@ export type KeyboardActionDefinition = | { id: "sidebar.toggle.right"; scope: KeyboardActionScope } | { id: "workspace.new"; scope: KeyboardActionScope } | { id: "worktree.new"; scope: KeyboardActionScope } - | { id: "worktree.archive"; scope: KeyboardActionScope }; + | { id: "workspace.archive"; scope: KeyboardActionScope }; export interface KeyboardActionHandler { handlerId: string; diff --git a/packages/app/src/keyboard/keyboard-shortcuts.ts b/packages/app/src/keyboard/keyboard-shortcuts.ts index d865f3ca7..08dfe7840 100644 --- a/packages/app/src/keyboard/keyboard-shortcuts.ts +++ b/packages/app/src/keyboard/keyboard-shortcuts.ts @@ -121,7 +121,7 @@ const SHORTCUT_HELP_SECTION_LABEL_KEYS: Record = { const SHORTCUT_HELP_LABEL_KEYS: Record = { "new-agent": "settings.shortcuts.help.openProject", "new-workspace": "settings.shortcuts.help.newWorkspace", - "archive-worktree": "settings.shortcuts.help.archiveWorktree", + "archive-workspace": "settings.shortcuts.help.archiveWorkspace", "workspace-tab-new": "settings.shortcuts.help.newTab", "workspace-tab-close-current": "settings.shortcuts.help.closeCurrentTab", "workspace-jump-index": "settings.shortcuts.help.jumpToWorkspace", @@ -221,28 +221,32 @@ const SHORTCUT_BINDINGS: readonly ShortcutBinding[] = [ }, }, - // --- Archive worktree --- + // --- Archive workspace --- { + // COMPAT(workspaceArchiveShortcutOverride): added in v0.1.106; remove after + // 2027-01-11 with a stored-override migration. Keeps existing custom chords. id: "worktree-archive-cmd-shift-backspace-mac", - action: "worktree.archive", + action: "workspace.archive", combo: "Cmd+Shift+Backspace", when: { mac: true, commandCenter: false }, help: { - id: "archive-worktree", + id: "archive-workspace", section: "projects", - label: "Archive worktree", + label: "Archive workspace", keys: ["mod", "shift", "Backspace"], }, }, { + // COMPAT(workspaceArchiveShortcutOverride): added in v0.1.106; remove after + // 2027-01-11 with a stored-override migration. Keeps existing custom chords. id: "worktree-archive-ctrl-shift-backspace-non-mac", - action: "worktree.archive", + action: "workspace.archive", combo: "Ctrl+Shift+Backspace", when: { mac: false, commandCenter: false, terminal: false }, help: { - id: "archive-worktree", + id: "archive-workspace", section: "projects", - label: "Archive worktree", + label: "Archive workspace", keys: ["mod", "shift", "Backspace"], }, }, diff --git a/packages/app/src/keyboard/route-shortcut.test.ts b/packages/app/src/keyboard/route-shortcut.test.ts index 0d6809477..3cf5608b0 100644 --- a/packages/app/src/keyboard/route-shortcut.test.ts +++ b/packages/app/src/keyboard/route-shortcut.test.ts @@ -30,7 +30,7 @@ describe("routeKeyboardShortcut — dispatch passthroughs", () => { ["agent.interrupt", { id: "agent.interrupt", scope: "global" }], ["workspace.tab.new", { id: "workspace.tab.new", scope: "workspace" }], ["workspace.new", { id: "workspace.new", scope: "sidebar" }], - ["worktree.archive", { id: "worktree.archive", scope: "sidebar" }], + ["workspace.archive", { id: "workspace.archive", scope: "sidebar" }], ["worktree.new", { id: "worktree.new", scope: "sidebar" }], ["workspace.terminal.new", { id: "workspace.terminal.new", scope: "workspace" }], ["workspace.tab.close.current", { id: "workspace.tab.close-current", scope: "workspace" }], diff --git a/packages/app/src/keyboard/route-shortcut.ts b/packages/app/src/keyboard/route-shortcut.ts index 82923f760..d771a8758 100644 --- a/packages/app/src/keyboard/route-shortcut.ts +++ b/packages/app/src/keyboard/route-shortcut.ts @@ -46,7 +46,7 @@ const PASSTHROUGH_DISPATCH: Record = { "agent.interrupt": { id: "agent.interrupt", scope: "global" }, "workspace.tab.new": { id: "workspace.tab.new", scope: "workspace" }, "workspace.new": { id: "workspace.new", scope: "sidebar" }, - "worktree.archive": { id: "worktree.archive", scope: "sidebar" }, + "workspace.archive": { id: "workspace.archive", scope: "sidebar" }, "worktree.new": { id: "worktree.new", scope: "sidebar" }, "workspace.terminal.new": { id: "workspace.terminal.new", scope: "workspace" }, "workspace.tab.close.current": { id: "workspace.tab.close-current", scope: "workspace" }, diff --git a/packages/app/src/screens/new-workspace/project-picker.ts b/packages/app/src/screens/new-workspace/project-picker.ts index bb373f394..f408c1913 100644 --- a/packages/app/src/screens/new-workspace/project-picker.ts +++ b/packages/app/src/screens/new-workspace/project-picker.ts @@ -77,11 +77,7 @@ function hasPendingArchiveForProject(input: { } } - const workspaceDirectory = getHostProjectSourceDirectory(input.project, input.selectedServerId); - return isWorkspaceArchivePending({ - serverId: input.selectedServerId, - workspaceDirectory, - }); + return false; } export function useNewWorkspaceProjectPicker({ diff --git a/packages/app/src/screens/workspace/workspace-screen.tsx b/packages/app/src/screens/workspace/workspace-screen.tsx index 2dea82fc0..ad250ea9a 100644 --- a/packages/app/src/screens/workspace/workspace-screen.tsx +++ b/packages/app/src/screens/workspace/workspace-screen.tsx @@ -62,7 +62,7 @@ import { ExplorerSidebar } from "@/components/explorer-sidebar"; import { SplitContainer } from "@/components/split-container"; import { RetainedPanel } from "@/components/retained-panel"; import { SourceControlPanelIcon } from "@/components/icons/source-control-panel-icon"; -import { WorkspaceGitActions } from "@/git/workspace-actions"; +import { WorkspaceActions } from "@/git/workspace-actions"; import { WorkspaceOpenInEditorButton } from "@/screens/workspace/workspace-open-in-editor-button"; import { WorkspaceScriptsButton } from "@/screens/workspace/workspace-scripts-button"; import { ImportSessionSheet } from "@/components/import-session-sheet"; @@ -3276,56 +3276,56 @@ function WorkspaceScreenContent({ hideLabels /> ) : null} - {!isMobile && isGitCheckout ? ( + {!isMobile && workspaceDirectory ? ( <> - {workspaceDirectory ? ( - - ) : null} - - - + {isGitCheckout ? ( + + + + {({ hovered, pressed }) => { + const active = isExplorerOpen || hovered || pressed; + const colorMapping = active ? foregroundColorMapping : mutedColorMapping; + return ( + <> + + {workspaceDescriptor?.diffStat ? ( + + ) : null} + + ); + }} + + + - {({ hovered, pressed }) => { - const active = isExplorerOpen || hovered || pressed; - const colorMapping = active ? foregroundColorMapping : mutedColorMapping; - return ( - <> - - {workspaceDescriptor?.diffStat ? ( - - ) : null} - - ); - }} - - - - - - {t("workspace.tabs.explorer.toggle")} - - - - - + + + {t("workspace.tabs.explorer.toggle")} + + + + + + ) : null} ) : null} {!isMobile && !isGitCheckout ? ( diff --git a/packages/app/src/workspace/project-workspace-archive.test.ts b/packages/app/src/workspace/project-workspace-archive.test.ts index 53cc41a2f..16af61e8a 100644 --- a/packages/app/src/workspace/project-workspace-archive.test.ts +++ b/packages/app/src/workspace/project-workspace-archive.test.ts @@ -31,7 +31,7 @@ describe("selectProjectWorkspacesToArchive", () => { expect(confirmWorktreeArchive).toHaveBeenCalledOnce(); expect(confirmWorktreeArchive).toHaveBeenCalledWith({ - worktreeName: "feature/risky", + workspaceName: "feature/risky", isDirty: true, aheadOfOrigin: 2, diffStat: { additions: 5, deletions: 1 }, @@ -73,7 +73,7 @@ describe("selectProjectWorkspacesToArchive", () => { expect(confirmWorktreeArchive).toHaveBeenCalledOnce(); expect(confirmWorktreeArchive).toHaveBeenCalledWith({ - worktreeName: "feature/risky", + workspaceName: "feature/risky", isDirty: true, aheadOfOrigin: 2, diffStat: { additions: 5, deletions: 1 }, diff --git a/packages/app/src/workspace/project-workspace-archive.ts b/packages/app/src/workspace/project-workspace-archive.ts index 243ca69b7..2de3b2447 100644 --- a/packages/app/src/workspace/project-workspace-archive.ts +++ b/packages/app/src/workspace/project-workspace-archive.ts @@ -24,7 +24,7 @@ export async function selectProjectWorkspacesToArchive( for (const workspace of workspaces) { if (workspace.workspaceKind === "worktree") { const shouldArchive = await confirmWorktreeArchive({ - worktreeName: workspace.name, + workspaceName: workspace.name, ...toWorktreeArchiveRisk(workspace), }); if (!shouldArchive) { diff --git a/packages/app/src/workspace/use-workspace-archive.ts b/packages/app/src/workspace/use-workspace-archive.ts index e9a7262ec..0d5c050be 100644 --- a/packages/app/src/workspace/use-workspace-archive.ts +++ b/packages/app/src/workspace/use-workspace-archive.ts @@ -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, diff --git a/packages/app/src/workspace/workspace-archive.test.ts b/packages/app/src/workspace/workspace-archive.test.ts index a8cfc3939..b4db600e6 100644 --- a/packages/app/src/workspace/workspace-archive.test.ts +++ b/packages/app/src/workspace/workspace-archive.test.ts @@ -144,23 +144,6 @@ describe("archiveWorkspaceOptimistically", () => { }), ).toBe(false); }); - - it("runs the after-hide hook after local state is hidden", async () => { - const archived = workspace(); - useSessionStore.getState().mergeWorkspaces(SERVER_ID, [archived]); - const client = createClient(vi.fn(async () => archivePayload({ workspaceId: archived.id }))); - const afterHide = vi.fn(() => { - expect(storedWorkspace(archived.id)).toBeUndefined(); - }); - - await archiveWorkspaceOptimistically({ - client, - workspace: target(), - afterHide, - }); - - expect(afterHide).toHaveBeenCalledOnce(); - }); }); describe("archiveWorkspacesOptimistically", () => { @@ -183,10 +166,7 @@ describe("archiveWorkspacesOptimistically", () => { const failures = await archiveWorkspacesOptimistically({ getClient: () => client, - workspaces: [ - target({ workspaceId: first.id, workspaceDirectory: first.workspaceDirectory }), - target({ workspaceId: second.id, workspaceDirectory: second.workspaceDirectory }), - ], + workspaces: [target({ workspaceId: first.id }), target({ workspaceId: second.id })], }); expect(failures).toHaveLength(1); @@ -219,12 +199,10 @@ describe("archiveWorkspacesOptimistically", () => { target({ serverId: SERVER_ID, workspaceId: first.id, - workspaceDirectory: first.workspaceDirectory, }), target({ serverId: SECOND_SERVER_ID, workspaceId: second.id, - workspaceDirectory: second.workspaceDirectory, }), ], }); diff --git a/packages/app/src/workspace/workspace-archive.ts b/packages/app/src/workspace/workspace-archive.ts index 1930a8464..0bb4bcf5f 100644 --- a/packages/app/src/workspace/workspace-archive.ts +++ b/packages/app/src/workspace/workspace-archive.ts @@ -9,7 +9,6 @@ import { i18n } from "@/i18n/i18next"; export interface WorkspaceArchiveTarget { serverId: string; workspaceId: string; - workspaceDirectory?: string | null; } interface WorkspaceArchiveClient { @@ -50,7 +49,6 @@ function hideWorkspaceOptimistically( markWorkspaceArchivePending({ serverId: workspace.serverId, workspaceId: workspace.workspaceId, - workspaceDirectory: workspace.workspaceDirectory, }); useSessionStore.getState().removeWorkspace(workspace.serverId, workspace.workspaceId); return { workspace: snapshot }; @@ -83,10 +81,8 @@ async function archiveWorkspaceOrThrow(input: { export async function archiveWorkspaceOptimistically(input: { client: WorkspaceArchiveClient; workspace: WorkspaceArchiveTarget; - afterHide?: () => void; }): Promise { const snapshot = hideWorkspaceOptimistically(input.workspace); - input.afterHide?.(); try { await archiveWorkspaceOrThrow({