useChat()
Transcript, send, stop, loading, and errors for one thread.
useChat is the hook for rendering one conversation. It streams the thread the server stored, keeps send/stop in sync, and re-renders when the thread moves — not when the user types.
The draft lives on useComposer(). That split is
deliberate: a component holding the text box should not re-render the
transcript on every keystroke.
function Chat() {
const chat = Assistant.useChat();
return (
<>
{chat.loading ? <p>Loading…</p> : null}
{chat.messages.map((message) => (
<article key={message.id}>{/* render parts */}</article>
))}
{chat.isRunning ? (
<button type="button" onClick={() => void chat.stop()}>
Stop
</button>
) : null}
</>
);
}Requires Assistant.ThreadProvider. See Chat for a full page.
Import
On the agent binding (usual):
const chat = Assistant.useChat();There is no import { useChat } from "@pai/react". The hook is Pai.agent("main").useChat.
Signature
function useChat(): ChatController<TContract>;Returns
ChatController
Extends ThreadController.
messages: PaiMessage<TContract>[]
Transcript, including optimistic local sends. Render message.parts, not a
single content string.
isRunning: boolean
Agent work or a local command is active.
activity: ChatActivity
Fine-grained local command flags.
isActive: boolean
Any tracked local command is in flight.
sending: boolean
regenerating: boolean
retrying: boolean
stopping: boolean
ThreadController
thread: Thread<TContract>
Underlying client handle (commands, files, …).
state: ThreadState<TContract>
Public snapshot.
thread: PaiThreadHead
threadId, version, status, title, metadata.
messages: PaiMessage<TContract>[]
Same transcript as chat.messages without optimistic local sends.
runs: RunState[]
activeRunId: RunId | null
usage: UsageSummary
capabilities: ThreadCapabilityView
error: ThreadErrorRecord | null
Durable safe run failure (code, message, optional errorId,
retryable). Not the same as error on the controller.
status: ThreadStatus
Thread lifecycle from state.thread.status.
loading: boolean
True until the first snapshot arrives. Empty messages plus loading is
not “no messages.”
watchStatus: "inactive" | "connecting" | "ready" | "reconnecting" | "failed"
Live observation lifecycle. Independent of loading and of agent execution.
watchError: Error | null
Terminal watch failure. Transient disconnects recover internally and do not set this.
error: Error | null
Local command or observation throw — not the same as state.error.
readOnly: boolean
True when ThreadProvider was marked read-only.
refresh: () => Promise<ThreadState<TContract>>
Pull the latest snapshot.
send: (input, options?) => Promise<RunHandle<TContract>>
Admit a user turn. Prefer composer.submit() for the text box; use send
for canned prompts.
stop: (input?) => Promise<ActionResult<TContract>>
Cancel the active run. Honor ctx.signal in tools or they keep running.
regenerate: (input, options?) => Promise<RunHandle<TContract>>
Re-run from a prior assistant message.
retry: (options?) => Promise<RunHandle<TContract>>
Retry a failed run.
getPendingAction: (message, part) => PendingActionView | null
Resolve the action sidecar for one tool part. See Tool UI.
writeToolData: (input) => Promise<unknown>
Write ToolData on the current thread.
Learn more
- Chat — full page, composer, errors
- Tool UI —
Assistant.Tooland pending actions useComposer()