feat(cockpit): Clawd mascot + live agent chat on Meet your agent

Rework the Meet-your-agent rail into a self-contained agent surface:

- Clawd, the Claude Code pixel crab, animates the LED matrix on a 26x16
  grid, coupled to the chat lifecycle (idle stare / claws pump while
  working / green flash on reply). Static antennas, permanent black eyes.
- A full free-form chat to the default agent (useAgentChat): free text
  goes to the blocking /webhook path so conversational replies actually
  render; the 3 canned prompts stay fire-and-forget + SSE (tool turns).
- Chat and the working log are separate panes; the 3 prompts became
  starter buttons; the ZeroClaw runtime + channels moved into an
  Advanced drawer. The whole agent rail is now theme-aware (light/dark).
- Strip the phase eyebrows from the sidebar + page headers; reduce the
  EnvSetup left column to editorial copy.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
Omar Sobh
2026-07-23 16:35:17 -07:00
co-authored by Claude Opus 4.8
parent 5975287d56
commit a9a5176f7c
13 changed files with 685 additions and 148 deletions
+144 -39
View File
@@ -1,9 +1,13 @@
import { useEffect } from 'react'
import { useLocation } from 'react-router-dom'
import { useSession } from '@/store/session'
import { useNodeFeed } from '@/lib/useNodeFeed'
import { useTelemetry } from '@/lib/useTelemetry'
import { useMatrixMirror } from '@/lib/useMatrixMirror'
import { useClawd, GRID_W } from '@/lib/clawSprite'
import { useAgentChat } from '@/lib/useAgentChat'
import { WaveformCanvas } from './WaveformCanvas'
import { RailAgent } from './RailAgent'
import type { NodeActivityKind } from '@/types'
import { cn } from '@/lib/utils'
import { ADD_LAYERS } from '@/lib/addLayers'
@@ -33,31 +37,46 @@ const NEXT_BY_PATH: Record<string, string[]> = {
'/workshop/add': ['L4', 'L5'],
}
function RailSection({ label, children }: { label: string; children: React.ReactNode }) {
function RailSection({ label, light, children }: { label: string; light?: boolean; children: React.ReactNode }) {
return (
<div className="mt-5">
<div className="font-mono text-[9.5px] tracking-[0.18em] text-rail-dim">{label}</div>
<div className={cn('font-mono text-[9.5px] tracking-[0.18em]', light ? 'text-ink-3' : 'text-rail-dim')}>{label}</div>
<div className="mt-2">{children}</div>
</div>
)
}
export function CockpitRail() {
export function CockpitRail({ variant = 'full' }: { variant?: 'full' | 'agent' } = {}) {
const teamId = useSession((s) => s.teamId)
const team = useSession((s) => s.team)
const connected = useSession((s) => s.device.connected)
const add = useSession((s) => s.add)
const submitted = useSession((s) => s.submission.code != null)
const setTried = useSession((s) => s.setTried)
const { pathname } = useLocation()
const feed = useNodeFeed(teamId, connected)
const tel = useTelemetry(connected)
const online = connected && feed.online
// On the "agent" rail (Meet your agent) the matrix shows Clawd, the crab —
// not a board mirror — driven by the live chat status, so we skip the mirror
// poll and animate the sprite instead.
const agentView = variant === 'agent'
const chat = useAgentChat(teamId, agentView && online)
const claw = useClawd(chat.status)
// Folding the canned starters into the rail means their completions now drive
// the Module 2 prefill (`tried`); bump it as starters finish (never lower it).
useEffect(() => {
if (agentView && chat.doneCount > 0) setTried(chat.doneCount)
}, [agentView, chat.doneCount, setTried])
// Pixel-perfect mirror of the physical matrix (real board frame). Falls back
// to the sim frame until the first real frame arrives.
const mirror = useMatrixMirror(teamId, online)
const matrixDots = mirror ?? tel.matrix
const mirror = useMatrixMirror(teamId, online && !agentView)
const matrixDots = agentView ? claw.dots : mirror ?? tel.matrix
const matrixLive = mirror != null
const matrixCols = agentView ? GRID_W : 13
// Clawd flashes green on a reply; the board mirror stays ASCII-orange.
const litColor = agentView && claw.color === 'green' ? 'oklch(0.82 0.17 152)' : 'oklch(0.7 0.2 34)'
const nodeName = team.name ? team.name.toLowerCase().replace(/\s+/g, '-') : 'crimson-node'
const nextSet = new Set(NEXT_BY_PATH[pathname] ?? [])
@@ -68,47 +87,95 @@ export function CockpitRail() {
const log = feed.activity.slice(0, 6)
// The agent rail is theme-aware (light in light mode); the instrument rail
// stays a fixed-dark device screen. The LED matrix itself is a screen either way.
const label = agentView ? 'text-ink-3' : 'text-rail-dim'
return (
<aside className="sticky top-6 rounded-[18px] bg-rail-bg p-[22px] text-rail-text shadow-[0_20px_50px_-24px_rgba(0,0,0,0.5)]">
<aside
className={cn(
'sticky top-6 rounded-[18px] p-[22px] shadow-[0_20px_50px_-24px_rgba(0,0,0,0.5)]',
agentView ? 'border border-line bg-surface-soft text-ink' : 'bg-rail-bg text-rail-text',
)}
>
{/* 1 · node header / heartbeat */}
<div className="flex items-center gap-[11px]">
<span
className={cn(
'h-2.5 w-2.5 rounded-full',
online ? 'bg-rail-green shadow-[0_0_10px_#3fd28a] animate-pulse' : 'bg-rail-dim2',
online
? agentView
? 'bg-green shadow-[0_0_10px_#3fd28a] animate-pulse'
: 'bg-rail-green shadow-[0_0_10px_#3fd28a] animate-pulse'
: agentView
? 'bg-ink-3'
: 'bg-rail-dim2',
)}
/>
<span className="font-mono text-sm font-semibold tracking-[0.02em] text-rail-text3">{nodeName}</span>
<span
className={cn(
'font-mono text-sm font-semibold tracking-[0.02em]',
agentView ? 'text-ink' : 'text-rail-text3',
)}
>
{nodeName}
</span>
<span
className={cn(
'font-mono text-[9px] tracking-[0.14em] rounded border px-[7px] py-0.5',
online ? 'text-rail-green border-[#2c6b4f]' : 'text-rail-dim2 border-rail-line',
online
? agentView
? 'text-green border-green/50'
: 'text-rail-green border-[#2c6b4f]'
: agentView
? 'text-ink-3 border-line'
: 'text-rail-dim2 border-rail-line',
)}
>
{online ? 'LIVE' : 'OFFLINE'}
</span>
<span className="ml-auto font-mono text-[10px] text-rail-dim">arduino uno q</span>
<span className={cn('ml-auto font-mono text-[10px]', label)}>arduino uno q</span>
</div>
{/* 2 · LED matrix 13×8 — real pixel mirror of the physical matrix */}
{/* 2 · LED matrix — board mirror (full rail) or Clawd the crab (agent) */}
<div className="mt-5">
<div className="flex items-center justify-between">
<div className="font-mono text-[9.5px] tracking-[0.18em] text-rail-dim">LED MATRIX · 13×8</div>
{matrixLive && (
<span className="font-mono text-[8.5px] tracking-[0.12em] text-rail-green">● MIRROR</span>
<div className={cn('font-mono text-[9.5px] tracking-[0.18em]', label)}>
{agentView ? 'LED MATRIX · 26×16' : 'LED MATRIX · 13×8'}
</div>
{agentView ? (
<span
className="font-mono text-[8.5px] tracking-[0.12em]"
style={{ color: claw.color === 'green' ? '#3fd28a' : '#ff7a3c' }}
>
{claw.color === 'green' ? '● REPLIED' : chat.status === 'working' ? '● WORKING' : '● CLAWD'}
</span>
) : (
matrixLive && <span className="font-mono text-[8.5px] tracking-[0.12em] text-rail-green">● MIRROR</span>
)}
</div>
<div className="mt-2 flex justify-center rounded-[10px] border border-rail-line bg-rail-inset px-[13px] py-3">
<div className="grid gap-1" style={{ gridTemplateColumns: 'repeat(13, 1fr)' }}>
{Array.from({ length: 104 }).map((_, i) => {
const lit = tel.live && matrixDots[i]
<div
className={cn(
'mt-2 rounded-[10px] border border-rail-line bg-rail-inset',
agentView ? 'p-3' : 'flex justify-center px-[13px] py-3',
)}
>
<div
className={cn('grid', agentView ? 'w-full gap-[2px]' : 'gap-1')}
style={{ gridTemplateColumns: `repeat(${matrixCols}, 1fr)` }}
>
{matrixDots.map((on, i) => {
const lit = agentView ? on : tel.live && on
return (
<span
key={i}
className="h-[9px] w-[9px] rounded-[2px] transition-[background] duration-75"
className={cn(
'rounded-[2px] transition-[background] duration-75',
agentView ? 'aspect-square w-full' : 'h-[9px] w-[9px]',
)}
style={{
background: lit ? 'oklch(0.7 0.2 34)' : 'oklch(0.28 0.01 260)',
boxShadow: lit ? '0 0 5px oklch(0.7 0.2 34)' : 'none',
background: lit ? litColor : 'oklch(0.28 0.01 260)',
boxShadow: lit ? `0 0 5px ${litColor}` : 'none',
}}
/>
)
@@ -117,6 +184,9 @@ export function CockpitRail() {
</div>
</div>
{/* 3 & 4 · sensor telemetry — omitted on the "agent" variant (Meet your agent) */}
{variant !== 'agent' && (
<>
{/* 3 · I2C bus (telemetry) */}
<RailSection label="I2C BUS · 100 kHz">
<div className="rounded-[10px] border border-rail-line2 bg-rail-panel px-1 py-1.5 font-mono text-xs">
@@ -178,35 +248,70 @@ export function CockpitRail() {
))}
</div>
</RailSection>
</>
)}
{/* 5 · agent activity log (real) */}
<RailSection label="AGENT ACTIVITY">
<div className="h-[132px] overflow-hidden rounded-[10px] border border-rail-line bg-rail-inset px-[13px] py-[11px] font-mono text-[11px] leading-[1.75]">
{log.length === 0 ? (
<div className="text-rail-dim2">idle — prompt your agent to see it work</div>
) : (
log.map((e, i) => (
<div key={i} className={cn('truncate', LOG_COLOR[e.kind])}>
{e.label}
</div>
))
)}
</div>
</RailSection>
{/* 5 · agent — chat + logs as separate panes (agent view) or the read-only log */}
{agentView ? (
<RailAgent
messages={chat.messages}
logs={chat.logs}
sending={chat.sending}
online={online}
starters={chat.starters}
onSend={(t, id) => void chat.send(t, id)}
/>
) : (
<RailSection label="AGENT ACTIVITY">
<div className="h-[132px] overflow-hidden rounded-[10px] border border-rail-line bg-rail-inset px-[13px] py-[11px] font-mono text-[11px] leading-[1.75]">
{log.length === 0 ? (
<div className="text-rail-dim2">idle — prompt your agent to see it work</div>
) : (
log.map((e, i) => (
<div key={i} className={cn('truncate', LOG_COLOR[e.kind])}>
{e.label}
</div>
))
)}
</div>
</RailSection>
)}
{/* 6 · ADD progress (real, local) */}
<RailSection label="AGENT DESIGN DOC">
<RailSection label="AGENT DESIGN DOC" light={agentView}>
<div className="flex flex-col gap-px font-mono text-[11px]">
{ADD_LAYERS.map(({ key, n, title }) => {
const st = layerState(key)
return (
<div key={key} className="flex items-center gap-2.5 px-0.5 py-[7px]">
<span className={cn(st === 'idle' ? 'text-[#4a5060]' : 'text-rail-blue')}>L{n}</span>
<span className={cn(st === 'idle' ? 'text-rail-dim2' : 'text-rail-text2')}>{title}</span>
<span
className={cn(
st === 'idle' ? (agentView ? 'text-ink-3' : 'text-[#4a5060]') : agentView ? 'text-blue' : 'text-rail-blue',
)}
>
L{n}
</span>
<span
className={cn(
st === 'idle' ? (agentView ? 'text-faint' : 'text-rail-dim2') : agentView ? 'text-ink-2' : 'text-rail-text2',
)}
>
{title}
</span>
<span
className={cn(
'ml-auto',
st === 'done' ? 'text-rail-green' : st === 'next' ? 'text-[#d9a441]' : 'text-rail-dim3',
st === 'done'
? agentView
? 'text-green'
: 'text-rail-green'
: st === 'next'
? agentView
? 'text-amber'
: 'text-[#d9a441]'
: agentView
? 'text-ink-3'
: 'text-rail-dim3',
)}
>
{st === 'done' ? 'done' : st === 'next' ? 'next' : '—'}