Skip to navigation

Operations

Your handle’s Allowed websites list controls custom apps. In your dashboard, go to Channels > Messaging. Before your transition, use Channels > Chat. Add your app’s origin. Until the list allows your origin, a configured appUrl is dropped with a console warning and Ada’s default app mounts. Local development needs no entry when both your app and the embedding page run on loopback hosts. See Local development.

client.operations provides typed helpers over the raw bridge event contract. Each helper carries the guards, debounces, and correlation logic that Ada’s own conversation app uses. Prefer these helpers over hand-built sendEvent calls. The raw events remain available as an escape hatch. See State and events for the underlying contract.

const handle = client.operations.sendMessage("Hello");
const message = await handle.settled({ timeoutMs: 10_000 });
// settled() resolves with the optimistic row, which carries no cursor
// yet. Report reads from a chat.messages subscription instead, over
// the rows that carry one:
client.subscribeKey("chat.messages", (messages) => {
for (const row of messages ?? []) {
if (row.cursor) client.operations.markRead(row.cursor);
}
});

How correlation works

The bridge has no request/response channel. The core frame answers every action through display-state transitions. The operations layer hides this:

  • Fire-and-forget helpers return void. Core reports any outcome through state, or not at all.
  • Correlated helpers return a handle or a Promise. A handle’s settled(opts?) method returns a Promise that settles from the state transition that answers the operation.
  • settled({ timeoutMs }) rejects with an Error when the answer does not arrive in time. Without timeoutMs, an unanswered operation stays pending until you destroy the client. client.destroy() then rejects every pending settled() promise with a BridgeClientDestroyedError. Detect that rejection by error.name === "BridgeClientDestroyedError".
  • Core publishes a failure signal when it drops a correlated action instead of running it. This covers a host-driven reset in progress, a startup that cannot safely accept the action yet, a disabled transcript feature, and invalid submission arguments. Each affected handle settles with that failure rather than hanging. A partial CSAT submit is the one exception: it reports no terminal outcome by contract, so its drop is silent too. On an older core document without these signals, pass timeoutMs.

Each helper below states whether it is fire-and-forget or correlated, and which state transition settles it.

A cached copy of the bridge runtime can predate the operations layer. Feature-detect before use: check client.operations before calling it, and check typeof bridge.messageKey === "function" before using a derivation helper.

Conversation

addReaction

addReaction(messageId: string, reaction: 1 | -1): void;

Reacts to an AI Agent message with a thumbs vote. Fire-and-forget.

cancelCapture

cancelCapture(label?: string): void;

Cancels the active capture field. Uses the block’s own cancel response and label; label overrides the visible label. No-op when no capture is active or the block has no cancel response. Fire-and-forget.

cancelListSelection

cancelListSelection(label?: string): void;

Cancels the active selectable list. Uses the block’s own cancel response and label; label overrides the visible label. No-op when no list selection is active or the block has no cancel response. Fire-and-forget.

canRetryMessage

canRetryMessage(message: Message): boolean;

Returns whether the retry affordance should show for a message. True when the message is a text message that core marked isRetryable, no disconnected Zendesk SDK blocks it, and the message belongs to the current conversation or predates conversation stamping. Pure read; sends nothing.

reportLinkClick

reportLinkClick(messageId: string, opts?: { navigateInHost?: boolean }): void;

Reports that the user activated a link message. Core resolves the stored message for tracking. With navigateInHost, core also performs validated same-tab navigation on the host page. Fire-and-forget.

retryMessage

retryMessage(messageId: string): void;

Retries a failed send by message id. Gate the affordance on canRetryMessage. Fire-and-forget.

selectOption

selectOption(option: OptionItem, messageId: string): void;

Answers an options block. The label falls back to the option id when the option has no label. Fire-and-forget.

selectQuickReply

selectQuickReply(reply: QuickReply, messageId: string): void;

Answers a quick-reply chip. Pass the chip object and its message id verbatim from state. Fire-and-forget.

Before you enable any interactive row (quick replies, options, retry), consult isHistoricalRow and chat.answeredInteractiveIds. Answering a row from a previous conversation sends its target into the current one.

The chat.answeredInteractiveIds set can shrink when core withdraws a failed programmatic answer. Read the set on every update. Do not retain an answered state after the set removes its ID.

