Chat
The chat object on every platform — its transcript, connection state, typing indicator and upcoming calls; open, close, send, retry and clear; earlier messages, a new conversation and the window size; the chat views and the surface switch — with what each returns, threading, and how sending, reconnecting and offline messages behave.
What a customer sees and how the chat and the call take turns are on Chat. Each SDK's bar already draws the chat; this page is for reading it, driving it, or drawing your own.
The chat object
Members
| What | React Native | iOS | Android | Flutter | Kotlin Multiplatform |
|---|---|---|---|---|---|
| The transcript, oldest first | messages (Turn[]) | turns | messages | turns (messages too) | turns |
| Connected | connected | online, status | connected, online | connected | status |
| Opening | connecting | status.isConnecting | connecting | connecting | status |
| Reconnecting | reconnecting | status is reconnecting(attempt:) | reconnecting (with connecting) | reconnecting (with connecting) | status |
| Gave up | failed, client.chatFailed | status is offline | failed | failed, error | status |
| The agent is typing | typing | typing | typing | agentTyping | typing |
| Open | open, setOpen | isOpen, open(size:), close() | open(size), connect(), close(), shown(visible) | isOpen, open(), close() | open(size), close() |
| Open at a size | client.openChat({ size }); chatSize is the size in force | open(size:); openedSize | open(size); openedSize | GraineChat.open(context, size:) | open(size) |
| Send | send(text), client.sendText(text) | send(_:) | send(text) | send(text) | send(text) |
| Try again after it gave up | retry(), client.retry(), or the next send | retry() | connect(), or the next send | open(), or the next send | retry() |
| Empty the transcript | clearChat(), client.clearChat() | clear() | clear() | clear() | clear() |
| Answer a component | useGraineWidget().submit, client.submitWidget(widgetId, data, summary) | the component view; answerComponent(widgetId:data:summary:) on the instance | the component view; answerWidget(widgetId, data, summary) on the instance | answerWidget(id, data, summary) | answerWidget(id, data, summary) |
| Upcoming calls | useGraineScheduling | upcomingCalls | upcomingCalls | upcomingCalls | upcomingCalls, refreshUpcomingCalls(), cancelUpcomingCall(id) |
| Start a new conversation | client.newConversation() | newConversation() | newConversation(done) | newConversation() | newConversation() |
| Lay out the rows a window draws | chatTurnRows(messages) (chatRows(items, read) for items of your own) | GraineChatRows.build(turns) | ChatRows.build(messages) | ChatRows.build(turns, now:) | GraineChatRows.build(turns) |
The React Native client also answers, at any moment: isConnected,
isMuted, isAgentSpeaking (booleans), connectionQuality (unknown outside a
call) and networkState (online, offline or unknown) — for code outside
React's render, where the hooks cannot reach.
Every member is read and called on the main thread on iOS and Android; changes
arrive through @Published (iOS), change listeners (Android), notifyListeners
(Flutter), state flows (Kotlin Multiplatform) and re-renders (React Native).
send
| Parameter | Type | |
|---|---|---|
text | String | The customer's message. Empty text is ignored |
Returns nothing — except React Native's client.sendText, which returns
{ id, queued }: queued is true when the chat was not connected.
What happens: the customer's turn appears at once. Sent while connected, it
goes straight to the agent (delivery: sent); typed while the chat is down, it
waits — Waiting for connection…, delivery: sending — and goes in order as
soon as the chat reconnects (React Native keeps up to 20 waiting). If the chat
cannot reconnect, the turn is marked failed and retry() tries again; in React
Native the turn keeps waiting (pending) and retry() connects the chat again. The typing indicator shows from the send until the agent's
first words, a component, the chat closing, or 12 seconds.
Reconnecting
After an unexpected drop, while the customer has the chat open, the SDK
reconnects by itself. iOS, Android, Flutter and Kotlin Multiplatform try six
times, 1, 2, 4, 8, 15 and 15 seconds apart, then stop at offline until the
customer sends a message or your app retries. React Native waits 1, 2, 4, 8, 15,
then 30 seconds between tries for as long as the customer is in the chat
(reconnectDelaysMs on the client changes the waits). It does not reconnect after
the chat was closed on purpose, after Graine refused it, or on a voice call. A
reconnect starts a new conversation with the agent; the transcript on screen
stays — and for a customer whose conversation continues, the reconnect brings the
earlier messages again, which replace those on screen without repeating any.
Android and Flutter say so with reconnecting (iOS reconnecting(attempt:),
Kotlin Multiplatform status), true from the drop — or a first try that failed —
until the chat is back, gives up or is closed. A chat that gave up says that too:
offline (iOS, Kotlin Multiplatform), failed (Android, Flutter) and, in React
Native, failed / client.chatFailed — after its first connect failed, or Graine
ended or refused the conversation (React Native never gives up on a network drop).
retry() — or the next message — connects again.
Earlier messages
For an identified customer with memory on, Graine keeps the last 50 messages with each agent, for 7 days after the latest, and a new chat with that agent starts from them — the next day, after a relaunch, on another of their devices (Memory → Conversations continue).
- The transcript begins with them, oldest first, followed by anything of the
customer's still sending or not sent. They arrive before the agent's first
reply, with the time each was said, delivered, never live, and with
restoredset —restored: truein React Native. Each carrieschannel:chat, orvoicefor something said on a call (iOSGraineChatChannel, AndroidGraineChatTurn.Channel, FlutterGraineChatChannel, Kotlin MultiplatformGraineChatChannel, React Nativechannel?: "voice"). - The chat views draw a chat message as a bubble, a run of messages from a voice call as one row — Voice call · 6:40 pm — and a date line (Today, Yesterday, or the date) wherever the day changes. The window opens on the conversation, not on the starters.
- No second welcome. The agent continues the conversation; the customer types next.
- Emptied when
forgetMemory()succeeds — a failed one leaves them on screen — onnewConversation(), and, as before, byidentify(null)or another customer signing in.
Anonymous customers, customers who turned memory off, and agents without memory start every chat empty. An SDK before 0.7.0 does not show the earlier messages; the agent still continues from them.
newConversation
Starts the chat over with this agent: a fresh thread.
Returns nothing: React Native and the website resolve, iOS and Kotlin
Multiplatform return, Flutter completes, and Android calls done(Unit, null) on
the main thread.
What happens: the agent's chat connection ends; Graine drops the
conversation it kept for this agent and the identified customer; the transcript
clears, with anything still waiting to go; and, when the customer was in the
chat, a fresh chat opens — the welcome message and the starters return.
Memory stays: the agent's memories of the customer are untouched
(forgetMemory() forgets them), and so are other agents' conversations. For an
anonymous customer nothing was kept: it only clears the transcript, and cannot
fail.
Errors the same as forgetMemory(): device_limit, unavailable,
network, rate_limited, invalid_key, origin_refused — iOS throws
GraineServiceError, Flutter and Kotlin Multiplatform GraineException, React
Native and the website reject, and Android's done hears the GraineError. Nothing changes then: the
chat is as it was, connected again if it was open.
The chat window does the same from its header: a New conversation control, a compose icon beside close, shown only while the window holds earlier messages (restored turns). It does not ask first — Call History keeps the conversation.
clear
Empties the chat on screen — React Native clearChat() (also client.clearChat(),
heard as chat_cleared with reason app), iOS, Android, Flutter and Kotlin
Multiplatform clear().
Only the screen. The conversation goes on — connected stays connected — a
message still waiting for the connection is still sent, and nothing Graine keeps
is touched: the conversation that continues comes back with the next history, and
the agent's memories stay. newConversation() starts over; forgetMemory()
forgets.
Views
GraineVoiceBar draws the chat window when the agent offers the chat, and the
switch when it offers both — at the window's size (safeArea gives it the
phone's insets for a sheet or a full screen). GraineAgentBar and GraineFab
draw it too, with components.
The chat as a screen of your own app. While it is on screen the customer is in
the chat and the bar steps aside; leaving puts back what was open. Without
onClose, no chevron is drawn.
Multi-agent API: with several agents, each
view takes the agent it draws — iOS GraineChatView(graine: GraineInApp.named("support")),
Android chatView.source = GraineInApp.get("support").chat, Flutter
GraineChat(app: GraineInApp.named('support')), Kotlin Multiplatform
GraineChat(agent = "support"); in React Native, the nearest provider's. Without
one, a view draws the default agent.
Window size
How big the chat opens: the design's Chat window → Window size in the Garage
(chat.size in the design: compact, large or full; anything else is
compact). Your app can choose otherwise, and one open's size wins over the
app's, which wins over the design's:
| Platform | For every chat | For one open |
|---|---|---|
| React Native | chatSize on GraineProvider (or the client's options) | client.openChat({ size }) |
| iOS | GraineConfig(chatSize: .large) | GraineInApp.shared.chat.open(size: .full) |
| Android | GraineConfig(chatSize = GraineChatSize.LARGE) | GraineInApp.get().chat.open(GraineChatSize.FULL) |
| Flutter | GraineConfig(chatSize: GraineChatSize.large) | GraineChat.open(context, size: GraineChatSize.full) |
| Kotlin Multiplatform | GraineConfig(chatSize = GraineChatSize.LARGE) | Graine.chat.open(GraineChatSize.FULL) |
| Website | GraineAgent.load({ chatSize }) | GraineAgent.open({ size }) |
A size passed to an open lasts until the chat closes, and moving to the call and back keeps it. With the bar on screen, the open moves it to the chat at that size. What each size looks like on a phone, a tablet and a computer: Chat → Window size. The type on each platform: Types → GraineChatSize.
Which surface is open
React Native: useGraineSurface(): { surface, surfaces, canSwitch, setSurface },
with surface voice or chat, and the provider's surface and
onSurfaceChange props. iOS: GraineSurface on GraineAgentBar. The dashboard's
What visitors get decides what can be chosen; an app can choose among those,
never add one.
The transcript
An earlier message, put back when the conversation continued:
Fields: GraineChatTurn. The
customer's role is customer (user in Flutter and Kotlin Multiplatform).

