Status: the macOS/iOS SwiftUI chat UI talks directly to the Gateway WebSocket. No embedded browser, no local static server.

What it is

Quick start

  1. Start the gateway.
  2. Open the WebChat UI (macOS/iOS app) or the Control UI chat tab.
  3. Ensure a valid gateway auth path is configured (shared-secret by default, even on loopback). See Auth basics (local vs remote).

How it works

Transcript and delivery model

Admission and transcript persistence are separate. A chat.send, sessions.send, or initial sessions.create acknowledgment can arrive while approved input waits in durable pending-input custody, including during workspace preparation. Skill-library selection and authoring preparation run after that acknowledgment, under the admitted run's cancellation and error handling, alongside workspace and reply startup. Preparation failures appear as run errors. The Gateway reserves the existing reply-admission ticket before acknowledging, so a later send cannot overtake a message whose preparation is still pending. An optional messageSeq comes only from a committed transcript receipt. Clients must not predict it from history length or treat status: "started" as persistence. The Control UI replaces its provisional source with accepted custody, then with the canonical row. Accepted inputs stay below saved conversation history until they are committed to the transcript. Its renderer keeps a loaded local preview in the same image element during this handoff while canonical media metadata and image bytes load. Authoritative text, media replacements, and removals still win. Unavailable or access-denied media shows a visible reason. Once custody, a consumption record, or a committed user-message receipt retires a local source, replayed terminal events cannot bring it back, even when its row is absent from a later history page. Submission identity stays separate from the execution run, so two intentionally identical sends remain two inputs.

WebChat has two separate data paths:

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.

Human mention delivery

The Control UI binds each selected person to the submitted message text. chat.send, the initial message on sessions.create, and sessions.send accept an optional mentions array of { profileId, start, end } annotations. There are at most ten annotations. start is inclusive and end is exclusive, measured in UTF-16 code units. The Gateway validates their text ranges and recipients before accepting the input. Plain @name text and agent output do not create human mentions. Copying or quoting text does not copy its recipient selections.

The mention becomes eligible for an Inbox entry and optional browser push only after the original human message is newly committed to the transcript. An early status: "started" acknowledgment, a staged initial message, or durable pending-input custody is not that commit. A queued or remotely placed first message therefore does not notify while it is still waiting to be recorded. A later agent failure does not undo a mention whose human message was already committed.

Transport retries preserve the exact submitted text, mention annotations, and original send identity, even when execution gets a different run id. Replaying that committed input does not create another mention or restore a dismissed entry. An intentional new send has a new identity and may notify again, even when its text is identical. Transcript loading, hidden continuations, and attachment enrichment are not new human sends.

The supporting RPCs require operator.read and use the authenticated profile, never a caller-selected Inbox owner:

Method or event Contract
users.mentionable Search with { sessionKey, agentId?, query? }, or use { agentId, visibility?, query? } before creating a session. Returns bounded { users, truncated } results with profile id, display name, optional avatar URL, and online status.
mentions.list Call with {} to fetch your current { gatewayInstanceId, revision, items } snapshot.
mentions.dismiss Pass { ids } containing up to 100 distinct visible entry ids. Returns the same snapshot shape after dismissal.
mentions.changed Targeted { gatewayInstanceId, revision } invalidation. Fetch mentions.list again. Revisions describe that connection's authorized view, not global Gateway activity.

Delivery is best-effort. The Inbox and replay bookkeeping survive Gateway restarts within their retention limits. Capacity limits can still skip alerts. Restarting or reconnecting does not resend old browser notifications or restore dismissed entries. Notification failures do not retry or undo the posted chat message. Browser push does not provide exactly-once delivery. See Inbox retention and notification preferences.

Control UI agents tools panel

Remote use

Configuration reference (WebChat)

Full configuration: Configuration

WebChat has no persisted config section. Gateway uses the built-in chat.history display limit. API clients can send per-request maxChars to override it for a single call. Clients that can expand tool output with chat.message.get may also send toolResultMaxChars to request shorter tool-result text previews without reducing user or assistant text. Complete JSON objects and arrays that fit the existing display limit retain that limit because clients use them for source cards and embedded views. This optional limit applies to chat.history and chat.startup, including history pages and delta catch-up; omit it to retain the default behavior. Repeat it on each page request. Legacy channels.webchat and gateway.webchat config is retired. Run apus doctor --fix to remove it.

Related global options:

Related