sendMessage

sendMessage(body: string, opts?: { secret?: boolean }): SendHandle;

Sends a chat message. Correlated: the returned SendHandle carries the minted tempMessageUuid, and settled() resolves with the row in chat.messages whose clientKey equals it. The optimistic row appears immediately, and the durable server echo keeps the same clientKey across the id swap.

settled() resolves with the first matching row, which is the optimistic one, so the resolved row has no cursor yet. The cursor arrives on the durable echo. Drive markRead from your chat.messages subscription, never from the settled row: markRead(message.cursor ?? "") on a settled row passes an empty cursor, which the helper drops silently.

  • The body is trimmed. An all-whitespace body sends nothing, and settled() rejects.
  • settled() rejects when core reports this send’s error before the row appears (rate limit, over-length body, session not ready, dispatch failure). Core stamps the failing send’s key on chat.error.clientKey, so the handle rejects only on its own failure. An unrelated error in the same window never rejects a delivered send. The rejection carries the chat.error text, and a repeat of the identical error still rejects.
  • secret: true sends a masked message. The raw body never enters the transcript, so no correlatable row exists and settled() never resolves. Pass timeoutMs or skip settled() for secret sends. While ui.secretMessage.isOpen is true, send with secret: true to match the composer’s mode.

submitCapture

submitCapture(value: string): CaptureHandle;

Submits the active capture field. The dataId comes from conversation.stateData.capture. Each call mints its own submitId and sends it with the event. Correlated: settled() resolves with the verdict for this submit only. An accepted value removes the capture block and resolves { ok: true }. A server validation rejection resolves { ok: false, message } with the validation error, observed on the isSubmitting falling edge. When no capture field is active, settled() rejects immediately.

Core stamps every other failure on the capture’s lastSubmitFailure, keyed with the submit’s own submitId: no session to round-trip, a value over the 500-character cap, a transport failure, a duplicate submit while one is in flight, or a host-driven reset. The handle resolves { ok: false, message } from that stamp. The handle never reads the global chat.error, so an unrelated failure in the same window cannot masquerade as this capture’s verdict. A second submitCapture sent while one is in flight settles on its own drop; it does not inherit the first submit’s verdict. On a core document that predates lastSubmitFailure, these failures are not capture-attributable, so pass timeoutMs.

submitListSelection

submitListSelection(selectedIds: string[]): void;

Submits the active selectable list. The dataId comes from conversation.stateData.listSelection. No-op when no list selection is active. Fire-and-forget.

Read and unread

markRead

markRead(cursor: string, opts?: { conversationId?: string }): void;

Advances the persisted read watermark. Call it with message.cursor for each message that scrolls into view. Do not reimplement read tracking; this helper carries the contract the reference app validates:

  • Reports are debounced 400 ms, so one scroll gesture produces one report.
  • The watermark is a monotonic lexical maximum over cursor strings. Calling with an older cursor never lowers it.
  • The accumulator and any pending report reset when chat.conversationId changes. A cursor collected in conversation A can never commit against conversation B.

One rule stays with the caller: the transcript preserves the previous conversation’s rows, so exclude messages stamped with a different conversationId before reporting their cursors. See the worked example.

Fire-and-forget. The committed watermark comes back on chat.lastReadCursor.

History

loadOlderMessages

loadOlderMessages(): void;

Requests an older page of history. No-op unless chat.canLoadMore is true and no load is already in flight. Fire-and-forget; the page arrives in chat.messages.

The helper sends chat.logs.loadMore. After a failed page, Core ignores requests during a delay for that conversation without setting chat.loadingPreviousMessages. The delay starts at 1 second and doubles after each failure, up to 30 seconds.

Retry on a new end-user scroll, not when the loading flag clears. The optional chat.previousMessagesLoadSeq counter counts successful older-history pages, including pages with no visible rows. When the counter rises, you can request the next page if chat.canLoadMore is true.

onHistoryLoaded

onHistoryLoaded(callback: (seq: number) => void): () => void;

Invokes callback each time restored or baseline history loads or reloads, when chat.recentMessagesLoadSeq advances. This is the one-shot trigger for “scroll to the bottom of restored history” and for seeding an unread divider. The subscription re-arms silently when the sequence resets to 0 on a chat clear. Returns an unsubscribe function.

