---
title: Frontend Frameworks
description: Put an eve agent behind a browser chat UI with useEveAgent.
url: "https://eve.dev/docs/guides/frontend"
docs_index: /llms.txt
---

> For an index of all documentation, see [/llms.txt](/llms.txt).

`useEveAgent` connects a browser UI to an eve agent. It starts a session, sends messages, and streams the agent's reply into state you can render. This page uses React; [Vue](/docs/guides/frontend/use-eve-agent-vue) and [Svelte](/docs/guides/frontend/use-eve-agent-svelte) work the same way.

## How it works

Your app and the agent share an origin. The browser calls the agent at `/eve/v1/*`, and a framework integration or your hosting routes those requests to the agent:

- [Next.js](/docs/guides/frontend/nextjs)
- [Nuxt](/docs/guides/frontend/nuxt)
- [SvelteKit](/docs/guides/frontend/sveltekit)
- [TanStack Start](/docs/guides/frontend/tanstack)

To generate a working chat app, run `eve add channel/web`. For scripts and server code that don't render UI, use the [Client SDK](/docs/guides/client).

## Build a basic chat

```tsx
"use client";

import { useEveAgent } from "eve/react";

export function Chat() {
  const agent = useEveAgent();
  const isResuming = agent.status === "resuming";

  return (
    <form
      onSubmit={(event) => {
        event.preventDefault();
        const form = new FormData(event.currentTarget);
        const message = String(form.get("message") ?? "").trim();
        if (message.length > 0 && !isResuming) {
          void agent.send(message);
        }
      }}
    >
      {agent.messages.map((message) => (
        <article key={message.id}>
          <header>{message.role}</header>
          {message.parts.map((part, index) =>
            part.type === "text" ? <p key={part.id ?? index}>{part.text}</p> : null,
          )}
        </article>
      ))}
      <input disabled={isResuming} name="message" />
      <button disabled={isResuming} type="submit">
        Send
      </button>
    </form>
  );
}
```

Most chat UIs need only a few values from the hook:

| Value              | Use it to                                                       |
| ------------------ | --------------------------------------------------------------- |
| `messages`         | Render the conversation                                         |
| `status`           | Disable input while `"resuming"`; show a spinner while `"busy"` |
| `error`            | Show the latest failure                                         |
| `activeTurn(view)` | Check whether a turn is open, so you can offer to cancel it     |
| `send`, `respond`  | Send a message, or answer a question                            |
| `cancel`           | Stop the running turn                                           |
| `newSession`       | Start a new conversation                                        |

See the [`useEveAgent` reference](/docs/guides/frontend/use-eve-agent) for everything else.

## Authenticate browser requests

Because the browser calls the agent on your own origin, it sends your app's cookies with every request. The agent decides whether to accept them in `agent/channels/eve.ts`.

Until you add an auth check there, production browser requests get `401`. Write an `AuthFn` that verifies your app's session; [Authentication](/docs/guides/auth-and-route-protection) shows how. For a public demo, `none()` from `eve/channels/auth` admits everyone; don't use it for private data.

