Control UI (Canvas)

One-line summary

The Control UI is a Lit-based single-page web application that provides a real-time operator dashboard for managing agents, sessions, channels, skills, cron jobs, and chat — all over a single WebSocket connection to the Gateway.

Responsibilities

  • Provide a browser-based control panel for all Gateway operations (chat, config, channels, agents, sessions, usage, cron, skills, debug, logs)
  • Maintain a persistent WebSocket connection with Ed25519 device authentication and automatic reconnection
  • Render real-time streaming chat with Markdown, tool use indicators, and file attachments
  • Offer a dynamic config editor driven by the server-side Zod schema
  • Support internationalization (en, pt-BR, zh-CN, zh-TW) and mobile-responsive layout

Architecture diagram

Key source files

FileLinesRole
ui/src/ui/app.ts616Main component: <openclaw-app> Lit element, 100+ reactive state properties, lifecycle delegation
ui/src/ui/app-render.ts1,141Render logic: renderApp() dispatches to tab-specific view functions
ui/src/ui/gateway.ts360Gateway client: GatewayBrowserClient — WebSocket, RPC, events, Ed25519 device auth, auto-reconnect
ui/src/ui/app-gateway.ts350Gateway integration: Bridges Gateway events to app state updates
ui/src/ui/navigation.ts166Client-side routing: History API, 13 tabs across 4 tab groups
ui/src/ui/types.ts641Type definitions: All interfaces for the UI layer
ui/src/ui/app-view-state.ts325State interface: AppViewState — the shape of all UI state
ui/src/ui/storage.ts92Persistence: LocalStorage wrapper for settings
ui/src/ui/views/chat.ts616Chat view: Streaming messages, markdown rendering, attachments
ui/src/ui/views/config.ts820Config editor: Dynamic form generated from server-side schema
ui/package.json27Dependencies: Lit 3.3, Vite 7.3, marked 17, DOMPurify 3.3
ui/vite.config.ts42Build config: Output to ../dist/control-ui/, port 5173 dev server

Data flow

WebSocket protocol

All communication uses a single WebSocket connection with three message types:

Client → Server (Request):
  { type: "req", id: "uuid", method: "chat.send", params: {...} }

Server → Client (Response):
  { type: "res", id: "uuid", ok: true, payload: {...} }

Server → Client (Event):
  { type: "event", event: "chat", payload: { state: "delta", ... }, seq: 42 }

Authentication handshake

1. Client opens WebSocket to ws://host:18789
2. Server sends "connect.challenge" event with nonce
3. Client signs nonce with Ed25519 device key (Web Crypto API)
4. Client sends "connect" RPC with:
   - role: "operator"
   - scopes: ["operator.admin", "operator.approvals", "operator.pairing"]
   - device identity (publicKey + signature)
   - token (stored from previous connect)
5. Server responds with hello-ok + canvas host URL

Key RPC methods

MethodPurpose
connectInitial handshake with device auth
chat.sendSend user message
chat.historyLoad chat messages
chat.abortCancel in-flight agent turn
config.get / config.saveRead/write config
channels.statusChannel connection states
agents.listList configured agents
sessions.listList session history
usage.queryToken/cost analytics
cron.jobs.listList scheduled jobs
skills.reportSkills catalog status

Event types (Server → Client)

EventPurpose
chatStreaming chat deltas, completion
agentTool use, thinking indicators
healthSystem health updates
presenceConnected clients
cronJob status changes
tickPeriodic state sync

Page structure

4 tab groups, 13 tabs total:

GroupTabsPurpose
ChatchatMain conversation interface
Controloverview, channels, instances, sessions, usage, cronSystem management
Agentagents, skills, nodesAgent configuration
Settingsconfig, debug, logsConfiguration and diagnostics

Technology stack

ComponentTechnologyVersion
UI FrameworkLit (Web Components)3.3.2
Build ToolVite7.3.1
Markdownmarked17.0.3
XSS SanitizationDOMPurify3.3.1
Crypto@noble/ed255193.0.0
TestingVitest + Playwright4.0.18
i18nCustom (4 locales)-

State management

No external state library. Uses Lit's built-in reactive system:

@state() property → mutation → automatic re-render → view update
  • 100+ reactive properties on OpenClawApp — single source of truth
  • Views are pure functions that receive AppViewState and return TemplateResult
  • Controllers handle user actions, call Gateway RPC, and update @state() properties
  • LocalStorage persists theme, selected tab, and device settings
  • IndexedDB stores Ed25519 device identity (persistent across sessions)

Reconnection strategy

On disconnect:
  1. Wait 800ms (initial delay)
  2. Attempt reconnect
  3. On failure: delay *= 1.5 (exponential backoff)
  4. Max delay: 15,000ms (15 seconds)
  5. On success: reset delay to 800ms

Event sequence tracking:
  - Each event has a monotonic `seq` number
  - Client detects gaps (missed events during disconnect)
  - Gap detected → full state resync from server

How it connects to other modules

  • Depends on:

    • gateway/ — WebSocket server hosts the Control UI and handles all RPC methods
    • Gateway serves the built dist/control-ui/ files on port 18789+1
  • Depended by:

    • None — the Control UI is a leaf node, purely a client

Build and deployment

Development:
  cd ui/ && npm run dev  →  Vite dev server on port 5173

Production:
  cd ui/ && npm run build  →  ../dist/control-ui/
  Gateway serves static files from dist/control-ui/ on port 18790

The Gateway auto-discovers and serves the built Control UI — no separate deployment needed.

My blind spots

  • Exact WebSocket message types for all 13 tabs — I've documented the main ones but each tab likely has additional RPC methods
  • How the dynamic config form in config.ts (820 lines) maps server-side Zod schema to form fields — need to trace the full schema → form pipeline
  • Whether the event sequence gap detection triggers a full reload or a selective resync
  • How exec-approval.ts works — the operator approval flow for tool execution
  • Canvas A2UI integration details — how the WebView-based canvas protocol works from the UI side

Change frequency

  • app.ts: Medium — new state properties added as features ship
  • gateway.ts: Low — WebSocket protocol is stable
  • app-render.ts: Medium — layout changes and new tab additions
  • views/: High — individual views change as features evolve
  • controllers/: High — business logic tied to feature development