Meet Frontfriend.Your brand. Your rules. Your agents, connected.Explore what’s new

Reasoning

A collapsible view of the agent’s thinking that follows the reasoning stream and can show its duration.

Interactive preview

The shared preview uses React Base. The source below uses Base UI. Each example runs with local data.

Installation

Complete project setup with react/base, then install these components:

pnpm exec frontfriend download trace collapsible message bubble button icon -f react --primitive base

Reasoning opens while thinking and closes when the stream ends. A user’s open or closed choice takes priority. An empty reasoning part is a plain line until text arrives. Supply the optional duration from message metadata.

Usage

Complete example

import { Message, MessageContent } from "@/components/ui/message";
import { Bubble, BubbleContent } from "@/components/ui/bubble";
import {
  Trace,
  TraceLine,
  TraceIcon,
  TraceTitle,
  TraceDescription,
  TraceCollapsible,
  TraceCollapsibleTrigger,
  TraceCollapsibleContent,
} from "@/components/ui/trace";
import { Icon } from "@/components/ui/icon";
import { Button } from "@/components/ui/button";
import * as React from "react";
const REASONING =
  "The deploy log has three phases. Install takes 38s and next build takes 52s, both normal for this project.\n\nImage optimization takes 3m 50s. It reruns on every deploy because its cache key includes the commit hash, so the cache never hits.";
const canvasStyle: 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 ReasoningLine({
  text,
  running,
  duration,
  defaultOpen,
  showIcon,
}: {
  text: string;
  running: boolean;
  duration?: number;
  defaultOpen?: boolean;
  showIcon?: boolean;
}) {
  const [open, setOpen] = React.useState<boolean | undefined>(defaultOpen);
  const line = (
    <TraceLine status={running ? "running" : undefined}>
      {showIcon && (
        <TraceIcon>
          <Icon name="CircleCheck" />
        </TraceIcon>
      )}
      <TraceTitle>{running ? "Thinking" : "Thought"}</TraceTitle>
      {!running && duration !== undefined && (
        <TraceDescription>{duration}s</TraceDescription>
      )}
    </TraceLine>
  );
  if (!text.length) return line;
  return (
    <TraceCollapsible open={open ?? running} onOpenChange={setOpen}>
      <TraceCollapsibleTrigger>{line}</TraceCollapsibleTrigger>
      <TraceCollapsibleContent>
        <div style={{ whiteSpace: "pre-wrap" }}>{text}</div>
      </TraceCollapsibleContent>
    </TraceCollapsible>
  );
}
const ANSWER =
  "Image optimization is the bottleneck. Key its cache on the lockfile and the image directory instead of the commit hash, and the deploy should take under two minutes.";
export default function AiReasoningExample() {
  const [run, setRun] = React.useState(0);
  const [position, setPosition] = React.useState(REASONING.length);
  const running = position < REASONING.length;
  React.useEffect(() => {
    if (!running) return;
    const timer = setTimeout(
      () => setPosition((value) => Math.min(value + 4, REASONING.length)),
      80,
    );
    return () => clearTimeout(timer);
  }, [position, running]);
  const parts = [
    {
      type: "reasoning" as const,
      text: REASONING.slice(0, position),
      state: running ? "streaming" : "done",
    },
  ];
  return (
    <div style={canvasStyle}>
      <Message align="end">
        <MessageContent>
          <Bubble align="end" variant="muted">
            <BubbleContent>Why is the checkout deploy so slow?</BubbleContent>
          </Bubble>
        </MessageContent>
      </Message>
      <Message>
        <MessageContent style={{ gap: 16 }}>
          <Trace>
            {parts
              .filter((part) => part.type === "reasoning")
              .map((part, index) => (
                <ReasoningLine
                  key={`${run}-${index}`}
                  text={part.text}
                  running={part.state === "streaming"}
                  duration={5}
                  defaultOpen={run === 0 ? true : undefined}
                />
              ))}
          </Trace>
          {!running && <p>{ANSWER}</p>}
        </MessageContent>
      </Message>
      <Button
        variant="outline"
        size="sm"
        style={{ marginTop: "auto", alignSelf: "flex-start" }}
        disabled={running}
        onClick={() => {
          setRun((value) => value + 1);
          setPosition(0);
        }}
      >
        Replay
      </Button>
    </div>
  );
}

Duration

