Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -93,7 +94,6 @@ export function WorkspaceListItem({
);
const utils = electronTrpc.useUtils();

// Derive isActive from route
const isActive = !!matchRoute({
to: "/workspace/$workspaceId",
params: { workspaceId: id },
Expand All @@ -118,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 },
Expand All @@ -132,46 +131,43 @@ 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,
workspaceBranch: branch,
workspaceId: id,
});

// Calculate total local changes (staged + unstaged + untracked)
// Prefer againstBase (committed diff vs base branch) over uncommitted changes only
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;
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(
workspaceTabs.flatMap((t) => extractPaneIdsFromLayout(t.layout)),
);
}, [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;
Expand Down Expand Up @@ -214,7 +210,6 @@ export function WorkspaceListItem({
}
};

// Drag and drop
const [{ isDragging }, drag] = useDrag(
() => ({
type: WORKSPACE_TYPE,
Expand Down Expand Up @@ -291,18 +286,15 @@ 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)
? { additions: pr.additions, deletions: pr.deletions }
: 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 = (
<button
Expand Down Expand Up @@ -337,13 +329,13 @@ export function WorkspaceListItem({
strokeWidth={STROKE_WIDTH}
/>
)}
{/* Status indicator - only show for non-working statuses */}
{/* Status indicator */}
{workspaceStatus && workspaceStatus !== "working" && (
<span className="absolute top-1 right-1">
<StatusIndicator status={workspaceStatus} />
</span>
)}
{/* Unread dot (only when no status) */}
{/* Unread dot */}
{isUnread && !workspaceStatus && (
<span className="absolute top-1 right-1 flex size-2">
<span className="relative inline-flex size-2 rounded-full bg-blue-500" />
Expand Down Expand Up @@ -433,7 +425,7 @@ export function WorkspaceListItem({
)}
style={{ cursor: isDragging ? "grabbing" : "pointer" }}
>
{/* Active indicator - left border */}
{/* Active indicator */}
{isActive && (
<div className="absolute left-0 top-0 bottom-0 w-0.5 bg-primary rounded-r" />
)}
Expand Down Expand Up @@ -537,7 +529,7 @@ export function WorkspaceListItem({
</span>
)}

{/* Diff stats (transforms to X on hover) or close button for worktree workspaces */}
{/* Diff stats / close button */}
{!isBranchWorkspace &&
(showDiffStats && diffStats ? (
<WorkspaceDiffStats
Expand Down Expand Up @@ -571,7 +563,7 @@ export function WorkspaceListItem({
))}
</div>

{/* Row 2: Git info (branch + PR badge) */}
{/* Branch + PR badge */}
{(showBranchSubtitle || pr) && (
<div className="flex items-center gap-2 text-[11px] w-full">
{showBranchSubtitle && (
Expand Down Expand Up @@ -611,7 +603,6 @@ export function WorkspaceListItem({
</ContextMenuItem>
);

// Wrap with context menu and hover card
if (isBranchWorkspace) {
return (
<>
Expand Down
Original file line number Diff line number Diff line change
@@ -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() {
Expand All @@ -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 (
Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src/renderer/screens/main/hooks/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { useBranchSyncInvalidation } from "./useBranchSyncInvalidation";
export { useGitChangesStatus } from "./useGitChangesStatus";
export { usePRStatus } from "./usePRStatus";
export { useWorkspaceRename } from "./useWorkspaceRename";
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { useGitChangesStatus } from "./useGitChangesStatus";
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
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 };
}
Loading