import { AGENT_DEFS, colorToCss } from './agent-defs.js'; import { setAgentState } from './agents.js'; import { appendChatMessage } from './ui.js'; /** * WebSocket URL resolution order: * 1. VITE_WS_URL env var (set at build time for custom deployments) * 2. Auto-derived from window.location — works when the Matrix is served from * the same host as the API (the default: /tower served by the API server) */ function resolveWsUrl() { const explicit = import.meta.env.VITE_WS_URL; if (explicit) return explicit; const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; return `${proto}//${window.location.host}/api/ws`; } const WS_URL = resolveWsUrl(); const agentById = Object.fromEntries(AGENT_DEFS.map(d => [d.id, d])); let ws = null; let connectionState = 'disconnected'; let jobCount = 0; let reconnectTimer = null; const RECONNECT_DELAY_MS = 5000; export function initWebSocket(_scene) { connect(); } function connect() { if (ws) { ws.onclose = null; ws.close(); } connectionState = 'connecting'; try { ws = new WebSocket(WS_URL); } catch { connectionState = 'disconnected'; scheduleReconnect(); return; } ws.onopen = () => { connectionState = 'connected'; clearTimeout(reconnectTimer); ws.send(JSON.stringify({ type: 'subscribe', channel: 'agents', clientId: crypto.randomUUID(), })); }; ws.onmessage = (event) => { try { handleMessage(JSON.parse(event.data)); } catch { } }; ws.onerror = () => { connectionState = 'disconnected'; }; ws.onclose = () => { connectionState = 'disconnected'; scheduleReconnect(); }; } function scheduleReconnect() { clearTimeout(reconnectTimer); reconnectTimer = setTimeout(connect, RECONNECT_DELAY_MS); } function handleMessage(msg) { switch (msg.type) { case 'ping': if (ws && ws.readyState === WebSocket.OPEN) { ws.send(JSON.stringify({ type: 'pong' })); } break; case 'agent_state': { if (msg.agentId && msg.state) { setAgentState(msg.agentId, msg.state); } break; } case 'job_started': { jobCount++; if (msg.agentId) setAgentState(msg.agentId, 'active'); logEvent(`JOB ${(msg.jobId || '').slice(0, 8)} ▶ started`); break; } case 'job_completed': { if (jobCount > 0) jobCount--; if (msg.agentId) setAgentState(msg.agentId, 'idle'); logEvent(`JOB ${(msg.jobId || '').slice(0, 8)} ✓ complete`); break; } case 'chat': { const def = agentById[msg.agentId]; if (def && msg.text) { appendChatMessage(def.label, msg.text, colorToCss(def.color), def.id); } break; } case 'agent_count': break; default: break; } } function logEvent(text) { appendChatMessage('SYS', text, '#003300', 'sys'); } export function getConnectionState() { return connectionState; } export function getJobCount() { return jobCount; }