diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/V2OpenInMenuButton/V2OpenInMenuButton.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/V2OpenInMenuButton/V2OpenInMenuButton.tsx index 46984021756..a539626ad3d 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/V2OpenInMenuButton/V2OpenInMenuButton.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/V2OpenInMenuButton/V2OpenInMenuButton.tsx @@ -18,16 +18,15 @@ import { HotkeyLabel, useHotkey, useHotkeyDisplay } from "renderer/hotkeys"; import { electronTrpc } from "renderer/lib/electron-trpc"; import { useV2ProjectDefaultApp } from "renderer/routes/_authenticated/hooks/useV2ProjectDefaultApp"; import { useThemeStore } from "renderer/stores"; +import { getV2WorktreeDisplayName } from "./utils/getV2WorktreeDisplayName"; interface V2OpenInMenuButtonProps { worktreePath: string; - branch: string; projectId: string; } export function V2OpenInMenuButton({ worktreePath, - branch, projectId, }: V2OpenInMenuButtonProps) { const activeTheme = useThemeStore((state) => state.activeTheme); @@ -57,6 +56,10 @@ export function V2OpenInMenuButton({ const showCopyPathShortcut = copyPathDisplay.text !== "Unassigned"; const isLoading = openInApp.isPending || copyPath.isPending; const isDark = activeTheme?.type === "dark"; + const displayName = useMemo( + () => getV2WorktreeDisplayName(worktreePath, projectId), + [worktreePath, projectId], + ); const handleOpenInEditor = useCallback(() => { if (openInApp.isPending || copyPath.isPending) return; @@ -107,9 +110,9 @@ export function V2OpenInMenuButton({ className="size-3.5 object-contain shrink-0" /> )} - {branch && ( + {displayName && ( - /{branch} + /{displayName} )} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/V2OpenInMenuButton/utils/getV2WorktreeDisplayName/getV2WorktreeDisplayName.test.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/V2OpenInMenuButton/utils/getV2WorktreeDisplayName/getV2WorktreeDisplayName.test.ts new file mode 100644 index 00000000000..0ee7425f869 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/V2OpenInMenuButton/utils/getV2WorktreeDisplayName/getV2WorktreeDisplayName.test.ts @@ -0,0 +1,45 @@ +import { describe, expect, test } from "bun:test"; +import { getV2WorktreeDisplayName } from "./getV2WorktreeDisplayName"; + +describe("getV2WorktreeDisplayName", () => { + const projectId = "proj-abc"; + + test("returns the branch-at-creation directory name from a v2 worktree path", () => { + const path = `/home/user/.superset/worktrees/${projectId}/andrew/foo`; + expect(getV2WorktreeDisplayName(path, projectId)).toBe("andrew/foo"); + }); + + test("preserves nested slashed branch names", () => { + const path = `/home/user/.superset/worktrees/${projectId}/feat/users/list`; + expect(getV2WorktreeDisplayName(path, projectId)).toBe("feat/users/list"); + }); + + test("does NOT change with the workspace's current branch — the fix scenario from #3759", () => { + // Workspace was created from `andrew/foo`, so the directory is fixed there. + // After stacked PR navigation, `workspaces.branch` becomes `andrew/foo-2`. + // The displayed label must reflect the on-disk directory, not the live branch. + const persistedPath = `/home/user/.superset/worktrees/${projectId}/andrew/foo`; + const liveBranchAfterStackNavigation = "andrew/foo-2"; + + const display = getV2WorktreeDisplayName(persistedPath, projectId); + + expect(display).toBe("andrew/foo"); + expect(display).not.toBe(liveBranchAfterStackNavigation); + }); + + test("falls back to the basename if the projectId marker is absent", () => { + const path = "/some/other/location/my-branch"; + expect(getV2WorktreeDisplayName(path, projectId)).toBe("my-branch"); + }); + + test("handles Windows-style separators in the fallback", () => { + const path = "C:\\Users\\me\\some\\place\\my-branch"; + expect(getV2WorktreeDisplayName(path, projectId)).toBe("my-branch"); + }); + + test("returns the original string if no separator is present", () => { + expect(getV2WorktreeDisplayName("standalone", projectId)).toBe( + "standalone", + ); + }); +}); diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/V2OpenInMenuButton/utils/getV2WorktreeDisplayName/getV2WorktreeDisplayName.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/V2OpenInMenuButton/utils/getV2WorktreeDisplayName/getV2WorktreeDisplayName.ts new file mode 100644 index 00000000000..d0ace8fec63 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/V2OpenInMenuButton/utils/getV2WorktreeDisplayName/getV2WorktreeDisplayName.ts @@ -0,0 +1,20 @@ +// V2 worktrees live at `/.superset/worktrees//`. +// The directory name is fixed at create time; `workspaces.branch` drifts as HEAD +// moves (git status sync, AI rename, manual checkout). Derive the label from the +// persisted path so it always matches the on-disk directory. +export function getV2WorktreeDisplayName( + worktreePath: string, + projectId: string, +): string { + const marker = `worktrees/${projectId}/`; + const idx = worktreePath.indexOf(marker); + if (idx >= 0) { + const tail = worktreePath.slice(idx + marker.length); + if (tail.length > 0) return tail; + } + const lastSep = Math.max( + worktreePath.lastIndexOf("/"), + worktreePath.lastIndexOf("\\"), + ); + return lastSep >= 0 ? worktreePath.slice(lastSep + 1) : worktreePath; +} diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/V2OpenInMenuButton/utils/getV2WorktreeDisplayName/index.ts b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/V2OpenInMenuButton/utils/getV2WorktreeDisplayName/index.ts new file mode 100644 index 00000000000..2474f3aa930 --- /dev/null +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/V2OpenInMenuButton/utils/getV2WorktreeDisplayName/index.ts @@ -0,0 +1 @@ +export { getV2WorktreeDisplayName } from "./getV2WorktreeDisplayName"; diff --git a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/V2WorkspaceOpenInButton/V2WorkspaceOpenInButton.tsx b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/V2WorkspaceOpenInButton/V2WorkspaceOpenInButton.tsx index eab12edd4d6..b0b6fe61f96 100644 --- a/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/V2WorkspaceOpenInButton/V2WorkspaceOpenInButton.tsx +++ b/apps/desktop/src/renderer/routes/_authenticated/_dashboard/components/TopBar/components/V2WorkspaceOpenInButton/V2WorkspaceOpenInButton.tsx @@ -26,7 +26,6 @@ export function V2WorkspaceOpenInButton({ .where(({ workspaces }) => eq(workspaces.id, workspaceId)) .select(({ workspaces, hosts }) => ({ id: workspaces.id, - branch: workspaces.branch, projectId: workspaces.projectId, hostMachineId: hosts?.machineId ?? null, })), @@ -55,7 +54,6 @@ export function V2WorkspaceOpenInButton({ return (