Skip to content
GuidesMigrate from assistant-uiMenu

Migrate from assistant-ui

Keep the assistant-ui runtime and swap only the rendering, or move to plain props. A concept map, both paths in code, and every component and primitive with its chatcn counterpart.

assistant-ui is built around a runtime: a provider owns the thread, and primitives read from it. chatcn has no runtime. Components take plain props, and a small adapter turns whatever owns your state into those props. That difference decides how you migrate: keep the runtime and change what renders, or drop the runtime and let useChat (or your own store) own the thread.

Two models

assistant-uichatcn
Thread stateThe runtime, behind AssistantRuntimeProviderYour hook or store: useChatcnChat, useChatcnAgUi, useChatcnLangGraph or your own
RenderingPrimitives that read context (ThreadPrimitive, MessagePrimitive)Components with props (Conversation, Message, MessageParts)
Message shapeThreadMessage with content partsChatcnMessage, a structural mirror of AI SDK UIMessage
Tool UImakeAssistantToolUI registered by tool nameA ToolRendererMap passed as a prop
ControlsPrimitives call the runtime (ComposerPrimitive.Send, ActionBarPrimitive.Reload)ChatcnActions callbacks. A missing action hides its control
Tests and storiesA runtime with a mock adapterStatic ChatcnMessage[] or a scripted stream, no provider

Path 1: keep the runtime

If your app depends on runtime features (thread lists from Assistant Cloud, branching, an external store adapter), keep them. chatcn-assistant-ui renders assistant-ui messages with chatcn text, reasoning and tool components and answers approvals through the runtime.

$ pnpm dlx shadcn@latest add https://chatcn.dev/r/chatcn-assistant-ui.json
components/assistant-message.tsx
"use client"

import { MessagePrimitive } from "@assistant-ui/react"
import { toolBashRenderers } from "@/components/ui/chatcn/tool-bash"
import { toolFileEditRenderers } from "@/components/ui/chatcn/tool-file-edit"
import { createChatcnPartComponents } from "@/lib/chatcn-assistant-ui"

// Module scope: new component identities would remount every part.
const components = createChatcnPartComponents({
  renderers: { ...toolBashRenderers, ...toolFileEditRenderers },
})

export function AssistantMessage() {
  return (
    <MessagePrimitive.Root className="flex flex-col gap-3">
      <MessagePrimitive.Parts components={components} />
    </MessagePrimitive.Root>
  )
}

Tools that already have a makeAssistantToolUI keep it; every other tool renders through chatcn. To use a whole block on the runtime, read the thread with useAuiState, convert it with toChatcnMessages and bind the controls with useChatcnAssistantUiActions.

tsx
"use client"

import { useAuiState } from "@assistant-ui/react"
import { Chatbot } from "@/components/chatcn/chatbot/chatbot"
import { toChatcnMessages, useChatcnAssistantUiActions } from "@/lib/chatcn-assistant-ui"

// Render inside your existing AssistantRuntimeProvider.
export function RuntimeChat() {
  const thread = useAuiState((s) => s.thread.messages)
  const running = useAuiState((s) => s.thread.isRunning)
  const actions = useChatcnAssistantUiActions()

  return (
    <Chatbot
      messages={toChatcnMessages(thread)}
      status={running ? "streaming" : "ready"}
      actions={actions}
    />
  )
}

Path 2: drop the runtime

If you used assistant-ui with AI SDK, the runtime was wrapping useChat. Call it directly through useChatcnChat and pass the result to a block or your own thread. Your route handler stays exactly as it is.

$ pnpm dlx shadcn@latest add https://chatcn.dev/r/chatbot.json
app/page.tsx
"use client"

import { DefaultChatTransport } from "ai"
import { Chatbot } from "@/components/chatcn/chatbot/chatbot"
import { useChatcnChat } from "@/hooks/use-chatcn-chat"

export default function Page() {
  const chat = useChatcnChat({ transport: new DefaultChatTransport({ api: "/api/chat" }) })
  return <Chatbot chat={chat} fullHeight />
}

Thread lists, persistence and branching become your code: ThreadList and ChatSidebar render whatever threads you load, and BranchNavigator shows the branch you pass it. On LangGraph or AG-UI, use their adapters instead of useChatcnChat.

Component map

Names on the left are the assistant-ui registry components; files on the right install under components/ui/chatcn/ and components/chatcn/, so both kits can live side by side while you move screen by screen.

assistant-uichatcnNotes
ThreadChatbot, or conversation, message, message-partsThe block has the empty state, composer, actions and errors built in
MarkdownTextMarkdownStreams without layout shift; math and Mermaid load on first use
ToolFallbackTool fallback, Tool callPlus a renderer per tool kind: bash, file edit, web search, MCP and more
ReasoningReasoning
ComposerAttachments, UserMessageAttachmentsattachment, drop-zonePromptInput takes files from paste, drop and the attach button
ThreadListthread-list, chat-sidebarGrouped by date, with search
TooltipIconButtonbutton with tooltip
AssistantModal, AssistantSidebarchatbot inside dialog, popover or chat-layoutThe block fills its parent, so any container works

Primitive map

assistant-ui primitivechatcn
ThreadPrimitive.Root, ViewportConversation, ConversationContent
ThreadPrimitive.MessagesMap your messages to Message and MessageParts
ThreadPrimitive.EmptyEmptyState
ThreadPrimitive.ScrollToBottomScrollToBottom
ThreadPrimitive.Suggestions, SuggestionSuggestions, Suggestion
ComposerPrimitive.Root, Input, Send, CancelPromptInput, PromptInputTextarea, PromptInputSubmit (Send becomes Stop)
ComposerPrimitive.AddAttachment, Attachments, AttachmentDropzonePromptInputAttachButton, PromptInputAttachments, DropZone
ComposerPrimitive.DictateDictationButton
ComposerPrimitive.QueueQueuedMessages
ComposerPrimitive.TriggerPopoverRichPromptInput with MentionMenu and SlashMenu
MessagePrimitive.PartsMessageParts, or keep it with createChatcnPartComponents
MessagePrimitive.ErrorMessageError
ActionBarPrimitive.Copy, Reload, EditMessageActions, MessageActionCopy, MessageAction, MessageEdit
ActionBarPrimitive.FeedbackPositive, FeedbackNegativeMessageFeedback
BranchPickerPrimitiveBranchNavigator
ThreadListPrimitive, ThreadListItemPrimitiveThreadList, ThreadListItem
ChainOfThoughtPrimitiveTrace, TraceGroup
makeAssistantToolUIAn entry in a ToolRendererMap

What changes in your code

  • Tool UIs. A makeAssistantToolUI({ toolName, render }) becomes { [toolName]: Renderer }, where the renderer receives the part and spreads toolBasePropsFromPart(props) into ToolCall.
  • Approvals. respondToToolApproval becomes actions.approve(id) and actions.deny(id, reason). The adapter keeps calling the runtime for you on path 1.
  • Status. thread.isRunning maps to a status of streaming or ready on blocks.
  • Styling. chatcn tokens are namespaced --chatcn-*, so they never touch your shadcn variables. See Theming in depth to map one onto the other.