From 23831f8703bf2a33aaf7d79c21e55090cfdb0613 Mon Sep 17 00:00:00 2001 From: Kainoa Date: Mon, 19 Jan 2026 10:22:48 -0800 Subject: [PATCH] feat: auto-refresh PR status on window focus and polling Add automatic PR status refresh to update sidebar buttons when PRs are created via terminal. Implements window focus refresh for all subscribed tasks and 30-second polling for the active task (pauses when hidden). Co-Authored-By: Claude Opus 4.5 --- src/renderer/App.tsx | 4 ++ src/renderer/hooks/useAutoPrRefresh.ts | 74 ++++++++++++++++++++++++++ src/renderer/lib/prStatusStore.ts | 10 ++++ 3 files changed, 88 insertions(+) create mode 100644 src/renderer/hooks/useAutoPrRefresh.ts diff --git a/src/renderer/App.tsx b/src/renderer/App.tsx index acb95bd8..59941a96 100644 --- a/src/renderer/App.tsx +++ b/src/renderer/App.tsx @@ -32,6 +32,7 @@ import { KeyboardSettingsProvider } from './contexts/KeyboardSettingsContext'; import { ToastAction } from './components/ui/toast'; import { Toaster } from './components/ui/toaster'; import { useToast } from './hooks/use-toast'; +import { useAutoPrRefresh } from './hooks/useAutoPrRefresh'; import { useGithubAuth } from './hooks/useGithubAuth'; import { useTheme } from './hooks/useTheme'; import useUpdateNotifier from './hooks/useUpdateNotifier'; @@ -151,6 +152,9 @@ const AppContent: React.FC = () => { // Show toast on update availability and kick off a background check useUpdateNotifier({ checkOnMount: true, onOpenSettings: () => setShowSettings(true) }); + // Auto-refresh PR status on window focus and periodic polling for active task + useAutoPrRefresh(activeTask?.path); + const defaultPanelLayout = React.useMemo(() => { const stored = loadPanelSizes(PANEL_LAYOUT_STORAGE_KEY, DEFAULT_PANEL_LAYOUT); const [storedLeft = DEFAULT_PANEL_LAYOUT[0], , storedRight = DEFAULT_PANEL_LAYOUT[2]] = diff --git a/src/renderer/hooks/useAutoPrRefresh.ts b/src/renderer/hooks/useAutoPrRefresh.ts new file mode 100644 index 00000000..1b8461d0 --- /dev/null +++ b/src/renderer/hooks/useAutoPrRefresh.ts @@ -0,0 +1,74 @@ +import { useEffect, useRef } from 'react'; +import { refreshPrStatus, refreshAllSubscribedPrStatus } from '../lib/prStatusStore'; + +const POLLING_INTERVAL_MS = 30000; // 30 seconds +const COOLDOWN_MS = 5000; // 5 second debounce for rapid focus/visibility events + +/** + * Auto-refreshes PR status via: + * 1. Window focus - refreshes all subscribed tasks (debounced) + * 2. Polling - refreshes active task every 30s (pauses when hidden) + */ +export function useAutoPrRefresh(activeTaskPath: string | undefined): void { + const lastFocusRefresh = useRef(0); + const lastVisibilityRefresh = useRef(0); + + // Window focus refresh (all subscribed tasks, debounced) + useEffect(() => { + const handleFocus = () => { + const now = Date.now(); + if (now - lastFocusRefresh.current < COOLDOWN_MS) return; + lastFocusRefresh.current = now; + refreshAllSubscribedPrStatus().catch(() => {}); + }; + + window.addEventListener('focus', handleFocus); + return () => window.removeEventListener('focus', handleFocus); + }, []); + + // Polling for active task (pauses when window hidden) + useEffect(() => { + if (!activeTaskPath) return; + + let intervalId: ReturnType | null = null; + + const startPolling = () => { + if (intervalId) return; + intervalId = setInterval(() => { + refreshPrStatus(activeTaskPath).catch(() => {}); + }, POLLING_INTERVAL_MS); + }; + + const stopPolling = () => { + if (intervalId) { + clearInterval(intervalId); + intervalId = null; + } + }; + + const handleVisibilityChange = () => { + if (document.hidden) { + stopPolling(); + } else { + const now = Date.now(); + if (now - lastVisibilityRefresh.current >= COOLDOWN_MS) { + lastVisibilityRefresh.current = now; + refreshPrStatus(activeTaskPath).catch(() => {}); + } + startPolling(); + } + }; + + // Start polling if visible + if (!document.hidden) { + startPolling(); + } + + document.addEventListener('visibilitychange', handleVisibilityChange); + + return () => { + stopPolling(); + document.removeEventListener('visibilitychange', handleVisibilityChange); + }; + }, [activeTaskPath]); +} diff --git a/src/renderer/lib/prStatusStore.ts b/src/renderer/lib/prStatusStore.ts index f66e8507..55fe88e4 100644 --- a/src/renderer/lib/prStatusStore.ts +++ b/src/renderer/lib/prStatusStore.ts @@ -46,6 +46,15 @@ export async function refreshPrStatus(taskPath: string): Promise { + const paths = Array.from(listeners.keys()); + await Promise.all(paths.map(refreshPrStatus)); +} + export function subscribeToPrStatus(taskPath: string, listener: Listener): () => void { const set = listeners.get(taskPath) || new Set(); set.add(listener); @@ -70,6 +79,7 @@ export function subscribeToPrStatus(taskPath: string, listener: Listener): () => taskListeners.delete(listener); if (taskListeners.size === 0) { listeners.delete(taskPath); + cache.delete(taskPath); // Clear cache when no subscribers } } };