AI SDK adapter
Bind useChat from AI SDK 7 to chatcn actions, and run it with a scripted transport when there is no server.
On this page
Installation
This installs useChatcnChat and the chatcn-ai-sdk adapter it builds on. Install chatcn-mock-transport too for demos and tests.
Usage
import { DefaultChatTransport } from "ai"
import { useChatcnChat } from "@/hooks/use-chatcn-chat"
const { chatcnMessages, actions, isStreaming } = useChatcnChat({
transport: new DefaultChatTransport({ api: "/api/chat" }),
})
<ToolCall
{...toolCallPropsFromPart(part)}
onApprove={(id, scope) => actions.approve?.(id, { scope })}
onDeny={(id, reason) => actions.deny?.(id, reason)}
/>useChatcnChat is useChat plus chatcnMessages (the same array, typed for chatcn), actions and isStreaming. Approvals resume the run on their own through lastAssistantMessageIsCompleteWithApprovalResponses unless you pass your own sendAutomaticallyWhen.
Without the hook
import { useChat } from "@ai-sdk/react"
import { toChatcnActions, toChatcnMessages } from "@/lib/chatcn-ai-sdk"
const chat = useChat({ transport })
const actions = toChatcnActions(chat)
const messages = toChatcnMessages(chat.messages)| ChatcnActions | AI SDK call |
|---|---|
send(text, { files }) | sendMessage({ text, files }) |
stop() | stop() |
regenerate(messageId?) | regenerate({ messageId }) |
approve(id, { scope }) | addToolApprovalResponse({ id, approved: true }). Persist the scope in onApprovalScope |
deny(id, reason) | addToolApprovalResponse({ id, approved: false, reason }) |
submitToolOutput(toolCallId, output) | addToolOutput({ tool, toolCallId, output }). The tool name is looked up for you; pass an Error for output-error |
editMessage(messageId, text) | sendMessage({ text, messageId }): replaces the message and regenerates from it |
Mock transport
createChatcnMockTransport plays chatcn scripts as a real UI message stream, so the components, hooks and approval flow behave exactly as with a server. An approval step ends the response with tool-approval-request; when the user answers, useChat resubmits and the same script resumes where it waited.
import { createChatcnMockTransport } from "@/lib/chatcn-mock-transport"
import { script } from "@/lib/chatcn-stream"
const turn = script()
.assistant()
.text("I'll run the web tests first.")
.toolCall({ name: "run_command" })
.streamInput({ command: "pnpm test --filter web" })
.awaitApproval({ requestReason: "Runs the test suite. It changes no files." })
.output({ exitCode: 0 })
.build()
const chat = useChat({ transport: createChatcnMockTransport(turn) })- Pass one script, a list (one per user turn, the last repeats) or a function of
{ turn, messages, trigger }. - Keep scripts stable (module constants): sessions are keyed by the script, so a transport created inline on every render still resumes approvals.
speedplays faster, andclock: createManualClock()drives it without timers in tests.
Notes
- Leaf components never import this adapter. Only blocks and apps do, so chatcn stays runtime agnostic.
- AI SDK messages already fit the chatcn contract; a type test pins the two together on every release.