WebChat: Native and Control UI Chat Over Gateway WebSocket
Learn how WebChat provides a native chat interface for the gateway, using the same sessions and routing rules as other channels. This guide is for developers integrating or using WebChat on macOS, iOS, or the Control UI.
Read this when
- Debugging or configuring WebChat access
- Understanding human mention delivery and retry behavior
Status: on macOS and iOS, the SwiftUI chat interface communicates with the Gateway through the WebSocket directly. There is no embedded browser and no local static server involved.
What it is
- A native chat interface for the gateway.
- It relies on the same sessions and routing rules as every other channel.
- Routing is deterministic: responses are always directed back to WebChat.
- History always comes from the gateway, not from local file monitoring. If the gateway cannot be reached, WebChat operates in read-only mode.
Quick start
- Launch the gateway.
- Open the WebChat interface (the macOS/iOS app) or the chat tab in the Control UI.
- Confirm that a valid gateway auth path is set up (shared-secret is the default, even when connected over loopback).
How it works
- The UI connects to the Gateway WebSocket and invokes the
chat.history,chat.send,chat.inject, andchat.message.getRPC methods. chat.historyis capped to keep things stable: the Gateway may shorten long text fields, drop heavy metadata, and swap out entries that are too large with[chat.history omitted: message too large]. While filling the requested visible-message window from the existing indexed transcript, history pages skip hidden and tool-only transcript entries. API clients can pass a per-requestmaxCharsto change the default limit for a single call.- When a visible assistant message was cut short in
chat.history, Show more pulls the full display-normalized entry inline throughchat.message.get, without enlarging the default history payload. Show less brings back the truncated preview and reuses the fetched content the next time it is expanded.chat.message.getfollows the same transcript branch and display rules aschat.history, but it targets a single entry bymessageIdand gives an honest unavailable reason when the full content can no longer be produced. chat.historytracks the active transcript branch for append-only session files, so abandoned rewrite branches and outdated prompt copies never show up in WebChat.- Compaction entries appear as a "Compacted history" divider, explaining that the compacted transcript is kept as a checkpoint, with an action to open session checkpoints (branch or restore, provided permissions allow).
- The Control UI remembers the backing Gateway
sessionIdreturned bychat.historyand sends it along on laterchat.sendcalls, so reconnects and page refreshes keep the same stored conversation unless the user starts or resets a session. - Foreground sends also include the displayed branch's leaf from the rendered history as
expectedLeafEntryId; if another client switched branches first, Control UI holds the message for review and refreshes the transcript rather than posting it to the new branch. Reconnect and restored-outbox replays deliberately omit this precondition after reconciling current history. - When you change a chat setting and send right away, Control UI shows Applying chat settings until that change and its session refresh complete. Later background session refreshes do not prolong this wait. Opening a pane without changing a setting does not trigger a settings wait.
chat.sendtakes an idempotency key (Control UI uses the run id); the Gateway dedupes repeated requests that reuse the same key, so retried or duplicate in-flight submits for the same session/message/attachments do not spawn a second run.- Replying to a specific message (right-click → Reply) sends the target's transcript id as
replyToIdonchat.send. The Gateway resolves that message from session history and hydrates the same channel-agnostic reply context metadata Discord replies use: agents seehas_reply_contextplus the untrusted "Reply target of current user message" block with sender label and body. (Webchat prompts keep volatile conversation ids such asreply_to_idsuppressed, per the existing byte-stable prompt policy for direct webchat sessions.) Reply targets without a persisted transcript id (for example pending sends) fall back to an inline quote in the message body. - Workspace startup files and pending
BOOTSTRAP.mdinstructions are provided through the agent system prompt's# Project Contextsection, not copied into the WebChat user message. If bootstrap content is truncated, the system prompt gets a short "Bootstrap Context Notice" instead; detailed counts and config knobs stay on diagnostic surfaces. - Display normalization on
chat.historystrips: runtime-only OpenClaw context, inbound envelope wrappers, inline delivery directive tags such as[[reply_to_current]],[[reply_to:<id>]], and[[audio_as_voice]], plain-text tool-call XML payloads (<tool_call>,<function_call>,<tool_calls>,<function_calls>, including truncated blocks), and leaked ASCII/full-width model control tokens. Assistant entries whose whole visible text is only the silent tokenNO_REPLY(case-insensitive) are omitted. - When a reply attachment cannot be read or prepared, WebChat preserves any deliverable attachments and shows one short failure warning without exposing local filesystem paths.
- Attachment directives owned by the current WebChat reply stay hidden in live transcript events while files are prepared. User prompts, fenced examples, and references outside that reply's attachment pipeline remain unchanged.
- Reasoning-flagged reply payloads (
isReasoning: true) are excluded from WebChat assistant content, transcript replay text, and audio content blocks, so thinking-only payloads do not surface as visible assistant messages or playable audio. chat.injectappends an assistant note directly to the transcript and broadcasts it to the UI (no agent run).- Aborted runs can keep partial assistant output visible in the UI. Gateway persists that partial text into transcript history when buffered output exists, and marks the entry with abort metadata.
Transcript and delivery model
WebChat has two separate data paths:
- The SQLite transcript rows are the durable model/runtime transcript. For normal agent runs, the embedded OpenClaw runtime persists model-visible
user,assistant, andtoolResultmessages through the session accessor. WebChat does not write arbitrary delivery, status, or helper text into that transcript. - Gateway
ReplyPayloadevents are the live delivery projection: normalized for WebChat/channel display, block streaming, directive tags, media embedding, TTS/audio flags, and UI fallback behavior. They are not themselves the canonical session log. - Harnesses that require visible replies through
tools.messagestill use WebChat as a current-run internal source reply sink. A targetlessmessage.sendfrom that active WebChat run is projected into the same chat and mirrored to the session transcript; WebChat does not become a reusable outbound channel and never inheritslastChannel. - WebChat injects assistant transcript entries only when the Gateway owns a displayed message outside a normal embedded agent turn:
chat.inject, non-agent command replies, aborted partial output, and WebChat-managed media transcript supplements. - If live assistant text appears during a run but disappears after history reload, check in order: whether the SQLite transcript contains the assistant text, whether
chat.historydisplay projection stripped it, then whether the Control UI optimistic-tail merge replaced local delivery state with the persisted snapshot.
Normal agent-run final answers should be durable because the embedded runtime writes the assistant message_end. Any fallback that mirrors a delivered final payload into the transcript must first avoid duplicating an assistant turn that the embedded runtime already wrote.
Control UI agents tools panel
- The Control UI
/agentsTools panel has an "Available Right Now" view backed bytools.effective(sessionKey=...): a server-derived, read-only projection of the current session's tool inventory, including core, plugin, channel-owned, and already-discovered MCP server tools. - A separate config-editing view (backed by
tools.catalog) covers profiles, per-agent overrides, and catalog semantics. - Runtime availability is session-scoped. Switching sessions on the same agent can change the "Available Right Now" list. If configured MCP servers have not been connected or changed since the last discovery, the panel shows a notice instead of silently starting MCP transports from the read path.
- The config editor does not imply runtime availability; effective access still follows policy precedence (
allow/deny, per-agent and provider/channel overrides).
Remote use
- Remote mode tunnels the gateway WebSocket over SSH/Tailscale.
- You do not need to run a separate WebChat server.
Configuration reference (WebChat)
For the complete setup, see Configuration.
WebChat does not keep a persisted configuration section. The built-in chat.history display limit is what Gateway relies on, while API clients may pass maxChars per request to change it for that particular call. The older channels.webchat and gateway.webchat configuration entries are no longer supported; execute openclaw doctor --fix to clear them out.
Global options that apply here:
gateway.port,gateway.bind: where the WebSocket listens, host and port.gateway.auth.mode,gateway.auth.token,gateway.auth.password: WebSocket authentication through a shared secret.gateway.auth.allowTailscale: when enabled, the browser Control UI chat tab can make use of Tailscale Serve identity headers.gateway.auth.mode: "trusted-proxy": reverse-proxy authentication for browser clients coming through an identity-aware non-loopback proxy source, as described in Trusted Proxy Auth.gateway.remote.url,gateway.remote.token,gateway.remote.password: where the remote gateway points.session.*: defaults for session storage and the main key.