Elements

03 / 122 · Reasoning

Reasoning panel

A collapsible trace that streams reasoning steps along a timeline, then settles into a summary.

  1. Reading the request

    The user wants drafts restored per thread, so the composer state has to move out of component state.

Installation

npx shadcn@latest add "@assistant-ui/elements-reasoning-panel"

Usage

import { ReasoningPanel } from "@/components/elements/reasoning-panel";

<ReasoningPanel
  steps={[{ title: "Scope", body: "Find the failing path." }]}
  visibleSteps={1}
  streaming
  open
  onOpenChange={setOpen}
  restingLabel="Reasoned for 4s"
  elapsed="2s"
/>

Props

steps*ReasoningStep[]Full list of reasoning steps available to reveal.
visibleSteps*numberHow many steps from the start are currently shown on the timeline.
streaming*booleanWhen true the trigger shows a shimmering Thinking label and the active step pulses.
open*booleanWhether the collapsible step list is expanded.
onOpenChange*(open: boolean) => voidCalled when the user expands or collapses the panel.
restingLabel*stringTrigger label once streaming finishes, for example a summary of how long it thought.
elapsedstringElapsed timer shown next to Thinking while streaming. Omit to hide it.
classNamestringExtra classes merged onto the root.

This element normalizes its counts and shares at the boundary, so a value from outside its range reads as the nearest end rather than reaching the DOM: a negative count shows nothing, one past the end shows everything, and a share stays within 0 to 100 percent. A prop that names a selection is the exception, and means nothing is selected.

Source

reasoning-panel.tsx
"use client";

import { ChevronDownIcon } from "lucide-react";
import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { cn } from "@/lib/utils";
import { collapsePanel, mono, ShimmerLabel, SwapLabel } from "./surfaces";
import { take } from "./range";

export interface ReasoningStep {
  title: string;
  body: string;
}

export interface ReasoningPanelProps {
  steps: ReasoningStep[];
  visibleSteps: number;
  streaming: boolean;
  open: boolean;
  onOpenChange: (open: boolean) => void;
  restingLabel: string;
  elapsed?: string;
  className?: string;
}

export function ReasoningPanel({
  steps,
  visibleSteps,
  streaming,
  open,
  onOpenChange,
  restingLabel,
  elapsed,
  className,
}: ReasoningPanelProps) {
  const shown = take(steps, visibleSteps);

  return (
    <Collapsible
      data-slot="reasoning-panel"
      open={open}
      onOpenChange={onOpenChange}
      className={cn("w-full max-w-sm", className)}
    >
      <CollapsibleTrigger className="group/trigger text-foreground/55 hover:text-foreground/90 flex items-center gap-1.5 py-1 text-[13.5px] transition-[color,scale] outline-none active:scale-[0.98]">
        <SwapLabel active={streaming ? 0 : 1} className="text-start">
          <>
            <ShimmerLabel
              active={streaming}
              className="relative inline-block leading-none"
            >
              Thinking
            </ShimmerLabel>
            {elapsed !== undefined && (
              <span className={cn(mono, "text-foreground/30 tabular-nums")}>
                {elapsed}
              </span>
            )}
          </>
          <>{restingLabel}</>
        </SwapLabel>
        <ChevronDownIcon className="size-3.5 shrink-0 opacity-60 transition-transform duration-200 ease-[cubic-bezier(0.32,0.72,0,1)] group-data-open/trigger:rotate-180 group-data-panel-open/trigger:rotate-180 motion-reduce:transition-none" />
      </CollapsibleTrigger>
      <CollapsibleContent className={cn(collapsePanel, "outline-none")}>
        <ol className="flex flex-col gap-4 pt-3 pb-1">
          {shown.map((step, i) => {
            const active = streaming && i === shown.length - 1;
            return (
              <li
                key={step.title}
                className="fade-in slide-in-from-bottom-1 animate-in fill-mode-both flex gap-3 duration-300"
              >
                <span
                  aria-hidden
                  className={cn(
                    "mt-[7px] size-[5px] shrink-0 rounded-full transition-colors duration-300",
                    active
                      ? "animate-pulse bg-blue-500 dark:bg-blue-400"
                      : "bg-foreground/20",
                  )}
                />
                <span className="flex min-w-0 flex-1 flex-col">
                  <p className="text-foreground/90 text-[13.5px] font-medium">
                    {step.title}
                  </p>
                  <p className="text-foreground/50 mt-0.5 text-[13px] leading-relaxed break-words">
                    {step.body}
                  </p>
                </span>
              </li>
            );
          })}
        </ol>
      </CollapsibleContent>
    </Collapsible>
  );
}