Surveys and End Chat

checkEndChatEligibility

checkEndChatEligibility(opts?: SettleOptions): Promise<EndChatEligibilityResult>;

Asks core whether End Chat should present a survey. Correlated: the helper latches csat.endChatEligibility.seq and resolves when it advances, because the contract has no request id. Resolves with { eligible, surveyTarget }. A check core drops (a host-driven reset in progress, or a startup that cannot accept it) resolves as ineligible. Pass timeoutMs to bound the wait.

endLiveChat

endLiveChat(): void;

Ends an active or pending human-agent handoff while keeping the conversation open. Fire-and-forget. For integrations configured to return to the AI Agent, omit the End Chat survey. Render any supplied post-agent survey. Core ignores the call when no handoff is active or pending.

resetApp

resetApp(): void;

Clears the transcript and starts a new conversation. Fire-and-forget.

If proactive conversation requests are in progress, both restart operations wait for them. If any request succeeds, Core satisfies the restart through that proactive conversation. Its messages and the existing transcript remain, and Core sends no extra greeting.

One restart can also use a completed success before its conversation appears. After that restart finishes, another explicit restart is a new request. This lets an end user retry when no conversation arrives. With no pending request or unused success awaiting conversation delivery, each operation starts its normal reset immediately.

Core discards a restart when a newer conversation, session, or accepted user send replaces its original context. If all requests fail and that context remains current, Core runs the latest accepted restart once. resetApp() clears the transcript, while startNewConversation() preserves it. An accepted user send or retry cancels a waiting restart before it can erase the newer message. A rejected send leaves the restart intact.

While app.conversationStartPending is true, render both restart controls as busy. Keep the transcript visible. Core clears the state when it finishes or discards the request. Treat an absent key on an older Core as false. resetApp() remains fire-and-forget and still sends if called while pending.

skipCsatAndEndChat

skipCsatAndEndChat(): void;

Ends the chat without presenting a survey. Fire-and-forget.

For integrations configured to return to the AI Agent after live chat ends, wait until liveAgent.liveChatEndedInConversation becomes true. Then call skipCsatAndEndChat() directly from the next End Chat control. Skip confirmation, eligibility checks, and the End Chat survey only in this case. Otherwise, use the normal confirmation and survey eligibility checks.

startNewConversation

startNewConversation(opts?: { cooldownMs?: number }): boolean;

Starts a new conversation while preserving prior messages. Applies a local cooldown (default 5 seconds) that swallows rapid re-clicks. Returns false while cooling down or while app.conversationStartPending is true, without sending anything. A pending refusal does not extend the cooldown. A true result means the request was sent, not that the conversation has started.

Proactive conversation requests follow the shared restart behavior described under resetApp.

submitCsat

submitCsat(data: CsatSubmitData, opts: { conversationId?: string | null; endConversation?: "inline" | "endChat"; partial?: boolean; submit?: boolean; surveyType: string; uiComplete?: boolean }): CsatSubmitHandle;

Submits a CSAT survey. settled() resolves { ok: true } when csat.submitSuccess.seq advances with a matching surveyType and conversation. It resolves { ok: false, message } when the matching csat.submitError.seq advances with non-null error text, including repeated errors with identical text. Core reports invalid scores and missing conversations when uiComplete is true. Router gates report dropped submits when uiComplete is true and partial is false. The helper matches conversationId only when you pass one.

The helper accepts these options:

  • endConversation defaults to "endChat" for surveyType: "end_chat"; otherwise, the helper omits it. The value reports which survey surface completed. After a successful write with uiComplete: true, core ends the current surveyed conversation only for answer_flow and end_chat. "endChat" uses End Chat behavior. "inline" omits ada:close_chat.

    Other survey types, including live_chat, keep the conversation open. Neither value ends a newer conversation that replaced the surveyed conversation.

  • submit defaults to !partial || surveyType === "end_chat", except that a proactive partial write omits it. Explicit values override this default. A successful non-partial POST with submit: true publishes ada:csat_submitted. Partial POSTs emit no event, including end_chat writes. This flag does not complete the survey UI or end the conversation by itself.

  • uiComplete defaults to !partial. When true, core marks a successful write submitted and advances csat.submitSuccess.seq. When false, the write reports no UI completion and does not end the conversation. A terminal failure requires uiComplete: true without inlineAnswerId. Only terminal failures set csat.submitError, advance its sequence, and set both scope keys. Only a valid terminal write clears the error and both scope keys.

    Silent failures preserve all four values. settled() resolves a failure only when the matching terminal error sequence advances.

