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:
co-authored by
Claude Opus 4.8
parent
5975287d56
commit
a9a5176f7c
@@ -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' : '—'}
|
||||
|
||||
Reference in New Issue
Block a user