Skip to content
GuidesMigrate from prompt-kitMenu

Migrate from prompt-kit

Every prompt-kit component and block with its chatcn counterpart, and what behaves differently.

prompt-kit and chatcn are both shadcn registries, so the move is mostly a rename. chatcn files install under components/ui/chatcn/, which means both kits can share a project while you replace one screen at a time. The larger differences are behavioral: chatcn components handle tool states and approvals, read AI SDK message parts directly, and keep motion to a short, written list.

How they differ

  • Base UI and Radix. chatcn serves both from the same URL through the {style} placeholder in components.json, so a Base UI project does not ship a second primitive layer.
  • Message parts in, components out. Pass AI SDK UIMessage parts to MessageParts, or map one tool part with toolCallPropsFromPart. Approvals, denials, errors and cancelled calls each have a designed state.
  • No typewriter or bouncing dots. Streaming text appears as it arrives. Markdown has an optional cadence that evens out bursts without per word effects, and pending states use a delayed Thinking label instead of a dots loader.
  • Composer details built in. PromptInput never sends while an IME is composing, turns Send into Stop in the same footprint, and takes files from paste, drop and the attach button.

Component map

prompt-kitchatcnNotes
PromptInput, PromptInputTextarea, PromptInputActions, PromptInputActionPrompt input: PromptInput, PromptInputTextarea, PromptInputToolbar, PromptInputActions, PromptInputButton, PromptInputSubmitonSubmit receives { text }. Pass status="streaming" and onStop instead of a loading flag
PromptSuggestionSuggestions
Message, MessageAvatar, MessageContent, MessageActions, MessageActionMessage and message-actions, same namesMessage takes from, and layout for bubble, flat or compact
MarkdownMarkdownPass streaming while text arrives
CodeBlock, CodeBlockCode, CodeBlockGroupCode block: CodeBlock, CodeBlockTabsShiki with a restrained palette (or gray with chatcn-code-mono), loaded on first use, highlighted while it streams
ChatContainerRoot, ChatContainerContent, ChatContainerScrollAnchorConversation: Conversation, ConversationContentStick to bottom and the reply anchor are built in; no anchor element needed
ScrollButtonscroll-to-bottomShows a count of messages that arrived below
Loaderspinner, shimmer-text, thinking-indicatorFewer variants on purpose: a spinner for work, a shimmer label for thinking
ResponseStreamMarkdown with cadence, or streaming-textNo typewriter or per word fade
FileUpload, FileUploadTrigger, FileUploadContentdrop-zone, attachment, PromptInputAttachButton
JSXPreviewJSX previewNothing is evaluated
ToolTool call and the tool renderersSpread toolCallPropsFromPart(part); approvals included
Source, SourceTrigger, SourceContentcitation, sources
ImageimageHolds its size before it loads
Steps, ChainOfThought and their partstrace, trace-group
SystemMessagesystem-notice
TextShimmershimmer-textThe only shimmer in chatcn
ThinkingBarthinking-indicator, with Stop in PromptInputSubmit
FeedbackBarmessage-feedbackThumbs up or down with reasons
Reasoning, ReasoningTrigger, ReasoningContentReasoning

Blocks

prompt-kit blockchatcn
Full chat app, full conversationChatbot
Sidebar chat historychat-sidebar, thread-list
Prompt input with actions or suggestionsprompt-input, suggestions
Conversation with scroll to bottomconversation, scroll-to-bottom
Conversation actions and avatarsmessage, message-actions

A message, rewritten

A typical prompt-kit message maps text parts to Markdown and tool parts to Tool. In chatcn, MessageParts walks the parts for you, in order, with reasoning, sources, files and the thinking gap between steps.

tsx
import type { UIMessage } from "ai"
import { Markdown } from "@/components/ui/chatcn/markdown"
import { Message, MessageContent } from "@/components/ui/chatcn/message"
import { MessageActionCopy, MessageActions } from "@/components/ui/chatcn/message-actions"
import { MessageParts } from "@/components/ui/chatcn/message-parts"
import { toChatcnMessages } from "@/lib/chatcn-ai-sdk"
import type { ChatcnActions, ChatcnMessage, ChatcnTextPart } from "@/lib/chatcn-types"

const textOf = (m: ChatcnMessage) =>
  m.parts.map((p) => (p.type === "text" ? (p as ChatcnTextPart).text : "")).join("")

export function ChatMessage({
  message,
  streaming,
  actions,
}: {
  message: UIMessage
  streaming: boolean
  actions: ChatcnActions
}) {
  const [chatcn] = toChatcnMessages([message])
  if (!chatcn) return null
  return (
    <Message from={chatcn.role} state={streaming ? "streaming" : "done"}>
      <MessageContent>
        <MessageParts
          message={chatcn}
          streaming={streaming}
          renderText={(text, ctx) => <Markdown streaming={ctx.streaming}>{text}</Markdown>}
          toolProps={() => ({
            onApprove: (id, scope) => actions.approve?.(id, { scope }),
            onDeny: (id, reason) => actions.deny?.(id, reason),
          })}
        />
      </MessageContent>
      <MessageActions>
        <MessageActionCopy value={() => textOf(chatcn)} />
      </MessageActions>
    </Message>
  )
}

Then remove the prompt-kit files you no longer import. The chatcn tokens are namespaced, so removing prompt-kit never changes chatcn's colors.