/** * API Client — all fetch calls + EventSource wrapper */ const BASE = '/api/v1'; class ApiClient { constructor() { this._eventSources = new Map(); this._bus = new EventTarget(); } // --- Event bus --- on(event, fn) { this._bus.addEventListener(event, fn); } off(event, fn) { this._bus.removeEventListener(event, fn); } _emit(event, detail) { this._bus.dispatchEvent(new CustomEvent(event, { detail })); } // --- HTTP helpers --- async _fetch(path, opts = {}) { try { const res = await fetch(`${BASE}${path}`, { headers: { 'Content-Type': 'application/json', ...opts.headers }, ...opts, }); if (!res.ok) { const err = await res.json().catch(() => ({ detail: res.statusText })); throw new Error(err.detail || `HTTP ${res.status}`); } return res.json(); } catch (e) { this._emit('error', { message: e.message, path }); throw e; } } // --- Health --- async checkHealth() { try { const data = await fetch('/health').then(r => r.json()); this._emit('health', data); return data; } catch { this._emit('health', { status: 'disconnected' }); return { status: 'disconnected' }; } } // --- Sessions --- async createSession(body) { const data = await this._fetch('/sessions', { method: 'POST', body: JSON.stringify(body), }); this._emit('session:created', data); return data; } async getSession(sessionId) { const data = await this._fetch(`/sessions/${sessionId}`); this._emit('session:state', data); return data; } async deleteSession(sessionId) { const data = await this._fetch(`/sessions/${sessionId}`, { method: 'DELETE' }); this._emit('session:deleted', { session_id: sessionId }); return data; } async getEvents(sessionId) { return this._fetch(`/sessions/${sessionId}/events`); } // --- Messages --- async sendMessage(sessionId, message, stream = false) { return this._fetch(`/sessions/${sessionId}/messages`, { method: 'POST', body: JSON.stringify({ message, stream }), }); } // --- SSE --- subscribeSSE(sessionId) { this.unsubscribeSSE(sessionId); const url = `${BASE}/sessions/${sessionId}/stream`; const es = new EventSource(url); const eventTypes = [ 'session.created', 'execution.started', 'agent.delta', 'tool.started', 'tool.completed', 'subagent.assigned', 'execution.completed', 'error', 'keepalive', ]; for (const type of eventTypes) { es.addEventListener(type, (e) => { try { const payload = JSON.parse(e.data); this._emit('sse', { type, ...payload }); this._emit(`sse:${type}`, payload); } catch { this._emit('sse', { type, raw: e.data }); } }); } es.onerror = () => { this._emit('sse:error', { sessionId }); }; this._eventSources.set(sessionId, es); return es; } unsubscribeSSE(sessionId) { const es = this._eventSources.get(sessionId); if (es) { es.close(); this._eventSources.delete(sessionId); } } unsubscribeAll() { for (const [id, es] of this._eventSources) { es.close(); } this._eventSources.clear(); } } export const api = new ApiClient();