> ## Documentation Index
> Fetch the complete documentation index at: https://docs.typewise.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Render messages and activity

> Understand the Chat SDK state and render messages, typing indicators, quick actions, and conversation status

Everything you render comes from one immutable state snapshot. This page
explains each part and how to handle common cases: quick actions, Human Agent
handoff, and closed conversations.

## The state snapshot

`chat.getState()` returns the current snapshot. It changes identity only when
something changed, so it works directly with React's `useSyncExternalStore`,
Svelte stores, and Vue refs.

**messages** `Message[]`

Greetings, history, live messages, and messages still sending, in order.

**Message**

**id** `string`

Server ID, or `local:…` for greetings and messages not yet confirmed.

**dedupeKey** `string`

Stays the same when `id` changes. Use `dedupeKey ?? id` as a render key.

**role** `string`

`user` (the visitor), `assistant` (the AI Agent), `agent` (a Human
Agent), or `system`.

**kind** `string`

`text`, `csat_request`, `email_verification_requested`,
`email_verification_verified`, or `error`. See [Render by
kind](#render-by-kind).

**text** `string`

The body with component tags removed, for plain-text interfaces.

**content** `string`

The body as authored: markdown that may contain components such as
`<QuickActions>`.

**status** `string`

`sending`, `sent`, or `failed`. Failed messages also have `error`.

**authorName** `string`

The Human Agent's display name, for `role: "agent"`.

**attachments** `Attachment[]`

`{ name, path, type, size }` for each file. See
[Attachments](https://docs.typewise.app/guides/channels/chat-sdk/attachments-surveys-verification.md#attachments).

**createdAt** `string`

ISO timestamp.

**activity** `Activity`

What to show as a typing indicator.

**activity.kind values**

**ai\_thinking** `kind`

The AI Agent is working. `activity.text` may carry progress, such as
"Checking your order".

**agent\_typing** `kind`

A Human Agent is typing.

**waiting\_for\_agent** `kind`

A Human Agent is reviewing the AI Agent's reply before it's sent.

**idle** `kind`

Nothing to show.

**conversation** `Conversation | null`

The open conversation, or `null` before the first message.

**Conversation**

**id** `string`

The conversation ID. It's the ticket ID in Typewise.

**isHandledByHuman** `boolean`

A Human Agent has taken over.

**isClosed** `boolean`

The conversation has ended.

**canReopen** `boolean`

The visitor can keep writing in this conversation with `reopen()`.

**status** `string`

The detailed ticket status, such as `ai_agent_processing` or `done`.

**connection** `string`

`idle`, `connecting`, `connected`, `disconnected`, `suspended`, `failed`, or
`closed`. Show a "Reconnecting…" hint for `disconnected` and `suspended`.

**config** `ChatConfig | null`

Your chat configuration from the platform. `null` until it loads: `resume()`
loads it, or call `chat.getConfig()`.

**ChatConfig**

**initialMessages** `string[]`

Greetings. They already appear in `messages` before a conversation.

**suggestions** `string[]`

Conversation starters to show before the first message.

**aiAssistantName** `string`

The name to show on AI Agent messages.

**headerTitle** `string`

The chat title.

**logo** `string`

Logo URL.

**primaryColor** `string`

Brand color.

**features** `string[]`

Enabled features: `file-upload`, `emoji`, and `transcription`.

**isLoading** `boolean`

`true` while the stored conversation loads. Show a skeleton instead of an
empty list.

### Subscribe to changes

`chat.subscribe(listener)` fires after every change with the new snapshot. To
react to one kind of change, use `chat.on(event, listener)`:

```javascript theme={null}
const unsubscribe = chat.on("message", (message) => {
  if (message.role !== "user") playNotificationSound();
});
```

Both return a function that unsubscribes. For every event, see the
[API reference](https://docs.typewise.app/guides/channels/chat-sdk/api-reference.md#events).

## Render by kind

Most messages are `text`. Handle the other kinds so the visitor sees the right
prompt:

| `kind` | Render |
| :- | :- |
| `text` | The message |
| `csat_request` | A rating prompt. See [Satisfaction surveys](https://docs.typewise.app/guides/channels/chat-sdk/attachments-surveys-verification.md#satisfaction-surveys) |
| `email_verification_requested` | An email and code form. See [Email verification](https://docs.typewise.app/guides/channels/chat-sdk/attachments-surveys-verification.md#email-verification) |
| `email_verification_verified` | A confirmation that the email is verified |
| `error` | A short notice that something went wrong |

`content` is markdown. To show formatting and links, render it with a markdown
library and sanitize the HTML. If you only need plain text, render `text`.

## Sending and failed messages

When the visitor sends a message, it appears at once with `status: "sending"`.
When the server confirms it, its `id` changes and `messageUpdated` fires with
the old id as `previousId`. Render with `dedupeKey ?? id` as the key so the
element isn't recreated.

> [!WARNING]
>
> The SDK never retries a failed send, because sending isn't idempotent. A
> failed message stays in `messages` with `status: "failed"`. Show that it
> wasn't delivered and let the visitor send it again.

## Quick actions

AI Agent messages can offer buttons, written as tags in `content`. To answer
one, send its `message` (or its `label`, if there's no `message`) with
`sendMessage`.

```mdx Message content theme={null}
<QuickActions>
  <QuickActions.Item label="Track my order" message="Where is my order?" />
</QuickActions>
```

```typescript quick-actions.ts theme={null}
import type { Message } from "@typewise/chat-sdk";

const itemPattern = /<QuickActions\.Item((?:\s+[\w-]+="[^"]*")*)\s*\/>/g;
const attributePattern = /([\w-]+)="([^"]+)"/g;

export function quickActionsOf(message: Message) {
  return [...(message.content ?? "").matchAll(itemPattern)].flatMap(
    ([, attributes = ""]) => {
      const values = new Map(
        [...attributes.matchAll(attributePattern)].map(
          ([, key = "", value = ""]) => [key, value],
        ),
      );
      const label = values.get("label");
      return label ? [{ label, message: values.get("message") ?? label }] : [];
    },
  );
}
```

```tsx QuickActionButtons.tsx theme={null}
import type { Message } from "@typewise/chat-sdk";
import { chat } from "./chat";
import { quickActionsOf } from "./quick-actions";

export function QuickActionButtons({ messages }: { messages: Message[] }) {
  const last = messages.at(-1);
  const actions = last && last.role !== "user" ? quickActionsOf(last) : [];

  return actions.map((action) => (
    <button key={action.label} onClick={() => chat.sendMessage(action.message)}>
      {action.label}
    </button>
  ));
}
```

Show the buttons only for the latest message, and only when it isn't from the
visitor. Selection lists and forms work the same way: send the chosen option's
text.

## Typing indicators

Show `activity` below the last message, and send the visitor's own typing state
to Human Agents:

```typescript Show activity theme={null}
function activityLabel(activity: Activity) {
  if (activity.kind === "ai_thinking") return activity.text ?? "Thinking…";
  if (activity.kind === "agent_typing") return "Typing…";
  if (activity.kind === "waiting_for_agent") return "Reviewing…";
  return null;
}
```

```typescript Send typing state theme={null}
input.addEventListener("input", () => chat.setTyping(input.value.length > 0));
form.addEventListener("submit", () => chat.setTyping(false));
```

## Handoff and closed conversations

* **A Human Agent joined.** When `conversation.isHandledByHuman` is `true`, show
  it in the header. `activity` stays `idle` until they start typing. Their
  messages have `role: "agent"` and an `authorName`.
* **The conversation closed.** When `conversation.isClosed` is `true`, replace
  the composer with one of these:
  * `chat.reopen()` lets the visitor keep writing in the same conversation, when
    `canReopen` is `true`.
  * `chat.startNewConversation()` closes it and starts a fresh one.
* **The visitor ends it.** For a close button, call `chat.close()`.

## Suggestions before the first message

Show `config.suggestions` as buttons while there's no conversation yet:

```tsx Suggestions.tsx theme={null}
export function Suggestions() {
  const { conversation, config } = useChat();
  if (conversation) return null;

  return config?.suggestions.map((suggestion) => (
    <button key={suggestion} onClick={() => chat.sendMessage(suggestion)}>
      {suggestion}
    </button>
  ));
}
```

## Related

* [Attachments, surveys, and verification](https://docs.typewise.app/guides/channels/chat-sdk/attachments-surveys-verification.md)
* [API reference](https://docs.typewise.app/guides/channels/chat-sdk/api-reference.md)
* [Chat configuration](https://docs.typewise.app/documentation/behavior/chat.md): greetings, suggestions,
  and branding in the platform


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.