When uiComplete is true, core cancels queued autosaves for the same conversation and survey type. All writes for one conversation share a queue, across survey types and conversation generations. Each survey type keeps its own accumulated answers. An in-flight request completes or reaches its own timeout before the next write starts. A generation change discards only unsent autosaves. Canceled autosaves emit no host event or completion result.

Each autosave POST has its own 20-second timeout from request start. A terminal Submit with uiComplete: true has one 20-second deadline from receipt of csat.submit in core. This deadline includes queue wait and request time. If the deadline expires in the queue, core reports the timeout and never posts that terminal write.

A queued terminal write retains its captured conversation after a conversation change. It still posts unless core is destroyed or its deadline expires. Completion results identify the submitted survey and conversation, including after a conversation change. Core changes global survey flags or ends the conversation only if the conversation generation remains current.

If a raw sendEvent("csat.submit", ...) omits both submit and uiComplete, core applies the same defaults as the helper. This includes the proactive partial exception and endConversation: "endChat" for end_chat. An explicit endConversation overrides that default.

To match legacy inline and post-agent counting, send every first answer with submit: true and omit partial. Use uiComplete: false for post-agent first answers and inline surveys with follow-ups. For an inline survey without follow-ups, use uiComplete: true without endConversation. Follow-ups exist when feedback chips, resolution, open feedback, CES, or NPS is enabled, regardless of question order. This includes surveys with satisfaction disabled and only one enabled question. For inline first answers, use raw sendEvent("csat.submit", ...) with a unique inlineAnswerId. Each successful write counts, emits ada:csat_submitted, and keeps the conversation open.

  1. Send scores immediately. Open the full form when follow-ups are enabled.
  2. Debounce only the saves for comments, chips, resolution, CES, and NPS for 1000 ms.
  3. Open End Chat and post-agent surveys directly in the full form when open feedback is first.
  4. On Close or Dismiss, send a pending first answer immediately. On unmount or a fatal handoff, discard it.
  5. Match the first answer’s inlineAnswerId against the csat.inlineSubmitErrorIds list. It holds the 200 most recent failed IDs, independent of conversation and survey type. Only that failure resets the inline answer and pending full-survey cache. Show no error text, so the end user can tap again to retry.

A change to a pending first answer replaces its delayed write. Do not also send a follow-up save for that change. Before a follow-up save, disable first-answer rollback. A failed follow-up save keeps the modal and all answers intact.

For End Chat and follow-up forms, save selections only when no enabled question is required. Send scores with submit: true, uiComplete: false and omit partial. For resolution, CES, and NPS, use raw sendEvent("csat.submit", ...) with uiComplete: false and omit both flags. Keep comments and chips local until Submit. For the End Chat Submit button, send submit: true, uiComplete: true, endConversation: "endChat". This write also counts and ends the current surveyed conversation after success.

Pass the right conversation scope:

  • For a survey rendered in the transcript, pass conversationId: message.conversationId. A transcript row can outlive the conversation it rates, and matching on survey type alone would let a late answer from a dead conversation settle the wrong survey.
  • For End Chat and proactive flows, omit conversationId. Core resolves the conversation itself. Passing null is the same as omitting it, on the wire and in correlation.

partial defaults to false. Setting it to true sends the legacy partial flag and defaults uiComplete to false. For end_chat, submit still defaults to true. For proactive surveys, the helper omits submit and core omits partial from the POST. For other survey types, submit defaults to false. Explicit submit and uiComplete values override those defaults.

With uiComplete: false, settled() receives no completion result from that write. Use timeoutMs to bound the wait. Do not use partial: true for a first answer that must count as a completed response.

trackCsatShown

trackCsatShown(surveyType: string, conversationId?: string | null): void;

Reports that a survey was displayed, for analytics. Fire-and-forget.

Composer

notifyComposerChanged

notifyComposerChanged(opts?: { secret?: boolean }): void;

