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
+10 -30
View File
@@ -4,10 +4,11 @@ import { MemoryRouter } from 'react-router-dom'
import { EnvSetup } from './EnvSetup'
import { useSession } from '@/store/session'
// EnvSetup now renders the agent chat + say-hi; stub the streaming/mode calls.
// The chat, starters, runtime and channels all moved to the cockpit rail
// (CockpitRail's "agent" variant) — the page itself is now editorial copy + a
// proceed gate. Stub the mode call the rail would otherwise make elsewhere.
vi.mock('@/lib/api', async (orig) => ({
...(await orig<typeof import('@/lib/api')>()),
openTeamActivity: () => () => {},
getMode: () => Promise.resolve({ localMode: false }),
}))
@@ -29,44 +30,23 @@ describe('EnvSetup — Meet your agent', () => {
sessionStorage.clear()
})
it('renders the heading', () => {
it('renders the heading and the editorial intro', () => {
renderPage()
expect(screen.getByRole('heading', { name: /meet your agent/i })).toBeInTheDocument()
expect(screen.getByTestId('agent-intro')).toBeInTheDocument()
})
it('prompts to claim a board first when not connected, and gates Proceed', () => {
it('guides to connect the board first when not connected, and gates Proceed', () => {
renderPage()
expect(screen.getByText(/claim your board first/i)).toBeInTheDocument()
expect(screen.getByText(/connect your board on the previous step/i)).toBeInTheDocument()
expect(screen.getByRole('button', { name: /proceed/i })).toBeDisabled()
})
it('shows the node as Connected with the board url when claimed', () => {
connect('http://192.168.1.7:8080')
renderPage()
const link = screen.getByRole('link', { name: /open your node/i })
expect(link).toHaveAttribute('href', 'http://192.168.1.7:8080')
expect(link).toHaveAttribute('target', '_blank')
expect(screen.getByText(/^connected$/i)).toBeInTheDocument()
})
it('falls back to the claim prompt when connected but there is no nodeUrl', () => {
connect(null)
renderPage()
expect(screen.queryByRole('link', { name: /open your node/i })).not.toBeInTheDocument()
expect(screen.getByText(/claim your board first/i)).toBeInTheDocument()
})
it('reveals the Your Agent section (say-hi + chat) once connected', () => {
connect()
renderPage()
expect(screen.getByTestId('your-agent')).toBeInTheDocument()
expect(screen.getByRole('button', { name: /say hi to your agent/i })).toBeInTheDocument()
expect(screen.getByTestId('agent-chat')).toBeInTheDocument()
})
it('enables Proceed once the board is connected (domain moved to Module 1)', () => {
it('enables Proceed once the board is connected', () => {
connect()
renderPage()
expect(screen.getByRole('button', { name: /proceed/i })).toBeEnabled()
// the connect nudge disappears once online
expect(screen.queryByText(/connect your board on the previous step/i)).toBeNull()
})
})