From f5efa857ca498bd20cd9b8e390a805915bf5e276 Mon Sep 17 00:00:00 2001 From: Shirone Date: Sat, 24 Jan 2026 22:05:29 +0100 Subject: [PATCH] fix: Prevent GitHub API rate limiting from frequent worktree PR fetching MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #685 This commit addresses the GitHub API rate limit issue caused by excessive worktree PR status fetching. ## Changes ### Server-side PR caching (list.ts) - Added `GitHubPRCacheEntry` interface and `githubPRCache` Map - Implemented 2-minute TTL cache for GitHub PR data - Modified `fetchGitHubPRs()` to check cache before making API calls - Added `forceRefresh` parameter to bypass cache when explicitly requested - Cache is properly cleared when force refresh is triggered ### Frontend polling reduction (worktree-panel.tsx) - Increased worktree polling interval from 5 seconds to 30 seconds - Reduces polling frequency by 6x while keeping UI reasonably fresh - Updated comment to reflect new polling strategy ### Type improvements (use-worktrees.ts) - Fixed `fetchWorktrees` callback signature to accept `silent` option - Returns proper type for removed worktrees detection ## Impact - Combined ~12x reduction in GitHub API calls - 2-minute cache prevents repeated API hits during normal operation - 30-second polling balances responsiveness with API conservation - Force refresh option allows users to manually update when needed 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- .../server/src/routes/worktree/routes/list.ts | 33 +++++++++++++++++-- .../worktree-panel/hooks/use-worktrees.ts | 20 +++++++---- .../worktree-panel/worktree-panel.tsx | 6 ++-- 3 files changed, 47 insertions(+), 12 deletions(-) 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) {