Migration from Web

Migrate an existing @assistant-ui/react app to the terminal with React Ink.

If you already have an assistant-ui web app, most of your code transfers directly. The runtime core is shared between @assistant-ui/react and @assistant-ui/react-ink via @assistant-ui/core — only the UI layer changes.

What stays the same

  • Runtime setupuseLocalRuntime, ChatModelAdapter, and all runtime options work identically.
  • AI SDK integration@assistant-ui/react-ai-sdk works with React Ink. Your runtime setup transfers directly.
  • Tool definitionsuseAssistantTool, makeAssistantTool, and tool UI renderers use the same API.
  • State hooksuseAuiState, useAui, and selector patterns are the same.
  • Backend code — Your API routes, streaming endpoints, and server-side logic need zero changes.

What changes

Web (@assistant-ui/react)Terminal (@assistant-ui/react-ink)
AssistantRuntimeProviderAssistantProvider (same purpose)
DOM primitives (div, button, input)Ink primitives (Box, Text, TextInput)
CSS / Tailwind stylingInk's flexbox + ANSI colors
@assistant-ui/ui (shadcn components)Build your own terminal UI with primitives
@assistant-ui/react-markdown@assistant-ui/react-ink-markdown (ANSI-rendered markdown)

Step-by-step

Install the React Ink package

npm install @assistant-ui/react-ink ink react

Keep your runtime hook

Your useLocalRuntime setup works as-is — no changes needed:

hooks/use-app-runtime.ts
// This file is identical to your web version
import { useLocalRuntime, type ChatModelAdapter } from "@assistant-ui/react-ink";

export function useAppRuntime(adapter: ChatModelAdapter) {
  return useLocalRuntime(adapter);
}

If you use a monorepo, you can share the adapter between web and terminal projects directly.

Replace the provider

// Before (web)
// import { AssistantRuntimeProvider } from "@assistant-ui/react";

// After (Terminal) — same concept, different package
import { AssistantProvider } from "@assistant-ui/react-ink";

export function App() {
  const runtime = useAppRuntime(adapter);
  return (
    <AssistantProvider runtime={runtime}>
      {/* your terminal UI */}
    </AssistantProvider>
  );
}

Rebuild the UI layer

This is the main migration effort. Web components don't render in the terminal. Replace them with Ink primitives:

// Web — DOM-based
// import { Thread } from "@/components/assistant-ui/thread";

// Terminal — use Ink primitives
import {
  ThreadRoot,
  ThreadMessages,
  ComposerInput,
} from "@assistant-ui/react-ink";
import { Box, Text } from "ink";

function ChatScreen() {
  return (
    <ThreadRoot>
      <ThreadMessages
        renderMessage={({ message }) => (
          <Box marginBottom={1}>
            <Text>
              {message.content.filter((p) => p.type === "text").map((p) => p.text).join("\n")}
            </Text>
          </Box>
        )}
      />
      <Box borderStyle="round" borderColor="gray" paddingX={1}>
        <ComposerInput submitOnEnter placeholder="Message..." autoFocus />
      </Box>
    </ThreadRoot>
  );
}

See the Primitives reference for the full list of available components.

Monorepo code sharing

In a monorepo, you can share everything except the UI layer:

packages/
  shared/
    hooks/          ← useAppRuntime with ChatModelAdapter (shared)
    tools/          ← tool definitions (shared)
  web/
    components/     ← DOM-based UI
  terminal/
    components/     ← Ink-based terminal UI

The runtime hook, tool definitions, and backend code are platform-agnostic. Only the UI components need separate implementations for web and terminal.