init claude-code
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
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<PrStatusState>(INITIAL_STATE)
|
||||
const timeoutRef = useRef<ReturnType<typeof setTimeout> | 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
|
||||
}
|
||||
Reference in New Issue
Block a user