/** * useProactive — React hook that drives tick generation for proactive mode. * * Mounted inside REPL.tsx when feature('PROACTIVE') || feature('KAIROS'). * Generates HH:MM:SS prompts at a fixed interval while * proactive mode is active and not blocked. */ import { useEffect, useRef } from 'react' import { TICK_TAG } from '../constants/xml.js' import { isProactiveActive, isProactivePaused, isContextBlocked, setNextTickAt, shouldTick, } from './index.js' /** Default interval between ticks (ms). Prompt cache TTL is ~5 min so we * stay well under that to keep the cache warm. */ const TICK_INTERVAL_MS = 30_000 type UseProactiveOpts = { isLoading: boolean queuedCommandsLength: number hasActiveLocalJsxUI: boolean isInPlanMode: boolean onSubmitTick: (prompt: string) => void onQueueTick: (prompt: string) => void } export function useProactive(opts: UseProactiveOpts): void { const optsRef = useRef(opts) optsRef.current = opts useEffect(() => { if (!isProactiveActive()) return let timer: ReturnType | null = null function scheduleTick(): void { const nextTs = Date.now() + TICK_INTERVAL_MS setNextTickAt(nextTs) timer = setTimeout(() => { timer = null // Guard: skip tick if any blocking condition is met if (!shouldTick()) { // Reschedule — conditions may clear later scheduleTick() return } const { isLoading, queuedCommandsLength, hasActiveLocalJsxUI, isInPlanMode, } = optsRef.current // Don't fire while a query is in-flight, plan mode is active, // a local JSX UI is showing, or commands are queued if ( isLoading || isInPlanMode || hasActiveLocalJsxUI || queuedCommandsLength > 0 ) { scheduleTick() return } const tickContent = `<${TICK_TAG}>${new Date().toLocaleTimeString()}` // If nothing is in the queue, submit directly; otherwise queue if (queuedCommandsLength === 0) { optsRef.current.onSubmitTick(tickContent) } else { optsRef.current.onQueueTick(tickContent) } // Schedule next tick scheduleTick() }, TICK_INTERVAL_MS) } scheduleTick() return () => { if (timer !== null) { clearTimeout(timer) timer = null } setNextTickAt(null) } }, [ // Re-mount when proactive state changes isProactiveActive(), isProactivePaused(), isContextBlocked(), ]) }