import { createHostEventSource } from './host-api'; let eventSource: EventSource | null = null; const HOST_EVENT_TO_IPC_CHANNEL: Record = { 'opencode:status': 'opencode:status', 'opencode:stderr': 'opencode:stderr', 'oauth:code': 'oauth:code', 'oauth:success': 'oauth:success', 'oauth:error': 'oauth:error', 'agent-browser:show': 'agent-browser:show', 'agent-browser:state': 'agent-browser:state', 'auth:session-changed': 'auth:session-changed', }; function getEventSource(): EventSource { if (!eventSource) { eventSource = createHostEventSource(); } return eventSource; } function allowSseFallback(): boolean { try { return window.localStorage.getItem('niancode:allow-sse-fallback') === '1'; } catch { return false; } } export function subscribeHostEvent( eventName: string, handler: (payload: T) => void, ): () => void { const ipc = window.electron?.ipcRenderer; const ipcChannel = HOST_EVENT_TO_IPC_CHANNEL[eventName]; if (ipcChannel && ipc?.on && ipc?.off) { const listener = (payload: unknown) => { handler(payload as T); }; // preload's `on()` wraps the callback in an internal subscription function // and returns a cleanup function that removes that exact wrapper. We MUST // use the returned cleanup rather than calling `off(channel, listener)`, // because `listener` !== the internal wrapper and removeListener would be // a no-op, leaking the subscription. const unsubscribe = ipc.on(ipcChannel, listener); if (typeof unsubscribe === 'function') { return unsubscribe; } // Fallback for environments where on() doesn't return cleanup return () => { ipc.off(ipcChannel, listener); }; } if (!allowSseFallback()) { console.warn(`[host-events] no IPC mapping for event "${eventName}", SSE fallback disabled`); return () => {}; } const source = getEventSource(); const listener = (event: Event) => { const payload = JSON.parse((event as MessageEvent).data) as T; handler(payload); }; source.addEventListener(eventName, listener); return () => { source.removeEventListener(eventName, listener); }; }