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/react0.3.0 or newer forconfirmandselect, and 0.4.0 or newer forcard.
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…",
},
},
},
})| Label | Shown when | Placeholder |
|---|---|---|
untitledGroup | The accessible name for a group whose reply carried no text to borrow | — |
optionCount | Announced once per group by a screen reader | {count} |
answered | The 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 | — |
chosen | The 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} |
streaming | The 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"}}| Field | Type | Notes |
|---|---|---|
componentId | string | Opaque key for this instance, echoed back for analytics only. It is not an authorization input, and the server neither resolves it nor trusts it. |
name | string | The render key. An open vocabulary, see below. |
props | object | Payload 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. |
fallbackText | string | Prose 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:
| What | Limit |
|---|---|
| Components per message | At most 4 |
confirm and select options | 1 to 10 per component |
Option label | At most 75 characters |
Option value | At most 200 characters |
fallbackText | At most 2,000 characters |
name | Lowercase letters, digits and hyphens, starting with a letter (^[a-z][a-z0-9-]*$), at most 64 characters. constructor is reserved and rejected |
componentId | At most 64 characters |
props | At 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.
Related#
- Response Components API: read and set the per-agent Component frequency over the agents API
- Choice buttons: the same dial in the dashboard
- Theming: the component class and data hooks
- Configuration: the label defaults