// Clawmates Live — client adapter // -------------------------------- // Drop-in: include this in any visualization page. It connects to the live bridge and // dispatches typed events. If no live URL is provided, it does NOTHING and the page keeps // its built-in synthetic animation (safe fallback). // // // // Live URL resolution order: // 1. window.CLAWMATES_LIVE_URL // 2. ?live= query param // 3. (none) → inert // // Usage from a visualization's logic: // ClawmatesLive.on('world.touch', e => worldEngine.touch(e.agentId, e.nodeId)); // ClawmatesLive.on('telemetry', e => setTopbar(e)); // Handlers registered AFTER events have arrived still receive the last replayed value // for stateful event types (status/task/telemetry/topology/routine/node). (function () { const TYPES = [ 'agent.status', 'agent.task.update', 'agent.reasoning.delta', 'agent.computer.frame', 'agent.tool.call', 'door.request', 'door.resolve', 'agent.message', 'world.touch', 'node.activity', 'topology.update', 'telemetry', 'routine.update', ]; const STATEFUL = new Set(['agent.status', 'agent.task.update', 'node.activity', 'telemetry', 'topology.update', 'routine.update']); const handlers = {}; // type → Set const last = {}; // stateKey → payload (replay buffer) TYPES.forEach(t => (handlers[t] = new Set())); function stateKey(type, d) { if (type === 'agent.status' || type === 'agent.task.update') return type + ':' + d.agentId; if (type === 'node.activity') return type + ':' + d.nodeId; if (type === 'routine.update') return type + ':' + d.routineId; return type; // telemetry, topology.update } function dispatch(type, data) { if (STATEFUL.has(type)) last[stateKey(type, data)] = data; (handlers[type] || []).forEach(fn => { try { fn(data); } catch (e) { console.warn('[ClawmatesLive]', type, e); } }); try { window.dispatchEvent(new CustomEvent('clawmates:' + type, { detail: data })); } catch {} } function resolveUrl() { if (window.CLAWMATES_LIVE_URL) return window.CLAWMATES_LIVE_URL; try { return new URLSearchParams(location.search).get('live') || null; } catch { return null; } } const API = { connected: false, url: null, on(type, fn) { if (!handlers[type]) handlers[type] = new Set(); handlers[type].add(fn); // replay last-known stateful value so late subscribers paint immediately for (const k in last) if (k === type || k.startsWith(type + ':')) { try { fn(last[k]); } catch {} } return () => handlers[type].delete(fn); }, off(type, fn) { handlers[type] && handlers[type].delete(fn); }, connect(url) { url = url || resolveUrl(); if (!url) { console.info('[ClawmatesLive] no live URL — running in synthetic/offline mode'); return; } this.url = url; const es = new EventSource(url); es.onopen = () => { this.connected = true; window.dispatchEvent(new CustomEvent('clawmates:open')); }; es.onerror = () => { this.connected = false; window.dispatchEvent(new CustomEvent('clawmates:reconnecting')); }; TYPES.forEach(type => es.addEventListener(type, ev => { let data; try { data = JSON.parse(ev.data); } catch { return; } dispatch(type, data); })); this._es = es; }, disconnect() { if (this._es) { this._es.close(); this._es = null; this.connected = false; } }, }; window.ClawmatesLive = API; // auto-connect on load if a URL is resolvable if (document.readyState !== 'loading') API.connect(); else window.addEventListener('DOMContentLoaded', () => API.connect()); })();