Reports a content-free composer-changed edge, which drives live-agent typing indicators. The event carries no payload by contract: composer text never crosses the frame boundary. The helper suppresses the event entirely in secret mode, when you pass secret: true or when ui.secretMessage.isOpen is true. Even a content-free typing edge signals that a secret is being typed. Fire-and-forget.

toggleSecretMessage

toggleSecretMessage(open: boolean): void;

Opens or closes the masked (secret) composer mode. Clear your local composer text on every transition. Fire-and-forget.

Files

startFileUpload

startFileUpload(file: File): FileUploadHandle;

Starts a live-agent file upload. The helper mints an uploadId and retains the exact { file, uploadId } pair, so the handle’s retry() replays exactly it. A fresh pair would orphan the original error state in core.

Progress and errors land on file.isUploading, file.transfer, and file.uploadError. Watch both flags: core can reject a selection and report file.uploadError without ever setting file.isUploading to true. The handle’s dismissError() clears a reported error.

Settings

downloadTranscript

downloadTranscript(opts?: SettleOptions): Promise<void>;

Downloads the transcript. Correlated: resolves or rejects when transcript.download.status reaches success or error with transcript.download.status.seq advanced past the call’s baseline. A request core drops without ever broadcasting pending (a host-driven reset in progress, or an AI Agent without transcript downloads enabled) still rejects. Check config.downloadTranscriptEnabled before you show the affordance. On a core document without the seq key, the helper falls back to the pending to terminal edge. The status key is global, so treat the request as single-flight. A call made while one is in flight joins the in-flight request and settles on its verdict. Pass timeoutMs to bound the wait.

emailTranscript

emailTranscript(email: string, opts?: SettleOptions): Promise<void>;

Emails the transcript. Correlated on transcript.email.status and its seq, with the same settle contract as downloadTranscript. An AI Agent without email transcripts enabled rejects every request; check config.emailTranscriptEnabled before you show the affordance. The helper trims the email and sends the trimmed value. An empty result rejects immediately without sending, because core drops an empty email silently. The single-flight rule is address-aware. A call for the SAME address as the in-flight request joins it and settles on its verdict. A call for a different address cannot join. Core rejects only that call, matched on the address it sent. The in-flight call is not affected: its request completes, writes the final status, and settles its own handle with the true outcome. A conflict rejection never means the in-flight email was not sent. On an older core document without the conflict signal, the conflicting call settles on the in-flight verdict instead.

requestNotificationPermission

requestNotificationPermission(): void;

Asks the host page to request Web Notification permission. Permission is per-origin, so your frame’s own permission value is not the governing one. Fire-and-forget. The result arrives on this state key (the prefix is legacy naming):

client.subscribeKey("chatter.notificationPermission", (permission) => {
// "default" | "granted" | "denied"
});

setAlertSound

setAlertSound(enabled: boolean): void;

Toggles the new-message alert sound. Fire-and-forget.

setLanguage

setLanguage(language: string): void;

Changes the conversation language. Pass a BCP 47 tag the AI Agent offers. The offered set is config.clientDefaultLanguage plus config.translatedLanguages; neither list alone is complete. Fire-and-forget.

setTextSize

setTextSize(size: "small" | "default" | "large"): void;

Sets the user’s in-widget text-size override. The persisted value comes back on appearance.userTextSize. Fire-and-forget.

setTheme

setTheme(theme: "light" | "dark" | "auto"): void;

Sets the user’s in-widget theme override. The persisted value comes back on appearance.userTheme. Fire-and-forget.

Chrome

close

close(): void;

Activates the header close affordance. Ends the chat when endChat.canEnd is true. Fire-and-forget.

dismissError

dismissError(): void;

Clears chat.error in core. Always clear the error in core rather than hiding it locally: chat.error keeps its value when the session’s second identical error arrives (only chat.error.seq advances), so a locally hidden error would render nothing the second time. Fire-and-forget.

dismissToast

dismissToast(): void;

Dismisses the visible toast. Fire-and-forget.

minimize

minimize(): void;

Minimizes the widget. Fire-and-forget.

reportAppError

reportAppError(error: unknown, componentStack?: string): void;

Reports a fatal app error to core, which renders the fallback UI and records telemetry. Callable from anywhere with a client reference, including a class error boundary outside any React provider. Fire-and-forget.

