Skip to main content
Use this guide to add a chat interface to an AG-UI route. Start with the preset Chat component. Move to composition only when you need layout control. For headless state, see Chat hooks.

Prerequisites

  • A Veryfront project with an AG-UI route, such as /api/ag-ui (see Create agent).
  • A configured provider for the route’s agent (see Providers).

Add the preset UI

Create a client page:
useChat() connects to /api/ag-ui by default. Chat renders the composer, message list, loading state, and scroll behavior. The MarkdownRendererProvider wrapper is required for readable answers. Assistants reply in Markdown, and veryfront/markdown presents plain escaped source until a renderer is installed, so a <Chat> without one shows ## Heading rather than a heading. The chat starters scaffold the app/markdown-renderer.tsx this sample imports; the minimal and agentic-workflow templates do not. Create the file first if your project does not already have it. See Render Markdown in chat. Wrap the other samples on this page the same way.

Add request preprocessing

Use beforeStream when the route needs to add context, enforce authorization, or stop a request before the agent runs:
Veryfront wraps untrusted system-role messages returned from beforeStream before they reach the agent. Retrieved documents are treated as reference data, not instructions.

Customize the preset

Configure the preset’s content, theme, and agent options. The preset always includes sources, multi-step rendering, message actions, scroll-to-bottom, and attachments:
For durable attachments, mount the upload handler behind your app’s auth and point Chat at that route:
The authorization callback must return literal true to permit a request. false and invalid runtime results such as undefined are denied. The preset sends same-origin cookies automatically. If your route instead requires an explicit bearer header, compose the headless useUpload() hook with its headers option rather than placing a secret token in Chat props. To change the upload limits, bound the complete multipart request separately from the file bytes:
For local prototypes or intentionally public upload routes, pass allowUnauthenticated: true explicitly.

Compose a custom layout

Use the composition components when the preset layout is too constrained:
<Chat.Empty> renders whatever it is given; it does not hide itself. The preset <Chat> decides when to show its empty state for you, but a custom layout owns that decision, so gate the empty state on the thread being empty. <Chat.If> reads the nearest <Chat.Root>, where ctx.isEmpty is true only while messages is empty. Without the gate, the empty state stays mounted below the conversation once the first message is sent. Use Message when individual message rendering needs custom structure:

Theming and the token scope

The chat and veryfront/ui primitives resolve their var(--token) styles against a scoped design-token stylesheet, so the tokens never leak to the rest of your page. The canonical scope attribute is data-vf-ui; data-vf-chat is kept as a compatibility alias (both are set on every scope element, and every token rule matches both), so existing selectors keep working. <Chat> establishes the scope for itself. When you compose the primitives around <Chat>, such as a sidebar, header, or uploads panel in your own shell, wrap that shell in one ChatThemeScope so everything inside it is themed:
If you target the scope from your own CSS or DOM queries, prefer [data-vf-ui]. [data-vf-chat] remains supported and will only be removed in a future major release.

Add conversation navigation

Wrap the chat and sidebar in a ConversationsProvider. The provider owns the conversation list and persistence; <ChatSidebar> and <Chat> both read it from context, so neither needs wiring:
The default local adapter fails closed. Unavailable, blocked, corrupt, full, or out-of-bounds storage reports a ConversationStoreError; it does not pretend the operation succeeded or silently switch to memory. The error’s operation is list, load, save, delete, or subscribe.

Keep conversations ephemeral

Use memoryConversationStore() for SSR, short-lived demos, or sensitive sessions whose transcript must not be written to browser storage. Create the store once per mounted tree:
The memory store is cleared when the tree is discarded or the page reloads. Do not create a module-level memory store in server-rendered code, because different requests would share the same store. Use chat context providers only when nested components need direct state access. Prefer preset props or composition components first.

Render Markdown in chat

veryfront/markdown presents plain escaped source until a renderer is installed, so <Chat> shows raw Markdown on its own. Every chat starter scaffolds a renderer in app/markdown-renderer.tsx and installs it around <Chat>, so a new project renders assistant answers with no extra setup. Add the same two pieces to a project without one. Install the parser, pinned to an exact version: these packages reach the browser through the module pipeline, where a floating ^ range resolves to whatever is latest at request time.
Then create the renderer and install it for the chat subtree:
The provider covers assistant answers and reasoning. Chat applies its own prose styling around whatever the renderer returns, so lists, headings, and inline code match the rest of the chat surface. Pin the parser to an exact version: these packages reach the browser through the module pipeline, where a floating range resolves to whatever is latest at request time. Your renderer owns parsing, sanitization, and link policy. To add syntax highlighting, tables, or math, extend it with the remark and rehype plugins you want rather than changing anything in chat.

Present Markdown source safely

veryfront/markdown is the dependency-free Markdown boundary used by chat surfaces. Without an installed rich renderer, it preserves the exact source in an escaped <pre><code> element. This is useful when source visibility matters more than semantic formatting:
The default does not claim that CommonMark, GFM, highlighting, or diagrams were rendered. It emits no Markdown-authored links, images, or raw HTML, and the escaped source is present in server HTML.

Install a semantic renderer

Semantic Markdown is an explicit extension capability. Select a trusted extension or application adapter that implements MarkdownRendererProps, then install its component for the relevant subtree. In this example, ProjectMarkdownRenderer comes from that adapter:
The per-instance renderer prop takes precedence over the provider. Pass renderer={null} when a nested surface must display plain source even though an ancestor installed a renderer. Parser-dependent options are forwarded only after a renderer has been selected. For example, replace fenced-code rendering without changing the renderer used for the rest of the document:
Use components to pass framework-neutral element overrides to the selected renderer:
The extension owns parsing, sanitization, unsafe link protocols, image policy, highlighting, and diagram security. Configure parser-specific remarkPlugins or rehypePlugins on that extension, not on core Markdown; removed or unknown core props are rejected instead of ignored. Renderer failures propagate, so handle them with an application error boundary when recovery is required. Never derive plugin lists from untrusted input, and bound untrusted source size before rendering.

Verify it worked

Run veryfront dev and open the page that renders the chat UI:
  • The composer renders and accepts input.
  • A submitted message streams tokens from /api/ag-ui.
  • The preset renders its default controls.
  • Custom layouts keep the message list and composer wired to the same AG-UI stream.
  • In a custom layout, the empty state disappears after the first message is sent and does not reappear below the conversation.
  • A persisted conversation remains in the sidebar after a page reload.
  • A conversation persistence failure renders an alert with the failed operation.
  • A chat using memoryConversationStore() starts empty after a page reload.
  • Standalone Markdown source is escaped and readable in the initial server HTML; an injected renderer is used only in the subtree where it is installed.
  • An assistant answer containing a list or a heading renders as formatted Markdown, not raw Markdown source, and the browser console reports no missing-Markdown-renderer warning.
If the assistant response is empty, check the dev-server log for provider or agent errors and confirm the AG-UI route is mounted.

Next