From ab97c57cad998445c81206451318eca34e6a8f69 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 2 Apr 2026 16:16:29 -0700 Subject: [PATCH 1/7] Everything on 10s --- .../githubQueryPolicy.test.ts | 99 +++++-------------- .../githubQueryPolicy/githubQueryPolicy.ts | 56 ++--------- .../WorkspaceListItem/constants.ts | 2 +- .../RightSidebar/ChangesView/ChangesView.tsx | 1 - 4 files changed, 35 insertions(+), 123 deletions(-) diff --git a/apps/desktop/src/renderer/lib/githubQueryPolicy/githubQueryPolicy.test.ts b/apps/desktop/src/renderer/lib/githubQueryPolicy/githubQueryPolicy.test.ts index dca23c1f3e0..4474dcc7d2c 100644 --- a/apps/desktop/src/renderer/lib/githubQueryPolicy/githubQueryPolicy.test.ts +++ b/apps/desktop/src/renderer/lib/githubQueryPolicy/githubQueryPolicy.test.ts @@ -5,42 +5,33 @@ import { } from "./githubQueryPolicy"; describe("getGitHubStatusQueryPolicy", () => { - test("enables focus-only refresh for the active changes sidebar diffs view", () => { - expect( - getGitHubStatusQueryPolicy("changes-sidebar", { - hasWorkspaceId: true, - isActive: true, - isReviewTabActive: false, - }), - ).toEqual({ - enabled: true, - refetchInterval: false, - refetchOnWindowFocus: true, - staleTime: 0, - }); + test("polls every 10s for any active surface", () => { + for (const surface of [ + "changes-sidebar", + "workspace-page", + "workspace-list-item", + "workspace-hover-card", + "workspace-row", + ] as const) { + expect( + getGitHubStatusQueryPolicy(surface, { + hasWorkspaceId: true, + isActive: true, + }), + ).toEqual({ + enabled: true, + refetchInterval: 10_000, + refetchOnWindowFocus: true, + staleTime: 10_000, + }); + } }); - test("enables polling for the active changes sidebar review view", () => { - expect( - getGitHubStatusQueryPolicy("changes-sidebar", { - hasWorkspaceId: true, - isActive: true, - isReviewTabActive: true, - }), - ).toEqual({ - enabled: true, - refetchInterval: 10_000, - refetchOnWindowFocus: true, - staleTime: 10_000, - }); - }); - - test("disables changes sidebar status when the surface is inactive", () => { + test("disables polling when surface is inactive", () => { expect( getGitHubStatusQueryPolicy("changes-sidebar", { hasWorkspaceId: true, isActive: false, - isReviewTabActive: true, }), ).toEqual({ enabled: false, @@ -50,59 +41,17 @@ describe("getGitHubStatusQueryPolicy", () => { }); }); - test("keeps the workspace page active without interval polling", () => { + test("disables polling when workspace id is missing", () => { expect( getGitHubStatusQueryPolicy("workspace-page", { - hasWorkspaceId: true, + hasWorkspaceId: false, isActive: true, }), - ).toEqual({ - enabled: true, - refetchInterval: false, - refetchOnWindowFocus: false, - staleTime: 300_000, - }); - }); - - test("keeps hover-card surfaces lazy without focus refresh", () => { - expect( - getGitHubStatusQueryPolicy("workspace-hover-card", { - hasWorkspaceId: true, - isActive: true, - }), - ).toEqual({ - enabled: true, - refetchInterval: false, - refetchOnWindowFocus: false, - staleTime: 300_000, - }); - }); - - test("keeps workspace list items cheaper than full-page PR surfaces", () => { - expect( - getGitHubStatusQueryPolicy("workspace-list-item", { - hasWorkspaceId: true, - isActive: true, - }), - ).toEqual({ - enabled: true, - refetchInterval: false, - refetchOnWindowFocus: false, - staleTime: 30_000, - }); - }); - - test("disables passive hover surfaces when they are not visible", () => { - expect( - getGitHubStatusQueryPolicy("workspace-row", { - hasWorkspaceId: true, - isActive: false, - }), ).toEqual({ enabled: false, refetchInterval: false, refetchOnWindowFocus: false, - staleTime: 300_000, + staleTime: 10_000, }); }); }); diff --git a/apps/desktop/src/renderer/lib/githubQueryPolicy/githubQueryPolicy.ts b/apps/desktop/src/renderer/lib/githubQueryPolicy/githubQueryPolicy.ts index 6109e168286..3cc86ca7af1 100644 --- a/apps/desktop/src/renderer/lib/githubQueryPolicy/githubQueryPolicy.ts +++ b/apps/desktop/src/renderer/lib/githubQueryPolicy/githubQueryPolicy.ts @@ -1,7 +1,5 @@ -const ACTIVE_GITHUB_STATUS_STALE_TIME_MS = 10_000; -const ACTIVE_GITHUB_STATUS_REFETCH_INTERVAL_MS = 10_000; -const WORKSPACE_LIST_ITEM_GITHUB_STATUS_STALE_TIME_MS = 30_000; -const PASSIVE_GITHUB_STATUS_STALE_TIME_MS = 5 * 60 * 1000; +const GITHUB_STATUS_STALE_TIME_MS = 10_000; +const GITHUB_STATUS_REFETCH_INTERVAL_MS = 10_000; const GITHUB_PR_COMMENTS_STALE_TIME_MS = 30_000; const GITHUB_PR_COMMENTS_REFETCH_INTERVAL_MS = 30_000; @@ -22,7 +20,6 @@ export interface GitHubQueryPolicy { interface GitHubStatusQueryPolicyOptions { hasWorkspaceId: boolean; isActive?: boolean; - isReviewTabActive?: boolean; } interface GitHubPRCommentsQueryPolicyOptions { @@ -33,53 +30,20 @@ interface GitHubPRCommentsQueryPolicyOptions { } /** - * Centralizes GitHub query behavior so passive hover surfaces stay cheap while - * active workspace surfaces still revalidate when they become relevant again. + * Centralizes GitHub query behavior — all surfaces poll at 10s when active. */ export function getGitHubStatusQueryPolicy( surface: GitHubStatusQuerySurface, - { - hasWorkspaceId, - isActive = true, - isReviewTabActive = false, - }: GitHubStatusQueryPolicyOptions, + { hasWorkspaceId, isActive = true }: GitHubStatusQueryPolicyOptions, ): GitHubQueryPolicy { const isEnabled = hasWorkspaceId && isActive; - switch (surface) { - case "changes-sidebar": - return { - enabled: isEnabled, - refetchInterval: - isEnabled && isReviewTabActive - ? ACTIVE_GITHUB_STATUS_REFETCH_INTERVAL_MS - : false, - refetchOnWindowFocus: isEnabled, - staleTime: isReviewTabActive ? ACTIVE_GITHUB_STATUS_STALE_TIME_MS : 0, - }; - case "workspace-page": - return { - enabled: isEnabled, - refetchInterval: false, - refetchOnWindowFocus: false, - staleTime: PASSIVE_GITHUB_STATUS_STALE_TIME_MS, - }; - case "workspace-list-item": - return { - enabled: isEnabled, - refetchInterval: false, - refetchOnWindowFocus: false, - staleTime: WORKSPACE_LIST_ITEM_GITHUB_STATUS_STALE_TIME_MS, - }; - case "workspace-hover-card": - case "workspace-row": - return { - enabled: isEnabled, - refetchInterval: false, - refetchOnWindowFocus: false, - staleTime: PASSIVE_GITHUB_STATUS_STALE_TIME_MS, - }; - } + return { + enabled: isEnabled, + refetchInterval: isEnabled ? GITHUB_STATUS_REFETCH_INTERVAL_MS : false, + refetchOnWindowFocus: isEnabled, + staleTime: GITHUB_STATUS_STALE_TIME_MS, + }; } export function getGitHubPRCommentsQueryPolicy({ diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/constants.ts b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/constants.ts index b00bf489828..8a854f3bde4 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/constants.ts +++ b/apps/desktop/src/renderer/screens/main/components/WorkspaceSidebar/WorkspaceListItem/constants.ts @@ -1,5 +1,5 @@ export const WORKSPACE_DND_TYPE = "WORKSPACE"; export const MAX_KEYBOARD_SHORTCUT_INDEX = 9; -export const GITHUB_STATUS_STALE_TIME = 30_000; +export const GITHUB_STATUS_STALE_TIME = 10_000; export const HOVER_CARD_OPEN_DELAY = 400; export const HOVER_CARD_CLOSE_DELAY = 100; 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 665214e6097..b1e5d08e6f8 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 @@ -97,7 +97,6 @@ export function ChangesView({ { hasWorkspaceId: !!workspaceId, isActive, - isReviewTabActive, }, ); From 5ab157e1cd26d2e8af3a3341e37bd74506439e4f Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 2 Apr 2026 16:24:05 -0700 Subject: [PATCH 2/7] Remove some logic --- .../githubQueryPolicy.test.ts | 65 +------------------ .../githubQueryPolicy/githubQueryPolicy.ts | 9 +-- .../RightSidebar/ChangesView/ChangesView.tsx | 2 - 3 files changed, 4 insertions(+), 72 deletions(-) diff --git a/apps/desktop/src/renderer/lib/githubQueryPolicy/githubQueryPolicy.test.ts b/apps/desktop/src/renderer/lib/githubQueryPolicy/githubQueryPolicy.test.ts index 4474dcc7d2c..2322ed41755 100644 --- a/apps/desktop/src/renderer/lib/githubQueryPolicy/githubQueryPolicy.test.ts +++ b/apps/desktop/src/renderer/lib/githubQueryPolicy/githubQueryPolicy.test.ts @@ -5,7 +5,7 @@ import { } from "./githubQueryPolicy"; describe("getGitHubStatusQueryPolicy", () => { - test("polls every 10s for any active surface", () => { + test("polls every 10s uniformly across all surfaces", () => { for (const surface of [ "changes-sidebar", "workspace-page", @@ -26,60 +26,15 @@ describe("getGitHubStatusQueryPolicy", () => { }); } }); - - test("disables polling when surface is inactive", () => { - expect( - getGitHubStatusQueryPolicy("changes-sidebar", { - hasWorkspaceId: true, - isActive: false, - }), - ).toEqual({ - enabled: false, - refetchInterval: false, - refetchOnWindowFocus: false, - staleTime: 10_000, - }); - }); - - test("disables polling when workspace id is missing", () => { - expect( - getGitHubStatusQueryPolicy("workspace-page", { - hasWorkspaceId: false, - isActive: true, - }), - ).toEqual({ - enabled: false, - refetchInterval: false, - refetchOnWindowFocus: false, - staleTime: 10_000, - }); - }); }); describe("getGitHubPRCommentsQueryPolicy", () => { - test("fetches review comments without polling when changes is open on diffs", () => { - expect( - getGitHubPRCommentsQueryPolicy({ - hasWorkspaceId: true, - hasActivePullRequest: true, - isActive: true, - isReviewTabActive: false, - }), - ).toEqual({ - enabled: true, - refetchInterval: false, - refetchOnWindowFocus: false, - staleTime: 30_000, - }); - }); - - test("polls review comments while the review tab is active", () => { + test("polls every 30s when active with a pull request", () => { expect( getGitHubPRCommentsQueryPolicy({ hasWorkspaceId: true, hasActivePullRequest: true, isActive: true, - isReviewTabActive: true, }), ).toEqual({ enabled: true, @@ -88,20 +43,4 @@ describe("getGitHubPRCommentsQueryPolicy", () => { staleTime: 30_000, }); }); - - test("disables comments when there is no active pull request", () => { - expect( - getGitHubPRCommentsQueryPolicy({ - hasWorkspaceId: true, - hasActivePullRequest: false, - isActive: true, - isReviewTabActive: true, - }), - ).toEqual({ - enabled: false, - refetchInterval: false, - refetchOnWindowFocus: false, - staleTime: 30_000, - }); - }); }); diff --git a/apps/desktop/src/renderer/lib/githubQueryPolicy/githubQueryPolicy.ts b/apps/desktop/src/renderer/lib/githubQueryPolicy/githubQueryPolicy.ts index 3cc86ca7af1..c581431a3cb 100644 --- a/apps/desktop/src/renderer/lib/githubQueryPolicy/githubQueryPolicy.ts +++ b/apps/desktop/src/renderer/lib/githubQueryPolicy/githubQueryPolicy.ts @@ -26,7 +26,6 @@ interface GitHubPRCommentsQueryPolicyOptions { hasWorkspaceId: boolean; hasActivePullRequest: boolean; isActive?: boolean; - isReviewTabActive?: boolean; } /** @@ -50,17 +49,13 @@ export function getGitHubPRCommentsQueryPolicy({ hasWorkspaceId, hasActivePullRequest, isActive = true, - isReviewTabActive = false, }: GitHubPRCommentsQueryPolicyOptions): GitHubQueryPolicy { const isEnabled = hasWorkspaceId && isActive && hasActivePullRequest; return { enabled: isEnabled, - refetchInterval: - isEnabled && isReviewTabActive - ? GITHUB_PR_COMMENTS_REFETCH_INTERVAL_MS - : false, - refetchOnWindowFocus: isEnabled && isReviewTabActive, + refetchInterval: isEnabled ? GITHUB_PR_COMMENTS_REFETCH_INTERVAL_MS : false, + refetchOnWindowFocus: isEnabled, staleTime: GITHUB_PR_COMMENTS_STALE_TIME_MS, }; } 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 b1e5d08e6f8..77661eed921 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 @@ -91,7 +91,6 @@ export function ChangesView({ const worktreePath = workspace?.worktreePath; const projectId = workspace?.projectId; const activeTab = useChangesStore((s) => s.activeTab); - const isReviewTabActive = isActive && activeTab === "review"; const githubStatusQueryPolicy = getGitHubStatusQueryPolicy( "changes-sidebar", { @@ -261,7 +260,6 @@ export function ChangesView({ hasWorkspaceId: !!workspaceId, hasActivePullRequest: !!activePullRequest, isActive, - isReviewTabActive, }); const refreshTimerRef = useRef | null>(null); const pendingRefreshRef = useRef({ From 43010cfcb1f0f68595c14f9b669a1aaf5e337548 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 2 Apr 2026 16:50:48 -0700 Subject: [PATCH 3/7] fix(desktop): hover surfaces fetch on hover with 10s debounce instead of polling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Active surfaces (changes-sidebar, workspace-page) still poll every 10s. Hover surfaces (list-item, row, hover-card) now fetch on hover and use staleTime as a debounce — no continuous background polling. --- .../githubQueryPolicy.test.ts | 26 ++++++++++++++----- .../githubQueryPolicy/githubQueryPolicy.ts | 15 ++++++++--- .../WorkspaceListItem/WorkspaceListItem.tsx | 9 ++++--- .../WorkspaceRow/WorkspaceRow.tsx | 11 +++++--- 4 files changed, 46 insertions(+), 15 deletions(-) diff --git a/apps/desktop/src/renderer/lib/githubQueryPolicy/githubQueryPolicy.test.ts b/apps/desktop/src/renderer/lib/githubQueryPolicy/githubQueryPolicy.test.ts index 2322ed41755..b10273700b5 100644 --- a/apps/desktop/src/renderer/lib/githubQueryPolicy/githubQueryPolicy.test.ts +++ b/apps/desktop/src/renderer/lib/githubQueryPolicy/githubQueryPolicy.test.ts @@ -5,13 +5,27 @@ import { } from "./githubQueryPolicy"; describe("getGitHubStatusQueryPolicy", () => { - test("polls every 10s uniformly across all surfaces", () => { + test("active surfaces poll every 10s", () => { + for (const surface of ["changes-sidebar", "workspace-page"] as const) { + expect( + getGitHubStatusQueryPolicy(surface, { + hasWorkspaceId: true, + isActive: true, + }), + ).toEqual({ + enabled: true, + refetchInterval: 10_000, + refetchOnWindowFocus: true, + staleTime: 10_000, + }); + } + }); + + test("hover surfaces rely on staleTime debounce, no polling", () => { for (const surface of [ - "changes-sidebar", - "workspace-page", "workspace-list-item", - "workspace-hover-card", "workspace-row", + "workspace-hover-card", ] as const) { expect( getGitHubStatusQueryPolicy(surface, { @@ -20,8 +34,8 @@ describe("getGitHubStatusQueryPolicy", () => { }), ).toEqual({ enabled: true, - refetchInterval: 10_000, - refetchOnWindowFocus: true, + refetchInterval: false, + refetchOnWindowFocus: false, staleTime: 10_000, }); } diff --git a/apps/desktop/src/renderer/lib/githubQueryPolicy/githubQueryPolicy.ts b/apps/desktop/src/renderer/lib/githubQueryPolicy/githubQueryPolicy.ts index c581431a3cb..a3a03eebb64 100644 --- a/apps/desktop/src/renderer/lib/githubQueryPolicy/githubQueryPolicy.ts +++ b/apps/desktop/src/renderer/lib/githubQueryPolicy/githubQueryPolicy.ts @@ -28,19 +28,28 @@ interface GitHubPRCommentsQueryPolicyOptions { isActive?: boolean; } +const HOVER_SURFACES: ReadonlySet = new Set([ + "workspace-list-item", + "workspace-row", + "workspace-hover-card", +]); + /** - * Centralizes GitHub query behavior — all surfaces poll at 10s when active. + * Active surfaces (changes-sidebar, workspace-page) poll every 10s. + * Hover surfaces don't poll — callers trigger refetch on hover, debounced by staleTime. */ export function getGitHubStatusQueryPolicy( surface: GitHubStatusQuerySurface, { hasWorkspaceId, isActive = true }: GitHubStatusQueryPolicyOptions, ): GitHubQueryPolicy { const isEnabled = hasWorkspaceId && isActive; + const isHover = HOVER_SURFACES.has(surface); return { enabled: isEnabled, - refetchInterval: isEnabled ? GITHUB_STATUS_REFETCH_INTERVAL_MS : false, - refetchOnWindowFocus: isEnabled, + refetchInterval: + isEnabled && !isHover ? GITHUB_STATUS_REFETCH_INTERVAL_MS : false, + refetchOnWindowFocus: isEnabled && !isHover, staleTime: GITHUB_STATUS_STALE_TIME_MS, }; } 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 92a7c84ac2e..567005de196 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 @@ -155,7 +155,7 @@ export function WorkspaceListItem({ }, ); - const { data: githubStatus } = + const { data: githubStatus, isStale: isGithubStatusStale, refetch: refetchGithubStatus } = electronTrpc.workspaces.getGitHubStatus.useQuery( { workspaceId: id }, githubStatusQueryPolicy, @@ -173,7 +173,6 @@ export function WorkspaceListItem({ { enabled: isBranchWorkspace, staleTime: GITHUB_STATUS_STALE_TIME, - refetchInterval: hasHovered ? GITHUB_STATUS_STALE_TIME : false, }, ); @@ -231,7 +230,11 @@ export function WorkspaceListItem({ }; const handleMouseEnter = () => { - if (!hasHovered) setHasHovered(true); + if (!hasHovered) { + setHasHovered(true); + } else if (isGithubStatusStale) { + void refetchGithubStatus(); + } if (isBranchWorkspace) void refetchAheadBehind(); }; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspacesListView/WorkspaceRow/WorkspaceRow.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspacesListView/WorkspaceRow/WorkspaceRow.tsx index ac377143963..52cc1ee4556 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspacesListView/WorkspaceRow/WorkspaceRow.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspacesListView/WorkspaceRow/WorkspaceRow.tsx @@ -60,8 +60,7 @@ export function WorkspaceRow({ hasHovered && workspace.type === "worktree" && !!workspace.workspaceId, }); - // Lazy-load GitHub status on hover to avoid N+1 queries - const { data: githubStatus } = + const { data: githubStatus, isStale: isGithubStatusStale, refetch: refetchGithubStatus } = electronTrpc.workspaces.getGitHubStatus.useQuery( { workspaceId: workspace.workspaceId ?? "" }, githubStatusQueryPolicy, @@ -87,7 +86,13 @@ export function WorkspaceRow({ type="button" onClick={handleClick} disabled={isOpening} - onMouseEnter={() => !hasHovered && setHasHovered(true)} + onMouseEnter={() => { + if (!hasHovered) { + setHasHovered(true); + } else if (isGithubStatusStale) { + void refetchGithubStatus(); + } + }} className={cn( "flex items-center gap-3 w-full px-4 py-2 group text-left", "hover:bg-background/50 transition-colors", From f6cf24a9ce963b9411129e4267c4a3ba4ab57283 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 2 Apr 2026 17:54:55 -0700 Subject: [PATCH 4/7] refactor(desktop): extract useHoverGitHubStatus hook for hover-triggered fetching Moves the hover debounce + queued refetch logic into a shared hook so WorkspaceListItem and WorkspaceRow are simple consumers. --- .../githubQueryPolicy/githubQueryPolicy.ts | 2 +- .../renderer/lib/githubQueryPolicy/index.ts | 1 + .../githubQueryPolicy/useHoverGitHubStatus.ts | 61 +++++++++++++++++++ .../WorkspaceListItem/WorkspaceListItem.tsx | 30 +++------ .../WorkspaceRow/WorkspaceRow.tsx | 28 +++------ 5 files changed, 78 insertions(+), 44 deletions(-) create mode 100644 apps/desktop/src/renderer/lib/githubQueryPolicy/useHoverGitHubStatus.ts diff --git a/apps/desktop/src/renderer/lib/githubQueryPolicy/githubQueryPolicy.ts b/apps/desktop/src/renderer/lib/githubQueryPolicy/githubQueryPolicy.ts index a3a03eebb64..c8c118c83be 100644 --- a/apps/desktop/src/renderer/lib/githubQueryPolicy/githubQueryPolicy.ts +++ b/apps/desktop/src/renderer/lib/githubQueryPolicy/githubQueryPolicy.ts @@ -1,4 +1,4 @@ -const GITHUB_STATUS_STALE_TIME_MS = 10_000; +export const GITHUB_STATUS_STALE_TIME_MS = 10_000; const GITHUB_STATUS_REFETCH_INTERVAL_MS = 10_000; const GITHUB_PR_COMMENTS_STALE_TIME_MS = 30_000; const GITHUB_PR_COMMENTS_REFETCH_INTERVAL_MS = 30_000; diff --git a/apps/desktop/src/renderer/lib/githubQueryPolicy/index.ts b/apps/desktop/src/renderer/lib/githubQueryPolicy/index.ts index de2aafc9ae8..ac1fd2cc036 100644 --- a/apps/desktop/src/renderer/lib/githubQueryPolicy/index.ts +++ b/apps/desktop/src/renderer/lib/githubQueryPolicy/index.ts @@ -6,3 +6,4 @@ export { getGitHubPRCommentsQueryPolicy, getGitHubStatusQueryPolicy, } from "./githubQueryPolicy"; +export { useHoverGitHubStatus } from "./useHoverGitHubStatus"; diff --git a/apps/desktop/src/renderer/lib/githubQueryPolicy/useHoverGitHubStatus.ts b/apps/desktop/src/renderer/lib/githubQueryPolicy/useHoverGitHubStatus.ts new file mode 100644 index 00000000000..40563f09594 --- /dev/null +++ b/apps/desktop/src/renderer/lib/githubQueryPolicy/useHoverGitHubStatus.ts @@ -0,0 +1,61 @@ +import { useEffect, useRef, useState } from "react"; +import { electronTrpc } from "renderer/lib/electron-trpc"; +import { + GITHUB_STATUS_STALE_TIME_MS, + type GitHubStatusQuerySurface, + getGitHubStatusQueryPolicy, +} from "./githubQueryPolicy"; + +interface UseHoverGitHubStatusOptions { + workspaceId: string | undefined; + surface: GitHubStatusQuerySurface; + isWorktree: boolean; +} + +export function useHoverGitHubStatus({ + workspaceId, + surface, + isWorktree, +}: UseHoverGitHubStatusOptions) { + const [hasHovered, setHasHovered] = useState(false); + + const queryPolicy = getGitHubStatusQueryPolicy(surface, { + hasWorkspaceId: !!workspaceId, + isActive: hasHovered && isWorktree, + }); + + const { + data: githubStatus, + dataUpdatedAt, + isStale, + refetch, + } = electronTrpc.workspaces.getGitHubStatus.useQuery( + { workspaceId: workspaceId ?? "" }, + queryPolicy, + ); + + const pendingRefetchRef = useRef | null>(null); + useEffect( + () => () => { + if (pendingRefetchRef.current) clearTimeout(pendingRefetchRef.current); + }, + [], + ); + + const onMouseEnter = () => { + if (!hasHovered) { + setHasHovered(true); + } else if (isStale) { + void refetch(); + } else if (!pendingRefetchRef.current) { + const msUntilStale = + GITHUB_STATUS_STALE_TIME_MS - (Date.now() - dataUpdatedAt); + pendingRefetchRef.current = setTimeout(() => { + pendingRefetchRef.current = null; + void refetch(); + }, Math.max(0, msUntilStale)); + } + }; + + return { githubStatus, hasHovered, onMouseEnter }; +} 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 567005de196..42659ff9f85 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 @@ -3,12 +3,12 @@ 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 { useEffect, useMemo, useRef, useState } from "react"; +import { useEffect, useMemo, useRef } from "react"; import { HiMiniXMark } from "react-icons/hi2"; import { HotkeyTooltipContent } from "renderer/components/HotkeyTooltipContent"; import { useCopyToClipboard } from "renderer/hooks/useCopyToClipboard"; import { electronTrpc } from "renderer/lib/electron-trpc"; -import { getGitHubStatusQueryPolicy } from "renderer/lib/githubQueryPolicy"; +import { useHoverGitHubStatus } from "renderer/lib/githubQueryPolicy"; import { useWorkspaceDeleteHandler } from "renderer/react-query/workspaces"; import { navigateToWorkspace } from "renderer/routes/_authenticated/_dashboard/utils/workspace-navigation"; import { WorkspaceRunIndicator } from "renderer/screens/main/components/WorkspaceRunIndicator"; @@ -67,7 +67,11 @@ export function WorkspaceListItem({ const isBranchWorkspace = type === "branch"; const navigate = useNavigate(); const matchRoute = useMatchRoute(); - const [hasHovered, setHasHovered] = useState(false); + const { githubStatus, hasHovered, onMouseEnter: onGithubMouseEnter } = useHoverGitHubStatus({ + workspaceId: id, + surface: "workspace-list-item", + isWorktree: type === "worktree", + }); const rename = useWorkspaceRename(id, name, branch); const workspaceStatus = useTabsStore((state) => { function* paneStatuses() { @@ -147,20 +151,6 @@ export function WorkspaceListItem({ const { showDeleteDialog, setShowDeleteDialog, handleDeleteClick } = useWorkspaceDeleteHandler(); - const githubStatusQueryPolicy = getGitHubStatusQueryPolicy( - "workspace-list-item", - { - hasWorkspaceId: !!id, - isActive: hasHovered && type === "worktree", - }, - ); - - const { data: githubStatus, isStale: isGithubStatusStale, refetch: refetchGithubStatus } = - electronTrpc.workspaces.getGitHubStatus.useQuery( - { workspaceId: id }, - githubStatusQueryPolicy, - ); - const { status: localChanges } = useGitChangesStatus({ worktreePath, enabled: hasHovered && !!worktreePath, @@ -230,11 +220,7 @@ export function WorkspaceListItem({ }; const handleMouseEnter = () => { - if (!hasHovered) { - setHasHovered(true); - } else if (isGithubStatusStale) { - void refetchGithubStatus(); - } + onGithubMouseEnter(); if (isBranchWorkspace) void refetchAheadBehind(); }; diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspacesListView/WorkspaceRow/WorkspaceRow.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspacesListView/WorkspaceRow/WorkspaceRow.tsx index 52cc1ee4556..f719e5e02f3 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspacesListView/WorkspaceRow/WorkspaceRow.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspacesListView/WorkspaceRow/WorkspaceRow.tsx @@ -7,7 +7,6 @@ import { import { toast } from "@superset/ui/sonner"; import { Tooltip, TooltipContent, TooltipTrigger } from "@superset/ui/tooltip"; import { cn } from "@superset/ui/utils"; -import { useState } from "react"; import { LuArrowRight, LuExternalLink, @@ -16,7 +15,7 @@ import { LuRotateCw, } from "react-icons/lu"; import { electronTrpc } from "renderer/lib/electron-trpc"; -import { getGitHubStatusQueryPolicy } from "renderer/lib/githubQueryPolicy"; +import { useHoverGitHubStatus } from "renderer/lib/githubQueryPolicy"; import { useWorkspaceDeleteHandler } from "renderer/react-query/workspaces/useWorkspaceDeleteHandler"; import { STROKE_WIDTH } from "../../WorkspaceSidebar/constants"; import { DeleteWorkspaceDialog } from "../../WorkspaceSidebar/WorkspaceListItem/components/DeleteWorkspaceDialog/DeleteWorkspaceDialog"; @@ -38,7 +37,11 @@ export function WorkspaceRow({ isOpening, }: WorkspaceRowProps) { const isBranch = workspace.type === "branch"; - const [hasHovered, setHasHovered] = useState(false); + const { githubStatus, onMouseEnter: onGithubMouseEnter } = useHoverGitHubStatus({ + workspaceId: workspace.workspaceId, + surface: "workspace-row", + isWorktree: workspace.type === "worktree", + }); const { showDeleteDialog, setShowDeleteDialog, handleDeleteClick } = useWorkspaceDeleteHandler(); const openFileInEditor = electronTrpc.external.openFileInEditor.useMutation({ @@ -54,17 +57,6 @@ export function WorkspaceRow({ }); } }; - const githubStatusQueryPolicy = getGitHubStatusQueryPolicy("workspace-row", { - hasWorkspaceId: !!workspace.workspaceId, - isActive: - hasHovered && workspace.type === "worktree" && !!workspace.workspaceId, - }); - - const { data: githubStatus, isStale: isGithubStatusStale, refetch: refetchGithubStatus } = - electronTrpc.workspaces.getGitHubStatus.useQuery( - { workspaceId: workspace.workspaceId ?? "" }, - githubStatusQueryPolicy, - ); const pr = githubStatus?.pr; const showDiffStats = pr && (pr.additions > 0 || pr.deletions > 0); @@ -86,13 +78,7 @@ export function WorkspaceRow({ type="button" onClick={handleClick} disabled={isOpening} - onMouseEnter={() => { - if (!hasHovered) { - setHasHovered(true); - } else if (isGithubStatusStale) { - void refetchGithubStatus(); - } - }} + onMouseEnter={onGithubMouseEnter} className={cn( "flex items-center gap-3 w-full px-4 py-2 group text-left", "hover:bg-background/50 transition-colors", From 14e07c7e2b73c6847a92f54cd3d1a387025d5371 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 2 Apr 2026 18:03:50 -0700 Subject: [PATCH 5/7] fix(desktop): clear pending refetch timer before stale refetch to avoid duplicate --- .../lib/githubQueryPolicy/useHoverGitHubStatus.ts | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src/renderer/lib/githubQueryPolicy/useHoverGitHubStatus.ts b/apps/desktop/src/renderer/lib/githubQueryPolicy/useHoverGitHubStatus.ts index 40563f09594..abbffbf9194 100644 --- a/apps/desktop/src/renderer/lib/githubQueryPolicy/useHoverGitHubStatus.ts +++ b/apps/desktop/src/renderer/lib/githubQueryPolicy/useHoverGitHubStatus.ts @@ -46,14 +46,21 @@ export function useHoverGitHubStatus({ if (!hasHovered) { setHasHovered(true); } else if (isStale) { + if (pendingRefetchRef.current) { + clearTimeout(pendingRefetchRef.current); + pendingRefetchRef.current = null; + } void refetch(); } else if (!pendingRefetchRef.current) { const msUntilStale = GITHUB_STATUS_STALE_TIME_MS - (Date.now() - dataUpdatedAt); - pendingRefetchRef.current = setTimeout(() => { - pendingRefetchRef.current = null; - void refetch(); - }, Math.max(0, msUntilStale)); + pendingRefetchRef.current = setTimeout( + () => { + pendingRefetchRef.current = null; + void refetch(); + }, + Math.max(0, msUntilStale), + ); } }; From bedafcfc0a1572b08299bea2eaa39fbfc29328f7 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 2 Apr 2026 18:05:52 -0700 Subject: [PATCH 6/7] Lint --- .../WorkspaceListItem/WorkspaceListItem.tsx | 6 +++++- .../WorkspacesListView/WorkspaceRow/WorkspaceRow.tsx | 11 ++++++----- 2 files changed, 11 insertions(+), 6 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 42659ff9f85..e9374ffc89e 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 @@ -67,7 +67,11 @@ export function WorkspaceListItem({ const isBranchWorkspace = type === "branch"; const navigate = useNavigate(); const matchRoute = useMatchRoute(); - const { githubStatus, hasHovered, onMouseEnter: onGithubMouseEnter } = useHoverGitHubStatus({ + const { + githubStatus, + hasHovered, + onMouseEnter: onGithubMouseEnter, + } = useHoverGitHubStatus({ workspaceId: id, surface: "workspace-list-item", isWorktree: type === "worktree", diff --git a/apps/desktop/src/renderer/screens/main/components/WorkspacesListView/WorkspaceRow/WorkspaceRow.tsx b/apps/desktop/src/renderer/screens/main/components/WorkspacesListView/WorkspaceRow/WorkspaceRow.tsx index f719e5e02f3..509e4608ef9 100644 --- a/apps/desktop/src/renderer/screens/main/components/WorkspacesListView/WorkspaceRow/WorkspaceRow.tsx +++ b/apps/desktop/src/renderer/screens/main/components/WorkspacesListView/WorkspaceRow/WorkspaceRow.tsx @@ -37,11 +37,12 @@ export function WorkspaceRow({ isOpening, }: WorkspaceRowProps) { const isBranch = workspace.type === "branch"; - const { githubStatus, onMouseEnter: onGithubMouseEnter } = useHoverGitHubStatus({ - workspaceId: workspace.workspaceId, - surface: "workspace-row", - isWorktree: workspace.type === "worktree", - }); + const { githubStatus, onMouseEnter: onGithubMouseEnter } = + useHoverGitHubStatus({ + workspaceId: workspace.workspaceId, + surface: "workspace-row", + isWorktree: workspace.type === "worktree", + }); const { showDeleteDialog, setShowDeleteDialog, handleDeleteClick } = useWorkspaceDeleteHandler(); const openFileInEditor = electronTrpc.external.openFileInEditor.useMutation({ From 7a9f1b28611f3ee10ef8338812c7e5fa69fc4c36 Mon Sep 17 00:00:00 2001 From: Kiet Ho Date: Thu, 2 Apr 2026 18:25:03 -0700 Subject: [PATCH 7/7] fix(desktop): accept null workspaceId in useHoverGitHubStatus --- .../src/renderer/lib/githubQueryPolicy/useHoverGitHubStatus.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/desktop/src/renderer/lib/githubQueryPolicy/useHoverGitHubStatus.ts b/apps/desktop/src/renderer/lib/githubQueryPolicy/useHoverGitHubStatus.ts index abbffbf9194..e833b9a7dca 100644 --- a/apps/desktop/src/renderer/lib/githubQueryPolicy/useHoverGitHubStatus.ts +++ b/apps/desktop/src/renderer/lib/githubQueryPolicy/useHoverGitHubStatus.ts @@ -7,7 +7,7 @@ import { } from "./githubQueryPolicy"; interface UseHoverGitHubStatusOptions { - workspaceId: string | undefined; + workspaceId: string | null | undefined; surface: GitHubStatusQuerySurface; isWorktree: boolean; }