// 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());
})();