AgentChat
Full agent chat pane with streamed turns, tool chips, and a composer.
AgentChat is the conversation pane for an agent platform: turns scroll in a message scroller that follows streams while you are pinned to the bottom (with a jump-to-latest button when you are not), user turns render as bubbles, agent turns as streamed markdown with ToolCallChip invocations and quiet reasoning lines, and the composer has a controls slot for ThinkingModelSelect. Typed against the AI SDK’s UIMessage — bring your own useChat transport.
The preview streams a scripted vibe-coding session through the real useChat lifecycle (no model or network): press send to play the next turn.
Install
npx shadcn@latest add https://ui.neon.com/r/agent-chat.json
Usage
import { useChat } from "@ai-sdk/react";
import { AgentChat } from "@/components/neon-ui/agent-chat";
const { messages, sendMessage, status } = useChat({ transport });
<AgentChat
messages={messages}
status={status}
onSend={(text) => sendMessage({ text })}
/>;
Wiring to Neon
Stream a Neon-hosted agent (e.g. a Mastra agent on a Neon Function) directly from the browser with a short-lived JWT, so your app server never sits in the path of the long stream:
const { messages, sendMessage, status } = useChat({
transport: new DefaultChatTransport({
api: `${agentUrl}/chat`,
prepareSendMessagesRequest: async ({ messages }) => ({
headers: { Authorization: `Bearer ${await getToken()}` },
body: { messages, model, reasoning_effort: effort },
}),
}),
});
Props
messagesUIMessage[]
Conversation from useChat.
UIMessage[]statusChatStatus
useChat status: "submitted" | "streaming" | "ready" | "error".
ChatStatusonSend(text: string) => void
Called with the trimmed composer text.
(text: string) => voidcontrols?ReactNode
Slot at the left of the composer's control row, e.g. ThinkingModelSelect.
ReactNodeemptyState?ReactNode
Rendered while the conversation is empty.
ReactNodedisabled?boolean
Disable the composer, e.g. while an environment provisions.
booleanfalseplaceholder?string
Composer placeholder text.
string"Describe a change…"Model switches
Attach AgentChatTurnMetadata (model, modelName, effort) to user turns via sendMessage metadata. When a turn’s model or effort differs from the previous turn, a quiet “switched to GPT-5.2 · high thinking” marker renders above it — part of the timeline, so it replays with the transcript. Try it in the preview: change the model or effort, then send.
sendMessage({ text }, { metadata: { model, modelName, effort } });
Sub-components
Both are exported for custom layouts.
ChatMessage
One turn from a UIMessage: user turns as bubbles, agent turns as markdown with reasoning and tool lines.
Neon Auth is already wired; I need a user_id column and a filtered query.
Done — books now carry a user_id, and every query filters by the signed-in user from Neon Auth. Sign in as two different users to verify the isolation.
ChatInput
The composer on its own: auto-growing textarea, controls slot, Enter to send.
ChatWorkingIndicator
The busy row: the Neon mark and the status line painted with the same shadcn shimmer gradient, sweeping in sync. Tune both at once with shimmer-duration-* or shimmer-color-* on the wrapper; the grain-resolve NeonLoader remains available standalone. Rendered automatically while a message is submitted; the label is yours.
Accessibility
The busy marker uses role="status"; the composer is a labeled textarea (Enter sends, Shift+Enter breaks); the scroller preserves reading position during streams and reduced motion is respected throughout.