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.
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’ssettled(opts?)method returns aPromisethat settles from the state transition that answers the operation. settled({ timeoutMs })rejects with anErrorwhen the answer does not arrive in time. WithouttimeoutMs, an unanswered operation stays pending until you destroy the client.client.destroy()then rejects every pendingsettled()promise with aBridgeClientDestroyedError. Detect that rejection byerror.name === "BridgeClientDestroyedError".- Core publishes a failure signal when it drops a correlated action instead of running it. This covers a host-driven
resetin 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. ApartialCSAT 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, passtimeoutMs.
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 onchat.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 thechat.errortext, and a repeat of the identical error still rejects.secret: truesends a masked message. The raw body never enters the transcript, so no correlatable row exists andsettled()never resolves. PasstimeoutMsor skipsettled()for secret sends. Whileui.secretMessage.isOpenis true, send withsecret: trueto 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.conversationIdchanges. 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:
-
endConversationdefaults to"endChat"forsurveyType: "end_chat"; otherwise, the helper omits it. The value reports which survey surface completed. After a successful write withuiComplete: true, core ends the current surveyed conversation only foranswer_flowandend_chat."endChat"uses End Chat behavior."inline"omitsada:close_chat.Other survey types, including
live_chat, keep the conversation open. Neither value ends a newer conversation that replaced the surveyed conversation. -
submitdefaults to!partial || surveyType === "end_chat", except that a proactive partial write omits it. Explicit values override this default. A successful non-partial POST withsubmit: truepublishesada:csat_submitted. Partial POSTs emit no event, includingend_chatwrites. This flag does not complete the survey UI or end the conversation by itself. -
uiCompletedefaults to!partial. When true, core marks a successful write submitted and advancescsat.submitSuccess.seq. When false, the write reports no UI completion and does not end the conversation. A terminal failure requiresuiComplete: truewithoutinlineAnswerId. Only terminal failures setcsat.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.
- Send scores immediately. Open the full form when follow-ups are enabled.
- Debounce only the saves for comments, chips, resolution, CES, and NPS for 1000 ms.
- Open End Chat and post-agent surveys directly in the full form when open feedback is first.
- On Close or Dismiss, send a pending first answer immediately. On unmount or a fatal handoff, discard it.
- Match the first answer’s
inlineAnswerIdagainst thecsat.inlineSubmitErrorIdslist. 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. Passingnullis 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):
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.
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:
Use a wider drawer:
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 passfalse. - 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:
select(selector, callback, opts?) does the same for a derived projection:
Options for both:
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:
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.
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.
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
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.