retryAfterOutage

retryAfterOutage(): void;

Retries after a response outage banner. Fire-and-forget.

setFrameStyles

setFrameStyles(styles: CustomAppFrameStyles): void;

Sets the position, size, and appearance of your custom app’s outer frame. Returns no result. Frame styles apply to desktop floating layouts only; in other layouts the frames fill their region. Style your app’s contents with your own CSS. To cover the whole screen for a moment, for example for an image viewer, use setHostViewportExpanded. It locks page scrolling and restores your layout when it ends.

All properties are optional. Numbers use CSS pixels. Percentages use the host viewport.

PropertyAccepted values
backgroundColor#rgb, #rgba, #rrggbb, #rrggbbaa, or "transparent".
borderColor#rgb, #rgba, #rrggbb, #rrggbbaa, or "transparent". Requires borderWidth.
borderRadiusNumber from 0 to 2048.
borderWidthNumber from 0 to 32. The border is solid. Without borderColor, the border is transparent.
bottomNumber from -4096 to 4096, percentage from "-100%" to "100%", or "auto".
boxShadow"none", or one shadow with optional inset, two to four pixel lengths, and a hex color. Maximum 256 characters.
heightNumber from 0 to 4096 or percentage from "0%" to "100%".
leftNumber from -4096 to 4096, percentage from "-100%" to "100%", or "auto".
maxHeightNumber from 0 to 4096 or percentage from "0%" to "100%".
maxWidthNumber from 0 to 4096 or percentage from "0%" to "100%".
minHeightNumber from 0 to 4096 or percentage from "0%" to "100%".
minWidthNumber from 0 to 4096 or percentage from "0%" to "100%".
rightNumber from -4096 to 4096, percentage from "-100%" to "100%", or "auto".
topNumber from -4096 to 4096, percentage from "-100%" to "100%", or "auto".
widthNumber from 0 to 4096 or percentage from "0%" to "100%".

Numbers must be finite. Percentage strings accept decimal digits, an optional minus sign, and no spaces, such as "60%" or "12.5%". For shadows, each length must stay between -256 and 256 pixels. The third length, the blur radius, cannot be negative. Zero can omit px. Other shadow lengths require px. Unknown properties, invalid values, CSS functions, and multiple shadows reject the whole call. The previous styles stay.

Center a modal frame with a translucent background:

if (typeof client.operations?.setFrameStyles === "function") {
client.operations.setFrameStyles({
top: 0,
right: 0,
bottom: 0,
left: 0,
width: 640,
height: 480,
backgroundColor: "#00000066",
borderRadius: 16,
});
}

Use a wider drawer:

client.operations?.setFrameStyles?.({ width: 520 });

Each call replaces the entire request. Omitted properties return to defaults. Call client.operations.setFrameStyles({}) to restore all defaults. The default frame has 24-pixel bottom and side gaps, with limits of 375 pixels wide and 720 pixels high. See Frame layout for the default CSS dimensions. When you supply dimensions, limits, or insets, omitted width and height values use 375px and 720px. Set all four insets to 0 to center a frame with a fixed width and height. Dimensions include the border. While the open frame covers the full viewport, the SDK locks page scrolling. Smaller frames do not lock page scrolling.

Without a custom width, height, maxWidth, or maxHeight, the default maximum dimensions remain 48 pixels below the viewport dimensions. Setting any of those properties changes the default maximum dimensions to the full viewport. Your maxWidth and maxHeight values replace those limits. setHostViewportExpanded(true) takes precedence until you pass false.

Styles survive close and reopen, reset(), and deleteHistory(). If reset or history deletion remounts your app document, the styles stay. The new document can replace or clear them. A widget reload or unrecoverable error clears the request. Fallback to the default app, destroy(), and stop() also clear it.

Read appearance.hostViewport to choose sizes for different host viewports. See the transparency and security notes before using a large or transparent frame. Older hosts ignore the request. Check that the operation exists before calling it, as the first example shows.

setHostViewportExpanded

setHostViewportExpanded(expanded: boolean): void;