If your app uses bearer tokens instead of cookies, pass them to the hook. See [Custom hosts and headers](#custom-hosts-and-headers).

## Send messages

Send text, or an array of parts to include files:

```tsx
await agent.send("Summarize this session.");

await agent.send([
  { type: "text", text: "What is in this file?" },
  {
    type: "file",
    data: fileDataUrl, // base64 data URL
    mediaType: "application/pdf",
    filename: "report.pdf",
  },
]);
```

`send()` resolves as soon as eve accepts the message. The reply then streams into `messages`.

A message sent while the agent is still working redirects the current turn, called steering. You don't need to do anything extra. To wait for the turn to finish instead, queue the message:

```tsx
await agent.send("Then summarize the second file.", { turnPolicy: "queue" });
```

To stop a running turn, call `cancel()`. Closing the page doesn't stop the turn on the server.

```tsx
import { activeTurn } from "eve/events";

if (activeTurn(agent.view) !== undefined) {
  await agent.cancel();
}
```

See [Sending](/docs/guides/frontend/use-eve-agent#sending) and [Cancelling](/docs/guides/frontend/use-eve-agent#cancelling) for exact timing.

## Human-in-the-loop prompts

Tools can ask a person to approve a call or answer a question. See [Human-in-the-loop](/docs/human-in-the-loop) for the server side. In the UI, read open prompts from `view` and answer with `respond()`:

```tsx
import { openInteractions } from "eve/events";

const pendingRequests = openInteractions(agent.view).filter(
  (item) => item.request.kind !== "sign-in" && item.answer === undefined,
);

return pendingRequests.map(({ interactionId, request }) => (
  <fieldset disabled={agent.status === "resuming"} key={interactionId}>
    <legend>{request.kind === "approval" ? "Approval required" : "Question"}</legend>
    <p>{request.prompt}</p>
    {request.options?.map((option) => (
      <button
        key={option.id}
        type="button"
        onClick={() => void agent.respond([{ interactionId, optionId: option.id }])}
      >
        {option.label}
      </button>
    ))}
  </fieldset>
));
```

- For a free-text answer, send `{ interactionId, text }`.
- A prompt stays `open` until eve decides it, not when you submit an answer. If the answer is refused, the prompt stays answerable.
- To show how a prompt ended, read its `outcome`. Only `accepted` and `declined` mean a person decided; a prompt can also end because its turn was cancelled. See [Interactions](/docs/guides/frontend/use-eve-agent#interactions).
- A prompt about a tool call also appears on that call's `tool` part as `part.interaction`. Other prompts appear as `interaction` message parts.

## Sign-ins and other interactions

When a tool or connection needs the user to sign in, for example with OAuth, the turn opens a sign-in prompt. It appears in `messages` as an `interaction` part. Render it like any other message; once the user signs in, the turn continues on its own.

```tsx
import { signInEnding, type EveMessagePart } from "eve/react";

function SignInPrompt({ part }: { part: EveMessagePart }) {
  if (part.type !== "interaction" || part.request.kind !== "sign-in") return null;
  const signIn = part.request.signIn;
  const name = signIn?.displayName ?? signIn?.name;

  if (part.status === "settled" && part.outcome !== undefined) {
    // "connected", "declined", "failed", "timed out", or "cancelled"
    return <p>{`${name}: ${signInEnding(part.outcome)}.`}</p>;
  }

  return (
    <section>
      <p>{signIn?.instructions ?? part.request.prompt}</p>
      {signIn?.userCode ? <code>{signIn.userCode}</code> : null}
      {signIn?.url ? <a href={signIn.url}>Sign in</a> : null}
    </section>
  );
}
```

`request.signIn` holds the provider's display name, instructions, device code, and sign-in URL. To list open sign-ins, use `openInteractions(view, { kind: "sign-in" })`.

## Attach page context per turn

`clientContext` tells the agent about the user's current page for one turn. It isn't saved to the conversation history:

```tsx
await agent.send("What should I do on this screen?", {
  clientContext: { route: "/billing", plan: "pro", seatsUsed: 4 },
});
```

Pass a string, an array of strings, or an object, which eve sends as JSON. The context also rides along with `respond()`.

To add context to every message, use `prepareSend`:

```tsx
const agent = useEveAgent({
  prepareSend: (input) => ({
    ...input,
    clientContext: { route: location.pathname },
  }),
});
```

## Resumable sessions

eve stores every conversation on the server. To reopen one after a reload, save two things, `session` and `events`, and pass them back:

```tsx
import { useEffect, useState } from "react";
import { useEveAgent } from "eve/react";
import type { ClientSessionState, SessionStreamEvent } from "eve/client";

type SavedEveChat = {
  events?: readonly SessionStreamEvent[];
  session?: ClientSessionState;
};

const [saved] = useState<SavedEveChat>(() => {
  const raw = localStorage.getItem("eve-chat");
  return raw ? JSON.parse(raw) : {};
});

const agent = useEveAgent({
  initialEvents: saved.events ?? [],
  initialSession: saved.session,
  resume: saved.session !== undefined,
});

useEffect(() => {
  localStorage.setItem(
    "eve-chat",
    JSON.stringify({ events: agent.events, session: agent.session }),
  );
}, [agent.events, agent.session]);
```

- `session` tells eve which conversation to continue and where to pick up. Save the whole object, as is.
- `events` lets you show the saved messages immediately, before eve catches up.
- If you only have a session ID, pass `initialSession: { sessionId, streamIndex: 0 }` with `resume: true`; eve replays the conversation from the start.

A few rules keep restored chats correct:

- **Save as you go.** Save on every change, not only when a turn finishes. A turn waiting on the user hasn't finished, but its messages must survive a reload.
- **Disable input while resuming.** While `status` is `"resuming"`, show the saved messages but disable sending.
- **One saved chat per conversation.** To switch conversations, remount the chat component, for example with `key={chat.id}`.
- **Create your chat record first.** If the user can reload right after sending, create your app's chat record before `send()`, and save the session ID from `onSessionChange`.

See [Resume a saved session](/docs/guides/frontend/use-eve-agent#resume-a-saved-session) for exactly how saved events and catch-up fit together.

## Start a new conversation

- `newSession()` clears the chat and starts fresh. The old conversation stays on the server.
- `end()` ends the conversation on the server. Call `newSession()` afterward to clear it locally.

To make the first reply faster, pass `prewarm: true`. eve then creates the next session before the user sends anything. Turn it on only after your auth headers are ready. See [Prewarming](/docs/guides/frontend/use-eve-agent#prewarming).

## Show subagent progress

When the agent delegates to a subagent, `messages` shows the delegated call, not the subagent's own messages. To follow subagents too, pass `followAgents: true`, then render `agents[sessionId].observation.conversation.messages`:

```tsx
const agent = useEveAgent({ followAgents: true });
```

See [Child agents](/docs/guides/frontend/use-eve-agent#child-agents) for limits, observation states, and showing what a single call caused.

## Lifecycle callbacks

- `onEvent(event)` runs for each stream event.
- `onError(error)` runs when something fails.
- `onFinish(snapshot)` runs when a live turn finishes.
- `onSessionChange(session)` runs when the session cursor changes. Save it here.

```tsx
const agent = useEveAgent({
  onEvent: (event) => console.debug(event.type),
  onError: (error) => toast.error(error.message),
  onFinish: (snapshot) => console.log(snapshot.status),
});
```

See [Lifecycle callbacks](/docs/guides/frontend/use-eve-agent#lifecycle-callbacks) for exactly when each one fires.

## Derive your own state

`view` already tracks turns, calls, and prompts. Query it with functions from `eve/events` instead of building your own state:

```tsx
import { activeTurn, activity, waiting } from "eve/events";

const turn = activeTurn(agent.view);
const currentActivity = activity(agent.view);
const blocked = turn === undefined ? undefined : waiting(agent.view, turn.turnId);
```

See [Use eve's answers](/docs/guides/frontend/use-eve-agent#use-eves-answers) for which query answers each common question, from a request's result to call timing and usage.

For something `view` doesn't cover, read the raw `events`:

```tsx
import { useMemo } from "react";

const toolCalls = useMemo(
  () => agent.events.filter((event) => event.type === "call.requested").length,
  [agent.events],
);
```

## Custom hosts and headers

Pass `baseUrl` when the agent is on another origin, and `auth` or `headers` for credentials. Functions run before every request, so tokens stay fresh:

```tsx
const agent = useEveAgent({
  baseUrl: "https://agent.example.com",
  auth: {
    bearer: async () => await getAccessToken(),
  },
});
```

When your app mounts several named agents, select one with `agent` instead:

```tsx
const support = useEveAgent({ agent: "support" });
```

## Per-framework integration

| Framework      | Integration                                              | Hook                                                                 |
| -------------- | -------------------------------------------------------- | -------------------------------------------------------------------- |
| Next.js        | [Next.js guide](/docs/guides/frontend/nextjs)            | [`useEveAgent` (React)](#build-a-basic-chat)                         |
| Nuxt           | [`eve/nuxt` module](/docs/guides/frontend/nuxt)          | [`useEveAgent` (Vue)](/docs/guides/frontend/use-eve-agent-vue)       |
| SvelteKit      | [`eveSvelteKit` plugin](/docs/guides/frontend/sveltekit) | [`useEveAgent` (Svelte)](/docs/guides/frontend/use-eve-agent-svelte) |
| TanStack Start | [`eveTanStack` plugin](/docs/guides/frontend/tanstack)   | [`useEveAgent` (React)](#build-a-basic-chat)                         |
| Any React      | same-origin or `baseUrl`                                 | [`useEveAgent` (React)](#build-a-basic-chat)                         |

## What to read next

- [`useEveAgent` reference](/docs/guides/frontend/use-eve-agent): every value, option, and behavior
- [Next.js](/docs/guides/frontend/nextjs): set up a chat app end to end
- [Client SDK](/docs/guides/client): call the agent from scripts and server code
- [Sessions, runs & streaming](/docs/concepts/sessions-runs-and-streaming): how sessions and the event stream work

---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)