'use client'; import React from 'react'; import { usePathname } from 'next/navigation'; import { getStatus, listSessions, wsManager } from '@/lib/api'; import { useChatStore } from '@/lib/store'; import type { ProcessWsEvent, SessionUpdatedEvent, WsEvent } from '@/types'; function scheduleWhenIdle(task: () => void, timeout = 1200): () => void { if (typeof window === 'undefined') { task(); return () => {}; } const idleWindow = window as Window & typeof globalThis & { requestIdleCallback?: (callback: IdleRequestCallback, options?: IdleRequestOptions) => number; cancelIdleCallback?: (handle: number) => void; }; if (typeof idleWindow.requestIdleCallback === 'function') { const id = idleWindow.requestIdleCallback(() => task(), { timeout }); return () => idleWindow.cancelIdleCallback?.(id); } const id = globalThis.setTimeout(task, 250); return () => globalThis.clearTimeout(id); } function isProcessEvent(data: WsEvent | Record): data is ProcessWsEvent { const type = typeof data.type === 'string' ? data.type : ''; return type.startsWith('process_') || type === 'process_cancel_ack'; } function isSessionUpdatedEvent(data: WsEvent | Record): data is SessionUpdatedEvent { return data.type === 'session_updated' && typeof data.session_id === 'string'; } export function AppRuntimeBridge() { const pathname = usePathname(); const sessionId = useChatStore((state) => state.sessionId); const setSessions = useChatStore((state) => state.setSessions); const setWsStatus = useChatStore((state) => state.setWsStatus); const setBeaverReady = useChatStore((state) => state.setBeaverReady); const resetProcessState = useChatStore((state) => state.resetProcessState); const ingestProcessEvent = useChatStore((state) => state.ingestProcessEvent); const statusCheckCleanupRef = React.useRef<(() => void) | null>(null); const statusCheckInFlightRef = React.useRef(false); const chatRuntimeEnabled = pathname === '/' || pathname.startsWith('/tasks') || pathname.startsWith('/notifications'); const loadSessions = React.useCallback(async () => { try { const sessions = await listSessions(); setSessions(sessions); } catch { // backend may still be offline during first render } }, [setSessions]); const scheduleStatusCheck = React.useCallback(() => { if (statusCheckInFlightRef.current) return; statusCheckCleanupRef.current?.(); statusCheckCleanupRef.current = scheduleWhenIdle(async () => { statusCheckInFlightRef.current = true; try { await getStatus(); setBeaverReady(true); } catch { setBeaverReady(false); } finally { statusCheckInFlightRef.current = false; } }); }, [setBeaverReady]); React.useEffect(() => { if (!chatRuntimeEnabled) { return; } void loadSessions(); }, [chatRuntimeEnabled, loadSessions]); React.useEffect(() => { if (!chatRuntimeEnabled) { wsManager.disconnect(); setWsStatus('disconnected'); setBeaverReady(null); return; } resetProcessState(); wsManager.connect(sessionId); }, [chatRuntimeEnabled, resetProcessState, sessionId, setBeaverReady, setWsStatus]); React.useEffect(() => { if (!chatRuntimeEnabled) { return; } const unsubStatus = wsManager.onStatusChange((status) => { setWsStatus(status); if (status === 'connected') { scheduleStatusCheck(); } else { statusCheckCleanupRef.current?.(); statusCheckCleanupRef.current = null; setBeaverReady(null); } }); return () => { statusCheckCleanupRef.current?.(); statusCheckCleanupRef.current = null; unsubStatus(); }; }, [chatRuntimeEnabled, scheduleStatusCheck, setBeaverReady, setWsStatus]); React.useEffect(() => { if (!chatRuntimeEnabled) { return; } const unsubMessage = wsManager.onMessage((data) => { if (isSessionUpdatedEvent(data)) { void loadSessions(); return; } if (isProcessEvent(data)) { ingestProcessEvent(data); } }); return () => { unsubMessage(); }; }, [chatRuntimeEnabled, ingestProcessEvent, loadSessions]); return null; }