Installation
Complete project setup with react/radix, then install these components:
pnpm exec frontfriend download trace code-block icon button message bubble -f react --primitive radix Count results from the tests, not from the tool status. Open failed suites and failed tests by default. A skipped test is distinct from a test that is waiting to run. The example uses fixed test results and does not execute tests.
Usage
Complete example
import { Message, MessageContent } from "@/components/ui/message";
import { Bubble, BubbleContent } from "@/components/ui/bubble";
import {
Trace,
TraceCollapsible,
TraceCollapsibleTrigger,
TraceCollapsibleContent,
TraceLine,
TraceIcon,
TraceTitle,
} from "@/components/ui/trace";
import { Icon } from "@/components/ui/icon";
import { CodeBlock, CodeBlockContent } from "@/components/ui/code-block";
import { Button } from "@/components/ui/button";
import * as React from "react";
type Phase = "complete" | "running" | "error" | "pending";
function useRun(initial: Phase) {
const [phase, setPhase] = React.useState(initial);
const timer = React.useRef<ReturnType<typeof setTimeout> | undefined>(
undefined,
);
React.useEffect(() => {
clearTimeout(timer.current);
setPhase(initial);
}, [initial]);
React.useEffect(() => () => clearTimeout(timer.current), []);
function replay() {
clearTimeout(timer.current);
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
setPhase("complete");
return;
}
setPhase("running");
timer.current = setTimeout(() => setPhase("complete"), 3600);
}
return { phase, replay };
}
const frame: React.CSSProperties = {
width: "100%",
maxWidth: 384,
minWidth: 0,
minHeight: 288,
marginInline: "auto",
display: "flex",
flexDirection: "column",
gap: 16,
paddingBlock: 24,
fontSize: "var(--text-sm)",
lineHeight: 1.5,
};
function TracePanel({
trigger,
children,
open: controlledOpen,
defaultOpen = false,
onOpenChange,
...props
}: React.ComponentProps<typeof TraceCollapsible> & {
trigger: React.ReactNode;
}) {
const contentRef = React.useRef<HTMLDivElement>(null);
const [height, setHeight] = React.useState<number>();
const [localOpen, setLocalOpen] = React.useState(defaultOpen);
const expanded = controlledOpen ?? localOpen;
const [hasOpened, setHasOpened] = React.useState(expanded);
React.useEffect(() => {
if (expanded) setHasOpened(true);
}, [expanded]);
React.useLayoutEffect(() => {
const content = contentRef.current;
const inner = content?.firstElementChild;
if (!expanded || !content || !inner) return;
const measure = () => setHeight(content.scrollHeight);
measure();
const observer = new ResizeObserver(measure);
observer.observe(inner);
return () => observer.disconnect();
}, [expanded]);
return (
<TraceCollapsible
{...props}
open={expanded}
onOpenChange={(value) => {
if (!value && contentRef.current) {
setHeight(contentRef.current.getBoundingClientRect().height);
}
setLocalOpen(value);
onOpenChange?.(value);
}}
>
<TraceCollapsibleTrigger>{trigger}</TraceCollapsibleTrigger>
<TraceCollapsibleContent
ref={contentRef}
style={
height === undefined
? undefined
: ({
"--radix-collapsible-content-height": `${height}px`,
} as React.CSSProperties)
}
forceMount={hasOpened ? true : undefined}
inert={!expanded}
>
{children}
</TraceCollapsibleContent>
</TraceCollapsible>
);
}
type TestResult = {
name: string;
status: "passed" | "failed" | "skipped";
error?: string;
};
const suites: {
name: string;
tests: TestResult[];
}[] = [
{
name: "cart.test.ts",
tests: [
{ name: "adds an item", status: "passed" },
{ name: "removes an item", status: "passed" },
{ name: "applies a discount code", status: "passed" },
],
},
{
name: "payment.test.ts",
tests: [
{ name: "charges the saved card", status: "passed" },
{
name: "retries a declined card",
status: "failed",
error:
"Expected 2 charge attempts, received 1\n\n at src/payment.test.ts:58:22",
},
{ name: "refunds a payment", status: "skipped" },
],
},
];
function counts(tests: TestResult[]) {
const failed = tests.filter((test) => test.status === "failed").length;
const skipped = tests.filter((test) => test.status === "skipped").length;
return `${tests.length} tests${failed ? `, ${failed} failed` : ""}${skipped ? `, ${skipped} skipped` : ""}`;
}
export default function AiTestResultsExample() {
const { phase, replay } = useRun("complete");
const [open, setOpen] = React.useState<Record<string, boolean>>({});
function expand(id: string, value: boolean) {
setOpen((current) => ({ ...current, [id]: value }));
}
return (
<div style={frame}>
<Message align="end">
<MessageContent>
<Bubble align="end" variant="muted">
<BubbleContent>Run the checkout tests.</BubbleContent>
</Bubble>
</MessageContent>
</Message>
<Message>
<MessageContent style={{ gap: 16, minWidth: 0 }}>
<Trace>
{phase === "complete" ? (
<TracePanel
open={
open.run ??
suites.some((suite) =>
suite.tests.some((test) => test.status === "failed"),
)
}
onOpenChange={(value) => expand("run", value)}
trigger={
<TraceLine
status={
suites.some((suite) =>
suite.tests.some((test) => test.status === "failed"),
)
? "error"
: undefined
}
>
<TraceIcon>
<Icon name="FlaskConical" />
</TraceIcon>
<TraceTitle>
Ran {counts(suites.flatMap((suite) => suite.tests))}
</TraceTitle>
</TraceLine>
}
>
<Trace>
{suites.map((suite) => (
<TracePanel
key={suite.name}
open={
open[suite.name] ??
suite.tests.some((test) => test.status === "failed")
}
onOpenChange={(value) => expand(suite.name, value)}
trigger={
<TraceLine
status={
suite.tests.some((test) => test.status === "failed")
? "error"
: undefined
}
>
<TraceIcon>
<Icon name="FileCode" />
</TraceIcon>
<TraceTitle>
{suite.name}, {counts(suite.tests)}
</TraceTitle>
</TraceLine>
}
>
<Trace>
{suite.tests.map((test) =>
test.status === "failed" ? (
<TracePanel
key={test.name}
open={open[test.name] ?? test.status === "failed"}
onOpenChange={(value) => expand(test.name, value)}
trigger={
<TraceLine status="error">
<TraceIcon>
<Icon name="X" />
</TraceIcon>
<TraceTitle>{test.name}</TraceTitle>
</TraceLine>
}
>
<CodeBlock size="sm">
<CodeBlockContent wrap>
{test.error}
</CodeBlockContent>
</CodeBlock>
</TracePanel>
) : (
<TraceLine key={test.name}>
<TraceIcon>
<Icon
name={
test.status === "skipped"
? "CircleDashed"
: "Check"
}
/>
</TraceIcon>
<TraceTitle>
{test.status === "skipped" ? "Skipped " : ""}
{test.name}
</TraceTitle>
</TraceLine>
),
)}
</Trace>
</TracePanel>
))}
</Trace>
</TracePanel>
) : (
<TraceLine status={phase}>
<TraceIcon>
<Icon name="FlaskConical" />
</TraceIcon>
<TraceTitle>
{`${
phase === "running"
? "Running"
: phase === "pending"
? "Waiting to run"
: "Failed to run"
} pnpm test checkout`}
</TraceTitle>
</TraceLine>
)}
</Trace>
{phase === "complete" && (
<p>
One test failed. The payment client does not retry a declined
card, so the second charge attempt never happens.
</p>
)}
</MessageContent>
</Message>
<div>
<Button
size="sm"
variant="outline"
onClick={replay}
disabled={phase === "running"}
>
Replay
</Button>
</div>
</div>
);
} AI SDK
The runTests tool returns suites with stable suite and test IDs, names, passed/failed/skipped status, and optional error text. The adapter derives counts from part.output.suites and opens failed suites. Tool execution errors use part.errorText; failed assertions use each test error.
The basic example runs without a provider. For the AI SDK adapter example only, install the SDK types:
pnpm add ai
Pass a message from your chat client to the adapter. The supplied message does not call a provider.
import type { UIMessage } from "ai";
import {
Trace,
TraceLine,
TraceIcon,
TraceTitle,
TraceCollapsible,
TraceCollapsibleTrigger,
TraceCollapsibleContent,
} from "@/components/ui/trace";
import { Icon } from "@/components/ui/icon";
import { CodeBlock, CodeBlockContent } from "@/components/ui/code-block";
import * as React from "react";
const frame: React.CSSProperties = {
width: "100%",
maxWidth: 384,
minWidth: 0,
minHeight: 288,
marginInline: "auto",
display: "flex",
flexDirection: "column",
gap: 16,
paddingBlock: 24,
fontSize: "var(--text-sm)",
lineHeight: 1.5,
};
type SdkTest = {
id: string;
name: string;
status: "passed" | "failed" | "skipped";
error?: string;
};
type SdkSuite = {
id: string;
name: string;
tests: SdkTest[];
};
type SdkTestMessage = UIMessage<
unknown,
{},
{
runTests: {
input: {
command: string;
};
output: {
suites: SdkSuite[];
};
};
}
>;
function TracePanel({
trigger,
children,
open: controlledOpen,
defaultOpen = false,
onOpenChange,
...props
}: React.ComponentProps<typeof TraceCollapsible> & {
trigger: React.ReactNode;
}) {
const contentRef = React.useRef<HTMLDivElement>(null);
const [height, setHeight] = React.useState<number>();
const [localOpen, setLocalOpen] = React.useState(defaultOpen);
const expanded = controlledOpen ?? localOpen;
const [hasOpened, setHasOpened] = React.useState(expanded);
React.useEffect(() => {
if (expanded) setHasOpened(true);
}, [expanded]);
React.useLayoutEffect(() => {
const content = contentRef.current;
const inner = content?.firstElementChild;
if (!expanded || !content || !inner) return;
const measure = () => setHeight(content.scrollHeight);
measure();
const observer = new ResizeObserver(measure);
observer.observe(inner);
return () => observer.disconnect();
}, [expanded]);
return (
<TraceCollapsible
{...props}
open={expanded}
onOpenChange={(value) => {
if (!value && contentRef.current) {
setHeight(contentRef.current.getBoundingClientRect().height);
}
setLocalOpen(value);
onOpenChange?.(value);
}}
>
<TraceCollapsibleTrigger>{trigger}</TraceCollapsibleTrigger>
<TraceCollapsibleContent
ref={contentRef}
style={
height === undefined
? undefined
: ({
"--radix-collapsible-content-height": `${height}px`,
} as React.CSSProperties)
}
forceMount={hasOpened ? true : undefined}
inert={!expanded}
>
{children}
</TraceCollapsibleContent>
</TraceCollapsible>
);
}
function SdkPanel({
title,
status,
children,
initialOpen = false,
}: {
title: string;
status?: "running" | "pending" | "error";
children: React.ReactNode;
initialOpen?: boolean;
}) {
const [open, setOpen] = React.useState<boolean>();
const expanded = open ?? initialOpen;
return (
<TracePanel
open={expanded}
onOpenChange={setOpen}
trigger={
<TraceLine status={status}>
<TraceIcon>
<Icon name={status === "error" ? "CircleAlert" : "Terminal"} />
</TraceIcon>
<TraceTitle>{title}</TraceTitle>
</TraceLine>
}
>
{children}
</TracePanel>
);
}
function TestResultsSdkAdapter({
message,
streaming,
}: {
message: SdkTestMessage;
streaming: boolean;
}) {
return (
<Trace>
{message.parts.map((part) => {
if (part.type !== "tool-runTests") return null;
if (part.state !== "output-available" || part.preliminary) {
const pending = part.state === "approval-requested";
const denied =
part.state === "output-denied" ||
(part.state === "approval-responded" && !part.approval.approved);
const failed =
part.state === "output-error" ||
(!pending && !denied && !streaming);
const title = `${denied ? "Denied" : pending ? "Waiting to run" : failed ? "Failed to run" : "Running"} ${part.input?.command ?? "tests"}`;
return (
<div key={part.toolCallId}>
<TraceLine
status={
pending
? "pending"
: failed
? "error"
: denied
? undefined
: "running"
}
>
<TraceIcon>
<Icon name="FlaskConical" />
</TraceIcon>
<TraceTitle>{title}</TraceTitle>
</TraceLine>
{part.state === "output-error" && (
<CodeBlock size="sm" variant="destructive">
<CodeBlockContent wrap>{part.errorText}</CodeBlockContent>
</CodeBlock>
)}
</div>
);
}
const tests = part.output.suites.flatMap((suite) => suite.tests),
failed = tests.filter((test) => test.status === "failed").length,
skipped = tests.filter((test) => test.status === "skipped").length;
return (
<SdkPanel
key={part.toolCallId}
title={`Ran ${tests.length} tests${failed ? `, ${failed} failed` : ""}${skipped ? `, ${skipped} skipped` : ""}`}
status={failed ? "error" : undefined}
initialOpen={failed > 0}
>
<Trace>
{part.output.suites.map((suite) => {
const failed = suite.tests.filter(
(test) => test.status === "failed",
).length;
return (
<SdkPanel
key={suite.id}
title={`${suite.name}, ${suite.tests.length} tests${failed ? `, ${failed} failed` : ""}`}
status={failed ? "error" : undefined}
initialOpen={failed > 0}
>
<Trace>
{suite.tests.map((test) =>
test.error ? (
<SdkPanel
key={test.id}
title={test.name}
status={
test.status === "failed" ? "error" : undefined
}
initialOpen
>
<CodeBlock size="sm" variant="destructive">
<CodeBlockContent wrap>
{test.error}
</CodeBlockContent>
</CodeBlock>
</SdkPanel>
) : (
<TraceLine
key={test.id}
status={
test.status === "failed" ? "error" : undefined
}
>
<TraceIcon>
<Icon
name={
test.status === "skipped"
? "CircleDashed"
: test.status === "failed"
? "X"
: "Check"
}
/>
</TraceIcon>
<TraceTitle>
{test.status === "skipped"
? `Skipped ${test.name}`
: test.name}
</TraceTitle>
</TraceLine>
),
)}
</Trace>
</SdkPanel>
);
})}
</Trace>
</SdkPanel>
);
})}
</Trace>
);
}
const sdkMessage: SdkTestMessage = {
id: "tests",
role: "assistant",
parts: [
{
type: "tool-runTests",
toolCallId: "checkout-tests",
state: "output-available",
input: { command: "pnpm test" },
output: {
suites: [
{
id: "cart",
name: "cart.test.ts",
tests: [
{
id: "total",
name: "calculates the order total",
status: "passed",
},
],
},
{
id: "payment",
name: "payment.test.ts",
tests: [
{
id: "retry",
name: "retries a declined card",
status: "failed",
error:
"Expected 2 charge attempts, received 1\n at src/payment.test.ts:58:22",
},
{
id: "refund",
name: "refunds a payment",
status: "skipped",
},
],
},
],
},
},
],
};
export default function AiTestResultsExample() {
return (
<div style={frame}>
<TestResultsSdkAdapter message={sdkMessage} streaming={false} />
</div>
);
} Suites
import {
Trace,
TraceCollapsible,
TraceCollapsibleTrigger,
TraceCollapsibleContent,
TraceLine,
TraceIcon,
TraceTitle,
} from "@/components/ui/trace";
import { Icon } from "@/components/ui/icon";
import { CodeBlock, CodeBlockContent } from "@/components/ui/code-block";
import * as React from "react";
const frame: React.CSSProperties = {
width: "100%",
maxWidth: 384,
minWidth: 0,
minHeight: 288,
marginInline: "auto",
display: "flex",
flexDirection: "column",
gap: 16,
paddingBlock: 24,
fontSize: "var(--text-sm)",
lineHeight: 1.5,
};
type Test = {
name: string;
status: "passed" | "failed" | "skipped";
error?: string;
};
function namedTestResultsSuitesCountTests(tests: Test[]) {
return {
total: tests.length,
failed: tests.filter((test) => test.status === "failed").length,
skipped: tests.filter((test) => test.status === "skipped").length,
};
}
const namedTestResultsSuitesSuites: {
name: string;
tests: Test[];
}[] = [
{
name: "auth.test.ts",
tests: [
{
name: "logs in with valid credentials",
status: "passed",
},
{
name: "rejects an invalid password",
status: "passed",
},
{
name: "refreshes an expired token",
status: "passed",
},
],
},
{
name: "user-api.test.ts",
tests: [
{
name: "creates a user",
status: "passed",
},
{
name: "updates a user profile",
status: "failed",
error: `Expected status 200, received 500
at Object.<anonymous> (src/user-api.test.ts:45:12)`,
},
{
name: "deletes a user",
status: "skipped",
},
],
},
{
name: "db.test.ts",
tests: [
{
name: "connects to the database",
status: "passed",
},
{
name: "times out a slow query",
status: "failed",
error: `Exceeded timeout of 5000 ms for a test.
at Object.<anonymous> (src/db.test.ts:22:3)`,
},
],
},
];
function TracePanel({
trigger,
children,
open: controlledOpen,
defaultOpen = false,
onOpenChange,
...props
}: React.ComponentProps<typeof TraceCollapsible> & {
trigger: React.ReactNode;
}) {
const contentRef = React.useRef<HTMLDivElement>(null);
const [height, setHeight] = React.useState<number>();
const [localOpen, setLocalOpen] = React.useState(defaultOpen);
const expanded = controlledOpen ?? localOpen;
const [hasOpened, setHasOpened] = React.useState(expanded);
React.useEffect(() => {
if (expanded) setHasOpened(true);
}, [expanded]);
React.useLayoutEffect(() => {
const content = contentRef.current;
const inner = content?.firstElementChild;
if (!expanded || !content || !inner) return;
const measure = () => setHeight(content.scrollHeight);
measure();
const observer = new ResizeObserver(measure);
observer.observe(inner);
return () => observer.disconnect();
}, [expanded]);
return (
<TraceCollapsible
{...props}
open={expanded}
onOpenChange={(value) => {
if (!value && contentRef.current) {
setHeight(contentRef.current.getBoundingClientRect().height);
}
setLocalOpen(value);
onOpenChange?.(value);
}}
>
<TraceCollapsibleTrigger>{trigger}</TraceCollapsibleTrigger>
<TraceCollapsibleContent
ref={contentRef}
style={
height === undefined
? undefined
: ({
"--radix-collapsible-content-height": `${height}px`,
} as React.CSSProperties)
}
forceMount={hasOpened ? true : undefined}
inert={!expanded}
>
{children}
</TraceCollapsibleContent>
</TraceCollapsible>
);
}
function namedTestResultsSuitesFormatCounts({
total,
failed,
skipped,
}: ReturnType<typeof namedTestResultsSuitesCountTests>) {
return [
`${total} tests`,
failed > 0 && `${failed} failed`,
skipped > 0 && `${skipped} skipped`,
]
.filter(Boolean)
.join(", ");
}
type TestResult = {
name: string;
status: "passed" | "failed" | "skipped";
error?: string;
};
function counts(tests: TestResult[]) {
const failed = tests.filter((test) => test.status === "failed").length;
const skipped = tests.filter((test) => test.status === "skipped").length;
return `${tests.length} tests${failed ? `, ${failed} failed` : ""}${skipped ? `, ${skipped} skipped` : ""}`;
}
function NamedTestResultsSuitesTestRow({ test }: { test: Test }) {
if (test.status === "failed") {
return (
<TracePanel
trigger={
<TraceLine status="error">
<TraceIcon>
<Icon name="X" />
</TraceIcon>
<TraceTitle>{test.name}</TraceTitle>
</TraceLine>
}
>
<CodeBlock size="sm">
<CodeBlockContent
wrap
style={{
maxHeight: 256,
}}
>
{test.error}
</CodeBlockContent>
</CodeBlock>
</TracePanel>
);
}
return (
<TraceLine>
<TraceIcon>
{test.status === "skipped" ? (
<Icon name="CircleDashed" />
) : (
<Icon name="Check" />
)}
</TraceIcon>
<TraceTitle>
{test.status === "skipped" ? `Skipped ${test.name}` : test.name}
</TraceTitle>
</TraceLine>
);
}
function TestResultsSuitesExample() {
const summary = namedTestResultsSuitesCountTests(
namedTestResultsSuitesSuites.flatMap((suite) => suite.tests),
);
return (
<Trace>
<TracePanel
defaultOpen
trigger={
<TraceLine status={summary.failed > 0 ? "error" : undefined}>
<TraceIcon>
<Icon name="FlaskConical" />
</TraceIcon>
<TraceTitle>
Ran {namedTestResultsSuitesFormatCounts(summary)}
</TraceTitle>
</TraceLine>
}
>
<Trace>
{namedTestResultsSuitesSuites.map((suite) => {
const counts = namedTestResultsSuitesCountTests(suite.tests);
return (
<TracePanel
key={suite.name}
defaultOpen={counts.failed > 0}
trigger={
<TraceLine status={counts.failed > 0 ? "error" : undefined}>
<TraceIcon>
<Icon name="FileCode" />
</TraceIcon>
<TraceTitle>
{`${suite.name}, ${namedTestResultsSuitesFormatCounts(counts)}`}
</TraceTitle>
</TraceLine>
}
>
<Trace>
{suite.tests.map((test) => (
<NamedTestResultsSuitesTestRow
key={test.name}
test={test}
/>
))}
</Trace>
</TracePanel>
);
})}
</Trace>
</TracePanel>
</Trace>
);
}
export default function AiTestResultsExample() {
return (
<div style={frame}>
<TestResultsSuitesExample />
</div>
);
} Components and API
This composition has no separate component API. Use the APIs for its installed components.
| Component | Target reference |
|---|---|
trace | Radix UI API |
code-block | Radix UI API |
icon | Radix UI API |
button | Radix UI API |
message | Radix UI API |
bubble | Radix UI API |