import { useEffect, useRef, useState } from 'react' import { getLastInteractionTime } from '../bootstrap/state.js' import { fetchPrStatus, type PrReviewState } from '../utils/ghPrStatus.js' const POLL_INTERVAL_MS = 60_000 const SLOW_GH_THRESHOLD_MS = 4_000 const IDLE_STOP_MS = 60 * 60_000 // stop polling after 60 min idle export type PrStatusState = { number: number | null url: string | null reviewState: PrReviewState | null lastUpdated: number } const INITIAL_STATE: PrStatusState = { number: null, url: null, reviewState: null, lastUpdated: 0, } /** * Polls PR review status every 60s while the session is active. * When no interaction is detected for 60 minutes, the loop stops — no * timers remain. React re-runs the effect when isLoading changes * (turn starts/ends), restarting the loop. Effect setup schedules * the next poll relative to the last fetch time so turn boundaries * don't spawn `gh` more than once per interval. Disables permanently * if a fetch exceeds 4s. * * Pass `enabled: false` to skip polling entirely (hook still must be * called unconditionally to satisfy the rules of hooks). */ export function usePrStatus(isLoading: boolean, enabled = true): PrStatusState { const [prStatus, setPrStatus] = useState(INITIAL_STATE) const timeoutRef = useRef | null>(null) const disabledRef = useRef(false) const lastFetchRef = useRef(0) useEffect(() => { if (!enabled) return if (disabledRef.current) return let cancelled = false let lastSeenInteractionTime = -1 let lastActivityTimestamp = Date.now() async function poll() { if (cancelled) return const currentInteractionTime = getLastInteractionTime() if (lastSeenInteractionTime !== currentInteractionTime) { lastSeenInteractionTime = currentInteractionTime lastActivityTimestamp = Date.now() } else if (Date.now() - lastActivityTimestamp >= IDLE_STOP_MS) { return } const start = Date.now() const result = await fetchPrStatus() if (cancelled) return lastFetchRef.current = start setPrStatus(prev => { const newNumber = result?.number ?? null const newReviewState = result?.reviewState ?? null if (prev.number === newNumber && prev.reviewState === newReviewState) { return prev } return { number: newNumber, url: result?.url ?? null, reviewState: newReviewState, lastUpdated: Date.now(), } }) if (Date.now() - start > SLOW_GH_THRESHOLD_MS) { disabledRef.current = true return } if (!cancelled) { timeoutRef.current = setTimeout(poll, POLL_INTERVAL_MS) } } const elapsed = Date.now() - lastFetchRef.current if (elapsed >= POLL_INTERVAL_MS) { void poll() } else { timeoutRef.current = setTimeout(poll, POLL_INTERVAL_MS - elapsed) } return () => { cancelled = true if (timeoutRef.current) { clearTimeout(timeoutRef.current) timeoutRef.current = null } } }, [isLoading, enabled]) return prStatus }