Skip to content
GitHub

Web SDK response components

How the Runbear chat widget renders the confirm, select and card components an agent attaches to its reply, what a click sends, and how components reach you on the API.


On this page

Alongside its prose, an agent can attach a response component: a small block the widget renders directly under the reply. v1 ships three: confirm, a short yes/no style question, select, a list of choices, and card, a formatted block of details.

The first two are offers the visitor answers. A card has nothing to activate: the agent shows it without pausing for a reply, so a card can arrive while the rest of the answer is still being written.

This page is the rendering side. Whether and how often an agent produces components is a per-agent setting: the Component frequency select under Agent settings → Choice buttons, or the same field on the Response Components API. Turning choice buttons off withholds every component, the card included, and scheduled or automated runs never offer one.

What you need#

  • Response components enabled for your organization (see the callout above).
  • An agent whose Component frequency is set above off.
  • An SDK release that carries the renderers: @runbear-io/react 0.3.0 or newer for confirm and select, and 0.4.0 or newer for card.

Under the floor for a given component the visitor reads it as prose instead of seeing it rendered: 0.2.x and earlier don't read components at all and show only the prose the message text carries, while 0.3.x renders a card's fallbackText in place of the formatted block but handles confirm and select normally. That is the intended behavior, not a defect.

A click is a message#

This section is about confirm and select; a card carries no options and never sends anything.

Activating an option sends that option's value into the conversation exactly as if the visitor had typed it. There is no separate callback and no component-level handler, and no answer state for you to read or reconcile: the transcript is the record. Three things worth planning for:

  • A click costs a turn. In session mode a pass carries a lifetime turn cap, 100 by default, which never resets for the life of that session. A click spends one exactly as a typed message would: the click is the visitor's next message, not an extra one on top of it, so buttons don't consume the cap any faster than typing does.
  • The conversation never pauses. A component is an offer, not a modal: nothing waits on a click, and answering in your own words instead is always valid. The message box behaves exactly as it does on any other turn.
  • One activation per open question. The widget drops a second click on a component that has already been used, so a double-click can't bill two turns. Each activation that does go through carries its own id, so two genuine answers aren't collapsed into one.

The visitor can see what a click will send. When an option's value differs from its label, the widget lists each such option under the buttons as a "Label → value" line, so a button reading "Approve" that sends "Approve the request" says so before it is clicked.

A turn that renders a component shows no suggestion chips, even with config.suggestions enabled: the chips would repeat the component's own options, and clicking one would send a message that makes the real component inert.

A component goes inert once it is answered#

Interactive components only; a card has no controls and no answered state.

A component stays interactive while no visitor message appears after the message that carried it and the widget is idle. Once a later visitor message exists, whether it came from a click, from typing, or from your own addUserMessage call, the controls stop sending.

Inert controls stay on screen, readable and focusable. They are the record of what was offered, so a scrolled-back conversation still reads correctly. A group the visitor clicked names the option they picked; one that went inert some other way says why (answered, or a reply still arriving). All three strings are overridable. To style the two states, see Theming.

An unrecognized component renders prose#

Every component carries a fallbackText string. A client that reads components but can't render this one shows that text instead of the controls: a name its renderer table predates, a known name whose props fail that renderer's shape check, or a renderer that threw, which also logs console.warn("Runbear: a message component failed to render"). On a reloaded transcript, a known name with unreadable props is dropped instead, leaving the message's own text to carry it. This is the forward-compatibility contract: a name added after your last upgrade reads as prose rather than breaking the widget.

A client that doesn't read components at all, such as an SDK from before this feature or your own code against the REST API, is covered by a different mechanism: a prose rendering of the component (a card's fallbackText, or a prompt's choices) is appended to the end of the assistant message's own content. It isn't byte-identical to what the component itself carries: markdown metacharacters are escaped on the way in, and a prompt's block is built from its captions. Each component appends its own paragraph, so a message carrying four cards ends with four of them. A client that does render the components should suppress those trailing blocks by position, one per component it rendered, rather than by matching their text.

Surviving a tab close#

