docs/web/control-ui.md
The Control UI is a small Vite + Lit single-page app served by the Gateway:
http://<host>:18789/gateway.controlUi.basePath (e.g. /openclaw)It speaks directly to the Gateway WebSocket on the same port.
If the Gateway is running on the same computer, open http://127.0.0.1:18789/ (or http://localhost:18789/).
If the page fails to load, start the Gateway first: openclaw gateway.
Auth is supplied during the WebSocket handshake via:
connect.params.auth.tokenconnect.params.auth.passwordgateway.auth.allowTailscale: truegateway.auth.mode: "trusted-proxy"The dashboard settings panel keeps a token for the current browser tab session and selected gateway URL; passwords are not persisted. Onboarding usually generates a gateway token for shared-secret auth on first connect, but password auth works too when gateway.auth.mode is "password".
Connecting from a new browser or device usually requires a one-time pairing approval, shown as disconnected (1008): pairing required.
If the browser retries pairing with changed auth details (role/scopes/public key), the previous pending request is superseded and a new requestId is created; re-run openclaw devices list before approving.
Switching an already-paired browser from read access to write/admin access is treated as an approval upgrade, not a silent reconnect: OpenClaw keeps the old approval active, blocks the broader reconnect, and asks you to approve the new scope set explicitly.
Once approved, the device is remembered and won't require re-approval unless you revoke it with openclaw devices revoke --device <id> --role <role>. See Devices CLI for token rotation, revocation, and the Paperclip / openclaw_gateway first-run approval flow.
An already paired administrator can create the iOS/Android connection QR without opening a terminal:
<Steps> <Step title="Open mobile pairing"> Select **Nodes**, then click **Pair mobile device** in the **Devices** card. </Step> <Step title="Connect the phone"> In the OpenClaw mobile app, open **Settings** → **Gateway** and scan the QR code. You can copy and paste the setup code instead. </Step> <Step title="Confirm the connection"> The official iOS/Android app connects automatically. If **Devices** shows a pending request, review its role and scopes before approving it. </Step> </Steps>Creating a setup code requires operator.admin; the button is disabled for sessions without it. A setup code contains a short-lived bootstrap credential, so treat the QR and copied code like a password while they are valid. For remote pairing, the Gateway must resolve to wss:// (for example, through Tailscale Serve/Funnel); plain ws:// is limited to loopback and private LAN addresses. See Pairing for the full security and fallback details.
The Control UI supports a per-browser personal identity (display name and avatar) attached to outgoing messages, for attribution in shared sessions. It lives in browser storage, scoped to the current browser profile, and is not synced to other devices or persisted server-side beyond the normal transcript authorship metadata on messages you send. Clearing site data or switching browsers resets it to empty.
The assistant avatar override follows the same browser-local pattern: uploaded overrides overlay the gateway-resolved identity locally and never round-trip through config.patch. The shared ui.assistant.avatar config field is still available for non-UI clients that write the field directly.
The Control UI fetches its runtime settings from /control-ui-config.json, resolved relative to the gateway's Control UI base path (for example /__openclaw__/control-ui-config.json under base path /__openclaw__/). That endpoint is gated by the same gateway auth as the rest of the HTTP surface: unauthenticated browsers cannot fetch it, and a successful fetch requires a valid gateway token/password, Tailscale Serve identity, or a trusted-proxy identity.
Open Settings in Simple view to see the Gateway Host card with the Gateway machine, LAN address, operating system, runtime, uptime, CPU load, memory, and state-volume disk space. The card refreshes every 10 seconds while visible through the system.info Gateway RPC, which requires the operator.read scope. Older Gateways and connections without that scope omit the card.
The Control UI localizes itself on first load based on your browser locale. To override it later, open Overview -> Gateway Access -> Language (the picker lives in the Gateway Access card, not under Appearance).
en, ar, de, es, fa, fr, hi, id, it, ja-JP, ko, nl, pl, pt-BR, ru, th, tr, uk, vi, zh-CN, zh-TWDocs translations are generated for the same non-English locale set, but the docs site's built-in Mintlify language picker only lists locale codes Mintlify accepts. Thai (th) and Persian (fa) docs are still generated in the publish repo; they may not appear in that picker until Mintlify supports those codes.
The Appearance panel has the built-in Claw, Knot, and Dash themes (Claw is default), plus one browser-local tweakcn import slot. To import a theme, open the tweakcn editor, choose or create a theme, click Share, and paste the copied link into Appearance. The importer also accepts https://tweakcn.com/r/themes/<id> registry URLs, editor URLs like https://tweakcn.com/editor/theme?theme=amethyst-haze, relative /themes/<id> paths, raw theme IDs, and default theme names such as amethyst-haze.
Imported themes are stored only in the current browser profile; they are not written to gateway config and do not sync across devices. Replacing the imported theme updates the one local slot; clearing it switches back to Claw if the imported theme was active.
Appearance also has a browser-local Text size setting, stored with the rest of Control UI preferences. It applies to chat text, composer text, tool cards, and chat sidebars, and keeps text inputs at least 16px so mobile Safari does not auto-zoom on focus.
The sidebar pins navigation above a scrollable recent-session list split into Pinned, one section per custom group (the session category, sorted alphabetically), and Ungrouped for the rest. Every pinned session stays visible, while unpinned sessions keep an independent nine-item recent budget. Opening a visible session moves the selection highlight without reordering the rows; an off-list deep link is surfaced at the top. Sessions with new activity since they were last read show an unread dot, and opening one marks it read. Each session row has a context menu (kebab button or right-click) with Pin/Unpin, Mark as unread/read, Rename, Fork, Move to group (including New group and Remove from group), Archive, and Delete; touch layouts keep the direct pin and menu controls visible. Group headers have their own menu (kebab button or right-click) with Rename group, New group, and Delete group; renaming or deleting a group updates every member session, including archived ones, and deleting a group keeps its sessions and moves them back to Ungrouped. Groups created from the header start empty and stay visible as move targets. The sort control in the session list header also has a Group by toggle: Custom groups (default) or None for one flat list (Pinned stays separate); the choice is stored in the current browser profile. Multi-agent setups show a compact scope control in the Ungrouped header. Overview is the only destination pinned by default; expand More to reach every other destination. Select Customize sidebar under More, or right-click the navigation area, to pin or unpin destinations and restore the defaults. The pinned set and More expansion state are stored in the current browser profile and survive reloads.
A Search field at the top of the sidebar opens the command palette (⌘K). The compact footer keeps connection status, Settings, Docs, mobile pairing, and the light/dark/system color-mode toggle together. The sidebar collapse toggle sits at the left edge of the top bar, macOS style; collapsing shrinks the sidebar to an icon rail. At drawer breakpoints, the same top bar button opens the sidebar as a slide-over drawer instead.
The dedicated MCP page is an operator view for OpenClaw-managed MCP servers under mcp.servers. It does not start MCP transports by itself; use it to inspect and edit saved config, then use openclaw mcp doctor --probe when you need live server proof.
Typical workflow:
mcp config section for server definitions, headers, TLS/mTLS paths, OAuth metadata, tool filters, and Codex projection metadata.openclaw mcp status --verbose, openclaw mcp doctor --probe, or openclaw mcp reload from a terminal for static diagnostics, live proof, or cached-runtime disposal.The page redacts credential-bearing URL-like values before rendering and quotes server names in command snippets so copied commands still work with spaces or shell metacharacters. Full CLI and config reference: MCP.
The Activity tab is an ephemeral browser-local observer for live tool activity, derived from the same Gateway session.tool / tool event stream that powers Chat tool cards. It does not add another Gateway event family, endpoint, durable activity store, metrics feed, or external observer stream.
Activity entries keep only sanitized summaries and redacted, truncated output previews. Tool argument values are not stored in Activity state; the UI shows that arguments are hidden and records only the argument field count. The in-memory list follows the current browser tab, survives navigation within the Control UI, and resets on page reload, session switch, or Clear.
The dockable operator terminal is disabled by default. To enable it, set gateway.terminal.enabled: true and restart the Gateway. The terminal requires an operator.admin connection and opens a host PTY in the active agent workspace. New tabs follow the currently selected chat agent.
Use Ctrl + backtick to toggle the dock. The layout supports bottom and right docking, resizes with the browser viewport, and keeps multiple shell tabs. See Gateway configuration for gateway.terminal.enabled and the optional gateway.terminal.shell override.
Sessions survive disconnects: a page reload, laptop sleep, or network blip detaches the session on the Gateway instead of killing it, and the same browser tab reattaches on reconnect with recent output replayed. Detached sessions are killed after gateway.terminal.detachedSessionTimeoutSeconds (default 300 seconds; 0 restores kill-on-disconnect). terminal.list shows attachable sessions, terminal.attach adopts one (tmux-style take-over), and terminal.text reads a session's recent output as plain text without attaching - an agent/tooling affordance.
The terminal is also available as a full-screen, terminal-only document at /?view=terminal. The iOS and Android apps embed this page in their Terminal screens, reusing the stored gateway credentials; availability follows the same gateway.terminal.enabled and operator.admin gate, and the page shows a notice when the connected Gateway does not offer the terminal.
Persistent provider, model, voice, transport, reasoning effort, exact VAD threshold, silence duration, and prefix padding defaults live in **Settings → Communications → Talk**; changing them requires `operator.admin` access. Configuring Gateway relay forces the backend relay path; configuring WebRTC keeps the session client-owned and fails instead of silently falling back to relay if the provider cannot create a browser session.
The Talk control itself is the microphone button in the composer toolbar. Its caret lists **System default** and every microphone exposed by the browser, including USB, Bluetooth, and virtual inputs. The selected device ID stays browser-local and is never sent to the Gateway; if that exact device disappears, Talk asks you to choose another input instead of silently recording from a different microphone. When Talk starts, the composer status row shows `Connecting Talk...`, then `Talk live` while audio is connected, or `Asking OpenClaw...` while a realtime tool call is consulting the configured larger model through `talk.client.toolCall`.
Maintainer live smoke: `OPENAI_API_KEY=... GEMINI_API_KEY=... node --import tsx scripts/dev/realtime-talk-live-smoke.ts` verifies the OpenAI backend WebSocket bridge, OpenAI browser WebRTC SDP exchange, Google Live constrained-token browser WebSocket setup, and the Gateway relay browser adapter with fake microphone media. The command prints provider status only and does not log secrets.
Once a session is established, a dropped Gateway connection does not log you out. The dashboard stays visible with a floating amber "Gateway connection lost — Reconnecting…" pill under the top bar while the client retries automatically with backoff (800 ms up to 15 s). Live updates and actions pause until the connection returns; Retry now in the pill forces an immediate attempt.
When this browser already holds credentials (a configured token/password or an approved device token), first opens and reloads show a small animated OpenClaw mark while the connection is established instead of flashing the login gate. The login gate only appears when no credentials are stored yet or when the Gateway actively rejects them (bad token/password, revoked pairing) — states that need your input rather than waiting.
The Control UI ships a manifest.webmanifest and a service worker, so modern browsers can install it as a standalone PWA. Web Push lets the Gateway wake the installed PWA with notifications even when the tab or browser window is not open.
If the page shows Protocol mismatch right after an OpenClaw update, first reopen the dashboard with openclaw dashboard and hard-refresh. If it still fails, clear site data for the dashboard origin or test in a private browser window; an old tab or browser service-worker cache can keep running a pre-update Control UI bundle against the newer Gateway.
| Surface | What it does |
|---|---|
ui/public/manifest.webmanifest | PWA manifest. Browsers offer "Install app" once it is reachable. |
ui/public/sw.js | Service worker that handles push events and notification clicks. |
push/vapid-keys.json (under the OpenClaw state dir) | Auto-generated VAPID keypair used to sign Web Push payloads. |
push/web-push-subscriptions.json | Persisted browser subscription endpoints. |
Override the VAPID keypair through env vars on the Gateway process when you want to pin keys (multi-host deployments, secrets rotation, or tests):
OPENCLAW_VAPID_PUBLIC_KEYOPENCLAW_VAPID_PRIVATE_KEYOPENCLAW_VAPID_SUBJECT (defaults to https://openclaw.ai)The Control UI uses these scope-gated Gateway methods to register and test browser subscriptions:
push.web.vapidPublicKey fetches the active VAPID public key.push.web.subscribe registers an endpoint plus keys.p256dh/keys.auth.push.web.unsubscribe removes a registered endpoint.push.web.test sends a test notification to the caller's subscription.Assistant messages can render hosted web content inline with the [embed ...] shortcode. The iframe sandbox policy is controlled by gateway.controlUi.embedSandbox:
{
gateway: {
controlUi: {
embedSandbox: "scripts",
},
},
}
Absolute external http(s) embed URLs stay blocked by default. To let [embed url="https://..."] load third-party pages, set gateway.controlUi.allowExternalEmbedUrls: true.
Grouped chat messages use a readable default max-width. Wide-monitor deployments can override it without patching bundled CSS by setting gateway.controlUi.chatMessageMaxWidth:
{
gateway: {
controlUi: {
chatMessageMaxWidth: "min(1280px, 82%)",
},
},
}
The value is validated before it reaches the browser. Supported forms include plain lengths and percentages such as 960px or 82%, plus constrained min(...), max(...), clamp(...), calc(...), and fit-content(...) width expressions.
```bash
openclaw gateway --tailscale serve
```
Open `https://<magicdns>/` (or your configured `gateway.controlUi.basePath`).
By default, Control UI/WebSocket Serve requests can authenticate via Tailscale identity headers (`tailscale-user-login`) when `gateway.auth.allowTailscale` is `true`. OpenClaw verifies the identity by resolving the `x-forwarded-for` address with `tailscale whois` and matching it to the header, and only accepts these when the request hits loopback with Tailscale's `x-forwarded-*` headers. For Control UI operator sessions with browser device identity, this verified Serve path also skips the device-pairing round trip; device-less browsers and node-role connections still follow the normal device checks. Set `gateway.auth.allowTailscale: false` if you want to require explicit shared-secret credentials even for Serve traffic, then use `gateway.auth.mode: "token"` or `"password"`.
For that async Serve identity path, failed auth attempts for the same client IP and auth scope are serialized before rate-limit writes. Concurrent bad retries from the same browser can therefore show `retry later` on the second request instead of two plain mismatches racing in parallel.
<Warning>
Tokenless Serve auth assumes the gateway host is trusted. If untrusted local code may run on that host, require token/password auth.
</Warning>
Open `http://<tailscale-ip>:18789/` (or your configured `gateway.controlUi.basePath`).
Paste the matching shared secret into the UI settings (sent as `connect.params.auth.token` or `connect.params.auth.password`).
If you open the dashboard over plain HTTP (http://<lan-ip> or http://<tailscale-ip>), the browser runs in a non-secure context and blocks WebCrypto. By default, OpenClaw blocks Control UI connections without device identity.
Documented exceptions:
gateway.controlUi.allowInsecureAuth=truegateway.auth.mode: "trusted-proxy"gateway.controlUi.dangerouslyDisableDeviceAuth=trueRecommended fix: use HTTPS (Tailscale Serve) or open the UI locally at https://<magicdns>/ (Serve) or http://127.0.0.1:18789/ (on the gateway host).
`allowInsecureAuth` is a local compatibility toggle only:
- It lets localhost Control UI sessions proceed without device identity in non-secure HTTP contexts.
- It does not bypass pairing checks.
- It does not relax remote (non-localhost) device identity requirements.
<Warning>
`dangerouslyDisableDeviceAuth` disables Control UI device identity checks and is a severe security downgrade. Revert quickly after emergency use.
</Warning>
See Tailscale for HTTPS setup guidance.
The Control UI ships a tight img-src policy: only same-origin assets, data: URLs, and locally generated blob: URLs are allowed. Remote http(s) and protocol-relative image URLs are rejected by the browser and never issue network fetches.
In practice:
/avatars/<id>) still render, including authenticated avatar routes the UI fetches and converts into local blob: URLs.data:image/... URLs still render.blob: URLs created by the Control UI still render.data: URLs; the operator browser never contacts the remote avatar host.This is always on and not configurable.
When gateway auth is configured, the Control UI avatar endpoint requires the same gateway token as the rest of the API:
GET /avatar/<agentId> returns the avatar image only to authenticated callers. GET /avatar/<agentId>?meta=1 returns the avatar metadata under the same rule.If you disable gateway auth (not recommended on shared hosts), the avatar route also becomes unauthenticated, in line with the rest of the gateway.
When gateway auth is configured, assistant local-media previews use a two-step route:
GET /__openclaw__/assistant-media?meta=1&source=<path> requires the normal Control UI operator auth; the browser sends the gateway token as a bearer header when checking availability.mediaTicket scoped to that exact source path.mediaTicket=<ticket> instead of the active gateway token or password. The ticket expires quickly and cannot authorize a different source.This keeps media rendering compatible with browser-native media elements without putting reusable gateway credentials in visible media URLs.
The Gateway serves static files from dist/control-ui:
pnpm ui:build
Optional absolute base (fixed asset URLs):
OPENCLAW_CONTROL_UI_BASE_PATH=/openclaw/ pnpm ui:build
Local development (separate dev server):
pnpm ui:dev
Then point the UI at your Gateway WS URL (e.g. ws://127.0.0.1:18789).
If the browser loads a blank dashboard and DevTools shows no useful error, an extension or early content script may have prevented the JavaScript module app from evaluating. The static page includes a plain HTML recovery panel that appears when <openclaw-app> is not registered after startup.
Use the panel's Try again action after changing the browser environment, or reload manually after these checks:
<all_urls> content scripts.The Control UI is static files; the WebSocket target is configurable and can differ from the HTTP origin. This is handy when you want the Vite dev server locally but the Gateway runs elsewhere.
<Steps> <Step title="Start the UI dev server"> ```bash pnpm ui:dev ``` </Step> <Step title="Open with gatewayUrl"> ```text http://localhost:5173/?gatewayUrl=ws%3A%2F%2F<gateway-host>%3A18789 ```Optional one-time auth (if needed):
```text
http://localhost:5173/?gatewayUrl=wss%3A%2F%2F<gateway-host>%3A18789#token=<gateway-token>
```
{
gateway: {
controlUi: {
allowedOrigins: ["http://localhost:5173"],
},
},
}
Remote access setup details: Remote access.