Agents page: reorganize into the Agent Command Center (per-agent live metrics)
Replace the single centered "anatomy profile" + resizable chat split with an
operator command center: compact 62px identity strip → 5-tile per-agent metrics
band → three independently-scrolling LIVE · BRAIN · SURFACE columns. Chat moves to
the computer's Chat app; the right computer pullout (DevicePanel) is untouched.
- backend: cm-api/routes/world.rs emits per-agent `telemetry{agentId,tokensPerMin,
costPerHr,loops,doorsPending}` in the SSE loop, from 4 batched GROUP BY queries
(usage_events tokens/min + credits/hr, active routines, pending approvals) — all
real, no migration. taxonomy `telemetry` gains optional agentId; stateKey now
keys it per-agent so slices don't clobber.
- frontend: new ClawCommandCenter + anatomy-cards (shared cards extracted from
Dashboard); useAgentTelemetry(agentId) feeds the metric band (Doors amber>0/
green=0); LIVE column streams the agent's task.update / reasoning.delta /
tool.call (replaces the mocked VitalsCard heatmap with a live activity chart).
- Dashboard: left region → full-height ClawCommandCenter; chat launcher opens the
computer Chat app; removed the dead anatomy cluster + unused imports.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
731adb6449
commit
11a1f22daa
@@ -0,0 +1,158 @@
|
|||||||
|
# Agent Command Center — Build Guide
|
||||||
|
|
||||||
|
How the Clawmates **Agent Command Center** page is built, and the rules to follow to recreate or extend it. The `.dc.html` in this folder is the working reference — open it directly in a browser. This doc is the recipe + the prompt to regenerate it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. What this page is
|
||||||
|
|
||||||
|
A single-agent **command center** — the close-up view of one agent in the fleet. It replaces the old "scrolling profile" (big avatar + year-long heatmap on top, live status buried at the bottom) with an operator surface where **live state and controls sit above the fold** and identity shrinks to a header strip.
|
||||||
|
|
||||||
|
It answers, at a glance:
|
||||||
|
1. **Is it healthy and how hard is it working?** → metrics band (throughput, spend, loops, memory, doors)
|
||||||
|
2. **What is it doing right now?** → LIVE column (working-on-now, reasoning stream, activity)
|
||||||
|
3. **What is its brain made of?** → BRAIN column (system prompt, personality, skills, capabilities)
|
||||||
|
4. **What can it touch, remember, and is it safe?** → SURFACE column (tools·doors, memory, safety)
|
||||||
|
5. **What's on its desktop?** → pinned right-side **computer pullout** (apps, now-running, dock)
|
||||||
|
|
||||||
|
The core interaction idea: **read on the left/center, act on the right.** Identity is small; the work is big.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. The layout system
|
||||||
|
|
||||||
|
Fixed full-viewport shell (`100vw × 100vh`, `overflow:hidden`). Lock this skeleton first.
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────────────────────────────────────────────────────────┐
|
||||||
|
│ TOP BAR (54px) logo · breadcrumb · live pill · tok/min · avatar │
|
||||||
|
├──┬─────────┬──────────────────────────────────────────┬───────────┤
|
||||||
|
│IC│ AGENT │ ┌ identity header strip ───────────────┐ │ COMPUTER │
|
||||||
|
│ON│ LIST │ ├ METRICS BAND (5 tiles) ──────────────┤ │ PULLOUT │
|
||||||
|
│54│ 222px │ │ LIVE │ BRAIN │ SURFACE │ │ 344px │
|
||||||
|
│ │ │ │ 1.15fr │ 1fr │ 1fr │ │ apps · │
|
||||||
|
│ │ │ │ (each col scrolls independently) │ │ running · │
|
||||||
|
│ │ │ └────────────────────────────────────────┘ │ dock │
|
||||||
|
├──┴─────────┴──────────────────────────────────────────┴───────────┤
|
||||||
|
│ STATUS BAR (28px) durable runner · checkpoint · sandbox · doors │
|
||||||
|
└──────────────────────────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Icon rail (54px):** product switcher (World / Agent / Infra). Active = coral glyph + coral left spine.
|
||||||
|
- **Agent list (222px):** every agent in the team. Row = avatar + status dot, name, mono role, live action line. Active row gets the 3px coral spine + coral tint.
|
||||||
|
- **Command-center area (fluid):** identity header → metrics band → three scrolling columns.
|
||||||
|
- **Computer pullout (344px, pinned):** mirrors the product's right-side computer panel. Stays put; cards live to its **left**. Toggle with the `showComputer` prop.
|
||||||
|
- **Status bar (28px):** ambient truth, always-on mono.
|
||||||
|
|
||||||
|
Only the three center columns and the panels' inner bodies scroll; all chrome is `flex:none`. The whole thing fits one 1080p screen.
|
||||||
|
|
||||||
|
### The three center columns
|
||||||
|
- **LIVE (1.15fr):** `WORKING ON NOW` card (task + step list with a blinking caret) → `REASONING STREAM` terminal (think/tool/door tagged lines) → `ACTIVITY · LIVE` bar chart.
|
||||||
|
- **BRAIN (1fr):** collapsible `SYSTEM PROMPT` (click header to expand) → `PERSONALITY` chips → `SKILLS` chips → `CAPABILITIES` chips.
|
||||||
|
- **SURFACE (1fr):** `TOOLS · DOORS` (gated/blocked list + inline §15 approval card) → `MEMORY` → `SAFETY · §15`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Color
|
||||||
|
|
||||||
|
Near-black warm-neutral base; **functional accents only** — don't add a color unless it means something. Aim for ~90% neutral.
|
||||||
|
|
||||||
|
| Token | Value | Used for |
|
||||||
|
|---|---|---|
|
||||||
|
| Canvas | `#08080a` | page background |
|
||||||
|
| Surface | `#0d0d10` | cards |
|
||||||
|
| Chrome | `#0a0a0c` / `#0b0b0e` | rails, panels, status bar |
|
||||||
|
| Hairline | `rgba(255,255,255,.06–.08)` | every border/divider |
|
||||||
|
| Text high | `#f3f3f5` | headings, key values |
|
||||||
|
| Text mid | `#8a8a92 / #9a9aa2` | body |
|
||||||
|
| Text low | `#5a5a62 / #6a6a72` | mono labels, meta |
|
||||||
|
|
||||||
|
**Accents (one meaning each):**
|
||||||
|
- **Coral `#ff6f61`** → brand + primary action + active nav. The coral→red fill `linear-gradient(135deg,#ff8a7a,#ff5f57)` is the one filled-button treatment.
|
||||||
|
- **Cyan `#5ec8d8`** → live / working / network / throughput / links.
|
||||||
|
- **Green `#5fd08a`** → healthy / done / loops running.
|
||||||
|
- **Amber `#e8b465`** → pending / door awaiting approval / spend / blocked tool.
|
||||||
|
- **Violet `#c98af0`** → reasoning ("think") + personality.
|
||||||
|
- **Teal `#6fd0c0`** → safety / sandbox.
|
||||||
|
|
||||||
|
The **doors** metric tile and status-bar text turn amber the moment an approval is pending and flip green when clear — that color shift is the page's most important signal.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Type
|
||||||
|
|
||||||
|
Two families, sharp roles:
|
||||||
|
- **Helvetica Neue** (system sans) — headings, names, prose.
|
||||||
|
- **JetBrains Mono** — everything machine: roles, status, IPs/hosts, the reasoning stream, stat labels, chips, breadcrumb, status bar. This sans/mono split is what makes it read as an operator tool, not a website.
|
||||||
|
|
||||||
|
Scale (floors): agent name `24/700/-.02em` · card titles `13–15/600–700` · body `11–13` · mono labels `9–10`, letter-spacing `.08–.12em`, low-contrast.
|
||||||
|
|
||||||
|
The uppercase mono kicker (`WORKING ON NOW`, `SYSTEM PROMPT`, `TOOLS · DOORS`) above every card is the connective rhythm — keep it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Components & patterns
|
||||||
|
|
||||||
|
- **Identity header strip** — small haloed avatar (62px, pulsing ring) + name + mono `role · spec`, then model chip + coral "Edit brain →" + settings gear. Identity is deliberately compact.
|
||||||
|
- **Metric tile** — mono label, big `22/700` value, mono unit. Notable tiles tint border + value (cyan throughput, amber spend, green loops, amber doors-when-pending). The throughput tile carries a live sparkline.
|
||||||
|
- **Card** — `#0d0d10` + hairline (or accent-tinted) border, 13px radius, mono kicker + icon chip header. The single repeated container.
|
||||||
|
- **Reasoning stream** — black terminal; each line is a colored tag (`think`/`tool`/`door`/`note`/`result`) + text; last line gets a blinking caret.
|
||||||
|
- **Chip** — mono, small, tinted pill for personality/skills/capabilities. A trailing `+N` chip implies overflow.
|
||||||
|
- **Inline §15 door** — amber-bordered approval card with a gradient top-rule: title, mono detail, Approve / Review / ✕. Approving clears it to a green "no doors awaiting approval" line and updates the metric + status bar.
|
||||||
|
- **Computer pullout** — pinned right panel: header w/ device icons, sandbox status, apps grid (real brand glyphs as inline SVG), NOW RUNNING list (loop/cron items with progress bars), bottom dock (Skills/Files/Routines/Settings).
|
||||||
|
- **Indicators** — reuse three primitives everywhere: colored **dot** (state), thin **bar** (utilization/progress), bordered **pill** (labeled status). Don't invent new ones.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Motion
|
||||||
|
|
||||||
|
Restrained and diegetic — movement only ever means "this is live" or "the view changed." Six keyframes:
|
||||||
|
`cm-blink` (live dots/caret), `cm-halo` (avatar ring pulse), `cm-type` (caret), `cm-bar` (sparkline/activity bars), `cm-fade` (0.2–0.25s panel/step entrance). No load chrome, no parallax, no decorative motion. All of it is gated behind the `liveStream` prop so it can be paused.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Voice
|
||||||
|
|
||||||
|
Technical, plain, security-forward. Buttons name the outcome ("Edit brain →", "Approve"). The §15 / door / sandbox / "no network" language repeats the trust story. Lead with what the agent is doing, not what it is.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. How it's built (structure)
|
||||||
|
|
||||||
|
This is a **Design Component** (`.dc.html`) — one self-contained file + `support.js` (the runtime; keep it beside the page).
|
||||||
|
|
||||||
|
- **Template** = markup styled **entirely inline** (only `@keyframes` + scrollbar rules live in `<helmet>`). Inline styles paint immediately as the file streams.
|
||||||
|
- **Logic** = `class Component extends DCLogic` at the bottom. State: `selected` (which agent), `promptOpen`, `doorApproved`. `renderVals()` returns every `{{ … }}` the template binds + all handlers.
|
||||||
|
- **Data is mocked** in an `AGENTS` map inside `renderVals()` (Morpheus, Smith). Each agent carries its metrics, task, reasoning, personality, skills, tools, memory, safety, door, prompt, and now-running list. **To wire real data, replace that map with your fetched agent state** — the template shape is stable.
|
||||||
|
- **Control flow:** `<sc-for>` repeats agent rows, steps, reasoning lines, chips, tools, running items; `<sc-if>` toggles the prompt body, the door card vs the cleared state, and the computer pullout. State-driven contents, not separate pages.
|
||||||
|
- **Props / tweaks:** `liveStream` (pause all animation), `showComputer` (show/hide the pullout), `defaultPromptOpen` (start with the system prompt expanded).
|
||||||
|
|
||||||
|
### To build a page in this style
|
||||||
|
1. Drop the **shell** (top bar / icon rail / agent list / command area / computer pullout / status bar); pin everything but the center columns and panel bodies.
|
||||||
|
2. Shrink **identity to a header strip**; never let the avatar dominate.
|
||||||
|
3. Put the glance-priority metrics in a **5-tile band** directly under it.
|
||||||
|
4. Split the work into **LIVE · BRAIN · SURFACE** columns, each scrolling independently.
|
||||||
|
5. Keep the **computer as a pinned right pullout**; all cards go to its left.
|
||||||
|
6. Use only the accents above, **one meaning each**; show state with the dot/bar/pill trio and the six keyframes — nothing more.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Prompt to regenerate
|
||||||
|
|
||||||
|
> Build a single-agent **command center** as a Design Component in the Clawmates dark-operator visual system (near-black `#08080a` canvas, `#0d0d10` cards, hairline borders, Helvetica Neue + JetBrains Mono, functional accents: coral `#ff6f61` brand/primary, cyan `#5ec8d8` live, green `#5fd08a` healthy, amber `#e8b465` pending, violet `#c98af0` reasoning, teal `#6fd0c0` safety).
|
||||||
|
>
|
||||||
|
> Fixed full-viewport shell: 54px top bar (logo · breadcrumb · live pill · tok/min · avatar) → body [54px icon rail · 222px agent list · fluid command area · 344px pinned computer pullout] → 28px status bar. Only the center columns and panel bodies scroll.
|
||||||
|
>
|
||||||
|
> Command area = compact identity header strip (small haloed 62px avatar + name + mono role·spec + model chip + coral "Edit brain →") → 5-tile metrics band (Throughput w/ sparkline, Spend, Loops, Memory, Doors — doors tile + status bar turn amber when an approval is pending) → three independently-scrolling columns: **LIVE** (working-on-now task w/ step list + caret, reasoning-stream terminal, activity bars), **BRAIN** (collapsible system prompt, personality / skills / capabilities chips), **SURFACE** (tools·doors w/ inline §15 approval card, memory, safety).
|
||||||
|
>
|
||||||
|
> Pinned right **computer pullout**: header w/ device icons, "sandbox live · no network", apps grid (Browser/Slack/Terminal/Obsidian/Chat/Add as inline-SVG glyphs), NOW RUNNING list (loop/cron items w/ progress bars), bottom dock (Skills/Files/Routines/Settings).
|
||||||
|
>
|
||||||
|
> Mock 2 agents (a PM and a researcher) in an `AGENTS` map; clicking a list row switches everything. Interactions: switch agent, expand/collapse system prompt, approve the pending door (clears it + updates the doors metric + status bar). Inline styles only; six restrained keyframes (blink/halo/type/bar/fade) gated behind a `liveStream` prop. Expose props: `liveStream`, `showComputer`, `defaultPromptOpen`. Fit one 1080p screen.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Files in this package
|
||||||
|
|
||||||
|
- `Clawmates Agent Command Center.dc.html` — the page (open directly in a browser).
|
||||||
|
- `support.js` — the runtime the `.dc.html` needs; keep it beside the page.
|
||||||
|
- `BUILD-GUIDE.md` — this document.
|
||||||
@@ -0,0 +1,592 @@
|
|||||||
|
<!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: .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-fade { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
|
||||||
|
::-webkit-scrollbar { width: 8px; height: 8px; }
|
||||||
|
::-webkit-scrollbar-thumb { background: rgba(255,255,255,.09); border-radius: 4px; }
|
||||||
|
::-webkit-scrollbar-track { background: transparent; }
|
||||||
|
</style>
|
||||||
|
</helmet>
|
||||||
|
|
||||||
|
<div style="width:100%; height:100vh; min-height:680px; 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; 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:8px; font-family:'JetBrains Mono',monospace; font-size:12px;">
|
||||||
|
<span style="color:#6a6a72;">Large World</span>
|
||||||
|
<span style="color:#3a3a40;">/</span>
|
||||||
|
<span style="color:#9a9aa2;">Agents</span>
|
||||||
|
<span style="color:#3a3a40;">/</span>
|
||||||
|
<span style="color:#fff; background:rgba(255,111,97,.14); border:1px solid rgba(255,111,97,.3); padding:3px 8px; border-radius:6px;">{{ selName }}</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:7px; height:7px; border-radius:50%; background:#5ec8d8; {{ blinkAnim }}"></span>{{ selStatusLabel }}
|
||||||
|
</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> {{ selTok }} tok/min
|
||||||
|
</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;">
|
||||||
|
|
||||||
|
<!-- 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:42px; height:46px; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:4px; border-radius:9px; color:#5a5a62; cursor:pointer;">
|
||||||
|
<svg width="19" height="19" 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:42px; height:46px; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:4px; border-radius:9px; color:#ff6f61; background:rgba(255,111,97,.1); cursor:pointer;">
|
||||||
|
<span style="position:absolute; left:0; top:7px; bottom:7px; width:3px; border-radius:0 3px 3px 0; background:#ff6f61;"></span>
|
||||||
|
<svg width="19" height="19" 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="width:42px; height:46px; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:4px; border-radius:9px; color:#5a5a62; cursor:pointer;">
|
||||||
|
<svg width="19" height="19" viewBox="0 0 20 20"><rect x="3.5" y="4" width="13" height="5" rx="1.4" stroke="currentColor" stroke-width="1.3" fill="none"></rect><rect x="3.5" y="11" width="13" height="5" rx="1.4" stroke="currentColor" stroke-width="1.3" fill="none"></rect></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: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>
|
||||||
|
|
||||||
|
<!-- AGENT LIST -->
|
||||||
|
<div style="width:222px; 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-y:auto; padding:6px;">
|
||||||
|
<div style="display:flex; flex-direction:column; gap:3px;">
|
||||||
|
<sc-for list="{{ agents }}" as="a" hint-placeholder-count="2">
|
||||||
|
<div style="display:flex; align-items:flex-start; gap:10px; padding:9px 10px; border-radius:10px; cursor:pointer; position:relative; background:{{ a.rowBg }}; border:1px solid {{ a.rowBorder }};" onClick="{{ a.onSelect }}">
|
||||||
|
<sc-if value="{{ a.selected }}" hint-placeholder-val="{{ true }}"><span style="position:absolute; left:0; top:9px; bottom:9px; width:3px; border-radius:0 3px 3px 0; background:#ff6f61;"></span></sc-if>
|
||||||
|
<div style="position:relative; width:32px; height:32px; flex:none;">
|
||||||
|
<div style="width:32px; height:32px; border-radius:9px; background:{{ a.grad }}; display:flex; align-items:center; justify-content:center; font-size:13px; font-weight:700; color:{{ a.ink }};">{{ a.initial }}</div>
|
||||||
|
<span style="position:absolute; right:-2px; bottom:-2px; width:10px; height:10px; border-radius:50%; background:{{ a.statusColor }}; border:2px solid #0b0b0e;"></span>
|
||||||
|
</div>
|
||||||
|
<div style="flex:1; min-width:0;">
|
||||||
|
<div style="font-size:13px; font-weight:600; color:{{ a.labelColor }};">{{ a.name }}</div>
|
||||||
|
<div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:{{ a.roleColor }};">{{ a.role }}</div>
|
||||||
|
<div style="font-family:'JetBrains Mono',monospace; font-size:9px; color:{{ a.statusColor }}; margin-top:4px; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;">{{ a.action }}</div>
|
||||||
|
</div>
|
||||||
|
</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:32px; border-radius:8px; border:1px solid rgba(94,200,216,.3); color:#5ec8d8; font-size:12px; font-weight:600; cursor:pointer;">⊕ Add to team</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- COMMAND CENTER -->
|
||||||
|
<div data-screen-label="Agent Command Center" style="flex:1; min-width:0; display:flex; flex-direction:column; min-height:0; background:radial-gradient(130% 100% at 30% 0%, #100e13 0%, #08080a 60%);">
|
||||||
|
|
||||||
|
<!-- IDENTITY HEADER STRIP -->
|
||||||
|
<div style="flex:none; display:flex; align-items:center; gap:16px; padding:16px 20px 14px; border-bottom:1px solid rgba(255,255,255,.06);">
|
||||||
|
<div style="position:relative; width:62px; height:62px; flex:none;">
|
||||||
|
<div style="position:absolute; inset:-5px; border-radius:50%; border:1.5px solid rgba(255,111,97,.4);"></div>
|
||||||
|
<div style="position:absolute; inset:0; border-radius:50%; background:rgba(255,111,97,.3); {{ haloAnim }}"></div>
|
||||||
|
<div style="position:relative; width:62px; height:62px; border-radius:50%; background:{{ selGrad }}; display:flex; align-items:center; justify-content:center; font-size:24px; font-weight:700; color:{{ selInk }}; box-shadow:0 0 30px rgba(255,111,97,.4);">{{ selInitial }}</div>
|
||||||
|
</div>
|
||||||
|
<div style="min-width:0;">
|
||||||
|
<div style="font-size:24px; font-weight:700; letter-spacing:-.02em; line-height:1.1;">{{ selName }}</div>
|
||||||
|
<div style="font-family:'JetBrains Mono',monospace; font-size:11px; color:#6a6a72; margin-top:3px;">{{ selRole }} · {{ selSpec }}</div>
|
||||||
|
</div>
|
||||||
|
<div style="flex:1;"></div>
|
||||||
|
<div style="display:flex; align-items:center; gap:8px; padding:7px 11px; border-radius:9px; background:#101014; border:1px solid rgba(255,255,255,.08);">
|
||||||
|
<span style="width:7px; height:7px; border-radius:50%; background:#ff6f61;"></span>
|
||||||
|
<span style="font-family:'JetBrains Mono',monospace; font-size:11px; color:#cfcfd5;">{{ selModel }}</span>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex; align-items:center; gap:7px; padding:8px 13px; border-radius:9px; background:linear-gradient(135deg,#ff8a7a,#ff5f57); color:#2a0d0a; font-size:12px; font-weight:700; cursor:pointer;">Edit brain →</div>
|
||||||
|
<div style="width:34px; height:34px; border-radius:9px; border:1px solid rgba(255,255,255,.1); display:flex; align-items:center; justify-content:center; color:#9a9aa2; cursor:pointer;"><svg width="16" height="16" viewBox="0 0 18 18"><circle cx="9" cy="9" r="2.4" fill="currentColor"></circle><path d="M9 1.5v2.2M9 14.3v2.2M1.5 9h2.2M14.3 9h2.2M3.6 3.6l1.6 1.6M12.8 12.8l1.6 1.6M14.4 3.6l-1.6 1.6M5.2 12.8l-1.6 1.6" stroke="currentColor" stroke-width="1.4"></path></svg></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- METRICS BAND -->
|
||||||
|
<div style="flex:none; display:flex; gap:12px; padding:14px 20px;">
|
||||||
|
<div style="flex:1.4; border-radius:11px; background:#0d0d10; border:1px solid rgba(94,200,216,.22); padding:11px 14px; display:flex; align-items:center; gap:12px;">
|
||||||
|
<div style="min-width:0;">
|
||||||
|
<div style="font-family:'JetBrains Mono',monospace; font-size:9px; letter-spacing:.1em; color:#6a6a72;">THROUGHPUT</div>
|
||||||
|
<div style="display:flex; align-items:baseline; gap:5px; margin-top:4px;"><span style="font-size:22px; font-weight:700; color:#5ec8d8; letter-spacing:-.01em;">{{ selTok }}</span><span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72;">tok/min</span></div>
|
||||||
|
</div>
|
||||||
|
<span style="flex:1;"></span>
|
||||||
|
<span style="display:flex; align-items:flex-end; gap:2px; height:30px;">
|
||||||
|
<span style="width:3px; background:#2c5a64; height:40%; border-radius:1px;"></span>
|
||||||
|
<span style="width:3px; background:#3a7e8c; height:68%; border-radius:1px;"></span>
|
||||||
|
<span style="width:3px; background:#4aa3b8; height:52%; border-radius:1px; transform-origin:bottom; {{ barAnim }}"></span>
|
||||||
|
<span style="width:3px; background:#5ec8d8; height:88%; border-radius:1px; transform-origin:bottom; {{ barAnim2 }}"></span>
|
||||||
|
<span style="width:3px; background:#4aa3b8; height:64%; border-radius:1px;"></span>
|
||||||
|
<span style="width:3px; background:#3a7e8c; height:46%; border-radius:1px;"></span>
|
||||||
|
<span style="width:3px; background:#5ec8d8; height:76%; border-radius:1px;"></span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div style="flex:1; border-radius:11px; background:#0d0d10; border:1px solid rgba(255,255,255,.06); padding:11px 14px;">
|
||||||
|
<div style="font-family:'JetBrains Mono',monospace; font-size:9px; letter-spacing:.1em; color:#6a6a72;">SPEND</div>
|
||||||
|
<div style="display:flex; align-items:baseline; gap:5px; margin-top:4px;"><span style="font-size:22px; font-weight:700; color:#e8b465; letter-spacing:-.01em;">{{ selSpend }}</span><span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72;">/hr</span></div>
|
||||||
|
</div>
|
||||||
|
<div style="flex:1; border-radius:11px; background:#0d0d10; border:1px solid rgba(255,255,255,.06); padding:11px 14px;">
|
||||||
|
<div style="font-family:'JetBrains Mono',monospace; font-size:9px; letter-spacing:.1em; color:#6a6a72;">LOOPS</div>
|
||||||
|
<div style="display:flex; align-items:baseline; gap:5px; margin-top:4px;"><span style="font-size:22px; font-weight:700; color:#5fd08a; letter-spacing:-.01em;">{{ selLoops }}</span><span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72;">running</span></div>
|
||||||
|
</div>
|
||||||
|
<div style="flex:1; border-radius:11px; background:#0d0d10; border:1px solid rgba(255,255,255,.06); padding:11px 14px;">
|
||||||
|
<div style="font-family:'JetBrains Mono',monospace; font-size:9px; letter-spacing:.1em; color:#6a6a72;">MEMORY</div>
|
||||||
|
<div style="display:flex; align-items:baseline; gap:5px; margin-top:4px;"><span style="font-size:22px; font-weight:700; color:#cfcfd5; letter-spacing:-.01em;">{{ selMem }}</span><span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72;">notes</span></div>
|
||||||
|
</div>
|
||||||
|
<div style="flex:1; border-radius:11px; background:#0d0d10; border:1px solid {{ doorsBorder }}; padding:11px 14px;">
|
||||||
|
<div style="font-family:'JetBrains Mono',monospace; font-size:9px; letter-spacing:.1em; color:{{ doorsLabelColor }};">DOORS</div>
|
||||||
|
<div style="display:flex; align-items:baseline; gap:5px; margin-top:4px;"><span style="font-size:22px; font-weight:700; color:{{ doorsValColor }}; letter-spacing:-.01em;">{{ doorsLive }}</span><span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#6a6a72;">pending</span></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- THREE COLUMNS -->
|
||||||
|
<div style="flex:1; min-height:0; display:flex; gap:14px; padding:0 20px 16px;">
|
||||||
|
|
||||||
|
<!-- LIVE COLUMN -->
|
||||||
|
<div style="flex:1.15; min-width:0; display:flex; flex-direction:column; gap:12px; overflow-y:auto;">
|
||||||
|
|
||||||
|
<div style="flex:none; border-radius:13px; border:1px solid rgba(94,200,216,.24); 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; {{ blinkAnim }}"></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;">{{ taskElapsed }}</span>
|
||||||
|
</div>
|
||||||
|
<div style="padding:13px 14px;">
|
||||||
|
<div style="font-size:15px; font-weight:600; margin-bottom:8px; line-height:1.35;">{{ taskTitle }} <span style="font-family:'JetBrains Mono',monospace; color:#ff8a7a;">{{ taskCode }}</span></div>
|
||||||
|
<div style="display:flex; flex-direction:column; gap:5px; font-family:'JetBrains Mono',monospace; font-size:11px; line-height:1.6;">
|
||||||
|
<sc-for list="{{ taskSteps }}" as="st" hint-placeholder-count="3">
|
||||||
|
<div style="color:#7a7a82;"><span style="color:{{ st.iconColor }};">{{ st.icon }}</span> {{ st.text }} <span style="color:#5a5a62;">{{ st.meta }}</span><sc-if value="{{ st.caret }}" hint-placeholder-val="{{ false }}"><span style="display:inline-block; width:6px; height:12px; background:#5ec8d8; margin-left:2px; vertical-align:-2px; {{ caretAnim }}"></span></sc-if></div>
|
||||||
|
</sc-for>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="flex:1; min-height:160px; border-radius:13px; 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:10px 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;">{{ selModelShort }} · {{ reasoningTime }}</span>
|
||||||
|
</div>
|
||||||
|
<div style="flex:1; min-height:0; overflow-y:auto; padding:12px 14px; font-family:'JetBrains Mono',monospace; font-size:11px; line-height:1.7;">
|
||||||
|
<sc-for list="{{ reasoning }}" as="r" hint-placeholder-count="4">
|
||||||
|
<div style="color:#9a9aa2; margin-bottom:7px;"><span style="color:{{ r.tagColor }};">{{ r.tag }}</span> {{ r.text }}<sc-if value="{{ r.caret }}" hint-placeholder-val="{{ false }}"><span style="display:inline-block; width:6px; height:12px; background:#5fd08a; margin-left:3px; vertical-align:-2px; {{ caretAnim }}"></span></sc-if></div>
|
||||||
|
</sc-for>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<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="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;">{{ selCommits }} commits · {{ selHot }} hot</span>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex; align-items:flex-end; gap:3px; height:46px;">
|
||||||
|
<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; {{ barAnim }}"></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; {{ barAnim2 }}"></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>
|
||||||
|
|
||||||
|
<!-- BRAIN COLUMN -->
|
||||||
|
<div style="flex:1; min-width:0; display:flex; flex-direction:column; gap:12px; overflow-y:auto;">
|
||||||
|
|
||||||
|
<div style="flex:none; border-radius:13px; background:#0d0d10; border:1px solid rgba(255,111,97,.22); overflow:hidden;">
|
||||||
|
<div style="display:flex; align-items:center; gap:8px; padding:12px 13px; cursor:pointer;" onClick="{{ togglePrompt }}">
|
||||||
|
<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"><path d="M4 2h6l2 2v10H4z" fill="none" stroke="#ff6f61" stroke-width="1.3" stroke-linejoin="round"></path><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:.08em; color:#ff8a7a;">SYSTEM PROMPT</span>
|
||||||
|
<span style="flex:1;"></span>
|
||||||
|
<span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:#5a5a62;">{{ promptChars }} ch</span>
|
||||||
|
<span style="font-family:'JetBrains Mono',monospace; font-size:11px; color:#ff8a7a;">{{ promptChevron }}</span>
|
||||||
|
</div>
|
||||||
|
<sc-if value="{{ promptOpen }}" hint-placeholder-val="{{ false }}">
|
||||||
|
<div style="padding:0 14px 13px; animation:cm-fade .2s ease;">
|
||||||
|
<div style="max-height:148px; overflow-y:auto; padding:11px 12px; border-radius:9px; background:#08080a; border:1px solid rgba(255,255,255,.06); font-family:'JetBrains Mono',monospace; font-size:11px; line-height:1.7; color:#9a9aa2; white-space:pre-wrap;">{{ promptText }}</div>
|
||||||
|
</div>
|
||||||
|
</sc-if>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="flex:none; border-radius:13px; background:#0d0d10; border:1px solid rgba(201,138,240,.22); padding:12px 13px;">
|
||||||
|
<div style="display:flex; align-items:center; gap:8px; margin-bottom:10px;">
|
||||||
|
<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:5px;">
|
||||||
|
<sc-for list="{{ personality }}" as="p" hint-placeholder-count="3">
|
||||||
|
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#cfcfd5; padding:4px 9px; border-radius:6px; background:rgba(201,138,240,.1); border:1px solid rgba(201,138,240,.18);">{{ p }}</span>
|
||||||
|
</sc-for>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="flex:none; border-radius:13px; background:#0d0d10; border:1px solid rgba(255,111,97,.18); padding:12px 13px;">
|
||||||
|
<div style="display:flex; align-items:center; gap:8px; margin-bottom:10px;">
|
||||||
|
<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;">{{ skillsCount }}</span>
|
||||||
|
</div>
|
||||||
|
<div style="display:flex; flex-wrap:wrap; gap:5px;">
|
||||||
|
<sc-for list="{{ skills }}" as="sk" hint-placeholder-count="4">
|
||||||
|
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#cfcfd5; padding:4px 9px; border-radius:6px; background:rgba(255,255,255,.05);">{{ sk }}</span>
|
||||||
|
</sc-for>
|
||||||
|
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#8a8a92; padding:4px 9px; border-radius:6px; background:rgba(255,255,255,.03);">{{ skillsMore }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="flex:none; border-radius:13px; background:#0d0d10; border:1px solid rgba(232,196,106,.2); padding:12px 13px;">
|
||||||
|
<div style="display:flex; align-items:center; gap:8px; margin-bottom:10px;">
|
||||||
|
<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"><rect x="3" y="3" width="12" height="12" rx="2.5" fill="none" stroke="#e8b465" stroke-width="1.5"></rect><rect x="6.5" y="6.5" width="5" height="5" rx="1" fill="#e8b465"></rect></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:5px;">
|
||||||
|
<sc-for list="{{ capabilities }}" as="c" hint-placeholder-count="3">
|
||||||
|
<span style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#cfcfd5; padding:4px 9px; border-radius:6px; background:rgba(255,255,255,.05);">{{ c }}</span>
|
||||||
|
</sc-for>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- SURFACE COLUMN -->
|
||||||
|
<div style="flex:1; min-width:0; display:flex; flex-direction:column; gap:12px; overflow-y:auto;">
|
||||||
|
|
||||||
|
<div style="flex:none; border-radius:13px; background:#0d0d10; border:1px solid rgba(94,200,216,.22); padding:12px 13px;">
|
||||||
|
<div style="display:flex; align-items:center; gap:8px; margin-bottom:10px;">
|
||||||
|
<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:6px; font-family:'JetBrains Mono',monospace; font-size:11px;">
|
||||||
|
<sc-for list="{{ tools }}" as="t" hint-placeholder-count="4">
|
||||||
|
<div style="display:flex; align-items:center;"><span style="flex:1; color:{{ t.nameColor }};">{{ t.name }}</span><span style="color:{{ t.stateColor }};">{{ t.state }}</span></div>
|
||||||
|
</sc-for>
|
||||||
|
</div>
|
||||||
|
<sc-if value="{{ showDoor }}" hint-placeholder-val="{{ true }}">
|
||||||
|
<div style="margin-top:11px; border-radius:10px; border:1px solid rgba(232,196,106,.4); background:linear-gradient(180deg,#1a1408,#12100a); overflow:hidden; animation:cm-fade .2s ease;">
|
||||||
|
<div style="height:3px; background:linear-gradient(90deg,#e8b465,#ff6f61);"></div>
|
||||||
|
<div style="padding:11px 12px;">
|
||||||
|
<div style="display:flex; align-items:center; gap:7px; margin-bottom:8px;">
|
||||||
|
<span style="font-family:'JetBrains Mono',monospace; font-size:9px; letter-spacing:.1em; color:#e8b465;">§15 DOOR · APPROVAL</span>
|
||||||
|
</div>
|
||||||
|
<div style="font-size:13px; font-weight:600; margin-bottom:3px;">{{ doorTitle }}</div>
|
||||||
|
<div style="font-family:'JetBrains Mono',monospace; font-size:10px; color:#8a8a92; line-height:1.6; margin-bottom:11px;">{{ doorDetail }}</div>
|
||||||
|
<div style="display:flex; gap:7px;">
|
||||||
|
<div style="flex:1; display:flex; align-items:center; justify-content:center; height:31px; border-radius:8px; background:linear-gradient(135deg,#ffb27a,#e8b465); color:#2a1d05; font-size:12px; font-weight:700; cursor:pointer;" onClick="{{ approveDoor }}">Approve</div>
|
||||||
|
<div style="flex:1; display:flex; align-items:center; justify-content:center; height:31px; border-radius:8px; border:1px solid rgba(255,255,255,.14); color:#cfcfd5; font-size:12px; font-weight:600; cursor:pointer;">Review</div>
|
||||||
|
<div style="width:34px; display:flex; align-items:center; justify-content:center; height:31px; border-radius:8px; border:1px solid rgba(255,255,255,.1); color:#8a8a92; cursor:pointer;" onClick="{{ approveDoor }}">✕</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</sc-if>
|
||||||
|
<sc-if value="{{ doorClear }}" hint-placeholder-val="{{ false }}">
|
||||||
|
<div style="margin-top:11px; display:flex; align-items:center; gap:7px; padding:9px 11px; border-radius:9px; background:rgba(95,208,138,.07); border:1px solid rgba(95,208,138,.22); font-family:'JetBrains Mono',monospace; font-size:10px; color:#5fd08a;">✓ no doors awaiting approval</div>
|
||||||
|
</sc-if>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="flex:none; border-radius:13px; background:#0d0d10; border:1px solid rgba(95,208,138,.2); padding:12px 13px;">
|
||||||
|
<div style="display:flex; align-items:center; gap:8px; margin-bottom:10px;">
|
||||||
|
<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:6px; font-family:'JetBrains Mono',monospace; font-size:11px; color:#9a9aa2;">
|
||||||
|
<sc-for list="{{ memory }}" as="m" hint-placeholder-count="3">
|
||||||
|
<div style="display:flex;"><span style="flex:1;">{{ m.k }}</span><span style="color:#cfcfd5;">{{ m.v }}</span></div>
|
||||||
|
</sc-for>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="flex:none; border-radius:13px; background:#0d0d10; border:1px solid rgba(111,208,192,.2); padding:12px 13px;">
|
||||||
|
<div style="display:flex; align-items:center; gap:8px; margin-bottom:10px;">
|
||||||
|
<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:6px; font-family:'JetBrains Mono',monospace; font-size:11px; color:#9a9aa2;">
|
||||||
|
<sc-for list="{{ safety }}" as="sf" hint-placeholder-count="3">
|
||||||
|
<div style="display:flex;"><span style="flex:1;">{{ sf.k }}</span><span style="color:{{ sf.color }};">{{ sf.v }}</span></div>
|
||||||
|
</sc-for>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- COMPUTER PULLOUT PANEL -->
|
||||||
|
<sc-if value="{{ showComputer }}" hint-placeholder-val="{{ true }}">
|
||||||
|
<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; animation:cm-fade .25s ease;">
|
||||||
|
<div style="flex:none; display:flex; align-items:center; gap:9px; padding:13px 14px 11px; 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;">{{ selName }}'s Computer</span>
|
||||||
|
<span style="flex:1;"></span>
|
||||||
|
<div style="display:flex; align-items:center; gap:8px; color:#5a5a62;">
|
||||||
|
<svg width="15" height="15" viewBox="0 0 18 18" style="cursor:pointer; color:#ff6f61;"><path d="M3 4a1.5 1.5 0 011.5-1.5h9A1.5 1.5 0 0115 4v6a1.5 1.5 0 01-1.5 1.5H7l-4 3.5V4z" fill="none" stroke="currentColor" stroke-width="1.3" stroke-linejoin="round"></path></svg>
|
||||||
|
<svg width="15" height="15" viewBox="0 0 18 18" style="cursor:pointer;"><rect x="2" y="3" width="14" height="9" rx="1.4" fill="none" stroke="currentColor" stroke-width="1.3"></rect><path d="M6 15h6" stroke="currentColor" stroke-width="1.3"></path></svg>
|
||||||
|
<svg width="15" height="15" viewBox="0 0 18 18" style="cursor:pointer;"><rect x="4" y="2.5" width="10" height="13" rx="1.6" fill="none" stroke="currentColor" stroke-width="1.3"></rect></svg>
|
||||||
|
<svg width="15" height="15" viewBox="0 0 18 18" style="cursor:pointer;"><rect x="5.5" y="2" width="7" height="14" rx="1.6" fill="none" stroke="currentColor" stroke-width="1.3"></rect><path d="M8 13.5h2" stroke="currentColor" stroke-width="1.3"></path></svg>
|
||||||
|
<span style="width:1px; height:14px; background:rgba(255,255,255,.1);"></span>
|
||||||
|
<svg width="14" height="14" viewBox="0 0 16 16" style="cursor:pointer;"><path d="M3.5 3.5l9 9M12.5 3.5l-9 9" stroke="currentColor" stroke-width="1.4"></path></svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="flex:none; display:flex; align-items:center; gap:7px; padding:9px 16px 0; font-family:'JetBrains Mono',monospace; font-size:10px; color:#5ec8d8;">
|
||||||
|
<span style="width:6px; height:6px; border-radius:50%; background:#5ec8d8; {{ blinkAnim }}"></span>sandbox live · no network
|
||||||
|
</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:13px;">APPS</div>
|
||||||
|
<div style="display:grid; grid-template-columns:repeat(4,1fr); gap:14px 6px; margin-bottom:22px;">
|
||||||
|
<div style="display:flex; flex-direction:column; align-items:center; gap:6px; cursor:pointer;"><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; cursor:pointer;"><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; cursor:pointer;"><div style="width:48px; height:48px; border-radius:13px; background:#070708; border:1px solid rgba(255,255,255,.1); display:flex; align-items:center; justify-content:center;"><svg width="22" height="22" viewBox="0 0 22 22"><rect x="2" y="3" width="18" height="15" rx="2" fill="none" stroke="#5fd08a" stroke-width="1.4"></rect><path d="M5 8l3 2.5-3 2.5M10.5 14h5" stroke="#5fd08a" stroke-width="1.4" fill="none"></path></svg></div><span style="font-size:10px; color:#b5b5bd;">Terminal</span></div>
|
||||||
|
<div style="display:flex; flex-direction:column; align-items:center; gap:6px; cursor:pointer;"><div style="width:48px; height:48px; border-radius:13px; background:linear-gradient(135deg,#c98af0,#7a4ad8); 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="M11 2 L18 8 L13 20 L6 16 L4 8 Z" fill="#fff" opacity=".92"></path><path d="M11 2 L13 20 M4 8 L13 20" stroke="#c98af0" stroke-width="1" fill="none"></path></svg></div><span style="font-size:10px; color:#b5b5bd;">Obsidian</span></div>
|
||||||
|
<div style="display:flex; flex-direction:column; align-items:center; gap:6px; cursor:pointer;"><div style="width:48px; height:48px; border-radius:13px; background:#070708; border:1px solid rgba(255,255,255,.1); display:flex; align-items:center; justify-content:center;"><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="none" stroke="#ff6f61" stroke-width="1.4" stroke-linejoin="round"></path></svg></div><span style="font-size:10px; color:#b5b5bd;">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:11px;">NOW RUNNING</div>
|
||||||
|
<sc-for list="{{ running }}" as="r" hint-placeholder-count="2">
|
||||||
|
<div style="padding:10px 11px; border-radius:10px; background:#101014; border:1px solid rgba(255,255,255,.06); margin-bottom:8px;">
|
||||||
|
<div style="display:flex; align-items:center; gap:7px; margin-bottom:6px;">
|
||||||
|
<span style="width:6px; height:6px; border-radius:50%; background:{{ r.dot }}; {{ r.dotAnim }}"></span>
|
||||||
|
<span style="font-size:12px; font-weight:600; color:#e6e6ea;">{{ r.name }}</span>
|
||||||
|
<span style="flex:1;"></span>
|
||||||
|
<span style="font-family:'JetBrains Mono',monospace; font-size:9px; color:{{ r.metaColor }};">{{ r.meta }}</span>
|
||||||
|
</div>
|
||||||
|
<sc-if value="{{ r.hasBar }}" hint-placeholder-val="{{ false }}">
|
||||||
|
<div style="height:4px; border-radius:2px; background:rgba(255,255,255,.08); overflow:hidden;"><div style="width:{{ r.bar }}; height:100%; background:linear-gradient(90deg,#5ec8d8,#4aa3b8);"></div></div>
|
||||||
|
</sc-if>
|
||||||
|
</div>
|
||||||
|
</sc-for>
|
||||||
|
</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; cursor:pointer;"><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; cursor:pointer;"><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; cursor:pointer;"><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; cursor:pointer;"><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:{{ doorsValColor }};">{{ doorsStatusText }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</x-dc>
|
||||||
|
<script type="text/x-dc" data-dc-script data-props="{"liveStream":{"editor":"boolean","default":true,"tsType":"boolean"},"showComputer":{"editor":"boolean","default":true,"tsType":"boolean"},"defaultPromptOpen":{"editor":"boolean","default":false,"tsType":"boolean"}}">
|
||||||
|
class Component extends DCLogic {
|
||||||
|
state = { selected: 'morpheus', promptOpen: null, doorApproved: false };
|
||||||
|
|
||||||
|
renderVals() {
|
||||||
|
const s = this.state;
|
||||||
|
const live = this.props.liveStream ?? true;
|
||||||
|
const showComputer = this.props.showComputer ?? true;
|
||||||
|
|
||||||
|
const AGENTS = {
|
||||||
|
morpheus: {
|
||||||
|
name: 'Morpheus', role: 'Project Manager', spec: 'Rust 2024 Specialist',
|
||||||
|
initial: 'M', grad: 'linear-gradient(135deg,#ff8a7a,#ff5f57)', ink: '#2a0d0a',
|
||||||
|
model: 'Claude Sonnet 4.5', modelShort: 'claude-sonnet', status: 'working', statusColor: '#5ec8d8',
|
||||||
|
action: '▸ reviewing PR #214',
|
||||||
|
tok: '38.4k', spend: '$0.42', loops: 3, mem: '2,418', doors: 1,
|
||||||
|
commits: '9,105', hot: '11',
|
||||||
|
taskTitle: 'Review PR #214 — borrow-checker fix in', taskCode: 'cm-runtime', taskElapsed: 'task · 02:14 elapsed',
|
||||||
|
taskSteps: [
|
||||||
|
{ icon: '✓', iconColor: '#5fd08a', text: 'read cm-runtime/src/sandbox.rs', meta: '· 3 hunks' },
|
||||||
|
{ icon: '✓', iconColor: '#5fd08a', text: 'ran cargo clippy --workspace', meta: '· clean' },
|
||||||
|
{ icon: '▸', iconColor: '#5ec8d8', text: "checking lifetime on &'a mut Guard across await", meta: '', caret: true },
|
||||||
|
],
|
||||||
|
reasoningTime: '18:16:09',
|
||||||
|
reasoning: [
|
||||||
|
{ tag: 'think', tagColor: '#c98af0', text: 'The original holds the MutexGuard across .await — the std::sync::Mutex-across-await footgun my rules forbid.' },
|
||||||
|
{ tag: 'note', tagColor: '#9a9aa2', text: 'The fix clones under a scoped lock, dropping the guard before the await point. Sound.' },
|
||||||
|
{ tag: 'tool', tagColor: '#5ec8d8', text: 'github.review.comment → door check…' },
|
||||||
|
{ tag: 'door', tagColor: '#e8b465', text: 'post review on PR #214 needs approval', caret: true },
|
||||||
|
],
|
||||||
|
personality: ['Analytical', 'Concise', 'Skeptical', 'Direct'],
|
||||||
|
skillsCount: 7, skills: ['Web research', 'Summarize', 'Data viz', 'Code review'], skillsMore: '+3',
|
||||||
|
capabilities: ['File mgmt', 'Scheduling', 'Code exec'],
|
||||||
|
tools: [
|
||||||
|
{ name: 'Email', nameColor: '#cfcfd5', state: 'gated ✓', stateColor: '#5fd08a' },
|
||||||
|
{ name: 'Slack', nameColor: '#cfcfd5', state: 'gated ✓', stateColor: '#5fd08a' },
|
||||||
|
{ name: 'Browser', nameColor: '#cfcfd5', state: 'gated ✓', stateColor: '#5fd08a' },
|
||||||
|
{ name: 'Shell', nameColor: '#7a7a82', state: 'blocked ⨯', stateColor: '#e8b465' },
|
||||||
|
],
|
||||||
|
memory: [
|
||||||
|
{ k: 'Long-term', v: '2,418 notes' },
|
||||||
|
{ k: 'Recent context', v: '18 msgs' },
|
||||||
|
{ k: 'Pinned facts', v: '4' },
|
||||||
|
],
|
||||||
|
safety: [
|
||||||
|
{ k: 'Sandbox', v: 'isolated', color: '#6fd0c0' },
|
||||||
|
{ k: 'Network', v: 'none', color: '#6fd0c0' },
|
||||||
|
{ k: 'Egress', v: 'gated', color: '#e8b465' },
|
||||||
|
],
|
||||||
|
doorTitle: 'Post code review on PR #214',
|
||||||
|
doorDetail: 'github.review.comment → external egress · drafts 1 comment',
|
||||||
|
running: [
|
||||||
|
{ name: 'Q3 competitor scan', dot: '#5ec8d8', blink: true, meta: 'loop · step 14', metaColor: '#5ec8d8', bar: '62%' },
|
||||||
|
{ name: 'Daily digest', dot: '#e8b465', blink: false, meta: 'cron · 08:00', metaColor: '#e8b465', bar: '' },
|
||||||
|
],
|
||||||
|
promptText: 'You are Morpheus, a senior engineering project manager for the Growth Team.\n\nYou review code with a focus on correctness, concurrency safety, and Rust 2024 idioms. You are concise and direct — flag the issue, propose the fix, cite the rule.\n\nNever hold a std::sync::Mutex guard across an .await point. Prefer scoped locks. Always run cargo clippy before approving.',
|
||||||
|
},
|
||||||
|
smith: {
|
||||||
|
name: 'Smith', role: 'Research Specialist', spec: 'Source Intelligence',
|
||||||
|
initial: 'S', grad: 'linear-gradient(135deg,#6fd0c0,#4aa3b8)', ink: '#06201f',
|
||||||
|
model: 'Claude Sonnet 4.5', modelShort: 'claude-sonnet', status: 'running', statusColor: '#5fd08a',
|
||||||
|
action: '▸ crawling 12 sources',
|
||||||
|
tok: '22.1k', spend: '$0.28', loops: 2, mem: '1,290', doors: 0,
|
||||||
|
commits: '4,602', hot: '6',
|
||||||
|
taskTitle: 'Crate audit — scan dependency tree in', taskCode: 'cargo audit', taskElapsed: 'task · 01:02 elapsed',
|
||||||
|
taskSteps: [
|
||||||
|
{ icon: '✓', iconColor: '#5fd08a', text: 'fetched advisory-db', meta: '· 184 crates' },
|
||||||
|
{ icon: '✓', iconColor: '#5fd08a', text: 'ran cargo audit --json', meta: '· 2 advisories' },
|
||||||
|
{ icon: '▸', iconColor: '#5ec8d8', text: 'summarizing RUSTSEC-2025-00xx (tokio)', meta: '', caret: true },
|
||||||
|
],
|
||||||
|
reasoningTime: '18:16:21',
|
||||||
|
reasoning: [
|
||||||
|
{ tag: 'search', tagColor: '#5ec8d8', text: 'crawling 12 sources for tokio CVE context and patched versions.' },
|
||||||
|
{ tag: 'note', tagColor: '#9a9aa2', text: 'Cross-referencing RUSTSEC + GHSA advisories; deduping by CVE id.' },
|
||||||
|
{ tag: 'tool', tagColor: '#5ec8d8', text: 'cargo.audit → running, no door required (read-only).' },
|
||||||
|
{ tag: 'result', tagColor: '#5fd08a', text: '2 advisories found · drafting summary for Morpheus', caret: true },
|
||||||
|
],
|
||||||
|
personality: ['Thorough', 'Curious', 'Literal'],
|
||||||
|
skillsCount: 6, skills: ['Web crawl', 'Summarize', 'Citations', 'Dedup'], skillsMore: '+2',
|
||||||
|
capabilities: ['Web fetch', 'Indexing', 'Export'],
|
||||||
|
tools: [
|
||||||
|
{ name: 'Browser', nameColor: '#cfcfd5', state: 'gated ✓', stateColor: '#5fd08a' },
|
||||||
|
{ name: 'Slack', nameColor: '#cfcfd5', state: 'gated ✓', stateColor: '#5fd08a' },
|
||||||
|
{ name: 'Email', nameColor: '#7a7a82', state: 'blocked ⨯', stateColor: '#e8b465' },
|
||||||
|
{ name: 'Shell', nameColor: '#7a7a82', state: 'blocked ⨯', stateColor: '#e8b465' },
|
||||||
|
],
|
||||||
|
memory: [
|
||||||
|
{ k: 'Long-term', v: '1,290 notes' },
|
||||||
|
{ k: 'Recent context', v: '11 msgs' },
|
||||||
|
{ k: 'Pinned facts', v: '2' },
|
||||||
|
],
|
||||||
|
safety: [
|
||||||
|
{ k: 'Sandbox', v: 'isolated', color: '#6fd0c0' },
|
||||||
|
{ k: 'Network', v: 'read-only', color: '#e8b465' },
|
||||||
|
{ k: 'Egress', v: 'none', color: '#6fd0c0' },
|
||||||
|
],
|
||||||
|
doorTitle: '', doorDetail: '',
|
||||||
|
running: [
|
||||||
|
{ name: 'Dependency watch', dot: '#5ec8d8', blink: true, meta: 'loop · step 7', metaColor: '#5ec8d8', bar: '48%' },
|
||||||
|
{ name: 'Nightly digest', dot: '#5fd08a', blink: false, meta: 'done · 02:00', metaColor: '#5fd08a', bar: '' },
|
||||||
|
],
|
||||||
|
promptText: 'You are Smith, a research specialist for the Growth Team.\n\nYou crawl, read, and synthesize sources into tightly-cited summaries. You never assert a fact without a source. You dedupe aggressively and prefer primary advisories (RUSTSEC, GHSA, NVD) over secondary reporting.\n\nYou are read-only by default — surface findings, do not act on them.',
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const a = AGENTS[s.selected] || AGENTS.morpheus;
|
||||||
|
const order = ['morpheus', 'smith'];
|
||||||
|
|
||||||
|
const agents = order.map(id => {
|
||||||
|
const ag = AGENTS[id];
|
||||||
|
const sel = id === s.selected;
|
||||||
|
return {
|
||||||
|
id, name: ag.name, role: ag.role, initial: ag.initial, grad: ag.grad, ink: ag.ink,
|
||||||
|
action: ag.action, statusColor: ag.statusColor,
|
||||||
|
selected: sel,
|
||||||
|
rowBg: sel ? 'rgba(255,111,97,.1)' : 'transparent',
|
||||||
|
rowBorder: sel ? 'rgba(255,111,97,.25)' : 'transparent',
|
||||||
|
labelColor: sel ? '#ffffff' : '#eaeaee',
|
||||||
|
roleColor: sel ? '#ff8a7a' : '#6a6a72',
|
||||||
|
onSelect: () => this.setState({ selected: id, doorApproved: false }),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
const defaultOpen = !!(this.props.defaultPromptOpen ?? false);
|
||||||
|
const promptOpen = s.promptOpen ?? defaultOpen;
|
||||||
|
const doorsLive = (a.doors > 0 && !s.doorApproved) ? a.doors : 0;
|
||||||
|
const showDoor = a.doors > 0 && !s.doorApproved;
|
||||||
|
const doorClear = a.doors === 0 || s.doorApproved;
|
||||||
|
const anim = (css) => live ? css : '';
|
||||||
|
|
||||||
|
return {
|
||||||
|
selName: a.name, selRole: a.role, selSpec: a.spec, selInitial: a.initial,
|
||||||
|
selGrad: a.grad, selInk: a.ink, selModel: a.model, selModelShort: a.modelShort,
|
||||||
|
selStatusLabel: a.status, selTok: a.tok, selSpend: a.spend, selLoops: a.loops, selMem: a.mem,
|
||||||
|
selCommits: a.commits, selHot: a.hot,
|
||||||
|
agents,
|
||||||
|
|
||||||
|
taskTitle: a.taskTitle, taskCode: a.taskCode, taskElapsed: a.taskElapsed, taskSteps: a.taskSteps,
|
||||||
|
reasoning: a.reasoning, reasoningTime: a.reasoningTime,
|
||||||
|
personality: a.personality,
|
||||||
|
skills: a.skills, skillsCount: a.skillsCount, skillsMore: a.skillsMore,
|
||||||
|
capabilities: a.capabilities,
|
||||||
|
tools: a.tools, memory: a.memory, safety: a.safety,
|
||||||
|
|
||||||
|
promptOpen, promptText: a.promptText,
|
||||||
|
promptChars: a.promptText.length.toLocaleString(),
|
||||||
|
promptChevron: promptOpen ? '▾' : '▸',
|
||||||
|
togglePrompt: () => this.setState({ promptOpen: !promptOpen }),
|
||||||
|
|
||||||
|
showDoor, doorClear, doorTitle: a.doorTitle, doorDetail: a.doorDetail,
|
||||||
|
approveDoor: () => this.setState({ doorApproved: true }),
|
||||||
|
doorsLive,
|
||||||
|
doorsBorder: doorsLive > 0 ? 'rgba(232,196,106,.3)' : 'rgba(255,255,255,.06)',
|
||||||
|
doorsLabelColor: doorsLive > 0 ? '#e8b465' : '#6a6a72',
|
||||||
|
doorsValColor: doorsLive > 0 ? '#e8b465' : '#5fd08a',
|
||||||
|
doorsStatusText: doorsLive > 0 ? (doorsLive + ' door awaiting approval') : 'no doors pending',
|
||||||
|
|
||||||
|
showComputer,
|
||||||
|
running: a.running.map(r => ({
|
||||||
|
name: r.name, meta: r.meta, metaColor: r.metaColor, dot: r.dot,
|
||||||
|
dotAnim: (live && r.blink) ? 'animation:cm-blink 1.4s infinite;' : '',
|
||||||
|
hasBar: !!r.bar, bar: r.bar,
|
||||||
|
})),
|
||||||
|
|
||||||
|
blinkAnim: anim('animation:cm-blink 1.4s infinite;'),
|
||||||
|
haloAnim: anim('animation:cm-halo 2s ease-out infinite;'),
|
||||||
|
caretAnim: anim('animation:cm-type 1s steps(1) infinite;'),
|
||||||
|
barAnim: anim('animation:cm-bar 1.6s ease-in-out infinite;'),
|
||||||
|
barAnim2: anim('animation:cm-bar 1.9s ease-in-out infinite;'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -122,6 +122,76 @@ async fn doors_pending(pool: &PgPool, ws: WorkspaceId) -> i64 {
|
|||||||
.unwrap_or(0)
|
.unwrap_or(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Live per-agent metrics for the agent command center's metric band.
|
||||||
|
#[derive(Default)]
|
||||||
|
struct AgentTele {
|
||||||
|
tokens_per_min: i64,
|
||||||
|
cost_per_hr: f64,
|
||||||
|
loops: i64,
|
||||||
|
doors: i64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Per-agent telemetry for every agent in the workspace, in 4 grouped queries
|
||||||
|
/// (not N×4): tokens over the last minute, credits over the last hour, active
|
||||||
|
/// routines, and pending approvals — all keyed by agent id.
|
||||||
|
async fn agent_telemetry(pool: &PgPool, ws: WorkspaceId) -> std::collections::HashMap<String, AgentTele> {
|
||||||
|
let mut m: std::collections::HashMap<String, AgentTele> = std::collections::HashMap::new();
|
||||||
|
let id_of = |r: &sqlx::postgres::PgRow| r.get::<uuid::Uuid, _>("agent_id").to_string();
|
||||||
|
|
||||||
|
// tokens in the last minute ≈ tokens/min.
|
||||||
|
for r in sqlx::query(
|
||||||
|
"SELECT agent_id, SUM(tokens_in + tokens_out)::bigint AS v FROM usage_events
|
||||||
|
WHERE workspace_id = $1 AND agent_id IS NOT NULL AND created_at > now() - interval '1 minute'
|
||||||
|
GROUP BY agent_id",
|
||||||
|
)
|
||||||
|
.bind(ws.as_uuid())
|
||||||
|
.fetch_all(pool)
|
||||||
|
.await
|
||||||
|
.unwrap_or_default()
|
||||||
|
{
|
||||||
|
m.entry(id_of(&r)).or_default().tokens_per_min = r.get("v");
|
||||||
|
}
|
||||||
|
// credits spent in the last hour ≈ spend/hr.
|
||||||
|
for r in sqlx::query(
|
||||||
|
"SELECT agent_id, SUM(credits)::float8 AS v FROM usage_events
|
||||||
|
WHERE workspace_id = $1 AND agent_id IS NOT NULL AND created_at > now() - interval '1 hour'
|
||||||
|
GROUP BY agent_id",
|
||||||
|
)
|
||||||
|
.bind(ws.as_uuid())
|
||||||
|
.fetch_all(pool)
|
||||||
|
.await
|
||||||
|
.unwrap_or_default()
|
||||||
|
{
|
||||||
|
m.entry(id_of(&r)).or_default().cost_per_hr = r.get("v");
|
||||||
|
}
|
||||||
|
// active routines = loops.
|
||||||
|
for r in sqlx::query(
|
||||||
|
"SELECT r.agent_id AS agent_id, count(*)::bigint AS v FROM routines r
|
||||||
|
JOIN agents a ON a.id = r.agent_id
|
||||||
|
WHERE a.workspace_id = $1 AND r.status = 'active' GROUP BY r.agent_id",
|
||||||
|
)
|
||||||
|
.bind(ws.as_uuid())
|
||||||
|
.fetch_all(pool)
|
||||||
|
.await
|
||||||
|
.unwrap_or_default()
|
||||||
|
{
|
||||||
|
m.entry(id_of(&r)).or_default().loops = r.get("v");
|
||||||
|
}
|
||||||
|
// pending approvals = doors.
|
||||||
|
for r in sqlx::query(
|
||||||
|
"SELECT requested_by_agent AS agent_id, count(*)::bigint AS v FROM approvals
|
||||||
|
WHERE workspace_id = $1 AND status = 'pending' GROUP BY requested_by_agent",
|
||||||
|
)
|
||||||
|
.bind(ws.as_uuid())
|
||||||
|
.fetch_all(pool)
|
||||||
|
.await
|
||||||
|
.unwrap_or_default()
|
||||||
|
{
|
||||||
|
m.entry(id_of(&r)).or_default().doors = r.get("v");
|
||||||
|
}
|
||||||
|
m
|
||||||
|
}
|
||||||
|
|
||||||
/// `GET /api/world/live` — the taxonomy SSE feed.
|
/// `GET /api/world/live` — the taxonomy SSE feed.
|
||||||
pub async fn world_live(State(state): State<AppState>, Authed(user): Authed) -> impl IntoResponse {
|
pub async fn world_live(State(state): State<AppState>, Authed(user): Authed) -> impl IntoResponse {
|
||||||
let pool = state.pool.clone();
|
let pool = state.pool.clone();
|
||||||
@@ -212,6 +282,22 @@ pub async fn world_live(State(state): State<AppState>, Authed(user): Authed) ->
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Per-agent telemetry → the command-center metric band (real data:
|
||||||
|
// usage_events tokens/credits, active routines, pending approvals).
|
||||||
|
let tele = agent_telemetry(&pool, ws).await;
|
||||||
|
for a in &roster {
|
||||||
|
let id = a.id.to_string();
|
||||||
|
let t = tele.get(&id);
|
||||||
|
yield sse("telemetry", json!({
|
||||||
|
"agentId": id,
|
||||||
|
"tokensPerMin": t.map(|x| x.tokens_per_min).unwrap_or(0),
|
||||||
|
"costPerHr": t.map(|x| x.cost_per_hr).unwrap_or(0.0),
|
||||||
|
"loops": t.map(|x| x.loops).unwrap_or(0),
|
||||||
|
"doorsPending": t.map(|x| x.doors).unwrap_or(0),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Workspace-wide telemetry (top-bar pills / Observe system strip).
|
||||||
yield sse(
|
yield sse(
|
||||||
"telemetry",
|
"telemetry",
|
||||||
json!({ "doorsPending": doors_pending(&pool, ws).await, "loops": runs.len() }),
|
json!({ "doorsPending": doors_pending(&pool, ws).await, "loops": runs.len() }),
|
||||||
|
|||||||
@@ -0,0 +1,286 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
// The Agent Command Center — the close-up operator surface for one agent.
|
||||||
|
// Identity shrinks to a header strip; live state + controls sit above the fold:
|
||||||
|
// a 5-tile metric band over three independently-scrolling columns (LIVE · BRAIN ·
|
||||||
|
// SURFACE). Read on the left/center, act on the right (the computer pullout).
|
||||||
|
|
||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { Activity, Brain, Camera, Cpu, Database, Drama, Settings, ShieldCheck, Wrench, Zap } from "lucide-react";
|
||||||
|
|
||||||
|
import type { DemoAgent } from "@/lib/dashboard-demo";
|
||||||
|
import { useAgentTelemetry, useLiveEvent } from "@/lib/live/useClawmatesLive";
|
||||||
|
import { AnatomyCard, PersonalityBody, SystemPromptCard, mono, tag, type RawBrain } from "./anatomy-cards";
|
||||||
|
import { AvatarModal } from "./AvatarModal";
|
||||||
|
import { AddToolModal } from "./AddToolModal";
|
||||||
|
|
||||||
|
const fmt = (n: number): string => (n >= 1000 ? `${(n / 1000).toFixed(n >= 10000 ? 0 : 1)}k` : `${n}`);
|
||||||
|
|
||||||
|
// ── Metric band ──────────────────────────────────────────────────────────────
|
||||||
|
function Sparkline({ values, color }: { values: number[]; color: string }) {
|
||||||
|
const max = Math.max(1, ...values);
|
||||||
|
const pts = values.map((v, i) => `${(i / Math.max(1, values.length - 1)) * 100},${26 - (v / max) * 24}`).join(" ");
|
||||||
|
return (
|
||||||
|
<svg width="50" height="26" viewBox="0 0 100 26" preserveAspectRatio="none" style={{ marginLeft: "auto", opacity: 0.85 }} aria-hidden>
|
||||||
|
<polyline points={pts} fill="none" stroke={color} strokeWidth="2" vectorEffect="non-scaling-stroke" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function MetricTile({ label, value, unit, tint, spark }: { label: string; value: string; unit?: string; tint?: string; spark?: number[] }) {
|
||||||
|
return (
|
||||||
|
<div style={{ flex: 1, minWidth: 0, borderRadius: 12, background: "#0d0d10", border: `1px solid ${tint ? `${tint}45` : "rgba(255,255,255,.07)"}`, padding: "10px 13px" }}>
|
||||||
|
<div style={{ fontFamily: mono, fontSize: 9.5, letterSpacing: ".1em", color: "#5a5a62", marginBottom: 5 }}>{label}</div>
|
||||||
|
<div style={{ display: "flex", alignItems: "baseline", gap: 5 }}>
|
||||||
|
<span style={{ fontSize: 22, fontWeight: 700, lineHeight: 1, color: tint ?? "#f3f3f5" }}>{value}</span>
|
||||||
|
{unit ? <span style={{ fontFamily: mono, fontSize: 10, color: "#6a6a72" }}>{unit}</span> : null}
|
||||||
|
{spark && spark.length > 1 ? <Sparkline values={spark} color={tint ?? "#5ec8d8"} /> : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── LIVE column ──────────────────────────────────────────────────────────────
|
||||||
|
type Step = { label: string; state: "done" | "active" | "pending" };
|
||||||
|
|
||||||
|
function WorkingOnNow({ agentId }: { agentId: string }) {
|
||||||
|
const [task, setTask] = useState<{ title: string; steps: Step[] } | null>(null);
|
||||||
|
useLiveEvent("agent.task.update", (d) => {
|
||||||
|
if (d.agentId === agentId) setTask({ title: d.title, steps: d.steps ?? [] });
|
||||||
|
});
|
||||||
|
const stepColor = (s: Step["state"]) => (s === "done" ? "#5fd08a" : s === "active" ? "#5ec8d8" : "#5a5a62");
|
||||||
|
return (
|
||||||
|
<AnatomyCard tint="#5ec8d8" label="WORKING ON NOW" icon={<Activity size={15} />}>
|
||||||
|
{!task ? (
|
||||||
|
<span style={{ fontFamily: mono, fontSize: 12.5, color: "#6a6a72" }}>idle — no active task. Live work streams here as the agent runs.</span>
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
<div style={{ fontSize: 13.5, fontWeight: 600, color: "#e6e6ea", marginBottom: task.steps.length ? 9 : 0 }}>{task.title}</div>
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: 5, fontFamily: mono, fontSize: 12 }}>
|
||||||
|
{task.steps.map((s, i) => (
|
||||||
|
<div key={i} style={{ display: "flex", gap: 7, color: stepColor(s.state) }}>
|
||||||
|
<span>{s.state === "done" ? "✓" : s.state === "active" ? "▸" : "·"}</span>
|
||||||
|
<span style={{ color: s.state === "pending" ? "#6a6a72" : "#cfcfd5" }}>
|
||||||
|
{s.label}
|
||||||
|
{s.state === "active" ? <span className="cm-blink" style={{ color: "#5ec8d8" }}> ▋</span> : null}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</AnatomyCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
type Think = { id: number; text: string; channel: "think" | "say" | "tool" };
|
||||||
|
const CHAN_COLOR: Record<Think["channel"], string> = { think: "#c98af0", say: "#cfcfd5", tool: "#5ec8d8" };
|
||||||
|
|
||||||
|
function ReasoningStream({ agentId }: { agentId: string }) {
|
||||||
|
const buf = useRef<Think[]>([]);
|
||||||
|
const seq = useRef(0);
|
||||||
|
const [lines, setLines] = useState<Think[]>([]);
|
||||||
|
useLiveEvent("agent.reasoning.delta", (d) => {
|
||||||
|
if (d.agentId !== agentId) return;
|
||||||
|
const t = d.text.trim();
|
||||||
|
if (t) buf.current = [...buf.current, { id: seq.current++, text: t, channel: d.channel ?? "think" }].slice(-80);
|
||||||
|
});
|
||||||
|
useLiveEvent("agent.tool.call", (d) => {
|
||||||
|
if (d.agentId !== agentId) return;
|
||||||
|
buf.current = [...buf.current, { id: seq.current++, text: `→ ${d.tool}${d.target ? ` ${d.target}` : ""}`, channel: "tool" as const }].slice(-80);
|
||||||
|
});
|
||||||
|
useEffect(() => {
|
||||||
|
const id = setInterval(() => setLines(buf.current.slice()), 300);
|
||||||
|
return () => clearInterval(id);
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<AnatomyCard tint="#c98af0" label="REASONING STREAM" icon={<Brain size={15} />}>
|
||||||
|
<div style={{ background: "#08080a", borderRadius: 8, border: "1px solid rgba(255,255,255,.05)", padding: "10px 12px", maxHeight: 320, overflowY: "auto", fontFamily: mono, fontSize: 11.5, lineHeight: 1.55 }}>
|
||||||
|
{lines.length === 0 ? (
|
||||||
|
<span style={{ color: "#3a3a40" }}>no reasoning yet — runs stream here.</span>
|
||||||
|
) : (
|
||||||
|
lines.map((l, i) => (
|
||||||
|
<div key={l.id} style={{ display: "flex", gap: 7 }}>
|
||||||
|
<span style={{ flex: "none", width: 34, color: CHAN_COLOR[l.channel], opacity: 0.8 }}>{l.channel}</span>
|
||||||
|
<span style={{ color: "#cfcfd5", whiteSpace: "pre-wrap", wordBreak: "break-word" }}>
|
||||||
|
{l.text}
|
||||||
|
{i === lines.length - 1 ? <span className="cm-blink" style={{ color: "#c98af0" }}> ▋</span> : null}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</AnatomyCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function ActivityBars({ agentId }: { agentId: string }) {
|
||||||
|
// Bucket tool-call counts into ~20 rolling time slots (one tick/sec).
|
||||||
|
const SLOTS = 22;
|
||||||
|
const buckets = useRef<number[]>(new Array(SLOTS).fill(0));
|
||||||
|
const [bars, setBars] = useState<number[]>(() => new Array(SLOTS).fill(0));
|
||||||
|
useLiveEvent("agent.tool.call", (d) => {
|
||||||
|
if (d.agentId === agentId) buckets.current[SLOTS - 1] += 1;
|
||||||
|
});
|
||||||
|
useEffect(() => {
|
||||||
|
const id = setInterval(() => {
|
||||||
|
buckets.current = [...buckets.current.slice(1), 0];
|
||||||
|
setBars(buckets.current.slice());
|
||||||
|
}, 1000);
|
||||||
|
return () => clearInterval(id);
|
||||||
|
}, []);
|
||||||
|
const max = Math.max(1, ...bars);
|
||||||
|
return (
|
||||||
|
<AnatomyCard tint="#5ec8d8" label="ACTIVITY · LIVE" icon={<Activity size={15} />}>
|
||||||
|
<div style={{ display: "flex", alignItems: "flex-end", gap: 3, height: 64 }}>
|
||||||
|
{bars.map((b, i) => (
|
||||||
|
<div key={i} style={{ flex: 1, height: `${Math.max(3, (b / max) * 100)}%`, borderRadius: 2, background: b > 0 ? "linear-gradient(180deg,#5ec8d8,#3a8694)" : "rgba(255,255,255,.06)", transition: "height .3s ease" }} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div style={{ fontFamily: mono, fontSize: 9.5, color: "#5a5a62", marginTop: 6 }}>tool calls · last {SLOTS}s</div>
|
||||||
|
</AnatomyCard>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Throughput tile with a rolling sparkline — keyed by agent so it resets on switch.
|
||||||
|
function ThroughputTile({ value }: { value: number }) {
|
||||||
|
const buf = useRef<number[]>([]);
|
||||||
|
const [spark, setSpark] = useState<number[]>([]);
|
||||||
|
useEffect(() => {
|
||||||
|
buf.current = [...buf.current, value].slice(-24);
|
||||||
|
}, [value]);
|
||||||
|
useEffect(() => {
|
||||||
|
const id = setInterval(() => setSpark(buf.current.slice()), 1000);
|
||||||
|
return () => clearInterval(id);
|
||||||
|
}, []);
|
||||||
|
return <MetricTile label="THROUGHPUT" value={fmt(value)} unit="tok/min" tint="#5ec8d8" spark={spark} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Command center ───────────────────────────────────────────────────────────
|
||||||
|
export function ClawCommandCenter({
|
||||||
|
agent,
|
||||||
|
teamName,
|
||||||
|
avatarUrl,
|
||||||
|
onToolsChanged,
|
||||||
|
brain,
|
||||||
|
onEditBrain,
|
||||||
|
}: {
|
||||||
|
agent: DemoAgent;
|
||||||
|
teamName: string;
|
||||||
|
avatarUrl?: string;
|
||||||
|
onToolsChanged?: () => void;
|
||||||
|
brain?: RawBrain;
|
||||||
|
onEditBrain?: () => void;
|
||||||
|
}) {
|
||||||
|
const c = agent.compartments;
|
||||||
|
const router = useRouter();
|
||||||
|
const [imgUrl, setImgUrl] = useState<string | null>(null);
|
||||||
|
const [seenImgId, setSeenImgId] = useState(agent.id);
|
||||||
|
if (seenImgId !== agent.id) { setSeenImgId(agent.id); setImgUrl(null); }
|
||||||
|
const [avatarOpen, setAvatarOpen] = useState(false);
|
||||||
|
const [addToolOpen, setAddToolOpen] = useState(false);
|
||||||
|
const shownAvatar = imgUrl ?? avatarUrl ?? null;
|
||||||
|
|
||||||
|
const tele = useAgentTelemetry(agent.id);
|
||||||
|
const doors = tele?.doorsPending ?? 0;
|
||||||
|
const skillsShown = c.skills.slice(0, 6);
|
||||||
|
const skillsRest = c.skills.length - skillsShown.length;
|
||||||
|
const row = (a: string, b: string, bc = "#cfcfd5") => (
|
||||||
|
<div style={{ display: "flex" }}><span style={{ flex: 1 }}>{a}</span><span style={{ color: bc }}>{b}</span></div>
|
||||||
|
);
|
||||||
|
const colStyle: React.CSSProperties = { flex: "1 1 0", minWidth: 0, minHeight: 0, overflowY: "auto", display: "flex", flexDirection: "column", gap: 14, paddingBottom: 8 };
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div style={{ position: "absolute", inset: 0, display: "flex", flexDirection: "column", background: "#08080a" }}>
|
||||||
|
{/* Identity header strip — compact. */}
|
||||||
|
<div style={{ flex: "none", display: "flex", alignItems: "center", gap: 15, padding: "15px 22px", borderBottom: "1px solid rgba(255,255,255,.06)" }}>
|
||||||
|
<div style={{ position: "relative", width: 62, height: 62, flex: "none" }}>
|
||||||
|
<div style={{ position: "absolute", inset: -5, borderRadius: "50%", border: "1.5px solid rgba(255,111,97,.35)" }} />
|
||||||
|
<div style={{ position: "absolute", inset: 0, borderRadius: "50%", background: "rgba(255,111,97,.3)", animation: "cm-halo 2s ease-out infinite" }} />
|
||||||
|
<button type="button" onClick={() => setAvatarOpen(true)} title="Click to edit image" style={{ position: "relative", width: 62, height: 62, borderRadius: "50%", border: 0, padding: 0, cursor: "pointer", background: shownAvatar ? `center/cover no-repeat url(${shownAvatar})` : agent.grad, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 24, fontWeight: 700, color: agent.ink, boxShadow: "0 0 32px rgba(255,111,97,.45)" }}>{shownAvatar ? null : agent.initial}</button>
|
||||||
|
<span aria-hidden style={{ position: "absolute", right: -1, bottom: -1, width: 22, height: 22, borderRadius: "50%", background: "#15151a", border: "1.5px solid #08080a", display: "flex", alignItems: "center", justifyContent: "center", color: "#cfcfd5", pointerEvents: "none" }}><Camera size={11} /></span>
|
||||||
|
</div>
|
||||||
|
<div style={{ minWidth: 0 }}>
|
||||||
|
<div style={{ fontSize: 20, fontWeight: 800, color: "#f3f3f5", letterSpacing: "-.02em" }}>{agent.name}</div>
|
||||||
|
<div style={{ fontFamily: mono, fontSize: 11.5, letterSpacing: ".06em", color: "#ff8a7a", marginTop: 2 }}>{agent.role}<span style={{ color: "#5a5a62" }}> · part of {teamName}</span></div>
|
||||||
|
</div>
|
||||||
|
<span style={{ flex: 1 }} />
|
||||||
|
<span style={{ fontFamily: mono, fontSize: 11, color: "#9a9aa2", padding: "5px 11px", borderRadius: 999, background: "rgba(255,255,255,.04)", border: "1px solid rgba(255,255,255,.1)" }}>{agent.model || "model —"}</span>
|
||||||
|
<button type="button" onClick={onEditBrain} style={{ display: "inline-flex", alignItems: "center", gap: 6, fontFamily: mono, fontSize: 11.5, color: "#ff8a7a", padding: "6px 13px", borderRadius: 8, background: "rgba(255,111,97,.08)", border: "1px solid rgba(255,111,97,.4)", cursor: "pointer" }}>Edit brain →</button>
|
||||||
|
<button type="button" aria-label="Settings" style={{ display: "inline-flex", alignItems: "center", justifyContent: "center", width: 34, height: 34, borderRadius: 8, color: "#9a9aa2", background: "transparent", border: "1px solid rgba(255,255,255,.1)", cursor: "pointer" }}><Settings size={16} /></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Metric band — per-agent. */}
|
||||||
|
<div style={{ flex: "none", display: "flex", gap: 10, padding: "12px 22px" }}>
|
||||||
|
<ThroughputTile key={agent.id} value={tele?.tokensPerMin ?? 0} />
|
||||||
|
<MetricTile label="SPEND" value={(tele?.costPerHr ?? 0).toFixed(2)} unit="cr/hr" tint="#e8b465" />
|
||||||
|
<MetricTile label="LOOPS" value={`${tele?.loops ?? 0}`} unit="active" tint="#5fd08a" />
|
||||||
|
<MetricTile label="MEMORY" value={fmt(brain?.stats.memories ?? 0)} unit="notes" />
|
||||||
|
<MetricTile label="DOORS" value={`${doors}`} unit={doors > 0 ? "pending" : "clear"} tint={doors > 0 ? "#e8b465" : "#5fd08a"} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Three independently-scrolling columns. */}
|
||||||
|
<div style={{ flex: "1 1 0", minHeight: 0, display: "flex", gap: 14, padding: "4px 22px 18px" }}>
|
||||||
|
{/* LIVE */}
|
||||||
|
<div style={{ ...colStyle, flex: "1.15 1 0" }}>
|
||||||
|
<WorkingOnNow key={`won-${agent.id}`} agentId={agent.id} />
|
||||||
|
<ReasoningStream key={`rs-${agent.id}`} agentId={agent.id} />
|
||||||
|
<ActivityBars key={`act-${agent.id}`} agentId={agent.id} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* BRAIN */}
|
||||||
|
<div style={colStyle}>
|
||||||
|
<SystemPromptCard prompt={agent.systemPrompt} />
|
||||||
|
<AnatomyCard tint="#c98af0" label="PERSONALITY" icon={<Drama size={15} />} collapsible>
|
||||||
|
<PersonalityBody raw={brain?.personality} fallback={c.personality} />
|
||||||
|
</AnatomyCard>
|
||||||
|
<AnatomyCard tint="#ff6f61" label="SKILLS" count={String(c.skills.length)} icon={<Zap size={15} />}>
|
||||||
|
<div style={{ display: "flex", flexWrap: "wrap", gap: 5 }}>
|
||||||
|
{skillsShown.map((s) => (<span key={s} style={tag("#ff6f61")}>{s}</span>))}
|
||||||
|
{skillsRest > 0 ? <span style={tag("#9a9aa2", true)}>+{skillsRest}</span> : null}
|
||||||
|
</div>
|
||||||
|
</AnatomyCard>
|
||||||
|
<AnatomyCard tint="#e8b465" label="CAPABILITIES" icon={<Cpu size={15} />}>
|
||||||
|
<div style={{ display: "flex", flexWrap: "wrap", gap: 5 }}>{c.capabilities.map((p) => (<span key={p} style={tag("#e8b465")}>{p}</span>))}</div>
|
||||||
|
</AnatomyCard>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* SURFACE */}
|
||||||
|
<div style={colStyle}>
|
||||||
|
<AnatomyCard tint="#5ec8d8" label="TOOLS · DOORS" icon={<Wrench size={15} />}>
|
||||||
|
{doors > 0 ? (
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: 9, padding: "8px 11px", borderRadius: 8, background: "rgba(232,180,101,.08)", border: "1px solid rgba(232,180,101,.4)", fontFamily: mono, fontSize: 11.5, color: "#e8b465" }}>
|
||||||
|
<span style={{ width: 6, height: 6, borderRadius: "50%", background: "#e8b465" }} className="cm-blink" />
|
||||||
|
§15 · {doors} door{doors === 1 ? "" : "s"} awaiting approval
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: 6, fontFamily: mono, fontSize: 12.5 }}>
|
||||||
|
{c.tools.map((t) => row(t.name, t.state === "gated" ? "gated ✓" : "blocked ⨯", t.state === "gated" ? "#5fd08a" : "#e8b465"))}
|
||||||
|
</div>
|
||||||
|
<button type="button" onClick={() => setAddToolOpen(true)} style={{ marginTop: 10, width: "100%", padding: "8px 0", borderRadius: 7, border: "1px dashed rgba(94,200,216,.4)", background: "transparent", color: "#5ec8d8", fontSize: 12.5, fontFamily: mono, cursor: "pointer" }}>+ Add tool</button>
|
||||||
|
</AnatomyCard>
|
||||||
|
<AnatomyCard tint="#5fd08a" label="MEMORY" count={brain ? String(brain.stats.memories) : undefined} icon={<Database size={15} />}>
|
||||||
|
{brain && brain.memory.length ? (
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: 5, fontFamily: mono, fontSize: 12.5, color: "#9a9aa2", maxHeight: 200, overflowY: "auto" }}>
|
||||||
|
{brain.memory.slice(0, 8).map((m, i) => (<div key={i} title={m} style={{ whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{m}</div>))}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: 6, fontFamily: mono, fontSize: 12.5, color: "#9a9aa2" }}>{brain ? <span style={{ color: "#6a6a72" }}>no memories yet — chat with this agent and they appear here.</span> : (<>{row("Long-term", c.memory.long)}{row("Recent ctx", c.memory.recent)}</>)}</div>
|
||||||
|
)}
|
||||||
|
</AnatomyCard>
|
||||||
|
<AnatomyCard tint="#6fd0c0" label="SAFETY · §15" icon={<ShieldCheck size={15} />}>
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: 6, fontFamily: mono, fontSize: 12.5, color: "#9a9aa2" }}>{row("Sandbox", c.safety.sandbox, "#6fd0c0")}{row("Network", c.safety.network, "#6fd0c0")}</div>
|
||||||
|
</AnatomyCard>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{avatarOpen ? (
|
||||||
|
<AvatarModal clawId={agent.id} clawName={agent.name} current={shownAvatar} onClose={() => setAvatarOpen(false)} onSaved={(url) => { setImgUrl(url); router.refresh(); }} />
|
||||||
|
) : null}
|
||||||
|
{addToolOpen ? (
|
||||||
|
<AddToolModal clawId={agent.id} clawName={agent.name} onClose={() => setAddToolOpen(false)} onAdded={() => onToolsChanged?.()} />
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ import { useEffect, useRef, useState, type CSSProperties } from "react";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter, useSearchParams } from "next/navigation";
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
import { useQueryStates } from "nuqs";
|
import { useQueryStates } from "nuqs";
|
||||||
import { Activity, Bot, Brain, Camera, Cpu, Database, Drama, History, MessageSquare, Monitor, PanelRight, ScrollText, ShieldCheck, Trash2, Users, Wrench, X, Zap } from "lucide-react";
|
import { Bot, Brain, History, MessageSquare, Monitor, PanelRight, Trash2, Users, Wrench, X } from "lucide-react";
|
||||||
|
|
||||||
import type { DemoAgent, DemoCompany, DemoOrg, DemoTeam } from "@/lib/dashboard-demo";
|
import type { DemoAgent, DemoCompany, DemoOrg, DemoTeam } from "@/lib/dashboard-demo";
|
||||||
import type { Agent } from "@/lib/api/schemas";
|
import type { Agent } from "@/lib/api/schemas";
|
||||||
@@ -29,11 +29,7 @@ import { InfraNav, FleetConsole, FleetStatusBar, FleetPill } from "./fleet/Fleet
|
|||||||
import { NodeMonitor } from "./fleet/NodeMonitor";
|
import { NodeMonitor } from "./fleet/NodeMonitor";
|
||||||
import { ConnectHostWizard } from "./ConnectHostWizard";
|
import { ConnectHostWizard } from "./ConnectHostWizard";
|
||||||
import { INFRA_CATALOG } from "@/components/computer/catalogs/infra";
|
import { INFRA_CATALOG } from "@/components/computer/catalogs/infra";
|
||||||
import { ClawChatSection } from "./ClawChatSection";
|
import { ClawCommandCenter } from "./ClawCommandCenter";
|
||||||
import { ResizableSplit } from "./ResizableSplit";
|
|
||||||
import { VitalsCard } from "./VitalsCard";
|
|
||||||
import { AvatarModal } from "./AvatarModal";
|
|
||||||
import { AddToolModal } from "./AddToolModal";
|
|
||||||
import { MasterPlannerModal } from "./MasterPlannerModal";
|
import { MasterPlannerModal } from "./MasterPlannerModal";
|
||||||
import { BrainRegistryPanel } from "./BrainRegistryPanel";
|
import { BrainRegistryPanel } from "./BrainRegistryPanel";
|
||||||
import { BrainHistoryModal } from "./BrainHistoryModal";
|
import { BrainHistoryModal } from "./BrainHistoryModal";
|
||||||
@@ -718,22 +714,11 @@ export function Dashboard({ user, orgs, claws }: { user?: { display_name?: strin
|
|||||||
<BrainRegistryPanel clawId={clawAgent.id} clawName={clawAgent.name} onApplied={() => setEnrichBump((b) => b + 1)} onClose={() => setRegistryOpen(false)} />
|
<BrainRegistryPanel clawId={clawAgent.id} clawName={clawAgent.name} onApplied={() => setEnrichBump((b) => b + 1)} onClose={() => setRegistryOpen(false)} />
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{/* Left region: anatomy profile + native chat, condensed by the
|
{/* Left region: the full-height agent command center (identity strip →
|
||||||
computer's width (and the registry rail). The two are a resizable
|
metrics band → LIVE/BRAIN/SURFACE columns). Chat lives in the computer's
|
||||||
split — drag the divider to grow the chat. */}
|
Chat app now. Condensed by the computer's width + the registry rail. */}
|
||||||
<div style={{ position: "absolute", top: 0, bottom: 0, left: registryOpen && device !== "full" ? 510 : 0, right: "var(--computer-width)", transition: "left var(--duration-normal) var(--ease-app), right var(--duration-normal) var(--ease-app)" }}>
|
<div style={{ position: "absolute", top: 0, bottom: 0, left: registryOpen && device !== "full" ? 510 : 0, right: "var(--computer-width)", transition: "left var(--duration-normal) var(--ease-app), right var(--duration-normal) var(--ease-app)" }}>
|
||||||
{chatMin ? (
|
<ClawCommandCenter agent={richAgent} brain={richBrain} teamName={team.name} avatarUrl={clawAgent.avatar || undefined} onToolsChanged={() => setEnrichBump((b) => b + 1)} onEditBrain={() => setRegistryOpen(true)} />
|
||||||
// Chat minimized → anatomy takes the whole left region.
|
|
||||||
<ClawAnatomyCanvas agent={richAgent} brain={richBrain} teamName={team.name} avatarUrl={clawAgent.avatar || undefined} onToolsChanged={() => setEnrichBump((b) => b + 1)} />
|
|
||||||
) : (
|
|
||||||
<ResizableSplit
|
|
||||||
defaultRatio={0.55}
|
|
||||||
minTop={44}
|
|
||||||
minBottom={180}
|
|
||||||
top={<ClawAnatomyCanvas agent={richAgent} brain={richBrain} teamName={team.name} avatarUrl={clawAgent.avatar || undefined} onToolsChanged={() => setEnrichBump((b) => b + 1)} />}
|
|
||||||
bottom={<ClawChatSection agent={clawAgent} onOpenComputerApp={(a) => setParams({ app: a })} onMinimize={() => setChatMin(true)} />}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
{/* Right: the original device computer (phone / tablet / desktop). */}
|
{/* Right: the original device computer (phone / tablet / desktop). */}
|
||||||
<DevicePanel agent={clawAgent} />
|
<DevicePanel agent={clawAgent} />
|
||||||
@@ -758,12 +743,10 @@ export function Dashboard({ user, orgs, claws }: { user?: { display_name?: strin
|
|||||||
<div style={{ position: "absolute", top: "50%", left: "50%", transform: "translate(-50%,-50%)", width: 4, height: 46, borderRadius: 3, background: resizing ? "#ff8a7a" : "rgba(255,255,255,.22)" }} />
|
<div style={{ position: "absolute", top: "50%", left: "50%", transform: "translate(-50%,-50%)", width: 4, height: 46, borderRadius: 3, background: resizing ? "#ff8a7a" : "rgba(255,255,255,.22)" }} />
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
{/* Top-right launchers: chat (left) + computer (right), each a coral
|
{/* Top-right launchers: chat shortcut (opens the computer's Chat app) +
|
||||||
icon when collapsed; controls show when the computer is open. */}
|
computer; controls show when the computer is open. */}
|
||||||
<div style={{ position: "absolute", top: 14, right: 16, zIndex: 50, display: "flex", alignItems: "center", gap: 8 }}>
|
<div style={{ position: "absolute", top: 14, right: 16, zIndex: 50, display: "flex", alignItems: "center", gap: 8 }}>
|
||||||
{chatMin ? (
|
<button type="button" aria-label="Open chat" title="Chat" onClick={() => setParams({ app: "chat", device: device === "full" || device === "tablet" ? device : "phone" })} style={{ display: "flex", alignItems: "center", justifyContent: "center", width: 38, height: 38, borderRadius: "50%", border: "1px solid rgba(255,111,97,.4)", background: "rgba(255,111,97,.08)", color: "#ff6f61", cursor: "pointer" }}><MessageSquare aria-hidden size={19} /></button>
|
||||||
<button type="button" aria-label="Open chat" title="Chat" onClick={() => setChatMin(false)} style={{ display: "flex", alignItems: "center", justifyContent: "center", width: 38, height: 38, borderRadius: "50%", border: "1px solid rgba(255,111,97,.4)", background: "rgba(255,111,97,.08)", color: "#ff6f61", cursor: "pointer" }}><MessageSquare aria-hidden size={19} /></button>
|
|
||||||
) : null}
|
|
||||||
{computerOpen ? (
|
{computerOpen ? (
|
||||||
<>
|
<>
|
||||||
<DeviceSizeToggle value={device} onChange={(d) => { setParams({ device: d }); setCustomWidth(null); }} />
|
<DeviceSizeToggle value={device} onChange={(d) => { setParams({ device: d }); setCustomWidth(null); }} />
|
||||||
@@ -833,235 +816,3 @@ function EmptyStage({ label }: { label: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Deploy wizard modal ─────────────────────────────────────────────────────
|
|
||||||
// ── Claw tier: the full-profile anatomy stage (sidebar is the all-claws tree) ──
|
|
||||||
|
|
||||||
// Inline markdown: **bold** and `code` → styled spans (everything else verbatim).
|
|
||||||
function renderInline(text: string, kp: string): React.ReactNode[] {
|
|
||||||
return text.split(/(\*\*[^*]+\*\*|`[^`]+`)/g).filter(Boolean).map((p, i) => {
|
|
||||||
if (p.startsWith("**") && p.endsWith("**")) return <strong key={`${kp}-${i}`} style={{ color: "#fff", fontWeight: 700 }}>{p.slice(2, -2)}</strong>;
|
|
||||||
if (p.startsWith("`") && p.endsWith("`")) return <code key={`${kp}-${i}`} style={{ fontFamily: mono, fontSize: "0.9em", background: "rgba(255,255,255,.06)", padding: "1px 5px", borderRadius: 4, color: "#e6e6ea" }}>{p.slice(1, -1)}</code>;
|
|
||||||
return <span key={`${kp}-${i}`}>{p}</span>;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// A tiny, dependency-free Markdown renderer: headings (#/##/###), bullet lists
|
|
||||||
// (- / *), bold/code, and paragraphs. Turns the raw prompt into readable prose
|
|
||||||
// instead of showing literal `##`.
|
|
||||||
function MarkdownText({ text, baseSize = 14 }: { text: string; baseSize?: number }) {
|
|
||||||
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
|
||||||
const blocks: React.ReactNode[] = [];
|
|
||||||
let para: string[] = [];
|
|
||||||
let list: string[] = [];
|
|
||||||
const flushPara = () => {
|
|
||||||
if (para.length) { const k = `p${blocks.length}`; blocks.push(<p key={k} style={{ margin: "0 0 10px", fontSize: baseSize, lineHeight: 1.65, color: "#d6d6dc" }}>{renderInline(para.join(" "), k)}</p>); para = []; }
|
|
||||||
};
|
|
||||||
const flushList = () => {
|
|
||||||
if (list.length) { const k = `u${blocks.length}`; blocks.push(<ul key={k} style={{ margin: "0 0 10px", paddingLeft: 18, display: "flex", flexDirection: "column", gap: 5 }}>{list.map((li, i) => <li key={i} style={{ fontSize: baseSize, lineHeight: 1.55, color: "#d6d6dc" }}>{renderInline(li, `${k}-${i}`)}</li>)}</ul>); list = []; }
|
|
||||||
};
|
|
||||||
for (const raw of lines) {
|
|
||||||
const line = raw.replace(/\s+$/, "");
|
|
||||||
const h = line.match(/^(#{1,6})\s+(.*)$/);
|
|
||||||
const li = line.match(/^\s*[-*]\s+(.*)$/);
|
|
||||||
if (h) { flushPara(); flushList(); const lvl = h[1].length; const size = lvl <= 1 ? baseSize + 5 : lvl === 2 ? baseSize + 2 : baseSize + 1; const k = `h${blocks.length}`; blocks.push(<div key={k} style={{ fontSize: size, fontWeight: 700, color: "#fff", letterSpacing: "-.01em", margin: blocks.length ? "15px 0 7px" : "0 0 7px" }}>{renderInline(h[2], k)}</div>); }
|
|
||||||
else if (li) { flushPara(); list.push(li[1]); }
|
|
||||||
else if (line.trim() === "") { flushPara(); flushList(); }
|
|
||||||
else { flushList(); para.push(line.replace(/^\s*>\s?/, "")); }
|
|
||||||
}
|
|
||||||
flushPara(); flushList();
|
|
||||||
return <div>{blocks}</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function SystemPromptCard({ prompt }: { prompt: string }) {
|
|
||||||
const [open, setOpen] = useState(true);
|
|
||||||
return (
|
|
||||||
<div style={{ borderRadius: 12, background: "#0f0f13", border: "1px solid rgba(255,111,97,.3)", padding: "16px 18px", boxShadow: "0 8px 22px rgba(0,0,0,.4)" }}>
|
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: open ? 13 : 0 }}>
|
|
||||||
<span style={{ width: 26, height: 26, flex: "none", borderRadius: 7, background: "rgba(255,111,97,.18)", border: "1px solid rgba(255,111,97,.35)", display: "inline-flex", alignItems: "center", justifyContent: "center", color: "#ff8a7a" }}><ScrollText size={15} /></span>
|
|
||||||
<span style={{ fontFamily: mono, fontSize: 12.5, fontWeight: 600, letterSpacing: ".05em", color: "#ff8a7a" }}>SYSTEM PROMPT</span>
|
|
||||||
<span style={{ flex: 1 }} />
|
|
||||||
<CollapseTick open={open} color="#ff8a7a" onClick={() => setOpen((o) => !o)} />
|
|
||||||
</div>
|
|
||||||
{open ? (prompt?.trim() ? <MarkdownText text={prompt} baseSize={14} /> : <p style={{ fontSize: 13, color: "#6a6a72", margin: 0 }}>No system prompt set.</p>) : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// A same-colored triangle toggle: points down when open, left when closed.
|
|
||||||
function CollapseTick({ open, color, onClick }: { open: boolean; color: string; onClick: () => void }) {
|
|
||||||
return (
|
|
||||||
<button type="button" onClick={onClick} aria-label={open ? "Collapse" : "Expand"} aria-expanded={open}
|
|
||||||
style={{ flex: "none", border: 0, background: "transparent", cursor: "pointer", color, padding: 4, display: "inline-flex", alignItems: "center", justifyContent: "center" }}>
|
|
||||||
<span style={{ display: "inline-block", fontSize: 11, lineHeight: 1, transform: open ? "rotate(0deg)" : "rotate(90deg)", transition: "transform .15s ease" }}>▼</span>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Personality is often stored as JSON (traits/tone/…). Render it as labeled
|
|
||||||
// sections instead of dumping raw JSON; fall back to tags / text otherwise.
|
|
||||||
function PersonalityBody({ raw, fallback }: { raw: string | null | undefined; fallback: string[] }) {
|
|
||||||
const tags = (arr: string[]) => <div style={{ display: "flex", flexWrap: "wrap", gap: 5 }}>{arr.map((p, i) => <span key={i} style={tag("#c98af0")}>{p}</span>)}</div>;
|
|
||||||
const trimmed = (raw ?? "").trim();
|
|
||||||
let parsed: unknown;
|
|
||||||
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
||||||
try { parsed = JSON.parse(trimmed); } catch { parsed = undefined; }
|
|
||||||
}
|
|
||||||
if (Array.isArray(parsed)) return tags(parsed.map((x) => String(x)));
|
|
||||||
if (parsed && typeof parsed === "object") {
|
|
||||||
return (
|
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: 9 }}>
|
|
||||||
{Object.entries(parsed as Record<string, unknown>).map(([k, v]) => (
|
|
||||||
<div key={k}>
|
|
||||||
<div style={{ fontFamily: mono, fontSize: 10.5, letterSpacing: ".08em", color: "#b9a6e0", marginBottom: 5, textTransform: "uppercase" }}>{k.replace(/_/g, " ")}</div>
|
|
||||||
{Array.isArray(v)
|
|
||||||
? tags(v.map((x) => String(x)))
|
|
||||||
: v && typeof v === "object"
|
|
||||||
? <div style={{ display: "flex", flexDirection: "column", gap: 3, fontSize: 13, color: "#cfcfd5", lineHeight: 1.55 }}>{Object.entries(v as Record<string, unknown>).map(([kk, vv]) => <div key={kk}><span style={{ color: "#8a8a92" }}>{kk}:</span> {String(vv)}</div>)}</div>
|
|
||||||
: <div style={{ fontSize: 13.5, color: "#d6d6dc", lineHeight: 1.55 }}>{String(v)}</div>}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (fallback.length) return tags(fallback);
|
|
||||||
if (trimmed) return <div style={{ fontSize: 13.5, color: "#d6d6dc", lineHeight: 1.6 }}>{trimmed}</div>;
|
|
||||||
return <span style={{ fontSize: 12, color: "#6a6a72" }}>—</span>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function AnatomyCard({ tint, label, count, icon, collapsible, children }: { tint: string; label: string; count?: string; icon?: React.ReactNode; collapsible?: boolean; children: React.ReactNode }) {
|
|
||||||
const [open, setOpen] = useState(true);
|
|
||||||
const collapsed = !!collapsible && !open;
|
|
||||||
return (
|
|
||||||
<div style={{ width: "100%", height: collapsed ? "auto" : "100%" }}>
|
|
||||||
<div style={{ height: collapsed ? "auto" : "100%", boxSizing: "border-box", display: "flex", flexDirection: "column", borderRadius: 12, background: "#0f0f13", border: `1px solid ${tint}38`, padding: 12, boxShadow: "0 8px 22px rgba(0,0,0,.4)" }}>
|
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: collapsed ? 0 : 11 }}>
|
|
||||||
<span style={{ width: 26, height: 26, flex: "none", borderRadius: 7, background: `${tint}26`, border: `1px solid ${tint}40`, display: "inline-flex", alignItems: "center", justifyContent: "center", color: tint }}>{icon}</span>
|
|
||||||
<span style={{ fontFamily: mono, fontSize: 12.5, fontWeight: 600, letterSpacing: ".05em", color: tint }}>{label}</span>
|
|
||||||
{count || collapsible ? <span style={{ flex: 1 }} /> : null}
|
|
||||||
{count ? <span style={{ fontFamily: mono, fontSize: 10, color: "#5a5a62" }}>{count}</span> : null}
|
|
||||||
{collapsible ? <CollapseTick open={open} color={tint} onClick={() => setOpen((o) => !o)} /> : null}
|
|
||||||
</div>
|
|
||||||
{!collapsed ? <div style={{ flex: 1, minHeight: 0 }}>{children}</div> : null}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
// A pill-shaped tag, tinted to its section's accent so it reads clearly as a
|
|
||||||
// tag. `dim` is the muted "+N more" variant.
|
|
||||||
const tag = (color = "#9a9aa2", dim = false): CSSProperties => ({ fontFamily: mono, fontSize: 12, color: dim ? "#8a8a92" : color, padding: "4px 10px", borderRadius: 7, background: dim ? "rgba(255,255,255,.04)" : `${color}1f`, border: `1px solid ${dim ? "rgba(255,255,255,.08)" : `${color}45`}` });
|
|
||||||
|
|
||||||
function ClawAnatomyCanvas({ agent, teamName, avatarUrl, onToolsChanged, brain }: { agent: DemoAgent; teamName: string; avatarUrl?: string; onToolsChanged?: () => void; brain?: RawBrain }) {
|
|
||||||
const c = agent.compartments;
|
|
||||||
const row = (a: string, b: string, bc = "#cfcfd5") => (<div style={{ display: "flex" }}><span style={{ flex: 1 }}>{a}</span><span style={{ color: bc }}>{b}</span></div>);
|
|
||||||
const skillsShown = c.skills.slice(0, 3);
|
|
||||||
const skillsRest = c.skills.length - skillsShown.length;
|
|
||||||
// Avatar: click opens the image modal (upload / generate / save). `imgUrl`
|
|
||||||
// holds a just-saved image for instant feedback; otherwise the persisted
|
|
||||||
// `avatarUrl` (agent.avatar) is shown. Both reset when the claw changes.
|
|
||||||
const [imgUrl, setImgUrl] = useState<string | null>(null);
|
|
||||||
const [seenImgId, setSeenImgId] = useState(agent.id);
|
|
||||||
if (seenImgId !== agent.id) { setSeenImgId(agent.id); setImgUrl(null); }
|
|
||||||
const [avatarOpen, setAvatarOpen] = useState(false);
|
|
||||||
const [addToolOpen, setAddToolOpen] = useState(false);
|
|
||||||
const shownAvatar = imgUrl ?? avatarUrl ?? null;
|
|
||||||
const router = useRouter();
|
|
||||||
return (
|
|
||||||
<div style={{ position: "absolute", inset: 0, display: "flex", flexDirection: "column", background: "radial-gradient(120% 90% at 50% 42%, #100e13 0%, #08080a 70%)" }}>
|
|
||||||
<div style={{ position: "relative", flex: 1, minHeight: 0, overflow: "auto", padding: "46px 20px 28px 44px" }}>
|
|
||||||
<div style={{ maxWidth: 920, margin: 0, display: "flex", flexDirection: "column", alignItems: "center", gap: 20 }}>
|
|
||||||
{/* Name (left) · avatar (center) · title (right), above the activity card. */}
|
|
||||||
<div style={{ width: "100%", display: "flex", flexDirection: "column", gap: 18 }}>
|
|
||||||
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12, width: "100%", padding: "0 2px" }}>
|
|
||||||
<div style={{ borderRadius: 13, background: "#0f0f13", border: "1px solid rgba(255,255,255,.1)", padding: "11px 22px", boxShadow: "0 8px 22px rgba(0,0,0,.4)" }}>
|
|
||||||
<div style={{ fontSize: 24, fontWeight: 800, color: "#f3f3f5", letterSpacing: "-.02em" }}>{agent.name}</div>
|
|
||||||
</div>
|
|
||||||
{/* Avatar — centered between the name and title. Click to edit. */}
|
|
||||||
<div style={{ position: "relative", width: 132, height: 132, flex: "none" }}>
|
|
||||||
<div style={{ position: "absolute", inset: -9, borderRadius: "50%", border: "1.5px solid rgba(255,111,97,.35)" }} />
|
|
||||||
<div style={{ position: "absolute", inset: 0, borderRadius: "50%", background: "rgba(255,111,97,.3)", animation: "cm-halo 2s ease-out infinite" }} />
|
|
||||||
<button type="button" onClick={() => setAvatarOpen(true)} title="Click to edit image" style={{ position: "relative", width: 132, height: 132, borderRadius: "50%", border: 0, padding: 0, cursor: "pointer", background: shownAvatar ? `center/cover no-repeat url(${shownAvatar})` : agent.grad, display: "flex", alignItems: "center", justifyContent: "center", fontSize: 48, fontWeight: 700, color: agent.ink, boxShadow: "0 0 56px rgba(255,111,97,.5)" }}>{shownAvatar ? null : agent.initial}</button>
|
|
||||||
<span aria-hidden style={{ position: "absolute", right: 2, bottom: 2, width: 32, height: 32, borderRadius: "50%", background: "#15151a", border: "1.5px solid #08080a", display: "flex", alignItems: "center", justifyContent: "center", color: "#cfcfd5", pointerEvents: "none" }}><Camera size={16} /></span>
|
|
||||||
</div>
|
|
||||||
<div style={{ borderRadius: 999, background: "rgba(255,111,97,.08)", border: "1px solid rgba(255,111,97,.3)", padding: "7px 18px" }}>
|
|
||||||
<span style={{ fontFamily: mono, fontSize: 12, letterSpacing: ".08em", color: "#ff8a7a" }}>{agent.role}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<VitalsCard seed={agent.id} />
|
|
||||||
</div>
|
|
||||||
<div style={{ width: "100%" }}><SystemPromptCard prompt={agent.systemPrompt} /></div>
|
|
||||||
{/* Card data mappings (intended real sources for live wiring):
|
|
||||||
NOW RUNNING = every active task this agent is currently working on.
|
|
||||||
SKILLS = the skills assigned to this agent (installed_skills).
|
|
||||||
PERSONALITY = the agent's persona.md / soul.md from the backend.
|
|
||||||
TOOLS = all tools granted to this agent; the "+ Add tool"
|
|
||||||
button installs more from the catalog (/api/skills). */}
|
|
||||||
{/* 3×3 grid: row1 Personality/Skills/Tools, row2 Memory/Capabilities/
|
|
||||||
Safety, row3 the full-width NOW RUNNING live log. */}
|
|
||||||
{/* PERSONALITY as its own full-width card under the system prompt. */}
|
|
||||||
<div style={{ width: "100%" }}>
|
|
||||||
<AnatomyCard tint="#c98af0" label="PERSONALITY" icon={<Drama size={15} />} collapsible><PersonalityBody raw={brain?.personality} fallback={c.personality} /></AnatomyCard>
|
|
||||||
</div>
|
|
||||||
{/* Skills · Tools · Memory across; Capabilities · Safety side by side;
|
|
||||||
then the full-width live log. */}
|
|
||||||
<div style={{ width: "100%", display: "flex", flexDirection: "column", gap: 14 }}>
|
|
||||||
<div style={{ display: "grid", gridTemplateColumns: "repeat(3, minmax(0, 1fr))", gap: 14, alignItems: "stretch" }}>
|
|
||||||
<AnatomyCard tint="#ff6f61" label="SKILLS" count={String(c.skills.length)} icon={<Zap size={15} />}><div style={{ display: "flex", flexWrap: "wrap", gap: 5 }}>{skillsShown.map((s) => (<span key={s} style={tag("#ff6f61")}>{s}</span>))}{skillsRest > 0 ? <span style={tag("#9a9aa2", true)}>+{skillsRest}</span> : null}</div></AnatomyCard>
|
|
||||||
<AnatomyCard tint="#5ec8d8" label="TOOLS · DOORS" icon={<Wrench size={15} />}>
|
|
||||||
<div style={{ display: "flex", flexDirection: "column", height: "100%" }}>
|
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: 6, fontFamily: mono, fontSize: 12.5 }}>{c.tools.map((t) => row(t.name, t.state === "gated" ? "gated ✓" : "blocked ⨯", t.state === "gated" ? "#5fd08a" : "#e8b465"))}</div>
|
|
||||||
<button type="button" onClick={() => setAddToolOpen(true)} style={{ marginTop: "auto", width: "100%", padding: "8px 0", borderRadius: 7, border: "1px dashed rgba(94,200,216,.4)", background: "transparent", color: "#5ec8d8", fontSize: 12.5, fontFamily: mono, cursor: "pointer" }}>+ Add tool</button>
|
|
||||||
</div>
|
|
||||||
</AnatomyCard>
|
|
||||||
<AnatomyCard tint="#5fd08a" label="MEMORY" count={brain ? String(brain.stats.memories) : undefined} icon={<Database size={15} />}>
|
|
||||||
{brain && brain.memory.length ? (
|
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: 5, fontFamily: mono, fontSize: 12.5, color: "#9a9aa2", maxHeight: 150, overflowY: "auto" }}>
|
|
||||||
{brain.memory.slice(0, 6).map((m, i) => (<div key={i} title={m} style={{ whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>{m}</div>))}
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: 6, fontFamily: mono, fontSize: 12.5, color: "#9a9aa2" }}>{brain ? <span style={{ color: "#6a6a72" }}>no memories yet — chat with this agent and they appear here.</span> : (<>{row("Long-term", c.memory.long)}{row("Recent ctx", c.memory.recent)}</>)}</div>
|
|
||||||
)}
|
|
||||||
</AnatomyCard>
|
|
||||||
</div>
|
|
||||||
<div style={{ display: "grid", gridTemplateColumns: "repeat(2, minmax(0, 1fr))", gap: 14, alignItems: "stretch" }}>
|
|
||||||
<AnatomyCard tint="#e8b465" label="CAPABILITIES" icon={<Cpu size={15} />}><div style={{ display: "flex", flexWrap: "wrap", gap: 5 }}>{c.capabilities.map((p) => (<span key={p} style={tag("#e8b465")}>{p}</span>))}</div></AnatomyCard>
|
|
||||||
<AnatomyCard tint="#6fd0c0" label="SAFETY · §15" icon={<ShieldCheck size={15} />}><div style={{ display: "flex", flexDirection: "column", gap: 6, fontFamily: mono, fontSize: 12.5, color: "#9a9aa2" }}>{row("Sandbox", c.safety.sandbox, "#6fd0c0")}{row("Network", c.safety.network, "#6fd0c0")}</div></AnatomyCard>
|
|
||||||
</div>
|
|
||||||
{/* Full-width live work log. */}
|
|
||||||
<div>
|
|
||||||
<AnatomyCard tint="#5ec8d8" label="NOW RUNNING · LIVE" count={agent.nowRunning.length ? `${agent.nowRunning.length} active` : "idle"} icon={<Activity size={15} />}>
|
|
||||||
<div style={{ minHeight: 200, maxHeight: 340, overflowY: "auto" }}>
|
|
||||||
{agent.nowRunning.length === 0 ? (
|
|
||||||
<span style={{ fontFamily: mono, fontSize: 12.5, color: "#6a6a72" }}>idle — no active tasks. The agent's live work log will stream here as it runs.</span>
|
|
||||||
) : (
|
|
||||||
<div style={{ display: "flex", flexDirection: "column", gap: 9 }}>
|
|
||||||
{agent.nowRunning.map((t, i) => (
|
|
||||||
<div key={i}>
|
|
||||||
<div style={{ display: "flex", alignItems: "center", gap: 6, marginBottom: t.progress != null ? 5 : 0 }}>
|
|
||||||
<span className={t.kind === "loop" ? "cm-blink" : ""} style={{ width: 6, height: 6, borderRadius: "50%", background: t.kind === "loop" ? "#5ec8d8" : "#e8b465" }} />
|
|
||||||
<span style={{ fontSize: 13.5, fontWeight: 600, color: "#e6e6ea" }}>{t.name}</span>
|
|
||||||
<span style={{ flex: 1 }} />
|
|
||||||
<span style={{ fontFamily: mono, fontSize: 11, color: t.kind === "loop" ? "#5ec8d8" : "#e8b465" }}>{t.detail}</span>
|
|
||||||
</div>
|
|
||||||
{t.progress != null ? (
|
|
||||||
<div style={{ height: 4, borderRadius: 2, background: "rgba(255,255,255,.08)", overflow: "hidden" }}><div style={{ width: `${t.progress}%`, height: "100%", background: "linear-gradient(90deg,#5ec8d8,#4aa3b8)" }} /></div>
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</AnatomyCard>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<span style={{ textAlign: "center", fontFamily: mono, fontSize: 10, color: "#3a3a40" }}>part of {teamName} · the computer panel runs this agent's apps & routines</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{avatarOpen ? (
|
|
||||||
<AvatarModal clawId={agent.id} clawName={agent.name} current={shownAvatar} onClose={() => setAvatarOpen(false)} onSaved={(url) => { setImgUrl(url); router.refresh(); }} />
|
|
||||||
) : null}
|
|
||||||
{addToolOpen ? (
|
|
||||||
<AddToolModal clawId={agent.id} clawName={agent.name} onClose={() => setAddToolOpen(false)} onAdded={() => onToolsChanged?.()} />
|
|
||||||
) : null}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,134 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
// Shared anatomy-card primitives — extracted so the agent command center
|
||||||
|
// (ClawCommandCenter) reuses the exact cards/styling. Behavior copied verbatim
|
||||||
|
// from Dashboard.tsx's in-file helpers.
|
||||||
|
|
||||||
|
import { useState, type CSSProperties } from "react";
|
||||||
|
import { ScrollText } from "lucide-react";
|
||||||
|
|
||||||
|
export const mono = "'JetBrains Mono', ui-monospace, monospace";
|
||||||
|
|
||||||
|
// `GET /api/claws/{id}/brain` — the claw's .brain (cm-brain / ClawhDF5).
|
||||||
|
export type RawBrain = {
|
||||||
|
exists: boolean;
|
||||||
|
system_prompt: string | null;
|
||||||
|
personality: string | null;
|
||||||
|
skills: { name: string; body: string }[];
|
||||||
|
tools: { name: string; state: string }[];
|
||||||
|
memory: string[];
|
||||||
|
stats: { skills: number; tools: number; memories: number };
|
||||||
|
} | null;
|
||||||
|
|
||||||
|
// Inline markdown: **bold** and `code` → styled spans (everything else verbatim).
|
||||||
|
function renderInline(text: string, kp: string): React.ReactNode[] {
|
||||||
|
return text.split(/(\*\*[^*]+\*\*|`[^`]+`)/g).filter(Boolean).map((p, i) => {
|
||||||
|
if (p.startsWith("**") && p.endsWith("**")) return <strong key={`${kp}-${i}`} style={{ color: "#fff", fontWeight: 700 }}>{p.slice(2, -2)}</strong>;
|
||||||
|
if (p.startsWith("`") && p.endsWith("`")) return <code key={`${kp}-${i}`} style={{ fontFamily: mono, fontSize: "0.9em", background: "rgba(255,255,255,.06)", padding: "1px 5px", borderRadius: 4, color: "#e6e6ea" }}>{p.slice(1, -1)}</code>;
|
||||||
|
return <span key={`${kp}-${i}`}>{p}</span>;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// A tiny, dependency-free Markdown renderer: headings, bullet lists, bold/code,
|
||||||
|
// paragraphs. Turns the raw prompt into readable prose instead of literal `##`.
|
||||||
|
export function MarkdownText({ text, baseSize = 14 }: { text: string; baseSize?: number }) {
|
||||||
|
const lines = text.replace(/\r\n/g, "\n").split("\n");
|
||||||
|
const blocks: React.ReactNode[] = [];
|
||||||
|
let para: string[] = [];
|
||||||
|
let list: string[] = [];
|
||||||
|
const flushPara = () => {
|
||||||
|
if (para.length) { const k = `p${blocks.length}`; blocks.push(<p key={k} style={{ margin: "0 0 10px", fontSize: baseSize, lineHeight: 1.65, color: "#d6d6dc" }}>{renderInline(para.join(" "), k)}</p>); para = []; }
|
||||||
|
};
|
||||||
|
const flushList = () => {
|
||||||
|
if (list.length) { const k = `u${blocks.length}`; blocks.push(<ul key={k} style={{ margin: "0 0 10px", paddingLeft: 18, display: "flex", flexDirection: "column", gap: 5 }}>{list.map((li, i) => <li key={i} style={{ fontSize: baseSize, lineHeight: 1.55, color: "#d6d6dc" }}>{renderInline(li, `${k}-${i}`)}</li>)}</ul>); list = []; }
|
||||||
|
};
|
||||||
|
for (const raw of lines) {
|
||||||
|
const line = raw.replace(/\s+$/, "");
|
||||||
|
const h = line.match(/^(#{1,6})\s+(.*)$/);
|
||||||
|
const li = line.match(/^\s*[-*]\s+(.*)$/);
|
||||||
|
if (h) { flushPara(); flushList(); const lvl = h[1].length; const size = lvl <= 1 ? baseSize + 5 : lvl === 2 ? baseSize + 2 : baseSize + 1; const k = `h${blocks.length}`; blocks.push(<div key={k} style={{ fontSize: size, fontWeight: 700, color: "#fff", letterSpacing: "-.01em", margin: blocks.length ? "15px 0 7px" : "0 0 7px" }}>{renderInline(h[2], k)}</div>); }
|
||||||
|
else if (li) { flushPara(); list.push(li[1]); }
|
||||||
|
else if (line.trim() === "") { flushPara(); flushList(); }
|
||||||
|
else { flushList(); para.push(line.replace(/^\s*>\s?/, "")); }
|
||||||
|
}
|
||||||
|
flushPara(); flushList();
|
||||||
|
return <div>{blocks}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A same-colored triangle toggle: points down when open, left when closed.
|
||||||
|
export function CollapseTick({ open, color, onClick }: { open: boolean; color: string; onClick: () => void }) {
|
||||||
|
return (
|
||||||
|
<button type="button" onClick={onClick} aria-label={open ? "Collapse" : "Expand"} aria-expanded={open}
|
||||||
|
style={{ flex: "none", border: 0, background: "transparent", cursor: "pointer", color, padding: 4, display: "inline-flex", alignItems: "center", justifyContent: "center" }}>
|
||||||
|
<span style={{ display: "inline-block", fontSize: 11, lineHeight: 1, transform: open ? "rotate(0deg)" : "rotate(90deg)", transition: "transform .15s ease" }}>▼</span>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function SystemPromptCard({ prompt }: { prompt: string }) {
|
||||||
|
const [open, setOpen] = useState(true);
|
||||||
|
return (
|
||||||
|
<div style={{ borderRadius: 12, background: "#0f0f13", border: "1px solid rgba(255,111,97,.3)", padding: "16px 18px", boxShadow: "0 8px 22px rgba(0,0,0,.4)" }}>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: open ? 13 : 0 }}>
|
||||||
|
<span style={{ width: 26, height: 26, flex: "none", borderRadius: 7, background: "rgba(255,111,97,.18)", border: "1px solid rgba(255,111,97,.35)", display: "inline-flex", alignItems: "center", justifyContent: "center", color: "#ff8a7a" }}><ScrollText size={15} /></span>
|
||||||
|
<span style={{ fontFamily: mono, fontSize: 12.5, fontWeight: 600, letterSpacing: ".05em", color: "#ff8a7a" }}>SYSTEM PROMPT</span>
|
||||||
|
<span style={{ flex: 1 }} />
|
||||||
|
<CollapseTick open={open} color="#ff8a7a" onClick={() => setOpen((o) => !o)} />
|
||||||
|
</div>
|
||||||
|
{open ? (prompt?.trim() ? <MarkdownText text={prompt} baseSize={14} /> : <p style={{ fontSize: 13, color: "#6a6a72", margin: 0 }}>No system prompt set.</p>) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Personality is often stored as JSON (traits/tone/…). Render it as labeled
|
||||||
|
// sections instead of dumping raw JSON; fall back to tags / text otherwise.
|
||||||
|
export function PersonalityBody({ raw, fallback }: { raw: string | null | undefined; fallback: string[] }) {
|
||||||
|
const tags = (arr: string[]) => <div style={{ display: "flex", flexWrap: "wrap", gap: 5 }}>{arr.map((p, i) => <span key={i} style={tag("#c98af0")}>{p}</span>)}</div>;
|
||||||
|
const trimmed = (raw ?? "").trim();
|
||||||
|
let parsed: unknown;
|
||||||
|
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
||||||
|
try { parsed = JSON.parse(trimmed); } catch { parsed = undefined; }
|
||||||
|
}
|
||||||
|
if (Array.isArray(parsed)) return tags(parsed.map((x) => String(x)));
|
||||||
|
if (parsed && typeof parsed === "object") {
|
||||||
|
return (
|
||||||
|
<div style={{ display: "flex", flexDirection: "column", gap: 9 }}>
|
||||||
|
{Object.entries(parsed as Record<string, unknown>).map(([k, v]) => (
|
||||||
|
<div key={k}>
|
||||||
|
<div style={{ fontFamily: mono, fontSize: 10.5, letterSpacing: ".08em", color: "#b9a6e0", marginBottom: 5, textTransform: "uppercase" }}>{k.replace(/_/g, " ")}</div>
|
||||||
|
{Array.isArray(v)
|
||||||
|
? tags(v.map((x) => String(x)))
|
||||||
|
: v && typeof v === "object"
|
||||||
|
? <div style={{ display: "flex", flexDirection: "column", gap: 3, fontSize: 13, color: "#cfcfd5", lineHeight: 1.55 }}>{Object.entries(v as Record<string, unknown>).map(([kk, vv]) => <div key={kk}><span style={{ color: "#8a8a92" }}>{kk}:</span> {String(vv)}</div>)}</div>
|
||||||
|
: <div style={{ fontSize: 13.5, color: "#d6d6dc", lineHeight: 1.55 }}>{String(v)}</div>}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (fallback.length) return tags(fallback);
|
||||||
|
if (trimmed) return <div style={{ fontSize: 13.5, color: "#d6d6dc", lineHeight: 1.6 }}>{trimmed}</div>;
|
||||||
|
return <span style={{ fontSize: 12, color: "#6a6a72" }}>—</span>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AnatomyCard({ tint, label, count, icon, collapsible, children }: { tint: string; label: string; count?: string; icon?: React.ReactNode; collapsible?: boolean; children: React.ReactNode }) {
|
||||||
|
const [open, setOpen] = useState(true);
|
||||||
|
const collapsed = !!collapsible && !open;
|
||||||
|
return (
|
||||||
|
<div style={{ width: "100%" }}>
|
||||||
|
<div style={{ boxSizing: "border-box", display: "flex", flexDirection: "column", borderRadius: 12, background: "#0f0f13", border: `1px solid ${tint}38`, padding: 12, boxShadow: "0 8px 22px rgba(0,0,0,.4)" }}>
|
||||||
|
<div style={{ display: "flex", alignItems: "center", gap: 8, marginBottom: collapsed ? 0 : 11 }}>
|
||||||
|
<span style={{ width: 26, height: 26, flex: "none", borderRadius: 7, background: `${tint}26`, border: `1px solid ${tint}40`, display: "inline-flex", alignItems: "center", justifyContent: "center", color: tint }}>{icon}</span>
|
||||||
|
<span style={{ fontFamily: mono, fontSize: 12.5, fontWeight: 600, letterSpacing: ".05em", color: tint }}>{label}</span>
|
||||||
|
{count || collapsible ? <span style={{ flex: 1 }} /> : null}
|
||||||
|
{count ? <span style={{ fontFamily: mono, fontSize: 10, color: "#5a5a62" }}>{count}</span> : null}
|
||||||
|
{collapsible ? <CollapseTick open={open} color={tint} onClick={() => setOpen((o) => !o)} /> : null}
|
||||||
|
</div>
|
||||||
|
{!collapsed ? <div>{children}</div> : null}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// A pill-shaped tag, tinted to its section's accent. `dim` is the muted "+N" variant.
|
||||||
|
export const tag = (color = "#9a9aa2", dim = false): CSSProperties => ({ fontFamily: mono, fontSize: 12, color: dim ? "#8a8a92" : color, padding: "4px 10px", borderRadius: 7, background: dim ? "rgba(255,255,255,.04)" : `${color}1f`, border: `1px solid ${dim ? "rgba(255,255,255,.08)" : `${color}45`}` });
|
||||||
@@ -43,8 +43,9 @@ export interface TaxonomyEvents {
|
|||||||
nodes?: { id: string; tier: NodeTier; label?: string; parentId?: string }[];
|
nodes?: { id: string; tier: NodeTier; label?: string; parentId?: string }[];
|
||||||
edges?: { from: string; to: string; kind?: "parent" | "peer" | "flow" }[];
|
edges?: { from: string; to: string; kind?: "parent" | "peer" | "flow" }[];
|
||||||
};
|
};
|
||||||
/** Top-bar pills + Observe System telemetry strip. */
|
/** Top-bar pills + Observe System telemetry strip. With `agentId` it's the
|
||||||
telemetry: { tokensPerMin?: number; costPerHr?: number; loops?: number; doorsPending?: number };
|
* per-agent slice for the command-center metric band; without, workspace-wide. */
|
||||||
|
telemetry: { agentId?: string; tokensPerMin?: number; costPerHr?: number; loops?: number; doorsPending?: number };
|
||||||
/** Observe System → ROUTINES & LOOPS; the scheduler view. */
|
/** Observe System → ROUTINES & LOOPS; the scheduler view. */
|
||||||
"routine.update": {
|
"routine.update": {
|
||||||
routineId: string;
|
routineId: string;
|
||||||
@@ -97,5 +98,9 @@ export function stateKey<T extends TaxonomyType>(type: T, d: TaxonomyPayload<T>)
|
|||||||
return `${type}:${(d as TaxonomyEvents["agent.status"]).agentId}`;
|
return `${type}:${(d as TaxonomyEvents["agent.status"]).agentId}`;
|
||||||
if (type === "node.activity") return `${type}:${(d as TaxonomyEvents["node.activity"]).nodeId}`;
|
if (type === "node.activity") return `${type}:${(d as TaxonomyEvents["node.activity"]).nodeId}`;
|
||||||
if (type === "routine.update") return `${type}:${(d as TaxonomyEvents["routine.update"]).routineId}`;
|
if (type === "routine.update") return `${type}:${(d as TaxonomyEvents["routine.update"]).routineId}`;
|
||||||
return type; // telemetry, topology.update
|
if (type === "telemetry") {
|
||||||
|
const a = (d as TaxonomyEvents["telemetry"]).agentId;
|
||||||
|
return a ? `telemetry:${a}` : "telemetry"; // per-agent slice vs workspace-wide
|
||||||
|
}
|
||||||
|
return type; // topology.update
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -186,6 +186,21 @@ export function useLiveState<T extends TaxonomyType, S>(
|
|||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** The per-agent telemetry slice for the command-center metric band. Re-subscribes
|
||||||
|
* when the agent changes so the replayed last-known value paints immediately. */
|
||||||
|
export function useAgentTelemetry(agentId: string | null): TaxonomyPayload<"telemetry"> | null {
|
||||||
|
const client = useClawmatesLive();
|
||||||
|
const [t, setT] = useState<TaxonomyPayload<"telemetry"> | null>(null);
|
||||||
|
useEffect(() => {
|
||||||
|
setT(null);
|
||||||
|
if (!agentId) return;
|
||||||
|
return client.on("telemetry", (d) => {
|
||||||
|
if (d.agentId === agentId) setT(d);
|
||||||
|
});
|
||||||
|
}, [client, agentId]);
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
// --- synthetic generator (no backend) — ports the demo loop so the World/Observe
|
// --- synthetic generator (no backend) — ports the demo loop so the World/Observe
|
||||||
// views breathe even with nothing wired. Stops the moment a real feed connects. --
|
// views breathe even with nothing wired. Stops the moment a real feed connects. --
|
||||||
type Emit = <T extends TaxonomyType>(type: T, data: TaxonomyPayload<T>) => void;
|
type Emit = <T extends TaxonomyType>(type: T, data: TaxonomyPayload<T>) => void;
|
||||||
|
|||||||
Reference in New Issue
Block a user