From 454ea11f655d3b928a09d7391f9da2a365656238 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Sat, 14 Feb 2026 01:51:27 -0800 Subject: [PATCH 1/2] fix(desktop): show diffs against base branch in sidebar workspace list MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sidebar was only counting staged/unstaged/untracked files for diff stats, ignoring the againstBase comparison entirely. This meant workspaces with committed changes ahead of main but no uncommitted changes showed no diff stats. Extract useGitChangesStatus hook to share the base-branch resolution logic (store override → detected default → "main" fallback) between ChangesContent and WorkspaceListItem. The sidebar now correctly shows the full diff against the base branch. --- .../WorkspaceListItem/WorkspaceListItem.tsx | 28 ++++++------ .../ChangesContent/ChangesContent.tsx | 24 +++------- .../src/renderer/screens/main/hooks/index.ts | 1 + .../main/hooks/useGitChangesStatus/index.ts | 1 + .../useGitChangesStatus.ts | 44 +++++++++++++++++++ 5 files changed, 67 insertions(+), 31 deletions(-) create mode 100644 apps/desktop/src/renderer/screens/main/hooks/useGitChangesStatus/index.ts create mode 100644 apps/desktop/src/renderer/screens/main/hooks/useGitChangesStatus/useGitChangesStatus.ts 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 fd3650a2779..52450b666f8 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 @@ -37,6 +37,7 @@ import { navigateToWorkspace } from "renderer/routes/_authenticated/_dashboard/u 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 { useGitChangesStatus } from "renderer/screens/main/hooks/useGitChangesStatus"; import { useWorkspaceRename } from "renderer/screens/main/hooks/useWorkspaceRename"; import { useTabsStore } from "renderer/stores/tabs/store"; import { extractPaneIdsFromLayout } from "renderer/stores/tabs/utils"; @@ -133,13 +134,11 @@ export function WorkspaceListItem({ ); // Lazy-load local git changes on hover - const { data: localChanges } = electronTrpc.changes.getStatus.useQuery( - { worktreePath }, - { - enabled: hasHovered && type === "worktree" && !!worktreePath, - staleTime: GITHUB_STATUS_STALE_TIME, - }, - ); + const { status: localChanges } = useGitChangesStatus({ + worktreePath, + enabled: hasHovered && type === "worktree", + staleTime: GITHUB_STATUS_STALE_TIME, + }); useBranchSyncInvalidation({ gitBranch: localChanges?.branch, @@ -147,14 +146,17 @@ export function WorkspaceListItem({ workspaceId: id, }); - // Calculate total local changes (staged + unstaged + untracked) + // Calculate diff stats against base branch, falling back to local uncommitted changes const localDiffStats = useMemo(() => { if (!localChanges) return null; - const allFiles = [ - ...localChanges.staged, - ...localChanges.unstaged, - ...localChanges.untracked, - ]; + const allFiles = + localChanges.againstBase.length > 0 + ? localChanges.againstBase + : [ + ...localChanges.staged, + ...localChanges.unstaged, + ...localChanges.untracked, + ]; const additions = allFiles.reduce((sum, f) => sum + (f.additions || 0), 0); const deletions = allFiles.reduce((sum, f) => sum + (f.deletions || 0), 0); if (additions === 0 && deletions === 0) return null; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ChangesContent/ChangesContent.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ChangesContent/ChangesContent.tsx index a412d45d296..ad11d5d024e 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ChangesContent/ChangesContent.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceView/ChangesContent/ChangesContent.tsx @@ -1,6 +1,6 @@ import { useParams } from "@tanstack/react-router"; import { electronTrpc } from "renderer/lib/electron-trpc"; -import { useChangesStore } from "renderer/stores/changes"; +import { useGitChangesStatus } from "renderer/screens/main/hooks/useGitChangesStatus"; import { InfiniteScrollView } from "./components/InfiniteScrollView"; export function ChangesContent() { @@ -11,23 +11,11 @@ export function ChangesContent() { ); const worktreePath = workspace?.worktreePath; - const { getBaseBranch } = useChangesStore(); - const baseBranch = getBaseBranch(worktreePath || ""); - const { data: branchData } = electronTrpc.changes.getBranches.useQuery( - { worktreePath: worktreePath || "" }, - { enabled: !!worktreePath }, - ); - - const effectiveBaseBranch = baseBranch ?? branchData?.defaultBranch ?? "main"; - - const { data: status, isLoading } = electronTrpc.changes.getStatus.useQuery( - { worktreePath: worktreePath || "", defaultBranch: effectiveBaseBranch }, - { - enabled: !!worktreePath, - refetchInterval: 2500, - refetchOnWindowFocus: true, - }, - ); + const { status, isLoading, effectiveBaseBranch } = useGitChangesStatus({ + worktreePath, + refetchInterval: 2500, + refetchOnWindowFocus: true, + }); if (!worktreePath) { return ( diff --git a/apps/desktop/src/renderer/screens/main/hooks/index.ts b/apps/desktop/src/renderer/screens/main/hooks/index.ts index b6ab6ddd54e..ec36673d68e 100644 --- a/apps/desktop/src/renderer/screens/main/hooks/index.ts +++ b/apps/desktop/src/renderer/screens/main/hooks/index.ts @@ -1,3 +1,4 @@ export { useBranchSyncInvalidation } from "./useBranchSyncInvalidation"; +export { useGitChangesStatus } from "./useGitChangesStatus"; export { usePRStatus } from "./usePRStatus"; export { useWorkspaceRename } from "./useWorkspaceRename"; diff --git a/apps/desktop/src/renderer/screens/main/hooks/useGitChangesStatus/index.ts b/apps/desktop/src/renderer/screens/main/hooks/useGitChangesStatus/index.ts new file mode 100644 index 00000000000..6512ff9a62c --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/hooks/useGitChangesStatus/index.ts @@ -0,0 +1 @@ +export { useGitChangesStatus } from "./useGitChangesStatus"; diff --git a/apps/desktop/src/renderer/screens/main/hooks/useGitChangesStatus/useGitChangesStatus.ts b/apps/desktop/src/renderer/screens/main/hooks/useGitChangesStatus/useGitChangesStatus.ts new file mode 100644 index 00000000000..3be0fa9db9e --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/hooks/useGitChangesStatus/useGitChangesStatus.ts @@ -0,0 +1,44 @@ +import { electronTrpc } from "renderer/lib/electron-trpc"; +import { useChangesStore } from "renderer/stores/changes"; + +interface UseGitChangesStatusOptions { + worktreePath: string | undefined; + enabled?: boolean; + refetchInterval?: number; + refetchOnWindowFocus?: boolean; + staleTime?: number; +} + +export function useGitChangesStatus({ + worktreePath, + enabled = true, + refetchInterval, + refetchOnWindowFocus, + staleTime, +}: UseGitChangesStatusOptions) { + const { getBaseBranch } = useChangesStore(); + const baseBranch = getBaseBranch(worktreePath || ""); + + const { data: branchData } = electronTrpc.changes.getBranches.useQuery( + { worktreePath: worktreePath || "" }, + { enabled: enabled && !!worktreePath }, + ); + + const effectiveBaseBranch = baseBranch ?? branchData?.defaultBranch ?? "main"; + + const { data: status, isLoading } = + electronTrpc.changes.getStatus.useQuery( + { + worktreePath: worktreePath || "", + defaultBranch: effectiveBaseBranch, + }, + { + enabled: enabled && !!worktreePath, + refetchInterval, + refetchOnWindowFocus, + staleTime, + }, + ); + + return { status, isLoading, effectiveBaseBranch }; +} From 8efcc1e09974d93b90fabc408a2e15334e2585fb Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Sat, 14 Feb 2026 01:56:16 -0800 Subject: [PATCH 2/2] chore(desktop): clean up comments in WorkspaceListItem --- .../WorkspaceListItem/WorkspaceListItem.tsx | 25 ++++++------------- .../useGitChangesStatus.ts | 25 +++++++++---------- 2 files changed, 19 insertions(+), 31 deletions(-) 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 52450b666f8..ee0e04438de 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 @@ -94,7 +94,6 @@ export function WorkspaceListItem({ ); const utils = electronTrpc.useUtils(); - // Derive isActive from route const isActive = !!matchRoute({ to: "/workspace/$workspaceId", params: { workspaceId: id }, @@ -119,11 +118,10 @@ export function WorkspaceListItem({ toast.error(`Failed to update unread status: ${error.message}`), }); - // Shared delete logic const { showDeleteDialog, setShowDeleteDialog, handleDeleteClick } = useWorkspaceDeleteHandler(); - // Lazy-load GitHub status on hover to avoid N+1 queries + // Lazy-load on hover to avoid N+1 queries for every sidebar item const { data: githubStatus } = electronTrpc.workspaces.getGitHubStatus.useQuery( { workspaceId: id }, @@ -133,7 +131,6 @@ export function WorkspaceListItem({ }, ); - // Lazy-load local git changes on hover const { status: localChanges } = useGitChangesStatus({ worktreePath, enabled: hasHovered && type === "worktree", @@ -146,7 +143,7 @@ export function WorkspaceListItem({ workspaceId: id, }); - // Calculate diff stats against base branch, falling back to local uncommitted changes + // Prefer againstBase (committed diff vs base branch) over uncommitted changes only const localDiffStats = useMemo(() => { if (!localChanges) return null; const allFiles = @@ -163,7 +160,6 @@ export function WorkspaceListItem({ return { additions, deletions }; }, [localChanges]); - // Memoize workspace pane IDs to avoid recalculating on every render const workspacePaneIds = useMemo(() => { const workspaceTabs = tabs.filter((t) => t.workspaceId === id); return new Set( @@ -171,9 +167,7 @@ export function WorkspaceListItem({ ); }, [tabs, id]); - // Compute aggregate status for workspace using shared priority logic const workspaceStatus = useMemo(() => { - // Generator avoids array allocation function* paneStatuses() { for (const paneId of workspacePaneIds) { yield panes[paneId]?.status; @@ -216,7 +210,6 @@ export function WorkspaceListItem({ } }; - // Drag and drop const [{ isDragging }, drag] = useDrag( () => ({ type: WORKSPACE_TYPE, @@ -293,7 +286,6 @@ export function WorkspaceListItem({ }); const pr = githubStatus?.pr; - // Show diff stats from PR if available, otherwise from local changes const diffStats = localDiffStats || (pr && (pr.additions > 0 || pr.deletions > 0) @@ -301,10 +293,8 @@ export function WorkspaceListItem({ : null); const showDiffStats = !!diffStats; - // Determine if we should show the branch subtitle const showBranchSubtitle = isBranchWorkspace || (!!name && name !== branch); - // Collapsed sidebar: show just the icon with hover card (worktree) or tooltip (branch) if (isCollapsed) { const collapsedButton = (