Makes the frame cover the full browser viewport. Use it for content that needs more room than the drawer, such as an image viewer. Pass false to return the frame to its configured size and position.

  • While the frame is expanded, the SDK locks page scrolling.
  • It works only in floating mode. In parentElement, headless and the mobile SDKs, the call does nothing.
  • It applies only while the drawer is open. The SDK remembers the request across close and reopen.
  • While it is active, it overrides setFrameStyles. Your latest frame styles apply when you pass false.
  • The SDK releases it on reset, history deletion, destroy, and timeout fallback.
  • The call returns nothing and confirms nothing.

To change the frame’s standing size or position, use setFrameStyles.

Observe state changes

Two primitives on the client complement the full subscribe(callback) subscription. Both return an unsubscribe function. client.destroy() releases every subscription and rejects every pending settled() promise with a BridgeClientDestroyedError.

subscribeKey(key, callback, opts?) fires only when one key’s value changes:

client.subscribeKey("chat.isGenerating", (generating, previous) => {
toggleTypingIndicator(generating === true);
});

select(selector, callback, opts?) does the same for a derived projection:

client.select(
(state) => state?.["chat.messages"]?.length ?? 0,
(count) => updateCounter(count),
);

Options for both:

OptionDefaultPurpose
emitInitialfalseAlso invoke the callback immediately with (currentValue, undefined).
equalsObject.isChange detector that decides whether the callback fires.

Object.is is a sound default for subscribeKey. State crosses the frame boundary by structured clone, and the client restores each structurally unchanged key’s previous reference before it notifies. An object key’s identity therefore changes exactly when its content does. For chat.messages, that is every streaming delta. For select, a selector that builds a new object per call defeats Object.is: pass a custom equals, or select a primitive signature (for example, a joined string of ids). See State and events.

In React, useBridgeStateKey(key) delegates to subscribeKey and re-renders only when its key’s value changes.

Derivation helpers

The loaded module exports pure helpers ported from Ada’s reference app. They take state or messages and return values. No client is needed:

const { filterDisplayable, groupMessages, messageKey } = bridge;

The derivation helpers are runtime-only. The npm package declares their types but ships no implementation, and createMockBridgeClient does not include them. In unit tests, inject the helpers into the code under test as plain function parameters or props. To exercise the real implementations, load the module with loadMessagingBridge() in an integration test. Do not re-implement them: a copy drifts from the runtime’s edge cases.

HelperReturns
filterDisplayable(messages, isConversationActive)The messages a transcript should render, in order. Drops non-rendering presence rows, unrenderable videos, superseded CSAT rows, and stale sign-in cancel chips. Pass chat.isConversationActive, defaulted to false. Apply your own chat.answeredInteractiveIds filtering afterwards. Then pass the result to selectUnread as its messages option.
findFirstUnread(messages, lastReadCursor)The first unread AI Agent or agent message past the read watermark, or null. Skips the user’s own messages and presence markers. Also returns null when the watermark is empty, which is the state of a user who has never had a read reported.
groupMessages(messages)Per-index { showDivider, isGroupedWithPrev } decisions: date dividers and sender-run grouping, with the exact rules the reference app renders with.
isAgentTyping(state)Whether someone is composing a reply: core’s typing verdict or the active agent’s own flag.
isConnectivityLost(state)Whether connectivity is lost, per outage.connectivityLost. This is the sanctioned signal; see the connectivity note.
isHistoricalRow(messageConversationId, state)Whether a row belongs to a conversation the end user has left. Consult it before enabling any interactive row. Unstamped rows count as current; stamped rows with no active conversation count as historical.
messageKey(message)The stable render key: streamId ?? clientKey ?? id.
resolveBotName(state)The name to attribute a message to: config.botName, else config.handle, else "Ada". Use it for sender labels and for screen reader announcements. Do not use it to name the AI Agent in your own UI. A handle is an internal identifier. Use resolveDisplayBotName for that.
resolveDisplayBotName(state)The name to show as the AI Agent’s own identity: config.botName, else empty. Use it for a header title, a greeting heading, or any place your UI names the AI Agent. It returns an empty string when the customer configured no name. Ada’s own widget shows no name in that case. It never falls back to the handle. This helper is absent on core builds published before it existed, so guard the call: bridge.resolveDisplayBotName?.(state) ?? "". Your npm version does not choose the runtime build.
selectUnread(state, opts?){ firstUnreadId, count } derived from the transcript and chat.lastReadCursor. Pass your filtered transcript as messages so the boundary anchors on a row you render and the count matches visible rows. The default is the raw chat.messages, which can anchor on a row filterDisplayable drops. Rows that render nothing never count. Pass boundaryId (a messageKey) to keep a shown divider anchored while newer messages arrive. An empty watermark yields count: 0, so seed first-contact users yourself; see the worked example.

