13 / 37 · Tool use
Terminal block
Command output that streams line by line and ends with an exit status.
pnpm vitest run composer
RUN v4.0.5 /apps/docs
Installation
npx shadcn@latest add "@assistant-ui/elements-terminal-block"
Usage
import { TerminalBlock } from "@/components/elements/terminal-block";
<TerminalBlock
command="pnpm test"
lines={["PASS thread.test.ts", "Tests: 2 passed"]}
visibleCount={2}
done
/>Props
command*stringCommand string shown in the terminal header.
lines*readonly string[]Output lines available to stream into the body.
visibleCount*numberHow many output lines from the start are currently shown.
variant"paper" | "ink"paper matches the surrounding surfaces; ink renders the classic dark terminal slab.
done*booleanWhen true the header shows exit 0; otherwise a spinner keeps spinning.
classNamestringExtra classes merged onto the root.
Source
terminal-block.tsx"use client";
import { CheckIcon, Loader2Icon } from "lucide-react";
import { cn } from "@/lib/utils";
import { mono, paper } from "./surfaces";
export function TerminalBlock({
command,
lines,
visibleCount,
done,
variant = "paper",
className,
}: {
command: string;
lines: readonly string[];
visibleCount: number;
done: boolean;
variant?: "paper" | "ink";
className?: string;
}) {
const ink = variant === "ink";
return (
<div
className={cn(
ink
? "bg-foreground dark:bg-popover shadow-[0_12px_32px_-16px_rgba(0,0,0,0.35)] dark:shadow-none"
: paper,
"w-full max-w-md overflow-hidden rounded-2xl font-mono text-xs",
className,
)}
>
<div className="flex items-center justify-between px-4 pt-3 pb-1.5">
<span
className={cn(
ink
? "text-background/90 dark:text-foreground/90"
: "text-foreground/90",
)}
>
{command}
</span>
{done ? (
<div className="flex items-center gap-1">
<CheckIcon className="size-3 text-emerald-500" />
<span
className={cn(
mono,
ink
? "text-background/40 dark:text-foreground/40"
: "text-foreground/40",
)}
>
exit 0
</span>
</div>
) : (
<Loader2Icon
className={cn(
"size-3 animate-spin motion-reduce:animate-none",
ink
? "text-background/35 dark:text-foreground/35"
: "text-foreground/35",
)}
/>
)}
</div>
<div
className={cn(
"flex min-h-[8.5rem] flex-col gap-1 px-4 pt-1 pb-3.5",
ink
? "text-background/55 dark:text-foreground/50"
: "text-foreground/50",
)}
>
{lines.slice(0, visibleCount).map((line, i) => {
const isLast = i === lines.length - 1;
return (
<div
key={`${i}-${line}`}
className={cn(
"fade-in animate-in fill-mode-both duration-300",
isLast &&
(ink
? "text-background/90 dark:text-foreground/90"
: "text-foreground/90"),
)}
>
{line}
</div>
);
})}
{!done && (
<span
aria-hidden
className="inline-block h-3 w-1.5 animate-pulse bg-blue-500/70 motion-reduce:animate-none dark:bg-blue-400/70"
/>
)}
</div>
</div>
);
}