diff --git a/apps/server/src/routes/worktree/routes/list.ts b/apps/server/src/routes/worktree/routes/list.ts index f0d9c030..8482f62c 100644 --- a/apps/server/src/routes/worktree/routes/list.ts +++ b/apps/server/src/routes/worktree/routes/list.ts @@ -39,8 +39,15 @@ interface GitHubRemoteCacheEntry { checkedAt: number; } +interface GitHubPRCacheEntry { + prs: Map; + fetchedAt: number; +} + const githubRemoteCache = new Map(); +const githubPRCache = new Map(); const GITHUB_REMOTE_CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes +const GITHUB_PR_CACHE_TTL_MS = 2 * 60 * 1000; // 2 minutes - avoid hitting GitHub on every poll interface WorktreeInfo { path: string; @@ -180,9 +187,24 @@ async function getGitHubRemoteStatus(projectPath: string): Promise> { +async function fetchGitHubPRs( + projectPath: string, + forceRefresh = false +): Promise> { + const now = Date.now(); + + if (!forceRefresh) { + const cached = githubPRCache.get(projectPath); + if (cached && now - cached.fetchedAt < GITHUB_PR_CACHE_TTL_MS) { + return cached.prs; + } + } else { + githubPRCache.delete(projectPath); + } + const prMap = new Map(); try { @@ -225,6 +247,11 @@ async function fetchGitHubPRs(projectPath: string): Promise(); for (const worktree of worktrees) { diff --git a/apps/ui/src/components/views/board-view/worktree-panel/hooks/use-worktrees.ts b/apps/ui/src/components/views/board-view/worktree-panel/hooks/use-worktrees.ts index 6a3276ec..ab3a87d0 100644 --- a/apps/ui/src/components/views/board-view/worktree-panel/hooks/use-worktrees.ts +++ b/apps/ui/src/components/views/board-view/worktree-panel/hooks/use-worktrees.ts @@ -95,12 +95,20 @@ export function useWorktrees({ ); // fetchWorktrees for backward compatibility - now just triggers a refetch - const fetchWorktrees = useCallback(async () => { - await queryClient.invalidateQueries({ - queryKey: queryKeys.worktrees.all(projectPath), - }); - return refetch(); - }, [projectPath, queryClient, refetch]); + // The silent option is accepted but not used (React Query handles loading states) + // Returns removed worktrees array if any were detected, undefined otherwise + const fetchWorktrees = useCallback( + async (_options?: { + silent?: boolean; + }): Promise | undefined> => { + await queryClient.invalidateQueries({ + queryKey: queryKeys.worktrees.all(projectPath), + }); + const result = await refetch(); + return result.data?.removedWorktrees; + }, + [projectPath, queryClient, refetch] + ); const currentWorktreePath = currentWorktree?.path ?? null; const selectedWorktree = currentWorktreePath diff --git a/apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx b/apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx index b1e800fe..6d376ea5 100644 --- a/apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx +++ b/apps/ui/src/components/views/board-view/worktree-panel/worktree-panel.tsx @@ -383,13 +383,13 @@ export function WorktreePanel({ const isMobile = useIsMobile(); - // Periodic interval check (5 seconds) to detect branch changes on disk - // Reduced from 1s to 5s to minimize GPU/CPU usage from frequent re-renders + // Periodic interval check (30 seconds) to detect branch changes on disk + // Reduced polling to lessen repeated worktree list calls while keeping UI reasonably fresh const intervalRef = useRef(null); useEffect(() => { intervalRef.current = setInterval(() => { fetchWorktrees({ silent: true }); - }, 5000); + }, 30000); return () => { if (intervalRef.current) {