Fleet P2b: run agent sandboxes on connected nodes (RemoteDriver + placement)
Agents can now provision their sandbox on a connected fleet node instead of the gateway host. Local stays the strict default, so existing agents are byte-for- byte unaffected until explicitly placed elsewhere. Security parity: the daemon links the REAL cm-sandbox DockerDriver and runs the typed container ops (sb_provision/sb_exec/sb_destroy/sb_health/sb_list) through it — identical hardening (cap-drop ALL, seccomp, no-net, read-only, non-root) to local sandboxes. cm-sandbox spec types are now Serialize/Deserialize so the spec crosses the channel. - cm-api: RemoteDriver (impl SandboxDriver over the node channel) + HubDriverProvider (impl cm_runtime::NodeDriverProvider, hands out a driver only for connected nodes via a sync online set) + NodeHub.call/is_connected. AppState.with_node_hub so the hub is shared with the placement provider. - cm-runtime SandboxManager: driver_for(node_id) routes by the recorded agent_containers.node_id (local default = existing driver, identical path); placement_node() reads the workspace setting and falls back to local if the node is offline; exec/release route accordingly. NodeDriverProvider trait. - DB: 0020_workspace_placement + repo (for_agent/get/set/clear). - main.rs: build the NodeHub first; inject HubDriverProvider into the agent manager + share the hub with AppState. - API+UI: GET/PUT /api/fleet/placement + a "Run agents on: Local / <node>" selector in the Fleet overview. Note: a node must be able to pull the agent image (the daemon docker-pulls it); interactive PTY for agent containers on remote nodes is not wired (Terminal app stays local) — the in-dashboard node shell already covers host access. Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
33aa9c0693
commit
fb59378aa2
Generated
+3
@@ -740,6 +740,7 @@ name = "clawmates-node"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64",
|
"base64",
|
||||||
|
"cm-sandbox",
|
||||||
"futures",
|
"futures",
|
||||||
"portable-pty",
|
"portable-pty",
|
||||||
"serde",
|
"serde",
|
||||||
@@ -834,6 +835,7 @@ name = "cm-api"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"async-stream",
|
"async-stream",
|
||||||
|
"async-trait",
|
||||||
"axum",
|
"axum",
|
||||||
"base64",
|
"base64",
|
||||||
"cm-auth",
|
"cm-auth",
|
||||||
@@ -846,6 +848,7 @@ dependencies = [
|
|||||||
"cm-orchestrator",
|
"cm-orchestrator",
|
||||||
"cm-runtime",
|
"cm-runtime",
|
||||||
"cm-safety",
|
"cm-safety",
|
||||||
|
"cm-sandbox",
|
||||||
"cm-scheduler",
|
"cm-scheduler",
|
||||||
"cm-secrets",
|
"cm-secrets",
|
||||||
"cm-testkit",
|
"cm-testkit",
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ serde_json = { workspace = true }
|
|||||||
sysinfo = "0.33"
|
sysinfo = "0.33"
|
||||||
portable-pty = "0.8"
|
portable-pty = "0.8"
|
||||||
base64 = "0.22"
|
base64 = "0.22"
|
||||||
|
cm-sandbox = { path = "../../cm-sandbox" }
|
||||||
|
|
||||||
[lints]
|
[lints]
|
||||||
workspace = true
|
workspace = true
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ use std::sync::Arc;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
|
use cm_sandbox::{DockerDriver, SandboxDriver, SandboxHandle, SandboxSpec};
|
||||||
use futures::{SinkExt, StreamExt};
|
use futures::{SinkExt, StreamExt};
|
||||||
use portable_pty::{native_pty_system, CommandBuilder, MasterPty, PtySize};
|
use portable_pty::{native_pty_system, CommandBuilder, MasterPty, PtySize};
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
@@ -208,6 +209,15 @@ async fn handle_frame(text: &str, out: &mpsc::UnboundedSender<String>, ptys: &Pt
|
|||||||
let _ = out.send(json!({ "t": "result", "id": id, "ok": ok, "output": output }).to_string());
|
let _ = out.send(json!({ "t": "result", "id": id, "ok": ok, "output": output }).to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Agent-sandbox container ops: drive the REAL DockerDriver so the
|
||||||
|
// hardening (cap-drop ALL, seccomp, no-net, read-only, non-root) is
|
||||||
|
// byte-identical to the gateway's local sandboxes.
|
||||||
|
op @ ("sb_provision" | "sb_exec" | "sb_destroy" | "sb_health" | "sb_list") => {
|
||||||
|
if let Some(id) = v.get("id").and_then(Value::as_u64) {
|
||||||
|
let (ok, output) = sb_op(op, &v).await;
|
||||||
|
let _ = out.send(json!({ "t": "result", "id": id, "ok": ok, "output": output }).to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
"pty_open" => {
|
"pty_open" => {
|
||||||
let sid = v.get("sid").and_then(Value::as_u64).unwrap_or(0);
|
let sid = v.get("sid").and_then(Value::as_u64).unwrap_or(0);
|
||||||
let cols = v.get("cols").and_then(Value::as_u64).unwrap_or(80) as u16;
|
let cols = v.get("cols").and_then(Value::as_u64).unwrap_or(80) as u16;
|
||||||
@@ -331,6 +341,71 @@ async fn sandbox_check() -> (bool, String) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn handle_of(v: &Value) -> SandboxHandle {
|
||||||
|
SandboxHandle {
|
||||||
|
id: v.get("cid").and_then(Value::as_str).unwrap_or_default().to_owned(),
|
||||||
|
name: v.get("cname").and_then(Value::as_str).unwrap_or_default().to_owned(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run an agent-sandbox container op via the local DockerDriver (full hardening),
|
||||||
|
/// returning the result payload as JSON or an error message.
|
||||||
|
async fn sb_op(op: &str, v: &Value) -> (bool, String) {
|
||||||
|
let driver = match DockerDriver::connect() {
|
||||||
|
Ok(d) => d,
|
||||||
|
Err(e) => return (false, format!("docker unavailable: {e}")),
|
||||||
|
};
|
||||||
|
match op {
|
||||||
|
"sb_provision" => {
|
||||||
|
let spec: SandboxSpec = match v.get("spec").and_then(|s| serde_json::from_value(s.clone()).ok()) {
|
||||||
|
Some(s) => s,
|
||||||
|
None => return (false, "invalid spec".to_owned()),
|
||||||
|
};
|
||||||
|
// Pull the agent image if the node doesn't have it yet (best effort).
|
||||||
|
let _ = tokio::process::Command::new("docker").args(["pull", "-q", &spec.image]).output().await;
|
||||||
|
match driver.provision(&spec).await {
|
||||||
|
Ok(h) => (true, json!({ "id": h.id, "name": h.name }).to_string()),
|
||||||
|
Err(e) => (false, format!("provision: {e}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"sb_exec" => {
|
||||||
|
let handle = handle_of(v);
|
||||||
|
let cmd: Vec<String> = v
|
||||||
|
.get("cmd")
|
||||||
|
.and_then(Value::as_array)
|
||||||
|
.map(|a| a.iter().filter_map(|x| x.as_str().map(str::to_owned)).collect())
|
||||||
|
.unwrap_or_default();
|
||||||
|
let refs: Vec<&str> = cmd.iter().map(String::as_str).collect();
|
||||||
|
match driver.exec(&handle, &refs).await {
|
||||||
|
Ok(r) => (
|
||||||
|
r.exit_code == 0,
|
||||||
|
json!({ "exit": r.exit_code, "stdout": r.stdout, "stderr": r.stderr }).to_string(),
|
||||||
|
),
|
||||||
|
Err(e) => (false, format!("exec: {e}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"sb_destroy" => match driver.destroy(&handle_of(v)).await {
|
||||||
|
Ok(()) => (true, "ok".to_owned()),
|
||||||
|
Err(e) => (false, format!("destroy: {e}")),
|
||||||
|
},
|
||||||
|
"sb_health" => match driver.health(&handle_of(v)).await {
|
||||||
|
Ok(alive) => (true, json!({ "alive": alive }).to_string()),
|
||||||
|
Err(e) => (false, format!("health: {e}")),
|
||||||
|
},
|
||||||
|
"sb_list" => {
|
||||||
|
let kind = v.get("kind").and_then(Value::as_str).unwrap_or("agent");
|
||||||
|
match driver.list_managed(kind).await {
|
||||||
|
Ok(list) => (
|
||||||
|
true,
|
||||||
|
json!(list.iter().map(|m| json!({ "id": m.id, "created_unix": m.created_unix })).collect::<Vec<_>>()).to_string(),
|
||||||
|
),
|
||||||
|
Err(e) => (false, format!("list: {e}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => (false, "unknown op".to_owned()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Best-effort: join the user's tailnet (BYO Tailscale) and enable Tailscale SSH
|
/// Best-effort: join the user's tailnet (BYO Tailscale) and enable Tailscale SSH
|
||||||
/// so they can reach this node keylessly. Failures are non-fatal — the WSS
|
/// so they can reach this node keylessly. Failures are non-fatal — the WSS
|
||||||
/// control channel works regardless.
|
/// control channel works regardless.
|
||||||
|
|||||||
@@ -144,6 +144,9 @@ async fn run() -> Result<(), String> {
|
|||||||
.map_err(|e| format!("s3 storage: {e}"))?,
|
.map_err(|e| format!("s3 storage: {e}"))?,
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
|
// The fleet node hub (live daemon channels) is shared between the sandbox
|
||||||
|
// placement provider and the API routes, so it must exist before the managers.
|
||||||
|
let node_hub = std::sync::Arc::new(cm_api::fleet::NodeHub::new());
|
||||||
// Environment tools need a container engine; absence is tolerated
|
// Environment tools need a container engine; absence is tolerated
|
||||||
// (shell.exec reports it per-call) so the API still serves.
|
// (shell.exec reports it per-call) so the API still serves.
|
||||||
let (sandboxes, browser, terminals) = if config.sandbox.enabled {
|
let (sandboxes, browser, terminals) = if config.sandbox.enabled {
|
||||||
@@ -151,12 +154,17 @@ async fn run() -> Result<(), String> {
|
|||||||
Ok(driver) => {
|
Ok(driver) => {
|
||||||
let driver: std::sync::Arc<dyn cm_sandbox::SandboxDriver> =
|
let driver: std::sync::Arc<dyn cm_sandbox::SandboxDriver> =
|
||||||
std::sync::Arc::new(driver);
|
std::sync::Arc::new(driver);
|
||||||
let agents = std::sync::Arc::new(cm_runtime::SandboxManager::new(
|
let agents = std::sync::Arc::new(
|
||||||
|
cm_runtime::SandboxManager::new(
|
||||||
driver.clone(),
|
driver.clone(),
|
||||||
pool.clone(),
|
pool.clone(),
|
||||||
"local",
|
"local",
|
||||||
&config.sandbox.image,
|
&config.sandbox.image,
|
||||||
));
|
)
|
||||||
|
.with_node_provider(std::sync::Arc::new(
|
||||||
|
cm_api::fleet::HubDriverProvider::new(node_hub.clone()),
|
||||||
|
)),
|
||||||
|
);
|
||||||
let browser = std::sync::Arc::new(
|
let browser = std::sync::Arc::new(
|
||||||
cm_runtime::SandboxManager::new(
|
cm_runtime::SandboxManager::new(
|
||||||
driver.clone(),
|
driver.clone(),
|
||||||
@@ -278,6 +286,7 @@ async fn run() -> Result<(), String> {
|
|||||||
|
|
||||||
let mut app = cm_api::router(
|
let mut app = cm_api::router(
|
||||||
cm_api::AppState::new(pool, runtime)
|
cm_api::AppState::new(pool, runtime)
|
||||||
|
.with_node_hub(node_hub.clone())
|
||||||
.with_broker(PathBuf::from(&config.broker.socket_path))
|
.with_broker(PathBuf::from(&config.broker.socket_path))
|
||||||
.with_oauth(config.oauth.clone())
|
.with_oauth(config.oauth.clone())
|
||||||
.with_billing(config.billing.clone())
|
.with_billing(config.billing.clone())
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ cm-db = { path = "../cm-db" }
|
|||||||
cm-domain = { path = "../cm-domain" }
|
cm-domain = { path = "../cm-domain" }
|
||||||
cm-orchestrator = { path = "../cm-orchestrator", features = ["provider"] }
|
cm-orchestrator = { path = "../cm-orchestrator", features = ["provider"] }
|
||||||
cm-runtime = { path = "../cm-runtime" }
|
cm-runtime = { path = "../cm-runtime" }
|
||||||
|
cm-sandbox = { path = "../cm-sandbox" }
|
||||||
cm-safety = { path = "../cm-safety" }
|
cm-safety = { path = "../cm-safety" }
|
||||||
|
async-trait = "0.1"
|
||||||
cm-scheduler = { path = "../cm-scheduler" }
|
cm-scheduler = { path = "../cm-scheduler" }
|
||||||
cm-secrets = { path = "../cm-secrets" }
|
cm-secrets = { path = "../cm-secrets" }
|
||||||
cm-topology = { path = "../cm-topology" }
|
cm-topology = { path = "../cm-topology" }
|
||||||
|
|||||||
+125
-2
@@ -3,7 +3,7 @@
|
|||||||
//! `GET /api/nodes/agent?token=…` (outbound), and we drive that socket to
|
//! `GET /api/nodes/agent?token=…` (outbound), and we drive that socket to
|
||||||
//! receive host-health heartbeats and to run commands on the node.
|
//! receive host-health heartbeats and to run commands on the node.
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::time::{Duration, Instant};
|
use std::time::{Duration, Instant};
|
||||||
@@ -12,9 +12,12 @@ use axum::extract::ws::{Message, WebSocket};
|
|||||||
use base64::Engine;
|
use base64::Engine;
|
||||||
use cm_db::repo::nodes::{self, NodeHealth};
|
use cm_db::repo::nodes::{self, NodeHealth};
|
||||||
use cm_domain::NodeId;
|
use cm_domain::NodeId;
|
||||||
|
use cm_sandbox::{
|
||||||
|
ExecResult, ManagedSandbox, PtySession, SandboxDriver, SandboxError, SandboxHandle, SandboxSpec,
|
||||||
|
};
|
||||||
use futures::{SinkExt, StreamExt};
|
use futures::{SinkExt, StreamExt};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::json;
|
use serde_json::{json, Value};
|
||||||
use sqlx::PgPool;
|
use sqlx::PgPool;
|
||||||
use tokio::sync::{mpsc, oneshot, Mutex};
|
use tokio::sync::{mpsc, oneshot, Mutex};
|
||||||
|
|
||||||
@@ -39,6 +42,9 @@ struct NodeConn {
|
|||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct NodeHub {
|
pub struct NodeHub {
|
||||||
conns: Mutex<HashMap<NodeId, Arc<NodeConn>>>,
|
conns: Mutex<HashMap<NodeId, Arc<NodeConn>>>,
|
||||||
|
/// Sync-readable set of connected node ids (so placement can check liveness
|
||||||
|
/// without an await — `conns` is behind an async mutex).
|
||||||
|
online: std::sync::Mutex<HashSet<NodeId>>,
|
||||||
/// Short-lived single-use terminal tickets (browser WS can't send a bearer).
|
/// Short-lived single-use terminal tickets (browser WS can't send a bearer).
|
||||||
tickets: Mutex<HashMap<String, (NodeId, Instant)>>,
|
tickets: Mutex<HashMap<String, (NodeId, Instant)>>,
|
||||||
}
|
}
|
||||||
@@ -71,6 +77,22 @@ impl NodeHub {
|
|||||||
.await
|
.await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sync liveness check (no await) — used by placement.
|
||||||
|
pub fn is_connected(&self, id: NodeId) -> bool {
|
||||||
|
self.online.lock().map(|s| s.contains(&id)).unwrap_or(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Send a typed op with JSON args and await its result (output is op-specific).
|
||||||
|
pub async fn call(&self, id: NodeId, op: &str, args: Value) -> Result<ExecOutput, String> {
|
||||||
|
self.request(id, |req_id| {
|
||||||
|
let mut o = args.as_object().cloned().unwrap_or_default();
|
||||||
|
o.insert("t".to_owned(), Value::String(op.to_owned()));
|
||||||
|
o.insert("id".to_owned(), Value::from(req_id));
|
||||||
|
Value::Object(o).to_string()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
}
|
||||||
|
|
||||||
/// Send a typed request frame and await the node's matching result.
|
/// Send a typed request frame and await the node's matching result.
|
||||||
async fn request(
|
async fn request(
|
||||||
&self,
|
&self,
|
||||||
@@ -205,6 +227,7 @@ pub async fn run_channel(pool: PgPool, hub: Arc<NodeHub>, node_id: NodeId, socke
|
|||||||
next_id: AtomicU64::new(0),
|
next_id: AtomicU64::new(0),
|
||||||
});
|
});
|
||||||
hub.conns.lock().await.insert(node_id, conn.clone());
|
hub.conns.lock().await.insert(node_id, conn.clone());
|
||||||
|
hub.online.lock().unwrap().insert(node_id);
|
||||||
|
|
||||||
let writer = async {
|
let writer = async {
|
||||||
while let Some(frame) = rx.recv().await {
|
while let Some(frame) = rx.recv().await {
|
||||||
@@ -271,5 +294,105 @@ pub async fn run_channel(pool: PgPool, hub: Arc<NodeHub>, node_id: NodeId, socke
|
|||||||
}
|
}
|
||||||
|
|
||||||
hub.conns.lock().await.remove(&node_id);
|
hub.conns.lock().await.remove(&node_id);
|
||||||
|
hub.online.lock().unwrap().remove(&node_id);
|
||||||
let _ = nodes::set_status(&pool, node_id, "offline").await;
|
let _ = nodes::set_status(&pool, node_id, "offline").await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── RemoteDriver: run agent sandboxes on a connected node over the channel ────
|
||||||
|
|
||||||
|
/// A `SandboxDriver` that proxies container ops to a connected fleet node's
|
||||||
|
/// daemon, which drives the REAL `DockerDriver` — so the hardening is identical
|
||||||
|
/// to a local sandbox. Interactive PTY is unsupported on remote nodes for now
|
||||||
|
/// (agents don't use it; the Terminal app stays local).
|
||||||
|
pub struct RemoteDriver {
|
||||||
|
hub: Arc<NodeHub>,
|
||||||
|
node_id: NodeId,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RemoteDriver {
|
||||||
|
pub fn new(hub: Arc<NodeHub>, node_id: NodeId) -> Self {
|
||||||
|
Self { hub, node_id }
|
||||||
|
}
|
||||||
|
async fn call(&self, op: &str, args: Value) -> Result<Value, SandboxError> {
|
||||||
|
let out = self
|
||||||
|
.hub
|
||||||
|
.call(self.node_id, op, args)
|
||||||
|
.await
|
||||||
|
.map_err(SandboxError::Engine)?;
|
||||||
|
if !out.ok {
|
||||||
|
return Err(SandboxError::Engine(out.output));
|
||||||
|
}
|
||||||
|
Ok(serde_json::from_str(&out.output).unwrap_or(Value::Null))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait::async_trait]
|
||||||
|
impl SandboxDriver for RemoteDriver {
|
||||||
|
async fn provision(&self, spec: &SandboxSpec) -> Result<SandboxHandle, SandboxError> {
|
||||||
|
let v = self.call("sb_provision", json!({ "spec": spec })).await?;
|
||||||
|
Ok(SandboxHandle {
|
||||||
|
id: v.get("id").and_then(Value::as_str).unwrap_or_default().to_owned(),
|
||||||
|
name: v.get("name").and_then(Value::as_str).unwrap_or_default().to_owned(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async fn exec(&self, handle: &SandboxHandle, cmd: &[&str]) -> Result<ExecResult, SandboxError> {
|
||||||
|
let v = self
|
||||||
|
.call("sb_exec", json!({ "cid": handle.id, "cname": handle.name, "cmd": cmd }))
|
||||||
|
.await?;
|
||||||
|
Ok(ExecResult {
|
||||||
|
exit_code: v.get("exit").and_then(Value::as_i64).unwrap_or(-1),
|
||||||
|
stdout: v.get("stdout").and_then(Value::as_str).unwrap_or_default().to_owned(),
|
||||||
|
stderr: v.get("stderr").and_then(Value::as_str).unwrap_or_default().to_owned(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
async fn attach_pty(&self, _h: &SandboxHandle, _cmd: &[&str], _c: u16, _r: u16, _e: &[String]) -> Result<PtySession, SandboxError> {
|
||||||
|
Err(SandboxError::Engine("interactive PTY is not supported on remote nodes yet".into()))
|
||||||
|
}
|
||||||
|
async fn resize_pty(&self, _exec_id: &str, _c: u16, _r: u16) -> Result<(), SandboxError> {
|
||||||
|
Err(SandboxError::Engine("pty resize is not supported on remote nodes".into()))
|
||||||
|
}
|
||||||
|
async fn destroy(&self, handle: &SandboxHandle) -> Result<(), SandboxError> {
|
||||||
|
self.call("sb_destroy", json!({ "cid": handle.id, "cname": handle.name })).await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
async fn health(&self, handle: &SandboxHandle) -> Result<bool, SandboxError> {
|
||||||
|
let v = self.call("sb_health", json!({ "cid": handle.id, "cname": handle.name })).await?;
|
||||||
|
Ok(v.get("alive").and_then(Value::as_bool).unwrap_or(false))
|
||||||
|
}
|
||||||
|
async fn list_managed(&self, kind: &str) -> Result<Vec<ManagedSandbox>, SandboxError> {
|
||||||
|
let v = self.call("sb_list", json!({ "kind": kind })).await?;
|
||||||
|
Ok(v.as_array()
|
||||||
|
.map(|a| {
|
||||||
|
a.iter()
|
||||||
|
.map(|m| ManagedSandbox {
|
||||||
|
id: m.get("id").and_then(Value::as_str).unwrap_or_default().to_owned(),
|
||||||
|
created_unix: m.get("created_unix").and_then(Value::as_i64).unwrap_or(0),
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
})
|
||||||
|
.unwrap_or_default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bridges the runtime's placement to live node channels: hands out a
|
||||||
|
/// `RemoteDriver` for any currently-connected fleet node (None ⇒ offline).
|
||||||
|
pub struct HubDriverProvider {
|
||||||
|
hub: Arc<NodeHub>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HubDriverProvider {
|
||||||
|
pub fn new(hub: Arc<NodeHub>) -> Self {
|
||||||
|
Self { hub }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl cm_runtime::NodeDriverProvider for HubDriverProvider {
|
||||||
|
fn driver(&self, node_id: &str) -> Option<Arc<dyn SandboxDriver>> {
|
||||||
|
let nid = NodeId::from(node_id.parse::<uuid::Uuid>().ok()?);
|
||||||
|
if self.hub.is_connected(nid) {
|
||||||
|
Some(Arc::new(RemoteDriver::new(self.hub.clone(), nid)))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -100,6 +100,13 @@ impl AppState {
|
|||||||
self.broker_socket = Some(socket);
|
self.broker_socket = Some(socket);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Share a pre-built node hub (so the fleet driver provider and the routes
|
||||||
|
/// use the same live channels).
|
||||||
|
pub fn with_node_hub(mut self, hub: std::sync::Arc<fleet::NodeHub>) -> AppState {
|
||||||
|
self.node_hub = hub;
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn router(state: AppState) -> Router {
|
pub fn router(state: AppState) -> Router {
|
||||||
@@ -124,6 +131,10 @@ pub fn router(state: AppState) -> Router {
|
|||||||
.delete(routes::tailscale::disconnect),
|
.delete(routes::tailscale::disconnect),
|
||||||
)
|
)
|
||||||
.route("/api/fleet/tailscale/devices", get(routes::tailscale::devices))
|
.route("/api/fleet/tailscale/devices", get(routes::tailscale::devices))
|
||||||
|
.route(
|
||||||
|
"/api/fleet/placement",
|
||||||
|
get(routes::tailscale::placement_get).put(routes::tailscale::placement_set),
|
||||||
|
)
|
||||||
.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))
|
||||||
|
|||||||
@@ -4,12 +4,48 @@
|
|||||||
|
|
||||||
use axum::extract::State;
|
use axum::extract::State;
|
||||||
use axum::Json;
|
use axum::Json;
|
||||||
use cm_db::repo::fleet_tailscale;
|
use cm_db::repo::{fleet_tailscale, nodes, workspace_placement};
|
||||||
|
use cm_domain::NodeId;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::{json, Value};
|
use serde_json::{json, Value};
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{ApiError, AppState, Authed};
|
use crate::{ApiError, AppState, Authed};
|
||||||
|
|
||||||
|
/// `GET /api/fleet/placement` — the workspace's default placement node (or null).
|
||||||
|
pub async fn placement_get(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Authed(user): Authed,
|
||||||
|
) -> Result<Json<Value>, ApiError> {
|
||||||
|
let node = workspace_placement::get(&state.pool, user.workspace_id).await?;
|
||||||
|
Ok(Json(json!({ "node": node })))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
pub struct PlacementReq {
|
||||||
|
pub node: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `PUT /api/fleet/placement` — set where new agent sandboxes provision. "local"
|
||||||
|
/// (or empty) reverts to the gateway host; a node id must belong to the caller.
|
||||||
|
pub async fn placement_set(
|
||||||
|
State(state): State<AppState>,
|
||||||
|
Authed(user): Authed,
|
||||||
|
Json(req): Json<PlacementReq>,
|
||||||
|
) -> Result<Json<Value>, ApiError> {
|
||||||
|
let node = req.node.trim();
|
||||||
|
if node.is_empty() || node == "local" {
|
||||||
|
workspace_placement::clear(&state.pool, user.workspace_id).await?;
|
||||||
|
return Ok(Json(json!({ "ok": true, "node": "local" })));
|
||||||
|
}
|
||||||
|
let nid = NodeId::from(node.parse::<Uuid>().map_err(|_| ApiError::NotFound)?);
|
||||||
|
nodes::get(&state.pool, nid, user.workspace_id)
|
||||||
|
.await?
|
||||||
|
.ok_or(ApiError::NotFound)?;
|
||||||
|
workspace_placement::set(&state.pool, user.workspace_id, node).await?;
|
||||||
|
Ok(Json(json!({ "ok": true, "node": node })))
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
#[derive(Deserialize)]
|
||||||
pub struct ConnectReq {
|
pub struct ConnectReq {
|
||||||
#[serde(rename = "apiKey")]
|
#[serde(rename = "apiKey")]
|
||||||
|
|||||||
@@ -23,4 +23,5 @@ pub mod terminal_tabs;
|
|||||||
pub mod threads;
|
pub mod threads;
|
||||||
pub mod topology_runs;
|
pub mod topology_runs;
|
||||||
pub mod users;
|
pub mod users;
|
||||||
|
pub mod workspace_placement;
|
||||||
pub mod workspaces;
|
pub mod workspaces;
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
//! Per-workspace default placement node for new agent sandboxes.
|
||||||
|
|
||||||
|
use cm_domain::{AgentId, WorkspaceId};
|
||||||
|
use sqlx::{PgPool, Row};
|
||||||
|
|
||||||
|
use crate::DbError;
|
||||||
|
|
||||||
|
/// The placement node for the workspace that owns `agent_id` (None ⇒ local).
|
||||||
|
pub async fn for_agent(pool: &PgPool, agent_id: AgentId) -> Result<Option<String>, DbError> {
|
||||||
|
let row = sqlx::query(
|
||||||
|
"SELECT p.node_id FROM workspace_placement p
|
||||||
|
JOIN agents a ON a.workspace_id = p.workspace_id
|
||||||
|
WHERE a.id = $1",
|
||||||
|
)
|
||||||
|
.bind(agent_id.as_uuid())
|
||||||
|
.fetch_optional(pool)
|
||||||
|
.await?;
|
||||||
|
Ok(row.map(|r| r.get("node_id")))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A workspace's placement node (None ⇒ local).
|
||||||
|
pub async fn get(pool: &PgPool, workspace_id: WorkspaceId) -> Result<Option<String>, DbError> {
|
||||||
|
let row = sqlx::query("SELECT node_id FROM workspace_placement WHERE workspace_id = $1")
|
||||||
|
.bind(workspace_id.as_uuid())
|
||||||
|
.fetch_optional(pool)
|
||||||
|
.await?;
|
||||||
|
Ok(row.map(|r| r.get("node_id")))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set the workspace's placement node.
|
||||||
|
pub async fn set(pool: &PgPool, workspace_id: WorkspaceId, node_id: &str) -> Result<(), DbError> {
|
||||||
|
sqlx::query(
|
||||||
|
"INSERT INTO workspace_placement (workspace_id, node_id, updated_at)
|
||||||
|
VALUES ($1, $2, now())
|
||||||
|
ON CONFLICT (workspace_id) DO UPDATE SET node_id = excluded.node_id, updated_at = now()",
|
||||||
|
)
|
||||||
|
.bind(workspace_id.as_uuid())
|
||||||
|
.bind(node_id)
|
||||||
|
.execute(pool)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Clear the workspace's placement (revert to local).
|
||||||
|
pub async fn clear(pool: &PgPool, workspace_id: WorkspaceId) -> Result<(), DbError> {
|
||||||
|
sqlx::query("DELETE FROM workspace_placement WHERE workspace_id = $1")
|
||||||
|
.bind(workspace_id.as_uuid())
|
||||||
|
.execute(pool)
|
||||||
|
.await?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -16,6 +16,6 @@ pub use outbox::{drain_once, spawn_drainer, EmailSender, LettreSender, SmtpConfi
|
|||||||
pub use runtime::{
|
pub use runtime::{
|
||||||
judge_model, ProviderRegistry, Runtime, RuntimeConfig, RuntimeError, StartedRun,
|
judge_model, ProviderRegistry, Runtime, RuntimeConfig, RuntimeError, StartedRun,
|
||||||
};
|
};
|
||||||
pub use sandboxes::SandboxManager;
|
pub use sandboxes::{NodeDriverProvider, SandboxManager};
|
||||||
pub use terminals::{DriveConfig, TerminalManager};
|
pub use terminals::{DriveConfig, TerminalManager};
|
||||||
pub use tools::{ClockNow, EmailSend, Tool, ToolContext, ToolRegistry};
|
pub use tools::{ClockNow, EmailSend, Tool, ToolContext, ToolRegistry};
|
||||||
|
|||||||
@@ -19,14 +19,25 @@ impl std::fmt::Debug for SandboxManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Supplies a sandbox driver for a placement node (a connected fleet node), so
|
||||||
|
/// the manager can run an agent's sandbox on a remote host instead of locally.
|
||||||
|
/// Implemented by the API layer over the live node channels; `None` ⇒ the node
|
||||||
|
/// is not connected (the manager falls back to local).
|
||||||
|
pub trait NodeDriverProvider: Send + Sync {
|
||||||
|
fn driver(&self, node_id: &str) -> Option<Arc<dyn SandboxDriver>>;
|
||||||
|
}
|
||||||
|
|
||||||
pub struct SandboxManager {
|
pub struct SandboxManager {
|
||||||
driver: Arc<dyn SandboxDriver>,
|
driver: Arc<dyn SandboxDriver>,
|
||||||
db: PgPool,
|
db: PgPool,
|
||||||
/// Placement node this manager's driver provisions onto ("local" today).
|
/// This manager's LOCAL placement id ("local"); remote placements come from
|
||||||
|
/// `node_provider` keyed by a fleet node's id.
|
||||||
node_id: String,
|
node_id: String,
|
||||||
image: String,
|
image: String,
|
||||||
egress: bool,
|
egress: bool,
|
||||||
/// Pre-provisioned, unassigned sandboxes (the warm pool, per-replica).
|
/// Resolves drivers for remote fleet nodes (None ⇒ local-only deployment).
|
||||||
|
node_provider: Option<Arc<dyn NodeDriverProvider>>,
|
||||||
|
/// Pre-provisioned, unassigned sandboxes (the warm pool, per-replica, local).
|
||||||
pool: Mutex<Vec<SandboxHandle>>,
|
pool: Mutex<Vec<SandboxHandle>>,
|
||||||
/// Warm-pool target; the background warmer keeps `pool` at this size.
|
/// Warm-pool target; the background warmer keeps `pool` at this size.
|
||||||
warm_target: std::sync::atomic::AtomicUsize,
|
warm_target: std::sync::atomic::AtomicUsize,
|
||||||
@@ -45,11 +56,64 @@ impl SandboxManager {
|
|||||||
node_id: node_id.to_owned(),
|
node_id: node_id.to_owned(),
|
||||||
image: image.to_owned(),
|
image: image.to_owned(),
|
||||||
egress: false,
|
egress: false,
|
||||||
|
node_provider: None,
|
||||||
pool: Mutex::new(Vec::new()),
|
pool: Mutex::new(Vec::new()),
|
||||||
warm_target: std::sync::atomic::AtomicUsize::new(0),
|
warm_target: std::sync::atomic::AtomicUsize::new(0),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Wire a fleet-node driver provider so agents placed on a connected node
|
||||||
|
/// run their sandbox there. Default (unset) = local-only, unchanged.
|
||||||
|
pub fn with_node_provider(mut self, provider: Arc<dyn NodeDriverProvider>) -> SandboxManager {
|
||||||
|
self.node_provider = Some(provider);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The driver that owns containers on `node_id`: the local driver for
|
||||||
|
/// "local" (or when the node isn't connected), else the node's remote driver.
|
||||||
|
fn driver_for(&self, node_id: &str) -> Arc<dyn SandboxDriver> {
|
||||||
|
if node_id != self.node_id {
|
||||||
|
if let Some(d) = self.node_provider.as_ref().and_then(|p| p.driver(node_id)) {
|
||||||
|
return d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.driver.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Where a NEW sandbox for this agent should run: the agent's workspace
|
||||||
|
/// placement setting if it points at a currently-connected node, else
|
||||||
|
/// "local" (the safe default — existing agents are unaffected).
|
||||||
|
async fn placement_node(&self, agent_id: AgentId) -> String {
|
||||||
|
match cm_db::repo::workspace_placement::for_agent(&self.db, agent_id).await {
|
||||||
|
Ok(Some(node)) if node != self.node_id => {
|
||||||
|
if self.node_provider.as_ref().and_then(|p| p.driver(&node)).is_some() {
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
self.node_id.clone()
|
||||||
|
}
|
||||||
|
_ => self.node_id.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The hardened spec for this manager's sandbox flavour.
|
||||||
|
fn spec(&self) -> SandboxSpec {
|
||||||
|
let short = uuid::Uuid::now_v7().simple().to_string();
|
||||||
|
SandboxSpec {
|
||||||
|
name: format!("tc-agent-{}", &short[short.len() - 12..]),
|
||||||
|
image: self.image.clone(),
|
||||||
|
memory_bytes: 512 * 1024 * 1024,
|
||||||
|
nano_cpus: 1_000_000_000,
|
||||||
|
pids_limit: 256,
|
||||||
|
egress: self.egress,
|
||||||
|
kind: if self.egress {
|
||||||
|
cm_sandbox::SandboxKind::Browser
|
||||||
|
} else {
|
||||||
|
cm_sandbox::SandboxKind::Agent
|
||||||
|
},
|
||||||
|
mounts: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// The `agent_containers.kind` discriminator for this manager.
|
/// The `agent_containers.kind` discriminator for this manager.
|
||||||
fn kind(&self) -> &'static str {
|
fn kind(&self) -> &'static str {
|
||||||
cm_sandbox::sandbox_kind(self.egress)
|
cm_sandbox::sandbox_kind(self.egress)
|
||||||
@@ -88,23 +152,8 @@ impl SandboxManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn provision_one(&self) -> Result<SandboxHandle, String> {
|
async fn provision_one(&self) -> Result<SandboxHandle, String> {
|
||||||
let short = uuid::Uuid::now_v7().simple().to_string();
|
|
||||||
let spec = SandboxSpec {
|
|
||||||
name: format!("tc-agent-{}", &short[short.len() - 12..]),
|
|
||||||
image: self.image.clone(),
|
|
||||||
memory_bytes: 512 * 1024 * 1024,
|
|
||||||
nano_cpus: 1_000_000_000,
|
|
||||||
pids_limit: 256,
|
|
||||||
egress: self.egress,
|
|
||||||
kind: if self.egress {
|
|
||||||
cm_sandbox::SandboxKind::Browser
|
|
||||||
} else {
|
|
||||||
cm_sandbox::SandboxKind::Agent
|
|
||||||
},
|
|
||||||
mounts: Vec::new(),
|
|
||||||
};
|
|
||||||
self.driver
|
self.driver
|
||||||
.provision(&spec)
|
.provision(&self.spec())
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("sandbox provision failed: {e}"))
|
.map_err(|e| format!("sandbox provision failed: {e}"))
|
||||||
}
|
}
|
||||||
@@ -121,25 +170,28 @@ impl SandboxManager {
|
|||||||
/// agent-authored code here is the point of the architecture.
|
/// agent-authored code here is the point of the architecture.
|
||||||
pub async fn exec(&self, agent_id: AgentId, command: &str) -> Result<ExecResult, String> {
|
pub async fn exec(&self, agent_id: AgentId, command: &str) -> Result<ExecResult, String> {
|
||||||
let kind = self.kind();
|
let kind = self.kind();
|
||||||
// Reuse the registry-recorded sandbox for this agent if it's alive — so a
|
// Reuse the registry-recorded sandbox if it's alive — on whatever node it
|
||||||
// different replica finds the same container instead of making another.
|
// runs (a different replica finds the same container instead of remaking).
|
||||||
if let Ok(Some(row)) = cm_db::repo::agent_containers::get(&self.db, agent_id, kind).await {
|
if let Ok(Some(row)) = cm_db::repo::agent_containers::get(&self.db, agent_id, kind).await {
|
||||||
|
let driver = self.driver_for(&row.node_id);
|
||||||
let handle = SandboxHandle {
|
let handle = SandboxHandle {
|
||||||
id: row.container_id,
|
id: row.container_id,
|
||||||
name: row.name,
|
name: row.name,
|
||||||
};
|
};
|
||||||
if self.driver.health(&handle).await.unwrap_or(false) {
|
if driver.health(&handle).await.unwrap_or(false) {
|
||||||
return self
|
return driver
|
||||||
.driver
|
|
||||||
.exec(&handle, &["sh", "-lc", command])
|
.exec(&handle, &["sh", "-lc", command])
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("sandbox exec failed: {e}"));
|
.map_err(|e| format!("sandbox exec failed: {e}"));
|
||||||
}
|
}
|
||||||
// Recorded but dead: clean both the container and the stale row.
|
// Recorded but dead: clean both the container and the stale row.
|
||||||
let _ = self.driver.destroy(&handle).await;
|
let _ = driver.destroy(&handle).await;
|
||||||
let _ = cm_db::repo::agent_containers::delete(&self.db, agent_id, kind).await;
|
let _ = cm_db::repo::agent_containers::delete(&self.db, agent_id, kind).await;
|
||||||
}
|
}
|
||||||
// Take a warm sandbox if one is ready (and still healthy), else provision.
|
// Provision a new sandbox on the agent's placement node ("local" default).
|
||||||
|
let node = self.placement_node(agent_id).await;
|
||||||
|
let (handle, node_id) = if node == self.node_id {
|
||||||
|
// Local: take a warm sandbox if ready (and still healthy), else provision.
|
||||||
let mut assigned = None;
|
let mut assigned = None;
|
||||||
while let Some(candidate) = self.pool.lock().await.pop() {
|
while let Some(candidate) = self.pool.lock().await.pop() {
|
||||||
if self.driver.health(&candidate).await.unwrap_or(false) {
|
if self.driver.health(&candidate).await.unwrap_or(false) {
|
||||||
@@ -152,17 +204,27 @@ impl SandboxManager {
|
|||||||
Some(handle) => handle,
|
Some(handle) => handle,
|
||||||
None => self.provision_one().await?,
|
None => self.provision_one().await?,
|
||||||
};
|
};
|
||||||
|
(handle, self.node_id.clone())
|
||||||
|
} else {
|
||||||
|
// Remote fleet node: provision directly via its driver (no warm pool).
|
||||||
|
let handle = self
|
||||||
|
.driver_for(&node)
|
||||||
|
.provision(&self.spec())
|
||||||
|
.await
|
||||||
|
.map_err(|e| format!("remote sandbox provision failed: {e}"))?;
|
||||||
|
(handle, node)
|
||||||
|
};
|
||||||
cm_db::repo::agent_containers::upsert(
|
cm_db::repo::agent_containers::upsert(
|
||||||
&self.db,
|
&self.db,
|
||||||
agent_id,
|
agent_id,
|
||||||
kind,
|
kind,
|
||||||
&self.node_id,
|
&node_id,
|
||||||
&handle.id,
|
&handle.id,
|
||||||
&handle.name,
|
&handle.name,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("registry upsert failed: {e}"))?;
|
.map_err(|e| format!("registry upsert failed: {e}"))?;
|
||||||
self.driver
|
self.driver_for(&node_id)
|
||||||
.exec(&handle, &["sh", "-lc", command])
|
.exec(&handle, &["sh", "-lc", command])
|
||||||
.await
|
.await
|
||||||
.map_err(|e| format!("sandbox exec failed: {e}"))
|
.map_err(|e| format!("sandbox exec failed: {e}"))
|
||||||
@@ -173,11 +235,12 @@ impl SandboxManager {
|
|||||||
pub async fn release_agent(&self, agent_id: AgentId) -> bool {
|
pub async fn release_agent(&self, agent_id: AgentId) -> bool {
|
||||||
match cm_db::repo::agent_containers::get(&self.db, agent_id, self.kind()).await {
|
match cm_db::repo::agent_containers::get(&self.db, agent_id, self.kind()).await {
|
||||||
Ok(Some(row)) => {
|
Ok(Some(row)) => {
|
||||||
|
let driver = self.driver_for(&row.node_id);
|
||||||
let handle = SandboxHandle {
|
let handle = SandboxHandle {
|
||||||
id: row.container_id,
|
id: row.container_id,
|
||||||
name: row.name,
|
name: row.name,
|
||||||
};
|
};
|
||||||
if let Err(e) = self.driver.destroy(&handle).await {
|
if let Err(e) = driver.destroy(&handle).await {
|
||||||
eprintln!("sandbox release: failed to remove {}: {e}", handle.id);
|
eprintln!("sandbox release: failed to remove {}: {e}", handle.id);
|
||||||
}
|
}
|
||||||
let _ = cm_db::repo::agent_containers::delete(&self.db, agent_id, self.kind()).await;
|
let _ = cm_db::repo::agent_containers::delete(&self.db, agent_id, self.kind()).await;
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
/// Hardening parameters for one agent sandbox. The non-negotiable controls
|
/// Hardening parameters for one agent sandbox. The non-negotiable controls
|
||||||
/// (uid 10001, cap-drop ALL, no-new-privileges, seccomp profile, read-only
|
/// (uid 10001, cap-drop ALL, no-new-privileges, seccomp profile, read-only
|
||||||
/// rootfs, no network) are enforced by the driver and are not configurable
|
/// rootfs, no network) are enforced by the driver and are not configurable
|
||||||
/// here by design — only resource limits vary per deployment.
|
/// here by design — only resource limits vary per deployment.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct SandboxSpec {
|
pub struct SandboxSpec {
|
||||||
/// Unique container name, e.g. `clawmates-sbx-{agent_id}`.
|
/// Unique container name, e.g. `clawmates-sbx-{agent_id}`.
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@@ -24,7 +26,7 @@ pub struct SandboxSpec {
|
|||||||
|
|
||||||
/// A read-write mount of a per-agent subpath of a named Docker volume into the
|
/// A read-write mount of a per-agent subpath of a named Docker volume into the
|
||||||
/// container — used to expose the Files drives inside the Terminal.
|
/// container — used to expose the Files drives inside the Terminal.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct DriveMount {
|
pub struct DriveMount {
|
||||||
/// The engine's named volume (e.g. `clawmates_filedata`).
|
/// The engine's named volume (e.g. `clawmates_filedata`).
|
||||||
pub volume: String,
|
pub volume: String,
|
||||||
@@ -37,7 +39,7 @@ pub struct DriveMount {
|
|||||||
|
|
||||||
/// The flavour of a sandbox container. Agent + Browser are hardened tool
|
/// The flavour of a sandbox container. Agent + Browser are hardened tool
|
||||||
/// sandboxes; Terminal is the interactive themed dev shell.
|
/// sandboxes; Terminal is the interactive themed dev shell.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub enum SandboxKind {
|
pub enum SandboxKind {
|
||||||
/// Hardened, no-egress agent tool sandbox (read-only rootfs, tmpfs home).
|
/// Hardened, no-egress agent tool sandbox (read-only rootfs, tmpfs home).
|
||||||
Agent,
|
Agent,
|
||||||
@@ -84,14 +86,14 @@ pub struct PtySession {
|
|||||||
pub input: std::pin::Pin<Box<dyn tokio::io::AsyncWrite + Send>>,
|
pub input: std::pin::Pin<Box<dyn tokio::io::AsyncWrite + Send>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct SandboxHandle {
|
pub struct SandboxHandle {
|
||||||
/// Container id assigned by the engine.
|
/// Container id assigned by the engine.
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ExecResult {
|
pub struct ExecResult {
|
||||||
pub exit_code: i64,
|
pub exit_code: i64,
|
||||||
pub stdout: String,
|
pub stdout: String,
|
||||||
@@ -100,7 +102,7 @@ pub struct ExecResult {
|
|||||||
|
|
||||||
/// A sandbox the driver currently knows about (label-filtered), used by the
|
/// A sandbox the driver currently knows about (label-filtered), used by the
|
||||||
/// reaper to find orphans — containers that outlived the process that made them.
|
/// reaper to find orphans — containers that outlived the process that made them.
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ManagedSandbox {
|
pub struct ManagedSandbox {
|
||||||
/// Engine container/pod id.
|
/// Engine container/pod id.
|
||||||
pub id: String,
|
pub id: String,
|
||||||
|
|||||||
@@ -188,6 +188,39 @@ export function LocalHardware() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Choose where new agent sandboxes provision (a connected node, or local). */
|
||||||
|
function PlacementSection() {
|
||||||
|
const { nodes } = useNodes();
|
||||||
|
const { data, refresh } = useFetchJson<{ node: string | null }>("/api/fleet/placement");
|
||||||
|
const current = data?.node ?? "local";
|
||||||
|
const online = nodes.filter((n) => n.status === "online");
|
||||||
|
const set = useCallback(
|
||||||
|
(node: string) => {
|
||||||
|
fetch("/api/fleet/placement", { method: "PUT", headers: { "content-type": "application/json" }, body: JSON.stringify({ node }) }).then(refresh);
|
||||||
|
},
|
||||||
|
[refresh],
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<section style={{ borderRadius: 16, background: "#0f0f13", border: "1px solid rgba(255,255,255,.08)", padding: 18 }}>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: 9, marginBottom: 12 }}>
|
||||||
|
<span style={{ width: 30, height: 30, borderRadius: 8, background: "rgba(255,111,97,.1)", border: "1px solid rgba(255,111,97,.25)", display: "flex", alignItems: "center", justifyContent: "center", color: "#ff8a7a" }}><Cpu size={15} /></span>
|
||||||
|
<span style={{ fontSize: 15, fontWeight: 700, color: "#f3f3f5", flex: 1 }}>Run agents on</span>
|
||||||
|
</div>
|
||||||
|
<select
|
||||||
|
value={current}
|
||||||
|
onChange={(e) => set(e.target.value)}
|
||||||
|
style={{ width: "100%", padding: "10px 12px", borderRadius: 10, border: "1px solid rgba(255,255,255,.14)", background: "#08080a", color: "#f3f3f5", fontSize: 13.5, cursor: "pointer" }}
|
||||||
|
>
|
||||||
|
<option value="local">Local — the gateway host (default)</option>
|
||||||
|
{online.map((n) => (
|
||||||
|
<option key={n.id} value={n.id}>{n.name}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<p style={{ fontSize: 12, color: "#7a7a82", marginTop: 10, marginBottom: 0, lineHeight: 1.5 }}>New agent sandboxes provision on this host. Falls back to local automatically if the node goes offline. Existing agents are unaffected until their sandbox is next created.</p>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
interface TsDevice {
|
interface TsDevice {
|
||||||
name: string | null;
|
name: string | null;
|
||||||
addr: string | null;
|
addr: string | null;
|
||||||
@@ -291,6 +324,10 @@ export function FleetOverview() {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div style={{ marginBottom: 22 }}>
|
||||||
|
<PlacementSection />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div style={{ display: "flex", flexWrap: "wrap", gap: 10 }}>
|
<div style={{ display: "flex", flexWrap: "wrap", gap: 10 }}>
|
||||||
{nodes.map((n) => (
|
{nodes.map((n) => (
|
||||||
<div key={n.id} style={{ display: "inline-flex", alignItems: "center", gap: 8, padding: "9px 13px", borderRadius: 999, background: "#101014", border: "1px solid rgba(255,255,255,.08)" }}>
|
<div key={n.id} style={{ display: "inline-flex", alignItems: "center", gap: 8, padding: "9px 13px", borderRadius: 999, background: "#101014", border: "1px solid rgba(255,255,255,.08)" }}>
|
||||||
|
|||||||
@@ -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=
|
||||||
@@ -0,0 +1,280 @@
|
|||||||
|
# Clawmates Frontend — Implementation & Wiring Handoff
|
||||||
|
|
||||||
|
> **Audience:** the build agent (or engineer) integrating these screens into the Clawmates
|
||||||
|
> platform (the Rust workspace + Next.js app) and feeding them live from the durable runner.
|
||||||
|
>
|
||||||
|
> **What this package is:** every Clawmates screen as a self-contained HTML file, plus a
|
||||||
|
> dependency-free real-time bridge and client adapter that make them update live. Read this
|
||||||
|
> file once, top to bottom, before wiring anything. Everything runs **offline in demo mode**
|
||||||
|
> with synthetic data, so you can see each screen breathe before touching the backend.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. Package layout
|
||||||
|
|
||||||
|
```
|
||||||
|
handoff/
|
||||||
|
├── IMPLEMENTATION.md ← this file
|
||||||
|
├── README.md ← 60-second start
|
||||||
|
├── install.sh ← idempotent installer (Node check, static server, .env)
|
||||||
|
├── .env.example ← copy → .env for live mode
|
||||||
|
├── screens/ ← the UI. Open any .dc.html directly in a browser — no build step.
|
||||||
|
│ ├── Clawmates World.dc.html
|
||||||
|
│ ├── Clawmates Observe.dc.html
|
||||||
|
│ ├── Clawmates Dashboard.dc.html
|
||||||
|
│ ├── Clawmates Infrastructure.dc.html
|
||||||
|
│ ├── Clawmates Auth.dc.html
|
||||||
|
│ ├── Clawmates Landing.dc.html
|
||||||
|
│ └── support.js ← the .dc runtime (MUST sit next to the .dc.html files)
|
||||||
|
└── realtime/
|
||||||
|
├── server.mjs ← demo + live SSE bridge (Node ≥20, zero deps)
|
||||||
|
├── clawmates-live.js ← browser client adapter → window.ClawmatesLive
|
||||||
|
└── events.schema.json ← the Clawmates Event Taxonomy (the contract)
|
||||||
|
```
|
||||||
|
|
||||||
|
### What a `.dc.html` file is
|
||||||
|
|
||||||
|
Each screen is a **Design Component**: a single HTML file whose markup + a small logic class are
|
||||||
|
rendered by the bundled `support.js` runtime. There is **no build step and no npm install to view
|
||||||
|
them** — open the file in a browser, or serve the `screens/` folder statically. `support.js` must
|
||||||
|
sit beside the `.dc.html` files (it does, in `screens/`).
|
||||||
|
|
||||||
|
To edit a screen's behavior you change two regions inside the file: the `<x-dc>…</x-dc>` template
|
||||||
|
(markup, inline-styled) and the `class Component extends DCLogic { … }` block (state + a
|
||||||
|
`renderVals()` that returns the values the template binds to). All live data flows into a screen by
|
||||||
|
calling `this.setState(...)` from a `ClawmatesLive.on(...)` handler — see §3.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. The screens
|
||||||
|
|
||||||
|
| Screen | Purpose | Primary live inputs (see §3 taxonomy) |
|
||||||
|
|---|---|---|
|
||||||
|
| **World** | The "Large World" map in three formations: **Hierarchy** (org▸company▸team▸claw tree), **Flat** (peer mesh), **Live** (Gource-style convergence on a `<canvas>`). | `topology.update`, `world.touch`, `node.activity`, `agent.status` |
|
||||||
|
| **Observe** | Linked observation deck — **Agent** close-up (live reasoning + the agent's live computer screen) and **System** mission-control (both claws, comms, routines, telemetry). | `agent.task.update`, `agent.reasoning.delta`, `agent.computer.frame`, `agent.message`, `telemetry`, `routine.update`, `door.request` |
|
||||||
|
| **Dashboard** | Tiered admin: org→company→team→claw with topology morph + the per-claw "anatomy" (brain compartments) and the agent's computer. | `agent.status`, `topology.update`, `routine.update` |
|
||||||
|
| **Infrastructure** | Fleet management. **Local & Tailscale first** (host health cards, tailnet device list, 3-step Connect-a-host wizard), then **Cloud** (AWS/GCP/Apple silicon/Hetzner/DigitalOcean connect + provision). | `host.health`, `host.status`, `tailscale.devices`, `cloud.runner` (see §5) |
|
||||||
|
| **Auth** | Clerk-style sign-in / sign-up with Google / Apple / GitHub OAuth + email. | n/a (wire to Clerk — see §6) |
|
||||||
|
| **Landing** | Marketing page (deploy ladder, 12 topologies, §15 safety, self-host). | static |
|
||||||
|
|
||||||
|
> **Canonical entry point for the product app:** World, Observe, Dashboard, and Infrastructure are
|
||||||
|
> the in-product surfaces. Auth gates them; Landing is public.
|
||||||
|
|
||||||
|
### 1a. Per-screen integration seams (where synthetic becomes live)
|
||||||
|
|
||||||
|
Each screen ships with a built-in synthetic animation so it looks alive offline. To make it *real*,
|
||||||
|
register handlers in the logic class' `componentDidMount()` and feed `setState`. The exact seams:
|
||||||
|
|
||||||
|
- **World → `_startLive()`** (the canvas engine). It holds `targets[]` (project/service/event nodes)
|
||||||
|
and `agents[]` whose `.target` is currently chosen on a random timer. Replace the random retarget
|
||||||
|
with **`world.touch`**: on each event, set the matching agent's `.target` to the index of `nodeId`
|
||||||
|
and force a beam; the node-heat/glow already keys off "hot". Rebuild `targets[]` from
|
||||||
|
`topology.update`. Hierarchy/Flat node lists also come from `topology.update`.
|
||||||
|
- **Observe → Agent mode.** Bind the WORKING-ON-NOW card to `agent.task.update` (replace), the
|
||||||
|
REASONING STREAM to `agent.reasoning.delta` (append), the live computer screen to
|
||||||
|
`agent.computer.frame` (replace). The door toast binds to `door.request` / `door.resolve`.
|
||||||
|
- **Observe → System mode & Dashboard.** Telemetry pills ← `telemetry`; comms list ← `agent.message`;
|
||||||
|
routines/loops ← `routine.update`; status dots ← `agent.status`.
|
||||||
|
- **Infrastructure.** Host health cards ← `host.health` + `host.status`; the Tailscale device list ←
|
||||||
|
`tailscale.devices`; cloud runners ← `cloud.runner`. The Connect-a-host and Connect-a-provider
|
||||||
|
wizards are driven by the daemon-pairing and provisioning APIs in §5.
|
||||||
|
|
||||||
|
Every seam is a few lines: `ClawmatesLive.on('world.touch', e => engine.touch(e.agentId, e.nodeId))`.
|
||||||
|
No markup changes — you are feeding values into components that already exist.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Run it (60 seconds, no backend)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x install.sh && ./install.sh
|
||||||
|
# Terminal 1 — synthetic event bridge
|
||||||
|
(cd realtime && CLAWMATES_MODE=demo node server.mjs)
|
||||||
|
# Terminal 2 — serve the screens
|
||||||
|
caddy file-server --root ./screens --listen :8080 # or: npx serve ./screens -l 8080
|
||||||
|
```
|
||||||
|
|
||||||
|
Open **http://localhost:8080/Clawmates%20World.dc.html?live=http://localhost:8420/live**, switch to
|
||||||
|
the **Live** formation, and the agents converge on events streamed from the bridge. Without `?live=`,
|
||||||
|
each screen runs its built-in synthetic animation (safe fallback). The `?live=` param (or
|
||||||
|
`window.CLAWMATES_LIVE_URL`) is read by `clawmates-live.js`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Real-time architecture & the Event Taxonomy
|
||||||
|
|
||||||
|
The runner emits many internal events; the **bridge** (`realtime/server.mjs`) normalizes them into a
|
||||||
|
small, stable taxonomy and fans them out as one SSE feed. The browser only ever sees these. Full JSON
|
||||||
|
Schema: `realtime/events.schema.json`. Human summary:
|
||||||
|
|
||||||
|
**Agent work** — `agent.status` `{agentId,status,role}` · `agent.task.update`
|
||||||
|
`{agentId,taskId,title,elapsedMs,steps[]}` · `agent.reasoning.delta` `{agentId,text}` (append) ·
|
||||||
|
`agent.computer.frame` `{agentId,app,url?,lines[]}`.
|
||||||
|
|
||||||
|
**Tools, doors & §15** — `agent.tool.call` `{agentId,tool,target,doorRequired}` · `door.request`
|
||||||
|
`{doorId,agentId,action,target,summary}` · `door.resolve` `{doorId,decision,by}`.
|
||||||
|
|
||||||
|
**Collaboration & world** — `agent.message` `{fromAgentId,toAgentId,text,ts}` · `world.touch`
|
||||||
|
`{agentId,nodeId,kind}` (the Gource retarget) · `node.activity` `{nodeId,label,kind,heat}` ·
|
||||||
|
`topology.update` `{formation,nodes[],edges[]}`.
|
||||||
|
|
||||||
|
**System** — `telemetry` `{tokensPerMin,costPerHr,loops,doorsPending}` · `routine.update`
|
||||||
|
`{routineId,name,kind,owner,schedule?,progress?,state}`.
|
||||||
|
|
||||||
|
**Infrastructure (new — see §5)** — `host.status` `{hostId,status}` · `host.health`
|
||||||
|
`{hostId,cpu,ram,memPressure,disk,load,containers}` · `tailscale.devices` `{tailnet,devices[]}` ·
|
||||||
|
`cloud.runner` `{provider,id,region,size,status,cpu,ram,agents,costPerHr}`.
|
||||||
|
|
||||||
|
### The integration seam in the bridge
|
||||||
|
|
||||||
|
`normalize(evt, payload)` in `server.mjs` translates **one raw runner event → zero+ taxonomy events**.
|
||||||
|
The upstream event names there (`turn.started`, `tool.invoked`, `runner.telemetry`, …) are
|
||||||
|
placeholders — **reconcile them with the actual cm-api SSE shapes.** Start by mapping just
|
||||||
|
`world.touch`, `agent.status`, and `telemetry`; that alone brings World and the top bars alive. Add
|
||||||
|
the rest incrementally. Unknown events are ignored, so the bridge is forward-compatible.
|
||||||
|
|
||||||
|
### The client adapter (`clawmates-live.js`)
|
||||||
|
|
||||||
|
Include it in a screen (or inject via the Next proxy). It opens an `EventSource`, dispatches each
|
||||||
|
event to `ClawmatesLive.on(type, fn)` handlers **and** a `window` `CustomEvent('clawmates:<type>')`,
|
||||||
|
and **replays the last value per stateful key** so a late-loading screen paints current state
|
||||||
|
immediately (mirrors the runner's checkpoint-resume). If no live URL is set, it is inert.
|
||||||
|
|
||||||
|
> **Security invariant — carry it through.** The bridge is **read-only** on the runner stream. It
|
||||||
|
> must never hold or forward secret-broker credentials, and it must expose **no** door-approval
|
||||||
|
> endpoint to the browser. Door *approvals* stay on the authenticated cm-api path; the bridge only
|
||||||
|
> *renders* that a door is pending so the UI can badge it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Wiring to cm-api [INTEGRATES WITH cm-api]
|
||||||
|
|
||||||
|
Live mode points the bridge at the runner's SSE gateway and exchanges a session for a bearer token,
|
||||||
|
exactly like the Next.js `/api` proxy. `.env`:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
CLAWMATES_MODE=live
|
||||||
|
CM_API_SSE_URL=http://127.0.0.1:8080/v1/runs/stream # cm-api streaming gateway
|
||||||
|
CM_API_TOKEN=sk-... # read-only, scoped to run events
|
||||||
|
LIVE_PORT=8420
|
||||||
|
LIVE_ALLOWED_ORIGINS=http://localhost:8080 # the screens' origin (CORS)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Deployment shapes:**
|
||||||
|
- **A) Standalone:** serve `screens/` with Caddy; run the bridge as its own systemd service beside the
|
||||||
|
product. Good for an ops/observability surface.
|
||||||
|
- **B) In-app (tightest):** mount each screen in the Next.js app via `<iframe src="/viz/world?live=/api/live">`
|
||||||
|
and proxy `/api/live` to the bridge so it inherits the app's session→bearer swap and CORS.
|
||||||
|
|
||||||
|
systemd unit for the bridge (read-only, hardened):
|
||||||
|
|
||||||
|
```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
|
||||||
|
NoNewPrivileges=true
|
||||||
|
ProtectSystem=strict
|
||||||
|
ProtectHome=true
|
||||||
|
PrivateTmp=true
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Infrastructure wiring (Local & Tailscale first, then Cloud)
|
||||||
|
|
||||||
|
The Infrastructure screen is the most backend-coupled. It maps directly onto the daemon + tailnet +
|
||||||
|
cloud-provisioning model.
|
||||||
|
|
||||||
|
### 5a. Local hosts — the `clawmates-node` daemon
|
||||||
|
|
||||||
|
The **Connect a host** wizard (Install → Connect → Verify) is the UI for the daemon-pairing flow:
|
||||||
|
|
||||||
|
1. **Install.** cm-api mints a **one-time token** and returns the install command shown in the wizard:
|
||||||
|
`clawmates-node --server https://clawmates.work --token <token>` (Docker should be present on the
|
||||||
|
node so it can run agents). The `curl … /install.sh | bash` one-liner needs the binary hosted; for
|
||||||
|
now `cargo build --release -p clawmates-node` once, then run the line.
|
||||||
|
2. **Connect.** The daemon dials home over an **outbound WSS** (NAT-friendly, no inbound port). When
|
||||||
|
cm-api sees the socket, emit `host.status {hostId,status:"connected"}` + the host specs; the wizard
|
||||||
|
flips to *Connected*.
|
||||||
|
3. **Verify.** cm-api runs the built-in check (`uname` + `docker version`) on the node; stream the
|
||||||
|
result so the wizard shows the verification and the host is added to the fleet.
|
||||||
|
|
||||||
|
Thereafter stream `host.health` (CPU / RAM / mem pressure / disk / load / containers) on an interval
|
||||||
|
→ the live host cards. If the host is on Tailscale, include its `100.x` IP so the card can show the
|
||||||
|
copyable `ssh <tailscale-ip>` target.
|
||||||
|
|
||||||
|
### 5b. Tailscale network — fleet metrics
|
||||||
|
|
||||||
|
The **Tailscale network** panel maps to: *Infra → Fleet → Tailscale network → Connect*. The user
|
||||||
|
pastes their tailnet (e.g. `your-org.ts.net`) + a Tailscale API key; cm-api polls the Tailscale API
|
||||||
|
and emits `tailscale.devices {tailnet, devices:[{name,ip,os,status,seen}]}` → the live device list
|
||||||
|
(online / last-seen / IP / OS). Note the daemon can also join the tailnet for the operator
|
||||||
|
(`tailscale up --ssh`, or pass `--tailscale-authkey <key>` to have the daemon run it) — `--ssh` is
|
||||||
|
what gives keyless, ACL-gated SSH (ensure the tailnet ACL permits SSH via an `ssh` rule / tag).
|
||||||
|
|
||||||
|
### 5c. Cloud providers — provision runners
|
||||||
|
|
||||||
|
The **Cloud** screen connects AWS / GCP / Apple silicon / Hetzner / DigitalOcean. The connect wizard
|
||||||
|
maps to:
|
||||||
|
1. **Credentials** → stored in the **secret broker** over its private socket; they never enter agent
|
||||||
|
code or any sandbox. (Same invariant as §3.)
|
||||||
|
2. **Region + instance size** → the provision request.
|
||||||
|
3. **Provision** → cm-api launches a VM from an image with `clawmates-node` baked in; on boot it dials
|
||||||
|
home over WSS (no inbound port) and the §15 sandbox profile is applied. Emit `cloud.runner` updates
|
||||||
|
→ the live cloud-runner cards (CPU/RAM, agent count, hourly cost). Deprovision terminates the VM.
|
||||||
|
|
||||||
|
**Ordering:** ship Local & Tailscale first (own hardware, zero inbound exposure); Cloud is the next
|
||||||
|
rung and reuses the same daemon + WSS + §15 model — it's the same fleet, just provisioned for you.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Auth (Clerk)
|
||||||
|
|
||||||
|
The Auth screen is a themed Clerk surface (Google / Apple / GitHub OAuth + email, sign-in ↔ sign-up
|
||||||
|
toggle). To make it real, replace the static markup with Clerk's `<SignIn>` / `<SignUp>` components
|
||||||
|
and pass an `appearance` theme that reuses the screen's tokens (near-black `#08080a`, coral
|
||||||
|
`#ff6f61`, the rounded inputs and primary button). cm-api auth is `local` by default or `clerk` at
|
||||||
|
runtime — match whichever the deployment uses. The bridge/screens never see auth secrets.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Verification checklist
|
||||||
|
|
||||||
|
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.
|
||||||
|
3. World `?live=…` → **Live** shows particles retargeting **in sync** with the `world.touch` lines in
|
||||||
|
the curl output (not the smooth random demo motion).
|
||||||
|
4. Kill the bridge mid-stream → the screen shows reconnect, then resumes (EventSource auto-reconnect +
|
||||||
|
adapter replay).
|
||||||
|
5. **[live]** Point at cm-api, trigger a run, and confirm `telemetry` numbers in the top bar move and
|
||||||
|
a real `door.request` raises the toast in Observe.
|
||||||
|
6. **[infra]** Run the daemon install line on a test node → `host.status` connected → health card
|
||||||
|
appears → `ssh <tailscale-ip>` target resolves.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Build order (recommended)
|
||||||
|
|
||||||
|
1. Bridge `normalize()` → reconcile upstream event names with cm-api; map `world.touch`, `agent.status`,
|
||||||
|
`telemetry` first.
|
||||||
|
2. World Live engine ← `world.touch` + `topology.update`. (Biggest payoff, smallest change.)
|
||||||
|
3. Observe Agent ← `agent.task.update` / `agent.reasoning.delta` / `agent.computer.frame`; door toast.
|
||||||
|
4. Infrastructure ← daemon pairing (§5a), tailnet metrics (§5b), then cloud provisioning (§5c).
|
||||||
|
5. Auth ← Clerk components with the theme tokens.
|
||||||
|
6. Promote the bridge from read-only mirror to a typed gateway with backpressure + per-tenant scoping;
|
||||||
|
add a time-scrubber to replay the runner's checkpoint log (Gource-style playback).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*The `.dc.html` files in `screens/` 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. Keep `support.js`
|
||||||
|
next to the screens.*
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
# Clawmates Frontend — handoff package
|
||||||
|
|
||||||
|
Every Clawmates screen as a self-contained HTML file, plus a dependency-free real-time bridge that
|
||||||
|
makes them update live from the durable runner. No build step to view the screens.
|
||||||
|
|
||||||
|
```
|
||||||
|
handoff/
|
||||||
|
├── IMPLEMENTATION.md ← READ THIS FIRST. Per-screen guide + full wiring to cm-api.
|
||||||
|
├── install.sh ← one-shot installer (idempotent)
|
||||||
|
├── .env.example ← copy → .env, edit for live mode
|
||||||
|
├── screens/ ← the UI (open directly in a browser, no build step)
|
||||||
|
│ ├── Clawmates World.dc.html · Large World: Hierarchy · Flat · Live (Gource)
|
||||||
|
│ ├── Clawmates Observe.dc.html · Agent close-up + System mission-control
|
||||||
|
│ ├── Clawmates Dashboard.dc.html · Tiered admin: org→company→team→claw + topology morph
|
||||||
|
│ ├── Clawmates Infrastructure.dc.html · Local & Tailscale first, then Cloud providers
|
||||||
|
│ ├── Clawmates Auth.dc.html · Clerk-style sign-in / sign-up (Google · Apple · GitHub)
|
||||||
|
│ ├── Clawmates Landing.dc.html · Marketing page
|
||||||
|
│ └── support.js · the .dc.html runtime (keep 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 (the contract)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 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 ./screens --listen :8080 # or: npx serve ./screens -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 events from the bridge.
|
||||||
|
|
||||||
|
Then read **IMPLEMENTATION.md** for the per-screen integration seams, the event taxonomy, the
|
||||||
|
Infrastructure (Tailscale + daemon + cloud) wiring, and the cm-api hookup.
|
||||||
@@ -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 screens/"
|
||||||
|
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 ./screens --listen :8080 # or: npx serve ./screens -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 IMPLEMENTATION.md for per-screen wiring, the event taxonomy, Infrastructure (Tailscale/daemon/cloud), and the systemd unit."
|
||||||
|
echo
|
||||||
@@ -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());
|
||||||
|
})();
|
||||||
@@ -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"] }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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": {}
|
||||||
|
}
|
||||||
@@ -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,146 @@
|
|||||||
|
<!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; }
|
||||||
|
input { outline: none; font-family: inherit; }
|
||||||
|
input::placeholder { color: #5a5a62; }
|
||||||
|
@keyframes cm-flow { to { stroke-dashoffset: -24; } }
|
||||||
|
@keyframes cm-blink { 0%,100% { opacity: 1; } 50% { opacity: .3; } }
|
||||||
|
@keyframes cm-halo { 0% { transform: scale(.7); opacity: .5; } 100% { transform: scale(1.9); opacity: 0; } }
|
||||||
|
</style>
|
||||||
|
</helmet>
|
||||||
|
|
||||||
|
<div style="display:flex; width:100%; min-height:100vh; background:#08080a; color:#f3f3f5;">
|
||||||
|
|
||||||
|
<!-- LEFT BRAND PANEL -->
|
||||||
|
<div style="flex:1; position:relative; border-right:1px solid rgba(255,255,255,.06); background:radial-gradient(120% 90% at 35% 30%, #0f0f14, #08080a 72%); padding:40px; display:flex; flex-direction:column; overflow:hidden;">
|
||||||
|
<div style="display:flex; align-items:center; gap:10px; position:relative; z-index:2;">
|
||||||
|
<svg width="24" height="24" 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:17px; font-weight:700; letter-spacing:-.01em;">Clawmates</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="flex:1; position:relative; min-height:280px;">
|
||||||
|
<svg viewBox="0 0 100 100" preserveAspectRatio="none" style="position:absolute; inset:0; width:100%; height:100%;">
|
||||||
|
<path d="M50,52 C40,38 34,32 24,26" fill="none" stroke="rgba(94,200,216,.45)" stroke-width="1.3" stroke-dasharray="3 4" vector-effect="non-scaling-stroke" style="animation:cm-flow 1.2s linear infinite;"></path>
|
||||||
|
<path d="M50,52 C40,64 34,70 26,76" fill="none" stroke="rgba(94,200,216,.45)" stroke-width="1.3" stroke-dasharray="3 4" vector-effect="non-scaling-stroke" style="animation:cm-flow 1.5s linear infinite;"></path>
|
||||||
|
<path d="M50,52 C52,40 53,30 54,20" fill="none" stroke="rgba(255,255,255,.1)" stroke-width="1.2" vector-effect="non-scaling-stroke"></path>
|
||||||
|
<path d="M50,52 C64,46 72,40 78,32" fill="none" stroke="rgba(255,255,255,.1)" stroke-width="1.2" vector-effect="non-scaling-stroke"></path>
|
||||||
|
<path d="M50,52 C64,58 72,66 78,74" fill="none" stroke="rgba(255,111,97,.55)" stroke-width="1.5" stroke-dasharray="3 4" vector-effect="non-scaling-stroke" style="animation:cm-flow .9s linear infinite;"></path>
|
||||||
|
</svg>
|
||||||
|
<div style="position:absolute; left:50%; top:52%; transform:translate(-50%,-50%); width:52px; height:52px; border-radius:50%; background:linear-gradient(135deg,#ff9a6a,#ff6f4a); display:flex; align-items:center; justify-content:center; font-size:19px; font-weight:700; color:#2a0d05; box-shadow:0 0 36px rgba(255,111,97,.5);">A</div>
|
||||||
|
<div style="position:absolute; left:24%; top:26%; transform:translate(-50%,-50%); width:36px; height:36px; border-radius:50%;"><div style="position:absolute; inset:0; border-radius:50%; background:rgba(94,200,216,.4); animation:cm-halo 1.9s ease-out infinite;"></div><div style="position:relative; width:36px; height:36px; border-radius:50%; background:linear-gradient(135deg,#6fd0c0,#4aa3b8); display:flex; align-items:center; justify-content:center; font-size:13px; font-weight:700; color:#06201f;">I</div></div>
|
||||||
|
<div style="position:absolute; left:54%; top:20%; transform:translate(-50%,-50%); width:32px; height:32px; border-radius:50%; background:linear-gradient(135deg,#e8c46a,#d89a3a); display:flex; align-items:center; justify-content:center; font-size:12px; font-weight:700; color:#2a1d05;">N</div>
|
||||||
|
<div style="position:absolute; left:78%; top:32%; transform:translate(-50%,-50%); width:32px; height:32px; border-radius:50%; background:linear-gradient(135deg,#c98af0,#9a5ad8); display:flex; align-items:center; justify-content:center; font-size:12px; font-weight:700; color:#1a0a2a; opacity:.7;">S</div>
|
||||||
|
<div style="position:absolute; left:26%; top:76%; transform:translate(-50%,-50%); width:32px; height:32px; border-radius:50%; background:linear-gradient(135deg,#8a9af0,#5a6ad8); display:flex; align-items:center; justify-content:center; font-size:12px; font-weight:700; color:#0a0e2a;">E</div>
|
||||||
|
<div style="position:absolute; left:78%; top:74%; transform:translate(-50%,-50%); width:38px; height:38px; border-radius:50%; background:linear-gradient(135deg,#ff8a7a,#ff5f57); display:flex; align-items:center; justify-content:center; font-size:14px; font-weight:700; color:#2a0d0a; border:2px solid #ff6f61;">M</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="position:relative; z-index:2; max-width:380px;">
|
||||||
|
<div style="font-size:24px; font-weight:700; letter-spacing:-.02em; line-height:1.25; margin-bottom:10px; text-wrap:balance;">Deploy agents at any scale — a single claw to a whole org.</div>
|
||||||
|
<div style="font-family:'JetBrains Mono',monospace; font-size:11px; color:#6a6a72; letter-spacing:.04em;">12 topologies · durable runner · §15-safe</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- RIGHT FORM PANEL -->
|
||||||
|
<div style="flex:none; width:520px; max-width:48vw; display:flex; align-items:center; justify-content:center; padding:40px;">
|
||||||
|
<div style="width:100%; max-width:380px;">
|
||||||
|
|
||||||
|
<h1 style="font-size:28px; font-weight:700; letter-spacing:-.02em; margin:0 0 8px;">{{ title }}</h1>
|
||||||
|
<p style="font-size:15px; color:#a8a8b0; margin:0 0 28px;">{{ subtitle }}</p>
|
||||||
|
|
||||||
|
<!-- OAUTH -->
|
||||||
|
<div style="display:flex; flex-direction:column; gap:10px;">
|
||||||
|
<div style="display:flex; align-items:center; justify-content:center; gap:10px; height:46px; border-radius:10px; border:1px solid rgba(255,255,255,.12); background:#141417; font-size:14px; font-weight:600; color:#f3f3f5; cursor:pointer; transition:background .15s;" style-hover="background:#1a1a1f;">
|
||||||
|
<svg width="18" height="18" viewBox="0 0 18 18"><path fill="#4285F4" d="M17.64 9.2c0-.64-.06-1.25-.16-1.84H9v3.48h4.84a4.14 4.14 0 01-1.8 2.72v2.26h2.92c1.7-1.57 2.68-3.88 2.68-6.62z"></path><path fill="#34A853" d="M9 18c2.43 0 4.47-.8 5.96-2.18l-2.92-2.26c-.8.54-1.84.86-3.04.86-2.34 0-4.32-1.58-5.03-3.7H.96v2.33A9 9 0 009 18z"></path><path fill="#FBBC05" d="M3.97 10.72a5.4 5.4 0 010-3.44V4.95H.96a9 9 0 000 8.1l3.01-2.33z"></path><path fill="#EA4335" d="M9 3.58c1.32 0 2.5.45 3.44 1.35l2.58-2.58A9 9 0 00.96 4.95l3.01 2.33C4.68 5.16 6.66 3.58 9 3.58z"></path></svg>
|
||||||
|
Continue with Google
|
||||||
|
</div>
|
||||||
|
<div style="display:flex; align-items:center; justify-content:center; gap:10px; height:46px; border-radius:10px; border:1px solid rgba(255,255,255,.12); background:#141417; font-size:14px; font-weight:600; color:#f3f3f5; cursor:pointer; transition:background .15s;" style-hover="background:#1a1a1f;">
|
||||||
|
<svg width="17" height="17" viewBox="0 0 24 24" fill="#fff"><path d="M16.36 12.78c.02 2.5 2.2 3.33 2.23 3.34-.02.06-.35 1.2-1.16 2.37-.7 1.02-1.43 2.03-2.58 2.05-1.13.02-1.5-.67-2.78-.67-1.29 0-1.69.65-2.76.69-1.11.04-1.96-1.1-2.66-2.11-1.45-2.1-2.56-5.92-1.07-8.51.74-1.28 2.06-2.1 3.49-2.12 1.09-.02 2.12.73 2.78.73.67 0 1.92-.9 3.24-.77.55.02 2.1.22 3.1 1.68-.08.05-1.85 1.08-1.83 3.22zM14.2 5.18c.59-.71.99-1.7.88-2.68-.85.03-1.88.57-2.49 1.28-.55.63-1.03 1.63-.9 2.6.95.07 1.92-.48 2.51-1.2z"></path></svg>
|
||||||
|
Continue with Apple
|
||||||
|
</div>
|
||||||
|
<div style="display:flex; align-items:center; justify-content:center; gap:10px; height:46px; border-radius:10px; border:1px solid rgba(255,255,255,.12); background:#141417; font-size:14px; font-weight:600; color:#f3f3f5; cursor:pointer; transition:background .15s;" style-hover="background:#1a1a1f;">
|
||||||
|
<svg width="17" height="17" viewBox="0 0 24 24" fill="#fff"><path d="M12 .5C5.37.5 0 5.87 0 12.5c0 5.3 3.44 9.8 8.21 11.39.6.11.82-.26.82-.58v-2.03c-3.34.73-4.04-1.61-4.04-1.61-.55-1.39-1.34-1.76-1.34-1.76-1.09-.75.08-.73.08-.73 1.2.09 1.84 1.24 1.84 1.24 1.07 1.83 2.81 1.3 3.5.99.11-.78.42-1.3.76-1.6-2.67-.3-5.47-1.33-5.47-5.93 0-1.31.47-2.38 1.24-3.22-.12-.3-.54-1.52.12-3.18 0 0 1.01-.32 3.3 1.23a11.5 11.5 0 016 0c2.29-1.55 3.3-1.23 3.3-1.23.66 1.66.24 2.88.12 3.18.77.84 1.24 1.91 1.24 3.22 0 4.61-2.81 5.62-5.49 5.92.43.37.81 1.1.81 2.22v3.29c0 .32.22.7.83.58A12.01 12.01 0 0024 12.5C24 5.87 18.63.5 12 .5z"></path></svg>
|
||||||
|
Continue with GitHub
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- DIVIDER -->
|
||||||
|
<div style="display:flex; align-items:center; gap:14px; margin:22px 0;">
|
||||||
|
<div style="flex:1; height:1px; background:rgba(255,255,255,.08);"></div>
|
||||||
|
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; letter-spacing:.1em; color:#5a5a62;">OR</span>
|
||||||
|
<div style="flex:1; height:1px; background:rgba(255,255,255,.08);"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- EMAIL FORM -->
|
||||||
|
<div style="display:flex; flex-direction:column; gap:14px;">
|
||||||
|
<sc-if value="{{ isSignup }}" hint-placeholder-val="{{ false }}">
|
||||||
|
<div>
|
||||||
|
<div style="font-size:12px; font-weight:600; color:#b5b5bd; margin-bottom:7px;">Full name</div>
|
||||||
|
<input type="text" placeholder="Ada Lovelace" style="width:100%; height:44px; border-radius:10px; border:1px solid rgba(255,255,255,.12); background:#0d0d10; color:#f3f3f5; font-size:14px; padding:0 14px;" style-focus="border-color:#ff6f61;">
|
||||||
|
</div>
|
||||||
|
</sc-if>
|
||||||
|
<div>
|
||||||
|
<div style="font-size:12px; font-weight:600; color:#b5b5bd; margin-bottom:7px;">Email address</div>
|
||||||
|
<input type="email" placeholder="[email protected]" style="width:100%; height:44px; border-radius:10px; border:1px solid rgba(255,255,255,.12); background:#0d0d10; color:#f3f3f5; font-size:14px; padding:0 14px;" style-focus="border-color:#ff6f61;">
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style="display:flex; align-items:center; margin-bottom:7px;">
|
||||||
|
<span style="font-size:12px; font-weight:600; color:#b5b5bd;">Password</span>
|
||||||
|
<span style="flex:1;"></span>
|
||||||
|
<sc-if value="{{ isSignin }}" hint-placeholder-val="{{ true }}">
|
||||||
|
<span style="font-size:12px; color:#ff8a7a; cursor:pointer;">Forgot?</span>
|
||||||
|
</sc-if>
|
||||||
|
</div>
|
||||||
|
<input type="password" placeholder="••••••••••" style="width:100%; height:44px; border-radius:10px; border:1px solid rgba(255,255,255,.12); background:#0d0d10; color:#f3f3f5; font-size:14px; padding:0 14px;" style-focus="border-color:#ff6f61;">
|
||||||
|
</div>
|
||||||
|
<div style="display:flex; align-items:center; justify-content:center; height:46px; border-radius:10px; background:linear-gradient(135deg,#ff8a7a,#ff5f57); color:#2a0d0a; font-size:15px; font-weight:700; cursor:pointer; margin-top:4px;" style-hover="filter:brightness(1.07);">{{ cta }}</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TOGGLE -->
|
||||||
|
<div style="text-align:center; margin-top:22px; font-size:14px; color:#8a8a92;">
|
||||||
|
{{ toggleText }} <span style="color:#ff8a7a; font-weight:600; cursor:pointer;" onClick="{{ toggle }}">{{ toggleLink }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CLERK FOOTER -->
|
||||||
|
<div style="display:flex; align-items:center; justify-content:center; gap:6px; margin-top:30px; font-family:'JetBrains Mono',monospace; font-size:10px; color:#4a4a52;">
|
||||||
|
<svg width="11" height="11" viewBox="0 0 20 20" fill="none"><rect x="4" y="9" width="12" height="8" rx="2" stroke="#4a4a52" stroke-width="1.5"></rect><path d="M7 9V6.5a3 3 0 016 0V9" stroke="#4a4a52" stroke-width="1.5"></path></svg>
|
||||||
|
Secured by Clerk
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-dc>
|
||||||
|
<script type="text/x-dc" data-dc-script data-props="{"$preview":{"width":1100,"height":760}}">
|
||||||
|
class Component extends DCLogic {
|
||||||
|
state = { mode: 'signin' };
|
||||||
|
renderVals() {
|
||||||
|
const signup = this.state.mode === 'signup';
|
||||||
|
return {
|
||||||
|
isSignup: signup,
|
||||||
|
isSignin: !signup,
|
||||||
|
title: signup ? 'Create your account' : 'Welcome back',
|
||||||
|
subtitle: signup ? 'Deploy your first claw in minutes.' : 'Sign in to your Clawmates workspace.',
|
||||||
|
cta: signup ? 'Create account' : 'Sign in',
|
||||||
|
toggleText: signup ? 'Already have an account?' : "Don't have an account?",
|
||||||
|
toggleLink: signup ? 'Sign in' : 'Sign up',
|
||||||
|
toggle: () => this.setState({ mode: signup ? 'signin' : 'signup' }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -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,708 @@
|
|||||||
|
<!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-blink { 0%,100% { opacity: 1; } 50% { opacity: .3; } }
|
||||||
|
@keyframes cm-spin { to { transform: rotate(360deg); } }
|
||||||
|
@keyframes cm-fade { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: translateY(0); } }
|
||||||
|
@keyframes cm-pip { 0%,100% { opacity: .35; } 50% { opacity: 1; } }
|
||||||
|
.cm-scroll::-webkit-scrollbar { width: 8px; }
|
||||||
|
.cm-scroll::-webkit-scrollbar-thumb { background: rgba(255,255,255,.1); border-radius: 4px; }
|
||||||
|
</style>
|
||||||
|
</helmet>
|
||||||
|
|
||||||
|
<div style="width:100%; height:100vh; min-height:660px; 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:#6a6a72;">Large World</span>
|
||||||
|
<span style="color:#3a3a40;">/</span>
|
||||||
|
<span style="font-family:'JetBrains Mono',monospace; font-size:12px; color:#6a6a72;">Agents</span>
|
||||||
|
<span style="color:#3a3a40;">/</span>
|
||||||
|
<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;">Infrastructure</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>{{ fleetOnline }}/{{ fleetTotal }} hosts online
|
||||||
|
</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="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="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="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="3" y="4" width="14" height="5" rx="1.6" stroke="currentColor" stroke-width="1.4" fill="none"></rect><rect x="3" y="11" width="14" height="5" rx="1.6" stroke="currentColor" stroke-width="1.4" fill="none"></rect><circle cx="6" cy="6.5" r="1" fill="currentColor"></circle><circle cx="6" cy="13.5" r="1" fill="currentColor"></circle></svg>
|
||||||
|
<span style="font-family:'JetBrains Mono',monospace; font-size:7px; letter-spacing:.05em;">INFRA</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 NAV -->
|
||||||
|
<div style="width:216px; 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 12px;">
|
||||||
|
<div style="font-family:'JetBrains Mono',monospace; font-size:10px; letter-spacing:.12em; color:#5a5a62;">INFRASTRUCTURE</div>
|
||||||
|
<div style="font-size:17px; font-weight:700; margin-top:3px;">Your fleet</div>
|
||||||
|
</div>
|
||||||
|
<div style="flex:1; overflow-y:auto; padding:4px 8px;" class="cm-scroll">
|
||||||
|
<div style="font-family:'JetBrains Mono',monospace; font-size:9px; letter-spacing:.14em; color:#5a5a62; padding:8px 8px 6px;">RUN HERE FIRST</div>
|
||||||
|
<div style="display:flex; align-items:center; gap:10px; padding:9px 10px; border-radius:9px; cursor:pointer; position:relative; {{ navLocalStyle }}" onClick="{{ goLocal }}">
|
||||||
|
<sc-if value="{{ isLocal }}" 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="16" height="16" viewBox="0 0 20 20" style="color:{{ navLocalIcon }};"><rect x="3" y="4" width="14" height="5" rx="1.6" stroke="currentColor" stroke-width="1.4" fill="none"></rect><rect x="3" y="11" width="14" height="5" rx="1.6" stroke="currentColor" stroke-width="1.4" fill="none"></rect><circle cx="6" cy="6.5" r="1" fill="currentColor"></circle><circle cx="6" cy="13.5" r="1" fill="currentColor"></circle></svg>
|
||||||
|
<div style="flex:1;"><div style="font-size:13px; font-weight:600; color:{{ navLocalText }};">Local & Tailscale</div></div>
|
||||||
|
<span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#5fd08a;">{{ fleetOnline }}</span>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex; flex-direction:column; gap:1px; margin:2px 0 10px 14px; padding-left:10px; border-left:1px solid rgba(255,255,255,.07);">
|
||||||
|
<div style="display:flex; align-items:center; gap:8px; padding:6px 9px; border-radius:7px; cursor:pointer;"><span style="width:5px; height:5px; border-radius:50%; background:#5fd08a;"></span><span style="font-size:12px; color:#b5b5bd;">Local hosts</span><span style="flex:1;"></span><span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#5a5a62;">{{ fleetTotal }}</span></div>
|
||||||
|
<div style="display:flex; align-items:center; gap:8px; padding:6px 9px; border-radius:7px; cursor:pointer;"><span style="width:5px; height:5px; border-radius:50%; background:#5ec8d8;"></span><span style="font-size:12px; color:#b5b5bd;">Tailscale network</span><span style="flex:1;"></span><span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#5a5a62;">{{ tsDeviceCount }}</span></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="font-family:'JetBrains Mono',monospace; font-size:9px; letter-spacing:.14em; color:#5a5a62; padding:8px 8px 6px;">NEXT</div>
|
||||||
|
<div style="display:flex; align-items:center; gap:10px; padding:9px 10px; border-radius:9px; cursor:pointer; position:relative; {{ navCloudStyle }}" onClick="{{ goCloud }}">
|
||||||
|
<sc-if value="{{ isCloud }}" 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="16" height="16" viewBox="0 0 20 20" style="color:{{ navCloudIcon }};"><path d="M6 14h9a3 3 0 000-6 4.5 4.5 0 00-8.7-1.2A3.4 3.4 0 006 14z" stroke="currentColor" stroke-width="1.4" fill="none"></path></svg>
|
||||||
|
<div style="flex:1;"><div style="font-size:13px; font-weight:600; color:{{ navCloudText }};">Cloud providers</div></div>
|
||||||
|
<span style="font-family:'JetBrains Mono',monospace; font-size:8px; color:#e8b465; padding:2px 6px; border-radius:5px; background:rgba(232,196,106,.1);">SOON</span>
|
||||||
|
</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:36px; border-radius:9px; background:linear-gradient(135deg,#ff8a7a,#ff5f57); color:#2a0d0a; font-size:13px; font-weight:700; cursor:pointer;" onClick="{{ openHostWizard }}"><span style="font-size:15px; line-height:1;">+</span> Connect a host</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CENTER -->
|
||||||
|
<div style="flex:1; min-width:0; background:radial-gradient(130% 100% at 50% 0%, #0d0d12 0%, #08080a 60%); display:flex; flex-direction:column; min-height:0;">
|
||||||
|
<sc-if value="{{ isLocal }}" hint-placeholder-val="{{ true }}">
|
||||||
|
<div style="flex:1; min-height:0; overflow-y:auto; padding:22px 26px 30px;" class="cm-scroll">
|
||||||
|
|
||||||
|
<div style="margin-bottom:6px; font-family:'JetBrains Mono',monospace; font-size:10px; letter-spacing:.14em; color:#5a5a62;">LOCAL & TAILSCALE</div>
|
||||||
|
<div style="display:flex; align-items:flex-end; gap:14px; margin-bottom:20px;">
|
||||||
|
<div><div style="font-size:26px; font-weight:700; letter-spacing:-.02em;">Your fleet</div><div style="font-size:13px; color:#8a8a92; margin-top:3px;">Run agents on hardware you own. Put a node on your tailnet, then pair it — no inbound port, no keys.</div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- fleet stats -->
|
||||||
|
<div style="display:grid; grid-template-columns:repeat(5,1fr); gap:10px; margin-bottom:22px;">
|
||||||
|
<div style="border-radius:12px; border:1px solid rgba(255,255,255,.07); background:#0d0d10; padding:13px 15px;"><div style="font-family:'JetBrains Mono',monospace; font-size:9px; letter-spacing:.1em; color:#6a6a72;">HOSTS</div><div style="font-size:22px; font-weight:700; margin-top:4px;">{{ fleetTotal }}</div></div>
|
||||||
|
<div style="border-radius:12px; border:1px solid rgba(95,208,138,.18); background:#0d0d10; padding:13px 15px;"><div style="font-family:'JetBrains Mono',monospace; font-size:9px; letter-spacing:.1em; color:#6a6a72;">ONLINE</div><div style="font-size:22px; font-weight:700; margin-top:4px; color:#5fd08a;">{{ fleetOnline }}</div></div>
|
||||||
|
<div style="border-radius:12px; border:1px solid rgba(255,255,255,.07); background:#0d0d10; padding:13px 15px;"><div style="font-family:'JetBrains Mono',monospace; font-size:9px; letter-spacing:.1em; color:#6a6a72;">vCPU</div><div style="font-size:22px; font-weight:700; margin-top:4px;">{{ vcpu }}</div></div>
|
||||||
|
<div style="border-radius:12px; border:1px solid rgba(255,255,255,.07); background:#0d0d10; padding:13px 15px;"><div style="font-family:'JetBrains Mono',monospace; font-size:9px; letter-spacing:.1em; color:#6a6a72;">MEMORY</div><div style="font-size:22px; font-weight:700; margin-top:4px;">{{ ram }}</div></div>
|
||||||
|
<div style="border-radius:12px; border:1px solid rgba(94,200,216,.18); background:#0d0d10; padding:13px 15px;"><div style="font-family:'JetBrains Mono',monospace; font-size:9px; letter-spacing:.1em; color:#6a6a72;">CONTAINERS</div><div style="font-size:22px; font-weight:700; margin-top:4px; color:#5ec8d8;">{{ containersRunning }}</div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Tailscale network -->
|
||||||
|
<div style="border-radius:14px; border:1px solid rgba(255,255,255,.08); background:#0c0c0f; margin-bottom:22px; overflow:hidden;">
|
||||||
|
<div style="display:flex; align-items:center; gap:11px; padding:13px 16px; border-bottom:1px solid rgba(255,255,255,.05);">
|
||||||
|
<div style="width:30px; height:30px; border-radius:8px; background:#fff; display:flex; align-items:center; justify-content:center;">
|
||||||
|
<svg width="17" height="17" viewBox="0 0 24 24"><g fill="#141414"><circle cx="5" cy="5" r="2.1" opacity=".3"></circle><circle cx="12" cy="5" r="2.1" opacity=".3"></circle><circle cx="19" cy="5" r="2.1" opacity=".3"></circle><circle cx="5" cy="12" r="2.1"></circle><circle cx="12" cy="12" r="2.1"></circle><circle cx="19" cy="12" r="2.1"></circle><circle cx="5" cy="19" r="2.1" opacity=".3"></circle><circle cx="12" cy="19" r="2.1"></circle><circle cx="19" cy="19" r="2.1" opacity=".3"></circle></g></svg>
|
||||||
|
</div>
|
||||||
|
<div style="flex:1;"><div style="font-size:14px; font-weight:700;">Tailscale network</div><div style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#6a6a72;">{{ tailnet }} · live device status</div></div>
|
||||||
|
<span style="display:flex; align-items:center; gap:6px; font-family:'JetBrains Mono',monospace; font-size:10px; color:#5fd08a; padding:4px 10px; border-radius:7px; background:rgba(95,208,138,.08); border:1px solid rgba(95,208,138,.25);"><span style="width:6px; height:6px; border-radius:50%; background:#5fd08a; animation:cm-blink 1.6s infinite;"></span>connected</span>
|
||||||
|
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#5ec8d8; cursor:pointer;" onClick="{{ openTailscale }}">manage →</span>
|
||||||
|
</div>
|
||||||
|
<div style="display:grid; grid-template-columns:1fr 1fr; gap:1px; background:rgba(255,255,255,.04);">
|
||||||
|
<sc-for list="{{ tsDevices }}" as="d" hint-placeholder-count="4">
|
||||||
|
<div style="display:flex; align-items:center; gap:10px; padding:11px 16px; background:#0c0c0f;">
|
||||||
|
<span style="width:7px; height:7px; border-radius:50%; background:{{ d.dot }};"></span>
|
||||||
|
<span style="font-size:13px; font-weight:600; color:#eaeaee;">{{ d.name }}</span>
|
||||||
|
<span style="flex:1;"></span>
|
||||||
|
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#8a8a92;">{{ d.os }}</span>
|
||||||
|
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#5ec8d8; width:96px; text-align:right;">{{ d.ip }}</span>
|
||||||
|
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#6a6a72; width:40px; text-align:right;">{{ d.seen }}</span>
|
||||||
|
</div>
|
||||||
|
</sc-for>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- host health cards -->
|
||||||
|
<div style="display:flex; align-items:center; gap:10px; margin-bottom:12px;">
|
||||||
|
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; letter-spacing:.12em; color:#5a5a62;">LOCAL HOSTS</span>
|
||||||
|
<span style="flex:1; height:1px; background:rgba(255,255,255,.06);"></span>
|
||||||
|
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#5ec8d8; cursor:pointer;" onClick="{{ openHostWizard }}">+ connect a host</span>
|
||||||
|
</div>
|
||||||
|
<div style="display:grid; grid-template-columns:repeat(auto-fill, minmax(290px, 1fr)); gap:14px;">
|
||||||
|
<sc-for list="{{ hostCards }}" as="h" hint-placeholder-count="3">
|
||||||
|
<div style="border-radius:13px; border:1px solid {{ h.accentBorder }}; background:#0d0d10; padding:15px; display:flex; flex-direction:column;">
|
||||||
|
<div style="display:flex; align-items:center; gap:9px; margin-bottom:13px;">
|
||||||
|
<span style="position:relative; width:9px; height:9px;"><span style="position:absolute; inset:0; border-radius:50%; background:{{ h.statusColor }};"></span></span>
|
||||||
|
<span style="font-size:14px; font-weight:700;">{{ h.name }}</span>
|
||||||
|
<span style="flex:1;"></span>
|
||||||
|
<span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:{{ h.statusColor }};">{{ h.statusLabel }}</span>
|
||||||
|
</div>
|
||||||
|
<div style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#6a6a72; margin-bottom:14px;">{{ h.os }}</div>
|
||||||
|
|
||||||
|
<div style="display:flex; flex-direction:column; gap:9px; margin-bottom:13px;">
|
||||||
|
<div>
|
||||||
|
<div style="display:flex; justify-content:space-between; font-family:'JetBrains Mono',monospace; font-size:9px; color:#8a8a92; margin-bottom:4px;"><span>CPU</span><span style="color:{{ h.cpuCol }};">{{ h.cpu }}%</span></div>
|
||||||
|
<div style="height:4px; border-radius:2px; background:rgba(255,255,255,.07); overflow:hidden;"><div style="width:{{ h.cpuW }}; height:100%; background:{{ h.cpuCol }};"></div></div>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div style="display:flex; justify-content:space-between; font-family:'JetBrains Mono',monospace; font-size:9px; color:#8a8a92; margin-bottom:4px;"><span>RAM</span><span style="color:{{ h.ramCol }};">{{ h.ram }}%</span></div>
|
||||||
|
<div style="height:4px; border-radius:2px; background:rgba(255,255,255,.07); overflow:hidden;"><div style="width:{{ h.ramW }}; height:100%; background:{{ h.ramCol }};"></div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display:flex; gap:6px; margin-bottom:13px;">
|
||||||
|
<div style="flex:1; text-align:center; padding:7px 0; border-radius:8px; background:rgba(255,255,255,.03);"><div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72;">disk</div><div style="font-size:12px; font-weight:600; margin-top:2px;">{{ h.disk }}%</div></div>
|
||||||
|
<div style="flex:1; text-align:center; padding:7px 0; border-radius:8px; background:rgba(255,255,255,.03);"><div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72;">load</div><div style="font-size:12px; font-weight:600; margin-top:2px;">{{ h.load }}</div></div>
|
||||||
|
<div style="flex:1; text-align:center; padding:7px 0; border-radius:8px; background:rgba(255,255,255,.03);"><div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72;">ctrs</div><div style="font-size:12px; font-weight:600; margin-top:2px;">{{ h.containers }}</div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<sc-if value="{{ h.hasSsh }}" hint-placeholder-val="{{ true }}">
|
||||||
|
<div style="display:flex; align-items:center; gap:8px; padding:8px 11px; border-radius:8px; background:#070708; border:1px solid rgba(255,255,255,.07);">
|
||||||
|
<span style="width:14px; height:14px; flex:none;"><svg width="14" height="14" viewBox="0 0 24 24"><g fill="#5ec8d8"><circle cx="6" cy="6" r="1.8"></circle><circle cx="12" cy="6" r="1.8" opacity=".4"></circle><circle cx="6" cy="12" r="1.8"></circle><circle cx="12" cy="12" r="1.8"></circle><circle cx="18" cy="12" r="1.8" opacity=".4"></circle><circle cx="12" cy="18" r="1.8" opacity=".4"></circle></g></svg></span>
|
||||||
|
<span style="font-family:'JetBrains Mono',monospace; font-size:11px; color:#cfcfd5; flex:1; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;">{{ h.sshTarget }}</span>
|
||||||
|
<span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#5ec8d8; cursor:pointer;">copy</span>
|
||||||
|
</div>
|
||||||
|
</sc-if>
|
||||||
|
<sc-if value="{{ h.pairing }}" hint-placeholder-val="{{ false }}">
|
||||||
|
<div style="display:flex; align-items:center; gap:8px; padding:8px 11px; border-radius:8px; background:rgba(232,196,106,.06); border:1px solid rgba(232,196,106,.2);">
|
||||||
|
<span style="width:11px; height:11px; border:2px solid #e8b465; border-top-color:transparent; border-radius:50%; animation:cm-spin .8s linear infinite;"></span>
|
||||||
|
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#e8b465;">waiting for daemon to dial home…</span>
|
||||||
|
</div>
|
||||||
|
</sc-if>
|
||||||
|
</div>
|
||||||
|
</sc-for>
|
||||||
|
|
||||||
|
<!-- add host tile -->
|
||||||
|
<div style="border-radius:13px; border:1.5px dashed rgba(255,255,255,.14); background:transparent; padding:15px; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:8px; min-height:200px; cursor:pointer;" onClick="{{ openHostWizard }}">
|
||||||
|
<div style="width:38px; height:38px; border-radius:11px; background:rgba(255,111,97,.12); display:flex; align-items:center; justify-content:center; color:#ff6f61; font-size:22px; font-weight:300;">+</div>
|
||||||
|
<div style="font-size:13px; font-weight:600; color:#cfcfd5;">Connect a host</div>
|
||||||
|
<div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72; text-align:center;">Mac · Linux · edge device</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</sc-if>
|
||||||
|
<sc-if value="{{ isCloud }}" hint-placeholder-val="{{ false }}">
|
||||||
|
<div style="flex:1; min-height:0; overflow-y:auto; padding:22px 26px 30px;" class="cm-scroll">
|
||||||
|
<div style="margin-bottom:6px; font-family:'JetBrains Mono',monospace; font-size:10px; letter-spacing:.14em; color:#5a5a62;">CLOUD PROVIDERS</div>
|
||||||
|
<div style="font-size:26px; font-weight:700; letter-spacing:-.02em; margin-bottom:4px;">Power the platform from the cloud</div>
|
||||||
|
<div style="font-size:13px; color:#8a8a92; max-width:600px; margin-bottom:22px; line-height:1.5;">Connect a provider and Clawmates provisions agent runners on demand. Each booted VM runs the same daemon, dials home over outbound WSS, and joins your fleet — credentials stay in the secret broker.</div>
|
||||||
|
|
||||||
|
<!-- how cloud access works -->
|
||||||
|
<div style="display:grid; grid-template-columns:repeat(3,1fr); gap:12px; margin-bottom:26px;">
|
||||||
|
<div style="border-radius:12px; border:1px solid rgba(255,255,255,.07); background:#0c0c0f; padding:15px;">
|
||||||
|
<div style="display:flex; align-items:center; gap:8px; margin-bottom:9px;"><span style="width:22px; height:22px; border-radius:6px; background:rgba(255,111,97,.14); display:flex; align-items:center; justify-content:center; font-family:'JetBrains Mono',monospace; font-size:11px; font-weight:700; color:#ff8a7a;">1</span><span style="font-size:13px; font-weight:700;">Connect credentials</span></div>
|
||||||
|
<div style="font-size:11.5px; color:#8a8a92; line-height:1.5;">Scoped IAM role or API token — held by the <span style="color:#cfcfd5;">secret broker</span>, never reaches agent code.</div>
|
||||||
|
</div>
|
||||||
|
<div style="border-radius:12px; border:1px solid rgba(255,255,255,.07); background:#0c0c0f; padding:15px;">
|
||||||
|
<div style="display:flex; align-items:center; gap:8px; margin-bottom:9px;"><span style="width:22px; height:22px; border-radius:6px; background:rgba(94,200,216,.14); display:flex; align-items:center; justify-content:center; font-family:'JetBrains Mono',monospace; font-size:11px; font-weight:700; color:#5ec8d8;">2</span><span style="font-size:13px; font-weight:700;">Provision a runner</span></div>
|
||||||
|
<div style="font-size:11.5px; color:#8a8a92; line-height:1.5;">We bake <span style="font-family:'JetBrains Mono',monospace; color:#cfcfd5;">clawmates-node</span> into the image; it boots and dials home — no inbound port.</div>
|
||||||
|
</div>
|
||||||
|
<div style="border-radius:12px; border:1px solid rgba(255,255,255,.07); background:#0c0c0f; padding:15px;">
|
||||||
|
<div style="display:flex; align-items:center; gap:8px; margin-bottom:9px;"><span style="width:22px; height:22px; border-radius:6px; background:rgba(95,208,138,.14); display:flex; align-items:center; justify-content:center; font-family:'JetBrains Mono',monospace; font-size:11px; font-weight:700; color:#5fd08a;">3</span><span style="font-size:13px; font-weight:700;">Agents run sandboxed</span></div>
|
||||||
|
<div style="font-size:11.5px; color:#8a8a92; line-height:1.5;">§15 network-isolated sandboxes on the cloud host; every egress is a gated door.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- provider grid -->
|
||||||
|
<div style="display:flex; align-items:center; gap:10px; margin-bottom:12px;">
|
||||||
|
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; letter-spacing:.12em; color:#5a5a62;">PROVIDERS</span>
|
||||||
|
<span style="flex:1; height:1px; background:rgba(255,255,255,.06);"></span>
|
||||||
|
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#6a6a72;">1 connected</span>
|
||||||
|
</div>
|
||||||
|
<div style="display:grid; grid-template-columns:repeat(auto-fill, minmax(232px, 1fr)); gap:14px; margin-bottom:28px;">
|
||||||
|
<sc-for list="{{ cloudProviders }}" as="p" hint-placeholder-count="5">
|
||||||
|
<div style="border-radius:14px; border:1px solid {{ p.cardBorder }}; background:#0d0d10; padding:18px; display:flex; flex-direction:column;">
|
||||||
|
<div style="display:flex; align-items:flex-start; gap:11px; margin-bottom:14px;">
|
||||||
|
<div style="width:42px; height:42px; border-radius:11px; background:{{ p.tile }}; display:flex; align-items:center; justify-content:center; font-weight:700; color:{{ p.ink }}; font-size:13px;">
|
||||||
|
<sc-if value="{{ p.isText }}" hint-placeholder-val="{{ true }}">{{ p.short }}</sc-if>
|
||||||
|
<sc-if value="{{ p.isApple }}" hint-placeholder-val="{{ false }}"><svg width="20" height="20" viewBox="0 0 24 24" fill="none"><rect x="6" y="6" width="12" height="12" rx="2.5" stroke="#1a1a1d" stroke-width="1.6"></rect><rect x="9.5" y="9.5" width="5" height="5" rx="1" fill="#1a1a1d"></rect><path d="M9 6V3M15 6V3M9 21v-3M15 21v-3M6 9H3M6 15H3M21 9h-3M21 15h-3" stroke="#1a1a1d" stroke-width="1.5" stroke-linecap="round"></path></svg></sc-if>
|
||||||
|
</div>
|
||||||
|
<div style="flex:1; min-width:0;">
|
||||||
|
<div style="font-size:14px; font-weight:700;">{{ p.name }}</div>
|
||||||
|
<div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72; margin-top:2px;">{{ p.regions }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#8a8a92; margin-bottom:16px; line-height:1.5;">{{ p.offers }}</div>
|
||||||
|
<div style="margin-top:auto; display:flex; align-items:center; justify-content:center; gap:7px; height:34px; border-radius:8px; cursor:pointer; font-family:'JetBrains Mono',monospace; font-size:11px; font-weight:600; {{ p.statusStyle }}" onClick="{{ p.onConnect }}">{{ p.statusLabel }}</div>
|
||||||
|
</div>
|
||||||
|
</sc-for>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- live cloud runners (connected provider) -->
|
||||||
|
<div style="display:flex; align-items:center; gap:10px; margin-bottom:12px;">
|
||||||
|
<div style="width:24px; height:24px; border-radius:7px; background:linear-gradient(135deg,#ff9a3c,#ff7314); display:flex; align-items:center; justify-content:center; font-family:'JetBrains Mono',monospace; font-size:9px; font-weight:700; color:#241200;">aws</div>
|
||||||
|
<span style="font-size:14px; font-weight:700;">Cloud runners</span>
|
||||||
|
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#5fd08a;">● 2 online · 5 agents</span>
|
||||||
|
<span style="flex:1;"></span>
|
||||||
|
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#e8b465;">~$0.44/hr</span>
|
||||||
|
</div>
|
||||||
|
<div style="display:grid; grid-template-columns:repeat(auto-fill, minmax(300px, 1fr)); gap:14px;">
|
||||||
|
<sc-for list="{{ cloudRunners }}" as="r" hint-placeholder-count="2">
|
||||||
|
<div style="border-radius:13px; border:1px solid rgba(95,208,138,.18); background:#0d0d10; padding:15px;">
|
||||||
|
<div style="display:flex; align-items:center; gap:9px; margin-bottom:12px;">
|
||||||
|
<span style="width:8px; height:8px; border-radius:50%; background:#5fd08a;"></span>
|
||||||
|
<span style="font-size:13px; font-weight:700;">{{ r.name }}</span>
|
||||||
|
<span style="flex:1;"></span>
|
||||||
|
<span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#5ec8d8;">{{ r.region }}</span>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex; gap:14px; margin-bottom:12px;">
|
||||||
|
<div style="flex:1;">
|
||||||
|
<div style="display:flex; justify-content:space-between; font-family:'JetBrains Mono',monospace; font-size:9px; color:#8a8a92; margin-bottom:4px;"><span>CPU</span><span style="color:#5ec8d8;">{{ r.cpu }}%</span></div>
|
||||||
|
<div style="height:4px; border-radius:2px; background:rgba(255,255,255,.07); overflow:hidden;"><div style="width:{{ r.cpuW }}; height:100%; background:#5ec8d8;"></div></div>
|
||||||
|
</div>
|
||||||
|
<div style="flex:1;">
|
||||||
|
<div style="display:flex; justify-content:space-between; font-family:'JetBrains Mono',monospace; font-size:9px; color:#8a8a92; margin-bottom:4px;"><span>RAM</span><span style="color:#5fd08a;">{{ r.ram }}%</span></div>
|
||||||
|
<div style="height:4px; border-radius:2px; background:rgba(255,255,255,.07); overflow:hidden;"><div style="width:{{ r.ramW }}; height:100%; background:#5fd08a;"></div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex; align-items:center; gap:8px; font-family:'JetBrains Mono',monospace; font-size:10px;">
|
||||||
|
<span style="color:#cfcfd5; padding:3px 8px; border-radius:6px; background:rgba(255,255,255,.04);">{{ r.agents }} agents</span>
|
||||||
|
<span style="flex:1;"></span>
|
||||||
|
<span style="color:#e8b465;">{{ r.cost }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</sc-for>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</sc-if>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- RIGHT PANEL -->
|
||||||
|
<div style="width:344px; flex:none; border-left:1px solid rgba(255,255,255,.06); background:#0b0b0e; display:flex; flex-direction:column; min-height:0;">
|
||||||
|
<div style="flex:none; display:flex; align-items:center; gap:8px; padding:13px 16px; border-bottom:1px solid rgba(255,255,255,.06);">
|
||||||
|
<sc-if value="{{ showMenu }}" hint-placeholder-val="{{ true }}"><span style="font-family:'JetBrains Mono',monospace; font-size:10px; letter-spacing:.14em; color:#5a5a62;">ADD TO FLEET</span></sc-if>
|
||||||
|
<sc-if value="{{ showWizard }}" hint-placeholder-val="{{ false }}"><span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#5ec8d8; cursor:pointer;" onClick="{{ backToMenu }}">← add to fleet</span></sc-if>
|
||||||
|
<sc-if value="{{ showTailscale }}" hint-placeholder-val="{{ false }}"><span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#5ec8d8; cursor:pointer;" onClick="{{ backToMenu }}">← add to fleet</span></sc-if>
|
||||||
|
<sc-if value="{{ showCloudMenu }}" hint-placeholder-val="{{ false }}"><span style="font-family:'JetBrains Mono',monospace; font-size:10px; letter-spacing:.14em; color:#5a5a62;">CONNECT A PROVIDER</span></sc-if>
|
||||||
|
<sc-if value="{{ showCloudConnect }}" hint-placeholder-val="{{ false }}"><span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#5ec8d8; cursor:pointer;" onClick="{{ backToMenu }}">← providers</span></sc-if>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="flex:1; min-height:0; overflow-y:auto; padding:16px;" class="cm-scroll">
|
||||||
|
|
||||||
|
<!-- MENU -->
|
||||||
|
<sc-if value="{{ showMenu }}" hint-placeholder-val="{{ true }}">
|
||||||
|
<div style="display:flex; flex-direction:column; gap:11px;">
|
||||||
|
<div style="border-radius:13px; border:1px solid rgba(255,111,97,.3); background:linear-gradient(180deg,#140f12,#0d0d10); padding:16px; cursor:pointer;" onClick="{{ openHostWizard }}">
|
||||||
|
<div style="display:flex; align-items:center; gap:11px; margin-bottom:9px;">
|
||||||
|
<div style="width:34px; height:34px; border-radius:9px; background:rgba(255,111,97,.16); display:flex; align-items:center; justify-content:center; color:#ff6f61;"><svg width="18" height="18" viewBox="0 0 20 20"><rect x="3" y="4" width="14" height="5" rx="1.6" stroke="currentColor" stroke-width="1.4" fill="none"></rect><rect x="3" y="11" width="14" height="5" rx="1.6" stroke="currentColor" stroke-width="1.4" fill="none"></rect></svg></div>
|
||||||
|
<div style="flex:1;"><div style="font-size:14px; font-weight:700;">Connect a host</div><div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#ff8a7a;">recommended first step</div></div>
|
||||||
|
<span style="color:#ff8a7a;">→</span>
|
||||||
|
</div>
|
||||||
|
<div style="font-size:12px; color:#9a9aa2; line-height:1.5;">Pair a Mac, Linux box, or edge device. The daemon dials home over outbound WSS — no inbound port, no keys.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="border-radius:13px; border:1px solid rgba(255,255,255,.1); background:#0d0d10; padding:16px; cursor:pointer;" onClick="{{ openTailscale }}">
|
||||||
|
<div style="display:flex; align-items:center; gap:11px; margin-bottom:9px;">
|
||||||
|
<div style="width:34px; height:34px; border-radius:9px; background:#fff; display:flex; align-items:center; justify-content:center;"><svg width="17" height="17" viewBox="0 0 24 24"><g fill="#141414"><circle cx="5" cy="5" r="2.1" opacity=".3"></circle><circle cx="12" cy="5" r="2.1" opacity=".3"></circle><circle cx="19" cy="5" r="2.1" opacity=".3"></circle><circle cx="5" cy="12" r="2.1"></circle><circle cx="12" cy="12" r="2.1"></circle><circle cx="19" cy="12" r="2.1"></circle><circle cx="5" cy="19" r="2.1" opacity=".3"></circle><circle cx="12" cy="19" r="2.1"></circle><circle cx="19" cy="19" r="2.1" opacity=".3"></circle></g></svg></div>
|
||||||
|
<div style="flex:1;"><div style="font-size:14px; font-weight:700;">Tailscale network</div><div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#5fd08a;">connected · {{ tsDeviceCount }} devices</div></div>
|
||||||
|
<span style="color:#8a8a92;">→</span>
|
||||||
|
</div>
|
||||||
|
<div style="font-size:12px; color:#9a9aa2; line-height:1.5;">Paste your tailnet + API key for live device status across the whole network.</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="border-radius:13px; border:1px solid rgba(255,255,255,.06); background:#0a0a0c; padding:16px; opacity:.7;">
|
||||||
|
<div style="display:flex; align-items:center; gap:11px; margin-bottom:9px;">
|
||||||
|
<div style="width:34px; height:34px; border-radius:9px; background:rgba(255,255,255,.05); display:flex; align-items:center; justify-content:center; color:#6a6a72;"><svg width="18" height="18" viewBox="0 0 20 20"><path d="M6 14h9a3 3 0 000-6 4.5 4.5 0 00-8.7-1.2A3.4 3.4 0 006 14z" stroke="currentColor" stroke-width="1.4" fill="none"></path></svg></div>
|
||||||
|
<div style="flex:1;"><div style="font-size:14px; font-weight:700; color:#9a9aa2;">Cloud providers</div><div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72;">AWS · GCP · Azure</div></div>
|
||||||
|
<span style="font-family:'JetBrains Mono',monospace; font-size:8px; color:#e8b465; padding:2px 6px; border-radius:5px; background:rgba(232,196,106,.1);">NEXT</span>
|
||||||
|
</div>
|
||||||
|
<div style="font-size:12px; color:#6a6a72; line-height:1.5;">Local & Tailscale first. Cloud provisioning arrives next.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</sc-if>
|
||||||
|
|
||||||
|
<!-- WIZARD -->
|
||||||
|
<sc-if value="{{ showWizard }}" hint-placeholder-val="{{ false }}">
|
||||||
|
<div style="animation:cm-fade .2s ease;">
|
||||||
|
<div style="font-size:16px; font-weight:700; margin-bottom:3px;">Connect a host</div>
|
||||||
|
<div style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#6a6a72; margin-bottom:16px;">3 steps · ~2 min</div>
|
||||||
|
|
||||||
|
<!-- stepper -->
|
||||||
|
<div style="display:flex; align-items:center; gap:6px; margin-bottom:18px;">
|
||||||
|
<span style="width:24px; height:24px; border-radius:50%; display:flex; align-items:center; justify-content:center; font-family:'JetBrains Mono',monospace; font-size:11px; font-weight:700; {{ stepIs1Style }}">1</span>
|
||||||
|
<span style="flex:1; height:1px; background:rgba(255,255,255,.1);"></span>
|
||||||
|
<span style="width:24px; height:24px; border-radius:50%; display:flex; align-items:center; justify-content:center; font-family:'JetBrains Mono',monospace; font-size:11px; font-weight:700; {{ stepIs2Style }}">2</span>
|
||||||
|
<span style="flex:1; height:1px; background:rgba(255,255,255,.1);"></span>
|
||||||
|
<span style="width:24px; height:24px; border-radius:50%; display:flex; align-items:center; justify-content:center; font-family:'JetBrains Mono',monospace; font-size:11px; font-weight:700; {{ stepIs3Style }}">3</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- STEP 1 INSTALL -->
|
||||||
|
<sc-if value="{{ step1 }}" hint-placeholder-val="{{ true }}">
|
||||||
|
<div style="animation:cm-fade .2s ease;">
|
||||||
|
<div style="font-size:14px; font-weight:700; margin-bottom:4px;">Install the daemon</div>
|
||||||
|
<div style="font-size:12px; color:#9a9aa2; line-height:1.55; margin-bottom:13px;">We minted a one-time token. Run this on the node you're adding:</div>
|
||||||
|
<div style="border-radius:10px; background:#070708; border:1px solid rgba(255,255,255,.08); overflow:hidden; margin-bottom:6px;">
|
||||||
|
<div style="display:flex; align-items:center; gap:7px; padding:7px 11px; border-bottom:1px solid rgba(255,255,255,.05);"><span style="width:6px; height:6px; border-radius:50%; background:#ff5f57;"></span><span style="width:6px; height:6px; border-radius:50%; background:#febc2e;"></span><span style="width:6px; height:6px; border-radius:50%; background:#28c840;"></span><span style="flex:1;"></span><span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#5ec8d8; cursor:pointer;" onClick="{{ copyToken }}">{{ copyLabel }}</span></div>
|
||||||
|
<div style="padding:12px; font-family:'JetBrains Mono',monospace; font-size:11px; line-height:1.7; color:#cfcfd5;"><span style="color:#5fd08a;">clawmates-node</span> \<br> --server <span style="color:#5ec8d8;">https://clawmates.work</span> \<br> --token <span style="color:#e8b465;">cmn_8f3a…d21</span></div>
|
||||||
|
</div>
|
||||||
|
<div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#5a5a62; margin-bottom:14px;">one-time token · expires in 15 min</div>
|
||||||
|
|
||||||
|
<div style="display:flex; flex-direction:column; gap:9px; margin-bottom:16px;">
|
||||||
|
<div style="display:flex; gap:9px; padding:10px 12px; border-radius:9px; background:rgba(94,200,216,.05); border:1px solid rgba(94,200,216,.18);">
|
||||||
|
<span style="color:#5ec8d8; flex:none;">ⓘ</span>
|
||||||
|
<div style="font-size:11px; color:#a8a8b0; line-height:1.5;"><span style="color:#cfcfd5; font-weight:600;">Docker</span> should be installed on the node so it can run agents later.</div>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex; gap:9px; padding:10px 12px; border-radius:9px; background:rgba(255,255,255,.03); border:1px solid rgba(255,255,255,.07);">
|
||||||
|
<span style="width:14px; flex:none;"><svg width="14" height="14" viewBox="0 0 24 24"><g fill="#9a9aa2"><circle cx="6" cy="6" r="1.8"></circle><circle cx="12" cy="6" r="1.8" opacity=".4"></circle><circle cx="6" cy="12" r="1.8"></circle><circle cx="12" cy="12" r="1.8"></circle><circle cx="18" cy="12" r="1.8" opacity=".4"></circle></g></svg></span>
|
||||||
|
<div style="font-size:11px; color:#a8a8b0; line-height:1.5;">Tip: run <span style="font-family:'JetBrains Mono',monospace; color:#cfcfd5;">sudo tailscale up --ssh</span> first for keyless, ACL-gated SSH to this node.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display:flex; align-items:center; justify-content:center; gap:8px; height:42px; border-radius:10px; background:linear-gradient(135deg,#ff8a7a,#ff5f57); color:#2a0d0a; font-size:13px; font-weight:700; cursor:pointer;" onClick="{{ toConnect }}">I've run it — waiting for connection →</div>
|
||||||
|
</div>
|
||||||
|
</sc-if>
|
||||||
|
|
||||||
|
<!-- STEP 2 CONNECT -->
|
||||||
|
<sc-if value="{{ step2 }}" hint-placeholder-val="{{ false }}">
|
||||||
|
<div style="animation:cm-fade .2s ease;">
|
||||||
|
<sc-if value="{{ connecting }}" hint-placeholder-val="{{ true }}">
|
||||||
|
<div style="display:flex; flex-direction:column; align-items:center; text-align:center; padding:24px 0;">
|
||||||
|
<div style="width:42px; height:42px; border:3px solid rgba(94,200,216,.25); border-top-color:#5ec8d8; border-radius:50%; animation:cm-spin .8s linear infinite; margin-bottom:16px;"></div>
|
||||||
|
<div style="font-size:14px; font-weight:700; margin-bottom:5px;">Waiting for the daemon to dial home…</div>
|
||||||
|
<div style="font-size:12px; color:#8a8a92; line-height:1.5; max-width:240px;">Outbound WSS connection · NAT-friendly · no inbound port opened.</div>
|
||||||
|
</div>
|
||||||
|
</sc-if>
|
||||||
|
<sc-if value="{{ connected }}" hint-placeholder-val="{{ false }}">
|
||||||
|
<div style="animation:cm-fade .2s ease;">
|
||||||
|
<div style="display:flex; align-items:center; gap:9px; margin-bottom:14px;"><span style="width:26px; height:26px; border-radius:50%; background:rgba(95,208,138,.16); display:flex; align-items:center; justify-content:center; color:#5fd08a; font-size:14px;">✓</span><div><div style="font-size:14px; font-weight:700;">Connected</div><div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#5fd08a;">daemon online · forge-02</div></div></div>
|
||||||
|
<div style="border-radius:11px; border:1px solid rgba(255,255,255,.08); background:#0d0d10; padding:13px 15px; margin-bottom:16px;">
|
||||||
|
<div style="font-family:'JetBrains Mono',monospace; font-size:9px; letter-spacing:.1em; color:#5a5a62; margin-bottom:10px;">HOST SPECS</div>
|
||||||
|
<div style="display:flex; flex-direction:column; gap:8px; font-family:'JetBrains Mono',monospace; font-size:11px;">
|
||||||
|
<div style="display:flex;"><span style="color:#6a6a72; width:78px;">hostname</span><span style="color:#cfcfd5;">forge-02</span></div>
|
||||||
|
<div style="display:flex;"><span style="color:#6a6a72; width:78px;">os</span><span style="color:#cfcfd5;">Ubuntu 24.04 · x86_64</span></div>
|
||||||
|
<div style="display:flex;"><span style="color:#6a6a72; width:78px;">cpu</span><span style="color:#cfcfd5;">16 vCPU · AMD EPYC</span></div>
|
||||||
|
<div style="display:flex;"><span style="color:#6a6a72; width:78px;">memory</span><span style="color:#cfcfd5;">64 GB</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex; align-items:center; justify-content:center; gap:8px; height:42px; border-radius:10px; background:linear-gradient(135deg,#ff8a7a,#ff5f57); color:#2a0d0a; font-size:13px; font-weight:700; cursor:pointer;" onClick="{{ toVerify }}">Verify the host →</div>
|
||||||
|
</div>
|
||||||
|
</sc-if>
|
||||||
|
</div>
|
||||||
|
</sc-if>
|
||||||
|
|
||||||
|
<!-- STEP 3 VERIFY -->
|
||||||
|
<sc-if value="{{ step3 }}" hint-placeholder-val="{{ false }}">
|
||||||
|
<div style="animation:cm-fade .2s ease;">
|
||||||
|
<div style="font-size:14px; font-weight:700; margin-bottom:4px;">Verify we can operate</div>
|
||||||
|
<div style="font-size:12px; color:#9a9aa2; line-height:1.55; margin-bottom:13px;">Built-in check confirms the daemon can run agents on this host.</div>
|
||||||
|
<div style="border-radius:10px; background:#070708; border:1px solid rgba(255,255,255,.08); padding:13px; font-family:'JetBrains Mono',monospace; font-size:11px; line-height:1.8; margin-bottom:16px;">
|
||||||
|
<div style="color:#5fd08a;">✓ uname -a</div>
|
||||||
|
<div style="color:#6a6a72; padding-left:14px;">Linux forge-02 6.8.0 x86_64 GNU/Linux</div>
|
||||||
|
<div style="color:#5fd08a; margin-top:4px;">✓ docker version</div>
|
||||||
|
<div style="color:#6a6a72; padding-left:14px;">Client/Server 27.1.1 · runtime ok</div>
|
||||||
|
<div style="color:#5fd08a; margin-top:4px;">✓ §15 sandbox profile applied</div>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex; align-items:center; justify-content:center; gap:8px; height:42px; border-radius:10px; background:linear-gradient(135deg,#7fe0a0,#5fd08a); color:#06281a; font-size:13px; font-weight:700; cursor:pointer;" onClick="{{ finishWizard }}">✓ Add forge-02 to fleet</div>
|
||||||
|
</div>
|
||||||
|
</sc-if>
|
||||||
|
</div>
|
||||||
|
</sc-if>
|
||||||
|
|
||||||
|
<!-- TAILSCALE CONNECT -->
|
||||||
|
<sc-if value="{{ showTailscale }}" hint-placeholder-val="{{ false }}">
|
||||||
|
<div style="animation:cm-fade .2s ease;">
|
||||||
|
<div style="display:flex; align-items:center; gap:10px; margin-bottom:14px;">
|
||||||
|
<div style="width:32px; height:32px; border-radius:8px; background:#fff; display:flex; align-items:center; justify-content:center;"><svg width="17" height="17" viewBox="0 0 24 24"><g fill="#141414"><circle cx="5" cy="5" r="2.1" opacity=".3"></circle><circle cx="12" cy="5" r="2.1" opacity=".3"></circle><circle cx="19" cy="5" r="2.1" opacity=".3"></circle><circle cx="5" cy="12" r="2.1"></circle><circle cx="12" cy="12" r="2.1"></circle><circle cx="19" cy="12" r="2.1"></circle><circle cx="5" cy="19" r="2.1" opacity=".3"></circle><circle cx="12" cy="19" r="2.1"></circle><circle cx="19" cy="19" r="2.1" opacity=".3"></circle></g></svg></div>
|
||||||
|
<div><div style="font-size:15px; font-weight:700;">Tailscale network</div><div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#5fd08a;">connected</div></div>
|
||||||
|
</div>
|
||||||
|
<div style="font-size:12px; color:#9a9aa2; line-height:1.55; margin-bottom:14px;">Connect your tailnet for live device status across the whole network — online, last-seen, IP, OS.</div>
|
||||||
|
|
||||||
|
<div style="margin-bottom:12px;">
|
||||||
|
<div style="font-family:'JetBrains Mono',monospace; font-size:9px; letter-spacing:.1em; color:#6a6a72; margin-bottom:6px;">TAILNET</div>
|
||||||
|
<div style="height:40px; border-radius:9px; border:1px solid rgba(255,255,255,.1); background:#0d0d10; display:flex; align-items:center; padding:0 12px; font-family:'JetBrains Mono',monospace; font-size:12px; color:#cfcfd5;">{{ tailnet }}</div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-bottom:16px;">
|
||||||
|
<div style="font-family:'JetBrains Mono',monospace; font-size:9px; letter-spacing:.1em; color:#6a6a72; margin-bottom:6px;">API KEY</div>
|
||||||
|
<div style="height:40px; border-radius:9px; border:1px solid rgba(255,255,255,.1); background:#0d0d10; display:flex; align-items:center; padding:0 12px; font-family:'JetBrains Mono',monospace; font-size:12px; color:#6a6a72;">tskey-api-••••••••••••3f9</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="border-radius:11px; border:1px solid rgba(255,255,255,.07); background:#0d0d10; overflow:hidden;">
|
||||||
|
<div style="font-family:'JetBrains Mono',monospace; font-size:9px; letter-spacing:.1em; color:#5a5a62; padding:10px 13px; border-bottom:1px solid rgba(255,255,255,.05);">{{ tsDeviceCount }} DEVICES</div>
|
||||||
|
<sc-for list="{{ tsDevices }}" as="d" hint-placeholder-count="4">
|
||||||
|
<div style="display:flex; align-items:center; gap:9px; padding:9px 13px; border-bottom:1px solid rgba(255,255,255,.03);">
|
||||||
|
<span style="width:6px; height:6px; border-radius:50%; background:{{ d.dot }};"></span>
|
||||||
|
<span style="font-size:12px; font-weight:600; color:#eaeaee;">{{ d.name }}</span>
|
||||||
|
<span style="flex:1;"></span>
|
||||||
|
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#5ec8d8;">{{ d.ip }}</span>
|
||||||
|
</div>
|
||||||
|
</sc-for>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</sc-if>
|
||||||
|
|
||||||
|
<!-- CLOUD MENU (provider quick-connect) -->
|
||||||
|
<sc-if value="{{ showCloudMenu }}" hint-placeholder-val="{{ false }}">
|
||||||
|
<div style="animation:cm-fade .2s ease;">
|
||||||
|
<div style="font-size:12px; color:#9a9aa2; line-height:1.5; margin-bottom:14px;">Pick a provider to connect. Credentials are held by the secret broker and never reach agent code.</div>
|
||||||
|
<div style="display:flex; flex-direction:column; gap:8px;">
|
||||||
|
<sc-for list="{{ cloudProviders }}" as="p" hint-placeholder-count="5">
|
||||||
|
<div style="display:flex; align-items:center; gap:11px; padding:11px 12px; border-radius:11px; border:1px solid {{ p.cardBorder }}; background:#0d0d10; cursor:pointer;" onClick="{{ p.onConnect }}">
|
||||||
|
<div style="width:32px; height:32px; flex:none; border-radius:8px; background:{{ p.tile }}; display:flex; align-items:center; justify-content:center; font-weight:700; color:{{ p.ink }}; font-size:11px;">
|
||||||
|
<sc-if value="{{ p.isText }}" hint-placeholder-val="{{ true }}">{{ p.short }}</sc-if>
|
||||||
|
<sc-if value="{{ p.isApple }}" hint-placeholder-val="{{ false }}"><svg width="16" height="16" viewBox="0 0 24 24" fill="none"><rect x="6" y="6" width="12" height="12" rx="2.5" stroke="#1a1a1d" stroke-width="1.8"></rect><rect x="9.5" y="9.5" width="5" height="5" rx="1" fill="#1a1a1d"></rect></svg></sc-if>
|
||||||
|
</div>
|
||||||
|
<div style="flex:1; min-width:0;">
|
||||||
|
<div style="font-size:13px; font-weight:600; color:#eaeaee;">{{ p.name }}</div>
|
||||||
|
<div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72;">{{ p.offers }}</div>
|
||||||
|
</div>
|
||||||
|
<span style="font-family:'JetBrains Mono',monospace; font-size:9px; padding:3px 8px; border-radius:6px; {{ p.statusStyle }}">{{ p.statusLabel }}</span>
|
||||||
|
</div>
|
||||||
|
</sc-for>
|
||||||
|
</div>
|
||||||
|
<div style="margin-top:16px; display:flex; gap:9px; padding:11px 12px; border-radius:10px; background:rgba(94,200,216,.05); border:1px solid rgba(94,200,216,.18);">
|
||||||
|
<span style="color:#5ec8d8; flex:none;">ⓘ</span>
|
||||||
|
<div style="font-size:11px; color:#a8a8b0; line-height:1.5;">Prefer your own hardware? <span style="color:#5ec8d8; cursor:pointer;" onClick="{{ goLocal }}">Local & Tailscale</span> has zero inbound exposure.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</sc-if>
|
||||||
|
|
||||||
|
<!-- CLOUD CONNECT WIZARD -->
|
||||||
|
<sc-if value="{{ showCloudConnect }}" hint-placeholder-val="{{ false }}">
|
||||||
|
<div style="animation:cm-fade .2s ease;">
|
||||||
|
<div style="display:flex; align-items:center; gap:10px; margin-bottom:16px;">
|
||||||
|
<div style="width:36px; height:36px; border-radius:9px; background:{{ selProvTile }}; display:flex; align-items:center; justify-content:center; font-weight:700; color:{{ selProvInk }}; font-size:12px;">
|
||||||
|
<sc-if value="{{ selProvIsText }}" hint-placeholder-val="{{ true }}">{{ selProvShort }}</sc-if>
|
||||||
|
<sc-if value="{{ selProvIsApple }}" hint-placeholder-val="{{ false }}"><svg width="18" height="18" viewBox="0 0 24 24" fill="none"><rect x="6" y="6" width="12" height="12" rx="2.5" stroke="#1a1a1d" stroke-width="1.8"></rect><rect x="9.5" y="9.5" width="5" height="5" rx="1" fill="#1a1a1d"></rect></svg></sc-if>
|
||||||
|
</div>
|
||||||
|
<div><div style="font-size:15px; font-weight:700;">{{ selProvName }}</div><div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72;">connect provider</div></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- creds -->
|
||||||
|
<sc-if value="{{ phaseCreds }}" hint-placeholder-val="{{ true }}">
|
||||||
|
<div style="animation:cm-fade .2s ease;">
|
||||||
|
<div style="display:flex; gap:9px; padding:10px 12px; border-radius:9px; background:rgba(95,208,138,.05); border:1px solid rgba(95,208,138,.2); margin-bottom:16px;">
|
||||||
|
<span style="width:14px; flex:none;"><svg width="14" height="14" viewBox="0 0 20 20"><path d="M10 2l5 2v4c0 4-2.5 6-5 7-2.5-1-5-3-5-7V4z" fill="none" stroke="#5fd08a" stroke-width="1.4" stroke-linejoin="round"></path></svg></span>
|
||||||
|
<div style="font-size:11px; color:#a8a8b0; line-height:1.5;">Stored in the <span style="color:#cfcfd5; font-weight:600;">secret broker</span> over a private socket. Never enters agent code or any sandbox.</div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-bottom:12px;">
|
||||||
|
<div style="font-family:'JetBrains Mono',monospace; font-size:9px; letter-spacing:.1em; color:#6a6a72; margin-bottom:6px;">ACCESS KEY ID</div>
|
||||||
|
<div style="height:40px; border-radius:9px; border:1px solid rgba(255,255,255,.1); background:#0d0d10; display:flex; align-items:center; padding:0 12px; font-family:'JetBrains Mono',monospace; font-size:12px; color:#cfcfd5;">AKIA••••••••••7Q</div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-bottom:12px;">
|
||||||
|
<div style="font-family:'JetBrains Mono',monospace; font-size:9px; letter-spacing:.1em; color:#6a6a72; margin-bottom:6px;">SECRET ACCESS KEY</div>
|
||||||
|
<div style="height:40px; border-radius:9px; border:1px solid rgba(255,255,255,.1); background:#0d0d10; display:flex; align-items:center; padding:0 12px; font-family:'JetBrains Mono',monospace; font-size:12px; color:#6a6a72;">••••••••••••••••••••</div>
|
||||||
|
</div>
|
||||||
|
<div style="margin-bottom:18px;">
|
||||||
|
<div style="font-family:'JetBrains Mono',monospace; font-size:9px; letter-spacing:.1em; color:#6a6a72; margin-bottom:6px;">SCOPE</div>
|
||||||
|
<div style="display:flex; flex-wrap:wrap; gap:6px;">
|
||||||
|
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#cfcfd5; padding:4px 9px; border-radius:6px; background:rgba(255,255,255,.04);">compute:provision</span>
|
||||||
|
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#cfcfd5; padding:4px 9px; border-radius:6px; background:rgba(255,255,255,.04);">compute:terminate</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex; align-items:center; justify-content:center; gap:8px; height:42px; border-radius:10px; background:linear-gradient(135deg,#ff8a7a,#ff5f57); color:#2a0d0a; font-size:13px; font-weight:700; cursor:pointer;" onClick="{{ toConfig }}">Store & continue →</div>
|
||||||
|
</div>
|
||||||
|
</sc-if>
|
||||||
|
|
||||||
|
<!-- config -->
|
||||||
|
<sc-if value="{{ phaseConfig }}" hint-placeholder-val="{{ false }}">
|
||||||
|
<div style="animation:cm-fade .2s ease;">
|
||||||
|
<div style="font-size:13px; font-weight:700; margin-bottom:11px;">Region</div>
|
||||||
|
<div style="display:flex; flex-wrap:wrap; gap:7px; margin-bottom:18px;">
|
||||||
|
<sc-for list="{{ regions }}" as="rg" hint-placeholder-count="3">
|
||||||
|
<span style="font-family:'JetBrains Mono',monospace; font-size:11px; padding:7px 12px; border-radius:8px; cursor:pointer; {{ rg.style }}" onClick="{{ rg.onPick }}">{{ rg.label }}</span>
|
||||||
|
</sc-for>
|
||||||
|
</div>
|
||||||
|
<div style="font-size:13px; font-weight:700; margin-bottom:11px;">Instance size</div>
|
||||||
|
<div style="display:flex; flex-wrap:wrap; gap:7px; margin-bottom:18px;">
|
||||||
|
<sc-for list="{{ sizes }}" as="sz" hint-placeholder-count="3">
|
||||||
|
<span style="font-family:'JetBrains Mono',monospace; font-size:11px; padding:7px 12px; border-radius:8px; cursor:pointer; {{ sz.style }}" onClick="{{ sz.onPick }}">{{ sz.label }}</span>
|
||||||
|
</sc-for>
|
||||||
|
</div>
|
||||||
|
<div style="border-radius:10px; border:1px solid rgba(255,255,255,.07); background:#0d0d10; padding:12px 14px; margin-bottom:16px; font-family:'JetBrains Mono',monospace; font-size:10px; color:#8a8a92; line-height:1.7;">
|
||||||
|
image: <span style="color:#5fd08a;">clawmates-node baked</span><br>boot: <span style="color:#5ec8d8;">dials home · outbound WSS</span><br>sandbox: <span style="color:#6fd0c0;">§15 applied on boot</span>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex; gap:8px;">
|
||||||
|
<div style="flex:none; width:44px; display:flex; align-items:center; justify-content:center; height:42px; border-radius:10px; border:1px solid rgba(255,255,255,.12); color:#9a9aa2; cursor:pointer;" onClick="{{ backToCreds }}">←</div>
|
||||||
|
<div style="flex:1; display:flex; align-items:center; justify-content:center; gap:8px; height:42px; border-radius:10px; background:linear-gradient(135deg,#ff8a7a,#ff5f57); color:#2a0d0a; font-size:13px; font-weight:700; cursor:pointer;" onClick="{{ provisionRunner }}">Provision runner →</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</sc-if>
|
||||||
|
|
||||||
|
<!-- provisioning -->
|
||||||
|
<sc-if value="{{ provisioning }}" hint-placeholder-val="{{ false }}">
|
||||||
|
<div style="display:flex; flex-direction:column; align-items:center; text-align:center; padding:24px 0; animation:cm-fade .2s ease;">
|
||||||
|
<div style="width:42px; height:42px; border:3px solid rgba(232,196,106,.25); border-top-color:#e8b465; border-radius:50%; animation:cm-spin .8s linear infinite; margin-bottom:16px;"></div>
|
||||||
|
<div style="font-size:14px; font-weight:700; margin-bottom:5px;">Provisioning runner…</div>
|
||||||
|
<div style="font-size:12px; color:#8a8a92; line-height:1.6; max-width:250px;">launching instance · baking daemon · dialing home over WSS</div>
|
||||||
|
</div>
|
||||||
|
</sc-if>
|
||||||
|
|
||||||
|
<!-- provisioned -->
|
||||||
|
<sc-if value="{{ provisioned }}" hint-placeholder-val="{{ false }}">
|
||||||
|
<div style="animation:cm-fade .2s ease;">
|
||||||
|
<div style="display:flex; align-items:center; gap:9px; margin-bottom:14px;"><span style="width:26px; height:26px; border-radius:50%; background:rgba(95,208,138,.16); display:flex; align-items:center; justify-content:center; color:#5fd08a; font-size:14px;">✓</span><div><div style="font-size:14px; font-weight:700;">Runner online</div><div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#5fd08a;">joined fleet · dialing home ok</div></div></div>
|
||||||
|
<div style="border-radius:11px; border:1px solid rgba(255,255,255,.08); background:#0d0d10; padding:13px 15px; margin-bottom:16px; font-family:'JetBrains Mono',monospace; font-size:11px; line-height:1.8;">
|
||||||
|
<div style="display:flex;"><span style="color:#6a6a72; width:70px;">instance</span><span style="color:#cfcfd5;">{{ size }}</span></div>
|
||||||
|
<div style="display:flex;"><span style="color:#6a6a72; width:70px;">region</span><span style="color:#cfcfd5;">{{ region }}</span></div>
|
||||||
|
<div style="display:flex;"><span style="color:#6a6a72; width:70px;">daemon</span><span style="color:#5fd08a;">online · §15</span></div>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex; align-items:center; justify-content:center; gap:8px; height:42px; border-radius:10px; background:linear-gradient(135deg,#7fe0a0,#5fd08a); color:#06281a; font-size:13px; font-weight:700; cursor:pointer;" onClick="{{ finishCloud }}">✓ Done</div>
|
||||||
|
</div>
|
||||||
|
</sc-if>
|
||||||
|
</div>
|
||||||
|
</sc-if>
|
||||||
|
|
||||||
|
</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;">● daemon control plane ok</span>
|
||||||
|
<span>tailnet {{ tailnet }}</span>
|
||||||
|
<span style="flex:1;"></span>
|
||||||
|
<span>outbound WSS · no inbound port</span>
|
||||||
|
<span style="color:#5ec8d8;">§15 sandbox-ready</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-dc>
|
||||||
|
<script type="text/x-dc" data-dc-script>
|
||||||
|
class Component extends DCLogic {
|
||||||
|
state = { nav: 'local', addMode: 'menu', step: 1, connecting: false, connected: false, copied: false, tsConnected: true,
|
||||||
|
cloudProvider: 'gcp', cloudPhase: 'creds', provisioning: false, provisioned: false, region: 'us-east-1', size: 'c7g.2xlarge' };
|
||||||
|
|
||||||
|
componentWillUnmount() { if (this._t) clearTimeout(this._t); if (this._t2) clearTimeout(this._t2); }
|
||||||
|
|
||||||
|
_enterConnecting() {
|
||||||
|
this.setState({ step: 2, connecting: true, connected: false });
|
||||||
|
if (this._t) clearTimeout(this._t);
|
||||||
|
this._t = setTimeout(() => this.setState({ connecting: false, connected: true }), 1800);
|
||||||
|
}
|
||||||
|
_enterProvisioning() {
|
||||||
|
this.setState({ cloudPhase: 'provisioning', provisioning: true, provisioned: false });
|
||||||
|
if (this._t2) clearTimeout(this._t2);
|
||||||
|
this._t2 = setTimeout(() => this.setState({ provisioning: false, provisioned: true }), 2200);
|
||||||
|
}
|
||||||
|
|
||||||
|
renderVals() {
|
||||||
|
const s = this.state;
|
||||||
|
const hosts = [
|
||||||
|
{ id:'forge', name:'forge-01', os:'Ubuntu 24.04 · x86_64', kind:'Linux', status:'online', cpu:34, ram:61, mem:'normal', memColor:'#5fd08a', disk:48, load:'1.24', containers:6, tsip:'100.92.14.3', spark:'0,13 8,9 16,11 24,5 32,8 40,4 48,7' },
|
||||||
|
{ id:'studio', name:'mac-studio', os:'macOS 15.3 · arm64', kind:'macOS', status:'online', cpu:12, ram:38, mem:'normal', memColor:'#5fd08a', disk:22, load:'0.86', containers:2, tsip:'100.92.14.7', spark:'0,12 8,13 16,10 24,11 32,7 40,9 48,8' },
|
||||||
|
{ id:'edge', name:'edge-rpi5', os:'Debian 12 · arm64', kind:'Linux', status:'pairing', cpu:0, ram:0, mem:'—', memColor:'#5a5a62', disk:0, load:'—', containers:0, tsip:'pending', spark:'0,8 8,8 16,8 24,8 32,8 40,8 48,8' },
|
||||||
|
];
|
||||||
|
const meter = (v, col) => ({ w: Math.max(3, v) + '%', col });
|
||||||
|
const cpuCol = v => v > 75 ? '#ff6f61' : v > 45 ? '#e8b465' : '#5ec8d8';
|
||||||
|
const ramCol = v => v > 80 ? '#ff6f61' : v > 55 ? '#e8b465' : '#5fd08a';
|
||||||
|
const hostCards = hosts.map(h => ({
|
||||||
|
...h,
|
||||||
|
online: h.status === 'online',
|
||||||
|
pairing: h.status === 'pairing',
|
||||||
|
statusColor: h.status === 'online' ? '#5fd08a' : h.status === 'pairing' ? '#e8b465' : '#5a5a62',
|
||||||
|
statusLabel: h.status === 'online' ? 'online' : h.status === 'pairing' ? 'pairing…' : 'offline',
|
||||||
|
accentBorder: h.status === 'online' ? 'rgba(95,208,138,.18)' : h.status === 'pairing' ? 'rgba(232,196,106,.3)' : 'rgba(255,255,255,.07)',
|
||||||
|
cpuW: Math.max(3, h.cpu) + '%', cpuCol: cpuCol(h.cpu),
|
||||||
|
ramW: Math.max(3, h.ram) + '%', ramCol: ramCol(h.ram),
|
||||||
|
diskW: Math.max(3, h.disk) + '%',
|
||||||
|
sshTarget: 'ssh ' + h.tsip,
|
||||||
|
hasSsh: h.status === 'online',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const tsDevices = [
|
||||||
|
{ name:'forge-01', ip:'100.92.14.3', os:'linux', status:'online', seen:'now', dot:'#5fd08a' },
|
||||||
|
{ name:'mac-studio', ip:'100.92.14.7', os:'macOS', status:'online', seen:'now', dot:'#5fd08a' },
|
||||||
|
{ name:'edge-rpi5', ip:'100.92.14.9', os:'linux', status:'online', seen:'2m', dot:'#5fd08a' },
|
||||||
|
{ name:'ops-laptop', ip:'100.92.14.21', os:'macOS', status:'offline', seen:'3h', dot:'#5a5a62' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// ---- cloud ----
|
||||||
|
const inCloud = s.nav === 'cloud';
|
||||||
|
const provData = [
|
||||||
|
{ id:'aws', name:'Amazon Web Services', short:'aws', tile:'linear-gradient(135deg,#ff9a3c,#ff7314)', ink:'#241200', offers:'EC2 · Fargate · Graviton', regions:'31 regions', connected:true, runners:2, icon:'text' },
|
||||||
|
{ id:'gcp', name:'Google Cloud', short:'GCP', tile:'linear-gradient(135deg,#5b9bf0,#3b6fd4)', ink:'#031027', offers:'Compute Engine · Cloud Run', regions:'40 regions', connected:false, runners:0, icon:'text' },
|
||||||
|
{ id:'apple', name:'Apple silicon', short:'', tile:'linear-gradient(135deg,#ececed,#b6b6be)', ink:'#1a1a1d', offers:'Mac instances · M-series', regions:'macOS / iOS builds', connected:false, runners:0, icon:'chip' },
|
||||||
|
{ id:'hetzner', name:'Hetzner', short:'HZ', tile:'linear-gradient(135deg,#e44a4a,#c5102d)', ink:'#2a0207', offers:'Cloud VMs · dedicated', regions:'EU · US · low cost', connected:false, runners:0, icon:'text' },
|
||||||
|
{ id:'do', name:'DigitalOcean', short:'DO', tile:'linear-gradient(135deg,#3aa0ff,#0069ff)', ink:'#021428', offers:'Droplets · App Platform · K8s', regions:'15 datacenters', connected:false, runners:0, icon:'text' },
|
||||||
|
];
|
||||||
|
const cloudProviders = provData.map(p => ({
|
||||||
|
...p,
|
||||||
|
isApple: p.icon === 'chip',
|
||||||
|
isText: p.icon === 'text',
|
||||||
|
statusLabel: p.connected ? '● connected' : 'Connect →',
|
||||||
|
statusStyle: p.connected
|
||||||
|
? 'color:#5fd08a; background:rgba(95,208,138,.08); border:1px solid rgba(95,208,138,.25);'
|
||||||
|
: 'color:#ff8a7a; background:rgba(255,111,97,.08); border:1px solid rgba(255,111,97,.28);',
|
||||||
|
cardBorder: p.connected ? 'rgba(95,208,138,.2)' : 'rgba(255,255,255,.08)',
|
||||||
|
onConnect: () => this.setState({ nav:'cloud', addMode:'cloud-connect', cloudProvider:p.id, cloudPhase:'creds', provisioning:false, provisioned:false }),
|
||||||
|
}));
|
||||||
|
const selProv = cloudProviders.find(p => p.id === s.cloudProvider) || cloudProviders[0];
|
||||||
|
const cloudRunners = [
|
||||||
|
{ name:'aws-ec2 · c7g.2xlarge', region:'us-east-1', status:'online', cpu:52, ram:44, agents:3, cost:'$0.29/hr', cpuW:'52%', ramW:'44%' },
|
||||||
|
{ name:'aws-ec2 · c7g.xlarge', region:'eu-west-1', status:'online', cpu:28, ram:36, agents:2, cost:'$0.15/hr', cpuW:'28%', ramW:'36%' },
|
||||||
|
];
|
||||||
|
const selChip = 'background:rgba(255,111,97,.14); border:1px solid rgba(255,111,97,.4); color:#ff8a7a;';
|
||||||
|
const idleChip = 'background:#0d0d10; border:1px solid rgba(255,255,255,.1); color:#9a9aa2;';
|
||||||
|
const regions = ['us-east-1','eu-west-1','ap-south-1'].map(r => ({ label:r, style: s.region===r?selChip:idleChip, onPick:()=>this.setState({region:r}) }));
|
||||||
|
const sizes = ['c7g.xlarge','c7g.2xlarge','c7g.4xlarge'].map(z => ({ label:z, style: s.size===z?selChip:idleChip, onPick:()=>this.setState({size:z}) }));
|
||||||
|
|
||||||
|
const navActive = 'background:rgba(255,111,97,.1);';
|
||||||
|
const navIdle = '';
|
||||||
|
return {
|
||||||
|
// cloud
|
||||||
|
cloudProviders, cloudRunners, regions, sizes,
|
||||||
|
selProvName: selProv?selProv.name:'', selProvTile: selProv?selProv.tile:'', selProvShort: selProv?selProv.short:'', selProvInk: selProv?selProv.ink:'', selProvIsApple: selProv?selProv.isApple:false, selProvIsText: selProv?selProv.isText:true,
|
||||||
|
phaseCreds: s.cloudPhase==='creds', phaseConfig: s.cloudPhase==='config', provisioning: s.provisioning, provisioned: s.provisioned,
|
||||||
|
toConfig: () => this.setState({ cloudPhase:'config' }),
|
||||||
|
backToCreds: () => this.setState({ cloudPhase:'creds' }),
|
||||||
|
provisionRunner: () => this._enterProvisioning(),
|
||||||
|
finishCloud: () => this.setState({ addMode:'menu', provisioning:false, provisioned:false }),
|
||||||
|
region: s.region, size: s.size,
|
||||||
|
showCloudMenu: inCloud && s.addMode!=='cloud-connect',
|
||||||
|
showCloudConnect: inCloud && s.addMode==='cloud-connect',
|
||||||
|
// nav
|
||||||
|
isLocal: s.nav==='local', isCloud: s.nav==='cloud',
|
||||||
|
navLocalStyle: s.nav==='local'?navActive:navIdle, navLocalIcon: s.nav==='local'?'#ff8a7a':'#8a8a92', navLocalText: s.nav==='local'?'#fff':'#cfcfd5',
|
||||||
|
navCloudStyle: s.nav==='cloud'?navActive:navIdle, navCloudIcon: s.nav==='cloud'?'#ff8a7a':'#6a6a72', navCloudText: s.nav==='cloud'?'#fff':'#9a9aa2',
|
||||||
|
goLocal: () => this.setState({ nav:'local', addMode:'menu' }),
|
||||||
|
goCloud: () => this.setState({ nav:'cloud', addMode:'menu' }),
|
||||||
|
// fleet
|
||||||
|
fleetTotal: 3, fleetOnline: 2, vcpu: 28, ram: '96 GB', containersRunning: 8,
|
||||||
|
hostCards, tsDevices, tsDeviceCount: tsDevices.length, tailnet: 'acme-org.ts.net',
|
||||||
|
tsConnected: s.tsConnected,
|
||||||
|
// right panel modes
|
||||||
|
showMenu: !inCloud && s.addMode==='menu', showWizard: !inCloud && s.addMode==='host-wizard', showTailscale: !inCloud && s.addMode==='tailscale',
|
||||||
|
openHostWizard: () => this.setState({ addMode:'host-wizard', step:1, connecting:false, connected:false }),
|
||||||
|
openTailscale: () => this.setState({ addMode:'tailscale' }),
|
||||||
|
backToMenu: () => this.setState({ addMode:'menu' }),
|
||||||
|
// wizard
|
||||||
|
step1: s.step===1, step2: s.step===2, step3: s.step===3,
|
||||||
|
stepIs1Style: this._stepDot(s.step,1), stepIs2Style: this._stepDot(s.step,2), stepIs3Style: this._stepDot(s.step,3),
|
||||||
|
connecting: s.connecting, connected: s.connected,
|
||||||
|
copied: s.copied,
|
||||||
|
copyToken: () => { this.setState({ copied:true }); setTimeout(()=>this.setState({copied:false}), 1400); },
|
||||||
|
copyLabel: s.copied ? 'copied ✓' : 'copy',
|
||||||
|
toInstall: () => this.setState({ step:1 }),
|
||||||
|
toConnect: () => this._enterConnecting(),
|
||||||
|
toVerify: () => this.setState({ step:3 }),
|
||||||
|
finishWizard: () => this.setState({ addMode:'menu', step:1 }),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
_stepDot(cur, n) {
|
||||||
|
if (cur === n) return 'background:#ff6f61; color:#2a0d0a;';
|
||||||
|
if (cur > n) return 'background:rgba(95,208,138,.2); color:#5fd08a; border:1px solid rgba(95,208,138,.4);';
|
||||||
|
return 'background:#141417; color:#6a6a72; border:1px solid rgba(255,255,255,.1);';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,344 @@
|
|||||||
|
<!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: .3; } }
|
||||||
|
@keyframes cm-halo { 0% { transform: scale(.7); opacity: .5; } 100% { transform: scale(1.9); opacity: 0; } }
|
||||||
|
@keyframes cm-drift { 0%,100% { transform: translateY(0); } 50% { transform: translateY(-6px); } }
|
||||||
|
</style>
|
||||||
|
</helmet>
|
||||||
|
|
||||||
|
<div style="background:#08080a; color:#f3f3f5; min-height:100vh; overflow-x:hidden;">
|
||||||
|
|
||||||
|
<!-- NAV -->
|
||||||
|
<div style="position:sticky; top:0; z-index:50; display:flex; align-items:center; gap:14px; padding:16px 32px; border-bottom:1px solid rgba(255,255,255,.06); background:rgba(8,8,10,.72); backdrop-filter:blur(14px);">
|
||||||
|
<div style="display:flex; align-items:center; gap:10px;">
|
||||||
|
<svg width="24" height="24" 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:17px; font-weight:700; letter-spacing:-.01em;">Clawmates</span>
|
||||||
|
</div>
|
||||||
|
<div style="flex:1;"></div>
|
||||||
|
<div style="display:flex; align-items:center; gap:28px; font-size:14px; color:#b5b5bd;">
|
||||||
|
<span style="cursor:pointer;">Platform</span>
|
||||||
|
<span style="cursor:pointer;">Topologies</span>
|
||||||
|
<span style="cursor:pointer;">Safety</span>
|
||||||
|
<span style="cursor:pointer;">Docs</span>
|
||||||
|
</div>
|
||||||
|
<div style="width:1px; height:22px; background:rgba(255,255,255,.1); margin:0 22px;"></div>
|
||||||
|
<span style="font-size:14px; color:#b5b5bd; cursor:pointer;">Sign in</span>
|
||||||
|
<div style="display:flex; align-items:center; gap:7px; padding:9px 16px; border-radius:9px; background:linear-gradient(135deg,#ff8a7a,#ff5f57); color:#2a0d0a; font-size:14px; font-weight:700; cursor:pointer;" style-hover="filter:brightness(1.07);">Get started</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- HERO -->
|
||||||
|
<div style="position:relative; max-width:1240px; margin:0 auto; padding:88px 32px 80px; display:grid; grid-template-columns:1.05fr .95fr; gap:48px; align-items:center;">
|
||||||
|
<div style="position:absolute; top:-40px; left:30%; width:520px; height:420px; background:radial-gradient(circle, rgba(255,111,97,.13), transparent 68%); pointer-events:none;"></div>
|
||||||
|
|
||||||
|
<div style="position:relative;">
|
||||||
|
<div style="display:inline-flex; align-items:center; gap:8px; font-family:'JetBrains Mono',monospace; font-size:11px; letter-spacing:.14em; color:#ff8a7a; padding:6px 11px; border-radius:7px; border:1px solid rgba(255,111,97,.25); background:rgba(255,111,97,.06); margin-bottom:26px;">
|
||||||
|
<span style="width:6px; height:6px; border-radius:50%; background:#ff6f61; animation:cm-blink 1.6s infinite;"></span>MULTI-AGENT PLATFORM
|
||||||
|
</div>
|
||||||
|
<h1 style="font-size:62px; line-height:1.02; font-weight:700; letter-spacing:-.035em; margin:0 0 22px; text-wrap:balance;">Deploy agents at<br>any scale.</h1>
|
||||||
|
<p style="font-size:18px; line-height:1.6; color:#a8a8b0; max-width:480px; margin:0 0 32px; text-wrap:pretty;">Every unit of work is a <span style="color:#f3f3f5; font-weight:600;">topology</span> — a graph of role-slots bound to real AI agents. Compose and run agentic systems from a single claw up to a whole org, on a durable, crash-resumable runner.</p>
|
||||||
|
<div style="display:flex; gap:12px; margin-bottom:30px;">
|
||||||
|
<div style="display:flex; align-items:center; gap:8px; padding:13px 22px; border-radius:10px; background:linear-gradient(135deg,#ff8a7a,#ff5f57); color:#2a0d0a; font-size:15px; font-weight:700; cursor:pointer;" style-hover="filter:brightness(1.07);">Start deploying →</div>
|
||||||
|
<div style="display:flex; align-items:center; gap:8px; padding:13px 22px; border-radius:10px; border:1px solid rgba(255,255,255,.14); color:#e6e6ea; font-size:15px; font-weight:600; cursor:pointer;" style-hover="background:rgba(255,255,255,.04);">Read the paper</div>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex; flex-wrap:wrap; gap:8px 18px; font-family:'JetBrains Mono',monospace; font-size:11px; color:#6a6a72;">
|
||||||
|
<span style="color:#5ec8d8;">12 topologies</span><span>·</span><span>durable runner</span><span>·</span><span style="color:#5fd08a;">§15-safe</span><span>·</span><span>self-hostable</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- hero constellation panel -->
|
||||||
|
<div style="position:relative; height:420px; border-radius:16px; border:1px solid rgba(255,255,255,.07); background:radial-gradient(120% 100% at 60% 30%, #0f0f14, #0a0a0c); overflow:hidden; box-shadow:0 30px 80px rgba(0,0,0,.4);">
|
||||||
|
<div style="position:absolute; top:14px; left:16px; font-family:'JetBrains Mono',monospace; font-size:10px; letter-spacing:.1em; color:#5a5a62;">GROWTH TEAM · hub-spoke · running</div>
|
||||||
|
<svg viewBox="0 0 100 100" preserveAspectRatio="none" style="position:absolute; inset:0; width:100%; height:100%;">
|
||||||
|
<path d="M50,52 C40,38 34,32 24,26" fill="none" stroke="rgba(94,200,216,.5)" stroke-width="1.4" stroke-dasharray="3 4" vector-effect="non-scaling-stroke" style="animation:cm-flow 1.2s linear infinite;"></path>
|
||||||
|
<path d="M50,52 C40,64 34,70 26,76" fill="none" stroke="rgba(94,200,216,.5)" stroke-width="1.4" stroke-dasharray="3 4" vector-effect="non-scaling-stroke" style="animation:cm-flow 1.4s linear infinite;"></path>
|
||||||
|
<path d="M50,52 C52,38 53,28 54,18" fill="none" stroke="rgba(255,255,255,.12)" stroke-width="1.3" vector-effect="non-scaling-stroke"></path>
|
||||||
|
<path d="M50,52 C64,46 72,40 80,32" fill="none" stroke="rgba(255,255,255,.12)" stroke-width="1.3" vector-effect="non-scaling-stroke"></path>
|
||||||
|
<path d="M50,52 C64,58 72,66 80,74" fill="none" stroke="rgba(255,111,97,.6)" stroke-width="1.6" stroke-dasharray="3 4" vector-effect="non-scaling-stroke" style="animation:cm-flow .9s linear infinite;"></path>
|
||||||
|
</svg>
|
||||||
|
<div style="position:absolute; left:50%; top:52%; transform:translate(-50%,-50%); display:flex; flex-direction:column; align-items:center; gap:7px;">
|
||||||
|
<div style="width:58px; height:58px; border-radius:50%; background:linear-gradient(135deg,#ff9a6a,#ff6f4a); display:flex; align-items:center; justify-content:center; font-size:21px; font-weight:700; color:#2a0d05; box-shadow:0 0 38px rgba(255,111,97,.5);">A</div>
|
||||||
|
<span style="font-size:11px; font-weight:600;">Atlas</span>
|
||||||
|
</div>
|
||||||
|
<div style="position:absolute; left:24%; top:26%; transform:translate(-50%,-50%); display:flex; flex-direction:column; align-items:center; gap:6px;"><div style="position:relative; width:40px; height:40px;"><div style="position:absolute; inset:0; border-radius:50%; background:rgba(94,200,216,.4); animation:cm-halo 1.9s ease-out infinite;"></div><div style="position:relative; width:40px; height:40px; border-radius:50%; background:linear-gradient(135deg,#6fd0c0,#4aa3b8); display:flex; align-items:center; justify-content:center; font-size:14px; font-weight:700; color:#06201f;">I</div></div><span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#5ec8d8;">research…</span></div>
|
||||||
|
<div style="position:absolute; left:54%; top:18%; transform:translate(-50%,-50%); width:38px; height:38px; border-radius:50%; background:linear-gradient(135deg,#e8c46a,#d89a3a); display:flex; align-items:center; justify-content:center; font-size:13px; font-weight:700; color:#2a1d05;">N</div>
|
||||||
|
<div style="position:absolute; left:80%; top:32%; transform:translate(-50%,-50%); width:38px; height:38px; border-radius:50%; background:linear-gradient(135deg,#c98af0,#9a5ad8); display:flex; align-items:center; justify-content:center; font-size:13px; font-weight:700; color:#1a0a2a; opacity:.7;">S</div>
|
||||||
|
<div style="position:absolute; left:26%; top:76%; transform:translate(-50%,-50%); width:38px; height:38px; border-radius:50%; background:linear-gradient(135deg,#8a9af0,#5a6ad8); display:flex; align-items:center; justify-content:center; font-size:13px; font-weight:700; color:#0a0e2a;">E</div>
|
||||||
|
<div style="position:absolute; left:80%; top:74%; transform:translate(-50%,-50%); display:flex; flex-direction:column; align-items:center; gap:6px;"><div style="position:relative; width:44px; height:44px;"><div style="position:absolute; inset:-5px; border-radius:50%; border:2px solid #ff6f61;"></div><div style="position:relative; width:44px; height:44px; border-radius:50%; background:linear-gradient(135deg,#ff8a7a,#ff5f57); display:flex; align-items:center; justify-content:center; font-size:15px; font-weight:700; color:#2a0d0a;">M</div></div><span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#ff8a7a;">Morpheus</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- DEPLOY LADDER -->
|
||||||
|
<div style="max-width:1240px; margin:0 auto; padding:64px 32px;">
|
||||||
|
<div style="text-align:center; margin-bottom:48px;">
|
||||||
|
<div style="font-family:'JetBrains Mono',monospace; font-size:11px; letter-spacing:.16em; color:#5ec8d8; margin-bottom:14px;">THE DEPLOY LADDER</div>
|
||||||
|
<h2 style="font-size:40px; font-weight:700; letter-spacing:-.03em; margin:0 0 14px;">One model, four scales.</h2>
|
||||||
|
<p style="font-size:17px; color:#a8a8b0; max-width:580px; margin:0 auto; line-height:1.55;">Each rung composes the one below — a company is staffed with teams, an org with companies. Pick a scale; we instantiate the topology and bind it to real, chattable claws.</p>
|
||||||
|
</div>
|
||||||
|
<div style="display:grid; grid-template-columns:repeat(4,1fr); gap:16px;">
|
||||||
|
<div style="position:relative; border-radius:14px; border:1px solid rgba(255,255,255,.08); background:#0d0d10; padding:22px; overflow:hidden;" style-hover="border-color:rgba(255,111,97,.35);">
|
||||||
|
<div style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#5a5a62; margin-bottom:18px;">01 · SINGLE</div>
|
||||||
|
<svg width="36" height="36" viewBox="0 0 36 36" style="margin-bottom:16px;"><circle cx="18" cy="18" r="8" fill="none" stroke="#ff6f61" stroke-width="1.6"></circle><circle cx="18" cy="18" r="3" fill="#ff6f61"></circle></svg>
|
||||||
|
<div style="font-size:18px; font-weight:700; margin-bottom:6px;">A claw</div>
|
||||||
|
<div style="font-size:13px; color:#8a8a92; line-height:1.5;">One agent in a §15 sandbox. Chat with it, give it skills, point it at a task.</div>
|
||||||
|
</div>
|
||||||
|
<div style="position:relative; border-radius:14px; border:1px solid rgba(255,255,255,.08); background:#0d0d10; padding:22px; overflow:hidden;" style-hover="border-color:rgba(255,111,97,.35);">
|
||||||
|
<div style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#5a5a62; margin-bottom:18px;">02 · TEAM</div>
|
||||||
|
<svg width="36" height="36" viewBox="0 0 36 36" style="margin-bottom:16px;"><circle cx="18" cy="9" r="3.4" fill="#ff6f61"></circle><circle cx="9" cy="26" r="3.4" fill="#ff8a7a"></circle><circle cx="27" cy="26" r="3.4" fill="#ff8a7a"></circle><path d="M18 9 L9 26 M18 9 L27 26 M9 26 L27 26" stroke="rgba(255,111,97,.5)" stroke-width="1.3"></path></svg>
|
||||||
|
<div style="font-size:18px; font-weight:700; margin-bottom:6px;">A team</div>
|
||||||
|
<div style="font-size:13px; color:#8a8a92; line-height:1.5;">A topology of claws — lead, researchers, writer, critic — running one of 12 patterns.</div>
|
||||||
|
</div>
|
||||||
|
<div style="position:relative; border-radius:14px; border:1px solid rgba(255,255,255,.08); background:#0d0d10; padding:22px; overflow:hidden;" style-hover="border-color:rgba(255,111,97,.35);">
|
||||||
|
<div style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#5a5a62; margin-bottom:18px;">03 · COMPANY</div>
|
||||||
|
<svg width="36" height="36" viewBox="0 0 36 36" style="margin-bottom:16px;"><rect x="5" y="20" width="9" height="9" rx="2" fill="none" stroke="#ff6f61" stroke-width="1.5"></rect><rect x="22" y="6" width="9" height="9" rx="2" fill="none" stroke="#ff6f61" stroke-width="1.5"></rect><rect x="22" y="20" width="9" height="9" rx="2" fill="none" stroke="#ff6f61" stroke-width="1.5"></rect><path d="M14 24 L22 11 M14 24 L22 24" stroke="rgba(255,111,97,.5)" stroke-width="1.3"></path></svg>
|
||||||
|
<div style="font-size:18px; font-weight:700; margin-bottom:6px;">A company</div>
|
||||||
|
<div style="font-size:13px; color:#8a8a92; line-height:1.5;">Teams composed into a coordinating topology. Run the parent, run every child.</div>
|
||||||
|
</div>
|
||||||
|
<div style="position:relative; border-radius:14px; border:1px solid rgba(255,111,97,.3); background:linear-gradient(180deg,#150f12,#0d0d10); padding:22px; overflow:hidden;">
|
||||||
|
<div style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#ff8a7a; margin-bottom:18px;">04 · ORG</div>
|
||||||
|
<svg width="36" height="36" viewBox="0 0 36 36" style="margin-bottom:16px;"><circle cx="18" cy="18" r="14" fill="none" stroke="rgba(255,111,97,.3)" stroke-width="1.2" stroke-dasharray="2 3"></circle><circle cx="18" cy="9" r="3" fill="#ff6f61"></circle><circle cx="27" cy="23" r="3" fill="#ff6f61"></circle><circle cx="9" cy="23" r="3" fill="#ff6f61"></circle><path d="M18 9 L27 23 L9 23 Z" stroke="rgba(255,111,97,.5)" stroke-width="1.3" fill="none"></path></svg>
|
||||||
|
<div style="font-size:18px; font-weight:700; margin-bottom:6px;">A whole org</div>
|
||||||
|
<div style="font-size:13px; color:#a8a8b0; line-height:1.5;">Companies under one roof. Recursive execution all the way down to the leaf claws.</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- TOPOLOGIES -->
|
||||||
|
<div style="border-top:1px solid rgba(255,255,255,.06); background:linear-gradient(180deg,#0a0a0c,#08080a);">
|
||||||
|
<div style="max-width:1240px; margin:0 auto; padding:72px 32px;">
|
||||||
|
<div style="display:flex; align-items:flex-end; justify-content:space-between; gap:24px; margin-bottom:40px; flex-wrap:wrap;">
|
||||||
|
<div>
|
||||||
|
<div style="font-family:'JetBrains Mono',monospace; font-size:11px; letter-spacing:.16em; color:#5ec8d8; margin-bottom:14px;">THE TAXONOMY</div>
|
||||||
|
<h2 style="font-size:40px; font-weight:700; letter-spacing:-.03em; margin:0 0 12px; max-width:620px; text-wrap:balance;">Twelve topologies.<br>Five execution patterns.</h2>
|
||||||
|
<p style="font-size:17px; color:#a8a8b0; max-width:520px; margin:0; line-height:1.55;">Classify a task, build the structure that fits it, and switch patterns without rewriting a thing — authority stays invariant across every one.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="display:grid; grid-template-columns:repeat(4,1fr); gap:14px;">
|
||||||
|
<sc-for list="{{ topologies }}" as="t" hint-placeholder-count="12">
|
||||||
|
<div style="border-radius:13px; border:1px solid rgba(255,255,255,.07); background:#0d0d10; padding:16px; transition:border-color .2s;" style-hover="border-color:rgba(255,255,255,.16);">
|
||||||
|
<div style="height:88px; margin-bottom:12px; display:flex; align-items:center; justify-content:center;">
|
||||||
|
<svg viewBox="0 0 100 100" preserveAspectRatio="xMidYMid meet" style="width:100px; height:88px;">
|
||||||
|
<sc-for list="{{ t.links }}" as="l" hint-placeholder-count="4">
|
||||||
|
<line x1="{{ l.x1 }}" y1="{{ l.y1 }}" x2="{{ l.x2 }}" y2="{{ l.y2 }}" stroke="{{ t.color }}" stroke-width="1.3" stroke-opacity="0.4" stroke-dasharray="{{ l.dash }}"></line>
|
||||||
|
</sc-for>
|
||||||
|
<sc-for list="{{ t.nodes }}" as="n" hint-placeholder-count="5">
|
||||||
|
<sc-if value="{{ n.isRect }}" hint-placeholder-val="{{ false }}">
|
||||||
|
<rect x="{{ n.rx }}" y="{{ n.ry }}" width="12" height="12" rx="2.5" fill="{{ t.color }}"></rect>
|
||||||
|
</sc-if>
|
||||||
|
<sc-if value="{{ n.isCircle }}" hint-placeholder-val="{{ true }}">
|
||||||
|
<circle cx="{{ n.x }}" cy="{{ n.y }}" r="{{ n.r }}" fill="{{ t.color }}"></circle>
|
||||||
|
</sc-if>
|
||||||
|
</sc-for>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div style="font-size:15px; font-weight:700; margin-bottom:3px;">{{ t.name }}</div>
|
||||||
|
<div style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#6a6a72;">{{ t.kind }}</div>
|
||||||
|
</div>
|
||||||
|
</sc-for>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- RECURSIVE EXECUTION -->
|
||||||
|
<div style="max-width:1240px; margin:0 auto; padding:72px 32px;">
|
||||||
|
<div style="display:grid; grid-template-columns:1fr 1fr; gap:48px; align-items:center;">
|
||||||
|
<div>
|
||||||
|
<div style="font-family:'JetBrains Mono',monospace; font-size:11px; letter-spacing:.16em; color:#5ec8d8; margin-bottom:14px;">RECURSIVE EXECUTION</div>
|
||||||
|
<h2 style="font-size:38px; font-weight:700; letter-spacing:-.03em; margin:0 0 16px; text-wrap:balance;">Run the parent, run everything beneath it.</h2>
|
||||||
|
<p style="font-size:17px; color:#a8a8b0; margin:0 0 26px; line-height:1.6;">Executing a node runs its entire sub-topology, all the way down to the leaf claws — on a durable runner that checkpoints every step.</p>
|
||||||
|
<div style="display:flex; flex-direction:column; gap:14px;">
|
||||||
|
<div style="display:flex; align-items:flex-start; gap:12px;"><span style="width:8px; height:8px; border-radius:2px; background:#5fd08a; margin-top:6px; flex:none;"></span><div><div style="font-size:15px; font-weight:600;">Crash-resumable</div><div style="font-size:13px; color:#8a8a92; line-height:1.5;">Checkpointed per step. A crash resumes where it left off — not from zero.</div></div></div>
|
||||||
|
<div style="display:flex; align-items:flex-start; gap:12px;"><span style="width:8px; height:8px; border-radius:2px; background:#5ec8d8; margin-top:6px; flex:none;"></span><div><div style="font-size:15px; font-weight:600;">Live + cancellable</div><div style="font-size:13px; color:#8a8a92; line-height:1.5;">Stream every turn over SSE; cancel a run mid-flight at any tier.</div></div></div>
|
||||||
|
<div style="display:flex; align-items:flex-start; gap:12px;"><span style="width:8px; height:8px; border-radius:2px; background:#ff6f61; margin-top:6px; flex:none;"></span><div><div style="font-size:15px; font-weight:600;">Recursive zoom canvas</div><div style="font-size:13px; color:#8a8a92; line-height:1.5;">One view for every tier — click a node to drill down, breadcrumb to climb back up.</div></div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="position:relative; height:340px; border-radius:16px; border:1px solid rgba(255,255,255,.07); background:#0c0c0f; overflow:hidden; box-shadow:0 24px 60px rgba(0,0,0,.4);">
|
||||||
|
<div style="display:flex; align-items:center; gap:7px; padding:13px 16px; border-bottom:1px solid rgba(255,255,255,.06); font-family:'JetBrains Mono',monospace; font-size:11px;">
|
||||||
|
<span style="color:#6a6a72;">Org</span><span style="color:#3a3a40;">/</span><span style="color:#9a9aa2;">Acme Corp</span><span style="color:#3a3a40;">/</span><span style="color:#ff8a7a;">Growth Team</span>
|
||||||
|
</div>
|
||||||
|
<div style="position:absolute; inset:48px 0 0 0;">
|
||||||
|
<svg viewBox="0 0 100 100" preserveAspectRatio="none" style="position:absolute; inset:0; width:100%; height:100%;">
|
||||||
|
<path d="M50,46 L24,26 M50,46 L76,26 M50,46 L28,74 M50,46 L74,74" stroke="rgba(255,255,255,.12)" stroke-width="1.2" vector-effect="non-scaling-stroke"></path>
|
||||||
|
<path d="M50,46 L24,26" stroke="rgba(94,200,216,.55)" stroke-width="1.5" stroke-dasharray="3 4" vector-effect="non-scaling-stroke" style="animation:cm-flow 1.1s linear infinite;"></path>
|
||||||
|
</svg>
|
||||||
|
<div style="position:absolute; left:50%; top:46%; transform:translate(-50%,-50%); width:46px; height:46px; border-radius:50%; background:linear-gradient(135deg,#ff9a6a,#ff6f4a); display:flex; align-items:center; justify-content:center; font-weight:700; color:#2a0d05; font-size:16px; box-shadow:0 0 30px rgba(255,111,97,.4);">A</div>
|
||||||
|
<div style="position:absolute; left:24%; top:26%; transform:translate(-50%,-50%); width:30px; height:30px; border-radius:50%; background:linear-gradient(135deg,#6fd0c0,#4aa3b8); display:flex; align-items:center; justify-content:center; font-weight:700; color:#06201f; font-size:12px;">I</div>
|
||||||
|
<div style="position:absolute; left:76%; top:26%; transform:translate(-50%,-50%); width:30px; height:30px; border-radius:50%; background:linear-gradient(135deg,#e8c46a,#d89a3a); display:flex; align-items:center; justify-content:center; font-weight:700; color:#2a1d05; font-size:12px;">N</div>
|
||||||
|
<div style="position:absolute; left:28%; top:74%; transform:translate(-50%,-50%); width:30px; height:30px; border-radius:50%; background:linear-gradient(135deg,#8a9af0,#5a6ad8); display:flex; align-items:center; justify-content:center; font-weight:700; color:#0a0e2a; font-size:12px;">E</div>
|
||||||
|
<div style="position:absolute; left:74%; top:74%; transform:translate(-50%,-50%); width:30px; height:30px; border-radius:50%; background:linear-gradient(135deg,#c98af0,#9a5ad8); display:flex; align-items:center; justify-content:center; font-weight:700; color:#1a0a2a; font-size:12px;">S</div>
|
||||||
|
</div>
|
||||||
|
<div style="position:absolute; bottom:12px; left:16px; font-family:'JetBrains Mono',monospace; font-size:10px; color:#5fd08a;">● step 142 · checkpoint 3s ago</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- COMPARE + MODELS ROW -->
|
||||||
|
<div style="max-width:1240px; margin:0 auto; padding:0 32px 72px;">
|
||||||
|
<div style="display:grid; grid-template-columns:1.2fr 1fr; gap:16px;">
|
||||||
|
<div style="border-radius:16px; border:1px solid rgba(255,255,255,.08); background:#0d0d10; padding:28px; display:flex; gap:28px; align-items:center;">
|
||||||
|
<div style="flex:1;">
|
||||||
|
<div style="font-family:'JetBrains Mono',monospace; font-size:11px; letter-spacing:.14em; color:#e8b465; margin-bottom:12px;">COMPARE & EVOLVE</div>
|
||||||
|
<h3 style="font-size:24px; font-weight:700; letter-spacing:-.02em; margin:0 0 10px;">Run one task across many topologies.</h3>
|
||||||
|
<p style="font-size:14px; color:#9a9aa2; margin:0; line-height:1.55;">Get a quality/cost Pareto front, then let a MAP-Elites search evolve better team configurations using the comparison harness as fitness.</p>
|
||||||
|
</div>
|
||||||
|
<svg width="180" height="150" viewBox="0 0 180 150" style="flex:none;">
|
||||||
|
<line x1="24" y1="126" x2="168" y2="126" stroke="rgba(255,255,255,.12)" stroke-width="1"></line>
|
||||||
|
<line x1="24" y1="14" x2="24" y2="126" stroke="rgba(255,255,255,.12)" stroke-width="1"></line>
|
||||||
|
<path d="M34,104 C70,96 108,52 156,30" fill="none" stroke="rgba(255,111,97,.5)" stroke-width="1.4" stroke-dasharray="3 3"></path>
|
||||||
|
<circle cx="34" cy="104" r="4" fill="#ff6f61"></circle>
|
||||||
|
<circle cx="68" cy="88" r="4" fill="#ff6f61"></circle>
|
||||||
|
<circle cx="104" cy="58" r="4" fill="#ff6f61"></circle>
|
||||||
|
<circle cx="156" cy="30" r="4" fill="#ff6f61"></circle>
|
||||||
|
<circle cx="58" cy="112" r="3" fill="#4a4a52"></circle>
|
||||||
|
<circle cx="92" cy="96" r="3" fill="#4a4a52"></circle>
|
||||||
|
<circle cx="120" cy="92" r="3" fill="#4a4a52"></circle>
|
||||||
|
<circle cx="138" cy="64" r="3" fill="#4a4a52"></circle>
|
||||||
|
<text x="92" y="144" fill="#5a5a62" font-family="'JetBrains Mono',monospace" font-size="9" text-anchor="middle">cost →</text>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div style="border-radius:16px; border:1px solid rgba(255,255,255,.08); background:#0d0d10; padding:28px;">
|
||||||
|
<div style="font-family:'JetBrains Mono',monospace; font-size:11px; letter-spacing:.14em; color:#5ec8d8; margin-bottom:12px;">HETEROGENEOUS</div>
|
||||||
|
<h3 style="font-size:24px; font-weight:700; letter-spacing:-.02em; margin:0 0 16px;">Bind any node to any model.</h3>
|
||||||
|
<div style="display:flex; flex-wrap:wrap; gap:8px;">
|
||||||
|
<span style="font-family:'JetBrains Mono',monospace; font-size:12px; color:#e6e6ea; padding:7px 13px; border-radius:8px; border:1px solid rgba(255,111,97,.3); background:rgba(255,111,97,.07);">Claude</span>
|
||||||
|
<span style="font-family:'JetBrains Mono',monospace; font-size:12px; color:#cfcfd5; padding:7px 13px; border-radius:8px; border:1px solid rgba(255,255,255,.1); background:rgba(255,255,255,.03);">Gemini</span>
|
||||||
|
<span style="font-family:'JetBrains Mono',monospace; font-size:12px; color:#cfcfd5; padding:7px 13px; border-radius:8px; border:1px solid rgba(255,255,255,.1); background:rgba(255,255,255,.03);">Groq</span>
|
||||||
|
<span style="font-family:'JetBrains Mono',monospace; font-size:12px; color:#cfcfd5; padding:7px 13px; border-radius:8px; border:1px solid rgba(255,255,255,.1); background:rgba(255,255,255,.03);">GLM</span>
|
||||||
|
<span style="font-family:'JetBrains Mono',monospace; font-size:12px; color:#cfcfd5; padding:7px 13px; border-radius:8px; border:1px solid rgba(255,255,255,.1); background:rgba(255,255,255,.03);">Kimi</span>
|
||||||
|
</div>
|
||||||
|
<p style="font-size:14px; color:#9a9aa2; margin:18px 0 0; line-height:1.55;">Mix backends across a single topology — a cheap model for fan-out, a strong one for the judge.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SAFETY -->
|
||||||
|
<div style="border-top:1px solid rgba(255,255,255,.06); background:linear-gradient(180deg,#0a0a0c,#08080a);">
|
||||||
|
<div style="max-width:1240px; margin:0 auto; padding:72px 32px;">
|
||||||
|
<div style="display:grid; grid-template-columns:1fr 1.1fr; gap:48px; align-items:center;">
|
||||||
|
<div>
|
||||||
|
<div style="font-family:'JetBrains Mono',monospace; font-size:11px; letter-spacing:.16em; color:#5fd08a; margin-bottom:14px;">§15 · SAFE BY CONSTRUCTION</div>
|
||||||
|
<h2 style="font-size:38px; font-weight:700; letter-spacing:-.03em; margin:0 0 16px; text-wrap:balance;">Authority is topology-invariant.</h2>
|
||||||
|
<p style="font-size:17px; color:#a8a8b0; margin:0 0 22px; line-height:1.6;">No choice of structure — and no switch between structures — can let an agent exceed its sandbox. The network segmentation <span style="color:#f3f3f5; font-weight:600;">is</span> the security model.</p>
|
||||||
|
<div style="display:flex; flex-direction:column; gap:12px; font-size:14px; color:#b5b5bd;">
|
||||||
|
<div style="display:flex; gap:10px; align-items:center;"><span style="color:#5fd08a;">✓</span> Agents run tool-free in network-isolated sandboxes</div>
|
||||||
|
<div style="display:flex; gap:10px; align-items:center;"><span style="color:#5fd08a;">✓</span> Every sandbox-leaving action is a gated, human-approvable "door"</div>
|
||||||
|
<div style="display:flex; gap:10px; align-items:center;"><span style="color:#5fd08a;">✓</span> A secret broker holds credentials that never reach agent code</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="border-radius:16px; border:1px solid rgba(255,255,255,.08); background:#0c0c0f; padding:24px; box-shadow:0 24px 60px rgba(0,0,0,.4);">
|
||||||
|
<div style="display:flex; flex-direction:column; gap:10px;">
|
||||||
|
<div style="display:flex; align-items:center; gap:12px; padding:14px 16px; border-radius:11px; border:1px dashed rgba(255,255,255,.14); background:#0f0f13;">
|
||||||
|
<span style="width:34px; height:34px; border-radius:9px; background:rgba(255,111,97,.14); display:flex; align-items:center; justify-content:center;"><svg width="18" height="18" viewBox="0 0 20 20"><rect x="4" y="4" width="12" height="12" rx="3" stroke="#ff6f61" stroke-width="1.5" fill="none"></rect><circle cx="10" cy="10" r="2" fill="#ff6f61"></circle></svg></span>
|
||||||
|
<div style="flex:1;"><div style="font-size:13px; font-weight:600;">Agent sandbox</div><div style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#6a6a72;">tool-free · no network</div></div>
|
||||||
|
<span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#ff8a7a; padding:3px 8px; border-radius:5px; background:rgba(255,111,97,.1);">isolated</span>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex; justify-content:center; color:#3a3a40; font-family:'JetBrains Mono',monospace; font-size:11px;">↓ gated door · human approval</div>
|
||||||
|
<div style="display:flex; align-items:center; gap:12px; padding:14px 16px; border-radius:11px; border:1px solid rgba(94,200,216,.22); background:#0f0f13;">
|
||||||
|
<span style="width:34px; height:34px; border-radius:9px; background:rgba(94,200,216,.14); display:flex; align-items:center; justify-content:center;"><svg width="18" height="18" viewBox="0 0 20 20"><path d="M10 2v6m0 0l3-3m-3 3L7 5M3 12v4a2 2 0 002 2h10a2 2 0 002-2v-4" stroke="#5ec8d8" stroke-width="1.5" fill="none" stroke-linecap="round"></path></svg></span>
|
||||||
|
<div style="flex:1;"><div style="font-size:13px; font-weight:600;">Door tool → egress</div><div style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#6a6a72;">email · slack · browser</div></div>
|
||||||
|
<span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#5fd08a; padding:3px 8px; border-radius:5px; background:rgba(95,208,138,.1);">approved</span>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex; align-items:center; gap:12px; padding:14px 16px; border-radius:11px; border:1px solid rgba(255,255,255,.08); background:#0f0f13;">
|
||||||
|
<span style="width:34px; height:34px; border-radius:9px; background:rgba(232,196,106,.14); display:flex; align-items:center; justify-content:center;"><svg width="18" height="18" viewBox="0 0 20 20"><rect x="4" y="9" width="12" height="8" rx="2" stroke="#e8b465" stroke-width="1.5" fill="none"></rect><path d="M7 9V6.5a3 3 0 016 0V9" stroke="#e8b465" stroke-width="1.5" fill="none"></path></svg></span>
|
||||||
|
<div style="flex:1;"><div style="font-size:13px; font-weight:600;">Secret broker</div><div style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#6a6a72;">private socket · creds never leave</div></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SELF-HOST -->
|
||||||
|
<div style="max-width:1240px; margin:0 auto; padding:72px 32px;">
|
||||||
|
<div style="border-radius:16px; border:1px solid rgba(255,255,255,.08); background:linear-gradient(180deg,#0e0e12,#0a0a0c); padding:40px; display:grid; grid-template-columns:1fr 1fr; gap:40px; align-items:center;">
|
||||||
|
<div>
|
||||||
|
<div style="font-family:'JetBrains Mono',monospace; font-size:11px; letter-spacing:.16em; color:#c98af0; margin-bottom:14px;">SELF-HOSTABLE</div>
|
||||||
|
<h2 style="font-size:32px; font-weight:700; letter-spacing:-.03em; margin:0 0 14px;">The whole platform, one node.</h2>
|
||||||
|
<p style="font-size:16px; color:#a8a8b0; margin:0; line-height:1.6;">A single-node Docker Compose deployment runs everything with the same network-segmented security model as the Kubernetes path. The server self-migrates on boot.</p>
|
||||||
|
</div>
|
||||||
|
<div style="border-radius:12px; background:#070708; border:1px solid rgba(255,255,255,.08); padding:20px; font-family:'JetBrains Mono',monospace; font-size:13px; line-height:1.9;">
|
||||||
|
<div style="color:#5a5a62;"># bring up the stack</div>
|
||||||
|
<div><span style="color:#5fd08a;">$</span> <span style="color:#cfcfd5;">cd deploy/compose</span></div>
|
||||||
|
<div><span style="color:#5fd08a;">$</span> <span style="color:#cfcfd5;">docker compose up -d</span></div>
|
||||||
|
<div style="margin-top:8px; color:#5a5a62;"># app → localhost:3000</div>
|
||||||
|
<div style="color:#5ec8d8;">✓ server self-migrated · owner provisioned</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- CTA -->
|
||||||
|
<div style="max-width:1240px; margin:0 auto; padding:40px 32px 96px;">
|
||||||
|
<div style="position:relative; border-radius:20px; border:1px solid rgba(255,111,97,.25); background:radial-gradient(120% 140% at 50% 0%, rgba(255,111,97,.12), #0c0c0f 60%); padding:72px 32px; text-align:center; overflow:hidden;">
|
||||||
|
<h2 style="font-size:46px; font-weight:700; letter-spacing:-.035em; margin:0 0 16px; text-wrap:balance;">Deploy your first claw.</h2>
|
||||||
|
<p style="font-size:18px; color:#a8a8b0; margin:0 0 30px;">From one agent to a whole org — same model, same guarantees.</p>
|
||||||
|
<div style="display:flex; gap:12px; justify-content:center;">
|
||||||
|
<div style="display:flex; align-items:center; gap:8px; padding:14px 26px; border-radius:11px; background:linear-gradient(135deg,#ff8a7a,#ff5f57); color:#2a0d0a; font-size:16px; font-weight:700; cursor:pointer;" style-hover="filter:brightness(1.07);">Get started free →</div>
|
||||||
|
<div style="display:flex; align-items:center; gap:8px; padding:14px 26px; border-radius:11px; border:1px solid rgba(255,255,255,.14); color:#e6e6ea; font-size:16px; font-weight:600; cursor:pointer;" style-hover="background:rgba(255,255,255,.04);">clawmates.work</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- FOOTER -->
|
||||||
|
<div style="border-top:1px solid rgba(255,255,255,.06); background:#0a0a0c;">
|
||||||
|
<div style="max-width:1240px; margin:0 auto; padding:40px 32px; display:flex; align-items:center; gap:16px; flex-wrap:wrap;">
|
||||||
|
<div style="display:flex; align-items:center; gap:10px;">
|
||||||
|
<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.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:14px; font-weight:700;">Clawmates</span>
|
||||||
|
</div>
|
||||||
|
<div style="flex:1;"></div>
|
||||||
|
<div style="display:flex; gap:24px; font-size:13px; color:#8a8a92;">
|
||||||
|
<span style="cursor:pointer;">Platform</span><span style="cursor:pointer;">Topologies</span><span style="cursor:pointer;">Safety</span><span style="cursor:pointer;">Docs</span><span style="cursor:pointer;">Paper</span>
|
||||||
|
</div>
|
||||||
|
<div style="width:100%; border-top:1px solid rgba(255,255,255,.05); margin-top:24px; padding-top:20px; font-family:'JetBrains Mono',monospace; font-size:11px; color:#5a5a62;">© 2026 Clawmates · authority is topology-invariant</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</x-dc>
|
||||||
|
<script type="text/x-dc" data-dc-script>
|
||||||
|
class Component extends DCLogic {
|
||||||
|
renderVals() {
|
||||||
|
const C = { coral:'#ff6f61', cyan:'#5ec8d8', green:'#5fd08a', amber:'#e8b465', purple:'#c98af0', teal:'#6fd0c0' };
|
||||||
|
const raw = [
|
||||||
|
{ name:'Hierarchical', kind:'tree command', color:C.coral, pts:[[50,15],[28,50],[72,50],[16,85],[40,85],[84,85]], edges:[[0,1],[0,2],[1,3],[1,4],[2,5]] },
|
||||||
|
{ name:'Pipeline', kind:'sequential', color:C.amber, pts:[[12,50],[37,50],[63,50],[88,50]], edges:[[0,1],[1,2],[2,3]] },
|
||||||
|
{ name:'Swarm', kind:'parallel', color:C.cyan, pts:[[26,30],[60,22],[82,52],[54,80],[22,64],[48,48]], edges:[[0,1],[1,2],[3,4],[5,0],[5,2],[5,3]] },
|
||||||
|
{ name:'Mesh', kind:'all-to-all', color:C.green, pts:[[50,14],[86,42],[70,84],[30,84],[14,42]], edges:[[0,1],[0,2],[0,3],[0,4],[1,2],[1,3],[1,4],[2,3],[2,4],[3,4]] },
|
||||||
|
{ name:'Debate', kind:'adversarial', color:C.purple, pts:[[50,12],[26,46],[26,82],[74,46],[74,82]], edges:[[1,3],[1,4],[2,3],[2,4],[0,1],[0,3]] },
|
||||||
|
{ name:'Hub-spoke', kind:'one coordinator', color:C.coral, pts:[[50,50],[50,16],[82,38],[70,82],[30,82],[18,38]], edges:[[0,1],[0,2],[0,3],[0,4],[0,5]], big:0 },
|
||||||
|
{ name:'Star-MoE', kind:'router + experts',color:C.teal, pts:[[50,50],[24,24],[76,24],[24,76],[76,76]], edges:[[0,1],[0,2],[0,3],[0,4]], big:0 },
|
||||||
|
{ name:'Market', kind:'bid / auction', color:C.amber, pts:[[50,50],[20,30],[80,30],[20,72],[80,72]], edges:[[0,1],[0,2],[0,3],[0,4]], big:0, dash:true },
|
||||||
|
{ name:'Ring', kind:'cyclic hand-off', color:C.cyan, pts:[[50,14],[81,32],[81,68],[50,86],[19,68],[19,32]], edges:[[0,1],[1,2],[2,3],[3,4],[4,5],[5,0]] },
|
||||||
|
{ name:'Flat', kind:'peers', color:C.purple, pts:[[30,32],[70,32],[30,68],[70,68]], edges:[] },
|
||||||
|
{ name:'Holacratic', kind:'nested circles', color:C.teal, pts:[[50,50],[50,22],[74,64],[26,64]], edges:[[0,1],[0,2],[0,3]], big:0 },
|
||||||
|
{ name:'Blackboard', kind:'shared memory', color:C.cyan, pts:[[50,50],[24,26],[76,26],[24,74],[76,74]], edges:[[0,1],[0,2],[0,3],[0,4]], rect:0, big:0 },
|
||||||
|
];
|
||||||
|
const topologies = raw.map(t => ({
|
||||||
|
name: t.name, kind: t.kind, color: t.color,
|
||||||
|
nodes: t.pts.map((p,i) => ({ x:p[0], y:p[1], r:(t.big===i?7:5), isRect:(t.rect===i), isCircle:(t.rect!==i), rx:p[0]-6, ry:p[1]-6 })),
|
||||||
|
links: t.edges.map(([a,b]) => ({ x1:t.pts[a][0], y1:t.pts[a][1], x2:t.pts[b][0], y2:t.pts[b][1], dash:(t.dash?'3 3':'0') })),
|
||||||
|
}));
|
||||||
|
return { topologies };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</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 & 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
@@ -0,0 +1,8 @@
|
|||||||
|
-- Per-workspace default placement: where new agent sandboxes provision. A fleet
|
||||||
|
-- node id runs agents on that connected node; absent (or 'local') = the gateway
|
||||||
|
-- host, as before. Falls back to 'local' at runtime if the node is offline.
|
||||||
|
CREATE TABLE workspace_placement (
|
||||||
|
workspace_id UUID PRIMARY KEY REFERENCES workspaces (id) ON DELETE CASCADE,
|
||||||
|
node_id TEXT NOT NULL,
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
Reference in New Issue
Block a user