PAIPAI

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

On this page