Merge: World page live WebGL Gource visualization (Phases A+B)
ci / gates (push) Has been cancelled
ci / rust (push) Has been cancelled
ci / sandbox-k8s (push) Has been cancelled
ci / frontend (push) Has been cancelled
ci / e2e (push) Has been cancelled

This commit is contained in:
Omar Sobh
2026-06-23 21:23:35 -07:00
22 changed files with 5278 additions and 2 deletions
+1
View File
@@ -102,6 +102,7 @@ pub fn router(state: AppState) -> Router {
Router::new() Router::new()
.route("/healthz", get(routes::health::healthz)) .route("/healthz", get(routes::health::healthz))
.route("/api/quota", get(quota::get_quota)) .route("/api/quota", get(quota::get_quota))
.route("/api/world/live", get(routes::world::world_live))
.route("/mcp", post(mcp_door::mcp)) .route("/mcp", post(mcp_door::mcp))
.route("/api/auth/login", post(routes::auth::login)) .route("/api/auth/login", post(routes::auth::login))
.route("/api/auth/logout", post(routes::auth::logout)) .route("/api/auth/logout", post(routes::auth::logout))
+1
View File
@@ -23,3 +23,4 @@ pub mod team;
pub mod teams; pub mod teams;
pub mod terminal; pub mod terminal;
pub mod topology; pub mod topology;
pub mod world;
+118
View File
@@ -0,0 +1,118 @@
//! `GET /api/world/live` — the Clawmates Event Taxonomy as a Server-Sent Events
//! feed for the World + Observe visualizations. Read-only and workspace-scoped.
//!
//! Phase 1 emits the *real* shape of the workspace: a `topology.update` of its
//! agents, an `agent.status` per agent (working when it holds a live container,
//! else idle), and a `telemetry` snapshot — polled, mirroring `run_events_sse`.
//!
//! The richer `world.touch` / `agent.tool.call` events (an agent converging on
//! the node it acts upon — the Gource centerpiece) come from normalizing the
//! durable runner's `run_events`; that is the extension seam (see `normalize`),
//! filled in as the runner emits node targets. Until then the client's synthetic
//! fallback supplies that motion.
use std::collections::HashSet;
use std::convert::Infallible;
use std::time::Duration;
use axum::extract::State;
use axum::response::sse::{Event, KeepAlive, Sse};
use axum::response::IntoResponse;
use cm_domain::WorkspaceId;
use serde_json::{json, Value};
use sqlx::{PgPool, Row};
use crate::{AppState, Authed};
fn sse(event: &str, data: Value) -> Result<Event, Infallible> {
Ok(Event::default().event(event).data(data.to_string()))
}
/// Agents that currently hold a live container (any kind) → "working".
async fn working_agents(pool: &PgPool, ws: WorkspaceId) -> HashSet<String> {
let rows = sqlx::query(
"SELECT DISTINCT a.id::text AS id
FROM agents a JOIN agent_containers ac ON ac.agent_id = a.id
WHERE a.workspace_id = $1",
)
.bind(ws.as_uuid())
.fetch_all(pool)
.await
.unwrap_or_default();
rows.into_iter().map(|r| r.get::<String, _>("id")).collect()
}
/// Count of doors (approvals) awaiting a decision in the workspace.
async fn doors_pending(pool: &PgPool, ws: WorkspaceId) -> i64 {
sqlx::query_scalar::<_, i64>(
"SELECT count(*) FROM approvals WHERE workspace_id = $1 AND status = 'pending'",
)
.bind(ws.as_uuid())
.fetch_one(pool)
.await
.unwrap_or(0)
}
/// `GET /api/world/live` — the taxonomy SSE feed.
pub async fn world_live(State(state): State<AppState>, Authed(user): Authed) -> impl IntoResponse {
let pool = state.pool.clone();
let ws = user.workspace_id;
let stream = async_stream::stream! {
let mut first = true;
// Remember last status per agent so we only push deltas after the seed.
let mut last: std::collections::HashMap<String, String> = std::collections::HashMap::new();
loop {
let roster = match cm_db::repo::agents::roster(&pool, ws).await {
Ok(r) => r,
Err(_) => break,
};
let working = working_agents(&pool, ws).await;
if first {
// Seed the world graph with the workspace's agents as nodes.
let nodes: Vec<Value> = roster
.iter()
.map(|a| json!({ "id": a.id.to_string(), "tier": "agent", "label": a.name }))
.collect();
yield sse("topology.update", json!({ "formation": "live", "nodes": nodes }));
}
for a in &roster {
let id = a.id.to_string();
let status = if working.contains(&id) { "working" } else { "idle" };
if last.get(&id).map(|s| s != status).unwrap_or(true) {
last.insert(id.clone(), status.to_string());
yield sse(
"agent.status",
json!({ "agentId": id, "status": status, "role": a.job_title }),
);
}
}
yield sse(
"telemetry",
json!({ "doorsPending": doors_pending(&pool, ws).await }),
);
first = false;
tokio::time::sleep(Duration::from_secs(2)).await;
}
};
Sse::new(stream).keep_alive(KeepAlive::default())
}
// THE NORMALIZE SEAM (future) -------------------------------------------------
// Translate one durable `run_events` row into zero+ taxonomy events, the Rust
// twin of the handoff bridge's normalize(). Wire this into the poll loop above
// once the runner emits node targets:
// turn.started -> agent.status(working) [+ agent.task.update]
// turn.token -> agent.reasoning.delta
// tool.invoked -> agent.tool.call [+ world.touch if a nodeId is present]
// door.requested -> door.request ; door.resolved -> door.resolve
// agent.message -> agent.message ; runner.telemetry -> telemetry
#[allow(dead_code)]
fn normalize(_event_type: &str, _payload: &Value) -> Vec<(&'static str, Value)> {
Vec::new()
}
+65
View File
@@ -22,6 +22,7 @@
"react": "19.2.4", "react": "19.2.4",
"react-dom": "19.2.4", "react-dom": "19.2.4",
"tailwind-merge": "^3.6.0", "tailwind-merge": "^3.6.0",
"three": "^0.169.0",
"zod": "^4.4.3" "zod": "^4.4.3"
}, },
"devDependencies": { "devDependencies": {
@@ -34,6 +35,7 @@
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"@types/three": "^0.169.0",
"@vitejs/plugin-react": "^6.0.2", "@vitejs/plugin-react": "^6.0.2",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "16.2.9", "eslint-config-next": "16.2.9",
@@ -2112,6 +2114,13 @@
"@testing-library/dom": ">=7.21.4" "@testing-library/dom": ">=7.21.4"
} }
}, },
"node_modules/@tweenjs/tween.js": {
"version": "23.1.3",
"resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
"integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
"dev": true,
"license": "MIT"
},
"node_modules/@tybys/wasm-util": { "node_modules/@tybys/wasm-util": {
"version": "0.10.2", "version": "0.10.2",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz",
@@ -2249,6 +2258,35 @@
"@types/react": "^19.2.0" "@types/react": "^19.2.0"
} }
}, },
"node_modules/@types/stats.js": {
"version": "0.17.4",
"resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
"integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/three": {
"version": "0.169.0",
"resolved": "https://registry.npmjs.org/@types/three/-/three-0.169.0.tgz",
"integrity": "sha512-oan7qCgJBt03wIaK+4xPWclYRPG9wzcg7Z2f5T8xYTNEF95kh0t0lklxLLYBDo7gQiGLYzE6iF4ta7nXF2bcsw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@tweenjs/tween.js": "~23.1.3",
"@types/stats.js": "*",
"@types/webxr": "*",
"@webgpu/types": "*",
"fflate": "~0.8.2",
"meshoptimizer": "~0.18.1"
}
},
"node_modules/@types/webxr": {
"version": "0.5.24",
"resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz",
"integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/whatwg-mimetype": { "node_modules/@types/whatwg-mimetype": {
"version": "3.0.2", "version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz", "resolved": "https://registry.npmjs.org/@types/whatwg-mimetype/-/whatwg-mimetype-3.0.2.tgz",
@@ -3031,6 +3069,13 @@
"url": "https://opencollective.com/vitest" "url": "https://opencollective.com/vitest"
} }
}, },
"node_modules/@webgpu/types": {
"version": "0.1.70",
"resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.70.tgz",
"integrity": "sha512-LFiNHHKMvmAEvwVew3JLJmTdShhbdwRFSImUshGhE2mGE8ybQzIo63l5uRp+YKnNx+8Qno8Kf6gN+DKMreIJCA==",
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/@xterm/addon-fit": { "node_modules/@xterm/addon-fit": {
"version": "0.10.0", "version": "0.10.0",
"resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz", "resolved": "https://registry.npmjs.org/@xterm/addon-fit/-/addon-fit-0.10.0.tgz",
@@ -4711,6 +4756,13 @@
"reusify": "^1.0.4" "reusify": "^1.0.4"
} }
}, },
"node_modules/fflate": {
"version": "0.8.3",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz",
"integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
"dev": true,
"license": "MIT"
},
"node_modules/file-entry-cache": { "node_modules/file-entry-cache": {
"version": "8.0.0", "version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
@@ -6167,6 +6219,13 @@
"node": ">= 8" "node": ">= 8"
} }
}, },
"node_modules/meshoptimizer": {
"version": "0.18.1",
"resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-0.18.1.tgz",
"integrity": "sha512-ZhoIoL7TNV4s5B6+rx5mC//fw8/POGyNxS/DZyCJeiZ12ScLfVwRE/GfsxwiTkMYYD5DmK2/JXnEVXqL4rF+Sw==",
"dev": true,
"license": "MIT"
},
"node_modules/micromatch": { "node_modules/micromatch": {
"version": "4.0.8", "version": "4.0.8",
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
@@ -7630,6 +7689,12 @@
"url": "https://opencollective.com/webpack" "url": "https://opencollective.com/webpack"
} }
}, },
"node_modules/three": {
"version": "0.169.0",
"resolved": "https://registry.npmjs.org/three/-/three-0.169.0.tgz",
"integrity": "sha512-Ed906MA3dR4TS5riErd4QBsRGPcx+HBDX2O5yYE5GqJeFQTPU+M56Va/f/Oph9X7uZo3W3o4l2ZhBZ6f6qUv0w==",
"license": "MIT"
},
"node_modules/tinybench": { "node_modules/tinybench": {
"version": "2.9.0", "version": "2.9.0",
"resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz",
+2
View File
@@ -25,6 +25,7 @@
"react": "19.2.4", "react": "19.2.4",
"react-dom": "19.2.4", "react-dom": "19.2.4",
"tailwind-merge": "^3.6.0", "tailwind-merge": "^3.6.0",
"three": "^0.169.0",
"zod": "^4.4.3" "zod": "^4.4.3"
}, },
"devDependencies": { "devDependencies": {
@@ -37,6 +38,7 @@
"@types/node": "^20", "@types/node": "^20",
"@types/react": "^19", "@types/react": "^19",
"@types/react-dom": "^19", "@types/react-dom": "^19",
"@types/three": "^0.169.0",
"@vitejs/plugin-react": "^6.0.2", "@vitejs/plugin-react": "^6.0.2",
"eslint": "^9", "eslint": "^9",
"eslint-config-next": "16.2.9", "eslint-config-next": "16.2.9",
@@ -20,7 +20,7 @@ import type { Agent } from "@/lib/api/schemas";
import { topologyById } from "@/lib/topologies"; import { topologyById } from "@/lib/topologies";
import { panelParsers, type DeviceSize } from "@/lib/url/panel-params"; import { panelParsers, type DeviceSize } from "@/lib/url/panel-params";
import { type FlowItem } from "./flow/TopologyFlow"; import { type FlowItem } from "./flow/TopologyFlow";
import { WorldFlow } from "./flow/WorldFlow"; import { WorldCanvas } from "../world/WorldCanvas";
import { StructureTree, orgNode, clawNode, type TreeItem } from "./StructureTree"; import { StructureTree, orgNode, clawNode, type TreeItem } from "./StructureTree";
import { UserMenu } from "./UserMenu"; import { UserMenu } from "./UserMenu";
import { ToolPanel, type ToolKey } from "./ToolPanel"; import { ToolPanel, type ToolKey } from "./ToolPanel";
@@ -571,7 +571,7 @@ export function Dashboard({ user, orgs, claws }: { user?: { display_name?: strin
<> <>
{/* Graph stage — condensed by the right slide-out's width. */} {/* Graph stage — condensed by the right slide-out's width. */}
<div style={{ position: "absolute", top: 0, bottom: 0, left: 0, right: "var(--world-width, 0px)", background: "radial-gradient(120% 90% at 55% 38%, #0e0e13 0%, #08080a 70%)", transition: "right var(--duration-normal) var(--ease-app)" }}> <div style={{ position: "absolute", top: 0, bottom: 0, left: 0, right: "var(--world-width, 0px)", background: "radial-gradient(120% 90% at 55% 38%, #0e0e13 0%, #08080a 70%)", transition: "right var(--duration-normal) var(--ease-app)" }}>
<WorldFlow roots={worldRoots} expanded={expanded} selectedId={worldSel} onToggleExpand={toggleExpand} onSelect={onWorldSelect} onOpenRuns={() => setRunsOpen(true)} /> <WorldCanvas roots={worldRoots} selectedId={worldSel} onSelect={onWorldSelect} onOpenRuns={() => setRunsOpen(true)} />
{/* Open the slide-out (top-right) when it's closed. */} {/* Open the slide-out (top-right) when it's closed. */}
{!worldPanelOpen ? ( {!worldPanelOpen ? (
<button type="button" aria-label="Open panel" title="Panel" onClick={() => setWorldPanelOpen(true)} style={{ position: "absolute", top: 14, right: 16, zIndex: 50, width: 38, height: 38, borderRadius: "50%", border: "1px solid rgba(255,111,97,.4)", background: "rgba(255,111,97,.08)", color: "#ff6f61", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center" }}><PanelRight aria-hidden size={19} /></button> <button type="button" aria-label="Open panel" title="Panel" onClick={() => setWorldPanelOpen(true)} style={{ position: "absolute", top: 14, right: 16, zIndex: 50, width: 38, height: 38, borderRadius: "50%", border: "1px solid rgba(255,111,97,.4)", background: "rgba(255,111,97,.08)", color: "#ff6f61", cursor: "pointer", display: "flex", alignItems: "center", justifyContent: "center" }}><PanelRight aria-hidden size={19} /></button>
@@ -0,0 +1,382 @@
"use client";
// The Large World — a Gource-inspired live visualization rendered in WebGL
// (three.js). The org▸company▸team structure is a force-directed tree; agents are
// glowing "pawns" that converge on the node they touch and beam it; world nodes
// glow with heat and fade when idle. Driven by the live taxonomy feed (with a
// synthetic fallback) so it breathes with real agent activity. Replaces WorldFlow.
import { useEffect, useRef, useState } from "react";
import * as THREE from "three";
import { EffectComposer } from "three/examples/jsm/postprocessing/EffectComposer.js";
import { RenderPass } from "three/examples/jsm/postprocessing/RenderPass.js";
import { UnrealBloomPass } from "three/examples/jsm/postprocessing/UnrealBloomPass.js";
import { useClawmatesLive } from "@/lib/live/useClawmatesLive";
import { WorldEngine, type Formation, type WorldSeed } from "./engine";
interface WorldCanvasProps {
roots: WorldSeed[];
selectedId: string | null;
onSelect: (id: string) => void;
expanded?: Set<string>;
onToggleExpand?: (id: string) => void;
onOpenRuns?: () => void;
}
const FORMATIONS: { id: Formation; label: string }[] = [
{ id: "hierarchy", label: "Hierarchy" },
{ id: "flat", label: "Flat" },
{ id: "live", label: "Live" },
];
const mono = "'Geist Mono', ui-monospace, monospace";
export function WorldCanvas({ roots, selectedId, onSelect }: WorldCanvasProps) {
const mountRef = useRef<HTMLDivElement>(null);
const labelLayerRef = useRef<HTMLDivElement>(null);
const live = useClawmatesLive();
const [formation, setFormation] = useState<Formation>("live");
// latest-prop refs so the imperative loop sees current values without re-init
const onSelectRef = useRef(onSelect);
const selectedRef = useRef(selectedId);
const engineRef = useRef<WorldEngine | null>(null);
const formationRef = useRef(formation);
useEffect(() => {
onSelectRef.current = onSelect;
selectedRef.current = selectedId;
formationRef.current = formation;
});
// seed the engine once from the structure roots
useEffect(() => {
const engine = new WorldEngine();
engine.seed(roots);
engineRef.current = engine;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
// wire the live feed into the engine
useEffect(() => {
const e = engineRef.current;
if (!e) return;
const offs = [
live.on("topology.update", (d) => e.onTopology(d)),
live.on("agent.status", (d) => e.onStatus(d)),
live.on("world.touch", (d) => e.onTouch(d)),
live.on("node.activity", (d) => e.onNodeActivity(d)),
];
return () => offs.forEach((off) => off());
}, [live]);
// the three.js scene + render loop
useEffect(() => {
const mount = mountRef.current;
const labelLayer = labelLayerRef.current;
const engine = engineRef.current;
if (!mount || !labelLayer || !engine) return;
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setPixelRatio(Math.min(2, window.devicePixelRatio || 1));
mount.appendChild(renderer.domElement);
renderer.domElement.style.display = "block";
const scene = new THREE.Scene();
const camera = new THREE.OrthographicCamera(-100, 100, 100, -100, 0.1, 1000);
camera.position.set(0, 0, 10);
const circle = new THREE.CircleGeometry(1, 28);
const nodeMeshes = new Map<string, THREE.Mesh>();
const pawnMeshes = new Map<string, THREE.Mesh>();
const labels = new Map<string, HTMLSpanElement>();
const edgeGeom = new THREE.BufferGeometry();
const edges = new THREE.LineSegments(
edgeGeom,
new THREE.LineBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.12 }),
);
scene.add(edges);
const beamGeom = new THREE.BufferGeometry();
const beams = new THREE.LineSegments(
beamGeom,
new THREE.LineBasicMaterial({ vertexColors: true, transparent: true, opacity: 0.9 }),
);
scene.add(beams);
const composer = new EffectComposer(renderer);
composer.addPass(new RenderPass(scene, camera));
composer.addPass(
new UnrealBloomPass(new THREE.Vector2(1, 1), /*strength*/ 0.9, /*radius*/ 0.5, /*threshold*/ 0.5),
);
const setSize = () => {
const w = mount.clientWidth || 1;
const h = mount.clientHeight || 1;
renderer.setSize(w, h);
composer.setSize(w, h);
};
setSize();
const ro = new ResizeObserver(setSize);
ro.observe(mount);
// smoothed camera fit
let viewCx = 0;
let viewCy = 0;
let viewHalf = 240;
// click → select nearest node/pawn (unproject pointer to world)
const onClick = (ev: MouseEvent) => {
const rect = renderer.domElement.getBoundingClientRect();
const ndc = new THREE.Vector3(
((ev.clientX - rect.left) / rect.width) * 2 - 1,
-((ev.clientY - rect.top) / rect.height) * 2 + 1,
0,
).unproject(camera);
let best: string | null = null;
let bestD = Infinity;
for (const n of engine.nodes.values()) {
if (n.tier === "root") continue;
const d = Math.hypot(n.x - ndc.x, n.y - ndc.y);
if (d < n.r + 10 && d < bestD) {
bestD = d;
best = n.id;
}
}
for (const p of engine.pawns.values()) {
const d = Math.hypot(p.x - ndc.x, p.y - ndc.y);
if (d < 16 && d < bestD) {
bestD = d;
best = p.id;
}
}
if (best) onSelectRef.current(best);
};
renderer.domElement.addEventListener("click", onClick);
const col = new THREE.Color();
let raf = 0;
let lastT = performance.now();
const frame = () => {
raf = requestAnimationFrame(frame);
if (document.hidden) return;
const now = performance.now();
const dt = Math.min(0.05, (now - lastT) / 1000);
lastT = now;
engine.formation = formationRef.current;
engine.step(dt);
// --- nodes ---
const liveNodeIds = new Set<string>();
let minX = Infinity;
let minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
for (const n of engine.nodes.values()) {
liveNodeIds.add(n.id);
let m = nodeMeshes.get(n.id);
if (!m) {
m = new THREE.Mesh(circle, new THREE.MeshBasicMaterial({ transparent: true }));
nodeMeshes.set(n.id, m);
scene.add(m);
}
const r = n.r * (1 + n.heat * 0.4);
m.position.set(n.x, n.y, 0);
m.scale.set(r, r, 1);
const mat = m.material as THREE.MeshBasicMaterial;
col.set(n.color).lerp(new THREE.Color(0xffffff), n.heat * 0.6);
mat.color.copy(col);
mat.opacity = n.alpha * (n.tier === "root" ? 1 : 0.55 + n.heat * 0.45);
if (n.tier !== "root") {
minX = Math.min(minX, n.x - r);
minY = Math.min(minY, n.y - r);
maxX = Math.max(maxX, n.x + r);
maxY = Math.max(maxY, n.y + r);
}
}
for (const [id, m] of nodeMeshes) {
if (!liveNodeIds.has(id)) {
scene.remove(m);
(m.material as THREE.Material).dispose();
nodeMeshes.delete(id);
}
}
// --- edges (child → parent) ---
const epos: number[] = [];
for (const n of engine.nodes.values()) {
if (!n.parentId) continue;
const p = engine.nodes.get(n.parentId);
if (!p) continue;
epos.push(n.x, n.y, 0, p.x, p.y, 0);
}
edgeGeom.setAttribute("position", new THREE.Float32BufferAttribute(epos, 3));
edgeGeom.attributes.position.needsUpdate = true;
edgeGeom.setDrawRange(0, epos.length / 3);
// --- pawns ---
const livePawnIds = new Set<string>();
for (const p of engine.pawns.values()) {
livePawnIds.add(p.id);
let m = pawnMeshes.get(p.id);
if (!m) {
m = new THREE.Mesh(circle, new THREE.MeshBasicMaterial({ transparent: true }));
pawnMeshes.set(p.id, m);
scene.add(m);
}
m.position.set(p.x, p.y, 1);
m.scale.set(9, 9, 1);
const mat = m.material as THREE.MeshBasicMaterial;
mat.color.set(p.color);
mat.opacity = p.alpha;
minX = Math.min(minX, p.x - 9);
minY = Math.min(minY, p.y - 9);
maxX = Math.max(maxX, p.x + 9);
maxY = Math.max(maxY, p.y + 9);
}
for (const [id, m] of pawnMeshes) {
if (!livePawnIds.has(id)) {
scene.remove(m);
(m.material as THREE.Material).dispose();
pawnMeshes.delete(id);
}
}
// --- beams ---
const bpos: number[] = [];
const bcol: number[] = [];
for (const b of engine.beams) {
col.set(b.color);
bpos.push(b.x1, b.y1, 0.5, b.x2, b.y2, 0.5);
bcol.push(col.r, col.g, col.b, col.r, col.g, col.b);
}
beamGeom.setAttribute("position", new THREE.Float32BufferAttribute(bpos, 3));
beamGeom.setAttribute("color", new THREE.Float32BufferAttribute(bcol, 3));
beamGeom.attributes.position.needsUpdate = true;
beamGeom.setDrawRange(0, bpos.length / 3);
// --- camera fit (smoothed) ---
if (minX < maxX) {
const cx = (minX + maxX) / 2;
const cy = (minY + maxY) / 2;
const aspect = (mount.clientWidth || 1) / (mount.clientHeight || 1);
const half = Math.max((maxX - minX) / 2, (maxY - minY) / 2 * aspect) * 1.18 + 40;
viewCx += (cx - viewCx) * 0.06;
viewCy += (cy - viewCy) * 0.06;
viewHalf += (half - viewHalf) * 0.06;
const vh = viewHalf / aspect;
camera.left = -viewHalf;
camera.right = viewHalf;
camera.top = vh;
camera.bottom = -vh;
camera.position.set(viewCx, viewCy, 10);
camera.updateProjectionMatrix();
}
// --- labels (structure nodes + hot world nodes + pawns + selected) ---
const wantLabels = new Map<string, { x: number; y: number; text: string; color: string; big: boolean }>();
for (const n of engine.nodes.values()) {
const struct = n.tier === "org" || n.tier === "company" || n.tier === "team";
const hot = (n.tier === "service" || n.tier === "event") && (n.heat > 0.12 || n.id === selectedRef.current);
if (struct || hot)
wantLabels.set(n.id, { x: n.x, y: n.y - n.r - 6, text: n.label, color: n.color, big: n.tier === "org" });
}
for (const p of engine.pawns.values())
wantLabels.set(p.id, { x: p.x, y: p.y, text: p.label, color: "#0a0a0a", big: false });
const W = mount.clientWidth || 1;
const H = mount.clientHeight || 1;
const v = new THREE.Vector3();
for (const [id, info] of wantLabels) {
let span = labels.get(id);
if (!span) {
span = document.createElement("span");
span.style.cssText = `position:absolute;transform:translate(-50%,-50%);font-family:${mono};white-space:nowrap;pointer-events:none;text-shadow:0 1px 3px rgba(0,0,0,.8)`;
labelLayer.appendChild(span);
labels.set(id, span);
}
v.set(info.x, info.y, 0).project(camera);
span.textContent = info.text;
span.style.left = `${(v.x * 0.5 + 0.5) * W}px`;
span.style.top = `${(-v.y * 0.5 + 0.5) * H}px`;
span.style.color = info.color;
span.style.fontSize = info.big ? "12px" : "10px";
span.style.fontWeight = id === selectedRef.current ? "700" : "500";
span.style.opacity = v.z > 1 ? "0" : "1";
}
for (const [id, span] of labels) {
if (!wantLabels.has(id)) {
span.remove();
labels.delete(id);
}
}
composer.render();
};
raf = requestAnimationFrame(frame);
return () => {
cancelAnimationFrame(raf);
ro.disconnect();
renderer.domElement.removeEventListener("click", onClick);
labels.forEach((s) => s.remove());
nodeMeshes.forEach((m) => (m.material as THREE.Material).dispose());
pawnMeshes.forEach((m) => (m.material as THREE.Material).dispose());
circle.dispose();
edgeGeom.dispose();
beamGeom.dispose();
composer.dispose();
renderer.dispose();
if (renderer.domElement.parentNode) renderer.domElement.parentNode.removeChild(renderer.domElement);
};
}, []);
return (
<div style={{ position: "absolute", inset: 0 }}>
<div ref={mountRef} style={{ position: "absolute", inset: 0 }} />
<div ref={labelLayerRef} style={{ position: "absolute", inset: 0, overflow: "hidden", pointerEvents: "none" }} />
{/* Formation switch (Gource-style camera/layout modes). */}
<div
style={{
position: "absolute",
top: 14,
left: 16,
zIndex: 30,
display: "flex",
gap: 4,
padding: 4,
borderRadius: 10,
background: "rgba(10,10,14,.7)",
border: "1px solid rgba(255,255,255,.08)",
backdropFilter: "blur(8px)",
}}
>
{FORMATIONS.map((f) => (
<button
key={f.id}
type="button"
onClick={() => setFormation(f.id)}
style={{
fontFamily: mono,
fontSize: 11,
letterSpacing: ".04em",
padding: "5px 11px",
borderRadius: 7,
border: "1px solid transparent",
cursor: "pointer",
color: formation === f.id ? "#fff" : "#8a8a92",
background: formation === f.id ? "rgba(255,111,97,.18)" : "transparent",
borderColor: formation === f.id ? "rgba(255,111,97,.4)" : "transparent",
}}
>
{f.label}
</button>
))}
</div>
</div>
);
}
export default WorldCanvas;
+317
View File
@@ -0,0 +1,317 @@
// The World engine — a Gource-inspired, framework-agnostic force-directed model.
// Structure (org ▸ company ▸ team) are physics nodes (branches); agents are
// "pawns" (Gource contributors) homed at their team that converge on the world
// node they touch and beam it; world nodes (services/events) appear on first
// touch and fade when idle (Gource's file-idle). WorldCanvas renders this state
// into three.js; the engine itself holds only math + state so it's testable and
// renderer-independent.
import type { TaxonomyEvents } from "@/lib/live/taxonomy";
export type Formation = "hierarchy" | "flat" | "live";
export type Tier = "root" | "org" | "company" | "team" | "service" | "event";
export interface GNode {
id: string;
tier: Tier;
label: string;
parentId: string | null;
depth: number;
x: number;
y: number;
vx: number;
vy: number;
r: number;
color: string;
heat: number; // 0..1, decays — drives glow/pulse
alpha: number; // fade in/out (world nodes)
lastSeen: number;
fixed?: boolean;
}
export interface GPawn {
id: string;
label: string;
color: string;
x: number;
y: number;
vx: number;
vy: number;
homeId: string | null;
targetId: string | null;
status: string;
idle: number;
alpha: number;
retime: number;
}
export interface Beam {
x1: number;
y1: number;
x2: number;
y2: number;
color: string;
life: number;
}
export interface WorldSeed {
id: string;
level: string; // "org" | "company" | "team" | "claw"
label: string;
status?: string;
children?: WorldSeed[];
}
const LEVEL_COLOR: Record<string, string> = {
org: "#c98af0",
company: "#8a9af0",
team: "#6fd0c0",
service: "#5ec8d8",
event: "#ff8a7a",
root: "#ff6f61",
};
const PAWN_COLORS = ["#ff5f57", "#4aa3b8", "#5fd08a", "#e8b465", "#c98af0", "#8a9af0"];
const ROOT = "__root__";
export class WorldEngine {
nodes = new Map<string, GNode>();
pawns = new Map<string, GPawn>();
beams: Beam[] = [];
formation: Formation = "live";
private homes = new Map<string, string>(); // agentId → teamId
private now = 0;
private pawnIdx = 0;
constructor() {
this.nodes.set(ROOT, {
id: ROOT,
tier: "root",
label: "",
parentId: null,
depth: 0,
x: 0,
y: 0,
vx: 0,
vy: 0,
r: 16,
color: LEVEL_COLOR.root,
heat: 0,
alpha: 1,
lastSeen: 0,
fixed: true,
});
}
/** Build the structure tree from the world roots; claws become pawns. */
seed(roots: WorldSeed[]) {
const walk = (item: WorldSeed, parentId: string, depth: number) => {
if (item.level === "claw" || item.level === "agent") {
this.homes.set(item.id, parentId);
this.ensurePawn(item.id, item.label, item.status, parentId);
return;
}
this.ensureNode(item.id, item.level as Tier, item.label, parentId, depth);
(item.children ?? []).forEach((c) => walk(c, item.id, depth + 1));
};
roots.forEach((r) => walk(r, ROOT, 1));
}
ensureNode(id: string, tier: Tier, label: string, parentId: string | null, depth: number): GNode {
let n = this.nodes.get(id);
if (!n) {
const parent = parentId ? this.nodes.get(parentId) : undefined;
const ang = this.nodes.size * 2.399; // golden angle spread
const rad = 60 + depth * 60;
n = {
id,
tier,
label,
parentId: parentId ?? ROOT,
depth,
x: (parent?.x ?? 0) + Math.cos(ang) * rad * 0.4,
y: (parent?.y ?? 0) + Math.sin(ang) * rad * 0.4,
vx: 0,
vy: 0,
r: tier === "org" ? 15 : tier === "company" ? 12 : tier === "team" ? 9 : 7,
color: LEVEL_COLOR[tier] ?? "#9a9aa2",
heat: 0,
alpha: 1,
lastSeen: this.now,
};
this.nodes.set(id, n);
}
n.lastSeen = this.now;
return n;
}
ensurePawn(id: string, label?: string, status?: string, homeId?: string | null): GPawn {
let p = this.pawns.get(id);
if (!p) {
const home = homeId ? this.nodes.get(homeId) : undefined;
p = {
id,
label: (label ?? id).slice(0, 2).toUpperCase(),
color: PAWN_COLORS[this.pawnIdx++ % PAWN_COLORS.length],
x: (home?.x ?? 0) + (Math.random() - 0.5) * 40,
y: (home?.y ?? 0) + (Math.random() - 0.5) * 40,
vx: 0,
vy: 0,
homeId: homeId ?? this.homes.get(id) ?? null,
targetId: null,
status: status ?? "online",
idle: 0,
alpha: 1,
retime: 1 + Math.random() * 2,
};
this.pawns.set(id, p);
}
if (label) p.label = label.slice(0, 2).toUpperCase();
if (status) p.status = status;
if (homeId) p.homeId = homeId;
return p;
}
// --- taxonomy event handlers ---------------------------------------------
onTopology(e: TaxonomyEvents["topology.update"]) {
if (e.formation) this.formation = e.formation;
for (const nd of e.nodes ?? []) {
if (nd.tier === "service" || nd.tier === "event") {
this.ensureNode(nd.id, nd.tier, nd.label ?? nd.id, ROOT, 1);
}
}
}
onStatus(e: TaxonomyEvents["agent.status"]) {
this.ensurePawn(e.agentId, undefined, e.status, this.homes.get(e.agentId) ?? null);
}
onTouch(e: TaxonomyEvents["world.touch"]) {
const node = this.ensureNode(e.nodeId, e.kind === "event" ? "event" : "service", e.nodeId, ROOT, 1);
const p = this.ensurePawn(e.agentId);
p.targetId = e.nodeId;
p.idle = 0;
p.retime = 1.2 + Math.random() * 1.6;
node.heat = Math.min(1, node.heat + 0.5);
}
onNodeActivity(e: TaxonomyEvents["node.activity"]) {
const node = this.ensureNode(e.nodeId, e.kind === "event" ? "event" : "service", e.label ?? e.nodeId, ROOT, 1);
if (e.label) node.label = e.label;
if (e.heat != null) node.heat = Math.max(node.heat, e.heat);
}
// --- simulation -----------------------------------------------------------
step(dt: number) {
this.now += dt;
const flat = this.formation === "flat";
const springK = flat ? 0.03 : 0.14;
const repulse = flat ? 9000 : 5200;
const friction = 0.82;
const arr = [...this.nodes.values()];
// global charge repulsion (all pairs) — O(n²), fine for low hundreds
for (let i = 0; i < arr.length; i++) {
const a = arr[i];
if (a.fixed) continue;
for (let j = i + 1; j < arr.length; j++) {
const b = arr[j];
let dx = a.x - b.x;
let dy = a.y - b.y;
let d2 = dx * dx + dy * dy;
if (d2 < 1) {
d2 = 1;
dx = Math.random() - 0.5;
dy = Math.random() - 0.5;
}
const d = Math.sqrt(d2);
const f = Math.min(repulse / d2, 700);
const ux = dx / d;
const uy = dy / d;
a.vx += ux * f * dt;
a.vy += uy * f * dt;
if (!b.fixed) {
b.vx -= ux * f * dt;
b.vy -= uy * f * dt;
}
}
}
for (const n of arr) {
if (n.fixed) continue;
const parent = this.nodes.get(n.parentId ?? ROOT) ?? this.nodes.get(ROOT)!;
const dx = parent.x - n.x;
const dy = parent.y - n.y;
const d = Math.hypot(dx, dy) || 1;
const desired = parent.r + n.r + (n.tier === "org" ? 130 : n.tier === "company" ? 95 : 64);
const f = (d - desired) * springK;
n.vx += (dx / d) * f * dt * 4;
n.vy += (dy / d) * f * dt * 4;
n.vx *= friction;
n.vy *= friction;
n.x += n.vx;
n.y += n.vy;
n.heat = Math.max(0, n.heat - dt * 0.5);
}
// world nodes fade out when idle (Gource file-idle), then are removed
for (const n of arr) {
if (n.tier === "service" || n.tier === "event") {
const idleAge = this.now - n.lastSeen;
n.alpha = idleAge > 22 ? Math.max(0, n.alpha - dt * 0.5) : Math.min(1, n.alpha + dt * 2);
if (n.alpha <= 0.02) this.nodes.delete(n.id);
}
}
this.stepPawns(dt);
for (let i = this.beams.length - 1; i >= 0; i--) {
this.beams[i].life -= dt * 2.2;
if (this.beams[i].life <= 0) this.beams.splice(i, 1);
}
}
private stepPawns(dt: number) {
const pawns = [...this.pawns.values()];
const worldNodes = [...this.nodes.values()].filter((n) => n.tier === "service" || n.tier === "event");
for (const p of pawns) {
// keep alive offline: drift to a random world node when untargeted
p.retime -= dt;
if (!p.targetId && p.retime <= 0 && worldNodes.length) {
p.targetId = worldNodes[Math.floor(Math.random() * worldNodes.length)].id;
p.retime = 1.5 + Math.random() * 2.5;
}
const target = p.targetId ? this.nodes.get(p.targetId) : undefined;
const home = p.homeId ? this.nodes.get(p.homeId) : undefined;
const dest = target ?? home ?? this.nodes.get(ROOT)!;
const ang = this.now * 1.4 + (p.id.charCodeAt(0) || 0);
const ox = dest.x + Math.cos(ang) * (dest.r + 22);
const oy = dest.y + Math.sin(ang) * (dest.r + 22);
p.vx += (ox - p.x) * dt * 3;
p.vy += (oy - p.y) * dt * 3;
for (const q of pawns) {
if (q === p) continue;
const dx = p.x - q.x;
const dy = p.y - q.y;
const d2 = dx * dx + dy * dy;
if (d2 < 900 && d2 > 0.5) {
const d = Math.sqrt(d2);
const f = 300 / d2;
p.vx += (dx / d) * f * dt;
p.vy += (dy / d) * f * dt;
}
}
p.vx *= 0.8;
p.vy *= 0.8;
p.x += p.vx;
p.y += p.vy;
if (target) {
const d = Math.hypot(p.x - target.x, p.y - target.y);
if (d < target.r + 34) {
target.heat = Math.min(1, target.heat + dt * 1.5);
if (Math.random() < dt * 5)
this.beams.push({ x1: p.x, y1: p.y, x2: target.x, y2: target.y, color: p.color, life: 1 });
if (Math.random() < dt * 0.7) p.targetId = null; // arrived; release
}
}
p.idle += dt;
const dim = p.status === "idle" || p.status === "offline";
p.alpha = dim ? Math.max(0.25, p.alpha - dt * 0.3) : Math.min(1, p.alpha + dt * 2);
}
}
}
+101
View File
@@ -0,0 +1,101 @@
// The Clawmates Event Taxonomy — the stable contract between the live feed (the
// cm-api /api/world/live SSE) and the visualizations (World + Observe). Mirrors
// realtime/events.schema.json. The runner emits many internal events; the server
// normalizes them into this small, typed set; the browser only ever sees these.
export type AgentStatus = "online" | "working" | "idle" | "offline";
export interface TaxonomyEvents {
/** Status dots everywhere; the list rail. */
"agent.status": { agentId: string; status: AgentStatus; role?: string };
/** Observe → "WORKING ON NOW" card. */
"agent.task.update": {
agentId: string;
taskId: string;
title: string;
elapsedMs?: number;
steps?: { label: string; state: "done" | "active" | "pending" }[];
};
/** Observe → REASONING STREAM (append). */
"agent.reasoning.delta": { agentId: string; text: string; channel?: "think" | "say" | "tool" };
/** Observe → the live computer-screen tile. */
"agent.computer.frame": {
agentId: string;
app: "browser" | "terminal" | "slack" | "claw-chat";
url?: string;
lines?: { text: string; kind?: "plain" | "add" | "del" | "ok" | "warn" | "cursor" }[];
};
/** Reasoning-stream "tool" lines. */
"agent.tool.call": { agentId: string; tool: string; target?: string; doorRequired?: boolean };
/** Door-approval toast + "doors awaiting approval" badge. */
"door.request": { doorId: string; agentId: string; action: string; target?: string; summary?: string };
/** Dismisses the toast; logs to history. */
"door.resolve": { doorId: string; decision: "approve" | "deny"; by?: string };
/** Observe System → INTER-AGENT COMMS; World → message-in-flight on a connector. */
"agent.message": { fromAgentId: string; toAgentId: string; text: string; ts?: string };
/** World → Live (Gource): the agent pawn retargets to nodeId and beams. */
"world.touch": { agentId: string; nodeId: string; kind?: "service" | "event"; weight?: number };
/** World → node glow/pulse intensity. */
"node.activity": { nodeId: string; label?: string; kind?: "service" | "event"; heat?: number };
/** World → re-layout (add/remove org units, agents, projects). */
"topology.update": {
formation: "hierarchy" | "flat" | "live";
nodes?: { id: string; tier: NodeTier; label?: string; parentId?: string }[];
edges?: { from: string; to: string; kind?: "parent" | "peer" | "flow" }[];
};
/** Top-bar pills + Observe System telemetry strip. */
telemetry: { tokensPerMin?: number; costPerHr?: number; loops?: number; doorsPending?: number };
/** Observe System → ROUTINES & LOOPS; the scheduler view. */
"routine.update": {
routineId: string;
name: string;
kind: "cron" | "loop";
owner?: string;
schedule?: string;
progress?: number;
state?: "running" | "scheduled" | "done" | "failed" | "door";
};
}
export type NodeTier = "org" | "company" | "team" | "agent" | "service" | "event";
export type TaxonomyType = keyof TaxonomyEvents;
export type TaxonomyPayload<T extends TaxonomyType> = TaxonomyEvents[T];
/** Every taxonomy type, in one array (drives the EventSource listeners). */
export const TAXONOMY_TYPES: TaxonomyType[] = [
"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",
];
/** Stateful types whose last value is replayed to late subscribers (mirrors the
* runner's resume-at-checkpoint), vs. ephemeral streaming events (deltas,
* touches, messages) which are not replayed. */
export const STATEFUL_TYPES = new Set<TaxonomyType>([
"agent.status",
"agent.task.update",
"node.activity",
"telemetry",
"topology.update",
"routine.update",
]);
/** The replay key per stateful event (one retained value per agent/node/routine). */
export function stateKey<T extends TaxonomyType>(type: T, d: TaxonomyPayload<T>): string {
if (type === "agent.status" || type === "agent.task.update")
return `${type}:${(d as TaxonomyEvents["agent.status"]).agentId}`;
if (type === "node.activity") return `${type}:${(d as TaxonomyEvents["node.activity"]).nodeId}`;
if (type === "routine.update") return `${type}:${(d as TaxonomyEvents["routine.update"]).routineId}`;
return type; // telemetry, topology.update
}
+271
View File
@@ -0,0 +1,271 @@
"use client";
// Native client for the Clawmates live feed — the in-app equivalent of the
// handoff's clawmates-live.js. A single shared EventSource to /api/world/live
// fans typed taxonomy events out to subscribers, replays the last-known value of
// stateful events to late subscribers (resume-at-checkpoint), and falls back to a
// synthetic generator when no feed is reachable so the views are always alive.
import { useEffect, useRef, useState } from "react";
import {
STATEFUL_TYPES,
stateKey,
TAXONOMY_TYPES,
type TaxonomyPayload,
type TaxonomyType,
} from "./taxonomy";
const DEFAULT_URL = "/api/world/live";
type AnyHandler = (d: unknown) => void;
class LiveClient {
private handlers = new Map<TaxonomyType, Set<AnyHandler>>();
private last = new Map<string, { type: TaxonomyType; data: unknown }>();
private es: EventSource | null = null;
private stopSynthetic: (() => void) | null = null;
private syntheticTimer: ReturnType<typeof setTimeout> | null = null;
private refcount = 0;
connected = false;
url: string | null = null;
acquire(url: string) {
this.refcount += 1;
if (this.refcount === 1) this.connect(url);
}
release() {
this.refcount = Math.max(0, this.refcount - 1);
if (this.refcount === 0) this.disconnect();
}
on<T extends TaxonomyType>(type: T, fn: (d: TaxonomyPayload<T>) => void): () => void {
let set = this.handlers.get(type);
if (!set) {
set = new Set();
this.handlers.set(type, set);
}
set.add(fn as AnyHandler);
// Replay the last-known stateful value(s) so a late subscriber paints now.
for (const { type: t, data } of this.last.values()) {
if (t === type) {
try {
(fn as AnyHandler)(data);
} catch {
/* ignore */
}
}
}
return () => {
set?.delete(fn as AnyHandler);
};
}
private dispatch<T extends TaxonomyType>(type: T, data: TaxonomyPayload<T>) {
if (STATEFUL_TYPES.has(type)) this.last.set(stateKey(type, data), { type, data });
const set = this.handlers.get(type);
if (set) {
for (const fn of set) {
try {
fn(data);
} catch (e) {
console.warn("[ClawmatesLive]", type, e);
}
}
}
}
private connect(url: string) {
if (typeof window === "undefined") return;
this.url = url;
try {
const es = new EventSource(url);
this.es = es;
es.onopen = () => {
this.connected = true;
this.clearSynthetic();
};
es.onerror = () => {
this.connected = false;
this.scheduleSynthetic();
};
for (const type of TAXONOMY_TYPES) {
es.addEventListener(type, (ev: MessageEvent) => {
let data: unknown;
try {
data = JSON.parse(ev.data);
} catch {
return;
}
this.dispatch(type, data as TaxonomyPayload<typeof type>);
});
}
} catch {
/* EventSource unavailable */
}
// If the feed never opens, animate synthetically after a short grace period.
this.scheduleSynthetic(2500);
}
private disconnect() {
if (this.es) {
this.es.close();
this.es = null;
}
this.clearSynthetic();
this.connected = false;
this.handlers.clear();
this.last.clear();
}
private scheduleSynthetic(delay = 1500) {
if (this.stopSynthetic || this.connected) return;
if (this.syntheticTimer) clearTimeout(this.syntheticTimer);
this.syntheticTimer = setTimeout(() => {
if (!this.connected && this.refcount > 0) this.startSynthetic();
}, delay);
}
private startSynthetic() {
if (this.stopSynthetic) return;
this.stopSynthetic = runSynthetic((type, data) =>
this.dispatch(type, data as TaxonomyPayload<typeof type>),
);
}
private clearSynthetic() {
if (this.syntheticTimer) {
clearTimeout(this.syntheticTimer);
this.syntheticTimer = null;
}
if (this.stopSynthetic) {
this.stopSynthetic();
this.stopSynthetic = null;
}
}
}
let singleton: LiveClient | null = null;
export function liveClient(): LiveClient {
if (!singleton) singleton = new LiveClient();
return singleton;
}
/** Ensure the shared feed is connected for this component's lifetime. */
export function useClawmatesLive(url: string = DEFAULT_URL): LiveClient {
const client = liveClient();
useEffect(() => {
client.acquire(url);
return () => client.release();
}, [client, url]);
return client;
}
/** Subscribe to one taxonomy event (with replay) for a component's lifetime. */
export function useLiveEvent<T extends TaxonomyType>(
type: T,
fn: (d: TaxonomyPayload<T>) => void,
) {
const client = useClawmatesLive();
const ref = useRef(fn);
useEffect(() => {
ref.current = fn;
});
useEffect(() => client.on(type, (d) => ref.current(d)), [client, type]);
}
/** A reactive slice of a stateful event (e.g. telemetry, an agent's status). */
export function useLiveState<T extends TaxonomyType, S>(
type: T,
select: (d: TaxonomyPayload<T>) => S,
initial: S,
): S {
const [state, setState] = useState<S>(initial);
useLiveEvent(type, (d) => setState(select(d)));
return state;
}
// --- synthetic generator (no backend) — ports the demo loop so the World/Observe
// views breathe even with nothing wired. Stops the moment a real feed connects. --
type Emit = <T extends TaxonomyType>(type: T, data: TaxonomyPayload<T>) => void;
function runSynthetic(emit: Emit): () => void {
const agents = [
{ id: "morpheus", role: "Project Manager" },
{ id: "smith", role: "Research Specialist" },
];
const nodes: { id: string; label: string; kind: "service" | "event" }[] = [
{ id: "runtime", label: "cm-runtime", kind: "service" },
{ id: "pr214", label: "PR #214", kind: "event" },
{ id: "advdb", label: "advisory-db", kind: "service" },
{ id: "slack", label: "#eng", kind: "event" },
{ id: "orch", label: "cm-orch", kind: "service" },
{ id: "deploy", label: "PROD deploy", kind: "event" },
{ id: "docs", label: "spec §15", kind: "service" },
{ id: "bench", label: "topo-bench", kind: "service" },
{ id: "broker", label: "secret-broker", kind: "service" },
];
const pick = <U,>(xs: U[]) => xs[Math.floor(Math.random() * xs.length)];
emit("topology.update", {
formation: "live",
nodes: nodes.map((n) => ({ id: n.id, tier: n.kind, label: n.label })),
});
agents.forEach((a) => emit("agent.status", { agentId: a.id, status: "working", role: a.role }));
const timers: ReturnType<typeof setInterval>[] = [];
timers.push(
setInterval(() => {
const a = pick(agents);
const n = pick(nodes);
emit("world.touch", { agentId: a.id, nodeId: n.id, kind: n.kind });
emit("node.activity", { nodeId: n.id, label: n.label, kind: n.kind, heat: 0.6 + Math.random() * 0.4 });
}, 700),
);
const thoughts = [
"checking lifetime on &'a mut Guard across await",
"cargo clippy --workspace clean",
"this holds a std::sync::Mutex across .await — fix it",
"cloning under a scoped lock, dropping the guard first",
];
timers.push(
setInterval(
() => emit("agent.reasoning.delta", { agentId: "morpheus", text: pick(thoughts) + " … " }),
1600,
),
);
timers.push(
setInterval(
() =>
emit("agent.message", {
fromAgentId: "morpheus",
toAgentId: "smith",
text: "can you audit the crate tree before I approve?",
ts: new Date().toISOString(),
}),
5200,
),
);
let tok = 38000;
timers.push(
setInterval(() => {
tok += (Math.random() - 0.5) * 4000;
emit("telemetry", { tokensPerMin: Math.round(tok), costPerHr: 0.42, loops: 3, doorsPending: 1 });
}, 2000),
);
timers.push(
setInterval(
() =>
emit("door.request", {
doorId: "d" + Math.floor(Math.random() * 1e9),
agentId: "morpheus",
action: "github.review.comment",
target: "PR #214",
summary: "Post code review on PR #214",
}),
12000,
),
);
return () => timers.forEach(clearInterval);
}
+18
View File
@@ -0,0 +1,18 @@
# Clawmates Live bridge — environment
# Copy to ../.env (the installer does this for you) and edit.
# demo = synthetic events, no backend needed (great first run)
# live = subscribe to the cm-api run SSE stream and normalize it
CLAWMATES_MODE=demo
# Port this bridge serves the browser SSE feed on
LIVE_PORT=8420
# Browser origins allowed to connect (CORS). Comma-separated.
LIVE_ALLOWED_ORIGINS=http://localhost:8080
# ---- live mode only -------------------------------------------------------
# Where the durable runner / cm-api publishes its run + turn SSE stream:
CM_API_SSE_URL=http://127.0.0.1:8080/v1/runs/stream
# Read-only service token scoped to run events (NOT a broker credential):
CM_API_TOKEN=
+289
View File
@@ -0,0 +1,289 @@
# Clawmates Visualization Layer — Agent Setup & Real-Time Wiring Handoff
> **Audience:** an autonomous build agent (or a human operator) provisioning a Linux node.
> **Goal:** install everything required to *serve* the Clawmates visualizations and *feed them live*
> from the durable runner so the World / Observe / Dashboard views update in real time as the
> claws actually work.
>
> Read this file top-to-bottom **once** before running anything. Every command is idempotent and
> safe to re-run. Where a step touches the existing Clawmates platform (the Rust workspace +
> Next.js app), it is clearly marked **[INTEGRATES WITH cm-api]**; you can also run the whole
> layer in **demo mode** with synthetic events and zero backend.
---
## 0. What you are installing
Three browser-native visualizations plus a thin real-time bridge:
| Asset | What it is | Lives in |
|---|---|---|
| **Clawmates World** | The Large World map with three formations — **Hierarchy** (org▸company▸team▸claw tree), **Flat** (peer mesh), and **Live** (Gource-style convergence on a `<canvas>`). | `visualizations/Clawmates World.dc.html` |
| **Clawmates Observe** | Two linked observation modes — **Agent** close-up (live reasoning stream + the agent's live computer screen) and **System** mission-control (both claws, comms, routines, telemetry). | `visualizations/Clawmates Observe.dc.html` |
| **Clawmates Dashboard** | The tiered admin dashboard (org→company→team→claw) with topology morph + agent anatomy. | `visualizations/Clawmates Dashboard.dc.html` |
| **Live bridge** | A Node service that subscribes to the runner's event stream, normalizes it to the **Clawmates Event Taxonomy** (§4), and re-emits one SSE feed the browsers consume. | `realtime/server.mjs` |
| **Client adapter** | `window.ClawmatesLive` — connects to the bridge, dispatches typed events, and exposes them to each visualization. | `realtime/clawmates-live.js` |
The visualizations are **`.dc.html` files**: self-contained HTML that renders via the bundled
`support.js` runtime. They open directly in a browser — no build step, no npm install to *view*
them. The only thing that needs installing is (a) a static web server to serve them and (b) Node
for the live bridge.
---
## 1. System requirements
- **OS:** Linux x86_64 (Debian/Ubuntu 22.04+ or any systemd distro). ARM64 works too.
- **CPU/RAM:** 2 vCPU / 2 GB is plenty; the canvas animation runs client-side in the browser.
- **Node.js:** v20 LTS or newer (for the live bridge only).
- **A static file server:** any of `caddy`, `nginx`, or `npx serve`. Caddy is recommended (auto-HTTPS, one binary).
- **Outbound network:** only needed to *download* packages during install. The visualizations and
bridge run fully offline afterward — consistent with the §15 "sandboxes have no network" model.
- **A modern browser** to display: Chromium/Chrome 120+ or Firefox 121+ (Canvas2D + ES2020).
---
## 2. One-shot install
From the unzipped package root:
```bash
chmod +x install.sh
./install.sh
```
`install.sh` will:
1. Verify Node ≥ 20 (and print install instructions if missing — it will **not** silently install a runtime).
2. Install the bridge's dependencies (`npm ci` in `realtime/`, single dependency: none beyond Node built-ins — it's intentionally dependency-free).
3. Install **Caddy** if no static server is found (via the official apt repo; falls back to `npx serve`).
4. Write a `.env` from `.env.example` if one does not exist.
5. Print the exact next commands for **demo mode** and **live mode**.
If you prefer to do it by hand, the rest of this document is the manual path.
---
## 3. Run it
### 3a. Demo mode (no backend — synthetic events)
Best first run: proves the whole pipe lights up.
```bash
# Terminal 1 — the live bridge in demo mode (emits synthetic agent activity)
cd realtime
CLAWMATES_MODE=demo node server.mjs
# → Live bridge on http://localhost:8420/live (SSE) · mode=demo
# Terminal 2 — serve the visualizations
caddy file-server --root ../visualizations --listen :8080
# (or: npx serve ../visualizations -l 8080)
```
Open **http://localhost:8080/Clawmates%20World.dc.html?live=http://localhost:8420/live**
The `?live=` query param tells the client adapter where the SSE feed is. Switch to the **Live**
formation and you should see the agent particles converge on nodes **driven by real events from the
bridge**, not the built-in synthetic loop. (Without `?live=`, every visualization falls back to its
self-contained demo animation, so it always looks alive even with nothing wired.)
### 3b. Live mode [INTEGRATES WITH cm-api]
Point the bridge at the durable runner's SSE gateway and give it a session token. The bridge swaps
the session for a bearer token exactly like the Next.js `/api` proxy does, then streams.
```bash
cd realtime
cp ../.env.example ../.env # then edit ../.env
CLAWMATES_MODE=live node server.mjs
```
Required `.env` values for live mode:
```ini
CLAWMATES_MODE=live
# Where the runner publishes its raw run/turn SSE stream (cm-api streaming gateway):
CM_API_SSE_URL=http://127.0.0.1:8080/v1/runs/stream
# Bearer token (or a session cookie the bridge will exchange):
CM_API_TOKEN=sk-... # service token scoped to read run events
# The port this bridge listens on for browsers:
LIVE_PORT=8420
# Comma-separated allowed origins for the browser SSE (CORS):
LIVE_ALLOWED_ORIGINS=http://localhost:8080
```
> **Security note (carry this through):** the bridge is *read-only* on the runner stream. It must
> **never** hold or forward secret-broker credentials, and it must not expose any door-*approval*
> endpoint to the public browser. Approvals stay on the authenticated cm-api path. The bridge only
> *renders* that a door is pending (`door.request`) so the UI can badge it; the actual approve/deny
> call goes through your existing authenticated API, not through this bridge.
---
## 4. The Clawmates Event Taxonomy (the contract)
This is the heart of the real-time layer. The runner produces many internal events; the bridge
**normalizes** them into this small, stable set. The browser only ever sees these. Full JSON Schema
is in `realtime/events.schema.json`; this is the human summary.
Every event is a line on the SSE feed: `event: <type>\ndata: <json>\n\n`.
### Agent lifecycle & work
| Type | Payload | Drives |
|---|---|---|
| `agent.status` | `{ agentId, status: "online"\|"working"\|"idle", role }` | Status dots in every view; the list rail. |
| `agent.task.update` | `{ agentId, taskId, title, elapsedMs, steps:[{label,state:"done"\|"active"\|"pending"}] }` | Observe → "WORKING ON NOW" card. |
| `agent.reasoning.delta` | `{ agentId, text }` (token chunk) | Observe → REASONING STREAM console (append). |
| `agent.computer.frame` | `{ agentId, app:"browser"\|"terminal"\|"slack", url?, lines?:[{text,kind}] }` | Observe → the live computer screen tile. |
### Tools, doors & safety (§15)
| Type | Payload | Drives |
|---|---|---|
| `agent.tool.call` | `{ agentId, tool, target, doorRequired:bool }` | Reasoning stream "tool" lines. |
| `door.request` | `{ doorId, agentId, action, target, summary }` | The door-approval toast + "doors awaiting approval" badge. |
| `door.resolve` | `{ doorId, decision:"approve"\|"deny", by }` | Dismisses the toast; logs to history. |
### Collaboration & world
| Type | Payload | Drives |
|---|---|---|
| `agent.message` | `{ fromAgentId, toAgentId, text, ts }` | Observe System → INTER-AGENT COMMS stream; World → message-in-flight on the connector. |
| `world.touch` | `{ agentId, nodeId, kind:"service"\|"event" }` | **World → Live (Gource): the agent particle retargets to `nodeId` and emits a beam.** |
| `node.activity` | `{ nodeId, label, kind, heat:0..1 }` | World → node glow/pulse intensity. |
| `topology.update` | `{ formation:"hierarchy"\|"flat"\|..., nodes:[...], edges:[...] }` | World → re-layout (add/remove org units, agents, projects). |
### System telemetry & routines
| Type | Payload | Drives |
|---|---|---|
| `telemetry` | `{ tokensPerMin, costPerHr, loops, doorsPending }` | Top-bar pills + Observe System telemetry strip. |
| `routine.update` | `{ routineId, name, kind:"cron"\|"loop", owner, schedule?, progress?, state }` | Observe System → ROUTINES & LOOPS; the scheduler view. |
**Mapping rule of thumb:** the runner already emits a checkpointed event per step on the durable
runner. In the bridge's `normalize()` you translate each runner event into one or more taxonomy
events above. Start by mapping just `world.touch`, `agent.status`, and `telemetry` — that alone makes
the Large World view come alive — then add the rest incrementally.
---
## 5. How each visualization consumes the feed
The client adapter `clawmates-live.js` is included by each `.dc.html` (or injected — see §6). It:
1. Reads `?live=<url>` (or `window.CLAWMATES_LIVE_URL`); if absent, **does nothing** and the view keeps
its built-in synthetic animation.
2. Opens an `EventSource` to that URL.
3. Dispatches each event as a `CustomEvent` on `window` **and** calls any registered handlers:
```js
window.ClawmatesLive.on('world.touch', e => worldEngine.touch(e.agentId, e.nodeId));
window.ClawmatesLive.on('agent.reasoning.delta', e => observe.appendReasoning(e.agentId, e.text));
```
4. Buffers events fired before a view registers, and replays the last value per `nodeId`/`agentId`
so a late-loading view paints current state immediately (mirrors the runner's "resume at
checkpoint" behavior).
### Integration points inside the `.dc.html` files (where synthetic → live)
- **World — Live canvas:** in the logic class, the `_startLive()` engine has `targets[]` (the
project/service/event nodes) and an `agents[]` array whose `.target` index is currently chosen at
random on a timer. **Replace that random retarget with `world.touch`:** when a `world.touch`
arrives, set the matching agent's `.target` to the index of `nodeId` and force a beam. Use
`topology.update` to rebuild `targets[]`. Everything else (beams, sparks, glow) already keys off
the node being "hot", so it just works.
- **Observe — Agent:** the WORKING-ON-NOW card, the REASONING STREAM, and the computer screen are
static markup today. Bind them to `agent.task.update`, `agent.reasoning.delta`, and
`agent.computer.frame` respectively (append for deltas; replace for task/frame).
- **Observe — System & Dashboard:** bind the telemetry pills to `telemetry`, the comms list to
`agent.message`, routines to `routine.update`, and the door toast to `door.request`/`door.resolve`.
Each of these is a small `ClawmatesLive.on(...)` handler that calls `this.setState(...)`. No re-layout
of the markup is needed — you are feeding values into components that already exist.
---
## 6. Embedding into the existing Next.js app [INTEGRATES WITH cm-api]
You have two deployment shapes; pick one.
**A) Standalone (fastest):** serve the `visualizations/` folder with Caddy as in §3 and link to it
from the app, or drop it behind the same domain at `/viz/*`. The bridge runs as its own systemd
service. Good for an ops/observability surface that lives beside the product.
**B) In-app (tightest):** mount each `.dc.html` inside the Next.js app via an `<iframe src="/viz/...">`,
and have the app pass the live URL through: `/viz/world?live=/api/live`. Proxy `/api/live` in Next
to the bridge so it inherits the same session→bearer swap and CORS as the rest of `/api`. This keeps
auth and origin identical to the product.
Either way, the bridge is the **only** new long-running process. Run it under systemd:
```ini
# /etc/systemd/system/clawmates-live.service
[Unit]
Description=Clawmates Live Visualization Bridge
After=network.target
[Service]
WorkingDirectory=/opt/clawmates/handoff/realtime
EnvironmentFile=/opt/clawmates/handoff/.env
ExecStart=/usr/bin/node server.mjs
Restart=always
RestartSec=2
# Hardening — read-only, no privileged access (consistent with §15 posture):
NoNewPrivileges=true
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
[Install]
WantedBy=multi-user.target
```
```bash
sudo cp -r . /opt/clawmates/handoff
sudo systemctl enable --now clawmates-live
sudo systemctl status clawmates-live
```
---
## 7. Verification checklist
Run these in order; each should pass before moving on.
1. `node realtime/server.mjs` (demo) prints `Live bridge on http://localhost:8420/live · mode=demo`.
2. `curl -N http://localhost:8420/live` streams `event: ...` lines that don't stop. (Ctrl-C to exit.)
3. The World view at `?live=...` → **Live** formation shows particles **retargeting in sync** with the
`world.touch` lines you see in the curl output (not the smooth random demo motion).
4. Kill the bridge mid-stream → the browser shows the reconnect state and **resumes** when it returns
(EventSource auto-reconnects; the adapter replays last-known state).
5. **[live]** Point at cm-api, trigger a real run, and confirm `telemetry` numbers in the top bar move.
---
## 8. Troubleshooting
- **View looks alive but ignores the feed** → you didn't pass `?live=` (or `window.CLAWMATES_LIVE_URL`
isn't set); it's running the built-in synthetic animation. That's the intended fallback.
- **CORS error in console** → add the browser's origin to `LIVE_ALLOWED_ORIGINS` in `.env`.
- **SSE connects then drops every ~30s** → a proxy is buffering/timing out SSE. Disable buffering
(`proxy_buffering off;` in nginx; Caddy is fine by default) and set read timeout high.
- **Canvas blank on the Live tab** → the tab was never the active formation at first paint; switch to
it once. (The engine starts on mount/visibility.)
- **High CPU** → only the Live canvas animates; cap devicePixelRatio (already capped at 2) or pause
rendering when the tab is hidden (`document.hidden`).
---
## 9. What to build next (ordered)
1. Wire `world.touch` + `topology.update` → the World Live engine. (Biggest visual payoff, smallest change.)
2. Make canvas nodes clickable → drive the right-hand PANEL and drill into an agent (switch to Observe/Agent).
3. Bind Observe Agent to `agent.task.update` / `agent.reasoning.delta` / `agent.computer.frame`.
4. Add a **time-scrubber** that replays the durable runner's checkpoint log — Gource-style playback of
the world's history.
5. Promote the bridge from read-only mirror to a typed gateway with backpressure + per-tenant scoping.
---
*Generated as a handoff for the Clawmates visualization layer. The `.dc.html` files are the source of
truth for the UI; this document plus `realtime/` is everything needed to serve them and make them
breathe with live agent activity.*
+34
View File
@@ -0,0 +1,34 @@
# Clawmates Visualization Layer — install package
A seamless, self-contained drop for a Linux node. It serves the three Clawmates
visualizations and feeds them **live** from the durable runner.
```
handoff/
├── AGENT_HANDOFF.md ← READ THIS FIRST. Deep setup + real-time wiring guide for the agent.
├── install.sh ← one-shot installer (idempotent)
├── .env.example ← copy → .env, edit for live mode
├── visualizations/ ← the UI (open directly in a browser, no build step)
│ ├── Clawmates World.dc.html · Large World: Hierarchy · Flat · Live (Gource) formations
│ ├── Clawmates Observe.dc.html · Agent close-up + System mission-control
│ ├── Clawmates Dashboard.dc.html · Tiered admin: org→company→team→claw + topology morph
│ └── support.js · the .dc.html runtime (required next to the html)
└── realtime/ ← the live bridge (dependency-free Node ≥20)
├── server.mjs · demo + live SSE bridge
├── clawmates-live.js · browser client adapter (window.ClawmatesLive)
├── events.schema.json · the Clawmates Event Taxonomy (JSON Schema)
└── package.json
```
## 60-second start (demo, no backend)
```bash
chmod +x install.sh && ./install.sh
(cd realtime && CLAWMATES_MODE=demo node server.mjs) &
caddy file-server --root ./visualizations --listen :8080 # or: npx serve ./visualizations -l 8080
```
Open **http://localhost:8080/Clawmates%20World.dc.html?live=http://localhost:8420/live**, switch to
the **Live** formation, and watch the agents converge on real events from the bridge.
Then read **AGENT_HANDOFF.md** to wire it to `cm-api` for production.
+63
View File
@@ -0,0 +1,63 @@
#!/usr/bin/env bash
# Clawmates Visualization Layer — installer (idempotent, safe to re-run)
set -euo pipefail
cd "$(dirname "$0")"
say() { printf '\033[1;38;5;209m›\033[0m %s\n' "$*"; }
ok() { printf '\033[1;32m✓\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m!\033[0m %s\n' "$*"; }
echo
say "Clawmates visualization layer — install"
echo
# 1) Node >= 20 -------------------------------------------------------------
if command -v node >/dev/null 2>&1; then
NODE_MAJOR="$(node -p 'process.versions.node.split(".")[0]')"
if [ "$NODE_MAJOR" -ge 20 ]; then ok "Node $(node -v)"; else
warn "Node $(node -v) found but >= 20 is required."
echo " Install Node 20 LTS: https://nodejs.org or 'sudo apt install nodejs' from NodeSource."
exit 1
fi
else
warn "Node not found. Install Node 20 LTS, then re-run:"
echo " curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - && sudo apt-get install -y nodejs"
exit 1
fi
# 2) Bridge deps (intentionally none beyond Node built-ins) -----------------
if [ -f realtime/package.json ]; then
( cd realtime && npm ci --omit=dev 2>/dev/null || npm install --omit=dev 2>/dev/null || true )
ok "Bridge ready (dependency-free)"
fi
# 3) Static file server ------------------------------------------------------
if command -v caddy >/dev/null 2>&1; then
ok "Caddy present — will serve visualizations/"
elif command -v npx >/dev/null 2>&1; then
warn "Caddy not found; will fall back to 'npx serve'."
else
warn "No static server found. Install Caddy: https://caddyserver.com/docs/install"
fi
# 4) .env --------------------------------------------------------------------
if [ ! -f .env ]; then
cp .env.example .env
ok "Wrote .env from .env.example — edit it before running live mode."
else
ok ".env already exists (left untouched)"
fi
echo
ok "Install complete."
echo
say "Next — DEMO mode (no backend):"
echo " (cd realtime && CLAWMATES_MODE=demo node server.mjs)"
echo " caddy file-server --root ./visualizations --listen :8080 # or: npx serve ./visualizations -l 8080"
echo " open http://localhost:8080/Clawmates%20World.dc.html?live=http://localhost:8420/live"
echo
say "Next — LIVE mode (wire to cm-api): edit .env, then"
echo " (cd realtime && CLAWMATES_MODE=live node server.mjs)"
echo
say "See AGENT_HANDOFF.md for the full architecture, event taxonomy, and systemd unit."
echo
+81
View File
@@ -0,0 +1,81 @@
// 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());
})();
+199
View File
@@ -0,0 +1,199 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "https://clawmates.work/schemas/events.schema.json",
"title": "Clawmates Event Taxonomy",
"description": "Normalized real-time events emitted by the live bridge and consumed by the visualizations. Each SSE frame is `event: <type>` + `data: <object matching the matching definition>`.",
"type": "object",
"oneOf": [
{ "$ref": "#/definitions/agent.status" },
{ "$ref": "#/definitions/agent.task.update" },
{ "$ref": "#/definitions/agent.reasoning.delta" },
{ "$ref": "#/definitions/agent.computer.frame" },
{ "$ref": "#/definitions/agent.tool.call" },
{ "$ref": "#/definitions/door.request" },
{ "$ref": "#/definitions/door.resolve" },
{ "$ref": "#/definitions/agent.message" },
{ "$ref": "#/definitions/world.touch" },
{ "$ref": "#/definitions/node.activity" },
{ "$ref": "#/definitions/topology.update" },
{ "$ref": "#/definitions/telemetry" },
{ "$ref": "#/definitions/routine.update" }
],
"definitions": {
"agent.status": {
"type": "object",
"required": ["agentId", "status"],
"properties": {
"agentId": { "type": "string" },
"status": { "enum": ["online", "working", "idle", "offline"] },
"role": { "type": "string" }
}
},
"agent.task.update": {
"type": "object",
"required": ["agentId", "taskId", "title"],
"properties": {
"agentId": { "type": "string" },
"taskId": { "type": "string" },
"title": { "type": "string" },
"elapsedMs": { "type": "integer", "minimum": 0 },
"steps": {
"type": "array",
"items": {
"type": "object",
"required": ["label", "state"],
"properties": {
"label": { "type": "string" },
"state": { "enum": ["done", "active", "pending"] }
}
}
}
}
},
"agent.reasoning.delta": {
"type": "object",
"required": ["agentId", "text"],
"properties": {
"agentId": { "type": "string" },
"text": { "type": "string", "description": "Token chunk to APPEND to the reasoning stream." },
"channel": { "enum": ["think", "say", "tool"], "default": "think" }
}
},
"agent.computer.frame": {
"type": "object",
"required": ["agentId", "app"],
"properties": {
"agentId": { "type": "string" },
"app": { "enum": ["browser", "terminal", "slack", "claw-chat"] },
"url": { "type": "string" },
"lines": {
"type": "array",
"items": {
"type": "object",
"required": ["text"],
"properties": {
"text": { "type": "string" },
"kind": { "enum": ["plain", "add", "del", "ok", "warn", "cursor"], "default": "plain" }
}
}
}
}
},
"agent.tool.call": {
"type": "object",
"required": ["agentId", "tool"],
"properties": {
"agentId": { "type": "string" },
"tool": { "type": "string" },
"target": { "type": "string" },
"doorRequired": { "type": "boolean", "default": false }
}
},
"door.request": {
"type": "object",
"required": ["doorId", "agentId", "action"],
"properties": {
"doorId": { "type": "string" },
"agentId": { "type": "string" },
"action": { "type": "string", "description": "e.g. github.review.comment" },
"target": { "type": "string" },
"summary": { "type": "string" }
}
},
"door.resolve": {
"type": "object",
"required": ["doorId", "decision"],
"properties": {
"doorId": { "type": "string" },
"decision": { "enum": ["approve", "deny"] },
"by": { "type": "string" }
}
},
"agent.message": {
"type": "object",
"required": ["fromAgentId", "toAgentId", "text"],
"properties": {
"fromAgentId": { "type": "string" },
"toAgentId": { "type": "string" },
"text": { "type": "string" },
"ts": { "type": "string", "format": "date-time" }
}
},
"world.touch": {
"type": "object",
"description": "An agent is converging on a project/service/event node (Gource). Drives the Live canvas retarget + beam.",
"required": ["agentId", "nodeId"],
"properties": {
"agentId": { "type": "string" },
"nodeId": { "type": "string" },
"kind": { "enum": ["service", "event"], "default": "service" },
"weight": { "type": "number", "minimum": 0, "maximum": 1, "default": 1 }
}
},
"node.activity": {
"type": "object",
"required": ["nodeId"],
"properties": {
"nodeId": { "type": "string" },
"label": { "type": "string" },
"kind": { "enum": ["service", "event"] },
"heat": { "type": "number", "minimum": 0, "maximum": 1 }
}
},
"topology.update": {
"type": "object",
"description": "Full or partial re-layout of the world graph.",
"required": ["formation"],
"properties": {
"formation": { "enum": ["hierarchy", "flat", "live"] },
"nodes": {
"type": "array",
"items": {
"type": "object",
"required": ["id", "tier"],
"properties": {
"id": { "type": "string" },
"tier": { "enum": ["org", "company", "team", "agent", "service", "event"] },
"label": { "type": "string" },
"parentId": { "type": "string" }
}
}
},
"edges": {
"type": "array",
"items": {
"type": "object",
"required": ["from", "to"],
"properties": {
"from": { "type": "string" },
"to": { "type": "string" },
"kind": { "enum": ["parent", "peer", "flow"], "default": "parent" }
}
}
}
}
},
"telemetry": {
"type": "object",
"properties": {
"tokensPerMin": { "type": "number" },
"costPerHr": { "type": "number" },
"loops": { "type": "integer" },
"doorsPending": { "type": "integer" }
}
},
"routine.update": {
"type": "object",
"required": ["routineId", "name", "kind"],
"properties": {
"routineId": { "type": "string" },
"name": { "type": "string" },
"kind": { "enum": ["cron", "loop"] },
"owner": { "type": "string" },
"schedule": { "type": "string", "description": "cron expr or next-run hint" },
"progress": { "type": "number", "minimum": 0, "maximum": 1 },
"state": { "enum": ["running", "scheduled", "done", "failed", "door"] }
}
}
}
}
+14
View File
@@ -0,0 +1,14 @@
{
"name": "clawmates-live-bridge",
"version": "1.0.0",
"private": true,
"type": "module",
"description": "Read-only real-time bridge: normalizes cm-api run events into the Clawmates Event Taxonomy and fans them out over SSE to the visualizations.",
"engines": { "node": ">=20" },
"scripts": {
"start": "node server.mjs",
"demo": "CLAWMATES_MODE=demo node server.mjs",
"live": "CLAWMATES_MODE=live node server.mjs"
},
"dependencies": {}
}
+221
View File
@@ -0,0 +1,221 @@
// Clawmates Live Visualization Bridge
// ------------------------------------
// Dependency-free Node (>=20) service. Two modes:
// CLAWMATES_MODE=demo → emits synthetic events from the taxonomy so the viz lights up offline.
// CLAWMATES_MODE=live → subscribes to the cm-api run SSE stream, normalizes via normalize(),
// and re-emits one fan-out SSE feed for browsers.
//
// It is READ-ONLY on the upstream stream. It never holds or forwards secret-broker credentials,
// and it exposes NO door-approval endpoint. Approvals stay on the authenticated cm-api path.
//
// Run: node server.mjs (reads ../.env if present, plus process env)
import http from 'node:http';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
// ---- tiny .env loader (no dependency) -------------------------------------
const __dir = path.dirname(fileURLToPath(import.meta.url));
for (const envPath of [path.join(__dir, '..', '.env'), path.join(__dir, '.env')]) {
try {
for (const line of fs.readFileSync(envPath, 'utf8').split('\n')) {
const m = line.match(/^\s*([A-Z0-9_]+)\s*=\s*(.*)\s*$/);
if (m && process.env[m[1]] === undefined) process.env[m[1]] = m[2].replace(/^["']|["']$/g, '');
}
} catch { /* no .env, that's fine */ }
}
const MODE = process.env.CLAWMATES_MODE || 'demo';
const PORT = parseInt(process.env.LIVE_PORT || '8420', 10);
const ALLOWED = (process.env.LIVE_ALLOWED_ORIGINS || 'http://localhost:8080')
.split(',').map(s => s.trim()).filter(Boolean);
// ---- fan-out hub -----------------------------------------------------------
const clients = new Set(); // res objects
const lastState = new Map(); // key → last event (for replay-on-connect)
function keyFor(type, d) {
if (type === 'agent.status' || type === 'agent.task.update') return type + ':' + d.agentId;
if (type === 'node.activity') return type + ':' + d.nodeId;
if (type === 'telemetry' || type === 'topology.update') return type;
if (type === 'routine.update') return type + ':' + d.routineId;
return null; // streaming/ephemeral events (deltas, touches, messages) are not replayed
}
function broadcast(type, data) {
const frame = `event: ${type}\ndata: ${JSON.stringify(data)}\n\n`;
const k = keyFor(type, data);
if (k) lastState.set(k, frame);
for (const res of clients) res.write(frame);
}
// ---- HTTP server: /live (SSE), /healthz -----------------------------------
const server = http.createServer((req, res) => {
const origin = req.headers.origin;
const cors = origin && ALLOWED.includes(origin) ? origin : ALLOWED[0] || '*';
if (req.url === '/healthz') {
res.writeHead(200, { 'content-type': 'application/json' });
return res.end(JSON.stringify({ ok: true, mode: MODE, clients: clients.size }));
}
if (req.url && req.url.startsWith('/live')) {
res.writeHead(200, {
'content-type': 'text/event-stream',
'cache-control': 'no-cache, no-transform',
'connection': 'keep-alive',
'access-control-allow-origin': cors,
'x-accel-buffering': 'no', // disable nginx buffering
});
res.write('retry: 2000\n\n'); // browser auto-reconnect after 2s
// replay current state so a late view paints immediately (mirrors checkpoint resume)
for (const frame of lastState.values()) res.write(frame);
clients.add(res);
const ka = setInterval(() => res.write(': keepalive\n\n'), 15000);
req.on('close', () => { clearInterval(ka); clients.delete(res); });
return;
}
res.writeHead(404); res.end('not found');
});
server.listen(PORT, () => {
console.log(`Live bridge on http://localhost:${PORT}/live (SSE) · mode=${MODE}`);
if (MODE === 'demo') startDemo();
else startLive();
});
// ===========================================================================
// LIVE MODE — subscribe to cm-api run SSE, normalize to taxonomy
// ===========================================================================
async function startLive() {
const url = process.env.CM_API_SSE_URL;
const token = process.env.CM_API_TOKEN;
if (!url) { console.error('CM_API_SSE_URL is required in live mode'); process.exit(1); }
console.log(`[live] subscribing to ${url}`);
// Native fetch streaming (Node 20+). Reconnects with backoff.
let backoff = 1000;
for (;;) {
try {
const resp = await fetch(url, { headers: token ? { authorization: `Bearer ${token}` } : {} });
if (!resp.ok || !resp.body) throw new Error('upstream ' + resp.status);
backoff = 1000;
const reader = resp.body.getReader();
const dec = new TextDecoder();
let buf = '';
for (;;) {
const { value, done } = await reader.read();
if (done) break;
buf += dec.decode(value, { stream: true });
const frames = buf.split('\n\n'); buf = frames.pop() || '';
for (const f of frames) handleUpstreamFrame(f);
}
} catch (e) {
console.error('[live] stream error:', e.message, '— retrying in', backoff, 'ms');
await new Promise(r => setTimeout(r, backoff));
backoff = Math.min(backoff * 2, 30000);
}
}
}
function handleUpstreamFrame(frame) {
// upstream frame: "event: <t>\ndata: <json>"
let evt = 'message', data = '';
for (const line of frame.split('\n')) {
if (line.startsWith('event:')) evt = line.slice(6).trim();
else if (line.startsWith('data:')) data += line.slice(5).trim();
}
let parsed; try { parsed = JSON.parse(data); } catch { return; }
for (const [type, payload] of normalize(evt, parsed)) broadcast(type, payload);
}
// THE INTEGRATION SEAM ------------------------------------------------------
// Translate one raw runner event into zero+ taxonomy events. Extend this map as
// you learn the runner's event shapes. Start with the three high-value ones.
function* normalize(evt, p) {
switch (evt) {
case 'turn.started':
yield ['agent.status', { agentId: p.agentId, status: 'working', role: p.role }];
if (p.task) yield ['agent.task.update', {
agentId: p.agentId, taskId: p.taskId, title: p.task.title,
elapsedMs: p.task.elapsedMs || 0, steps: p.task.steps || [],
}];
break;
case 'turn.token': // streamed reasoning
yield ['agent.reasoning.delta', { agentId: p.agentId, text: p.text, channel: p.channel || 'think' }];
break;
case 'tool.invoked':
yield ['agent.tool.call', { agentId: p.agentId, tool: p.tool, target: p.target, doorRequired: !!p.doorRequired }];
// a tool that leaves the sandbox surfaces as a touch on the world graph
if (p.nodeId) yield ['world.touch', { agentId: p.agentId, nodeId: p.nodeId, kind: p.nodeKind || 'service' }];
break;
case 'door.requested':
yield ['door.request', { doorId: p.doorId, agentId: p.agentId, action: p.action, target: p.target, summary: p.summary }];
break;
case 'door.resolved':
yield ['door.resolve', { doorId: p.doorId, decision: p.decision, by: p.by }];
break;
case 'agent.message':
yield ['agent.message', { fromAgentId: p.from, toAgentId: p.to, text: p.text, ts: p.ts }];
break;
case 'runner.telemetry':
yield ['telemetry', { tokensPerMin: p.tokensPerMin, costPerHr: p.costPerHr, loops: p.loops, doorsPending: p.doorsPending }];
break;
case 'routine.tick':
yield ['routine.update', { routineId: p.routineId, name: p.name, kind: p.kind, owner: p.owner, schedule: p.schedule, progress: p.progress, state: p.state }];
break;
// unknown events are ignored — the bridge is forward-compatible.
}
}
// ===========================================================================
// DEMO MODE — synthetic world activity so everything lights up with no backend
// ===========================================================================
function startDemo() {
const agents = ['morpheus', 'smith'];
const nodes = [
['runtime','cm-runtime','service'], ['pr214','PR #214','event'], ['advdb','advisory-db','service'],
['slack','#eng','event'], ['orch','cm-orch','service'], ['deploy','PROD deploy','event'],
['docs','spec §15','service'], ['bench','topo-bench','service'], ['broker','secret-broker','service'],
];
// seed topology + status
broadcast('topology.update', {
formation: 'live',
nodes: nodes.map(([id, label, kind]) => ({ id, tier: kind, label })),
});
agents.forEach(a => broadcast('agent.status', { agentId: a, status: 'working', role: a === 'morpheus' ? 'Project Manager' : 'Research Specialist' }));
// agents converge on random nodes (Gource)
setInterval(() => {
const a = agents[Math.random() * agents.length | 0];
const [id, label, kind] = nodes[Math.random() * nodes.length | 0];
broadcast('world.touch', { agentId: a, nodeId: id, kind });
broadcast('node.activity', { nodeId: id, label, kind, heat: 0.6 + Math.random() * 0.4 });
}, 700);
// reasoning + comms
const thoughts = [
'checking lifetime on &\'a mut Guard across await',
'cargo clippy --workspace clean',
'this holds a std::sync::Mutex across .await — fix it',
'cloning under a scoped lock, dropping the guard first',
];
setInterval(() => broadcast('agent.reasoning.delta', { agentId: 'morpheus', text: thoughts[Math.random() * thoughts.length | 0] + ' … ' }), 1600);
setInterval(() => broadcast('agent.message', { fromAgentId: 'morpheus', toAgentId: 'smith', text: 'can you audit the crate tree before I approve?', ts: new Date().toISOString() }), 5200);
// telemetry drift
let tok = 38000;
setInterval(() => {
tok += (Math.random() - 0.5) * 4000;
broadcast('telemetry', { tokensPerMin: Math.round(tok), costPerHr: 0.42, loops: 3, doorsPending: 1 });
}, 2000);
// an occasional door
setInterval(() => {
broadcast('door.request', { doorId: 'd' + Date.now(), agentId: 'morpheus', action: 'github.review.comment', target: 'PR #214', summary: 'Post code review on PR #214' });
}, 12000);
console.log('[demo] emitting synthetic events — open a viz with ?live=http://localhost:' + PORT + '/live');
}
@@ -0,0 +1,650 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="./support.js"></script>
</head>
<body>
<x-dc>
<helmet>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; background: #08080a; }
body { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; }
@keyframes cm-flow { to { stroke-dashoffset: -24; } }
@keyframes cm-blink { 0%,100% { opacity: 1; } 50% { opacity: .25; } }
@keyframes cm-halo { 0% { transform: scale(.7); opacity: .55; } 100% { transform: scale(1.9); opacity: 0; } }
@keyframes cm-fade { from { opacity: 0; } to { opacity: 1; } }
</style>
</helmet>
<div style="width:100%; height:100vh; min-height:640px; background:#08080a; display:flex; flex-direction:column; color:#f3f3f5; overflow:hidden;">
<!-- TOP BAR -->
<div style="height:54px; flex:none; display:flex; align-items:center; gap:14px; padding:0 18px; border-bottom:1px solid rgba(255,255,255,.06); background:linear-gradient(180deg,#0d0d10,#0a0a0c);">
<div style="display:flex; align-items:center; gap:10px;">
<svg width="22" height="22" viewBox="0 0 22 22" fill="none">
<path d="M11 3 L18.5 17 L3.5 17 Z" stroke="#ff6f61" stroke-width="1.3" stroke-linejoin="round" opacity="0.55"></path>
<circle cx="11" cy="3.5" r="2.4" fill="#ff6f61"></circle><circle cx="18" cy="17" r="2.4" fill="#ff6f61"></circle><circle cx="4" cy="17" r="2.4" fill="#ff6f61"></circle>
</svg>
<span style="font-size:15px; font-weight:700; color:#f3f3f5; letter-spacing:-.01em;">Clawmates</span>
</div>
<div style="width:1px; height:22px; background:rgba(255,255,255,.08);"></div>
<div style="display:flex; align-items:center; gap:6px; font-family:'JetBrains Mono',monospace; font-size:12px;">
<span style="cursor:pointer; border-radius:6px;" style="{{ crumbOrg }}" onClick="{{ goOrg }}">Acme Org</span>
<span style="color:#3a3a40;">/</span>
<span style="cursor:pointer; border-radius:6px;" style="{{ crumbCo }}" onClick="{{ goCompany }}">Acme Corp</span>
<span style="color:#3a3a40;">/</span>
<span style="cursor:pointer; border-radius:6px;" style="{{ crumbTeam }}" onClick="{{ goTeam }}">Growth Team</span>
<sc-if value="{{ isClaw }}" hint-placeholder-val="{{ false }}">
<span style="color:#3a3a40;">/</span>
<span style="cursor:pointer; border-radius:6px;" style="{{ crumbClaw }}">{{ selName }}</span>
</sc-if>
</div>
<div style="flex:1;"></div>
<div style="display:flex; align-items:center; gap:7px; font-family:'JetBrains Mono',monospace; font-size:11px; color:#5fd08a; padding:5px 10px; border:1px solid rgba(95,208,138,.25); border-radius:7px; background:rgba(95,208,138,.06);">
<span style="width:7px; height:7px; border-radius:50%; background:#5fd08a; animation:cm-blink 1.6s infinite;"></span>
6 claws · running
</div>
<div style="display:flex; align-items:center; gap:7px; padding:6px 12px; border-radius:8px; background:linear-gradient(135deg,#ff8a7a,#ff5f57); color:#2a0d0a; font-size:12px; font-weight:700; cursor:pointer;">
<span style="font-size:15px; line-height:1;">+</span> Deploy from template
</div>
<div style="width:32px; height:32px; border-radius:8px; background:linear-gradient(135deg,#ff8a7a,#ff5f57); display:flex; align-items:center; justify-content:center; font-size:13px; font-weight:700; color:#2a0d0a;">O</div>
</div>
<!-- BODY -->
<div style="flex:1; display:flex; min-height:0;">
<!-- STRUCTURE RAIL -->
<div style="width:60px; flex:none; border-right:1px solid rgba(255,255,255,.06); background:#0a0a0c; display:flex; flex-direction:column; align-items:center; padding:14px 0;">
<div style="display:flex; flex-direction:column; gap:6px; align-items:center;">
<div style="position:relative; width:42px; height:48px; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:4px; border-radius:10px; cursor:pointer; color:{{ railOrgColor }}; background:{{ railOrgBg }};" onClick="{{ goOrg }}">
<sc-if value="{{ railOrgOn }}" hint-placeholder-val="{{ false }}"><span style="position:absolute; left:0; top:8px; bottom:8px; width:3px; border-radius:0 3px 3px 0; background:#ff6f61;"></span></sc-if>
<svg width="20" height="20" viewBox="0 0 20 20"><circle cx="10" cy="10" r="7.5" stroke="currentColor" stroke-width="1.4" fill="none"></circle><circle cx="10" cy="10" r="2.6" fill="currentColor"></circle></svg>
<span style="font-family:'JetBrains Mono',monospace; font-size:8px;">ORG</span>
</div>
<div style="position:relative; width:42px; height:48px; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:4px; border-radius:10px; cursor:pointer; color:{{ railCoColor }}; background:{{ railCoBg }};" onClick="{{ goCompany }}">
<sc-if value="{{ railCoOn }}" hint-placeholder-val="{{ false }}"><span style="position:absolute; left:0; top:8px; bottom:8px; width:3px; border-radius:0 3px 3px 0; background:#ff6f61;"></span></sc-if>
<svg width="20" height="20" viewBox="0 0 20 20"><rect x="3.5" y="5" width="6" height="11" rx="1" stroke="currentColor" stroke-width="1.4" fill="none"></rect><rect x="10.5" y="2.5" width="6" height="13.5" rx="1" stroke="currentColor" stroke-width="1.4" fill="none"></rect></svg>
<span style="font-family:'JetBrains Mono',monospace; font-size:8px;">CO</span>
</div>
<div style="position:relative; width:42px; height:48px; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:4px; border-radius:10px; cursor:pointer; color:{{ railTeamColor }}; background:{{ railTeamBg }};" onClick="{{ goTeam }}">
<sc-if value="{{ railTeamOn }}" hint-placeholder-val="{{ true }}"><span style="position:absolute; left:0; top:8px; bottom:8px; width:3px; border-radius:0 3px 3px 0; background:#ff6f61;"></span></sc-if>
<svg width="20" height="20" viewBox="0 0 20 20"><circle cx="10" cy="5" r="2.2" fill="currentColor"></circle><circle cx="5" cy="13.5" r="2.2" fill="currentColor"></circle><circle cx="15" cy="13.5" r="2.2" fill="currentColor"></circle><path d="M10 5 L5 13.5 M10 5 L15 13.5 M5 13.5 L15 13.5" stroke="currentColor" stroke-width="1.1" opacity=".5"></path></svg>
<span style="font-family:'JetBrains Mono',monospace; font-size:8px;">TEAM</span>
</div>
<div style="position:relative; width:42px; height:48px; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:4px; border-radius:10px; cursor:pointer; color:{{ railClawColor }}; background:{{ railClawBg }};" onClick="{{ goClawView }}">
<sc-if value="{{ railClawOn }}" hint-placeholder-val="{{ true }}"><span style="position:absolute; left:0; top:8px; bottom:8px; width:3px; border-radius:0 3px 3px 0; background:#ff6f61;"></span></sc-if>
<svg width="20" height="20" viewBox="0 0 20 20"><rect x="4" y="4" width="12" height="12" rx="3.5" stroke="currentColor" stroke-width="1.4" fill="none"></rect><circle cx="10" cy="10" r="2.4" fill="currentColor"></circle></svg>
<span style="font-family:'JetBrains Mono',monospace; font-size:8px;">CLAW</span>
</div>
</div>
<div style="flex:1;"></div>
<div style="width:30px; height:30px; border-radius:8px; border:1px dashed rgba(255,255,255,.16); display:flex; align-items:center; justify-content:center; color:#ff6f61; font-size:18px; font-weight:300; cursor:pointer;">+</div>
</div>
<!-- CONTEXT LIST -->
<div style="width:252px; flex:none; border-right:1px solid rgba(255,255,255,.06); background:#0b0b0e; display:flex; flex-direction:column; min-height:0;">
<sc-if value="{{ isTeam }}" hint-placeholder-val="{{ true }}">
<div style="padding:16px 16px 12px;">
<div style="font-family:'JetBrains Mono',monospace; font-size:10px; letter-spacing:.12em; color:#5a5a62; margin-bottom:6px;">TEAM · 6 CLAWS</div>
<div style="font-size:18px; font-weight:700; color:#f3f3f5; letter-spacing:-.01em;">Growth Team</div>
</div>
<div style="display:flex; gap:18px; padding:0 16px; border-bottom:1px solid rgba(255,255,255,.06);">
<div style="font-size:12px; font-weight:600; color:#f3f3f5; padding-bottom:9px; border-bottom:2px solid #ff6f61;">Members</div>
<div style="font-size:12px; font-weight:500; color:#6a6a72; padding-bottom:9px; border-bottom:2px solid transparent; cursor:pointer;">Templates</div>
</div>
<div style="flex:1; overflow-y:auto; padding:8px;">
<div style="display:flex; flex-direction:column; gap:2px;">
<sc-for list="{{ nodes }}" as="m" hint-placeholder-count="6">
<div style="display:flex; align-items:center; gap:10px; padding:8px 10px; border-radius:9px; cursor:pointer; position:relative; transition:background .15s; background:{{ m.rowBg }};" onClick="{{ m.onSelect }}">
<sc-if value="{{ m.selected }}" hint-placeholder-val="{{ false }}"><span style="position:absolute; left:0; top:8px; bottom:8px; width:3px; border-radius:0 3px 3px 0; background:#ff6f61;"></span></sc-if>
<div style="width:30px; height:30px; flex:none; border-radius:8px; background:{{ m.grad }}; display:flex; align-items:center; justify-content:center; font-size:12px; font-weight:700; color:{{ m.ink }};">{{ m.initial }}</div>
<div style="flex:1; min-width:0;">
<div style="font-size:13px; font-weight:600; color:{{ m.labelColor }};">{{ m.name }}</div>
<div style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#6a6a72;">{{ m.role }}</div>
</div>
<span style="width:7px; height:7px; border-radius:50%; background:{{ m.statusColor }};"></span>
</div>
</sc-for>
</div>
</div>
<div style="flex:none; padding:10px 12px; border-top:1px solid rgba(255,255,255,.06);">
<div style="display:flex; align-items:center; justify-content:center; gap:7px; height:34px; border-radius:8px; background:rgba(255,111,97,.1); border:1px solid rgba(255,111,97,.25); color:#ff8a7a; font-size:12px; font-weight:600; cursor:pointer;">
<span style="font-size:15px; line-height:1;">+</span> Deploy claw from template
</div>
</div>
</sc-if>
<sc-if value="{{ isCompany }}" hint-placeholder-val="{{ false }}">
<div style="padding:16px 16px 12px;">
<div style="font-family:'JetBrains Mono',monospace; font-size:10px; letter-spacing:.12em; color:#5a5a62; margin-bottom:6px;">COMPANY · 4 TEAMS</div>
<div style="font-size:18px; font-weight:700; color:#f3f3f5; letter-spacing:-.01em;">Acme Corp</div>
</div>
<div style="display:flex; gap:18px; padding:0 16px; border-bottom:1px solid rgba(255,255,255,.06);">
<div style="font-size:12px; font-weight:600; color:#f3f3f5; padding-bottom:9px; border-bottom:2px solid #ff6f61;">Teams</div>
<div style="font-size:12px; font-weight:500; color:#6a6a72; padding-bottom:9px; border-bottom:2px solid transparent; cursor:pointer;">Templates</div>
</div>
<div style="flex:1; overflow-y:auto; padding:8px;">
<div style="display:flex; flex-direction:column; gap:2px;">
<div style="display:flex; align-items:center; gap:10px; padding:9px 10px; border-radius:9px; cursor:pointer;" onClick="{{ enterTeam }}">
<div style="width:30px; height:30px; flex:none; border-radius:8px; background:#141417; border:1px solid rgba(255,255,255,.08); display:flex; align-items:center; justify-content:center;"><span style="width:8px; height:8px; border-radius:2px; background:#5ec8d8;"></span></div>
<div style="flex:1;"><div style="font-size:13px; font-weight:600; color:#eaeaee;">Intake Team</div><div style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#6a6a72;">star-MoE · 3</div></div>
</div>
<div style="display:flex; align-items:center; gap:10px; padding:9px 10px; border-radius:9px; cursor:pointer; background:rgba(255,111,97,.12); border:1px solid rgba(255,111,97,.28); position:relative;" onClick="{{ enterTeam }}">
<span style="position:absolute; left:0; top:8px; bottom:8px; width:3px; border-radius:0 3px 3px 0; background:#ff6f61;"></span>
<div style="width:30px; height:30px; flex:none; border-radius:8px; background:#1a1216; border:1px solid rgba(255,111,97,.3); display:flex; align-items:center; justify-content:center;"><span style="width:8px; height:8px; border-radius:50%; background:#ff6f61;"></span></div>
<div style="flex:1;"><div style="font-size:13px; font-weight:600; color:#fff;">Growth Team</div><div style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#ff8a7a;">hub-spoke · 6 · running</div></div>
</div>
<div style="display:flex; align-items:center; gap:10px; padding:9px 10px; border-radius:9px; cursor:pointer;" onClick="{{ enterTeam }}">
<div style="width:30px; height:30px; flex:none; border-radius:8px; background:#141417; border:1px solid rgba(255,255,255,.08); display:flex; align-items:center; justify-content:center;"><span style="width:8px; height:8px; border-radius:50%; background:#5ec8d8;"></span></div>
<div style="flex:1;"><div style="font-size:13px; font-weight:600; color:#eaeaee;">Research Team</div><div style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#6a6a72;">blackboard · 4</div></div>
</div>
<div style="display:flex; align-items:center; gap:10px; padding:9px 10px; border-radius:9px; cursor:pointer;" onClick="{{ enterTeam }}">
<div style="width:30px; height:30px; flex:none; border-radius:8px; background:#141417; border:1px solid rgba(255,255,255,.08); display:flex; align-items:center; justify-content:center;"><span style="width:8px; height:8px; border-radius:2px; background:#3a3a40;"></span></div>
<div style="flex:1;"><div style="font-size:13px; font-weight:600; color:#eaeaee;">Ops Team</div><div style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#6a6a72;">holacratic · 5</div></div>
</div>
</div>
</div>
<div style="flex:none; padding:10px 12px; border-top:1px solid rgba(255,255,255,.06);">
<div style="display:flex; align-items:center; justify-content:center; gap:7px; height:34px; border-radius:8px; background:rgba(255,111,97,.1); border:1px solid rgba(255,111,97,.25); color:#ff8a7a; font-size:12px; font-weight:600; cursor:pointer;">
<span style="font-size:15px; line-height:1;">+</span> Deploy team from template
</div>
</div>
</sc-if>
<sc-if value="{{ isOrg }}" hint-placeholder-val="{{ false }}">
<div style="padding:16px 16px 12px;">
<div style="font-family:'JetBrains Mono',monospace; font-size:10px; letter-spacing:.12em; color:#5a5a62; margin-bottom:6px;">ORG · 3 COMPANIES</div>
<div style="font-size:18px; font-weight:700; color:#f3f3f5; letter-spacing:-.01em;">Acme Org</div>
</div>
<div style="display:flex; gap:18px; padding:0 16px; border-bottom:1px solid rgba(255,255,255,.06);">
<div style="font-size:12px; font-weight:600; color:#f3f3f5; padding-bottom:9px; border-bottom:2px solid #ff6f61;">Companies</div>
<div style="font-size:12px; font-weight:500; color:#6a6a72; padding-bottom:9px; border-bottom:2px solid transparent; cursor:pointer;">Templates</div>
</div>
<div style="flex:1; overflow-y:auto; padding:8px;">
<div style="display:flex; flex-direction:column; gap:2px;">
<div style="display:flex; align-items:center; gap:10px; padding:9px 10px; border-radius:9px; cursor:pointer; background:rgba(255,111,97,.12); border:1px solid rgba(255,111,97,.28); position:relative;" onClick="{{ goCompany }}">
<span style="position:absolute; left:0; top:8px; bottom:8px; width:3px; border-radius:0 3px 3px 0; background:#ff6f61;"></span>
<div style="width:30px; height:30px; flex:none; border-radius:8px; background:#1a1216; border:1px solid rgba(255,111,97,.3); display:flex; align-items:center; justify-content:center; color:#ff6f61;"><svg width="15" height="15" viewBox="0 0 20 20"><rect x="3.5" y="5" width="6" height="11" rx="1" stroke="currentColor" stroke-width="1.4" fill="none"></rect><rect x="10.5" y="2.5" width="6" height="13.5" rx="1" stroke="currentColor" stroke-width="1.4" fill="none"></rect></svg></div>
<div style="flex:1;"><div style="font-size:13px; font-weight:600; color:#fff;">Acme Corp</div><div style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#ff8a7a;">4 teams · 18 claws</div></div>
</div>
<div style="display:flex; align-items:center; gap:10px; padding:9px 10px; border-radius:9px; cursor:pointer;" onClick="{{ goCompany }}">
<div style="width:30px; height:30px; flex:none; border-radius:8px; background:#141417; border:1px solid rgba(255,255,255,.08); display:flex; align-items:center; justify-content:center; color:#6a6a72;"><svg width="15" height="15" viewBox="0 0 20 20"><rect x="3.5" y="5" width="6" height="11" rx="1" stroke="currentColor" stroke-width="1.4" fill="none"></rect><rect x="10.5" y="2.5" width="6" height="13.5" rx="1" stroke="currentColor" stroke-width="1.4" fill="none"></rect></svg></div>
<div style="flex:1;"><div style="font-size:13px; font-weight:600; color:#eaeaee;">Helix Labs</div><div style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#6a6a72;">2 teams · 9 claws</div></div>
</div>
<div style="display:flex; align-items:center; gap:10px; padding:9px 10px; border-radius:9px; cursor:pointer;" onClick="{{ goCompany }}">
<div style="width:30px; height:30px; flex:none; border-radius:8px; background:#141417; border:1px solid rgba(255,255,255,.08); display:flex; align-items:center; justify-content:center; color:#6a6a72;"><svg width="15" height="15" viewBox="0 0 20 20"><rect x="3.5" y="5" width="6" height="11" rx="1" stroke="currentColor" stroke-width="1.4" fill="none"></rect><rect x="10.5" y="2.5" width="6" height="13.5" rx="1" stroke="currentColor" stroke-width="1.4" fill="none"></rect></svg></div>
<div style="flex:1;"><div style="font-size:13px; font-weight:600; color:#eaeaee;">Vega Studio</div><div style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#6a6a72;">1 team · 4 claws</div></div>
</div>
</div>
</div>
<div style="flex:none; padding:10px 12px; border-top:1px solid rgba(255,255,255,.06);">
<div style="display:flex; align-items:center; justify-content:center; gap:7px; height:34px; border-radius:8px; background:rgba(255,111,97,.1); border:1px solid rgba(255,111,97,.25); color:#ff8a7a; font-size:12px; font-weight:600; cursor:pointer;">
<span style="font-size:15px; line-height:1;">+</span> Deploy company from template
</div>
</div>
</sc-if>
<sc-if value="{{ isClaw }}" hint-placeholder-val="{{ false }}">
<div style="padding:14px 16px 12px;">
<div style="display:flex; align-items:center; gap:8px; font-family:'JetBrains Mono',monospace; font-size:10px; color:#6a6a72; cursor:pointer; margin-bottom:12px;" onClick="{{ goTeam }}">← Growth Team</div>
<div style="display:flex; align-items:center; gap:11px;">
<div style="width:42px; height:42px; flex:none; border-radius:11px; background:{{ selGrad }}; display:flex; align-items:center; justify-content:center; font-size:17px; font-weight:700; color:{{ selInk }};">{{ selInitial }}</div>
<div style="min-width:0;">
<div style="font-size:17px; font-weight:700; color:#f3f3f5; letter-spacing:-.01em;">{{ selName }}</div>
<div style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#6a6a72;">{{ selRole }} · Growth Team</div>
</div>
</div>
<div style="margin-top:11px; display:flex; align-items:center; gap:7px; padding:7px 10px; border-radius:8px; background:#101014; border:1px solid rgba(255,255,255,.07);">
<span style="width:7px; height:7px; border-radius:50%; background:#ff6f61;"></span>
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#9a9aa2;">Claude Sonnet 4.5</span>
<span style="flex:1;"></span>
<span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#5fd08a;">online</span>
</div>
</div>
<div style="padding:0 12px 6px;">
<div style="font-family:'JetBrains Mono',monospace; font-size:10px; letter-spacing:.12em; color:#5a5a62; padding:6px 6px 8px;">COMPARTMENTS</div>
<div style="display:flex; flex-direction:column; gap:1px;">
<div style="display:flex; align-items:center; gap:9px; padding:8px 10px; border-radius:8px; background:rgba(255,111,97,.1); color:#f3f3f5; font-size:12px; font-weight:600;"><span style="width:6px; height:6px; border-radius:50%; background:#ff6f61;"></span>Anatomy</div>
<div style="display:flex; align-items:center; gap:9px; padding:8px 10px; border-radius:8px; color:#9a9aa2; font-size:12px; cursor:pointer;"><span style="width:6px; height:6px; border-radius:50%; background:#ff6f61;"></span>Skills <span style="flex:1;"></span><span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#5a5a62;">7</span></div>
<div style="display:flex; align-items:center; gap:9px; padding:8px 10px; border-radius:8px; color:#9a9aa2; font-size:12px; cursor:pointer;"><span style="width:6px; height:6px; border-radius:50%; background:#5ec8d8;"></span>Tools <span style="flex:1;"></span><span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#5a5a62;">4</span></div>
<div style="display:flex; align-items:center; gap:9px; padding:8px 10px; border-radius:8px; color:#9a9aa2; font-size:12px; cursor:pointer;"><span style="width:6px; height:6px; border-radius:50%; background:#c98af0;"></span>Personality</div>
<div style="display:flex; align-items:center; gap:9px; padding:8px 10px; border-radius:8px; color:#9a9aa2; font-size:12px; cursor:pointer;"><span style="width:6px; height:6px; border-radius:50%; background:#e8b465;"></span>Capabilities</div>
<div style="display:flex; align-items:center; gap:9px; padding:8px 10px; border-radius:8px; color:#9a9aa2; font-size:12px; cursor:pointer;"><span style="width:6px; height:6px; border-radius:50%; background:#5fd08a;"></span>Memory</div>
<div style="display:flex; align-items:center; gap:9px; padding:8px 10px; border-radius:8px; color:#9a9aa2; font-size:12px; cursor:pointer;"><span style="width:6px; height:6px; border-radius:50%; background:#6fd0c0;"></span>Safety · §15</div>
</div>
</div>
</sc-if>
</div>
<!-- CANVAS -->
<div style="flex:1; position:relative; min-width:0; overflow:hidden;">
<sc-if value="{{ isTeam }}" hint-placeholder-val="{{ true }}">
<div style="position:absolute; inset:0; display:flex; flex-direction:column; background:radial-gradient(120% 90% at 55% 40%, #0e0e13 0%, #08080a 70%);">
<!-- topology morph selector (reserved band) -->
<div style="flex:none; display:flex; align-items:center; gap:10px; padding:14px 18px 10px;">
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; letter-spacing:.12em; color:#5a5a62; flex:none;">TOPOLOGY</span>
<div style="flex:1; min-width:0; display:flex; gap:6px; overflow-x:auto; white-space:nowrap; padding-bottom:2px;">
<sc-for list="{{ topos }}" as="t" hint-placeholder-count="6">
<div style="flex:none; font-family:'JetBrains Mono',monospace; font-size:11px; font-weight:600; padding:5px 11px; border-radius:7px; cursor:pointer; transition:all .2s; {{ t.style }}" onClick="{{ t.onPick }}">{{ t.label }}</div>
</sc-for>
</div>
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#5ec8d8; flex:none; cursor:pointer; padding:5px 10px; border:1px solid rgba(94,200,216,.22); border-radius:7px;">⇄ compare all</span>
</div>
<!-- current topology caption -->
<div style="flex:none; padding:0 18px 4px;">
<div style="font-size:22px; font-weight:700; color:#f3f3f5; letter-spacing:-.01em;">{{ topoLabel }}</div>
<div style="font-family:'JetBrains Mono',monospace; font-size:11px; color:#6a6a72; margin-top:2px;">{{ topoKind }}</div>
</div>
<span style="position:absolute; bottom:14px; left:18px; right:18px; z-index:5; font-family:'JetBrains Mono',monospace; font-size:10px; color:#3a3a40;">click a claw to open its computer · pick a topology to re-wire the team</span>
<!-- graph stage -->
<div style="position:relative; flex:1; min-height:0;">
<svg viewBox="0 0 100 100" preserveAspectRatio="none" style="position:absolute; inset:0; width:100%; height:100%;">
<sc-for list="{{ linkPaths }}" as="lk" hint-placeholder-count="5">
<path d="{{ lk.d }}" fill="none" stroke="{{ lk.stroke }}" stroke-width="{{ lk.width }}" stroke-dasharray="{{ lk.dash }}" vector-effect="non-scaling-stroke" style="{{ lk.anim }} transition:stroke .3s;"></path>
</sc-for>
</svg>
<sc-for list="{{ nodes }}" as="n" hint-placeholder-count="6">
<div style="position:absolute; left:{{ n.x }}; top:{{ n.y }}; transform:translate(-50%,-50%); transition:left .55s cubic-bezier(.4,0,.2,1), top .55s cubic-bezier(.4,0,.2,1); display:flex; flex-direction:column; align-items:center; gap:7px; z-index:3; cursor:pointer;" onClick="{{ n.onSelect }}">
<div style="position:relative; width:50px; height:50px;">
<sc-if value="{{ n.running }}" hint-placeholder-val="{{ false }}"><div style="position:absolute; inset:0; border-radius:50%; background:rgba(94,200,216,.4); animation:cm-halo 1.8s ease-out infinite;"></div></sc-if>
<sc-if value="{{ n.selected }}" hint-placeholder-val="{{ false }}"><div style="position:absolute; inset:-6px; border-radius:50%; border:2px solid #ff6f61; box-shadow:0 0 0 4px rgba(255,111,97,.12);"></div></sc-if>
<div style="position:relative; width:50px; height:50px; border-radius:50%; background:{{ n.grad }}; display:flex; align-items:center; justify-content:center; font-size:18px; font-weight:700; color:{{ n.ink }}; box-shadow:0 0 28px rgba(0,0,0,.45);">{{ n.initial }}</div>
<span style="position:absolute; right:-1px; bottom:-1px; width:12px; height:12px; border-radius:50%; background:{{ n.statusColor }}; border:2px solid #0a0a0c;"></span>
</div>
<div style="text-align:center;">
<div style="font-size:12px; font-weight:600; color:{{ n.labelColor }};">{{ n.name }}</div>
<div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72;">{{ n.role }}</div>
</div>
</div>
</sc-for>
</div>
</div>
</sc-if>
<sc-if value="{{ isCompany }}" hint-placeholder-val="{{ false }}">
<div style="position:absolute; inset:0; background:#08080a; background-image:linear-gradient(rgba(255,255,255,.022) 1px, transparent 1px), linear-gradient(90deg, rgba(255,255,255,.022) 1px, transparent 1px); background-size:28px 28px;">
<div style="position:absolute; top:16px; left:18px; z-index:5; display:flex; align-items:center; gap:10px;">
<span style="font-family:'JetBrains Mono',monospace; font-size:11px; letter-spacing:.1em; color:#e8b465; padding:4px 9px; border-radius:6px; border:1px solid rgba(232,196,106,.25); background:rgba(232,196,106,.07);">PIPELINE</span>
<span style="font-family:'JetBrains Mono',monospace; font-size:11px; color:#5a5a62;">company topology · click a team to enter its claws</span>
</div>
<div style="position:absolute; top:14px; right:18px; z-index:5; font-family:'JetBrains Mono',monospace; font-size:10px; color:#5a5a62;">x:248 y:112 · z:1.0×</div>
<svg viewBox="0 0 100 100" preserveAspectRatio="none" style="position:absolute; inset:0; width:100%; height:100%;">
<path d="M16,34 H31 V22 H46" fill="none" stroke="rgba(94,200,216,.5)" stroke-width="1.6" stroke-dasharray="3 4" vector-effect="non-scaling-stroke" style="animation:cm-flow 1.1s linear infinite;"></path>
<path d="M16,34 H30 V66 H44" fill="none" stroke="rgba(232,196,106,.45)" stroke-width="1.6" stroke-dasharray="3 4" vector-effect="non-scaling-stroke" style="animation:cm-flow 1.4s linear infinite;"></path>
<path d="M46,22 H63 V46 H80" fill="none" stroke="rgba(255,111,97,.55)" stroke-width="1.8" stroke-dasharray="3 4" vector-effect="non-scaling-stroke" style="animation:cm-flow .9s linear infinite;"></path>
<path d="M44,66 H62 V46 H80" fill="none" stroke="rgba(255,255,255,.12)" stroke-width="1.5" vector-effect="non-scaling-stroke"></path>
</svg>
<div style="position:absolute; left:16%; top:34%; transform:translate(-50%,-50%); width:158px; z-index:3; cursor:pointer;" onClick="{{ enterTeam }}">
<div style="border-radius:12px; background:#0f0f13; border:1px solid rgba(255,255,255,.09); padding:11px 12px; box-shadow:0 8px 24px rgba(0,0,0,.4);">
<div style="display:flex; align-items:center; gap:7px; margin-bottom:8px;"><span style="width:7px; height:7px; border-radius:2px; background:#5ec8d8;"></span><span style="font-size:13px; font-weight:700; color:#eaeaee;">Intake Team</span></div>
<div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72; margin-bottom:9px;">star-MoE · 3 claws</div>
<div style="display:flex; gap:4px;"><span style="width:15px; height:15px; border-radius:5px; background:linear-gradient(135deg,#6fd0c0,#4aa3b8);"></span><span style="width:15px; height:15px; border-radius:5px; background:linear-gradient(135deg,#8a9af0,#5a6ad8);"></span><span style="width:15px; height:15px; border-radius:5px; background:linear-gradient(135deg,#e8c46a,#d89a3a);"></span></div>
</div>
</div>
<div style="position:absolute; left:46%; top:22%; transform:translate(-50%,-50%); width:170px; z-index:4; cursor:pointer;" onClick="{{ enterTeam }}">
<div style="border-radius:12px; background:#141014; border:1.5px solid #ff6f61; padding:11px 12px; box-shadow:0 0 0 4px rgba(255,111,97,.1), 0 10px 28px rgba(0,0,0,.5);">
<div style="display:flex; align-items:center; gap:7px; margin-bottom:8px;"><span style="width:7px; height:7px; border-radius:50%; background:#ff6f61; animation:cm-blink 1.5s infinite;"></span><span style="font-size:13px; font-weight:700; color:#fff;">Growth Team</span><span style="flex:1;"></span><span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#ff8a7a;">▾ enter</span></div>
<div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#ff8a7a; margin-bottom:9px;">hub-spoke · 6 claws · running</div>
<div style="display:flex; gap:4px;"><span style="width:15px; height:15px; border-radius:5px; background:linear-gradient(135deg,#ff9a6a,#ff6f4a);"></span><span style="width:15px; height:15px; border-radius:5px; background:linear-gradient(135deg,#6fd0c0,#4aa3b8);"></span><span style="width:15px; height:15px; border-radius:5px; background:linear-gradient(135deg,#8a9af0,#5a6ad8);"></span><span style="width:15px; height:15px; border-radius:5px; background:linear-gradient(135deg,#e8c46a,#d89a3a);"></span><span style="width:15px; height:15px; border-radius:5px; background:#1a1a1e; display:flex; align-items:center; justify-content:center; font-family:'JetBrains Mono',monospace; font-size:8px; color:#8a8a92;">+2</span></div>
</div>
</div>
<div style="position:absolute; left:44%; top:66%; transform:translate(-50%,-50%); width:158px; z-index:3; cursor:pointer;" onClick="{{ enterTeam }}">
<div style="border-radius:12px; background:#0f0f13; border:1px solid rgba(255,255,255,.09); padding:11px 12px; box-shadow:0 8px 24px rgba(0,0,0,.4);">
<div style="display:flex; align-items:center; gap:7px; margin-bottom:8px;"><span style="width:7px; height:7px; border-radius:50%; background:#5ec8d8; animation:cm-blink 1.7s infinite;"></span><span style="font-size:13px; font-weight:700; color:#eaeaee;">Research Team</span></div>
<div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72; margin-bottom:9px;">blackboard · 4 claws</div>
<div style="display:flex; gap:4px;"><span style="width:15px; height:15px; border-radius:5px; background:linear-gradient(135deg,#8a9af0,#5a6ad8);"></span><span style="width:15px; height:15px; border-radius:5px; background:linear-gradient(135deg,#6fd0c0,#4aa3b8);"></span><span style="width:15px; height:15px; border-radius:5px; background:linear-gradient(135deg,#c98af0,#9a5ad8);"></span><span style="width:15px; height:15px; border-radius:5px; background:#1a1a1e; display:flex; align-items:center; justify-content:center; font-family:'JetBrains Mono',monospace; font-size:8px; color:#8a8a92;">+1</span></div>
</div>
</div>
<div style="position:absolute; left:80%; top:46%; transform:translate(-50%,-50%); width:152px; z-index:3; cursor:pointer;" onClick="{{ enterTeam }}">
<div style="border-radius:12px; background:#0f0f13; border:1px solid rgba(255,255,255,.09); padding:11px 12px; box-shadow:0 8px 24px rgba(0,0,0,.4);">
<div style="display:flex; align-items:center; gap:7px; margin-bottom:8px;"><span style="width:7px; height:7px; border-radius:50%; background:#3a3a40;"></span><span style="font-size:13px; font-weight:700; color:#eaeaee;">Ops Team</span></div>
<div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72; margin-bottom:9px;">holacratic · 5 claws</div>
<div style="display:flex; gap:4px;"><span style="width:15px; height:15px; border-radius:5px; background:linear-gradient(135deg,#e8c46a,#d89a3a);"></span><span style="width:15px; height:15px; border-radius:5px; background:linear-gradient(135deg,#ff9a6a,#ff6f4a);"></span><span style="width:15px; height:15px; border-radius:5px; background:#1a1a1e; display:flex; align-items:center; justify-content:center; font-family:'JetBrains Mono',monospace; font-size:8px; color:#8a8a92;">+3</span></div>
</div>
</div>
<span style="position:absolute; bottom:16px; left:18px; z-index:5; font-family:'JetBrains Mono',monospace; font-size:10px; color:#3a3a40;">↑ zoom out to Org · ↓ click a team to enter</span>
</div>
</sc-if>
<sc-if value="{{ isOrg }}" hint-placeholder-val="{{ false }}">
<div style="position:absolute; inset:0; background:radial-gradient(120% 90% at 50% 40%, #0e0e13 0%, #08080a 70%);">
<div style="position:absolute; top:16px; left:18px; z-index:5; display:flex; align-items:center; gap:10px;">
<span style="font-family:'JetBrains Mono',monospace; font-size:11px; letter-spacing:.1em; color:#c98af0; padding:4px 9px; border-radius:6px; border:1px solid rgba(201,138,240,.25); background:rgba(201,138,240,.07);">PORTFOLIO</span>
<span style="font-family:'JetBrains Mono',monospace; font-size:11px; color:#5a5a62;">org topology · click a company to descend</span>
</div>
<svg viewBox="0 0 100 100" preserveAspectRatio="none" style="position:absolute; inset:0; width:100%; height:100%;">
<path d="M50,46 L26,28" fill="none" stroke="rgba(255,111,97,.5)" stroke-width="1.8" stroke-dasharray="3 4" vector-effect="non-scaling-stroke" style="animation:cm-flow 1s linear infinite;"></path>
<path d="M50,46 L76,30" fill="none" stroke="rgba(255,255,255,.12)" stroke-width="1.5" vector-effect="non-scaling-stroke"></path>
<path d="M50,46 L62,74" fill="none" stroke="rgba(255,255,255,.12)" stroke-width="1.5" vector-effect="non-scaling-stroke"></path>
</svg>
<div style="position:absolute; left:50%; top:46%; transform:translate(-50%,-50%); display:flex; flex-direction:column; align-items:center; gap:9px; z-index:3;">
<div style="width:60px; height:60px; border-radius:16px; background:#141417; border:1px solid rgba(255,255,255,.1); display:flex; align-items:center; justify-content:center; color:#9a9aa2; box-shadow:0 0 30px rgba(0,0,0,.5);"><svg width="26" height="26" viewBox="0 0 20 20"><circle cx="10" cy="10" r="7.5" stroke="currentColor" stroke-width="1.4" fill="none"></circle><circle cx="10" cy="10" r="2.6" fill="currentColor"></circle></svg></div>
<div style="text-align:center;"><div style="font-size:13px; font-weight:700; color:#fff;">Acme Org</div><div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72;">3 companies</div></div>
</div>
<div style="position:absolute; left:26%; top:28%; transform:translate(-50%,-50%); display:flex; flex-direction:column; align-items:center; gap:8px; z-index:3; cursor:pointer;" onClick="{{ goCompany }}">
<div style="width:54px; height:54px; border-radius:14px; background:linear-gradient(135deg,#ff8a7a,#ff5f57); display:flex; align-items:center; justify-content:center; color:#2a0d0a; box-shadow:0 0 26px rgba(255,111,97,.4);"><svg width="22" height="22" viewBox="0 0 20 20"><rect x="3.5" y="5" width="6" height="11" rx="1" stroke="currentColor" stroke-width="1.6" fill="none"></rect><rect x="10.5" y="2.5" width="6" height="13.5" rx="1" stroke="currentColor" stroke-width="1.6" fill="none"></rect></svg></div>
<div style="text-align:center;"><div style="font-size:12px; font-weight:700; color:#fff;">Acme Corp</div><div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#ff8a7a;">4 teams · enter ▸</div></div>
</div>
<div style="position:absolute; left:76%; top:30%; transform:translate(-50%,-50%); display:flex; flex-direction:column; align-items:center; gap:8px; z-index:3; cursor:pointer;" onClick="{{ goCompany }}">
<div style="width:48px; height:48px; border-radius:13px; background:#141417; border:1px solid rgba(255,255,255,.1); display:flex; align-items:center; justify-content:center; color:#8a8a92;"><svg width="20" height="20" viewBox="0 0 20 20"><rect x="3.5" y="5" width="6" height="11" rx="1" stroke="currentColor" stroke-width="1.4" fill="none"></rect><rect x="10.5" y="2.5" width="6" height="13.5" rx="1" stroke="currentColor" stroke-width="1.4" fill="none"></rect></svg></div>
<div style="text-align:center;"><div style="font-size:12px; font-weight:600; color:#dcdce2;">Helix Labs</div><div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72;">2 teams</div></div>
</div>
<div style="position:absolute; left:62%; top:74%; transform:translate(-50%,-50%); display:flex; flex-direction:column; align-items:center; gap:8px; z-index:3; cursor:pointer;" onClick="{{ goCompany }}">
<div style="width:48px; height:48px; border-radius:13px; background:#141417; border:1px solid rgba(255,255,255,.1); display:flex; align-items:center; justify-content:center; color:#8a8a92;"><svg width="20" height="20" viewBox="0 0 20 20"><rect x="3.5" y="5" width="6" height="11" rx="1" stroke="currentColor" stroke-width="1.4" fill="none"></rect><rect x="10.5" y="2.5" width="6" height="13.5" rx="1" stroke="currentColor" stroke-width="1.4" fill="none"></rect></svg></div>
<div style="text-align:center;"><div style="font-size:12px; font-weight:600; color:#dcdce2;">Vega Studio</div><div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72;">1 team</div></div>
</div>
</div>
</sc-if>
<sc-if value="{{ isClaw }}" hint-placeholder-val="{{ false }}">
<div style="position:absolute; inset:0; display:flex; flex-direction:column; background:radial-gradient(120% 90% at 50% 42%, #100e13 0%, #08080a 70%);">
<div style="flex:none; display:flex; align-items:center; gap:10px; padding:14px 18px 6px;">
<span style="font-family:'JetBrains Mono',monospace; font-size:11px; letter-spacing:.1em; color:#ff8a7a; padding:4px 9px; border-radius:6px; border:1px solid rgba(255,111,97,.25); background:rgba(255,111,97,.07);">ANATOMY</span>
<span style="font-family:'JetBrains Mono',monospace; font-size:11px; color:#6a6a72;">what {{ selName }} is made of · compartments wired to the core</span>
</div>
<div style="position:relative; flex:1; min-height:0; overflow:auto; padding:22px 20px 28px;">
<div style="max-width:820px; margin:0 auto; display:flex; flex-direction:column; align-items:center; gap:20px;">
<!-- core -->
<div style="display:flex; flex-direction:column; align-items:center; gap:9px;">
<div style="position:relative; width:78px; height:78px;">
<div style="position:absolute; inset:-7px; border-radius:50%; border:1.5px solid rgba(255,111,97,.35);"></div>
<div style="position:absolute; inset:0; border-radius:50%; background:rgba(255,111,97,.32); animation:cm-halo 2s ease-out infinite;"></div>
<div style="position:relative; width:78px; height:78px; border-radius:50%; background:{{ selGrad }}; display:flex; align-items:center; justify-content:center; font-size:28px; font-weight:700; color:{{ selInk }}; box-shadow:0 0 44px rgba(255,111,97,.45);">{{ selInitial }}</div>
</div>
<div style="text-align:center;">
<div style="font-size:14px; font-weight:700; color:#fff;">{{ selName }}</div>
<div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#8a8a92; letter-spacing:.04em;">CLAUDE SONNET · CORE</div>
</div>
</div>
<div style="width:100%; display:grid; grid-template-columns:repeat(auto-fit, minmax(196px, 1fr)); gap:14px; align-items:start;">
<!-- SKILLS -->
<div style="width:100%;">
<div style="border-radius:12px; background:#0f0f13; border:1px solid rgba(255,111,97,.22); padding:11px; box-shadow:0 8px 22px rgba(0,0,0,.4);">
<div style="display:flex; align-items:center; gap:7px; margin-bottom:9px;"><span style="width:18px; height:18px; border-radius:5px; background:rgba(255,111,97,.16); display:flex; align-items:center; justify-content:center;"><svg width="11" height="11" viewBox="0 0 18 18"><path d="M10 1 L3 10 H8 L7 17 L15 7 H9 Z" fill="#ff6f61"></path></svg></span><span style="font-family:'JetBrains Mono',monospace; font-size:10px; letter-spacing:.08em; color:#ff8a7a;">SKILLS</span><span style="flex:1;"></span><span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#5a5a62;">7</span></div>
<div style="display:flex; flex-wrap:wrap; gap:4px;">
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#cfcfd5; padding:3px 7px; border-radius:5px; background:rgba(255,255,255,.05);">Web research</span>
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#cfcfd5; padding:3px 7px; border-radius:5px; background:rgba(255,255,255,.05);">Summarize</span>
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#cfcfd5; padding:3px 7px; border-radius:5px; background:rgba(255,255,255,.05);">Data viz</span>
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#8a8a92; padding:3px 7px; border-radius:5px; background:rgba(255,255,255,.03);">+4</span>
</div>
</div>
</div>
<!-- PERSONALITY -->
<div style="width:100%;">
<div style="border-radius:12px; background:#0f0f13; border:1px solid rgba(201,138,240,.22); padding:11px; box-shadow:0 8px 22px rgba(0,0,0,.4);">
<div style="display:flex; align-items:center; gap:7px; margin-bottom:9px;"><span style="width:18px; height:18px; border-radius:5px; background:rgba(201,138,240,.16); display:flex; align-items:center; justify-content:center;"><svg width="11" height="11" viewBox="0 0 18 18"><circle cx="9" cy="6" r="3.4" fill="#c98af0"></circle><path d="M3 16c0-3.3 2.7-6 6-6s6 2.7 6 6" fill="#c98af0"></path></svg></span><span style="font-family:'JetBrains Mono',monospace; font-size:10px; letter-spacing:.08em; color:#c98af0;">PERSONALITY</span></div>
<div style="display:flex; flex-wrap:wrap; gap:4px;">
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#cfcfd5; padding:3px 7px; border-radius:5px; background:rgba(255,255,255,.05);">Analytical</span>
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#cfcfd5; padding:3px 7px; border-radius:5px; background:rgba(255,255,255,.05);">Concise</span>
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#cfcfd5; padding:3px 7px; border-radius:5px; background:rgba(255,255,255,.05);">Skeptical</span>
</div>
</div>
</div>
<!-- MEMORY -->
<div style="width:100%;">
<div style="border-radius:12px; background:#0f0f13; border:1px solid rgba(95,208,138,.22); padding:11px; box-shadow:0 8px 22px rgba(0,0,0,.4);">
<div style="display:flex; align-items:center; gap:7px; margin-bottom:9px;"><span style="width:18px; height:18px; border-radius:5px; background:rgba(95,208,138,.16); display:flex; align-items:center; justify-content:center;"><svg width="11" height="11" viewBox="0 0 18 18"><rect x="3" y="3" width="12" height="12" rx="2" fill="none" stroke="#5fd08a" stroke-width="1.6"></rect><path d="M6 7h6M6 11h4" stroke="#5fd08a" stroke-width="1.4"></path></svg></span><span style="font-family:'JetBrains Mono',monospace; font-size:10px; letter-spacing:.08em; color:#5fd08a;">MEMORY</span></div>
<div style="display:flex; flex-direction:column; gap:5px; font-family:'JetBrains Mono',monospace; font-size:10px; color:#9a9aa2;">
<div style="display:flex;"><span style="flex:1;">Long-term</span><span style="color:#cfcfd5;">2,418 notes</span></div>
<div style="display:flex;"><span style="flex:1;">Recent ctx</span><span style="color:#cfcfd5;">18 msgs</span></div>
</div>
</div>
</div>
<!-- TOOLS -->
<div style="width:100%;">
<div style="border-radius:12px; background:#0f0f13; border:1px solid rgba(94,200,216,.22); padding:11px; box-shadow:0 8px 22px rgba(0,0,0,.4);">
<div style="display:flex; align-items:center; gap:7px; margin-bottom:9px;"><span style="width:18px; height:18px; border-radius:5px; background:rgba(94,200,216,.16); display:flex; align-items:center; justify-content:center;"><svg width="11" height="11" viewBox="0 0 18 18"><path d="M4 3v12M4 4h8l-2 3 2 3H4" fill="none" stroke="#5ec8d8" stroke-width="1.5" stroke-linejoin="round"></path></svg></span><span style="font-family:'JetBrains Mono',monospace; font-size:10px; letter-spacing:.08em; color:#5ec8d8;">TOOLS · DOORS</span></div>
<div style="display:flex; flex-direction:column; gap:4px; font-family:'JetBrains Mono',monospace; font-size:10px;">
<div style="display:flex; align-items:center;"><span style="flex:1; color:#cfcfd5;">Email</span><span style="color:#5fd08a;">gated ✓</span></div>
<div style="display:flex; align-items:center;"><span style="flex:1; color:#cfcfd5;">Slack</span><span style="color:#5fd08a;">gated ✓</span></div>
<div style="display:flex; align-items:center;"><span style="flex:1; color:#cfcfd5;">Browser</span><span style="color:#5fd08a;">gated ✓</span></div>
<div style="display:flex; align-items:center;"><span style="flex:1; color:#7a7a82;">Shell</span><span style="color:#e8b465;">blocked ⨯</span></div>
</div>
</div>
</div>
<!-- CAPABILITIES -->
<div style="width:100%;">
<div style="border-radius:12px; background:#0f0f13; border:1px solid rgba(232,196,106,.22); padding:11px; box-shadow:0 8px 22px rgba(0,0,0,.4);">
<div style="display:flex; align-items:center; gap:7px; margin-bottom:9px;"><span style="width:18px; height:18px; border-radius:5px; background:rgba(232,196,106,.16); display:flex; align-items:center; justify-content:center;"><svg width="11" height="11" viewBox="0 0 18 18"><circle cx="9" cy="9" r="6" fill="none" stroke="#e8b465" stroke-width="1.6"></circle><path d="M9 5v4l3 2" stroke="#e8b465" stroke-width="1.4" fill="none"></path></svg></span><span style="font-family:'JetBrains Mono',monospace; font-size:10px; letter-spacing:.08em; color:#e8b465;">CAPABILITIES</span></div>
<div style="display:flex; flex-wrap:wrap; gap:4px;">
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#cfcfd5; padding:3px 7px; border-radius:5px; background:rgba(255,255,255,.05);">File mgmt</span>
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#cfcfd5; padding:3px 7px; border-radius:5px; background:rgba(255,255,255,.05);">Scheduling</span>
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#cfcfd5; padding:3px 7px; border-radius:5px; background:rgba(255,255,255,.05);">Code exec</span>
</div>
</div>
</div>
<!-- SAFETY -->
<div style="width:100%;">
<div style="border-radius:12px; background:#0f0f13; border:1px solid rgba(111,208,192,.22); padding:11px; box-shadow:0 8px 22px rgba(0,0,0,.4);">
<div style="display:flex; align-items:center; gap:7px; margin-bottom:9px;"><span style="width:18px; height:18px; border-radius:5px; background:rgba(111,208,192,.16); display:flex; align-items:center; justify-content:center;"><svg width="11" height="11" viewBox="0 0 18 18"><path d="M9 2l5 2v4c0 4-2.5 6.5-5 8-2.5-1.5-5-4-5-8V4z" fill="none" stroke="#6fd0c0" stroke-width="1.5" stroke-linejoin="round"></path></svg></span><span style="font-family:'JetBrains Mono',monospace; font-size:10px; letter-spacing:.08em; color:#6fd0c0;">SAFETY · §15</span></div>
<div style="display:flex; flex-direction:column; gap:5px; font-family:'JetBrains Mono',monospace; font-size:10px; color:#9a9aa2;">
<div style="display:flex;"><span style="flex:1;">Sandbox</span><span style="color:#6fd0c0;">isolated</span></div>
<div style="display:flex;"><span style="flex:1;">Network</span><span style="color:#6fd0c0;">none</span></div>
</div>
</div>
</div>
</div>
<span style="text-align:center; font-family:'JetBrains Mono',monospace; font-size:10px; color:#3a3a40;">click a compartment to inspect · the computer panel runs this claw's apps & routines</span>
</div>
</div>
</div>
</sc-if>
</div>
<!-- COMPUTER PANEL -->
<sc-if value="{{ showComputer }}" hint-placeholder-val="{{ true }}">
<div style="width:330px; flex:none; border-left:1px solid rgba(255,255,255,.06); background:#0b0b0e; display:flex; flex-direction:column; min-height:0; animation:cm-fade .25s ease;">
<div style="padding:16px 16px 12px; border-bottom:1px solid rgba(255,255,255,.06);">
<div style="display:flex; align-items:center; gap:10px;">
<div style="position:relative; width:38px; height:38px;">
<div style="width:38px; height:38px; border-radius:10px; background:{{ selGrad }}; display:flex; align-items:center; justify-content:center; font-size:16px; font-weight:700; color:{{ selInk }};">{{ selInitial }}</div>
<span style="position:absolute; right:-2px; bottom:-2px; width:11px; height:11px; border-radius:50%; background:#5fd08a; border:2px solid #0b0b0e;"></span>
</div>
<div style="flex:1;">
<div style="font-size:14px; font-weight:700; color:#f3f3f5;">{{ selName }}'s Computer</div>
<div style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#5ec8d8;">● sandbox live · no network</div>
</div>
<div style="width:26px; height:26px; border-radius:7px; border:1px solid rgba(255,255,255,.1); display:flex; align-items:center; justify-content:center; color:#6a6a72; font-size:13px; cursor:pointer;">⤢</div>
</div>
</div>
<div style="flex:1; overflow-y:auto; padding:16px;">
<div style="font-family:'JetBrains Mono',monospace; font-size:10px; letter-spacing:.12em; color:#5a5a62; margin-bottom:12px;">APPS</div>
<div style="display:grid; grid-template-columns:repeat(4,1fr); gap:10px 6px; margin-bottom:22px;">
<div style="display:flex; flex-direction:column; align-items:center; gap:6px;">
<div style="width:48px; height:48px; border-radius:13px; background:#fff; display:flex; align-items:center; justify-content:center; box-shadow:0 3px 10px rgba(0,0,0,.4);"><svg width="26" height="26" viewBox="0 0 26 26"><circle cx="13" cy="13" r="11" fill="#fff" stroke="#e0e0e0"></circle><circle cx="13" cy="13" r="4.4" fill="#4a90e2"></circle><path d="M13 8.6 H24" stroke="#ea4335" stroke-width="3.6"></path><path d="M9.2 11 L4 3.4" stroke="#34a853" stroke-width="3.6"></path><path d="M13 17.4 L7.5 22.5" stroke="#fbbc05" stroke-width="3.6"></path></svg></div>
<span style="font-size:10px; color:#b5b5bd;">Browser</span>
</div>
<div style="display:flex; flex-direction:column; align-items:center; gap:6px;">
<div style="width:48px; height:48px; border-radius:13px; background:#fff; display:flex; align-items:center; justify-content:center; box-shadow:0 3px 10px rgba(0,0,0,.4);"><svg width="22" height="22" viewBox="0 0 22 22"><rect x="9" y="2" width="4" height="11" rx="2" fill="#36c5f0"></rect><rect x="9" y="13" width="4" height="7" rx="2" fill="#2eb67d"></rect><rect x="2" y="9" width="11" height="4" rx="2" fill="#ecb22e"></rect><rect x="9" y="9" width="11" height="4" rx="2" fill="#e01e5a"></rect></svg></div>
<span style="font-size:10px; color:#b5b5bd;">Slack</span>
</div>
<div style="display:flex; flex-direction:column; align-items:center; gap:6px;">
<div style="width:48px; height:48px; border-radius:13px; background:linear-gradient(135deg,#ff8a7a,#ff5f57); display:flex; align-items:center; justify-content:center; box-shadow:0 3px 10px rgba(0,0,0,.4);"><svg width="22" height="22" viewBox="0 0 22 22"><path d="M3 5a2 2 0 012-2h12a2 2 0 012 2v8a2 2 0 01-2 2H8l-4 4v-4H5a2 2 0 01-2-2z" fill="#fff"></path></svg></div>
<span style="font-size:10px; color:#b5b5bd;">Claw Chat</span>
</div>
<div style="display:flex; flex-direction:column; align-items:center; gap:6px;">
<div style="width:48px; height:48px; border-radius:13px; border:1.5px dashed rgba(255,255,255,.18); display:flex; align-items:center; justify-content:center; color:#ff6f61; font-size:22px; font-weight:300; cursor:pointer;">+</div>
<span style="font-size:10px; color:#8a8a92;">Add Apps</span>
</div>
</div>
<div style="font-family:'JetBrains Mono',monospace; font-size:10px; letter-spacing:.12em; color:#5a5a62; margin-bottom:10px;">NOW RUNNING</div>
<div style="display:flex; flex-direction:column; gap:8px;">
<div style="padding:10px 11px; border-radius:10px; background:#101014; border:1px solid rgba(255,255,255,.06);">
<div style="display:flex; align-items:center; gap:7px; margin-bottom:6px;">
<span style="width:6px; height:6px; border-radius:50%; background:#5ec8d8; animation:cm-blink 1.4s infinite;"></span>
<span style="font-size:12px; font-weight:600; color:#e6e6ea;">Q3 competitor scan</span>
<span style="flex:1;"></span>
<span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#5ec8d8;">loop · step 14</span>
</div>
<div style="height:4px; border-radius:2px; background:rgba(255,255,255,.08); overflow:hidden;"><div style="width:62%; height:100%; background:linear-gradient(90deg,#5ec8d8,#4aa3b8);"></div></div>
</div>
<div style="padding:10px 11px; border-radius:10px; background:#101014; border:1px solid rgba(255,255,255,.06); display:flex; align-items:center; gap:7px;">
<span style="width:6px; height:6px; border-radius:50%; background:#e8b465;"></span>
<span style="font-size:12px; font-weight:600; color:#e6e6ea;">Daily digest</span>
<span style="flex:1;"></span>
<span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#e8b465;">cron · 08:00</span>
</div>
</div>
</div>
<div style="flex:none; margin:0 12px 14px; padding:12px; border-radius:14px; background:#121216; border:1px solid rgba(255,255,255,.07); display:flex; justify-content:space-around;">
<div style="display:flex; flex-direction:column; align-items:center; gap:6px;"><div style="width:40px; height:40px; border-radius:11px; background:linear-gradient(135deg,#ff8a7a,#ff5f57); display:flex; align-items:center; justify-content:center;"><svg width="18" height="18" viewBox="0 0 18 18"><path d="M10 1 L3 10 H8 L7 17 L15 7 H9 Z" fill="#fff"></path></svg></div><span style="font-size:10px; color:#b5b5bd;">Skills</span></div>
<div style="display:flex; flex-direction:column; align-items:center; gap:6px;"><div style="width:40px; height:40px; border-radius:11px; background:linear-gradient(135deg,#ff8a7a,#ff5f57); display:flex; align-items:center; justify-content:center;"><svg width="18" height="18" viewBox="0 0 18 18"><path d="M2 5a1.5 1.5 0 011.5-1.5H7l1.5 1.5h6A1.5 1.5 0 0116 6.5v7A1.5 1.5 0 0114.5 15h-11A1.5 1.5 0 012 13.5z" fill="#fff"></path></svg></div><span style="font-size:10px; color:#b5b5bd;">Files</span></div>
<div style="display:flex; flex-direction:column; align-items:center; gap:6px;"><div style="width:40px; height:40px; border-radius:11px; background:linear-gradient(135deg,#ff8a7a,#ff5f57); display:flex; align-items:center; justify-content:center;"><svg width="18" height="18" viewBox="0 0 18 18"><path d="M9 2a5 5 0 00-5 5c0 4-1.5 5-1.5 5h13S14 11 14 7a5 5 0 00-5-5z" fill="#fff"></path><path d="M7.5 15a1.5 1.5 0 003 0" fill="#fff"></path></svg></div><span style="font-size:10px; color:#ff8a7a;">Routines</span></div>
<div style="display:flex; flex-direction:column; align-items:center; gap:6px;"><div style="width:40px; height:40px; border-radius:11px; background:linear-gradient(135deg,#ff8a7a,#ff5f57); display:flex; align-items:center; justify-content:center;"><svg width="18" height="18" viewBox="0 0 18 18"><circle cx="9" cy="9" r="2.6" fill="#fff"></circle><path d="M9 1.5v2M9 14.5v2M1.5 9h2M14.5 9h2M3.7 3.7l1.4 1.4M12.9 12.9l1.4 1.4M14.3 3.7l-1.4 1.4M5.1 12.9l-1.4 1.4" stroke="#fff" stroke-width="1.5"></path></svg></div><span style="font-size:10px; color:#b5b5bd;">Settings</span></div>
</div>
</div>
</sc-if>
</div>
<!-- STATUS BAR -->
<div style="height:28px; flex:none; display:flex; align-items:center; gap:18px; padding:0 16px; border-top:1px solid rgba(255,255,255,.06); background:#0a0a0c; font-family:'JetBrains Mono',monospace; font-size:10px; color:#5a5a62;">
<span style="color:#5fd08a;">● durable runner ok</span>
<span>checkpoint 3s ago</span>
<span style="flex:1;"></span>
<span>§15 sandbox: isolated</span>
<span style="color:#5ec8d8;">2 doors awaiting approval</span>
</div>
</div>
</x-dc>
<script type="text/x-dc" data-dc-script>
class Component extends DCLogic {
state = { tier: 'team', topology: 'hub-spoke', selected: 'morpheus' };
renderVals() {
const s = this.state;
const claws = [
{ id:'atlas', name:'Atlas', role:'team lead', initial:'A', grad:'linear-gradient(135deg,#ff9a6a,#ff6f4a)', ink:'#2a0d05', status:'online' },
{ id:'iris', name:'Iris', role:'researcher', initial:'I', grad:'linear-gradient(135deg,#6fd0c0,#4aa3b8)', ink:'#06201f', status:'running' },
{ id:'echo', name:'Echo', role:'researcher', initial:'E', grad:'linear-gradient(135deg,#8a9af0,#5a6ad8)', ink:'#0a0e2a', status:'running' },
{ id:'nova', name:'Nova', role:'writer', initial:'N', grad:'linear-gradient(135deg,#e8c46a,#d89a3a)', ink:'#2a1d05', status:'online' },
{ id:'sable', name:'Sable', role:'critic', initial:'S', grad:'linear-gradient(135deg,#c98af0,#9a5ad8)', ink:'#1a0a2a', status:'idle' },
{ id:'morpheus',name:'Morpheus',role:'analyst', initial:'M', grad:'linear-gradient(135deg,#ff8a7a,#ff5f57)', ink:'#2a0d0a', status:'online' },
];
const POS = {
'hub-spoke': [[50,48],[22,26],[24,72],[50,15],[78,28],[78,70]],
'pipeline': [[11,50],[27,50],[42,50],[58,50],[73,50],[89,50]],
'ring': [[50,15],[80,32],[80,68],[50,85],[20,68],[20,32]],
'mesh': [[31,26],[69,24],[85,52],[67,80],[31,80],[15,52]],
'swarm': [[40,38],[60,32],[66,55],[48,66],[33,55],[53,47]],
'debate': [[26,22],[26,50],[26,78],[74,22],[74,50],[74,78]],
};
const LNK = {
'hub-spoke': [[0,1],[0,2],[0,3],[0,4],[0,5]],
'pipeline': [[0,1],[1,2],[2,3],[3,4],[4,5]],
'ring': [[0,1],[1,2],[2,3],[3,4],[4,5],[5,0]],
'mesh': [[0,1],[1,2],[2,3],[3,4],[4,5],[5,0],[0,3],[1,4],[2,5]],
'swarm': [[0,1],[0,2],[0,3],[0,4],[0,5],[1,3],[2,4]],
'debate': [[0,3],[1,4],[2,5],[0,4],[1,3],[1,5],[2,4]],
};
const pos = POS[s.topology] || POS['hub-spoke'];
const lnks = LNK[s.topology] || [];
const sc = st => st==='running' ? '#5ec8d8' : st==='online' ? '#5fd08a' : '#3a3a40';
const selIdx = claws.findIndex(c => c.id === s.selected);
const nodes = claws.map((c, i) => ({
name:c.name, role:c.role, initial:c.initial, grad:c.grad, ink:c.ink,
x: pos[i][0] + '%', y: pos[i][1] + '%',
selected: c.id === s.selected,
running: c.status === 'running',
statusColor: sc(c.status),
labelColor: c.id === s.selected ? '#ffffff' : '#dcdce2',
rowBg: c.id === s.selected ? 'rgba(255,111,97,.12)' : 'transparent',
onSelect: () => this.setState({ selected: c.id, tier: 'claw' }),
}));
const linkPaths = lnks.map(([a,b]) => {
const ax=pos[a][0], ay=pos[a][1], bx=pos[b][0], by=pos[b][1];
let stroke='rgba(255,255,255,.12)', width=1.5, dash='0', anim='';
if (a===selIdx || b===selIdx) { stroke='rgba(255,111,97,.6)'; width=1.8; dash='3 4'; anim='animation:cm-flow .9s linear infinite;'; }
else if (a===1||a===2||b===1||b===2) { stroke='rgba(94,200,216,.5)'; width=1.6; dash='3 4'; anim='animation:cm-flow 1.2s linear infinite;'; }
return { d:`M ${ax},${ay} L ${bx},${by}`, stroke, width, dash, anim };
});
const topoMeta = {
'hub-spoke':{label:'Hub-Spoke',kind:'one coordinator routes every claw'},
'pipeline': {label:'Pipeline', kind:'output of each claw feeds the next'},
'ring': {label:'Ring', kind:'cyclic hand-off around the loop'},
'mesh': {label:'Mesh', kind:'every claw talks to every claw'},
'swarm': {label:'Swarm', kind:'parallel claws, loose coordination'},
'debate': {label:'Debate', kind:'adversarial cross-examination'},
};
const topos = ['hub-spoke','pipeline','ring','mesh','swarm','debate'].map(id => {
const active = id === s.topology;
return {
id, label: topoMeta[id].label, active,
style: active
? 'background:rgba(255,111,97,.14); border:1px solid rgba(255,111,97,.45); color:#ff8a7a;'
: 'background:#101014; border:1px solid rgba(255,255,255,.08); color:#9a9aa2;',
onPick: () => this.setState({ topology: id }),
};
});
const sel = claws.find(c => c.id === s.selected) || null;
const crumb = on => on
? 'color:#fff; background:rgba(255,111,97,.14); border:1px solid rgba(255,111,97,.3); padding:3px 8px; border-radius:6px;'
: 'color:#6a6a72; padding:3px 5px;';
const railC = on => on ? '#ff6f61' : '#5a5a62';
const railB = on => on ? 'rgba(255,111,97,.1)' : 'transparent';
const isClaw = s.tier==='claw';
return {
isOrg:s.tier==='org', isCompany:s.tier==='company', isTeam:s.tier==='team', isClaw,
showComputer: isClaw,
nodes, linkPaths, topos,
topoLabel: topoMeta[s.topology].label, topoKind: topoMeta[s.topology].kind,
selName: sel?sel.name:'', selGrad: sel?sel.grad:'', selInk: sel?sel.ink:'',
selInitial: sel?sel.initial:'', selRole: sel?sel.role:'',
crumbOrg:crumb(s.tier==='org'), crumbCo:crumb(s.tier==='company'), crumbTeam:crumb(s.tier==='team'), crumbClaw:crumb(isClaw),
railOrgColor:railC(s.tier==='org'), railOrgBg:railB(s.tier==='org'), railOrgOn:s.tier==='org',
railCoColor:railC(s.tier==='company'), railCoBg:railB(s.tier==='company'), railCoOn:s.tier==='company',
railTeamColor:railC(s.tier==='team'), railTeamBg:railB(s.tier==='team'), railTeamOn:s.tier==='team',
railClawColor:railC(isClaw), railClawBg:railB(isClaw), railClawOn:isClaw,
goClawView:()=>this.setState({tier:'claw'}),
goOrg:()=>this.setState({tier:'org'}),
goCompany:()=>this.setState({tier:'company'}),
goTeam:()=>this.setState({tier:'team'}),
enterTeam:()=>this.setState({tier:'team'}),
selectClawTeam:(id)=>this.setState({selected:id, tier:'team'}),
};
}
}
</script>
</body>
</html>
@@ -0,0 +1,458 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="./support.js"></script>
</head>
<body>
<x-dc>
<helmet>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; }
@keyframes cm-flow { to { stroke-dashoffset: -24; } }
@keyframes cm-blink { 0%,100% { opacity: 1; } 50% { opacity: .25; } }
@keyframes cm-halo { 0% { transform: scale(.7); opacity: .5; } 100% { transform: scale(1.9); opacity: 0; } }
@keyframes cm-type { 0%,100% { opacity: 1; } 50% { opacity: 0; } }
@keyframes cm-bar { 0%,100% { transform: scaleY(.4); } 50% { transform: scaleY(1); } }
@keyframes cm-sweep { 0% { transform: translateX(-100%); } 100% { transform: translateX(320%); } }
</style>
</helmet>
<div style="min-width:100%; min-height:100vh; box-sizing:border-box; padding:48px; background:#e3e3e6; width:max-content;">
<div style="display:flex; gap:56px; align-items:flex-start;">
<!-- FRAME A -->
<div style="flex:none; width:1520px;">
<div style="display:flex; align-items:baseline; gap:10px; margin-bottom:14px;">
<span style="font-family:'JetBrains Mono',monospace; font-size:12px; font-weight:700; letter-spacing:.14em; color:#1a1a1d;">DIRECTION A</span>
<span style="font-family:'JetBrains Mono',monospace; font-size:12px; letter-spacing:.06em; color:#6a6a70;">Close-up — watch one claw think & act, brain folds away</span>
</div>
<div style="width:1520px; height:960px; background:#08080a; border-radius:12px; overflow:hidden; box-shadow:0 24px 60px rgba(0,0,0,.28); border:1px solid rgba(255,255,255,.06); display:flex; flex-direction:column; position:relative; color:#f3f3f5;">
<div style="height:52px; flex:none; display:flex; align-items:center; gap:14px; padding:0 16px; border-bottom:1px solid rgba(255,255,255,.06); background:linear-gradient(180deg,#0d0d10,#0a0a0c);">
<div style="display:flex; align-items:center; gap:9px;">
<svg width="20" height="20" viewBox="0 0 22 22" fill="none"><path d="M11 3 L18.5 17 L3.5 17 Z" stroke="#ff6f61" stroke-width="1.3" stroke-linejoin="round" opacity="0.55"></path><circle cx="11" cy="3.5" r="2.2" fill="#ff6f61"></circle><circle cx="18" cy="17" r="2.2" fill="#ff6f61"></circle><circle cx="4" cy="17" r="2.2" fill="#ff6f61"></circle></svg>
<span style="font-size:14px; font-weight:700;">Clawmates</span>
</div>
<span style="font-family:'JetBrains Mono',monospace; font-size:12px; color:#6a6a72;">Large World</span>
<span style="color:#3a3a40;">/</span>
<span style="font-family:'JetBrains Mono',monospace; font-size:12px; color:#9a9aa2;">Agents</span>
<!-- linked mode toggle -->
<div style="margin-left:8px; display:flex; padding:3px; border-radius:9px; background:#141417; border:1px solid rgba(255,255,255,.08);">
<span style="font-family:'JetBrains Mono',monospace; font-size:11px; font-weight:600; padding:5px 12px; border-radius:6px; color:#8a8a92; cursor:pointer;">System</span>
<span style="font-family:'JetBrains Mono',monospace; font-size:11px; font-weight:600; padding:5px 12px; border-radius:6px; background:rgba(255,111,97,.16); color:#ff8a7a; cursor:pointer;">Agent</span>
</div>
<div style="flex:1;"></div>
<div style="display:flex; align-items:center; gap:7px; font-family:'JetBrains Mono',monospace; font-size:11px; color:#5ec8d8; padding:5px 10px; border:1px solid rgba(94,200,216,.25); border-radius:7px; background:rgba(94,200,216,.06);">
<span style="width:6px; height:6px; border-radius:50%; background:#5ec8d8; animation:cm-blink 1.2s infinite;"></span>working
</div>
<div style="display:flex; align-items:center; gap:5px; font-family:'JetBrains Mono',monospace; font-size:11px; color:#8a8a92;">
<span style="color:#e8b465;">▮</span> 38.4k tok/min
</div>
<div style="width:30px; height:30px; border-radius:8px; background:linear-gradient(135deg,#ff8a7a,#ff5f57); display:flex; align-items:center; justify-content:center; font-size:12px; font-weight:700; color:#2a0d0a;">O</div>
</div>
<div style="flex:1; display:flex; min-height:0;">
<!-- icon nav -->
<div style="width:54px; flex:none; border-right:1px solid rgba(255,255,255,.06); background:#0a0a0c; display:flex; flex-direction:column; align-items:center; padding:12px 0; gap:6px;">
<div style="width:40px; height:44px; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:4px; border-radius:9px; color:#5a5a62;">
<svg width="18" height="18" viewBox="0 0 20 20"><circle cx="10" cy="10" r="7.5" stroke="currentColor" stroke-width="1.4" fill="none"></circle><circle cx="10" cy="10" r="2.4" fill="currentColor"></circle></svg>
<span style="font-family:'JetBrains Mono',monospace; font-size:7px; letter-spacing:.05em;">WORLD</span>
</div>
<div style="position:relative; width:40px; height:44px; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:4px; border-radius:9px; color:#ff6f61; background:rgba(255,111,97,.1);">
<span style="position:absolute; left:0; top:7px; bottom:7px; width:3px; border-radius:0 3px 3px 0; background:#ff6f61;"></span>
<svg width="18" height="18" viewBox="0 0 20 20"><rect x="4" y="4" width="12" height="12" rx="3.5" stroke="currentColor" stroke-width="1.4" fill="none"></rect><circle cx="10" cy="10" r="2.2" fill="currentColor"></circle></svg>
<span style="font-family:'JetBrains Mono',monospace; font-size:7px; letter-spacing:.05em;">AGENT</span>
</div>
<div style="flex:1;"></div>
<div style="width:28px; height:28px; border-radius:8px; border:1px dashed rgba(255,255,255,.16); display:flex; align-items:center; justify-content:center; color:#ff6f61; font-size:16px; font-weight:300;">+</div>
</div>
<!-- agent list -->
<div style="width:218px; flex:none; border-right:1px solid rgba(255,255,255,.06); background:#0b0b0e; display:flex; flex-direction:column; min-height:0;">
<div style="padding:14px 14px 10px;">
<div style="font-family:'JetBrains Mono',monospace; font-size:10px; letter-spacing:.12em; color:#5a5a62;">2 AGENTS · LIVE</div>
<div style="font-size:17px; font-weight:700; margin-top:3px;">Agents</div>
</div>
<div style="flex:1; overflow:hidden; padding:6px;">
<div style="display:flex; align-items:flex-start; gap:10px; padding:9px 10px; border-radius:10px; background:rgba(255,111,97,.1); border:1px solid rgba(255,111,97,.25); position:relative; margin-bottom:4px;">
<span style="position:absolute; left:0; top:9px; bottom:9px; width:3px; border-radius:0 3px 3px 0; background:#ff6f61;"></span>
<div style="position:relative; width:32px; height:32px; flex:none;">
<div style="width:32px; height:32px; border-radius:9px; background:linear-gradient(135deg,#ff8a7a,#ff5f57); display:flex; align-items:center; justify-content:center; font-size:13px; font-weight:700; color:#2a0d0a;">M</div>
<span style="position:absolute; right:-2px; bottom:-2px; width:10px; height:10px; border-radius:50%; background:#5ec8d8; border:2px solid #0b0b0e;"></span>
</div>
<div style="flex:1; min-width:0;">
<div style="font-size:13px; font-weight:600; color:#fff;">Morpheus</div>
<div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#ff8a7a;">Project Manager</div>
<div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#5ec8d8; margin-top:4px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">▸ reviewing PR #214</div>
</div>
</div>
<div style="display:flex; align-items:flex-start; gap:10px; padding:9px 10px; border-radius:10px;">
<div style="position:relative; width:32px; height:32px; flex:none;">
<div style="width:32px; height:32px; border-radius:9px; background:linear-gradient(135deg,#6fd0c0,#4aa3b8); display:flex; align-items:center; justify-content:center; font-size:13px; font-weight:700; color:#06201f;">S</div>
<span style="position:absolute; right:-2px; bottom:-2px; width:10px; height:10px; border-radius:50%; background:#5fd08a; border:2px solid #0b0b0e;"></span>
</div>
<div style="flex:1; min-width:0;">
<div style="font-size:13px; font-weight:600; color:#eaeaee;">Smith</div>
<div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72;">Research Specialist</div>
<div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#5fd08a; margin-top:4px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">▸ crawling 12 sources</div>
</div>
</div>
</div>
<div style="flex:none; padding:10px 12px; border-top:1px solid rgba(255,255,255,.06);">
<div style="display:flex; align-items:center; justify-content:center; gap:7px; height:32px; border-radius:8px; border:1px solid rgba(94,200,216,.3); color:#5ec8d8; font-size:12px; font-weight:600;">⊕ Add to teams</div>
</div>
</div>
<div style="width:560px; flex:none; border-right:1px solid rgba(255,255,255,.06); background:#0a0a0c; display:flex; flex-direction:column; min-height:0;">
<!-- agent header -->
<div style="flex:none; display:flex; align-items:center; gap:14px; padding:16px 18px 12px;">
<div style="position:relative; width:48px; height:48px;">
<div style="position:absolute; inset:-4px; border-radius:50%; border:1.5px solid rgba(255,111,97,.4);"></div>
<div style="width:48px; height:48px; border-radius:50%; background:linear-gradient(135deg,#ff8a7a,#ff5f57); display:flex; align-items:center; justify-content:center; font-size:18px; font-weight:700; color:#2a0d0a; box-shadow:0 0 26px rgba(255,111,97,.4);">M</div>
</div>
<div style="flex:1;">
<div style="font-size:22px; font-weight:700; letter-spacing:-.01em;">Morpheus</div>
<div style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#6a6a72;">Project Manager · Rust 2024 Specialist</div>
</div>
<div style="display:flex; gap:6px;">
<div style="width:30px; height:30px; border-radius:8px; border:1px solid rgba(255,255,255,.1); display:flex; align-items:center; justify-content:center; color:#9a9aa2;"><svg width="14" height="14" viewBox="0 0 16 16"><path d="M2 8h12M8 2v12" stroke="currentColor" stroke-width="1.4"></path></svg></div>
</div>
</div>
<div style="flex:1; overflow:hidden; padding:0 16px 16px; display:flex; flex-direction:column; gap:12px;">
<!-- NOW: current task + live reasoning -->
<div style="flex:none; border-radius:13px; border:1px solid rgba(94,200,216,.22); background:linear-gradient(180deg,#0c1416,#0b0e0f); overflow:hidden;">
<div style="display:flex; align-items:center; gap:8px; padding:11px 14px; border-bottom:1px solid rgba(255,255,255,.05);">
<span style="width:7px; height:7px; border-radius:50%; background:#5ec8d8; animation:cm-blink 1.2s infinite;"></span>
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; letter-spacing:.12em; color:#5ec8d8;">WORKING ON NOW</span>
<span style="flex:1;"></span>
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#6a6a72;">task · 02:14 elapsed</span>
</div>
<div style="padding:13px 14px;">
<div style="font-size:15px; font-weight:600; margin-bottom:4px;">Review PR #214 — borrow-checker fix in <span style="font-family:'JetBrains Mono',monospace; color:#ff8a7a;">cm-runtime</span></div>
<div style="font-family:'JetBrains Mono',monospace; font-size:11px; color:#7a7a82; line-height:1.7;">
<div><span style="color:#5fd08a;">✓</span> read cm-runtime/src/sandbox.rs <span style="color:#5a5a62;">· 3 hunks</span></div>
<div><span style="color:#5fd08a;">✓</span> ran cargo clippy --workspace <span style="color:#5a5a62;">· clean</span></div>
<div><span style="color:#5ec8d8;">▸</span> checking lifetime on <span style="color:#cfcfd5;">&'a mut Guard</span> across await<span style="display:inline-block; width:6px; height:12px; background:#5ec8d8; margin-left:2px; vertical-align:-2px; animation:cm-type 1s steps(1) infinite;"></span></div>
</div>
</div>
</div>
<!-- brain summary (folded) -->
<div style="flex:none; border-radius:13px; border:1px solid rgba(255,255,255,.07); background:#0d0d10; padding:13px 14px;">
<div style="display:flex; align-items:center; gap:8px; margin-bottom:11px;">
<span style="width:18px; height:18px; border-radius:5px; background:rgba(255,111,97,.16); display:flex; align-items:center; justify-content:center;"><svg width="11" height="11" viewBox="0 0 16 16"><rect x="3" y="2" width="10" height="12" rx="2" fill="none" stroke="#ff6f61" stroke-width="1.3"></rect><path d="M6 6h4M6 9h4" stroke="#ff6f61" stroke-width="1.2"></path></svg></span>
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; letter-spacing:.12em; color:#ff8a7a;">BRAIN</span>
<span style="flex:1;"></span>
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#5ec8d8; cursor:pointer;">Edit brain →</span>
</div>
<div style="display:flex; flex-wrap:wrap; gap:6px; margin-bottom:11px;">
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#cfcfd5; padding:3px 8px; border-radius:6px; background:rgba(255,255,255,.05);">system prompt · 3,318 ch</span>
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#cfcfd5; padding:3px 8px; border-radius:6px; background:rgba(255,255,255,.05);">personality · senior reviewer</span>
</div>
<div style="display:flex; gap:8px;">
<div style="flex:1; text-align:center; padding:8px 0; border-radius:8px; background:rgba(255,255,255,.03);"><div style="font-size:16px; font-weight:700; color:#ff8a7a;">5</div><div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72;">skills</div></div>
<div style="flex:1; text-align:center; padding:8px 0; border-radius:8px; background:rgba(255,255,255,.03);"><div style="font-size:16px; font-weight:700; color:#5ec8d8;">4</div><div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72;">tools · doors</div></div>
<div style="flex:1; text-align:center; padding:8px 0; border-radius:8px; background:rgba(255,255,255,.03);"><div style="font-size:16px; font-weight:700; color:#5fd08a;">10</div><div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72;">memories</div></div>
</div>
</div>
<!-- activity -->
<div style="flex:1; min-height:0; border-radius:13px; border:1px solid rgba(255,255,255,.07); background:#0d0d10; padding:13px 14px; display:flex; flex-direction:column;">
<div style="display:flex; align-items:center; gap:8px; margin-bottom:12px;">
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; letter-spacing:.12em; color:#5fd08a;">● ACTIVITY · LIVE</span>
<span style="flex:1;"></span>
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#6a6a72;">9,105 commits · 11 hot</span>
</div>
<div style="flex:1; display:flex; align-items:flex-end; gap:3px;">
<span style="flex:1; background:#1e3a2a; border-radius:2px; height:30%;"></span>
<span style="flex:1; background:#2e6e44; border-radius:2px; height:55%;"></span>
<span style="flex:1; background:#3a9457; border-radius:2px; height:42%;"></span>
<span style="flex:1; background:#46c46a; border-radius:2px; height:78%;"></span>
<span style="flex:1; background:#e8b465; border-radius:2px; height:62%; transform-origin:bottom; animation:cm-bar 1.6s ease-in-out infinite;"></span>
<span style="flex:1; background:#2e6e44; border-radius:2px; height:48%;"></span>
<span style="flex:1; background:#3a9457; border-radius:2px; height:70%;"></span>
<span style="flex:1; background:#46c46a; border-radius:2px; height:90%; transform-origin:bottom; animation:cm-bar 1.9s ease-in-out infinite;"></span>
<span style="flex:1; background:#2e6e44; border-radius:2px; height:36%;"></span>
<span style="flex:1; background:#3a9457; border-radius:2px; height:58%;"></span>
<span style="flex:1; background:#1e3a2a; border-radius:2px; height:44%;"></span>
<span style="flex:1; background:#46c46a; border-radius:2px; height:66%;"></span>
</div>
</div>
</div>
</div>
<div style="flex:1; min-width:0; background:#0c0c0f; display:flex; flex-direction:column; min-height:0;">
<!-- pane header / app tabs -->
<div style="flex:none; display:flex; align-items:center; gap:10px; padding:11px 16px; border-bottom:1px solid rgba(255,255,255,.06);">
<span style="width:7px; height:7px; border-radius:50%; background:#ff6f61;"></span>
<span style="font-size:13px; font-weight:700;">Morpheus's Computer</span>
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#5ec8d8;">● sandbox live · no network</span>
<span style="flex:1;"></span>
<div style="display:flex; gap:4px; padding:3px; border-radius:8px; background:#141417; border:1px solid rgba(255,255,255,.08);">
<span style="display:flex; align-items:center; gap:5px; font-family:'JetBrains Mono',monospace; font-size:11px; font-weight:600; padding:4px 9px; border-radius:5px; background:rgba(94,200,216,.16); color:#5ec8d8;">◐ Browser</span>
<span style="display:flex; align-items:center; gap:5px; font-family:'JetBrains Mono',monospace; font-size:11px; font-weight:600; padding:4px 9px; border-radius:5px; color:#8a8a92;">▸ Terminal</span>
<span style="display:flex; align-items:center; gap:5px; font-family:'JetBrains Mono',monospace; font-size:11px; font-weight:600; padding:4px 9px; border-radius:5px; color:#8a8a92;">◇ Slack</span>
</div>
</div>
<!-- LIVE SCREEN (browser the agent is operating) -->
<div style="flex:1.3; min-height:0; margin:14px 16px 0; border-radius:11px; border:1px solid rgba(255,255,255,.09); background:#fff; overflow:hidden; position:relative; display:flex; flex-direction:column; box-shadow:0 14px 40px rgba(0,0,0,.5);">
<!-- live cursor sweep -->
<div style="position:absolute; top:0; left:0; right:0; height:2px; background:linear-gradient(90deg,transparent,#5ec8d8,transparent); width:40%; animation:cm-sweep 3s ease-in-out infinite; z-index:5;"></div>
<div style="flex:none; display:flex; align-items:center; gap:8px; padding:8px 12px; background:#f1f1f3; border-bottom:1px solid #e2e2e6;">
<span style="display:flex; gap:5px;"><span style="width:9px; height:9px; border-radius:50%; background:#ff5f57;"></span><span style="width:9px; height:9px; border-radius:50%; background:#febc2e;"></span><span style="width:9px; height:9px; border-radius:50%; background:#28c840;"></span></span>
<div style="flex:1; height:22px; border-radius:6px; background:#fff; border:1px solid #dcdce0; display:flex; align-items:center; padding:0 10px; font-family:'JetBrains Mono',monospace; font-size:11px; color:#6a6a72;">github.com/clawarmada/cm-runtime/pull/214/files</div>
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#9a9aa2;">agent-controlled</span>
</div>
<div style="flex:1; min-height:0; overflow:hidden; padding:14px 16px; background:#fff; color:#1a1a1d;">
<div style="font-size:13px; font-weight:700; color:#0a0a0c; margin-bottom:8px;">Files changed · sandbox.rs</div>
<div style="font-family:'JetBrains Mono',monospace; font-size:11px; line-height:1.75;">
<div style="background:#ffebe9; color:#82071e; padding:1px 6px; border-radius:3px;">- let guard = self.lock.lock().unwrap();</div>
<div style="background:#ffebe9; color:#82071e; padding:1px 6px; border-radius:3px;">- do_async(&guard).await;</div>
<div style="background:#dafbe1; color:#0a6b2c; padding:1px 6px; border-radius:3px; margin-top:2px;">+ let data = { self.lock.lock().unwrap().clone() };</div>
<div style="background:#dafbe1; color:#0a6b2c; padding:1px 6px; border-radius:3px;">+ do_async(&data).await;</div>
</div>
<div style="margin-top:14px; display:inline-flex; align-items:center; gap:7px; padding:6px 11px; border-radius:7px; background:#5ec8d822; border:1px solid #5ec8d855; font-family:'JetBrains Mono',monospace; font-size:11px; color:#1f7a8c;">
<span style="width:6px; height:6px; border-radius:50%; background:#1f7a8c; animation:cm-blink 1s infinite;"></span> agent is reading line 142…
</div>
</div>
</div>
<!-- STREAMING OUTPUT console -->
<div style="flex:1; min-height:0; margin:12px 16px 16px; border-radius:11px; border:1px solid rgba(255,255,255,.08); background:#070708; overflow:hidden; display:flex; flex-direction:column;">
<div style="flex:none; display:flex; align-items:center; gap:8px; padding:9px 13px; border-bottom:1px solid rgba(255,255,255,.06);">
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; letter-spacing:.12em; color:#5fd08a;">▌ REASONING STREAM</span>
<span style="flex:1;"></span>
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#5a5a62;">claude-sonnet · 18:16:09</span>
</div>
<div style="flex:1; min-height:0; overflow:hidden; padding:12px 14px; font-family:'JetBrains Mono',monospace; font-size:11px; line-height:1.7;">
<div style="color:#6a6a72;"><span style="color:#c98af0;">think</span> The original holds the MutexGuard across .await — that's the std::sync::Mutex-across-await footgun my rules forbid.</div>
<div style="color:#9a9aa2; margin-top:6px;">The fix clones under a scoped lock, dropping the guard before the await point. Sound.</div>
<div style="color:#6a6a72; margin-top:6px;"><span style="color:#5ec8d8;">tool</span> github.review.comment <span style="color:#5a5a62;">→ door check…</span></div>
<div style="color:#e8b465; margin-top:6px;">⏸ door <span style="color:#cfcfd5;">post review on PR #214</span> needs approval<span style="display:inline-block; width:6px; height:12px; background:#5fd08a; margin-left:3px; vertical-align:-2px; animation:cm-type 1s steps(1) infinite;"></span></div>
</div>
</div>
</div>
</div>
<!-- door approval toast -->
<div style="position:absolute; right:18px; bottom:42px; width:332px; z-index:20; border-radius:13px; border:1px solid rgba(232,196,106,.4); background:linear-gradient(180deg,#1a1408,#12100a); box-shadow:0 18px 50px rgba(0,0,0,.6); overflow:hidden;">
<div style="height:3px; background:linear-gradient(90deg,#e8b465,#ff6f61);"></div>
<div style="padding:13px 15px;">
<div style="display:flex; align-items:center; gap:8px; margin-bottom:9px;">
<span style="width:24px; height:24px; border-radius:7px; background:rgba(232,196,106,.16); display:flex; align-items:center; justify-content:center;"><svg width="13" height="13" viewBox="0 0 16 16"><path d="M8 1l5 2v4c0 4-2.5 6-5 7-2.5-1-5-3-5-7V3z" fill="none" stroke="#e8b465" stroke-width="1.4" stroke-linejoin="round"></path></svg></span>
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; letter-spacing:.1em; color:#e8b465;">§15 DOOR · APPROVAL</span>
<span style="flex:1;"></span>
<span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72;">expand ▾</span>
</div>
<div style="font-size:13px; font-weight:600; margin-bottom:3px;">Post code review on PR #214</div>
<div style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#8a8a92; line-height:1.6; margin-bottom:12px;">Morpheus · github.review.comment → external egress · drafts 1 comment</div>
<div style="display:flex; gap:8px;">
<div style="flex:1; display:flex; align-items:center; justify-content:center; height:32px; border-radius:8px; background:linear-gradient(135deg,#ffb27a,#e8b465); color:#2a1d05; font-size:12px; font-weight:700;">Approve</div>
<div style="flex:1; display:flex; align-items:center; justify-content:center; height:32px; border-radius:8px; border:1px solid rgba(255,255,255,.14); color:#cfcfd5; font-size:12px; font-weight:600;">Review</div>
<div style="width:34px; display:flex; align-items:center; justify-content:center; height:32px; border-radius:8px; border:1px solid rgba(255,255,255,.1); color:#8a8a92;">✕</div>
</div>
</div>
</div>
<div style="height:26px; flex:none; display:flex; align-items:center; gap:16px; padding:0 16px; border-top:1px solid rgba(255,255,255,.06); background:#0a0a0c; font-family:'JetBrains Mono',monospace; font-size:10px; color:#5a5a62;">
<span style="color:#5fd08a;">● durable runner ok</span>
<span>checkpoint 3s ago</span>
<span style="flex:1;"></span>
<span>§15 sandbox: isolated</span>
<span style="color:#e8b465;">1 door awaiting approval</span>
</div>
</div>
</div>
<!-- FRAME B -->
<div style="flex:none; width:1520px;">
<div style="display:flex; align-items:baseline; gap:10px; margin-bottom:14px;">
<span style="font-family:'JetBrains Mono',monospace; font-size:12px; font-weight:700; letter-spacing:.14em; color:#1a1a1d;">DIRECTION B</span>
<span style="font-family:'JetBrains Mono',monospace; font-size:12px; letter-spacing:.06em; color:#6a6a70;">System mode — mission control, both claws working together</span>
</div>
<div style="width:1520px; height:960px; background:#08080a; border-radius:12px; overflow:hidden; box-shadow:0 24px 60px rgba(0,0,0,.28); border:1px solid rgba(255,255,255,.06); display:flex; flex-direction:column; position:relative; color:#f3f3f5;">
<div style="height:52px; flex:none; display:flex; align-items:center; gap:14px; padding:0 16px; border-bottom:1px solid rgba(255,255,255,.06); background:linear-gradient(180deg,#0d0d10,#0a0a0c);">
<div style="display:flex; align-items:center; gap:9px;">
<svg width="20" height="20" viewBox="0 0 22 22" fill="none"><path d="M11 3 L18.5 17 L3.5 17 Z" stroke="#ff6f61" stroke-width="1.3" stroke-linejoin="round" opacity="0.55"></path><circle cx="11" cy="3.5" r="2.2" fill="#ff6f61"></circle><circle cx="18" cy="17" r="2.2" fill="#ff6f61"></circle><circle cx="4" cy="17" r="2.2" fill="#ff6f61"></circle></svg>
<span style="font-size:14px; font-weight:700;">Clawmates</span>
</div>
<span style="font-family:'JetBrains Mono',monospace; font-size:12px; color:#9a9aa2;">Large World</span>
<div style="margin-left:8px; display:flex; padding:3px; border-radius:9px; background:#141417; border:1px solid rgba(255,255,255,.08);">
<span style="font-family:'JetBrains Mono',monospace; font-size:11px; font-weight:600; padding:5px 12px; border-radius:6px; background:rgba(94,200,216,.16); color:#5ec8d8; cursor:pointer;">System</span>
<span style="font-family:'JetBrains Mono',monospace; font-size:11px; font-weight:600; padding:5px 12px; border-radius:6px; color:#8a8a92; cursor:pointer;">Agent</span>
</div>
<div style="flex:1;"></div>
<div style="display:flex; align-items:center; gap:7px; font-family:'JetBrains Mono',monospace; font-size:11px; color:#5fd08a; padding:5px 10px; border:1px solid rgba(95,208,138,.25); border-radius:7px; background:rgba(95,208,138,.06);">
<span style="width:6px; height:6px; border-radius:50%; background:#5fd08a; animation:cm-blink 1.6s infinite;"></span>2 claws · autonomous
</div>
<div style="display:flex; align-items:center; gap:5px; font-family:'JetBrains Mono',monospace; font-size:11px; color:#8a8a92;"><span style="color:#e8b465;">▮</span> $0.42 / hr</div>
<div style="width:30px; height:30px; border-radius:8px; background:linear-gradient(135deg,#ff8a7a,#ff5f57); display:flex; align-items:center; justify-content:center; font-size:12px; font-weight:700; color:#2a0d0a;">O</div>
</div>
<div style="flex:1; display:flex; min-height:0;">
<div style="width:54px; flex:none; border-right:1px solid rgba(255,255,255,.06); background:#0a0a0c; display:flex; flex-direction:column; align-items:center; padding:12px 0; gap:6px;">
<div style="position:relative; width:40px; height:44px; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:4px; border-radius:9px; color:#5ec8d8; background:rgba(94,200,216,.1);">
<span style="position:absolute; left:0; top:7px; bottom:7px; width:3px; border-radius:0 3px 3px 0; background:#5ec8d8;"></span>
<svg width="18" height="18" viewBox="0 0 20 20"><circle cx="10" cy="10" r="7.5" stroke="currentColor" stroke-width="1.4" fill="none"></circle><circle cx="10" cy="10" r="2.4" fill="currentColor"></circle></svg>
<span style="font-family:'JetBrains Mono',monospace; font-size:7px; letter-spacing:.05em;">WORLD</span>
</div>
<div style="width:40px; height:44px; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:4px; border-radius:9px; color:#5a5a62;">
<svg width="18" height="18" viewBox="0 0 20 20"><rect x="4" y="4" width="12" height="12" rx="3.5" stroke="currentColor" stroke-width="1.4" fill="none"></rect><circle cx="10" cy="10" r="2.2" fill="currentColor"></circle></svg>
<span style="font-family:'JetBrains Mono',monospace; font-size:7px; letter-spacing:.05em;">AGENT</span>
</div>
<div style="flex:1;"></div>
<div style="width:28px; height:28px; border-radius:8px; border:1px dashed rgba(255,255,255,.16); display:flex; align-items:center; justify-content:center; color:#ff6f61; font-size:16px; font-weight:300;">+</div>
</div>
<div style="flex:1; min-width:0; display:flex; flex-direction:column; min-height:0;">
<div style="flex:none; display:flex; align-items:center; gap:12px; padding:13px 16px 4px;">
<div style="font-family:'JetBrains Mono',monospace; font-size:10px; letter-spacing:.14em; color:#5a5a62;">LARGE WORLD · SYSTEM</div>
<span style="flex:1;"></span>
<div style="display:flex; gap:10px;">
<div style="display:flex; align-items:center; gap:8px; padding:7px 13px; border-radius:9px; border:1px solid rgba(255,255,255,.07); background:#0d0d10;">
<span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72;">THROUGHPUT</span>
<span style="font-family:'JetBrains Mono',monospace; font-size:13px; font-weight:700; color:#5ec8d8;">38.4k</span>
<span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72;">tok/min</span>
<span style="display:flex; align-items:flex-end; gap:1.5px; height:14px; margin-left:2px;">
<span style="width:2px; background:#5ec8d8; height:40%;"></span><span style="width:2px; background:#5ec8d8; height:70%;"></span><span style="width:2px; background:#5ec8d8; height:55%; animation:cm-bar 1.4s ease-in-out infinite; transform-origin:bottom;"></span><span style="width:2px; background:#5ec8d8; height:90%; animation:cm-bar 1.7s ease-in-out infinite; transform-origin:bottom;"></span><span style="width:2px; background:#5ec8d8; height:65%;"></span>
</span>
</div>
<div style="display:flex; align-items:center; gap:8px; padding:7px 13px; border-radius:9px; border:1px solid rgba(255,255,255,.07); background:#0d0d10;">
<span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72;">SPEND</span>
<span style="font-family:'JetBrains Mono',monospace; font-size:13px; font-weight:700; color:#e8b465;">$0.42</span>
<span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72;">/hr</span>
</div>
<div style="display:flex; align-items:center; gap:8px; padding:7px 13px; border-radius:9px; border:1px solid rgba(255,255,255,.07); background:#0d0d10;">
<span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72;">LOOPS</span>
<span style="font-family:'JetBrains Mono',monospace; font-size:13px; font-weight:700; color:#5fd08a;">3</span>
<span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72;">running</span>
</div>
<div style="display:flex; align-items:center; gap:8px; padding:7px 13px; border-radius:9px; border:1px solid rgba(232,196,106,.3); background:rgba(232,196,106,.06);">
<span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#e8b465;">DOORS</span>
<span style="font-family:'JetBrains Mono',monospace; font-size:13px; font-weight:700; color:#e8b465;">1</span>
<span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72;">pending</span>
</div>
</div>
</div>
<div style="flex:1; min-height:0; display:flex; gap:14px; padding:14px 16px;">
<div style="flex:1.5; min-width:0; display:flex; flex-direction:column; gap:14px;">
<!-- Morpheus tile -->
<div style="flex:1; min-height:0; border-radius:13px; border:1px solid rgba(255,111,97,.22); background:#0d0d10; overflow:hidden; display:flex; flex-direction:column;">
<div style="flex:none; display:flex; align-items:center; gap:9px; padding:10px 13px; border-bottom:1px solid rgba(255,255,255,.05);">
<div style="position:relative; width:26px; height:26px;"><div style="width:26px; height:26px; border-radius:7px; background:linear-gradient(135deg,#ff8a7a,#ff5f57); display:flex; align-items:center; justify-content:center; font-size:11px; font-weight:700; color:#2a0d0a;">M</div><span style="position:absolute; right:-2px; bottom:-2px; width:9px; height:9px; border-radius:50%; background:#5ec8d8; border:2px solid #0d0d10;"></span></div>
<span style="font-size:13px; font-weight:700;">Morpheus</span>
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#5ec8d8;">▸ reviewing PR #214 · Browser</span>
<span style="flex:1;"></span>
<span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72; cursor:pointer;">open ⤢</span>
</div>
<div style="flex:1; min-height:0; margin:10px 12px; border-radius:9px; background:#fff; overflow:hidden; position:relative; box-shadow:0 8px 24px rgba(0,0,0,.4);">
<div style="position:absolute; top:0; left:0; right:0; height:2px; background:linear-gradient(90deg,transparent,#5ec8d8,transparent); width:35%; animation:cm-sweep 3.2s ease-in-out infinite; z-index:4;"></div>
<div style="display:flex; align-items:center; gap:6px; padding:6px 10px; background:#f1f1f3; border-bottom:1px solid #e2e2e6;">
<span style="display:flex; gap:4px;"><span style="width:7px; height:7px; border-radius:50%; background:#ff5f57;"></span><span style="width:7px; height:7px; border-radius:50%; background:#febc2e;"></span><span style="width:7px; height:7px; border-radius:50%; background:#28c840;"></span></span>
<div style="flex:1; height:18px; border-radius:5px; background:#fff; border:1px solid #dcdce0; display:flex; align-items:center; padding:0 8px; font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72;">github.com · pull/214/files</div>
</div>
<div style="padding:10px 12px; font-family:'JetBrains Mono',monospace; font-size:10px; line-height:1.7; color:#1a1a1d;">
<div style="background:#ffebe9; color:#82071e; padding:0 5px; border-radius:3px;">- do_async(&guard).await;</div>
<div style="background:#dafbe1; color:#0a6b2c; padding:0 5px; border-radius:3px; margin-top:2px;">+ do_async(&data).await;</div>
<div style="margin-top:8px; display:inline-flex; align-items:center; gap:5px; padding:4px 8px; border-radius:6px; background:#5ec8d822; color:#1f7a8c; font-size:9px;"><span style="width:5px; height:5px; border-radius:50%; background:#1f7a8c; animation:cm-blink 1s infinite;"></span> reading line 142…</div>
</div>
</div>
</div>
<!-- collaboration connector -->
<div style="flex:none; display:flex; align-items:center; gap:10px; padding:0 14px;">
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#6a6a72;">M</span>
<div style="flex:1; height:1px; position:relative; background:rgba(255,255,255,.08);">
<span style="position:absolute; top:-3px; left:0; width:7px; height:7px; border-radius:50%; background:#5ec8d8; animation:cm-sweep 2.4s linear infinite;"></span>
</div>
<span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#5ec8d8; padding:3px 8px; border-radius:6px; background:rgba(94,200,216,.1); border:1px solid rgba(94,200,216,.22);">Morpheus → Smith · "need crate audit"</span>
<div style="flex:1; height:1px; background:rgba(255,255,255,.08);"></div>
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#6a6a72;">S</span>
</div>
<!-- Smith tile -->
<div style="flex:1; min-height:0; border-radius:13px; border:1px solid rgba(111,208,192,.22); background:#0d0d10; overflow:hidden; display:flex; flex-direction:column;">
<div style="flex:none; display:flex; align-items:center; gap:9px; padding:10px 13px; border-bottom:1px solid rgba(255,255,255,.05);">
<div style="position:relative; width:26px; height:26px;"><div style="width:26px; height:26px; border-radius:7px; background:linear-gradient(135deg,#6fd0c0,#4aa3b8); display:flex; align-items:center; justify-content:center; font-size:11px; font-weight:700; color:#06201f;">S</div><span style="position:absolute; right:-2px; bottom:-2px; width:9px; height:9px; border-radius:50%; background:#5fd08a; border:2px solid #0d0d10;"></span></div>
<span style="font-size:13px; font-weight:700;">Smith</span>
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#5fd08a;">▸ crawling 12 sources · Terminal</span>
<span style="flex:1;"></span>
<span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72; cursor:pointer;">open ⤢</span>
</div>
<div style="flex:1; min-height:0; margin:10px 12px; border-radius:9px; background:#070708; overflow:hidden; position:relative; border:1px solid rgba(255,255,255,.06);">
<div style="padding:10px 12px; font-family:'JetBrains Mono',monospace; font-size:10px; line-height:1.7;">
<div style="color:#5fd08a;">$ cargo audit --json | jq '.vulnerabilities'</div>
<div style="color:#6a6a72;"> fetching advisory-db… <span style="color:#5fd08a;">done</span></div>
<div style="color:#9a9aa2;"> scanned 184 crates · <span style="color:#e8b465;">2 advisories</span></div>
<div style="color:#5ec8d8;"> ▸ tokio 1.x · RUSTSEC-2025-00xx<span style="display:inline-block; width:5px; height:11px; background:#5fd08a; margin-left:2px; vertical-align:-1px; animation:cm-type 1s steps(1) infinite;"></span></div>
</div>
</div>
</div>
</div>
<div style="flex:1; min-width:0; display:flex; flex-direction:column; gap:14px;">
<!-- inter-agent comms -->
<div style="flex:1.2; min-height:0; border-radius:13px; border:1px solid rgba(255,255,255,.07); background:#0d0d10; overflow:hidden; display:flex; flex-direction:column;">
<div style="flex:none; display:flex; align-items:center; gap:8px; padding:11px 14px; border-bottom:1px solid rgba(255,255,255,.05);">
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; letter-spacing:.12em; color:#c98af0;">⇄ INTER-AGENT COMMS</span>
<span style="flex:1;"></span>
<span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#5fd08a;">● live</span>
</div>
<div style="flex:1; min-height:0; overflow:hidden; padding:12px 14px; display:flex; flex-direction:column; gap:11px;">
<div style="display:flex; gap:9px;">
<div style="width:22px; height:22px; flex:none; border-radius:6px; background:linear-gradient(135deg,#ff8a7a,#ff5f57); display:flex; align-items:center; justify-content:center; font-size:10px; font-weight:700; color:#2a0d0a;">M</div>
<div style="flex:1;"><div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#ff8a7a; margin-bottom:2px;">Morpheus · 18:15:52</div><div style="font-size:12px; color:#cfcfd5; line-height:1.5;">PR #214 touches tokio — can you audit the crate tree before I approve?</div></div>
</div>
<div style="display:flex; gap:9px;">
<div style="width:22px; height:22px; flex:none; border-radius:6px; background:linear-gradient(135deg,#6fd0c0,#4aa3b8); display:flex; align-items:center; justify-content:center; font-size:10px; font-weight:700; color:#06201f;">S</div>
<div style="flex:1;"><div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#5fd08a; margin-bottom:2px;">Smith · 18:16:03</div><div style="font-size:12px; color:#cfcfd5; line-height:1.5;">On it. Running cargo audit across 184 crates now.</div></div>
</div>
<div style="display:flex; gap:9px; opacity:.85;">
<div style="width:22px; height:22px; flex:none; border-radius:6px; background:linear-gradient(135deg,#6fd0c0,#4aa3b8); display:flex; align-items:center; justify-content:center; font-size:10px; font-weight:700; color:#06201f;">S</div>
<div style="flex:1;"><div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#5fd08a; margin-bottom:2px;">Smith · typing…</div><div style="font-size:12px; color:#7a7a82; line-height:1.5;">2 advisories found, summarizing<span style="display:inline-block; width:5px; height:11px; background:#5fd08a; margin-left:2px; vertical-align:-1px; animation:cm-type 1s steps(1) infinite;"></span></div></div>
</div>
</div>
</div>
<!-- routines & loops -->
<div style="flex:1; min-height:0; border-radius:13px; border:1px solid rgba(255,255,255,.07); background:#0d0d10; overflow:hidden; display:flex; flex-direction:column;">
<div style="flex:none; display:flex; align-items:center; gap:8px; padding:11px 14px; border-bottom:1px solid rgba(255,255,255,.05);">
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; letter-spacing:.12em; color:#5fd08a;">↻ ROUTINES &amp; LOOPS</span>
<span style="flex:1;"></span>
<span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72; cursor:pointer;">+ new</span>
</div>
<div style="flex:1; min-height:0; overflow:hidden; padding:11px 14px; display:flex; flex-direction:column; gap:9px;">
<div style="padding:9px 11px; border-radius:9px; background:#101014; border:1px solid rgba(94,200,216,.18);">
<div style="display:flex; align-items:center; gap:7px; margin-bottom:7px;"><span style="width:6px; height:6px; border-radius:50%; background:#5ec8d8; animation:cm-blink 1.4s infinite;"></span><span style="font-size:12px; font-weight:600;">Dependency watch</span><span style="flex:1;"></span><span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#5ec8d8;">loop · Smith · step 7</span></div>
<div style="height:4px; border-radius:2px; background:rgba(255,255,255,.08); overflow:hidden;"><div style="width:48%; height:100%; background:linear-gradient(90deg,#5ec8d8,#4aa3b8);"></div></div>
</div>
<div style="padding:9px 11px; border-radius:9px; background:#101014; border:1px solid rgba(255,255,255,.06); display:flex; align-items:center; gap:7px;"><span style="width:6px; height:6px; border-radius:50%; background:#e8b465;"></span><span style="font-size:12px; font-weight:600;">PR triage</span><span style="flex:1;"></span><span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#e8b465;">cron · Morpheus · 09:00</span></div>
<div style="padding:9px 11px; border-radius:9px; background:#101014; border:1px solid rgba(255,255,255,.06); display:flex; align-items:center; gap:7px;"><span style="width:6px; height:6px; border-radius:50%; background:#5fd08a;"></span><span style="font-size:12px; font-weight:600;">Nightly digest</span><span style="flex:1;"></span><span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#5fd08a;">done · 02:00 · 1m</span></div>
</div>
</div>
</div>
</div>
</div>
</div>
<div style="height:26px; flex:none; display:flex; align-items:center; gap:16px; padding:0 16px; border-top:1px solid rgba(255,255,255,.06); background:#0a0a0c; font-family:'JetBrains Mono',monospace; font-size:10px; color:#5a5a62;">
<span style="color:#5fd08a;">● durable runner ok</span>
<span>2 agents · 3 loops</span>
<span>checkpoint 3s ago</span>
<span style="flex:1;"></span>
<span>§15 sandbox: isolated</span>
<span style="color:#e8b465;">1 door awaiting approval</span>
</div>
</div>
</div>
</div>
</div>
</x-dc>
</body>
</html>
@@ -0,0 +1,464 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="./support.js"></script>
</head>
<body>
<x-dc>
<helmet>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; background: #08080a; }
body { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; -webkit-font-smoothing: antialiased; }
@keyframes cm-flow { to { stroke-dashoffset: -24; } }
@keyframes cm-blink { 0%,100% { opacity: 1; } 50% { opacity: .25; } }
@keyframes cm-halo { 0% { transform: scale(.7); opacity: .5; } 100% { transform: scale(2); opacity: 0; } }
@keyframes cm-dash { to { stroke-dashoffset: -40; } }
</style>
</helmet>
<div style="width:100%; height:100vh; min-height:640px; background:#08080a; color:#f3f3f5; display:flex; flex-direction:column; overflow:hidden;">
<!-- TOP BAR -->
<div style="height:50px; flex:none; display:flex; align-items:center; gap:13px; padding:0 16px; border-bottom:1px solid rgba(255,255,255,.06); background:linear-gradient(180deg,#0d0d10,#0a0a0c);">
<div style="display:flex; align-items:center; gap:9px;">
<svg width="20" height="20" viewBox="0 0 22 22" fill="none"><path d="M11 3 L18.5 17 L3.5 17 Z" stroke="#ff6f61" stroke-width="1.3" stroke-linejoin="round" opacity="0.55"></path><circle cx="11" cy="3.5" r="2.2" fill="#ff6f61"></circle><circle cx="18" cy="17" r="2.2" fill="#ff6f61"></circle><circle cx="4" cy="17" r="2.2" fill="#ff6f61"></circle></svg>
<span style="font-size:14px; font-weight:700;">Clawmates</span>
</div>
<span style="font-family:'JetBrains Mono',monospace; font-size:12px; color:#f3f3f5; background:rgba(255,111,97,.12); border:1px solid rgba(255,111,97,.28); padding:3px 9px; border-radius:6px;">Large World</span>
<span style="color:#3a3a40;">/</span>
<span style="font-family:'JetBrains Mono',monospace; font-size:12px; color:#6a6a72;">Agents</span>
<div style="flex:1;"></div>
<div style="display:flex; align-items:center; gap:7px; font-family:'JetBrains Mono',monospace; font-size:11px; color:#5fd08a; padding:5px 10px; border:1px solid rgba(95,208,138,.25); border-radius:7px; background:rgba(95,208,138,.06);">
<span style="width:6px; height:6px; border-radius:50%; background:#5fd08a; animation:cm-blink 1.6s infinite;"></span>1 org · 2 agents
</div>
<div style="width:30px; height:30px; border-radius:8px; background:linear-gradient(135deg,#ff8a7a,#ff5f57); display:flex; align-items:center; justify-content:center; font-size:12px; font-weight:700; color:#2a0d0a;">O</div>
</div>
<!-- BODY -->
<div style="flex:1; display:flex; min-height:0;">
<!-- ICON RAIL -->
<div style="width:54px; flex:none; border-right:1px solid rgba(255,255,255,.06); background:#0a0a0c; display:flex; flex-direction:column; align-items:center; padding:12px 0; gap:6px;">
<div style="position:relative; width:40px; height:44px; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:4px; border-radius:9px; color:#ff6f61; background:rgba(255,111,97,.1);">
<span style="position:absolute; left:0; top:7px; bottom:7px; width:3px; border-radius:0 3px 3px 0; background:#ff6f61;"></span>
<svg width="18" height="18" viewBox="0 0 20 20"><circle cx="10" cy="10" r="7.5" stroke="currentColor" stroke-width="1.4" fill="none"></circle><circle cx="10" cy="10" r="2.4" fill="currentColor"></circle></svg>
<span style="font-family:'JetBrains Mono',monospace; font-size:7px; letter-spacing:.05em;">WORLD</span>
</div>
<div style="width:40px; height:44px; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:4px; border-radius:9px; color:#5a5a62;">
<svg width="18" height="18" viewBox="0 0 20 20"><rect x="4" y="4" width="12" height="12" rx="3.5" stroke="currentColor" stroke-width="1.4" fill="none"></rect><circle cx="10" cy="10" r="2.2" fill="currentColor"></circle></svg>
<span style="font-family:'JetBrains Mono',monospace; font-size:7px; letter-spacing:.05em;">AGENT</span>
</div>
<div style="flex:1;"></div>
<div style="width:28px; height:28px; border-radius:8px; border:1px dashed rgba(255,255,255,.16); display:flex; align-items:center; justify-content:center; color:#ff6f61; font-size:16px; font-weight:300;">+</div>
</div>
<!-- LEFT LIST -->
<div style="width:200px; flex:none; border-right:1px solid rgba(255,255,255,.06); background:#0b0b0e; display:flex; flex-direction:column; min-height:0;">
<div style="padding:14px 14px 10px;">
<div style="font-family:'JetBrains Mono',monospace; font-size:10px; letter-spacing:.12em; color:#5a5a62;">1 ORG · 2 AGENTS</div>
<div style="font-size:17px; font-weight:700; margin-top:3px;">Large World</div>
</div>
<div style="flex:1; overflow:hidden; padding:6px;">
<!-- WORLD_TREE_LIST -->
</div>
</div>
<!-- CENTER STAGE -->
<div style="flex:1; position:relative; min-width:0; overflow:hidden; background:radial-gradient(130% 100% at 50% 0%, #0e0e13 0%, #08080a 65%);">
<!-- VIEW SWITCHER -->
<div style="position:absolute; top:14px; left:50%; transform:translateX(-50%); z-index:20; display:flex; padding:4px; border-radius:11px; background:rgba(14,14,18,.85); border:1px solid rgba(255,255,255,.1); backdrop-filter:blur(10px); gap:3px;">
<sc-for list="{{ modes }}" as="m" hint-placeholder-count="3">
<div style="display:flex; align-items:center; gap:7px; font-family:'JetBrains Mono',monospace; font-size:12px; font-weight:600; padding:7px 15px; border-radius:8px; cursor:pointer; transition:all .2s; {{ m.style }}" onClick="{{ m.onPick }}">{{ m.icon }} {{ m.label }}</div>
</sc-for>
</div>
<!-- GRAPH TOOLS -->
<div style="position:absolute; top:14px; left:16px; z-index:15; width:150px; border-radius:11px; border:1px solid rgba(255,255,255,.08); background:rgba(14,14,18,.7); backdrop-filter:blur(8px); padding:10px;">
<div style="font-family:'JetBrains Mono',monospace; font-size:9px; letter-spacing:.12em; color:#5a5a62; margin-bottom:9px;">GRAPH TOOLS</div>
<div style="display:flex; flex-direction:column; gap:6px;">
<div style="display:flex; align-items:center; gap:8px; font-size:11px; color:#ff8a7a; padding:6px 8px; border-radius:7px; background:rgba(255,111,97,.08);">▣ Save layout</div>
<div style="display:flex; align-items:center; gap:8px; font-size:11px; color:#cfcfd5; padding:6px 8px; border-radius:7px; background:rgba(255,255,255,.03);">⤢ Fit to view</div>
<div style="display:flex; align-items:center; gap:8px; font-size:11px; color:#cfcfd5; padding:6px 8px; border-radius:7px; background:rgba(255,255,255,.03);">↺ Reset layout</div>
</div>
</div>
<!-- caption -->
<div style="position:absolute; bottom:16px; left:50%; transform:translateX(-50%); z-index:15; text-align:center;">
<div style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#5a5a62;">{{ caption }}</div>
</div>
<!-- VIEW LAYERS -->
<div style="position:absolute; inset:0; display:{{ hShow }};">
<div style="position:absolute; inset:0;">
<svg viewBox="0 0 100 100" preserveAspectRatio="none" style="position:absolute; inset:0; width:100%; height:100%;">
<path d="M50,15 C50,22 42,24 42,31" fill="none" stroke="rgba(95,208,138,.4)" stroke-width="1.1" stroke-dasharray="2 3" vector-effect="non-scaling-stroke" style="animation:cm-dash 2s linear infinite;"></path>
<path d="M42,33 C42,42 50,44 50,51" fill="none" stroke="rgba(95,208,138,.4)" stroke-width="1.1" stroke-dasharray="2 3" vector-effect="non-scaling-stroke" style="animation:cm-dash 2.4s linear infinite;"></path>
<path d="M50,55 C50,64 38,66 36,74" fill="none" stroke="rgba(95,208,138,.35)" stroke-width="1.1" stroke-dasharray="2 3" vector-effect="non-scaling-stroke" style="animation:cm-dash 2.1s linear infinite;"></path>
<path d="M50,55 C50,64 60,66 62,74" fill="none" stroke="rgba(255,111,97,.5)" stroke-width="1.2" stroke-dasharray="2 3" vector-effect="non-scaling-stroke" style="animation:cm-dash 1.6s linear infinite;"></path>
</svg>
<!-- Zeus (ORG) -->
<div style="position:absolute; left:50%; top:15%; transform:translate(-50%,-50%); display:flex; align-items:center; gap:10px; padding:10px 14px; border-radius:12px; background:#11101a; border:1px solid rgba(201,138,240,.4); box-shadow:0 8px 24px rgba(0,0,0,.4); cursor:pointer;">
<div style="width:32px; height:32px; border-radius:9px; background:linear-gradient(135deg,#c98af0,#9a5ad8); display:flex; align-items:center; justify-content:center; font-weight:700; color:#1a0a2a; font-size:14px;">Z</div>
<div><div style="font-size:14px; font-weight:700;">Zeus</div><div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#c98af0;">ORG · 1 company</div></div>
<span style="color:#5a5a62; font-size:11px; margin-left:6px;">▾</span>
</div>
<!-- thor (COMPANY) -->
<div style="position:absolute; left:42%; top:32%; transform:translate(-50%,-50%); display:flex; align-items:center; gap:10px; padding:10px 14px; border-radius:12px; background:#0e0f1a; border:1px solid rgba(138,154,240,.4); box-shadow:0 8px 24px rgba(0,0,0,.4); cursor:pointer;">
<div style="width:32px; height:32px; border-radius:9px; background:linear-gradient(135deg,#8a9af0,#5a6ad8); display:flex; align-items:center; justify-content:center; font-weight:700; color:#0a0e2a; font-size:14px;">T</div>
<div><div style="font-size:14px; font-weight:700;">thor</div><div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#8a9af0;">COMPANY · 1 team</div></div>
<span style="color:#5a5a62; font-size:11px; margin-left:6px;">▾</span>
</div>
<!-- POD01 (TEAM) -->
<div style="position:absolute; left:50%; top:53%; transform:translate(-50%,-50%); display:flex; align-items:center; gap:10px; padding:10px 14px; border-radius:12px; background:#0a1412; border:1px solid rgba(95,208,138,.4); box-shadow:0 8px 24px rgba(0,0,0,.4); cursor:pointer;">
<div style="width:32px; height:32px; border-radius:9px; background:linear-gradient(135deg,#6fd0c0,#4aa3b8); display:flex; align-items:center; justify-content:center; font-weight:700; color:#06201f; font-size:14px;">P</div>
<div><div style="font-size:14px; font-weight:700;">POD01</div><div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#5fd08a;">TEAM · 2 agents</div></div>
<span style="color:#5a5a62; font-size:11px; margin-left:6px;">▾</span>
</div>
<!-- Morpheus -->
<div style="position:absolute; left:36%; top:76%; transform:translate(-50%,-50%); display:flex; flex-direction:column; align-items:center; gap:7px; cursor:pointer;">
<div style="width:44px; height:44px; border-radius:50%; background:linear-gradient(135deg,#ff8a7a,#ff5f57); display:flex; align-items:center; justify-content:center; font-size:16px; font-weight:700; color:#2a0d0a; box-shadow:0 0 22px rgba(255,111,97,.35); position:relative;">M<span style="position:absolute; right:1px; bottom:1px; width:9px; height:9px; border-radius:50%; background:#5fd08a; border:2px solid #08080a;"></span></div>
<div style="text-align:center;"><div style="font-size:12px; font-weight:600;">Morpheus</div><div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72;">Project Manager</div></div>
</div>
<!-- Smith (selected) -->
<div style="position:absolute; left:62%; top:76%; transform:translate(-50%,-50%); display:flex; flex-direction:column; align-items:center; gap:7px; cursor:pointer;">
<div style="position:relative; width:50px; height:50px;">
<div style="position:absolute; inset:-5px; border-radius:50%; border:2px solid #ff6f61; box-shadow:0 0 0 4px rgba(255,111,97,.12);"></div>
<div style="position:absolute; inset:0; border-radius:50%; background:rgba(255,111,97,.4); animation:cm-halo 1.8s ease-out infinite;"></div>
<div style="position:relative; width:50px; height:50px; border-radius:50%; background:linear-gradient(135deg,#ff8a7a,#ff5f57); display:flex; align-items:center; justify-content:center; font-size:18px; font-weight:700; color:#2a0d0a; box-shadow:0 0 28px rgba(255,111,97,.5);">S<span style="position:absolute; right:2px; bottom:2px; width:10px; height:10px; border-radius:50%; background:#5fd08a; border:2px solid #08080a;"></span></div>
</div>
<div style="text-align:center;"><div style="font-size:12px; font-weight:700; color:#fff;">Smith</div><div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#ff8a7a;">Research Specialist</div></div>
</div>
</div>
</div>
<div style="position:absolute; inset:0; display:{{ fShow }};">
<div style="position:absolute; inset:0;">
<svg viewBox="0 0 100 100" preserveAspectRatio="none" style="position:absolute; inset:0; width:100%; height:100%;">
<!-- flat peer mesh: no hierarchy, every node links to peers -->
<path d="M50,20 L82,44" stroke="rgba(255,255,255,.1)" stroke-width="1" vector-effect="non-scaling-stroke"></path>
<path d="M82,44 L68,78" stroke="rgba(255,255,255,.1)" stroke-width="1" vector-effect="non-scaling-stroke"></path>
<path d="M68,78 L32,78" stroke="rgba(255,111,97,.45)" stroke-width="1.2" stroke-dasharray="2 3" vector-effect="non-scaling-stroke" style="animation:cm-dash 1.8s linear infinite;"></path>
<path d="M32,78 L18,44" stroke="rgba(255,255,255,.1)" stroke-width="1" vector-effect="non-scaling-stroke"></path>
<path d="M18,44 L50,20" stroke="rgba(255,255,255,.1)" stroke-width="1" vector-effect="non-scaling-stroke"></path>
<path d="M50,20 L68,78" stroke="rgba(94,200,216,.35)" stroke-width="1" stroke-dasharray="2 3" vector-effect="non-scaling-stroke" style="animation:cm-dash 2.3s linear infinite;"></path>
<path d="M50,20 L32,78" stroke="rgba(255,255,255,.07)" stroke-width="1" vector-effect="non-scaling-stroke"></path>
<path d="M18,44 L82,44" stroke="rgba(255,255,255,.07)" stroke-width="1" vector-effect="non-scaling-stroke"></path>
</svg>
<!-- peer chip: Zeus -->
<div style="position:absolute; left:50%; top:20%; transform:translate(-50%,-50%); display:flex; flex-direction:column; align-items:center; gap:7px; cursor:pointer;">
<div style="width:40px; height:40px; border-radius:11px; background:linear-gradient(135deg,#c98af0,#9a5ad8); display:flex; align-items:center; justify-content:center; font-weight:700; color:#1a0a2a; font-size:15px;">Z</div>
<div style="text-align:center;"><div style="font-size:12px; font-weight:600;">Zeus</div><div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72;">org</div></div>
</div>
<!-- thor -->
<div style="position:absolute; left:82%; top:44%; transform:translate(-50%,-50%); display:flex; flex-direction:column; align-items:center; gap:7px; cursor:pointer;">
<div style="width:40px; height:40px; border-radius:11px; background:linear-gradient(135deg,#8a9af0,#5a6ad8); display:flex; align-items:center; justify-content:center; font-weight:700; color:#0a0e2a; font-size:15px;">T</div>
<div style="text-align:center;"><div style="font-size:12px; font-weight:600;">thor</div><div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72;">company</div></div>
</div>
<!-- POD01 -->
<div style="position:absolute; left:68%; top:78%; transform:translate(-50%,-50%); display:flex; flex-direction:column; align-items:center; gap:7px; cursor:pointer;">
<div style="width:40px; height:40px; border-radius:11px; background:linear-gradient(135deg,#6fd0c0,#4aa3b8); display:flex; align-items:center; justify-content:center; font-weight:700; color:#06201f; font-size:15px;">P</div>
<div style="text-align:center;"><div style="font-size:12px; font-weight:600;">POD01</div><div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72;">team</div></div>
</div>
<!-- Smith (selected) -->
<div style="position:absolute; left:32%; top:78%; transform:translate(-50%,-50%); display:flex; flex-direction:column; align-items:center; gap:7px; cursor:pointer;">
<div style="position:relative; width:46px; height:46px;">
<div style="position:absolute; inset:-4px; border-radius:50%; border:2px solid #ff6f61;"></div>
<div style="position:relative; width:46px; height:46px; border-radius:50%; background:linear-gradient(135deg,#ff8a7a,#ff5f57); display:flex; align-items:center; justify-content:center; font-weight:700; color:#2a0d0a; font-size:16px;">S<span style="position:absolute; right:1px; bottom:1px; width:9px; height:9px; border-radius:50%; background:#5fd08a; border:2px solid #08080a;"></span></div>
</div>
<div style="text-align:center;"><div style="font-size:12px; font-weight:700; color:#fff;">Smith</div><div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#ff8a7a;">agent</div></div>
</div>
<!-- Morpheus -->
<div style="position:absolute; left:18%; top:44%; transform:translate(-50%,-50%); display:flex; flex-direction:column; align-items:center; gap:7px; cursor:pointer;">
<div style="width:46px; height:46px; border-radius:50%; background:linear-gradient(135deg,#ff8a7a,#ff5f57); display:flex; align-items:center; justify-content:center; font-weight:700; color:#2a0d0a; font-size:16px; position:relative;">M<span style="position:absolute; right:1px; bottom:1px; width:9px; height:9px; border-radius:50%; background:#5fd08a; border:2px solid #08080a;"></span></div>
<div style="text-align:center;"><div style="font-size:12px; font-weight:600;">Morpheus</div><div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72;">agent</div></div>
</div>
</div>
</div>
<div style="position:absolute; inset:0; display:{{ lShow }};">
<div style="position:absolute; inset:0;">
<canvas ref="{{ canvasRef }}" style="width:100%; height:100%; display:block;"></canvas>
<!-- live legend -->
<div style="position:absolute; top:64px; left:16px; z-index:14; display:flex; flex-direction:column; gap:7px; padding:11px 13px; border-radius:11px; border:1px solid rgba(255,255,255,.08); background:rgba(14,14,18,.7); backdrop-filter:blur(8px);">
<div style="font-family:'JetBrains Mono',monospace; font-size:9px; letter-spacing:.12em; color:#5a5a62; margin-bottom:1px;">CONVERGING ON</div>
<div style="display:flex; align-items:center; gap:8px;"><span style="width:9px; height:9px; border-radius:50%; background:#ff8a7a;"></span><span style="font-family:'JetBrains Mono',monospace; font-size:11px; color:#cfcfd5;">services</span></div>
<div style="display:flex; align-items:center; gap:8px;"><span style="width:9px; height:9px; border-radius:50%; background:#5ec8d8;"></span><span style="font-family:'JetBrains Mono',monospace; font-size:11px; color:#cfcfd5;">events</span></div>
<div style="display:flex; align-items:center; gap:8px; margin-top:4px;"><span style="width:9px; height:9px; border-radius:50%; background:#ff5f57; box-shadow:0 0 8px #ff5f57;"></span><span style="font-family:'JetBrains Mono',monospace; font-size:11px; color:#cfcfd5;">agents (M · S)</span></div>
</div>
<!-- live HUD -->
<div style="position:absolute; top:64px; right:16px; z-index:14; display:flex; gap:8px;">
<div style="display:flex; align-items:center; gap:6px; padding:6px 11px; border-radius:8px; border:1px solid rgba(94,200,216,.25); background:rgba(94,200,216,.06); font-family:'JetBrains Mono',monospace; font-size:10px; color:#5ec8d8;"><span style="width:6px; height:6px; border-radius:50%; background:#5ec8d8; animation:cm-blink 1s infinite;"></span>9 active nodes</div>
<div style="display:flex; align-items:center; gap:6px; padding:6px 11px; border-radius:8px; border:1px solid rgba(255,255,255,.08); background:rgba(20,20,24,.6); font-family:'JetBrains Mono',monospace; font-size:10px; color:#9a9aa2;">▮▮ 14 touches/min</div>
</div>
</div>
</div>
<!-- ZOOM -->
<div style="position:absolute; bottom:16px; left:16px; z-index:15; display:flex; flex-direction:column; gap:5px;">
<div style="width:28px; height:28px; border-radius:7px; border:1px solid rgba(255,255,255,.1); background:rgba(20,20,24,.7); display:flex; align-items:center; justify-content:center; color:#9a9aa2; font-size:15px;">+</div>
<div style="width:28px; height:28px; border-radius:7px; border:1px solid rgba(255,255,255,.1); background:rgba(20,20,24,.7); display:flex; align-items:center; justify-content:center; color:#9a9aa2; font-size:15px;">−</div>
<div style="width:28px; height:28px; border-radius:7px; border:1px solid rgba(255,255,255,.1); background:rgba(20,20,24,.7); display:flex; align-items:center; justify-content:center; color:#9a9aa2;"><svg width="12" height="12" viewBox="0 0 14 14"><path d="M2 2h3M2 2v3M12 2h-3M12 2v3M2 12h3M2 12v-3M12 12h-3M12 12v-3" stroke="currentColor" stroke-width="1.3"></path></svg></div>
</div>
<!-- MINIMAP -->
<div style="position:absolute; bottom:16px; right:16px; z-index:15; width:150px; height:96px; border-radius:9px; border:1px solid rgba(255,255,255,.08); background:rgba(12,12,15,.8); overflow:hidden;">
<div style="position:absolute; top:5px; left:8px; font-family:'JetBrains Mono',monospace; font-size:8px; letter-spacing:.1em; color:#5a5a62;">{{ minimapLabel }}</div>
<svg viewBox="0 0 150 96" style="position:absolute; inset:0; width:100%; height:100%; opacity:.7;">
<line x1="75" y1="48" x2="50" y2="30" stroke="rgba(255,255,255,.12)" stroke-width="1"></line>
<line x1="75" y1="48" x2="100" y2="34" stroke="rgba(255,255,255,.12)" stroke-width="1"></line>
<line x1="75" y1="48" x2="60" y2="66" stroke="rgba(255,255,255,.12)" stroke-width="1"></line>
<line x1="75" y1="48" x2="96" y2="64" stroke="rgba(255,255,255,.12)" stroke-width="1"></line>
<circle cx="75" cy="48" r="4" fill="#ff6f61"></circle>
<circle cx="50" cy="30" r="2.5" fill="#5ec8d8"></circle>
<circle cx="100" cy="34" r="2.5" fill="#5fd08a"></circle>
<circle cx="60" cy="66" r="2.5" fill="#c98af0"></circle>
<circle cx="96" cy="64" r="2.5" fill="#e8b465"></circle>
<rect x="60" y="58" width="42" height="30" rx="2" fill="none" stroke="rgba(255,255,255,.2)" stroke-width="1"></rect>
</svg>
</div>
</div>
<!-- RIGHT PANEL -->
<div style="width:300px; flex:none; border-left:1px solid rgba(255,255,255,.06); background:#0b0b0e; display:flex; flex-direction:column; min-height:0;">
<div style="display:flex; align-items:center; gap:8px; padding:12px 14px; border-bottom:1px solid rgba(255,255,255,.06);">
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; letter-spacing:.14em; color:#5a5a62;">PANEL</span>
<span style="flex:1;"></span>
<span style="color:#5a5a62; font-size:13px;">✕</span>
</div>
<div style="flex:1; overflow-y:auto; padding:18px 16px;">
<div style="display:flex; flex-direction:column; align-items:center; text-align:center; margin-bottom:18px;">
<div style="position:relative; width:64px; height:64px; margin-bottom:11px;">
<div style="position:absolute; inset:-3px; border-radius:50%; border:1.5px solid rgba(255,111,97,.4);"></div>
<div style="width:64px; height:64px; border-radius:50%; background:linear-gradient(135deg,#ff8a7a,#ff5f57); display:flex; align-items:center; justify-content:center; font-size:24px; font-weight:700; color:#2a0d0a;">S</div>
</div>
<div style="font-size:17px; font-weight:700;">Smith</div>
<div style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#6a6a72; margin-top:2px;">Research Specialist</div>
<span style="margin-top:8px; font-family:'JetBrains Mono',monospace; font-size:9px; color:#5fd08a; padding:3px 9px; border-radius:6px; background:rgba(95,208,138,.1); border:1px solid rgba(95,208,138,.25);">● online</span>
</div>
<div style="font-family:'JetBrains Mono',monospace; font-size:9px; letter-spacing:.14em; color:#5a5a62; margin-bottom:10px;">BELONGS TO</div>
<div style="display:flex; flex-direction:column; gap:1px; margin-bottom:18px;">
<div style="display:flex; align-items:center; gap:9px; padding:9px 10px; border-radius:8px;"><span style="width:7px; height:7px; border-radius:50%; background:#5fd08a;"></span><span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72; width:54px;">TEAM</span><span style="font-size:13px; font-weight:600;">POD01</span></div>
<div style="display:flex; align-items:center; gap:9px; padding:9px 10px; border-radius:8px;"><span style="width:7px; height:7px; border-radius:50%; background:#8a9af0;"></span><span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72; width:54px;">COMPANY</span><span style="font-size:13px; font-weight:600;">thor</span></div>
<div style="display:flex; align-items:center; gap:9px; padding:9px 10px; border-radius:8px;"><span style="width:7px; height:7px; border-radius:50%; background:#c98af0;"></span><span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72; width:54px;">ORG</span><span style="font-size:13px; font-weight:600;">Zeus</span></div>
</div>
<div style="display:flex; gap:8px; margin-bottom:18px;">
<div style="flex:1; text-align:center; padding:12px 0; border-radius:10px; border:1px solid rgba(255,255,255,.07); background:#0d0d10;"><div style="font-size:18px; font-weight:700;">0</div><div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72; margin-top:2px;">SKILLS</div></div>
<div style="flex:1; text-align:center; padding:12px 0; border-radius:10px; border:1px solid rgba(255,255,255,.07); background:#0d0d10;"><div style="font-size:18px; font-weight:700;">0</div><div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72; margin-top:2px;">TOOLS</div></div>
<div style="flex:1; text-align:center; padding:12px 0; border-radius:10px; border:1px solid rgba(255,255,255,.07); background:#0d0d10;"><div style="font-size:18px; font-weight:700;">0</div><div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72; margin-top:2px;">RUNNING</div></div>
</div>
</div>
<div style="flex:none; padding:12px 14px; border-top:1px solid rgba(255,255,255,.06);">
<div style="display:flex; align-items:center; justify-content:center; gap:8px; height:38px; border-radius:9px; background:linear-gradient(135deg,#ff8a7a,#ff5f57); color:#2a0d0a; font-size:13px; font-weight:700; cursor:pointer;">⛶ More details</div>
</div>
</div>
</div>
<!-- STATUS BAR -->
<div style="height:26px; flex:none; display:flex; align-items:center; gap:16px; padding:0 16px; border-top:1px solid rgba(255,255,255,.06); background:#0a0a0c; font-family:'JetBrains Mono',monospace; font-size:10px; color:#5a5a62;">
<span style="color:#5fd08a;">● durable runner ok</span>
<span>checkpoint 3s ago</span>
<span style="flex:1;"></span>
<span>§15 sandbox: isolated</span>
<span style="color:#e8b465;">doors awaiting approval</span>
</div>
</div>
</x-dc>
<script type="text/x-dc" data-dc-script>
class Component extends DCLogic {
state = { view: 'live', selected: 'smith' };
componentDidUpdate(prevProps, prevState) {
if (prevState && prevState.view !== this.state.view) {
if (this.state.view === 'live') this._startLive();
else this._stopLive();
}
}
componentWillUnmount() { this._stopLive(); }
_setCanvas = (el) => {
this._cv = el;
if (el && this.state.view === 'live') this._startLive();
else if (!el) this._stopLive();
};
_stopLive() { if (this._raf) { cancelAnimationFrame(this._raf); this._raf = null; } }
_startLive() {
const cv = this._cv;
if (!cv) return;
this._stopLive();
// Gource-style world: a root blooms into area/service/event nodes (the "files"),
// and agent particles stream toward whatever node is currently active, emitting beams.
const targets = [
{ id:'runtime', label:'cm-runtime', kind:'service', col:'#ff8a7a', ang:-0.5, rad:0.30 },
{ id:'pr214', label:'PR #214', kind:'event', col:'#5ec8d8', ang:0.15, rad:0.40 },
{ id:'advdb', label:'advisory-db',kind:'service', col:'#5fd08a', ang:0.9, rad:0.34 },
{ id:'slack', label:'#eng', kind:'event', col:'#c98af0', ang:1.7, rad:0.42 },
{ id:'orch', label:'cm-orch', kind:'service', col:'#e8b465', ang:2.5, rad:0.30 },
{ id:'deploy', label:'PROD deploy',kind:'event', col:'#ff6f61', ang:3.5, rad:0.40 },
{ id:'docs', label:'spec §15', kind:'service', col:'#6fd0c0', ang:4.3, rad:0.33 },
{ id:'bench', label:'topo-bench', kind:'service', col:'#8a9af0', ang:5.1, rad:0.41 },
{ id:'broker', label:'secret-broker',kind:'service',col:'#9a9aa2', ang:5.7, rad:0.28 },
];
const agents = [
{ id:'m', label:'M', col:'#ff5f57', ink:'#2a0d0a', t:0, target:1 },
{ id:'s', label:'S', col:'#4aa3b8', ink:'#06201f', t:2, target:2 },
];
const beams = []; // {ax, ay, tx, ty, col, life}
const sparks = []; // {x, y, col, r, life}
const dpr = Math.min(2, window.devicePixelRatio || 1);
const ctx = cv.getContext('2d');
let W = 0, H = 0, cx = 0, cy = 0, rmin = 0;
const resize = () => {
const r = cv.getBoundingClientRect();
W = r.width; H = r.height;
cv.width = W * dpr; cv.height = H * dpr;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
cx = W / 2; cy = H / 2 + 8; rmin = Math.min(W, H);
};
resize();
this._onResize = resize;
window.addEventListener('resize', resize);
const tpos = (t) => ({ x: cx + Math.cos(t.ang) * rmin * t.rad, y: cy + Math.sin(t.ang) * rmin * t.rad });
// each agent orbits + drifts toward its current target; retargets periodically
agents.forEach(a => { a.x = cx; a.y = cy; a.retime = 1.5 + Math.random()*2; });
let last = performance.now();
const tick = (now) => {
const dt = Math.min(0.05, (now - last) / 1000); last = now;
ctx.clearRect(0, 0, W, H);
// faint tree: root -> each target
ctx.lineWidth = 1;
targets.forEach(t => {
const p = tpos(t);
ctx.strokeStyle = 'rgba(255,255,255,0.05)';
ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(p.x, p.y); ctx.stroke();
});
// target nodes
targets.forEach(t => {
const p = tpos(t); t._x = p.x; t._y = p.y;
const pulse = t._hot ? 1 + 0.25 * Math.sin(now/120) : 1;
const baseR = (t.kind === 'service' ? 7 : 5) * pulse;
// glow when hot
if (t._hot) {
const g = ctx.createRadialGradient(p.x, p.y, 0, p.x, p.y, 26);
g.addColorStop(0, t.col + '55'); g.addColorStop(1, t.col + '00');
ctx.fillStyle = g; ctx.beginPath(); ctx.arc(p.x, p.y, 26, 0, 7); ctx.fill();
}
ctx.fillStyle = t.col; ctx.globalAlpha = t._hot ? 1 : 0.55;
ctx.beginPath(); ctx.arc(p.x, p.y, baseR, 0, 7); ctx.fill();
ctx.globalAlpha = 1;
ctx.fillStyle = t._hot ? 'rgba(255,255,255,0.85)' : 'rgba(255,255,255,0.4)';
ctx.font = "600 10px 'JetBrains Mono', monospace";
ctx.textAlign = 'center';
ctx.fillText(t.label, p.x, p.y - baseR - 7);
t._hot = false;
});
// root
const rg = ctx.createRadialGradient(cx, cy, 0, cx, cy, 30);
rg.addColorStop(0, 'rgba(255,111,97,0.5)'); rg.addColorStop(1, 'rgba(255,111,97,0)');
ctx.fillStyle = rg; ctx.beginPath(); ctx.arc(cx, cy, 30, 0, 7); ctx.fill();
ctx.fillStyle = '#ff6f61'; ctx.beginPath(); ctx.arc(cx, cy, 9, 0, 7); ctx.fill();
ctx.fillStyle = '#fff'; ctx.font = "700 9px 'JetBrains Mono', monospace"; ctx.textAlign = 'center';
ctx.fillText('Z', cx, cy + 3);
// agents move + emit beams
agents.forEach(a => {
a.retime -= dt;
if (a.retime <= 0) { a.target = Math.floor(Math.random() * targets.length); a.retime = 1.2 + Math.random()*2.2; }
const tg = targets[a.target]; const p = tpos(tg);
// approach a point near the target (orbit a bit)
a.t += dt;
const ox = p.x + Math.cos(a.t*1.5) * 26;
const oy = p.y + Math.sin(a.t*1.5) * 26;
a.x += (ox - a.x) * Math.min(1, dt * 2.2);
a.y += (oy - a.y) * Math.min(1, dt * 2.2);
// proximity → emit beam + heat the node
const d = Math.hypot(a.x - p.x, a.y - p.y);
if (d < 60) {
tg._hot = true;
if (Math.random() < 0.5) beams.push({ ax:a.x, ay:a.y, tx:p.x, ty:p.y, col:a.col, life:1 });
if (Math.random() < 0.25) sparks.push({ x:p.x, y:p.y, col:tg.col, r:2, life:1 });
}
});
// beams
for (let i = beams.length - 1; i >= 0; i--) {
const b = beams[i]; b.life -= dt * 2.5;
if (b.life <= 0) { beams.splice(i, 1); continue; }
ctx.strokeStyle = b.col + Math.floor(b.life * 200).toString(16).padStart(2,'0');
ctx.lineWidth = 1.5 * b.life + 0.4;
ctx.beginPath(); ctx.moveTo(b.ax, b.ay); ctx.lineTo(b.tx, b.ty); ctx.stroke();
}
// sparks
for (let i = sparks.length - 1; i >= 0; i--) {
const s = sparks[i]; s.life -= dt * 1.8; s.r += dt * 22;
if (s.life <= 0) { sparks.splice(i, 1); continue; }
ctx.strokeStyle = s.col + Math.floor(s.life * 160).toString(16).padStart(2,'0');
ctx.lineWidth = 1.2;
ctx.beginPath(); ctx.arc(s.x, s.y, s.r, 0, 7); ctx.stroke();
}
// agent avatars on top
agents.forEach(a => {
ctx.fillStyle = a.col;
ctx.shadowColor = a.col; ctx.shadowBlur = 14;
ctx.beginPath(); ctx.arc(a.x, a.y, 11, 0, 7); ctx.fill();
ctx.shadowBlur = 0;
ctx.fillStyle = a.ink; ctx.font = "700 11px 'JetBrains Mono', monospace"; ctx.textAlign = 'center';
ctx.fillText(a.label, a.x, a.y + 4);
});
this._raf = requestAnimationFrame(tick);
};
this._raf = requestAnimationFrame(tick);
}
renderVals() {
const v = this.state.view;
const mk = (id, label, icon) => {
const active = v === id;
return { id, label, icon, active,
style: active ? 'background:rgba(255,111,97,.16); color:#ff8a7a;' : 'color:#9a9aa2;',
onPick: () => this.setState({ view: id }) };
};
const captions = {
hierarchy: 'hierarchy — org ▸ company ▸ team ▸ claw',
flat: 'flat topology — every node a peer, no nesting',
live: 'live — agents converge on the projects, services & events they touch (Gource-style)',
};
const minimaps = { hierarchy:'TREE', flat:'FLAT', live:'LIVE' };
return {
modes: [ mk('hierarchy','Hierarchy','▤'), mk('flat','Flat','⬡'), mk('live','Live','✦') ],
isHierarchy: v==='hierarchy', isFlat: v==='flat', isLive: v==='live',
hShow: v==='hierarchy'?'block':'none', fShow: v==='flat'?'block':'none', lShow: v==='live'?'block':'none',
caption: captions[v], minimapLabel: minimaps[v],
canvasRef: this._setCanvas,
};
}
}
</script>
</body>
</html>
File diff suppressed because it is too large Load Diff