Files
clawmates/handoff/realtime/clawmates-live.js
T
Omar SobhandClaude Opus 4.8 39bacbd1d2 World page: live WebGL Gource-style visualization (Phases A+B)
Replaces the static React-Flow world forest with a real-time WebGL view of the
swarm working, plus the live event contract that feeds it.

Phase A — the live contract + feed:
- frontend/src/lib/live/taxonomy.ts — the 13-event Clawmates Event Taxonomy as
  typed TS (the shared World/Observe contract).
- frontend/src/lib/live/useClawmatesLive.ts — native shared-singleton live client
  (EventSource to /api/world/live, typed fan-out, replay-of-last-state to late
  subscribers, refcounted, synthetic fallback so views are always alive).
- crates/cm-api/src/routes/world.rs — GET /api/world/live, authed + workspace-
  scoped SSE emitting the taxonomy (real agent.status from live-container state,
  a topology.update of the workspace's agents, telemetry with real doorsPending),
  mirroring run_events_sse. normalize() seam documented for run_events->world.touch.

Phase B — the WebGL engine:
- frontend/src/components/world/engine.ts — Gource-inspired force-directed model:
  org/company/team tree (sibling repulsion + parent spring + friction), agent
  pawns that converge on the touched node and beam it, world nodes that glow with
  heat and fade when idle. Framework-agnostic (renderer-independent) state+math.
- frontend/src/components/world/WorldCanvas.tsx — three.js scene (ortho cam,
  UnrealBloom), render loop syncing engine state, camera auto-fit, HTML labels,
  raycast click->select, Hierarchy/Flat/Live formation switch.
- Dashboard.tsx: swap <WorldFlow/> -> <WorldCanvas/> at the world-tier seam
  (shared claw-tier pieces untouched; WorldFlow.tsx kept for now).
- Adds three (+ @types/three).

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
2026-06-23 21:23:34 -07:00

82 lines
3.6 KiB
JavaScript

// 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).
//
// <script src="clawmates-live.js"></script>
//
// Live URL resolution order:
// 1. window.CLAWMATES_LIVE_URL
// 2. ?live=<url> 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<fn>
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());
})();