# 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). |