Architecture
Nerve is a web interface for OpenClaw — chat, voice input, TTS, and agent monitoring in the browser. It connects to the OpenClaw gateway over WebSocket and provides a rich UI for interacting with AI agents.
System Diagram
┌──────────────────────────────────────────────────────────────────┐
│ Browser (React SPA) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌───────────┐ ┌────────────────┐ │
│ │ ChatPanel│ │ Sessions │ │ Workspace │ │ Command Palette│ │
│ └────┬─────┘ └────┬─────┘ └─────┬─────┘ └────────────────┘ │
│ │ │ │ │
│ ┌────┴──────────────┴──────────────┴────────────────────────┐ │
│ │ React Contexts (Gateway, Session, Chat, Settings)│ │
│ └────────────────────────────┬──────────────────────────────┘ │
│ │ WebSocket (/ws proxy) │
└───────────────────────────────┼──────────────────────────────────┘
│
┌───────────────────────────────┼──────────────────────────────────┐
│ Nerve Server (Hono + Node) │ │
│ │ │
│ ┌────────────────────────────┴─────────────┐ │
│ │ WebSocket Proxy (ws-proxy.ts) │ │
│ │ - Intercepts connect.challenge │ │
│ │ - Injects device identity (Ed25519) │ │
│ └────────────────────────────┬──────────────┘ │
│ │ │
│ ┌───────────────┐ ┌────────┴─────┐ ┌───────────────────────┐ │
│ │ REST API │ │ SSE Stream │ │ Static File Server │ │
│ │ /api/* │ │ /api/events │ │ Vite build → dist/ │ │
│ └───────┬───────┘ └──────────────┘ └───────────────────────┘ │
│ │ │
│ ┌───────┴──────────────────────────────────────────────────┐ │
│ │ Services: TTS (OpenAI, Replicate, Edge), Whisper, │ │
│ │ Claude Usage, TTS Cache, Usage Tracker │ │
│ └──────────────────────────────────────────────────────────┘ │
└──────────────────────────────┬───────────────────────────────────┘
│ HTTP / WS
┌──────────┴──────────┐
│ OpenClaw Gateway │
│ (ws://127.0.0.1: │
│ 18789) │
└─────────────────────┘Frontend Structure
Built with React 19, TypeScript, Vite, and Tailwind CSS v4.
Entry Point
| File | Purpose |
|---|---|
src/main.tsx | Mounts the React tree with the provider hierarchy: ErrorBoundary → StrictMode → GatewayProvider → SettingsProvider → SessionProvider → ChatProvider → App |
src/App.tsx | Root layout — wires contexts to lazy-loaded panels, manages keyboard shortcuts and command palette |
Context Providers (State Management)
All global state flows through four React contexts, nested in dependency order:
| Context | File | Responsibilities |
|---|---|---|
| GatewayContext | src/contexts/GatewayContext.tsx | WebSocket connection lifecycle, RPC method calls, event fan-out via pub/sub pattern, model/thinking status polling, activity sparkline |
| SettingsContext | src/contexts/SettingsContext.tsx | Sound, TTS provider/model, wake word, panel ratio, theme, font, telemetry/events visibility. Persists to localStorage |
| SessionContext | src/contexts/SessionContext.tsx | Session list (via gateway RPC), granular agent status tracking (IDLE/THINKING/STREAMING/DONE/ERROR), busy state derivation, unread session tracking, agent log, event log, session CRUD (delete, spawn, rename, abort) |
| ChatContext | src/contexts/ChatContext.tsx | Chat messages, streaming state, processing stage indicator, activity log (tool calls), send/abort/reset, infinite scroll history, TTS voice fallback |
Data flow pattern: Contexts subscribe to gateway events via GatewayContext.subscribe(). The SessionContext listens for agent and chat events to update granular status. The ChatContext listens for streaming deltas and lifecycle events to render real-time responses.
Feature Modules
Each feature lives in src/features/<name>/ with its own components, hooks, types, and operations.
features/chat/
The main chat interface.
| File | Purpose |
|---|---|
ChatPanel.tsx | Full chat view — message list with infinite scroll, input bar, streaming indicator, search |
InputBar.tsx | Text input with voice recording, image attachment, tab completion, input history |
MessageBubble.tsx | Renders individual messages (user, assistant, tool, system) with markdown |
ToolCallBlock.tsx | Renders tool call blocks with name, arguments, and results |
DiffView.tsx | Side-by-side diff rendering for file edits |
FileContentView.tsx | Syntax-highlighted file content display |
ImageLightbox.tsx | Full-screen image viewer |
SearchBar.tsx | In-chat message search (Cmd+F) |
MemoriesSection.tsx | Inline memory display within chat |
edit-blocks.ts | Parses edit/diff blocks from tool output |
extractImages.ts | Extracts image content blocks from messages |
image-compress.ts | Client-side image compression before upload |
types.ts | Chat-specific types (ChatMsg, ImageAttachment) |
utils.ts | Chat utility functions |
useMessageSearch.ts | Hook for message search filtering |
operations/ | Pure business logic (no React): loadHistory.ts, sendMessage.ts, streamEventHandler.ts |
components/ | Sub-components: ActivityLog, ChatHeader, HeartbeatPulse, ProcessingIndicator, ScrollToBottomButton, StreamingMessage, ThinkingDots, ToolGroupBlock, useModelEffort |
features/sessions/
Session management sidebar.
| File | Purpose |
|---|---|
SessionList.tsx | Hierarchical session tree with parent-child relationships |
SessionNode.tsx | Individual session row with status indicator, context menu |
SessionInfoPanel.tsx | Session detail panel (model, tokens, thinking level) |
SpawnAgentDialog.tsx | Dialog for spawning sub-agents with task/model/thinking config |
sessionTree.ts | Builds tree structure from flat session list using parentId |
statusUtils.ts | Maps agent status to icons and labels |
features/workspace/
Workspace file editor and management tabs.
| File | Purpose |
|---|---|
WorkspacePanel.tsx | Container for workspace tabs |
WorkspaceTabs.tsx | Tab switcher (Memory, Config, Crons, Skills) |
tabs/MemoryTab.tsx | View/edit MEMORY.md and daily files |
tabs/ConfigTab.tsx | Edit workspace files (SOUL.md, TOOLS.md, USER.md, etc.) |
tabs/CronsTab.tsx | Cron job management (list, create, toggle, run) |
tabs/CronDialog.tsx | Cron creation/edit dialog |
tabs/SkillsTab.tsx | View installed skills with eligibility status |
hooks/useWorkspaceFile.ts | Fetch/save workspace files via REST API |
hooks/useCrons.ts | Cron CRUD operations via REST API |
hooks/useSkills.ts | Fetch skills list |
features/settings/
Settings drawer with tabbed sections.
| File | Purpose |
|---|---|
SettingsDrawer.tsx | Slide-out drawer container |
ConnectionSettings.tsx | Gateway URL/token, reconnect |
AudioSettings.tsx | TTS provider, model, voice, wake word |
AppearanceSettings.tsx | Theme, font selection |
features/tts/
Text-to-speech integration.
| File | Purpose |
|---|---|
useTTS.ts | Core TTS hook — speaks text via server /api/tts endpoint. Supports OpenAI, Replicate, Edge providers |
useTTSConfig.ts | Server-side TTS voice configuration management |
features/voice/
Voice input and audio feedback.
| File | Purpose |
|---|---|
useVoiceInput.ts | Web Speech API integration for voice-to-text with Whisper fallback |
audio-feedback.ts | Notification sounds (ping on response complete) |
features/markdown/
Markdown rendering pipeline.
| File | Purpose |
|---|---|
MarkdownRenderer.tsx | react-markdown with remark-gfm, syntax highlighting via highlight.js |
CodeBlockActions.tsx | Copy/run buttons on code blocks |
features/charts/
Inline chart rendering.
| File | Purpose |
|---|---|
InlineChart.tsx | Renders bar, line, pie, area charts from [chart:{...}] markers |
extractCharts.ts | Parses chart markers from message text |
features/command-palette/
Cmd+K command palette.
| File | Purpose |
|---|---|
CommandPalette.tsx | Fuzzy-search command list |
commands.ts | Command definitions (new session, reset, theme, TTS, etc.) |
features/connect/
| File | Purpose |
|---|---|
ConnectDialog.tsx | Initial gateway connection dialog with auto-connect from /api/connect-defaults |
features/activity/
| File | Purpose |
|---|---|
AgentLog.tsx | Scrolling agent activity log (tool calls, lifecycle events) |
EventLog.tsx | Raw gateway event stream display |
features/dashboard/
| File | Purpose |
|---|---|
TokenUsage.tsx | Token usage and cost display |
MemoryList.tsx | Memory listing component |
useLimits.ts | Claude Code / Codex rate limit polling |
features/memory/
| File | Purpose |
|---|---|
MemoryEditor.tsx | Inline memory editing |
MemoryItem.tsx | Individual memory display with edit/delete |
AddMemoryDialog.tsx | Dialog for adding new memories |
ConfirmDeleteDialog.tsx | Delete confirmation |
useMemories.ts | Memory CRUD operations |
Shared Components
| Path | Purpose |
|---|---|
components/TopBar.tsx | Header with agent log, token data, event indicators |
components/StatusBar.tsx | Footer with connection state, session count, sparkline, context meter |
components/ResizablePanels.tsx | Draggable split layout (chat left, panels right) |
components/ContextMeter.tsx | Visual context window usage bar |
components/ConfirmDialog.tsx | Reusable confirmation modal |
components/ErrorBoundary.tsx | Top-level error boundary |
components/PanelErrorBoundary.tsx | Per-panel error boundary (isolates failures) |
components/NerveLogo.tsx | SVG logo component |
components/skeletons/ | Loading skeleton components (Message, Session, Memory) |
components/ui/ | Primitives: button, card, dialog, input, switch, scroll-area, collapsible, AnimatedNumber, InlineSelect |
Hooks
| Hook | File | Purpose |
|---|---|---|
useWebSocket | hooks/useWebSocket.ts | Core WebSocket management — connect, RPC, auto-reconnect with exponential backoff |
useConnectionManager | hooks/useConnectionManager.ts | Auto-connect logic, credential persistence in sessionStorage |
useDashboardData | hooks/useDashboardData.ts | Fetches memories and token data via REST + SSE |
useServerEvents | hooks/useServerEvents.ts | SSE client for /api/events |
useInputHistory | hooks/useInputHistory.ts | Up/down arrow input history |
useTabCompletion | hooks/useTabCompletion.ts | Tab completion for slash commands |
useKeyboardShortcuts | hooks/useKeyboardShortcuts.ts | Global keyboard shortcut registration |
useGitInfo | hooks/useGitInfo.ts | Git branch/status display |
Libraries
| File | Purpose |
|---|---|
lib/constants.ts | App constants: context window limits (with dynamic getContextLimit() fallback), wake/stop/cancel phrase builders, attachment limits |
lib/themes.ts | Theme definitions and CSS variable application |
lib/fonts.ts | Font configuration |
lib/formatting.ts | Message formatting utilities |
lib/sanitize.ts | HTML sanitization via DOMPurify |
lib/highlight.ts | Syntax highlighting configuration |
lib/utils.ts | cn() classname merge utility (clsx + tailwind-merge) |
lib/progress-colors.ts | Color scales for progress indicators |
lib/text/isStructuredMarkdown.ts | Detects structured markdown for rendering decisions |
Backend Structure
Built with Hono (lightweight web framework), TypeScript, running on Node.js ≥22.
Entry Point
| File | Purpose |
|---|---|
server/index.ts | Starts HTTP + HTTPS servers, sets up WebSocket proxy, file watchers, graceful shutdown |
server/app.ts | Hono app definition — middleware stack, route mounting, static file serving with SPA fallback |
Middleware Stack
Applied in order in app.ts:
| Middleware | File | Purpose |
|---|---|---|
| Error handler | middleware/error-handler.ts | Catches unhandled errors, returns consistent JSON. Shows stack in dev |
| Logger | Hono built-in | Request logging |
| CORS | Hono built-in + custom | Whitelist of localhost origins + ALLOWED_ORIGINS env var. Validates via URL constructor |
| Security headers | middleware/security-headers.ts | Standard security headers (CSP, X-Frame-Options, etc.) |
| Body limit | Hono built-in | Configurable max body size (from config.limits.maxBodyBytes) |
| Compression | Hono built-in | gzip/deflate/brotli on all routes except SSE (/api/events) |
| Cache headers | middleware/cache-headers.ts | Hashed assets → immutable, API → no-cache, non-hashed static → must-revalidate |
| Rate limiting | middleware/rate-limit.ts | Per-IP sliding window, applied per-route (not as global middleware in app.ts). Separate limits for general API vs TTS/transcribe. Client ID from socket or custom header |
API Routes
| Route | File | Methods | Purpose |
|---|---|---|---|
/health | routes/health.ts | GET | Health check with gateway connectivity probe |
/api/connect-defaults | routes/connect-defaults.ts | GET | Pre-fill gateway URL/token for browser. Token only returned for loopback clients |
/api/events | routes/events.ts | GET | SSE stream for real-time push (memory.changed, tokens.updated, status.changed, ping) |
/api/events/test | routes/events.ts | POST | Broadcasts a test event (development only) |
/api/tts | routes/tts.ts | POST | Text-to-speech with provider auto-selection (OpenAI → Replicate → Edge). LRU cache with TTL |
/api/tts/config | routes/tts.ts | GET, PUT | TTS voice configuration per provider (read / partial update) |
/api/transcribe | routes/transcribe.ts | POST | Audio transcription via OpenAI Whisper. Multipart file upload, MIME validation |
/api/agentlog | routes/agent-log.ts | GET, POST | Agent activity log persistence. Zod-validated entries. Mutex-protected file I/O |
/api/tokens | routes/tokens.ts | GET | Token usage statistics — scans session transcripts, persists high water mark |
/api/memories | routes/memories.ts | GET, POST, DELETE | Memory management — reads MEMORY.md + daily files, stores/deletes via gateway tool invocation |
/api/memories/section | routes/memories.ts | GET, PUT | Read/replace a specific memory section by title |
/api/gateway/models | routes/gateway.ts | GET | Available models via openclaw models list. Allowlist support |
/api/gateway/session-info | routes/gateway.ts | GET | Current session model/thinking level |
/api/gateway/session-patch | routes/gateway.ts | POST | Change model/effort for a session |
/api/server-info | routes/server-info.ts | GET | Server time, gateway uptime, agent name |
/api/version | routes/version.ts | GET | Package version from package.json |
/api/git-info | routes/git-info.ts | GET, POST, DELETE | Git branch/status. Session workdir registration |
/api/workspace | routes/workspace.ts | GET | List available workspace file keys and their existence status |
/api/workspace/:key | routes/workspace.ts | GET, PUT | Read/write workspace files (strict key→file allowlist: soul, tools, identity, user, agents, heartbeat) |
/api/crons | routes/crons.ts | GET, POST, PATCH, DELETE | Cron job CRUD via gateway tool invocation |
/api/crons/:id/toggle | routes/crons.ts | POST | Toggle cron enabled/disabled |
/api/crons/:id/run | routes/crons.ts | POST | Run cron job immediately |
/api/crons/:id/runs | routes/crons.ts | GET | Cron run history |
/api/skills | routes/skills.ts | GET | List skills via openclaw skills list --json |
/api/files | routes/files.ts | GET | Serve local image files (MIME-type restricted, directory traversal blocked) |
/api/claude-code-limits | routes/claude-code-limits.ts | GET | Claude Code rate limits via PTY + CLI parsing |
/api/codex-limits | routes/codex-limits.ts | GET | Codex rate limits via OpenAI API with local file fallback |
Server Libraries
| File | Purpose |
|---|---|
lib/config.ts | Centralized configuration from env vars — ports, keys, paths, limits. Validated at startup |
lib/ws-proxy.ts | WebSocket proxy — client→gateway with device identity injection (Ed25519 challenge-response) |
lib/device-identity.ts | Ed25519 keypair generation/persistence for gateway auth. Stored in ~/.nerve/device-identity.json |
lib/gateway-client.ts | HTTP client for gateway tool invocation API (/tools/invoke) |
lib/file-watcher.ts | Watches MEMORY.md and memory/ directory, broadcasts SSE events on changes |
lib/files.ts | Async file helpers (readJSON, writeJSON, readText) |
lib/mutex.ts | Async mutex for serializing file read-modify-write. Includes keyed mutex variant |
lib/cached-fetch.ts | Generic TTL cache with in-flight request deduplication |
lib/usage-tracker.ts | Persistent token usage high water mark tracking |
lib/tts-config.ts | TTS voice configuration file management |
lib/openclaw-bin.ts | Resolves openclaw binary path (env → sibling of node → common paths → PATH) |
Services
| File | Purpose |
|---|---|
services/openai-tts.ts | OpenAI TTS API client (gpt-4o-mini-tts, tts-1, tts-1-hd) |
services/replicate-tts.ts | Replicate API client for hosted TTS models (Qwen3-TTS). WAV→MP3 via ffmpeg |
services/edge-tts.ts | Microsoft Edge Read-Aloud TTS via WebSocket protocol. Free, zero-config. Includes Sec-MS-GEC token generation |
services/tts-cache.ts | LRU in-memory TTS cache with TTL expiry (100 MB budget) |
services/openai-whisper.ts | OpenAI Whisper transcription client |
services/claude-usage.ts | Claude Code CLI usage/limits parser via node-pty |
Data Flow
WebSocket Proxy
Browser WS → /ws?target=ws://gateway:18789/ws → ws-proxy.ts → OpenClaw Gateway- Client connects to
/wsendpoint on Nerve server - Proxy validates target URL against
WS_ALLOWED_HOSTSallowlist - Proxy opens upstream WebSocket to the gateway
- On
connect.challengeevent, proxy intercepts the client'sconnectrequest and injects Ed25519 device identity (deviceblock with signed nonce) - After handshake, all messages are transparently forwarded bidirectionally
- Pending messages are buffered (capped at 100 messages / 1 MB) while upstream connects
Server-Sent Events (SSE)
Browser → GET /api/events → SSE stream (text/event-stream)Events pushed by the server:
memory.changed— File watcher detects MEMORY.md or daily file changestokens.updated— Token usage data changedstatus.changed— Gateway status changedping— Keep-alive every 30 seconds
SSE is excluded from compression middleware to avoid buffering.
REST API
REST endpoints serve two purposes:
- Proxy to gateway — Routes like
/api/crons,/api/memories(POST/DELETE),/api/gateway/*invoke gateway tools viainvokeGatewayTool() - Local server data — Routes like
/api/tokens,/api/agentlog,/api/server-inforead from local files or process info
Gateway RPC (via WebSocket)
The frontend calls gateway methods via GatewayContext.rpc():
| Method | Purpose |
|---|---|
status | Get current agent model, thinking level |
sessions.list | List active sessions |
sessions.delete | Delete a session |
sessions.reset | Clear session context |
sessions.patch | Rename a session |
chat.send | Send a message (with idempotency key) |
chat.history | Load message history |
chat.abort | Abort current generation |
connect | Initial handshake with auth/device identity |
Event Types (Gateway → Client)
| Event | Payload | Purpose |
|---|---|---|
connect.challenge | { nonce } | Auth handshake initiation |
chat | { sessionKey, state, message?, content? } | Chat state changes: started, delta, final, error, aborted |
agent | { sessionKey, state, stream, data? } | Agent lifecycle: lifecycle.start/end/error, tool.start/result, assistant stream |
cron | { name } | Cron job triggered |
exec.approval.request | — | Exec approval requested |
exec.approval.resolved | — | Exec approval granted |
presence | — | Presence updates |
Build System
Development
npm run dev # Vite dev server (frontend) — port 3080
npm run dev:server # tsx watch (backend) — port 3081Vite proxies /api and /ws to the backend dev server.
Production
npm run prod # Builds frontend + backend, then starts
# Equivalent to:
npm run build # tsc -b && vite build → dist/
npm run build:server # tsc -p config/tsconfig.server.json → server-dist/
npm start # node server-dist/index.jsVite Configuration
- Plugins:
@vitejs/plugin-react,@tailwindcss/vite - Path alias:
@/→./src/ - Manual chunks:
react-vendor,markdown(react-markdown + highlight.js),ui-vendor(lucide-react),utils(clsx, tailwind-merge, dompurify) - HTTPS: Auto-enabled if
certs/cert.pemandcerts/key.pemexist
TypeScript Configuration
Project references with four configs:
config/tsconfig.app.json— Frontend (src/)config/tsconfig.node.json— Vite/build toolingconfig/tsconfig.server.json— Backend (server/) → compiled toserver-dist/config/tsconfig.scripts.json— Setup scripts
Testing
Framework: Vitest with jsdom environment for React tests.
npm test # Run all tests
npm run test:coverage # With V8 coverageTest Files
| Test | Coverage |
|---|---|
src/hooks/useWebSocket.test.ts | WebSocket connection, RPC, reconnection |
src/hooks/useServerEvents.test.ts | SSE client |
src/features/tts/useTTS.test.ts | TTS hook behavior |
src/features/voice/useVoiceInput.test.ts | Voice input |
src/features/voice/audio-feedback.test.ts | Audio feedback |
src/features/sessions/unreadSessions.test.ts | Unread tracking |
src/lib/formatting.test.ts | Message formatting |
src/lib/constants.test.ts | Constants validation |
src/lib/sanitize.test.ts | HTML sanitization |
src/lib/voice-prefix.test.ts | Voice prefix parsing |
server/routes/health.test.ts | Health endpoint |
server/services/tts-cache.test.ts | TTS cache LRU/TTL |
server/middleware/rate-limit.test.ts | Rate limiting |
server/lib/mutex.test.ts | Async mutex |
Configuration
- Environment: jsdom (browser APIs mocked)
- Setup:
src/test/setup.ts - Exclusions:
node_modules/,server-dist/(avoids duplicate compiled test files) - Coverage: V8 provider, text + HTML + lcov reporters