source dump of claude code
at main 106 lines 3.2 kB view raw
1import { useEffect, useRef, useState } from 'react' 2import { getLastInteractionTime } from '../bootstrap/state.js' 3import { fetchPrStatus, type PrReviewState } from '../utils/ghPrStatus.js' 4 5const POLL_INTERVAL_MS = 60_000 6const SLOW_GH_THRESHOLD_MS = 4_000 7const IDLE_STOP_MS = 60 * 60_000 // stop polling after 60 min idle 8 9export type PrStatusState = { 10 number: number | null 11 url: string | null 12 reviewState: PrReviewState | null 13 lastUpdated: number 14} 15 16const INITIAL_STATE: PrStatusState = { 17 number: null, 18 url: null, 19 reviewState: null, 20 lastUpdated: 0, 21} 22 23/** 24 * Polls PR review status every 60s while the session is active. 25 * When no interaction is detected for 60 minutes, the loop stops — no 26 * timers remain. React re-runs the effect when isLoading changes 27 * (turn starts/ends), restarting the loop. Effect setup schedules 28 * the next poll relative to the last fetch time so turn boundaries 29 * don't spawn `gh` more than once per interval. Disables permanently 30 * if a fetch exceeds 4s. 31 * 32 * Pass `enabled: false` to skip polling entirely (hook still must be 33 * called unconditionally to satisfy the rules of hooks). 34 */ 35export function usePrStatus(isLoading: boolean, enabled = true): PrStatusState { 36 const [prStatus, setPrStatus] = useState<PrStatusState>(INITIAL_STATE) 37 const timeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null) 38 const disabledRef = useRef(false) 39 const lastFetchRef = useRef(0) 40 41 useEffect(() => { 42 if (!enabled) return 43 if (disabledRef.current) return 44 45 let cancelled = false 46 let lastSeenInteractionTime = -1 47 let lastActivityTimestamp = Date.now() 48 49 async function poll() { 50 if (cancelled) return 51 52 const currentInteractionTime = getLastInteractionTime() 53 if (lastSeenInteractionTime !== currentInteractionTime) { 54 lastSeenInteractionTime = currentInteractionTime 55 lastActivityTimestamp = Date.now() 56 } else if (Date.now() - lastActivityTimestamp >= IDLE_STOP_MS) { 57 return 58 } 59 60 const start = Date.now() 61 const result = await fetchPrStatus() 62 if (cancelled) return 63 lastFetchRef.current = start 64 65 setPrStatus(prev => { 66 const newNumber = result?.number ?? null 67 const newReviewState = result?.reviewState ?? null 68 if (prev.number === newNumber && prev.reviewState === newReviewState) { 69 return prev 70 } 71 return { 72 number: newNumber, 73 url: result?.url ?? null, 74 reviewState: newReviewState, 75 lastUpdated: Date.now(), 76 } 77 }) 78 79 if (Date.now() - start > SLOW_GH_THRESHOLD_MS) { 80 disabledRef.current = true 81 return 82 } 83 84 if (!cancelled) { 85 timeoutRef.current = setTimeout(poll, POLL_INTERVAL_MS) 86 } 87 } 88 89 const elapsed = Date.now() - lastFetchRef.current 90 if (elapsed >= POLL_INTERVAL_MS) { 91 void poll() 92 } else { 93 timeoutRef.current = setTimeout(poll, POLL_INTERVAL_MS - elapsed) 94 } 95 96 return () => { 97 cancelled = true 98 if (timeoutRef.current) { 99 clearTimeout(timeoutRef.current) 100 timeoutRef.current = null 101 } 102 } 103 }, [isLoading, enabled]) 104 105 return prStatus 106}