From 538c9910596fed8dfc4849063b246c9b08e6b317 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Sat, 17 Jan 2026 14:19:54 -0800 Subject: [PATCH 1/4] fix(desktop): poll PR status for all open workspaces Previously, GitHub PR status was only fetched when hovering over a workspace or for the active workspace. This adds background polling for all open worktree workspaces so PR badges update automatically. The polling is designed to be completely non-blocking with silent failures and deferred initial fetches. --- .../WorkspaceSidebar/WorkspaceSidebar.tsx | 4 ++ .../screens/main/hooks/usePRStatus/index.ts | 1 + .../hooks/usePRStatus/usePRStatusPolling.ts | 53 +++++++++++++++++++ 3 files changed, 58 insertions(+) create mode 100644 apps/desktop/src/renderer/screens/main/hooks/usePRStatus/usePRStatusPolling.ts diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebar.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebar.tsx index abec0753976..31d41673a82 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebar.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebar.tsx @@ -1,5 +1,6 @@ import { useMemo } from "react"; import { useWorkspaceShortcuts } from "renderer/hooks/useWorkspaceShortcuts"; +import { usePRStatusPolling } from "renderer/screens/main/hooks/usePRStatus"; import { PortsList } from "./PortsList"; import { ProjectSection } from "./ProjectSection"; import { WorkspaceSidebarFooter } from "./WorkspaceSidebarFooter"; @@ -14,6 +15,9 @@ export function WorkspaceSidebar({ }: WorkspaceSidebarProps) { const { groups } = useWorkspaceShortcuts(); + // Poll GitHub PR status for all open worktree workspaces in the background + usePRStatusPolling(); + // Calculate shortcut base indices for each project group using cumulative offsets const projectShortcutIndices = useMemo( () => diff --git a/apps/desktop/src/renderer/screens/main/hooks/usePRStatus/index.ts b/apps/desktop/src/renderer/screens/main/hooks/usePRStatus/index.ts index f552343f7ed..3d6b146c958 100644 --- a/apps/desktop/src/renderer/screens/main/hooks/usePRStatus/index.ts +++ b/apps/desktop/src/renderer/screens/main/hooks/usePRStatus/index.ts @@ -1 +1,2 @@ export { usePRStatus } from "./usePRStatus"; +export { usePRStatusPolling } from "./usePRStatusPolling"; diff --git a/apps/desktop/src/renderer/screens/main/hooks/usePRStatus/usePRStatusPolling.ts b/apps/desktop/src/renderer/screens/main/hooks/usePRStatus/usePRStatusPolling.ts new file mode 100644 index 00000000000..3974667ef03 --- /dev/null +++ b/apps/desktop/src/renderer/screens/main/hooks/usePRStatus/usePRStatusPolling.ts @@ -0,0 +1,53 @@ +import { electronTrpc } from "renderer/lib/electron-trpc"; + +/** Polling interval for background PR status updates (30 seconds) */ +const PR_POLLING_INTERVAL_MS = 30_000; + +/** Stale time to prevent unnecessary refetches (25 seconds) */ +const STALE_TIME_MS = 25_000; + +/** + * Background hook that polls GitHub PR status for all open worktree workspaces. + * This enables PR status badges to update automatically across all workspaces, + * not just the active one. + * + * Designed to be completely non-blocking: + * - Queries run in background with no suspense + * - Failures are silently ignored (retries disabled) + * - Initial fetch is deferred to avoid blocking render + * + * Should be called once at the app level (e.g., in WorkspaceSidebar). + */ +export function usePRStatusPolling() { + const { data: groups = [] } = + electronTrpc.workspaces.getAllGrouped.useQuery(); + + // Get all worktree workspace IDs (branch workspaces don't have PRs) + const worktreeWorkspaceIds = groups + .flatMap((group) => group.workspaces) + .filter((workspace) => workspace.type === "worktree") + .map((workspace) => workspace.id); + + // Poll GitHub status for each worktree workspace (non-blocking) + electronTrpc.useQueries((t) => + worktreeWorkspaceIds.map((workspaceId) => + t.workspaces.getGitHubStatus( + { workspaceId }, + { + // Polling configuration + refetchInterval: PR_POLLING_INTERVAL_MS, + refetchOnWindowFocus: false, + refetchOnMount: false, + refetchOnReconnect: false, + + // Non-blocking configuration + staleTime: STALE_TIME_MS, + retry: false, // Don't retry failures - silent fail + + // Use cached data while fetching + placeholderData: (previousData) => previousData, + }, + ), + ), + ); +} From b49e42eb658f0678dcf64bae4d5d951ff3d2cb01 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Sat, 17 Jan 2026 16:31:37 -0800 Subject: [PATCH 2/4] chore: clean up comments --- .../WorkspaceSidebar/WorkspaceSidebar.tsx | 2 -- .../hooks/usePRStatus/usePRStatusPolling.ts | 25 +++---------------- 2 files changed, 4 insertions(+), 23 deletions(-) diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebar.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebar.tsx index 31d41673a82..07d595388ba 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebar.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceSidebar.tsx @@ -14,8 +14,6 @@ export function WorkspaceSidebar({ isCollapsed = false, }: WorkspaceSidebarProps) { const { groups } = useWorkspaceShortcuts(); - - // Poll GitHub PR status for all open worktree workspaces in the background usePRStatusPolling(); // Calculate shortcut base indices for each project group using cumulative offsets diff --git a/apps/desktop/src/renderer/screens/main/hooks/usePRStatus/usePRStatusPolling.ts b/apps/desktop/src/renderer/screens/main/hooks/usePRStatus/usePRStatusPolling.ts index 3974667ef03..14a4ba674c6 100644 --- a/apps/desktop/src/renderer/screens/main/hooks/usePRStatus/usePRStatusPolling.ts +++ b/apps/desktop/src/renderer/screens/main/hooks/usePRStatus/usePRStatusPolling.ts @@ -1,50 +1,33 @@ import { electronTrpc } from "renderer/lib/electron-trpc"; -/** Polling interval for background PR status updates (30 seconds) */ const PR_POLLING_INTERVAL_MS = 30_000; - -/** Stale time to prevent unnecessary refetches (25 seconds) */ const STALE_TIME_MS = 25_000; /** - * Background hook that polls GitHub PR status for all open worktree workspaces. - * This enables PR status badges to update automatically across all workspaces, - * not just the active one. - * - * Designed to be completely non-blocking: - * - Queries run in background with no suspense - * - Failures are silently ignored (retries disabled) - * - Initial fetch is deferred to avoid blocking render - * - * Should be called once at the app level (e.g., in WorkspaceSidebar). + * Polls GitHub PR status for all open worktree workspaces. + * Must be non-blocking to avoid degrading sidebar responsiveness. */ export function usePRStatusPolling() { const { data: groups = [] } = electronTrpc.workspaces.getAllGrouped.useQuery(); - // Get all worktree workspace IDs (branch workspaces don't have PRs) + // Branch workspaces don't have PRs const worktreeWorkspaceIds = groups .flatMap((group) => group.workspaces) .filter((workspace) => workspace.type === "worktree") .map((workspace) => workspace.id); - // Poll GitHub status for each worktree workspace (non-blocking) electronTrpc.useQueries((t) => worktreeWorkspaceIds.map((workspaceId) => t.workspaces.getGitHubStatus( { workspaceId }, { - // Polling configuration refetchInterval: PR_POLLING_INTERVAL_MS, refetchOnWindowFocus: false, refetchOnMount: false, refetchOnReconnect: false, - - // Non-blocking configuration staleTime: STALE_TIME_MS, - retry: false, // Don't retry failures - silent fail - - // Use cached data while fetching + retry: false, placeholderData: (previousData) => previousData, }, ), From da0f4ad71cefe706e8b051611941f54f882167fd Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Sat, 17 Jan 2026 16:33:46 -0800 Subject: [PATCH 3/4] refactor: consolidate PR status queries to use background polling cache --- .../WorkspaceListItem/WorkspaceListItem.tsx | 15 +++------------ .../Sidebar/ChangesView/ChangesView.tsx | 6 ++---- 2 files changed, 5 insertions(+), 16 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 0c107874352..638d429d20d 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 @@ -16,7 +16,7 @@ import { toast } from "@superset/ui/sonner"; import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; import { cn } from "@superset/ui/utils"; import { useMatchRoute, useNavigate } from "@tanstack/react-router"; -import { useMemo, useState } from "react"; +import { useMemo } from "react"; import { useDrag, useDrop } from "react-dnd"; import { HiMiniXMark } from "react-icons/hi2"; import { LuEye, LuEyeOff, LuFolder, LuFolderGit2 } from "react-icons/lu"; @@ -79,7 +79,6 @@ export function WorkspaceListItem({ const navigate = useNavigate(); const matchRoute = useMatchRoute(); const reorderWorkspaces = useReorderWorkspaces(); - const [hasHovered, setHasHovered] = useState(false); const rename = useWorkspaceRename(id, name); const tabs = useTabsStore((s) => s.tabs); const panes = useTabsStore((s) => s.panes); @@ -108,12 +107,12 @@ export function WorkspaceListItem({ const { showDeleteDialog, setShowDeleteDialog, handleDeleteClick } = useWorkspaceDeleteHandler(); - // Lazy-load GitHub status on hover to avoid N+1 queries + // Reads from cache populated by usePRStatusPolling const { data: githubStatus } = electronTrpc.workspaces.getGitHubStatus.useQuery( { workspaceId: id }, { - enabled: hasHovered && type === "worktree", + enabled: type === "worktree", staleTime: GITHUB_STATUS_STALE_TIME, }, ); @@ -144,12 +143,6 @@ export function WorkspaceListItem({ } }; - const handleMouseEnter = () => { - if (!hasHovered) { - setHasHovered(true); - } - }; - const handleOpenInFinder = () => { if (worktreePath) { openInFinder.mutate(worktreePath); @@ -205,7 +198,6 @@ export function WorkspaceListItem({