Use messageKey as your render key and as your unread-boundary anchor. A raw message id changes twice: a streaming bubble swaps to its final id, and an optimistic send swaps to its durable id. Keying on the raw id replays animations, double-fires arrival effects, and breaks unread anchors.

Worked example: an unread badge

A headless integration that shows an unread count (a “red dot”) and clears it when the user opens the panel.

One trap sits in the first-contact case, which is exactly when a proactive badge matters most. The watermark starts empty for a user who has never had a read reported, and selectUnread yields count: 0 for an empty watermark. Without seeding, a proactive or agent message that arrives before the user ever opens the panel shows no badge, silently, until the first markRead commits. Seed it yourself: while chat.lastReadCursor is empty, count every non-user row as unread.

const bridge = await loadMessagingBridge();
const client = bridge.createBridgeClient();
client.sendEvent("app.initialize");
// 1. Keep the badge in sync. selectUnread returns a fresh object,
// so select the primitive count. While the watermark is empty
// (no read ever reported), count every non-user row as unread;
// selectUnread reports 0 for an empty watermark.
client.select(
(state) => {
if (!state?.["chat.lastReadCursor"]) {
return (state?.["chat.messages"] ?? []).filter(
(m) => m.sender !== "user" && m.type !== "presence",
).length;
}
return bridge.selectUnread(state).count;
},
(count) => {
badge.hidden = count === 0;
badge.textContent = String(count);
},
{ emitInitial: true },
);
// 2. While the panel is open, advance the read watermark.
// Exclude rows stamped with a different conversation: the
// transcript preserves them, but their cursors are foreign.
client.subscribeKey("chat.messages", (messages) => {
if (!panelIsOpen) return;
const current = client.getState()?.["chat.conversationId"] ?? null;
for (const message of messages ?? []) {
if (message.conversationId != null && message.conversationId !== current) continue;
if (message.cursor) client.operations.markRead(message.cursor);
}
});

The badge count falls to zero on its own: markRead commits the debounced watermark, core advances chat.lastReadCursor, and the select subscription re-derives the count. To also render an unread divider, pass your filtered transcript as messages, anchor the divider on selectUnread(state, { messages }).firstUnreadId, and keep it stable with the boundaryId option.

Types

Handle and option types ship in the npm package. Import them from @ada-cx/messaging-bridge.

BridgeClientDestroyedError

The rejection every pending settled() promise receives when client.destroy() runs. The npm package exports the type only. Detect it by error.name === "BridgeClientDestroyedError", never by instanceof: the class value lives in the CDN runtime.

CaptureHandle

{ settled(opts?: SettleOptions): Promise<OperationResult> }

Returned by submitCapture.

CsatSubmitHandle

{ settled(opts?: SettleOptions): Promise<OperationResult> }

Returned by submitCsat.

CustomAppFrameStyles

All properties are optional. Each setFrameStyles call replaces the previous request. See the operation’s value table for the accepted properties and limits.

EndChatEligibilityResult

{ eligible: boolean | null; surveyTarget: "bot" | "agent" | null }

Resolved by checkEndChatEligibility. surveyTarget names which survey End Chat should present.

FileUploadHandle

{ uploadId: string; retry(): void; dismissError(): void }

Returned by startFileUpload.

ObserveOptions

{ equals?: (a: T, b: T) => boolean; emitInitial?: boolean }

Options for subscribeKey and select.

OperationResult

{ ok: true } | { ok: false; message: string }

The outcome of a correlated submit.

SendHandle

{ tempMessageUuid: string; settled(opts?: SettleOptions): Promise<Message> }

Returned by sendMessage. tempMessageUuid is the uuid put on the wire; the sent message is findable in chat.messages by clientKey === tempMessageUuid.

SettleOptions

{ timeoutMs?: number }

Accepted by every settled() method, by checkEndChatEligibility, and by the transcript helpers. When set, the promise rejects with an Error after timeoutMs milliseconds without an answer.