Installation
Complete project setup with react/base, then install these components:
pnpm exec frontfriend download trace code-block icon button message bubble -f react --primitive base Show application frames first. Keep the original stack text when a frame cannot be parsed. Each application frame in this example opens its source context; replace that disclosure with an editor link when your application can open files.
Usage
Complete example
import { Message, MessageContent } from "@/components/ui/message";
import { Bubble, BubbleContent } from "@/components/ui/bubble";
import {
Trace,
TraceCollapsible,
TraceCollapsibleTrigger,
TraceLine,
TraceIcon,
TraceTitle,
TraceCollapsibleContent,
} 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 stack = `TypeError: Cannot read properties of undefined (reading 'email')
at seedUsers (prisma/seed.ts:27:34)
at main (prisma/seed.ts:51:9)
at Module._compile (node:internal/modules/cjs/loader:1554:14)
at Object.transformer (node_modules/tsx/dist/register.cjs:3:1104)
at Module.load (node:internal/modules/cjs/loader:1289:32)
at process.processTicksAndRejections (node:internal/process/task_queues:105:5)`;
const sourcePreviews: Record<string, string> = {
"prisma/seed.ts:27":
"25 for (const user of fixtures) {\n26 const profile = user.profile\n27 await createUser(profile.email)\n28 }",
"prisma/seed.ts:51":
"49 async function main() {\n50 await connect()\n51 await seedUsers()\n52 }",
};
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,
};
export default function AiStackTraceExample() {
const { phase, replay } = useRun("complete");
const [error, ...lines] = stack.split("\n").map((line) => line.trim());
const frames = lines.map((line) => {
const match = line.match(/^at (.+) \((.+):(\d+):(\d+)\)$/);
return {
line,
name: match?.[1],
file: match?.[2],
lineNumber: match?.[3],
source: sourcePreviews[`${match?.[2]}:${match?.[3]}`] ?? line,
internal: line.includes("node_modules") || line.includes("node:"),
};
});
const appFrames = frames.filter((entry) => !entry.internal);
const internalFrames = frames.filter((entry) => entry.internal);
return (
<div style={frame}>
<Message align="end">
<MessageContent>
<Bubble align="end" variant="muted">
<BubbleContent>Reset the database and seed it.</BubbleContent>
</Bubble>
</MessageContent>
</Message>
<Message>
<MessageContent style={{ gap: 16, minWidth: 0 }}>
<Trace>
<TraceCollapsible>
<TraceCollapsibleTrigger>
<TraceLine>
<TraceIcon>
<Icon name="Terminal" />
</TraceIcon>
<TraceTitle>
Ran pnpm prisma migrate reset --skip-seed
</TraceTitle>
</TraceLine>
</TraceCollapsibleTrigger>
<TraceCollapsibleContent keepMounted>
<CodeBlock size="sm">
<CodeBlockContent wrap>
{
"Database reset successful\n\nmigrations/\n └─ 20260912_init/\n └─ 20260920_add_profiles/"
}
</CodeBlockContent>
</CodeBlock>
</TraceCollapsibleContent>
</TraceCollapsible>
{phase === "running" || phase === "pending" ? (
<TraceLine status={phase}>
<TraceIcon>
<Icon name="Terminal" />
</TraceIcon>
<TraceTitle>
{phase === "running" ? "Running" : "Waiting to run"} pnpm
prisma db seed
</TraceTitle>
</TraceLine>
) : (
<TraceCollapsible defaultOpen>
<TraceCollapsibleTrigger>
<TraceLine status="error">
<TraceIcon>
<Icon name="CircleAlert" />
</TraceIcon>
<TraceTitle>Failed: {error}</TraceTitle>
</TraceLine>
</TraceCollapsibleTrigger>
<TraceCollapsibleContent keepMounted>
<div style={{ display: "grid", gap: 8 }}>
<CodeBlock size="sm" variant="destructive">
<CodeBlockContent wrap>{error}</CodeBlockContent>
</CodeBlock>
<Trace>
{appFrames.map((entry) => (
<TraceCollapsible key={entry.line}>
<TraceCollapsibleTrigger>
<TraceLine>
<TraceIcon>
<Icon name="FileCode" />
</TraceIcon>
<TraceTitle>
{entry.name
? `${entry.name} in ${entry.file}:${entry.lineNumber}`
: entry.line}
</TraceTitle>
</TraceLine>
</TraceCollapsibleTrigger>
<TraceCollapsibleContent keepMounted>
<CodeBlock size="sm">
<CodeBlockContent wrap>
{entry.source}
</CodeBlockContent>
</CodeBlock>
</TraceCollapsibleContent>
</TraceCollapsible>
))}
<TraceCollapsible>
<TraceCollapsibleTrigger>
<TraceLine>
<TraceIcon>
<Icon name="Layers" />
</TraceIcon>
<TraceTitle>
{internalFrames.length} framework frames
</TraceTitle>
</TraceLine>
</TraceCollapsibleTrigger>
<TraceCollapsibleContent keepMounted>
<CodeBlock size="sm">
<CodeBlockContent wrap>
{internalFrames
.map((entry) => entry.line)
.join("\n")}
</CodeBlockContent>
</CodeBlock>
</TraceCollapsibleContent>
</TraceCollapsible>
</Trace>
</div>
</TraceCollapsibleContent>
</TraceCollapsible>
)}
</Trace>
{phase === "complete" && (
<p>
The migrations ran, but the seed failed in seedUsers. One fixture
has no profile, so profile.email is undefined.
</p>
)}
</MessageContent>
</Message>
<div>
<Button
size="sm"
variant="outline"
onClick={replay}
disabled={phase === "running"}
>
Replay
</Button>
</div>
</div>
);
} AI SDK
The adapter splits output-error errorText into the error message, application frames, and framework frames. Other states use the tool state mapping. Send stack details only in development: an onError callback in toUIMessageStreamResponse can return error.stack for an Error. Production errors should omit server paths.
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.
Server fragment for app/api/chat/route.ts.
import type { streamText } from "ai";
export function stackResponse(result: ReturnType<typeof streamText>, development: boolean) {
return result.toUIMessageStreamResponse({
onError: (error) => error instanceof Error
? development ? error.stack ?? error.message : error.message
: String(error),
});
}import type { UIMessage } from "ai";
import {
Trace,
TraceCollapsible,
TraceCollapsibleTrigger,
TraceLine,
TraceIcon,
TraceTitle,
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 SdkCommandMessage = UIMessage<
unknown,
{},
{
runCommand: {
input: {
command: string;
};
output: {
stdout: string;
exitCode: number;
};
};
}
>;
type SdkCommandPart = Extract<
SdkCommandMessage["parts"][number],
{
type: "tool-runCommand";
}
>;
function sdkCommandState(part: SdkCommandPart, streaming: boolean) {
const command = part.input?.command ?? "command";
const denied =
part.state === "output-denied" ||
(part.state === "approval-responded" && !part.approval.approved);
const finished = part.state === "output-available" && !part.preliminary;
const waiting = part.state === "approval-requested";
const approved =
part.state === "approval-responded" && part.approval.approved;
const failed =
part.state === "output-error" ||
(finished && part.output.exitCode !== 0) ||
(!denied && !finished && !waiting && !approved && !streaming);
const status = denied
? undefined
: waiting
? "pending"
: failed
? "error"
: finished
? undefined
: "running";
const title = `${denied ? "Denied" : waiting ? "Waiting to run" : failed ? "Failed to run" : finished ? "Ran" : "Running"} ${command}`;
const output =
part.state === "output-error"
? part.errorText
: part.state === "output-available"
? part.output.stdout
: undefined;
return { status, title, output } as const;
}
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 (
<TraceCollapsible open={expanded} onOpenChange={setOpen}>
<TraceCollapsibleTrigger>
<TraceLine status={status}>
<TraceIcon>
<Icon name={status === "error" ? "CircleAlert" : "Terminal"} />
</TraceIcon>
<TraceTitle>{title}</TraceTitle>
</TraceLine>
</TraceCollapsibleTrigger>
<TraceCollapsibleContent keepMounted>{children}</TraceCollapsibleContent>
</TraceCollapsible>
);
}
function SdkCommandRow({
part,
streaming,
}: {
part: SdkCommandPart;
streaming: boolean;
}) {
const view = sdkCommandState(part, streaming);
const output = view.output;
if (output !== undefined)
return (
<SdkPanel
title={view.title}
status={view.status}
initialOpen={view.status === "error" || view.status === "running"}
>
<CodeBlock
size="sm"
variant={view.status === "error" ? "destructive" : "default"}
>
<CodeBlockContent wrap style={{ maxHeight: 256 }}>
{output}
</CodeBlockContent>
</CodeBlock>
</SdkPanel>
);
return (
<TraceLine status={view.status}>
<TraceIcon>
<Icon name="Terminal" />
</TraceIcon>
<TraceTitle>{view.title}</TraceTitle>
</TraceLine>
);
}
function StackTraceSdkAdapter({
message,
streaming,
}: {
message: SdkCommandMessage;
streaming: boolean;
}) {
return (
<Trace>
{message.parts.map((part) => {
if (part.type !== "tool-runCommand") return null;
if (part.state !== "output-error")
return (
<SdkCommandRow
key={part.toolCallId}
part={part}
streaming={streaming}
/>
);
const [error, ...frames] = part.errorText
.split("\n")
.map((line) => line.trim())
.filter(Boolean);
const appFrames = frames.filter(
(line) => !/node_modules|node:internal/.test(line),
);
const frameworkFrames = frames.filter((line) =>
/node_modules|node:internal/.test(line),
);
return (
<SdkPanel
key={part.toolCallId}
title={error || part.errorText}
status="error"
initialOpen
>
<CodeBlock size="sm" variant="destructive">
<CodeBlockContent wrap>{error}</CodeBlockContent>
</CodeBlock>
<Trace>
{appFrames.map((line, index) => (
<TraceLine key={index}>
<TraceIcon>
<Icon name="FileCode" />
</TraceIcon>
<TraceTitle>{line}</TraceTitle>
</TraceLine>
))}
{frameworkFrames.length > 0 && (
<SdkPanel title={`${frameworkFrames.length} framework frames`}>
<CodeBlock size="sm">
<CodeBlockContent wrap>
{frameworkFrames.join("\n")}
</CodeBlockContent>
</CodeBlock>
</SdkPanel>
)}
</Trace>
</SdkPanel>
);
})}
</Trace>
);
}
const sdkMessage: SdkCommandMessage = {
id: "seed",
role: "assistant",
parts: [
{
type: "tool-runCommand",
toolCallId: "seed-command",
state: "output-error",
input: { command: "pnpm prisma db seed" },
errorText:
"TypeError: Cannot read properties of undefined (reading 'email')\n at seedUsers (prisma/seed.ts:27:18)\n at main (prisma/seed.ts:51:3)\n at processTicksAndRejections (node:internal/process/task_queues:95:5)",
},
],
};
export default function AiStackTraceExample() {
return (
<div style={frame}>
<StackTraceSdkAdapter message={sdkMessage} streaming={false} />
</div>
);
} Frames
import {
Trace,
TraceCollapsible,
TraceCollapsibleTrigger,
TraceLine,
TraceIcon,
TraceTitle,
TraceCollapsibleContent,
} from "@/components/ui/trace";
import { Icon } from "@/components/ui/icon";
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,
};
const namedStackTraceFramesSTACK = `TypeError: Cannot read properties of undefined (reading 'total')
at formatInvoice (app/lib/billing.ts:42:31)
at InvoiceRow (app/components/invoice-row.tsx:18:12)
at renderWithHooks (node_modules/react-dom/cjs/react-dom.development.js:14985:18)
at beginWork (node_modules/react-dom/cjs/react-dom.development.js:19049:16)
at performUnitOfWork (node_modules/react-dom/cjs/react-dom.development.js:22776:12)
at workLoopSync (node_modules/react-dom/cjs/react-dom.development.js:22707:5)`;
const namedStackTraceFramesLines = namedStackTraceFramesSTACK
.split("\n")
.map((line) => line.trim());
const namedStackTraceFramesError = namedStackTraceFramesLines[0];
function namedStackTraceFramesParseFrame(line: string) {
const match = line.match(/^at (.+) \((.+):(\d+):(\d+)\)$/);
return {
line,
name: match?.[1],
file: match?.[2],
lineNumber: match?.[3],
internal: line.includes("node_modules"),
};
}
const namedStackTraceFramesFrames = namedStackTraceFramesLines
.slice(1)
.map(namedStackTraceFramesParseFrame);
const namedStackTraceFramesAppFrames = namedStackTraceFramesFrames.filter(
(frame) => !frame.internal,
);
const namedStackTraceFramesInternalFrames = namedStackTraceFramesFrames.filter(
(frame) => frame.internal,
);
function StackTraceFramesExample() {
return (
<Trace>
<TraceCollapsible defaultOpen>
<TraceCollapsibleTrigger>
<TraceLine status="error">
<TraceIcon>
<Icon name="CircleX" />
</TraceIcon>
<TraceTitle>Failed: {namedStackTraceFramesError}</TraceTitle>
</TraceLine>
</TraceCollapsibleTrigger>
<TraceCollapsibleContent keepMounted>
<p>{namedStackTraceFramesError}</p>
<Trace>
{namedStackTraceFramesAppFrames.map((frame) => (
<a key={frame.line} href={`#${frame.file}:${frame.lineNumber}`}>
<TraceLine>
<TraceIcon>
<Icon name="FileCode" />
</TraceIcon>
<TraceTitle>
{frame.name} in {frame.file}:{frame.lineNumber}
</TraceTitle>
</TraceLine>
</a>
))}
<TraceCollapsible>
<TraceCollapsibleTrigger>
<TraceLine>
<TraceIcon>
<Icon name="Layers" />
</TraceIcon>
<TraceTitle>
{namedStackTraceFramesInternalFrames.length} framework
frames
</TraceTitle>
</TraceLine>
</TraceCollapsibleTrigger>
<TraceCollapsibleContent>
<ol>
{namedStackTraceFramesInternalFrames.map((frame) => (
<li key={frame.line}>{frame.line}</li>
))}
</ol>
</TraceCollapsibleContent>
</TraceCollapsible>
</Trace>
</TraceCollapsibleContent>
</TraceCollapsible>
</Trace>
);
}
export default function AiStackTraceExample() {
return (
<div style={frame}>
<StackTraceFramesExample />
</div>
);
} Components and API
This composition has no separate component API. Use the APIs for its installed components.
| Component | Target reference |
|---|---|
trace | Base UI API |
code-block | Base UI API |
icon | Base UI API |
button | Base UI API |
message | Base UI API |
bubble | Base UI API |