diff --git a/apps/desktop/src/lib/trpc/routers/changes/status.ts b/apps/desktop/src/lib/trpc/routers/changes/status.ts index 43334e8ae03..cc4b1fea638 100644 --- a/apps/desktop/src/lib/trpc/routers/changes/status.ts +++ b/apps/desktop/src/lib/trpc/routers/changes/status.ts @@ -1,3 +1,6 @@ +import { projects, workspaces, worktrees } from "@superset/local-db"; +import { and, eq, isNull, not } from "drizzle-orm"; +import { localDb } from "main/lib/local-db"; import type { ChangedFile, GitChangesStatus } from "shared/changes-types"; import simpleGit from "simple-git"; import { z } from "zod"; @@ -30,6 +33,10 @@ export const createStatusRouter = () => { // Use --no-optional-locks to avoid holding locks on the repository const status = await getStatusNoLock(input.worktreePath); const parsed = parseGitStatus(status); + syncWorkspaceBranch({ + worktreePath: input.worktreePath, + currentBranch: parsed.branch, + }); // Run independent operations in parallel const [branchComparison, trackingStatus] = await Promise.all([ @@ -94,6 +101,81 @@ export const createStatusRouter = () => { }); }; +/** + * Update local DB branch fields to match the current git branch for a worktree + * or main repo workspace path. + */ +function syncWorkspaceBranch({ + worktreePath, + currentBranch, +}: { + worktreePath: string; + currentBranch: string; +}): void { + if (!currentBranch || currentBranch === "HEAD") { + return; + } + + try { + const worktree = localDb + .select({ id: worktrees.id }) + .from(worktrees) + .where(eq(worktrees.path, worktreePath)) + .get(); + + if (worktree) { + localDb + .update(worktrees) + .set({ branch: currentBranch }) + .where( + and( + eq(worktrees.id, worktree.id), + not(eq(worktrees.branch, currentBranch)), + ), + ) + .run(); + + localDb + .update(workspaces) + .set({ branch: currentBranch }) + .where( + and( + eq(workspaces.worktreeId, worktree.id), + isNull(workspaces.deletingAt), + not(eq(workspaces.branch, currentBranch)), + ), + ) + .run(); + + return; + } + + const project = localDb + .select({ id: projects.id }) + .from(projects) + .where(eq(projects.mainRepoPath, worktreePath)) + .get(); + if (!project) { + return; + } + + localDb + .update(workspaces) + .set({ branch: currentBranch }) + .where( + and( + eq(workspaces.projectId, project.id), + eq(workspaces.type, "branch"), + isNull(workspaces.deletingAt), + not(eq(workspaces.branch, currentBranch)), + ), + ) + .run(); + } catch (error) { + console.warn("[changes/status] Failed to sync branch:", error); + } +} + interface BranchComparison { commits: GitChangesStatus["commits"]; againstBase: ChangedFile[]; diff --git a/apps/desktop/src/lib/trpc/routers/workspaces/procedures/git-status.ts b/apps/desktop/src/lib/trpc/routers/workspaces/procedures/git-status.ts index c17292d293b..1f978cb5200 100644 --- a/apps/desktop/src/lib/trpc/routers/workspaces/procedures/git-status.ts +++ b/apps/desktop/src/lib/trpc/routers/workspaces/procedures/git-status.ts @@ -131,9 +131,11 @@ export const createGitStatusProcedures = () => { // Extract worktree name from path (last segment) const worktreeName = worktree.path.split("/").pop() ?? worktree.branch; + const branchName = worktree.branch; return { worktreeName, + branchName, createdAt: worktree.createdAt, gitStatus: worktree.gitStatus ?? null, githubStatus: worktree.githubStatus ?? null, diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/WorkspaceListItem.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/WorkspaceListItem.tsx index 9b34144a33c..83d53e9cf29 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/WorkspaceListItem.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/WorkspaceListItem.tsx @@ -36,6 +36,7 @@ import { import { navigateToWorkspace } from "renderer/routes/_authenticated/_dashboard/utils/workspace-navigation"; import { AsciiSpinner } from "renderer/screens/main/components/AsciiSpinner"; import { StatusIndicator } from "renderer/screens/main/components/StatusIndicator"; +import { useBranchSyncInvalidation } from "renderer/screens/main/hooks/useBranchSyncInvalidation"; import { useWorkspaceRename } from "renderer/screens/main/hooks/useWorkspaceRename"; import { useTabsStore } from "renderer/stores/tabs/store"; import { extractPaneIdsFromLayout } from "renderer/stores/tabs/utils"; @@ -136,6 +137,12 @@ export function WorkspaceListItem({ }, ); + useBranchSyncInvalidation({ + gitBranch: localChanges?.branch, + workspaceBranch: branch, + workspaceId: id, + }); + // Calculate total local changes (staged + unstaged + untracked) const localDiffStats = useMemo(() => { if (!localChanges) return null; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/components/WorkspaceHoverCard/WorkspaceHoverCard.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/components/WorkspaceHoverCard/WorkspaceHoverCard.tsx index 76942c2accc..aee13dfe0c2 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/components/WorkspaceHoverCard/WorkspaceHoverCard.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/components/WorkspaceHoverCard/WorkspaceHoverCard.tsx @@ -39,6 +39,7 @@ export function WorkspaceHoverCardContent({ const needsRebase = worktreeInfo?.gitStatus?.needsRebase; const worktreeName = worktreeInfo?.worktreeName; + const branchName = worktreeInfo?.branchName; const hasCustomAlias = workspaceAlias && worktreeName && workspaceAlias !== worktreeName; @@ -49,19 +50,19 @@ export function WorkspaceHoverCardContent({ {hasCustomAlias && (
{workspaceAlias}
)} - {worktreeName && ( + {branchName && (
Branch {repoUrl && branchExistsOnRemote ? ( - {worktreeName} + {branchName} - {worktreeName} + {branchName} )}
diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/ChangesView.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/ChangesView.tsx index 2d997692dbf..7526e8b0d7a 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/ChangesView.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/RightSidebar/ChangesView/ChangesView.tsx @@ -14,6 +14,7 @@ import { useEffect, useMemo, useState } from "react"; import { HiMiniMinus, HiMiniPlus } from "react-icons/hi2"; import { LuUndo2 } from "react-icons/lu"; import { electronTrpc } from "renderer/lib/electron-trpc"; +import { useBranchSyncInvalidation } from "renderer/screens/main/hooks/useBranchSyncInvalidation"; import { useChangesStore } from "renderer/stores/changes"; import type { ChangeCategory, ChangedFile } from "shared/changes-types"; import { CategorySection } from "./components/CategorySection"; @@ -69,6 +70,12 @@ export function ChangesView({ onFileOpen, isExpandedView }: ChangesViewProps) { }, ); + useBranchSyncInvalidation({ + gitBranch: status?.branch, + workspaceBranch: workspace?.branch, + workspaceId: workspaceId ?? "", + }); + const handleRefresh = () => { refetch(); refetchGithubStatus(); diff --git a/apps/desktop/src/renderer/screens/main/hooks/index.ts b/apps/desktop/src/renderer/screens/main/hooks/index.ts index 8b8a83fbc13..b6ab6ddd54e 100644 --- a/apps/desktop/src/renderer/screens/main/hooks/index.ts +++ b/apps/desktop/src/renderer/screens/main/hooks/index.ts @@ -1,2 +1,3 @@ +export { useBranchSyncInvalidation } from "./useBranchSyncInvalidation"; export { usePRStatus } from "./usePRStatus"; export { useWorkspaceRename } from "./useWorkspaceRename"; diff --git a/apps/desktop/src/renderer/screens/main/hooks/useBranchSyncInvalidation/index.ts b/apps/desktop/src/renderer/screens/main/hooks/useBranchSyncInvalidation/index.ts new file mode 100644 index 00000000000..c96f630a162 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/hooks/useBranchSyncInvalidation/index.ts @@ -0,0 +1 @@ +export { useBranchSyncInvalidation } from "./useBranchSyncInvalidation"; diff --git a/apps/desktop/src/renderer/screens/main/hooks/useBranchSyncInvalidation/useBranchSyncInvalidation.ts b/apps/desktop/src/renderer/screens/main/hooks/useBranchSyncInvalidation/useBranchSyncInvalidation.ts new file mode 100644 index 00000000000..6f8425c92c6 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/hooks/useBranchSyncInvalidation/useBranchSyncInvalidation.ts @@ -0,0 +1,29 @@ +import { useEffect } from "react"; +import { electronTrpc } from "renderer/lib/electron-trpc"; + +/** + * Invalidates workspace-related caches when the git branch (from status polling) + * diverges from the branch stored in the local DB workspace record. + * + * This keeps sidebar labels and hover cards in sync after external `git switch`. + */ +export function useBranchSyncInvalidation({ + gitBranch, + workspaceBranch, + workspaceId, +}: { + gitBranch: string | undefined; + workspaceBranch: string | undefined; + workspaceId: string; +}) { + const utils = electronTrpc.useUtils(); + + useEffect(() => { + if (!gitBranch || gitBranch === "HEAD" || !workspaceBranch) return; + if (gitBranch !== workspaceBranch) { + utils.workspaces.getAllGrouped.invalidate(); + utils.workspaces.get.invalidate({ id: workspaceId }); + utils.workspaces.getWorktreeInfo.invalidate({ workspaceId }); + } + }, [gitBranch, workspaceBranch, workspaceId, utils]); +}