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 } } };