# terminal-kit > terminal-kit is a shadcn registry of React components for terminal-style agent UIs. Install individual components or the full bundle via the shadcn CLI. ## Table of Contents - [Introduction](#introduction) - [Installation](#installation) - [Theming](#theming) - [SessionContent streaming](#sessioncontent-streaming) - [Scripted demo chat](#scripted-demo-chat) - [Components](#components) - [Terminal Window](#terminal-window) - [Message](#message) - [Session Content](#session-content) - [Input](#input) - [Question Prompt](#question-prompt) - [Edit Block](#edit-block) - [Thinking Indicator](#thinking-indicator) - [Stream Text](#stream-text) ## Introduction # terminal-kit terminal-kit gives you shadcn components that look and behave like a real agent terminal — for production UIs and for scripted showcases. ## Demo ## Install ## Components | Component | Install slug | Description | |---|---|---| | [TerminalWindow](https://www.terminal-kit.com/docs/terminal-window) | `terminal-window` | Window chrome, line primitives, and theme (base) | | [Message](https://www.terminal-kit.com/docs/message) | `message` | Submitted user message row | | [SessionContent](https://www.terminal-kit.com/docs/session-content) | `session-content` | Streaming transcript container with auto-scroll | | [Input](https://www.terminal-kit.com/docs/input) | `input` | Prompt input with status metadata | | [QuestionPrompt](https://www.terminal-kit.com/docs/question-prompt) | `question-prompt` | Multiple-choice Q&A prompt | | [EditBlock](https://www.terminal-kit.com/docs/edit-block) | `edit-block` | File path + line numbers + diff | | [ThinkingIndicator](https://www.terminal-kit.com/docs/thinking-indicator) | `thinking-indicator` | Animated dot-matrix thinking state | | [StreamText](https://www.terminal-kit.com/docs/stream-text) | `stream-text` | Word-by-word reveal by default; `mode="fade"` fades each word in | Use `full-bundle` to install all eight at once. ## Documentation - [Installation](https://www.terminal-kit.com/docs/installation) — add components to your project - [Theming](https://www.terminal-kit.com/docs/theming) — tokens, dark/light mode, custom palettes - [Component docs](https://www.terminal-kit.com/docs/terminal-window) — API reference and live previews ## License MIT ## Installation ### Prerequisites - Node.js 18+ - React 19+ - A project with [shadcn/ui](https://ui.shadcn.com/docs/installation) initialized ### Full bundle ```bash npx shadcn@latest add "https://www.terminal-kit.com/r/full-bundle.json" ``` Installs eight components plus barrel exports (`components/terminal-kit/index.ts`). The CLI creates multiple files — one registry item can map to several source files (for example `terminal-window` includes shell primitives). ### Single component ```bash npx shadcn@latest add "https://www.terminal-kit.com/r/[component].json" ``` Replace `[component]` with one of: terminal-window, message, session-content, input, question-prompt, edit-block, thinking-indicator, stream-text. ### Usage ```tsx import { TerminalWindow, Input, InputLevelMeta, } from "@/components/terminal-kit" export function AgentShell() { return ( } /> } > {/* agent content */} ) } ``` ## Theming Theme CSS ships with `terminal-window` via the shadcn registry (`styles/terminal-theme.css` merged into your globals). The same install also adds `lib/terminal-themes.ts`, `hooks/use-detected-terminal-theme.ts`, and `components/terminal-theme-provider.tsx` — required by `TerminalWindow`, `Input`, `EditBlock`, `ThinkingIndicator`, and `QuestionPrompt`. Components read scoped tokens such as `var(--terminal-fg)` — avoid hard-coded colors. Built-in palettes: `default`, `grok`, `claude`. Pass `theme` on `TerminalWindow`. Each palette includes dark and light tokens. ```tsx {/* warm Grok palette */} {/* Claude Code product page palette */} {/* neutral cool default — omit theme prop for the same result */} ``` Site light/dark (optional) via `next-themes` on ``: ```tsx // app/layout.tsx import { ThemeProvider } from "@/components/theme-provider" export default function RootLayout({ children }: { children: React.ReactNode }) { return ( {children} ) } // TerminalWindow follows html.dark — light site mode → light terminal palette ``` Override tokens on the window: ```tsx {/* --terminal-teal is scoped to this window */} ``` Several components use `layout="auto"` to match the Claude palette (flat EditBlock, stacked Input, symbol ThinkingIndicator, etc.). Pass `theme="claude"` on `TerminalWindow` so auto layouts stay in sync. ## SessionContent streaming Use `SessionContent` with `streaming` and `autoScroll` to reveal transcript children one at a time. - `sessionPause` — **StreamText only**. Optional ms to wait before the next line. Other children get automatic pauses (Message ~280ms, EditBlock ~520ms, StreamText via `estimateStreamDurationMs(text)`). - `sessionTail` — **ThinkingIndicator only**. Pin live thinking at the transcript tail, outside the streaming queue. ```tsx import { EditBlock, Message, SessionContent, Input, InputLevelMeta, StreamText, TerminalWindow, ThinkingIndicator, } from "@/components/terminal-kit" const primaryButtonEdit = `export function PrimaryButton({ children, className, ...props }: ButtonProps) { return ( ) }` export function AgentSession() { return ( } /> } > /make-interfaces-feel-better I'll improve the primary button hover — before I edit, a quick preference check. Got it — easing opacity and adding a motion-safe press scale on hover. Done — hover now eases to 90% opacity and the button presses in on click. Want the same pass on the secondary and ghost variants? ) } ``` ## Scripted demo chat For landing pages, README embeds, and marketing demos you usually want a **scripted** session — not a live LLM. Drive phases with React state and timers, like the site showcase (`components/app/demo-terminal.tsx`). ### Pattern 1. **Acts, not one transcript** — Act 1 streams an intro and opens a question; Act 2 (after the user picks an option) mounts a second `SessionContent` with a new `resetKey` so streaming replays. 2. **QuestionPrompt in the footer only** — While `questionOpen`, swap `TerminalWindow` `footer` from `Input` to `QuestionPrompt`. Do **not** mount the active prompt inside `SessionContent` (that is for transcript lines). After `onSelect`, render `QuestionAnswerSummary` in the transcript. Keep `pinScrollBottom={!questionOpen}` so scroll behaves while options are visible. 3. **`StreamTextSlot` + hidden spacer** — Reserve a stable slot before copy starts streaming. If you conditionally mount `StreamText` from nothing, `SessionContent` sees a new child and the reveal queue restarts. 4. **`ThinkingIndicator sessionTail`** — Show while waiting or streaming; stays pinned at the bottom of the transcript, outside the one-by-one queue. 5. **`sessionPause` on `StreamText`** — Pair with `estimateStreamDurationMs(text, { speed, mode })` so the next line waits until typing finishes. 6. **`QuestionAnswerSummary`** — After `onSelect`, render the answered Q&A row in the transcript (Act 2), not another footer prompt. 7. **Passive demos** — Optional `setTimeout(() => handleAnswer("2"), 1800)` auto-picks an option so the story continues without clicks. ### Full example ```tsx "use client" import * as React from "react" import { EditBlock, estimateStreamDurationMs, Input, InputLevelMeta, Message, QuestionAnswerSummary, QuestionPrompt, SessionContent, StreamText, TerminalWindow, ThinkingIndicator, } from "@/components/terminal-kit" const QUESTION = "Which interaction should we prioritize?" const OPTIONS = [ { id: "1", label: "Hover feedback", description: "Ease opacity, subtle press-in" }, { id: "2", label: "Focus ring", description: "Keyboard-visible focus state" }, ] as const const INTRO = "I'll improve the primary button hover — before I edit, a quick preference check." const CONTINUE = "Got it — I'll strengthen the focus-visible ring so keyboard users get a clear target." const CLOSING = "Done — hover now eases to 90% opacity and the button presses in on click." const STREAM_SPEED = 80 const streamPause = (text: string) => estimateStreamDurationMs(text, { speed: STREAM_SPEED, mode: "fade" }) /** Keeps SessionContent child count stable before a line starts streaming. */ function SessionSpacer() { return
} function StreamTextSlot({ active, children, onComplete, ...props }: React.ComponentProps & { active: boolean }) { if (!active) return return ( {children} ) } export function ScriptedDemoChat() { const [questionOpen, setQuestionOpen] = React.useState(false) const [answered, setAnswered] = React.useState(false) const [selectedAnswer, setSelectedAnswer] = React.useState(null) const [showThinking, setShowThinking] = React.useState(false) const [introActive, setIntroActive] = React.useState(false) const handleAnswer = React.useCallback((optionId: string) => { const label = OPTIONS.find((option) => option.id === optionId)?.label ?? optionId setSelectedAnswer(label) setAnswered(true) setQuestionOpen(false) setShowThinking(true) }, []) // Act 1: thinking → stream intro → open question in footer React.useEffect(() => { const t = window.setTimeout(() => setShowThinking(true), 400) return () => window.clearTimeout(t) }, []) React.useEffect(() => { if (!showThinking || introActive || questionOpen) return const t = window.setTimeout(() => setIntroActive(true), 900) return () => window.clearTimeout(t) }, [showThinking, introActive, questionOpen]) // Optional: auto-pick an option for passive landing-page demos React.useEffect(() => { if (!questionOpen || answered) return const t = window.setTimeout(() => handleAnswer("2"), 1800) return () => window.clearTimeout(t) }, [answered, handleAnswer, questionOpen]) const footer = questionOpen && !answered ? ( ) : ( } /> ) return ( /make-interfaces-feel-better { setShowThinking(false) setQuestionOpen(true) }} > {INTRO} {showThinking && !answered ? ( ) : null} {answered && selectedAnswer ? ( {CONTINUE} setShowThinking(false)} > {CLOSING} {showThinking ? ( ) : null} ) : null} ) } ``` ### Static alternative If you do not need phased state (no footer question swap, no timers), a single `SessionContent` with all children declared upfront is enough — see the SessionContent streaming example above. ## Components ### Terminal Window Window chrome, line primitives, scroll body, and theme tokens. Base for every terminal-kit component. Source: `components/terminal-kit/shell/terminal-window.tsx` #### Install ```bash npx shadcn@latest add "https://www.terminal-kit.com/r/terminal-window.json" ``` #### Example ```tsx import { Message, TerminalBody, TerminalWindow, ThoughtLine, } from "@/components/terminal-kit" /make-interfaces-feel-better ``` #### API #### TerminalWindow Top-level shell with header chrome, scroll body, and optional footer. | Prop | Type | Default | Description | | --- | --- | --- | --- | | `path` | `string` | `undefined` | Path label in the header. | | `showTrafficLights` | `boolean` | `true` | Show macOS-style traffic lights in the header. | | `theme` | `` | — | Built-in palette. Default is a neutral dark terminal; grok is warm dark; claude matches the Claude Code product page terminal mockup. | | `variant` | `` | — | | | `footer` | `ReactNode` | — | Pinned footer slot — usually Input. | | `header` | `ReactNode` | — | Optional header slot above scroll content. | | `fill` | `boolean` | `false` | Stretch to fill a fixed-height window and scroll internally. | | `pinScrollBottom` | `boolean` | `false` | Pin scroll content to the bottom as new lines appear. | | `bodyClassName` | `string` | — | Classes for the inner body layout wrapper. | | `headerAction` | `ReactNode` | — | Optional control rendered at the end of the window chrome header. | | `className` | `string` | — | Additional CSS classes on the window shell. | | `...props` | `HTMLDivElement` | — | Standard div props. | #### TerminalBody (advanced) Deprecated — pass footer, header, fill, and pinScrollBottom to TerminalWindow instead. | Prop | Type | Default | Description | | --- | --- | --- | --- | | `footer` | `ReactNode` | `false` | Pinned footer slot — usually Input. | | `fill` | `boolean` | `false` | Stretch to fill a fixed-height window and scroll internally. | ### Message Submitted user message row, shown inside SessionContent. Source: `components/terminal-kit/session/message.tsx` #### Install ```bash npx shadcn@latest add "https://www.terminal-kit.com/r/message.json" ``` #### Example ```tsx import { Message, SessionContent, TerminalWindow } from "@/components/terminal-kit" Make the primary button feel more responsive on hover ``` #### API #### Message Prompt symbol shown before the text. | Prop | Type | Default | Description | | --- | --- | --- | --- | | `prompt` | `string` | — | Prompt symbol shown before the text. | | `className` | `string` | — | CSS classes on the message row (e.g. override bg or text color). | | `children` (required) | `ReactNode` | — | Prompt text content. | | `...props` | `HTMLDivElement` | — | Standard div props. | ### Session Content Transcript container with optional streaming reveal and auto-scroll. Source: `components/terminal-kit/session/session-content.tsx` #### Install ```bash npx shadcn@latest add "https://www.terminal-kit.com/r/session-content.json" ``` #### Example ```tsx import { EditBlock, Message, SessionContent, StreamText, ThinkingIndicator, TerminalWindow, } from "@/components/terminal-kit" /make-interfaces-feel-better I'll tighten the primary button hover — easing opacity and adding a motion-safe press scale. ``` #### API #### SessionContent Transcript container with optional streaming reveal and auto-scroll. With streaming enabled, SessionContent reveals one child at a time and waits between lines. Only StreamText accepts sessionPause (typed override for that wait). Other children get automatic pauses — e.g. Message ~280ms, EditBlock (file prop) ~520ms, StreamText via estimateStreamDurationMs(text). Use ThinkingIndicator sessionTail to pin live thinking at the tail (skips the queue). | Prop | Type | Default | Description | | --- | --- | --- | --- | | `children` (required) | `ReactNode` | — | Transcript lines to render inside the session. | | `streaming` | `boolean` | `false` | Reveal children one at a time like a live agent session. | | `autoScroll` | `boolean` | `false` | Keep the scroll container pinned to the bottom. | | `stagger` | `number` | `0.45` | Seconds between streamed items when streaming is enabled. | | `delay` | `number` | `0` | Seconds before the first streamed item appears. | | `resetKey` | `string` | — | Change to replay the enter sequence. | | `className` | `string` | — | Additional CSS classes on the session root. | ### Input Bottom prompt input with blinking cursor and status metadata. Source: `components/terminal-kit/input/input.tsx` #### Install ```bash npx shadcn@latest add "https://www.terminal-kit.com/r/input.json" ``` #### Example ```tsx import { Input, InputLevelMeta, TerminalWindow } from "@/components/terminal-kit" } /> } /> ``` #### API #### Input Bottom prompt input with blinking cursor and footer metadata. ShellInput is a deprecated alias. | Prop | Type | Default | Description | | --- | --- | --- | --- | | `value` | `string` | — | Controlled input value. | | `onSubmit` | `(value: string) => void` | — | Called on Enter without Shift. | | `prompt` | `string` | — | | | `status` | `ReactNode` | — | Inline right metadata on grok compact layout; shown in the default composer meta tray (with metaRight) or Claude stacked footer when metaRight is omitted. | | `metaLeft` | `ReactNode` | — | Footer hint on the left — default composer and Claude stacked layouts. | | `metaRight` | `ReactNode` | — | Footer hint on the right — default composer and Claude stacked layouts. | | `placeholder` | `string` | — | Placeholder text when empty. | | `disabled` | `boolean` | `false` | Disable input interaction. | | `showCursor` | `boolean` | `false` | Keep the blinking cursor visible even when unfocused. | | `layout` | `` | — | auto — composer with footer meta on default, inline box on grok, stacked lines on Claude. inline keeps the bordered box with status on the right. | | `className` | `string` | — | Additional CSS classes. | #### InputLevelMeta (advanced) Lightning + level label for default composer metaRight (defaults to max). | Prop | Type | Default | Description | | --- | --- | --- | --- | | `children` | `ReactNode` | — | Level label beside the icon. | ### Question Prompt Multiple-choice Q&A prompt for ambiguous tasks. Source: `components/terminal-kit/agent/question-prompt.tsx` #### Install ```bash npx shadcn@latest add "https://www.terminal-kit.com/r/question-prompt.json" ``` #### Example ```tsx import { QuestionPrompt, TerminalWindow } from "@/components/terminal-kit" // Active ask_user UI — pin to TerminalWindow footer, not SessionContent. console.log(id)} /> } /> ``` #### API #### QuestionPrompt Multiple-choice ask_user UI. Mount the active prompt on TerminalWindow footer (swap with Input while open) — do not put it in SessionContent. After the user answers, render QuestionAnswerSummary in the transcript (or pass selectedId on QuestionPrompt in footer if you keep it there). | Prop | Type | Default | Description | | --- | --- | --- | --- | | `question` (required) | `string` | — | Question text shown above options. | | `selectedId` | `string` | — | When set, renders QuestionAnswerSummary instead of the prompt. | | `answer` | `string` | — | Answer text for the summary when selectedId is set. Defaults to the selected option label. | | `answeredHeader` | `string` | — | Header line for the Claude-style answered summary. | | `layout` | `` | — | auto — Claude flat list, default soft card, grok numbered panel. bubble/card/panel force a specific style. | | `trailingOption` | `QuestionOption` | — | Optional row below a divider after the custom option (e.g. Chat about this). Claude layout only. | | `initialActiveIndex` | `number` | `0` | Highlight this option on mount (0-based). | | `index` | `number` | `1` | Current question index for multi-step flows (1-based). | | `total` | `number` | `1` | Total questions in a multi-step flow. | | `customOptionLabel` | `string` | — | Label for the built-in custom answer row. | | `customOptionPlaceholder` | `string` | — | Placeholder for the custom answer field. | | `onSelect` | `(id: string, customValue?: string) => void` | — | Called when the user selects an option or submits a custom answer. | | `className` | `string` | — | Additional CSS classes. | ### Edit Block File edit block with line numbers and diff highlights. Source: `components/terminal-kit/agent/edit-block.tsx` #### Install ```bash npx shadcn@latest add "https://www.terminal-kit.com/r/edit-block.json" ``` #### Example ```tsx import { EditBlock, TerminalWindow } from "@/components/terminal-kit" const code = `export async function checkout(cart: Cart) { const total = calculateTotal(cart) if (total <= 0) { throw new ValidationError("Total must be positive") } return stripe.charges.create({ amount: total, currency: "usd" }) }` ``` #### API #### EditBlock File path shown in the header. | Prop | Type | Default | Description | | --- | --- | --- | --- | | `file` (required) | `string` | — | File path shown in the header. | | `startLine` | `number` | `1` | Line number for the first line of code. | | `highlightLines` | `number[]` | — | Line numbers to highlight as changed. | | `removed` | `EditBlockRemovedLine[]` | — | Removed lines rendered inline in the diff view. | | `removedLines` | `number[]` | — | Line numbers to render with removed-line styling. | | `addedCount` | `number` | — | Override the added-line count shown in the header. | | `removedCount` | `number` | — | Override the removed-line count shown in the header. | | `defaultExpanded` | `boolean` | `false` | Whether the code block starts expanded. | | `collapsedMaxLines` | `number` | `5` | Max visible lines when collapsed. | | `layout` | `` | — | auto — flat on Claude, compact inline on grok, bordered panel on default. | | `className` | `string` | — | Additional CSS classes. | | `...props` | `HTMLDivElement` | — | Standard div props. | ### Thinking Indicator Animated dot-matrix thinking state while the agent works. Source: `components/terminal-kit/ui/thinking-indicator.tsx` #### Install ```bash npx shadcn@latest add "https://www.terminal-kit.com/r/thinking-indicator.json" ``` #### Example ```tsx import { ThinkingIndicator, TerminalWindow } from "@/components/terminal-kit" Thinking… ``` #### API #### ThinkingIndicator Animated thinking state while the agent works. BusyIndicator is a deprecated alias. | Prop | Type | Default | Description | | --- | --- | --- | --- | | `label` | `string` | — | Status text. Alias for children when both are strings. | | `children` | `ReactNode` | — | Status content. Overrides label when provided. | | `variant` | `` | — | auto — ASCII symbol loop on Claude, blinking cursor on grok, dot matrix on default. ascii, cursor, and dots force a specific animation. | | `tone` | `` | — | Text emphasis — active uses brighter foreground. | | `dotProps` | `Partial` | — | Customize the dot animation — speed, trail, pulseCenter, dotSize, color. | | `asciiProps` | `Partial` | — | Customize the Claude-style symbol loop — frames, speed, color. Defaults to CLAUDE_THINKING_FRAMES. | | `sessionTail` | `boolean` | — | SessionContent integration — skip the streaming queue and render at the transcript tail (e.g. live thinking). Not used by ThinkingIndicator itself; SessionContent reads this off the child element. | | `className` | `string` | — | Additional CSS classes. | #### TerminalDotMatrix (advanced) Low-level 3×3 dot animation primitive used by ThinkingIndicator. | Prop | Type | Default | Description | | --- | --- | --- | --- | | `speed` | `number` | `120` | Milliseconds per ring step (lower is faster). | | `trail` | `number` | `4` | Number of cells that fade behind the comet head. | | `pulseCenter` | `boolean` | `true` | Pulse the center dot as a core. Pass false to leave it dark. | | `dotSize` | `number` | `2` | Dot size in pixels. | | `color` | `string` | — | Override the dot color (any CSS color). Defaults to the tone color. | | `tone` | `` | — | Color preset used when color is not set. | | `frames` | `ReadonlyArray` | — | Custom animation — a list of frames, each 9 opacity values (0–1) for the 3x3 grid, row-major. Overrides the built-in spin. | ### Stream Text Agent text that types out or fades in word-by-word, plus the useTextStream hook. Source: `components/terminal-kit/ui/stream-text.tsx` #### Install ```bash npx shadcn@latest add "https://www.terminal-kit.com/r/stream-text.json" ``` #### API #### StreamText Simulated streaming agent text rendered inside a TerminalLine. | Prop | Type | Default | Description | | --- | --- | --- | --- | | `children` (required) | `string` | — | The text to stream. Must be a plain string. | | `mode` | `` | — | Plain reveals word-by-word with no transition. Fade adds a fade-in per word. | | `speed` | `number` | `26` | 1 (slowest) to 100 (fastest). Controls delay between words. | | `enabled` | `boolean` | `true` | Start streaming. Set false to hold until revealed. | | `onComplete` | `() => void` | — | Called once the full text is revealed. | | `sessionPause` | `number` | — | SessionContent integration only — ms to wait before the next line when streaming. Put this on StreamText only (other components do not type it). SessionContent reads it from the child element; StreamText itself ignores it. Defaults to estimateStreamDurationMs(text). | | `segmentDelay` | `number` | — | Delay between word segments in ms (overrides speed). | | `fadeDuration` | `number` | — | Fade-in duration per segment in ms (overrides speed). | | `className` | `string` | — | CSS classes on the TerminalLine wrapper (e.g. text-[var(--terminal-dim)] for agent prose). | #### useTextStream (advanced) Headless hook behind StreamText. Returns segments, isComplete, and timing. | Prop | Type | Default | Description | | --- | --- | --- | --- | | `text` (required) | `string` | — | The text to stream. | | `mode` | `` | — | Plain reveals word-by-word with no transition. Fade adds a fade-in per word. | | `speed` | `number` | `20` | 1 (slowest) to 100 (fastest). Controls delay between words. | | `segmentDelay / fadeDuration` | `number` | — | Fine-grained timing overrides (override speed). |