import { useState, useEffect, useRef, useCallback } from 'react' export type HighlightElement = 'editor' | 'tab' | 'properties' | 'notelist' | null export interface AiActivity { highlightElement: HighlightElement highlightPath: string | null } export interface AiActivityCallbacks { onOpenNote?: (path: string) => void onOpenTab?: (path: string) => void onSetFilter?: (type: string) => void onVaultChanged?: (path?: string) => void } const WS_UI_URL = 'ws://localhost:9711' const HIGHLIGHT_DURATION_MS = 800 const RECONNECT_DELAY_MS = 3000 /** * Listens on the UI WebSocket bridge (port 9711) for UI action events * from the MCP server. Handles highlight, open_note, open_tab, set_filter, * and vault_changed actions. */ export function useAiActivity(callbacks?: AiActivityCallbacks): AiActivity { const [highlightElement, setHighlightElement] = useState(null) const [highlightPath, setHighlightPath] = useState(null) const timerRef = useRef | null>(null) const callbacksRef = useRef(callbacks) useEffect(() => { callbacksRef.current = callbacks }) const handleMessage = useCallback((event: MessageEvent) => { try { const data = JSON.parse(event.data as string) if (data.type !== 'ui_action') return switch (data.action) { case 'highlight': setHighlightElement(data.element ?? null) setHighlightPath(data.path ?? null) if (timerRef.current) clearTimeout(timerRef.current) timerRef.current = setTimeout(() => { setHighlightElement(null) setHighlightPath(null) }, HIGHLIGHT_DURATION_MS) break case 'open_note': if (data.path) callbacksRef.current?.onOpenNote?.(data.path) break case 'open_tab': if (data.path) callbacksRef.current?.onOpenTab?.(data.path) break case 'set_filter': if (data.filterType) callbacksRef.current?.onSetFilter?.(data.filterType) break case 'vault_changed': callbacksRef.current?.onVaultChanged?.(data.path) break } } catch { // Ignore parse errors from malformed messages } }, []) useEffect(() => { let ws: WebSocket | null = null let mounted = true let reconnectTimer: ReturnType | null = null function connect() { if (!mounted) return try { ws = new WebSocket(WS_UI_URL) ws.onmessage = handleMessage ws.onclose = () => { if (mounted) reconnectTimer = setTimeout(connect, RECONNECT_DELAY_MS) } ws.onerror = () => { /* Silent — bridge may not be running */ } } catch { if (mounted) reconnectTimer = setTimeout(connect, RECONNECT_DELAY_MS) } } connect() return () => { mounted = false ws?.close() if (timerRef.current) clearTimeout(timerRef.current) if (reconnectTimer) clearTimeout(reconnectTimer) } }, [handleMessage]) return { highlightElement, highlightPath } }