Components are part of the conversation, so they come back with it. Whether the conversation itself comes back is decided by auth.storage, which by default discards it when the tab closes. See Where the session is stored. Components vanishing when the tab closes is that default working, not a defect.

Customizing the rendered strings#

Every user-visible string the built-in confirm and select components render can be replaced; a card shows only the agent's own text. Each field is optional and is defaulted where it is shown, so a partial override replaces only what it names.

const client = new Runbear({
  assistantId: "…",
  config: {
    components: {
      labels: {
        untitledGroup: "Choose an option",
        optionCount: "{count} options available",
        answered: "Answered.",
        chosen: "You chose: {label}",
        streaming: "Waiting for the assistant…",
      },
    },
  },
})
LabelShown whenPlaceholder
untitledGroupThe accessible name for a group whose reply carried no text to borrow—
optionCountAnnounced once per group by a screen reader{count}
answeredThe group is inert because a later message answered it and the visitor didn't click it on this page: a reloaded transcript, or a second group in the same turn—
chosenThe visitor clicked one of this group's options on this page. Shown from the click onward and outranks answered, so write it as a record ("You chose: X"), not as a transient "Sending…"{label}
streamingThe group is inert because the assistant is still replying—

The values shown in the example are the defaults.

Components on the API#

If you read the API directly rather than through the SDK, components reach you on two carriers, both carrying the same object.

On the thread listing. GET /v1/threads/{threadId}/messages adds an optional components array to each message, in the order the components were produced, omitted entirely when the message carried none. At most 4 per message.

On the stream. Both streaming routes, POST /v1/threads/{threadId}/runs/stream and the streaming form of POST /v1/chat/completions, publish a thread.message.component event (see Streaming). It carries no message id, and its position relative to thread.message.completed is not guaranteed: a confirm or select follows the completed reply it decorates, while a card can be published mid-turn, before it. Attach the component to the assistant turn you are currently rendering rather than to the last message you saw complete.

{"event":"thread.message.component","data":{"componentId":"9f2b…","name":"confirm","props":{"options":[{"label":"Approve","value":"Approve the request","style":"primary"},{"label":"Reject","value":"Reject the request","style":"danger"}]},"fallbackText":"You can reply with:\n• Approve\n• Reject"}}
FieldTypeNotes
componentIdstringOpaque key for this instance, echoed back for analytics only. It is not an authorization input, and the server neither resolves it nor trusts it.
namestringThe render key. An open vocabulary, see below.
propsobjectPayload whose shape depends on name, passed through verbatim. Built-in shapes: confirm and select carry an options array of { label, value, style? }, with the question itself in the message's content; an option's style is default (outline), primary or danger (destructive), and any other token renders as default; card carries a title, an optional subtitle, and an optional fields array of { label, value }. The SDK shows at most 10 fields and cuts a title past 200 characters, a subtitle past 500, a field label past 75 and a field value past 300 with "…"; fields beyond the tenth collapse into one "…and N more" line. A card without a title shows its fallbackText. A client that does not recognise name must not read props.
fallbackTextstringProse to show whenever no renderer matches name. Never empty.

The SDK exports ChatComponent for this object, and ConfirmProps and SelectProps for narrowing props once you have checked name. CardProps is exported from 0.5.0 (not yet released). See Types.

Server limits#

Runbear holds every component to these bounds before it is published:

WhatLimit
Components per messageAt most 4
confirm and select options1 to 10 per component
Option labelAt most 75 characters
Option valueAt most 200 characters
fallbackTextAt most 2,000 characters
nameLowercase letters, digits and hyphens, starting with a letter (^[a-z][a-z0-9-]*$), at most 64 characters. constructor is reserved and rejected
componentIdAt most 64 characters
propsAt most 32,768 characters of JSON

Posting a click yourself#

To post a click from your own client, send the option's value as an ordinary message. Give the message a distinct id if you send two identical values in one conversation, or the second collapses into the first and the agent answers the first choice. A message id is 1–200 characters. Don't start one with rbc:: that prefix is reserved for the SDK's own click ids, whose shape is rbc:<uuid>:<uuid>, and anything else behind it rejects the whole run with a 400.