import {
  Trace,
  TraceLine,
  TraceIcon,
  TraceTitle,
  TraceDescription,
  TraceCollapsible,
  TraceCollapsibleTrigger,
  TraceCollapsibleContent,
} from "@/components/ui/trace";
import { Icon } from "@/components/ui/icon";
import * as React from "react";
function ReasoningLine({
  text,
  running,
  duration,
  defaultOpen,
  showIcon,
}: {
  text: string;
  running: boolean;
  duration?: number;
  defaultOpen?: boolean;
  showIcon?: boolean;
}) {
  const [open, setOpen] = React.useState<boolean | undefined>(defaultOpen);
  const line = (
    <TraceLine status={running ? "running" : undefined}>
      {showIcon && (
        <TraceIcon>
          <Icon name="CircleCheck" />
        </TraceIcon>
      )}
      <TraceTitle>{running ? "Thinking" : "Thought"}</TraceTitle>
      {!running && duration !== undefined && (
        <TraceDescription>{duration}s</TraceDescription>
      )}
    </TraceLine>
  );
  if (!text.length) return line;
  return (
    <TraceCollapsible open={open ?? running} onOpenChange={setOpen}>
      <TraceCollapsibleTrigger>{line}</TraceCollapsibleTrigger>
      <TraceCollapsibleContent>
        <div style={{ whiteSpace: "pre-wrap" }}>{text}</div>
      </TraceCollapsibleContent>
    </TraceCollapsible>
  );
}
export default function AiReasoningExample() {
  return (
    <div
      style={{
        width: "100%",
        maxWidth: 384,
        marginInline: "auto",
        paddingBlock: 24,
      }}
    >
      <Trace>
        <ReasoningLine
          text="The build log shows install and compile finish in under a minute. Image optimization takes almost four, and it reruns on every deploy because the cache key includes the commit hash."
          running={false}
          duration={12}
          showIcon
        />
      </Trace>
    </div>
  );
}

AI SDK

This optional adapter accepts a UIMessage from your chat client. The complete example needs no AI SDK or provider. Install this extra package only for the adapter:

pnpm add ai

Render every reasoning part. A line runs only when both the chat and the part are streaming. Empty text stays a plain line. Each line follows the stream until the user changes its open state.

import type { UIMessage } from "ai";
import {
  Trace,
  TraceLine,
  TraceIcon,
  TraceTitle,
  TraceCollapsible,
  TraceCollapsibleTrigger,
  TraceCollapsibleContent,
} from "@/components/ui/trace";
import { Icon } from "@/components/ui/icon";
import * as React from "react";
function ReasoningPart({ text, running }: { text: string; running: boolean }) {
  const [open, setOpen] = React.useState<boolean>();
  const line = (
    <TraceLine status={running ? "running" : undefined}>
      <TraceIcon>
        <Icon name={running ? "LoaderCircle" : "CircleCheck"} />
      </TraceIcon>
      <TraceTitle>{running ? "Thinking" : "Thought"}</TraceTitle>
    </TraceLine>
  );
  if (!text.length) return line;
  return (
    <TraceCollapsible open={open ?? running} onOpenChange={setOpen}>
      <TraceCollapsibleTrigger>{line}</TraceCollapsibleTrigger>
      <TraceCollapsibleContent>
        <div style={{ whiteSpace: "pre-wrap" }}>{text}</div>
      </TraceCollapsibleContent>
    </TraceCollapsible>
  );
}
function ReasoningAdapter({
  message,
  streaming,
}: {
  message: UIMessage;
  streaming: boolean;
}) {
  return (
    <Trace>
      {message.parts
        .filter((part) => part.type === "reasoning")
        .map((part, index) => (
          <ReasoningPart
            key={`${message.id}-${index}`}
            text={part.text}
            running={streaming && part.state === "streaming"}
          />
        ))}
    </Trace>
  );
}
const REASONING =
  "The deploy log has three phases. Install takes 38s and next build takes 52s, both normal for this project.\n\nImage optimization takes 3m 50s. It reruns on every deploy because its cache key includes the commit hash, so the cache never hits.";
const ANSWER =
  "Image optimization is the bottleneck. Key its cache on the lockfile and the image directory instead of the commit hash, and the deploy should take under two minutes.";
export default function AiReasoningExample() {
  return (
    <div
      style={{
        width: "100%",
        maxWidth: 384,
        marginInline: "auto",
        paddingBlock: 24,
      }}
    >
      <ReasoningAdapter
        message={{
          id: "answer",
          role: "assistant",
          parts: [
            { type: "reasoning", text: REASONING, state: "done" },
            { type: "text", text: ANSWER },
          ],
        }}
        streaming={false}
      />
    </div>
  );
}

Components and API

This composition has no separate component API. Use the APIs for its installed components.

ComponentTarget reference
trace Base UI API
collapsible Base UI API
message Base UI API
bubble Base UI API
button Base UI API
icon Base UI API