Skip to content
WorkspaceAI SDK adapterMenu

AI SDK adapter

Bind useChat from AI SDK 7 to chatcn actions, and run it with a scripted transport when there is no server.

Real useChat, a scripted transport and an approval, with no API key.

Installation

$ pnpm dlx shadcn@latest add https://chatcn.dev/r/use-chatcn-chat.json

This installs useChatcnChat and the chatcn-ai-sdk adapter it builds on. Install chatcn-mock-transport too for demos and tests.

$ pnpm dlx shadcn@latest add https://chatcn.dev/r/chatcn-mock-transport.json

Usage

tsx
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

tsx
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)
ChatcnActionsAI 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.

tsx
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.
  • speed plays faster, and clock: 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.