Getting Started

Build AI chat interfaces for the terminal with @assistant-ui/react-ink.

Quick Start

The fastest way to get started with assistant-ui for the terminal.

Create a new project

npx assistant-ui@latest create --ink my-chat-app
cd my-chat-app

Manual Setup

If you prefer to add assistant-ui to an existing Node.js project, follow these steps.

Install dependencies

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

Create a chat model adapter

Define how your app communicates with your AI backend. This example uses a simple streaming adapter:

adapters/my-chat-adapter.ts
import type { ChatModelAdapter } from "@assistant-ui/react-ink";

export const myChatAdapter: ChatModelAdapter = {
  async *run({ messages, abortSignal }) {
    const response = await fetch("http://localhost:3000/api/chat", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ messages }),
      signal: abortSignal,
    });

    const reader = response.body?.getReader();
    if (!reader) throw new Error("No response body");

    const decoder = new TextDecoder();
    let fullText = "";

    while (true) {
      const { done, value } = await reader.read();
      if (done) break;

      const chunk = decoder.decode(value, { stream: true });
      fullText += chunk;
      yield { content: [{ type: "text", text: fullText }] };
    }
  },
};

This is the same ChatModelAdapter interface used in @assistant-ui/react and @assistant-ui/react-native. If you already have an adapter, you can reuse it as-is.

Set up the runtime

app.tsx
import { Box, Text } from "ink";
import {
  AssistantProvider,
  useLocalRuntime,
} from "@assistant-ui/react-ink";
import { myChatAdapter } from "./adapters/my-chat-adapter.js";

export function App() {
  const runtime = useLocalRuntime(myChatAdapter);

  return (
    <AssistantProvider runtime={runtime}>
      <Box flexDirection="column" padding={1}>
        <Text bold color="cyan">My Terminal Chat</Text>
        {/* your chat UI */}
      </Box>
    </AssistantProvider>
  );
}

Build your chat UI

Use primitives to compose your terminal chat interface:

components/thread.tsx
import { Box, Text } from "ink";
import {
  ThreadRoot,
  ThreadMessages,
  ThreadEmpty,
  ComposerInput,
  useThreadIsRunning,
} from "@assistant-ui/react-ink";
import type { ThreadMessage } from "@assistant-ui/react-ink";
import { MarkdownText } from "@assistant-ui/react-ink-markdown";

const Message = ({ message }: { message: ThreadMessage }) => {
  const isUser = message.role === "user";
  const text = message.content
    .filter((p) => p.type === "text")
    .map((p) => ("text" in p ? p.text : ""))
    .join("");

  if (isUser) {
    return (
      <Box marginBottom={1}>
        <Text bold color="green">You: </Text>
        <Text wrap="wrap">{text}</Text>
      </Box>
    );
  }

  return (
    <Box flexDirection="column" marginBottom={1}>
      <Text bold color="blue">AI:</Text>
      <MarkdownText text={text} />
    </Box>
  );
};

const StatusIndicator = () => {
  const isRunning = useThreadIsRunning();
  if (!isRunning) return null;
  return (
    <Box marginBottom={1}>
      <Text color="yellow">Thinking...</Text>
    </Box>
  );
};

export const Thread = () => {
  return (
    <ThreadRoot>
      <ThreadEmpty>
        <Box marginBottom={1}>
          <Text dimColor>No messages yet. Start typing below!</Text>
        </Box>
      </ThreadEmpty>

      <ThreadMessages renderMessage={(props) => <Message {...props} />} />

      <StatusIndicator />

      <Box borderStyle="round" borderColor="gray" paddingX={1}>
        <Text color="gray">{"> "}</Text>
        <ComposerInput
          submitOnEnter
          placeholder="Type a message..."
          autoFocus
        />
      </Box>
    </ThreadRoot>
  );
};

Render with Ink

index.tsx
import { render } from "ink";
import { App } from "./app.js";

render(<App />);

What's Next?