> ## Documentation Index
> Fetch the complete documentation index at: https://browseruse-0aece648-magnus-concurrency-docs.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Chat UI

> Full end-to-end example. Build a chat UI with live browser preview, follow-up tasks, recording, and streaming messages.

<Card title="Full source code" icon="github" href="https://github.com/browser-use/chat-ui-example">
  Clone and run in minutes. Next.js + Browser Use SDK v3.
</Card>

This tutorial walks through the [chat-ui-example](https://github.com/browser-use/chat-ui-example) — a Next.js app that lets users chat with a Browser Use agent in real time. We focus on the SDK integration, not the UI components.

The app has two pages:

1. **Home** — the user types a task, the app creates a session and sends the task.
2. **Session** — live browser preview, streaming messages, follow-ups, and recording download.

All SDK calls live in a single file: `src/lib/api.ts`.

## Setup

```typescript api.ts theme={null}
import { BrowserUse } from "browser-use-sdk/v3";

// Server-only — no NEXT_PUBLIC_ prefix, never exposed to the browser
const apiKey = process.env.BROWSER_USE_API_KEY ?? "";
export const client = new BrowserUse({ apiKey });
```

<Note>
  The API key uses `BROWSER_USE_API_KEY` (no `NEXT_PUBLIC_` prefix) so it stays server-side. All SDK calls go through [server actions](https://nextjs.org/docs/app/guides/forms) — never call the SDK directly from client components.
</Note>

***

## 1. Create a session

```typescript actions.ts theme={null}
"use server";
import { client } from "./api";

export async function createSession() {
  const session = await client.sessions.create({
    keepAlive: true,
    enableRecording: true,
  });
  return { id: session.id, liveUrl: session.liveUrl, status: session.status };
}
```

* **`keepAlive: true`** keeps the session open after each task so the user can send follow-ups (default is `false`).
* **`enableRecording: true`** produces an MP4 video of the browser session.
* **`liveUrl`** is returned immediately — no waiting or extra call needed.

The home page creates the session, navigates to the session page (passing `liveUrl` and the initial task via URL params), and the session page takes over from there:

```typescript page.tsx theme={null}
async function handleSend(message: string) {
  const session = await createSession();

  router.push(
    `/session/${session.id}?liveUrl=${encodeURIComponent(session.liveUrl)}&task=${encodeURIComponent(message)}`
  );
}
```

***

## 2. Stream messages with `for await`

Instead of polling `sessions.get()` and `sessions.messages()` separately, use `client.run()` — it streams messages and resolves when the task completes:

```typescript session-context.tsx theme={null}
const streamTask = useCallback(async (task: string) => {
  const run = client.run(task, { sessionId });

  for await (const msg of run) {
    setMessages((prev) => [...prev, msg]);
  }

  // Iterator done — task reached terminal state
  setSession(run.result);
}, [sessionId]);
```

The `for await` loop yields each message as it arrives. When the loop ends, `run.result` contains the final session state (status, output, etc.). No separate status polling needed.

Wire it up in a `useEffect` to auto-run the initial task from URL params:

```typescript session-context.tsx theme={null}
useEffect(() => {
  if (!initialTask) return;
  sendMessage(initialTask);
}, []);
```

***

## 3. Follow-up tasks

Follow-ups call the same `streamTask` function — the stream already includes the user message, so no optimistic insert is needed:

```typescript session-context.tsx theme={null}
const sendMessage = useCallback(async (task: string) => {
  await streamTask(task);
}, [streamTask]);
```

The SDK auto-sets `keepAlive: true` when targeting an existing session, so follow-up tasks work without extra config.

***

## 4. Recording

Fetch the MP4 URL after the session ends (recording was enabled in step 1):

```typescript session-context.tsx theme={null}
useEffect(() => {
  if (!isTerminal) return;

  client.sessions.waitForRecording(sessionId).then((urls) => {
    if (urls.length) setRecordingUrls(urls);
  });
}, [isTerminal, sessionId]);
```

`waitForRecording` polls for up to 15 seconds and returns presigned MP4 download URLs. Returns an empty array if the agent answered without opening a browser.

***

## 5. Stop a task

```typescript actions.ts theme={null}
export async function stopTask(id: string) {
  await client.sessions.stop(id, { strategy: "task" });
}
```

Using `strategy: "task"` stops only the current task, keeping the session alive for follow-ups.

***

## 6. Session page

The session page consumes everything through a context provider:

```typescript session/[id]/page.tsx theme={null}
function SessionPage() {
  const { session, turns, isBusy, isTerminal, recordingUrls, sendMessage, stopTask } =
    useSession();

  return (
    <div className="flex h-screen w-full overflow-hidden">
      {/* Chat column */}
      <div className="flex-1 flex flex-col min-w-0">
        <ChatMessages turns={turns} isBusy={isBusy} />
        <ChatInput
          onSend={sendMessage}
          onStop={stopTask}
          disabled={isTerminal}
        />
      </div>

      {/* Live browser view — liveUrl available from session creation */}
      <BrowserPanel liveUrl={session?.liveUrl} />
    </div>
  );
}
```

***

## Summary

| Method                               | Purpose                                          |
| ------------------------------------ | ------------------------------------------------ |
| `client.sessions.create()`           | Create a session (returns `liveUrl` immediately) |
| `client.run()`                       | Send a task and stream messages with `for await` |
| `client.sessions.stop()`             | Stop the current task                            |
| `client.sessions.waitForRecording()` | Get MP4 recording URLs                           |
