WebChat on macOS: Embedding and Debugging
This page explains how the macOS menu bar app embeds the gateway WebChat as a native SwiftUI view, including its split layout and session management. It is intended for developers who need to debug or customize the WebChat integration.
Read this when
- Debugging mac WebChat view or loopback port
- Choosing colors for native chat sessions
The macOS menu bar app presents the WebChat interface as a native SwiftUI view. It links to the Gateway and, by default, uses the primary session for the chosen agent (main, or global when session.scope equals global).
The full chat window uses a native split layout:
- Sessions sidebar: a searchable list of sessions with pinned, gateway-backed group, and recent categories. Child sessions spawned from a parent appear nested under it within each category; collapsed parents show summaries of running, failed, and unread descendants. Context menus provide actions for session info, rename, pin, fork, read/unread, archive/restore, copy session key, and delete. The main new-session action (or Shift-Cmd-N) creates a session immediately via
sessions.create; the adjacent options popover lets you pick an agent and request a managed worktree with an optional base ref. - Window toolbar: a context-usage ring (showing tokens and session cost, with a compact action), model controls, and a session actions menu. Models are arranged by provider, with the default provider listed first, while pinned and recent models stay at the top. The controls can inherit or override the model's thinking level, set tool-call verbosity, and toggle Fast responses. The menu can rename or fork the current session and adjust its pin, read, or archive state. Sessions… (Shift-Cmd-S) opens the Active/Archived manager for gateway search, group management, session inspection, rename, pin, archive, and restore. Select mode applies pin, unpin, archive, or delete to multiple active sessions while keeping individual failures visible. Separate menu checkmarks control the display of assistant reasoning and tool activity; both default to on and persist across launches.
- Transcript and composer: assistant messages appear as plain text with an avatar, while user messages use accent bubbles. Pending agent questions render as native cards with single- or multi-select options, free-text Other answers, expiry countdowns, and shared terminal state. Empty chats show desktop starter prompts. Typing
/triggers slash-command autocomplete powered bycommands.list, with arrow/Tab/Return/Escape keyboard navigation. Right-click a message to copy its visible Markdown without hidden reasoning. Truncated assistant messages also offer Open Full Message, which loads a selectable Markdown reader. Use Listen for gateway TTS with a local speech fallback. - Voice controls: the composer can start or stop the existing macOS Talk Mode without replacing its menu-bar overlay. While Talk Mode is active, the composer displays its listening/thinking/speaking state, live audio activity, and an expandable rolling transcript. Right-click the Talk button to choose System Default or a connected microphone; this matches the microphone selection used by Voice Wake and push-to-talk. If a selected microphone disconnects, the active Talk session falls back to the system default and retries the selection the next time Talk Mode starts. A separate microphone action records a voice note when Talk Mode does not own audio capture.
The anchored compact chat panel from the menu bar keeps the compact single-column layout with the same model, thinking, verbosity, and Fast controls inline, plus starter prompts, Talk Mode, voice notes, and Listen. Assistant reasoning and tool activity remain hidden in this compact surface.
Multiple Gateway windows
Go to Settings → Gateways to add or remove reusable Gateway profiles. Each profile includes a private-network ws:// or secure wss:// endpoint and its optional token or password; credentials are stored in the macOS Keychain. Secure profiles maintain their own system-trust-gated first-use certificate pin and do not inherit gateway.remote.tlsFingerprint from the primary Gateway. Dashboard windows enforce that same saved-profile pinning policy. Removing a profile also closes its open windows and shuts down its secondary connection.
Choose File → New Gateway Window… or press Cmd-N, then select one of those saved profiles. The picker remembers the most recently used profile. Every selection creates a new independent window, so the same Gateway can appear in multiple windows with different active sessions and navigation state.
Each saved profile owns one shared Gateway connection, device-auth scope, transcript cache, offline outbox, and route leases. Windows for that profile reuse those resources while staying independently navigable. Windows for different profiles stay connected and run chats simultaneously.
The menu-bar app's configured Gateway remains the owner of Mac node capabilities and Talk Mode. Additional Gateway windows are operator-only, so a second Gateway cannot silently retarget global microphone or device controls. Listen/TTS and normal chat actions use the window's own Gateway connection.
Gateway picker
The dashboard header shows a Gateway picker when the Mac app has at least two configured Gateways. Choose a Gateway to replace the current dashboard in the same window, or Option-click it to open a separate dashboard window. Set as primary… makes the viewed token-authenticated profile the Mac app's primary Gateway after confirmation; this resets Talk Mode, the widget panel, and chat connections. While connected, the sidebar footer also shows the current Gateway and marks it when it is primary. Password-only profiles can be viewed but cannot be made primary.
Quick Chat bar
Press Option-Space (⌥Space) or choose Quick Chat from the menu bar menu to open a floating composer for the main session. Change the global shortcut with the recorder in Settings → General → Quick Chat shortcut.
Quick Chat shows the targeted agent (avatar or emoji, with the agent's name as the placeholder) and sends to that agent's main session. After Return accepts a send, the bar stays open and expands downward with the streamed Markdown reply and recent transcript. The bar input remains the composer. Press Command-Return to send and open the same target in the full chat window, Shift-Return for a newline, or Escape to dismiss the whole bar and reply area. Clicking outside also dismisses it. When relevant macOS permissions are missing, an attached strip offers Grant and Not now actions.
Use the microphone button to dictate into the composer. Partial speech results replace the dictated span live while preserving text that was already in the composer. Press the button again, Return, or Escape to stop; sending, hiding, or unfocusing Quick Chat also releases the microphone. The first use asks for macOS Microphone and Speech Recognition access. Quick Chat uses Apple Speech and may use its network services; only passive Voice Wake requires on-device recognition.
The compact model control shows the target session's current model and reasoning level. A model choice updates that session and therefore persists there, while a reasoning choice applies only to each message sent from the current Quick Chat presentation. Local choices reset when the bar hides. Switching agents or choosing a recent session keeps explicit choices but reloads the newly targeted session's underlying model state.
Click the history button to choose from the five most recently updated sessions or return to New message to <agent>. A recent selection sends to that exact session and changes the placeholder to Reply in <session>. Hiding Quick Chat resets this temporary target to the selected agent's main session; switching agents from the avatar menu also clears it.
Command-Return opens the conversation of the agent that received the send, including when session scope is global.
The camera button opens a menu for Capture Window… or Capture Area…. Window capture labels every visible window; area capture dims each display while you drag a region and shows its live size. The selected screenshot is sent to the chosen agent with any typed text as its caption. The first use asks for macOS Screen Recording access. Escape, clicking empty space, or clicking without a meaningful area drag cancels.
Use the document-text button to attach text from the focused app's focused window. Quick Chat shows the result as a removable context chip rather than placing the captured text in the composer; sending appends the chip's text to the outgoing message and then clears it. This requires macOS Accessibility permission. Attached text also clears whenever Quick Chat closes, so context from one presentation cannot leak into a later send.
Once a reply is complete, select Paste to <app> to take its visible assistant text, minus any hidden reasoning, and place it on the general pasteboard, then insert it into whichever app was active. macOS Accessibility permission is needed for this. The clipboard's existing contents get overwritten, and Quick Chat closes afterward.
To turn the whole feature off, go to Settings → General → Quick Chat; the shortcut recorder lives in that same area.
- Local mode: establishes a direct connection to the local Gateway WebSocket.
- Remote mode: relies on the configured direct
ws:///wss://route or the app-managed SSH tunnel to carry the data plane.
Launch and debugging
-
Manually: open via Lobster menu -> "Open Chat".
-
Auto-open for testing:
dist/OpenClaw.app/Contents/MacOS/OpenClaw --chat(
--webchatworks as a legacy alias.) -
Logs:
./scripts/clawlog.sh(subsystemai.openclaw, categoryWebChatSwiftUI).
How it is wired
- Data plane: Gateway WS methods
chat.history,chat.message.get,chat.send,chat.abort,chat.inject, along withquestion.listandquestion.resolve, plus eventschat,agent,presence,tick,health; question cards follow thequestion.requestedandquestion.resolvedevents and pull fresh data fromquestion.listafter reconnects. - A display-normalized transcript is what
chat.historyprovides: visible text gets stripped of inline directive tags, plain-text tool-call XML payloads (<tool_call>,<function_call>,<tool_calls>,<function_calls>, including truncated blocks) and leaked model control tokens are removed, assistant rows made purely of silent tokens like exactNO_REPLY/no_replyare left out, and oversized rows may be swapped for a truncated placeholder. - Session: defaults to the primary session as described earlier; switching between sessions is supported in the UI.
- Session groups: the path-free group catalog is owned by
sessions.groups.list,sessions.groups.put,sessions.groups.rename, andsessions.groups.delete. Optional New Session folder/worktree defaults are owned by write-scopedsessions.groups.defaultsandsessions.groups.update. Membership comes from the sessioncategory, updated viasessions.patchor assigned duringsessions.create. - Unread state: once a session becomes active and its live history loads without error, the app clears that session's unread marker. If history loading fails, the marker stays; a transient patch failure gets retried on the next activation.
- Onboarding keeps first-run setup separate by using a dedicated session.
- Offline cache: per gateway (
~/Library/Application Support/OpenClaw/chat-cache.sqlite), the app holds a small read-only cache of recent chat sessions and transcripts: cold opens show the last known transcript right away and refresh once the Gateway responds, and recent chats remain browsable while offline (sending stays disabled until the connection returns).
Security surface
- Over SSH, remote mode forwards only the Gateway WebSocket control port.
Known limitations
- The UI targets chat sessions rather than a full browser sandbox.