LLM index: /llms.txt

Chain of Thought

Structured timeline for assistant/tool execution traces. Use it to display sequential steps (web search, code search, file reads, tool calls) with per-step status, optional expandable output, and an auto-closing root when all steps complete.

Pulled customer profile and recent events
Reviewed billing history and usage spikes
Proposed resolution with confidence score
Task complete
import * as React from "react";
import {
  AiBrain01Icon,
  Analytics01Icon,
  CheckmarkCircle01Icon,
  FileSearchIcon,
  IdeaIcon,
} from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";

import {
  ChainOfThought,
  ChainOfThoughtComplete,
  ChainOfThoughtContent,
  ChainOfThoughtStep,
  ChainOfThoughtStepTitle,
  ChainOfThoughtTrigger,
} from "@/components/nexus-ui/chain-of-thought";

function ChainOfThoughtDefault() {
  return (
    <div className="w-full">
      <ChainOfThought autoCloseOnAllComplete={false}>
        <ChainOfThoughtTrigger
          icon={
            <HugeiconsIcon
              icon={AiBrain01Icon}
              strokeWidth={1.75}
              className="size-4"
            />
          }
        >
          Triaged support ticket with account activity context
        </ChainOfThoughtTrigger>

        <ChainOfThoughtContent>
          <ChainOfThoughtStep status="completed">
            <ChainOfThoughtStepTitle
              icon={
                <HugeiconsIcon
                  icon={FileSearchIcon}
                  strokeWidth={1.75}
                  className="size-4"
                />
              }
            >
              Pulled customer profile and recent events
            </ChainOfThoughtStepTitle>
          </ChainOfThoughtStep>

          <ChainOfThoughtStep status="completed">
            <ChainOfThoughtStepTitle
              icon={
                <HugeiconsIcon
                  icon={Analytics01Icon}
                  strokeWidth={1.75}
                  className="size-4"
                />
              }
            >
              Reviewed billing history and usage spikes
            </ChainOfThoughtStepTitle>
          </ChainOfThoughtStep>

          <ChainOfThoughtStep status="completed">
            <ChainOfThoughtStepTitle
              icon={
                <HugeiconsIcon
                  icon={IdeaIcon}
                  strokeWidth={1.75}
                  className="size-4"
                />
              }
            >
              Proposed resolution with confidence score
            </ChainOfThoughtStepTitle>
          </ChainOfThoughtStep>

          <ChainOfThoughtComplete
            label="Task complete"
            icon={
              <HugeiconsIcon
                icon={CheckmarkCircle01Icon}
                strokeWidth={1.75}
                className="size-4"
              />
            }
          />
        </ChainOfThoughtContent>
      </ChainOfThought>
    </div>
  );
}

export default ChainOfThoughtDefault;

Installation

npx shadcn@latest add @nexus-ui/chain-of-thought
pnpm dlx shadcn@latest add @nexus-ui/chain-of-thought
yarn dlx shadcn@latest add @nexus-ui/chain-of-thought
bunx shadcn@latest add @nexus-ui/chain-of-thought

Install the following dependencies:

npx shadcn@latest add collapsible && npm install @hugeicons/react @hugeicons/core-free-icons tw-shimmer
pnpm dlx shadcn@latest add collapsible && pnpm add @hugeicons/react @hugeicons/core-free-icons tw-shimmer
yarn dlx shadcn@latest add collapsible && yarn add @hugeicons/react @hugeicons/core-free-icons tw-shimmer
bunx shadcn@latest add collapsible && bun add @hugeicons/react @hugeicons/core-free-icons tw-shimmer

Copy and paste the following code into your project.

components/nexus-ui/chain-of-thought.tsx
"use client";

import * as React from "react";
import { Alert02Icon, ArrowDown01Icon } from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";

import {
  Collapsible,
  CollapsibleContent,
  CollapsibleTrigger,
} from "@/components/ui/collapsible";
import { TextShimmer } from "@/components/nexus-ui/text-shimmer";
import { useOnChange } from "@/lib/use-on-change";
import { cn } from "@/lib/utils";

type ChainOfThoughtRootContextValue = {
  registerStep: (id: string, status: ChainOfThoughtStepStatus) => void;
  allStepsComplete: boolean;
  hasAnyError: boolean;
};

const ChainOfThoughtRootContext =
  React.createContext<ChainOfThoughtRootContextValue | null>(null);

function useChainOfThoughtRootContext(component: string) {
  const ctx = React.useContext(ChainOfThoughtRootContext);
  if (!ctx) {
    throw new Error(`${component} must be used within <ChainOfThought>`);
  }
  return ctx;
}

type ChainOfThoughtStepContextValue = {
  status: ChainOfThoughtStepStatus;
  hasContent: boolean;
};

const ChainOfThoughtStepContext =
  React.createContext<ChainOfThoughtStepContextValue | null>(null);

function useChainOfThoughtStepContext(component: string) {
  const ctx = React.useContext(ChainOfThoughtStepContext);
  if (!ctx) {
    throw new Error(`${component} must be used within <ChainOfThoughtStep>`);
  }
  return ctx;
}

type ChainOfThoughtStepStatus = "pending" | "active" | "completed" | "error";

type ChainOfThoughtProps = Omit<
  React.ComponentProps<typeof Collapsible>,
  "open" | "defaultOpen" | "onOpenChange"
> & {
  open?: boolean;
  defaultOpen?: boolean;
  onOpenChange?: (open: boolean) => void;
  autoCloseOnAllComplete?: boolean;
};

function ChainOfThought({
  className,
  open: openProp,
  defaultOpen = true,
  onOpenChange,
  autoCloseOnAllComplete = true,
  children,
  ...props
}: ChainOfThoughtProps) {
  const isControlled = openProp !== undefined;
  const [internalOpen, setInternalOpen] = React.useState(defaultOpen);
  const [stepStatuses, setStepStatuses] = React.useState<
    Record<string, ChainOfThoughtStepStatus>
  >({});

  const { allStepsComplete, hasAnyError } = React.useMemo(() => {
    const statuses = Object.values(stepStatuses);
    return {
      allStepsComplete:
        statuses.length > 0 &&
        statuses.every((status) => status === "completed"),
      hasAnyError: statuses.some((status) => status === "error"),
    };
  }, [stepStatuses]);

  const open = isControlled ? openProp : internalOpen;

  const registerStep = React.useCallback(
    (id: string, status: ChainOfThoughtStepStatus) => {
      setStepStatuses((prev) => {
        if (prev[id] === status) return prev;
        return { ...prev, [id]: status };
      });
    },
    [],
  );

  const contextValue = React.useMemo(
    () => ({ registerStep, allStepsComplete, hasAnyError }),
    [allStepsComplete, hasAnyError, registerStep],
  );

  const handleOpenChange = React.useCallback(
    (nextOpen: boolean) => {
      if (!isControlled) {
        setInternalOpen(nextOpen);
      }
      onOpenChange?.(nextOpen);
    },
    [isControlled, onOpenChange],
  );

  useOnChange(allStepsComplete, (current, previous) => {
    if (!autoCloseOnAllComplete || isControlled) return;
    if (!previous && current) {
      setInternalOpen(false);
      onOpenChange?.(false);
    }
  });

  return (
    <ChainOfThoughtRootContext.Provider value={contextValue}>
      <Collapsible
        data-slot="chain-of-thought"
        className={cn("not-prose w-full", className)}
        open={open}
        onOpenChange={handleOpenChange}
        {...props}
      >
        {children}
      </Collapsible>
    </ChainOfThoughtRootContext.Provider>
  );
}

type ChainOfThoughtTriggerProps = React.ComponentProps<
  typeof CollapsibleTrigger
> & {
  label?: React.ReactNode;
  icon?: React.ReactNode;
};

function ChainOfThoughtTrigger({
  className,
  icon,
  label,
  children,
  ...props
}: ChainOfThoughtTriggerProps) {
  const { allStepsComplete, hasAnyError } = useChainOfThoughtRootContext(
    "ChainOfThoughtTrigger",
  );
  const isActive = !allStepsComplete && !hasAnyError;

  return (
    <CollapsibleTrigger
      data-slot="chain-of-thought-trigger"
      data-active={String(isActive)}
      className={cn(
        "group flex w-full cursor-pointer items-center gap-1.25 overflow-hidden text-muted-foreground transition-colors hover:text-foreground",
        className,
      )}
      {...props}
    >
      {icon}
      <div className="flex min-w-0 flex-1 items-start gap-1.25 overflow-hidden">
        <TextShimmer
          className="truncate text-left text-sm leading-4.5 text-ellipsis whitespace-nowrap"
          spread={10}
          invertLight
          disableShimmer={!isActive}
        >
          {children ?? label}
        </TextShimmer>
        <HugeiconsIcon
          icon={ArrowDown01Icon}
          strokeWidth={2}
          className="ml-0.5 size-4 shrink-0 opacity-0 transition-all group-hover:opacity-100 group-data-[state=open]:rotate-180 group-data-[state=open]:opacity-100"
        />
      </div>
    </CollapsibleTrigger>
  );
}

type ChainOfThoughtContentProps = React.ComponentProps<
  typeof CollapsibleContent
>;

function ChainOfThoughtContent({
  className,
  children,
  ...props
}: ChainOfThoughtContentProps) {
  return (
    <CollapsibleContent
      data-slot="chain-of-thought-content"
      className={cn(
        "mt-3 space-y-3",
        "overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down",
        className,
      )}
      {...props}
    >
      {children}
    </CollapsibleContent>
  );
}

function hasIconInStepTitle(children: React.ReactNode): boolean {
  return React.Children.toArray(children).some(
    (child) =>
      React.isValidElement<{ icon?: React.ReactNode }>(child) &&
      child.props.icon != null,
  );
}

type ChainOfThoughtStepProps = Omit<
  React.ComponentProps<typeof Collapsible>,
  "open" | "defaultOpen" | "onOpenChange"
> & {
  status?: ChainOfThoughtStepStatus;
  hasContent?: boolean;
  showConnector?: boolean;
  open?: boolean;
  defaultOpen?: boolean;
  onOpenChange?: (open: boolean) => void;
  autoCloseOnComplete?: boolean;
};

function ChainOfThoughtStep({
  className,
  status = "pending",
  hasContent = false,
  showConnector,
  open: openProp,
  defaultOpen = true,
  onOpenChange,
  autoCloseOnComplete = true,
  children,
  ...props
}: ChainOfThoughtStepProps) {
  const { registerStep } = useChainOfThoughtRootContext("ChainOfThoughtStep");
  const stepId = React.useId();
  const showConnectorResolved = showConnector ?? hasIconInStepTitle(children);
  const isControlled = openProp !== undefined;
  const canAutoManageOpen = !isControlled && hasContent;
  const [internalOpen, setInternalOpen] = React.useState(
    () =>
      defaultOpen ||
      (hasContent && (status === "active" || status === "error")),
  );
  const open = isControlled ? openProp : internalOpen;

  React.useEffect(() => {
    registerStep(stepId, status);
  }, [registerStep, status, stepId]);

  const handleOpenChange = React.useCallback(
    (nextOpen: boolean) => {
      if (!isControlled) {
        setInternalOpen(nextOpen);
      }
      onOpenChange?.(nextOpen);
    },
    [isControlled, onOpenChange],
  );

  useOnChange(status, (current, previous) => {
    if (
      canAutoManageOpen &&
      (current === "active" || current === "error") &&
      previous !== current
    ) {
      setInternalOpen(true);
      onOpenChange?.(true);
      return;
    }

    if (!canAutoManageOpen || !autoCloseOnComplete || previous === undefined) {
      return;
    }

    if (previous !== "completed" && current === "completed") {
      setInternalOpen(false);
      onOpenChange?.(false);
    }
  });

  return (
    <ChainOfThoughtStepContext.Provider value={{ status, hasContent }}>
      <Collapsible
        data-slot="chain-of-thought-step"
        className={cn("relative w-full fade-in-0", className)}
        open={open}
        onOpenChange={handleOpenChange}
        {...props}
      >
        {children}
        {showConnectorResolved ? (
          <div
            className={cn(
              "absolute top-4.75 -bottom-2.75 left-2 -mx-px w-px",
              status === "error" ? "bg-destructive/20" : "bg-border/50",
            )}
          ></div>
        ) : null}
      </Collapsible>
    </ChainOfThoughtStepContext.Provider>
  );
}

type ChainOfThoughtStepTitleSharedProps = {
  label?: React.ReactNode;
  icon?: React.ReactNode;
  children?: React.ReactNode;
  collapsible?: boolean;
};

type ChainOfThoughtStepTitleCollapsibleProps =
  ChainOfThoughtStepTitleSharedProps &
    Omit<React.ComponentProps<typeof CollapsibleTrigger>, "children"> & {
      collapsible: true;
    };

type ChainOfThoughtStepTitleStaticProps = ChainOfThoughtStepTitleSharedProps &
  React.HTMLAttributes<HTMLDivElement> & {
    collapsible?: false;
  };

type ChainOfThoughtStepTitleProps =
  | ChainOfThoughtStepTitleCollapsibleProps
  | ChainOfThoughtStepTitleStaticProps;

function ChainOfThoughtStepTitle({
  className,
  label: labelProp,
  icon,
  collapsible,
  children,
  ...props
}: ChainOfThoughtStepTitleProps) {
  const { hasContent, status } = useChainOfThoughtStepContext(
    "ChainOfThoughtStepTitle",
  );
  const isCollapsible = collapsible ?? hasContent;
  const isActive = status === "active";
  const isError = status === "error";
  const resolvedIcon =
    isError && icon ? (
      <HugeiconsIcon icon={Alert02Icon} strokeWidth={1.75} className="size-4" />
    ) : (
      icon
    );
  const label = children ?? labelProp;

  if (!isCollapsible) {
    const staticProps = props as React.HTMLAttributes<HTMLDivElement>;

    return (
      <div
        data-slot="chain-of-thought-step-title"
        data-active={String(isActive)}
        className={cn(
          "group flex items-center text-sm leading-4.5 text-muted-foreground",
          isError && "text-destructive",
          resolvedIcon ? "gap-2" : "gap-0",
          className,
        )}
        {...staticProps}
      >
        {resolvedIcon ? (
          <div className="relative flex size-4 shrink-0 items-center justify-center">
            {resolvedIcon}
          </div>
        ) : null}
        <TextShimmer
          className="truncate text-left text-sm leading-4.5 text-ellipsis whitespace-nowrap"
          spread={10}
          invertLight
          disableShimmer={!isActive}
        >
          {label}
        </TextShimmer>
      </div>
    );
  }

  return (
    <CollapsibleTrigger
      data-slot="chain-of-thought-step-title"
      data-active={String(isActive)}
      className={cn(
        "group flex w-full cursor-pointer items-center text-sm text-muted-foreground transition-colors hover:text-foreground",
        isError && "text-destructive hover:text-destructive/90",
        resolvedIcon ? "gap-2" : "gap-0",
        className,
      )}
      {...(props as Omit<
        React.ComponentProps<typeof CollapsibleTrigger>,
        "children"
      >)}
    >
      {resolvedIcon ? (
        <div className="relative flex size-4 shrink-0 items-center justify-center">
          {resolvedIcon}
        </div>
      ) : null}
      <div className="flex min-w-0 flex-1 items-start gap-1.25 overflow-hidden">
        <TextShimmer
          className="truncate text-left text-sm leading-4.5 text-ellipsis whitespace-nowrap"
          spread={10}
          invertLight
          disableShimmer={!isActive}
        >
          {label}
        </TextShimmer>
        <HugeiconsIcon
          icon={ArrowDown01Icon}
          strokeWidth={2}
          className="ml-0.5 size-4 shrink-0 opacity-0 transition-all group-hover:opacity-100 group-data-[state=open]:rotate-180 group-data-[state=open]:opacity-100"
        />
      </div>
    </CollapsibleTrigger>
  );
}

type ChainOfThoughtStepContentProps = React.ComponentProps<
  typeof CollapsibleContent
>;

function ChainOfThoughtStepContent({
  className,
  children,
  ...props
}: ChainOfThoughtStepContentProps) {
  return (
    <CollapsibleContent
      data-slot="chain-of-thought-step-content"
      className={cn(
        "mt-2 ml-6",
        "overflow-hidden data-[state=closed]:animate-collapsible-up data-[state=open]:animate-collapsible-down",
        className,
      )}
      {...props}
    >
      {children}
    </CollapsibleContent>
  );
}

type ChainOfThoughtCompleteProps = React.HTMLAttributes<HTMLDivElement> & {
  icon?: React.ReactNode;
  label: React.ReactNode;
};

function ChainOfThoughtComplete({
  className,
  icon,
  label,
  ...props
}: ChainOfThoughtCompleteProps) {
  return (
    <div
      data-slot="chain-of-thought-complete"
      className={cn(
        "mt-0 flex items-center gap-2 text-sm leading-4.5 text-muted-foreground fade-in-0",
        !icon && "gap-0",
        className,
      )}
      {...props}
    >
      {icon}
      <span>{label}</span>
    </div>
  );
}

export type { ChainOfThoughtStepStatus };
export {
  ChainOfThought,
  ChainOfThoughtTrigger,
  ChainOfThoughtContent,
  ChainOfThoughtStep,
  ChainOfThoughtStepTitle,
  ChainOfThoughtStepContent,
  ChainOfThoughtComplete,
};
lib/use-on-change.ts
"use client";

import * as React from "react";

/**
 * Runs `onChange` when `value` changes (compared against previous render).
 */
export function useOnChange<T>(
  value: T,
  onChange: (current: T, previous: T) => void,
  isUpdated: (previous: T, current: T) => boolean = Object.is,
) {
  const previousRef = React.useRef(value);

  React.useEffect(() => {
    const previous = previousRef.current;
    if (!isUpdated(previous, value)) {
      onChange(value, previous);
    }
    previousRef.current = value;
  }, [value, onChange, isUpdated]);
}

Update import paths to match your project setup.

Usage

import {
  ChainOfThought,
  ChainOfThoughtTrigger,
  ChainOfThoughtContent,
  ChainOfThoughtStep,
  ChainOfThoughtStepTitle,
  ChainOfThoughtStepContent,
  ChainOfThoughtComplete,
} from "@/components/nexus-ui/chain-of-thought";
<ChainOfThought>
  <ChainOfThoughtTrigger>
    Thinking...
  </ChainOfThoughtTrigger>
  <ChainOfThoughtContent>
    <ChainOfThoughtStep status="active" hasContent>
      <ChainOfThoughtStepTitle icon={<SearchIcon />}>
        Searching the web...
      </ChainOfThoughtStepTitle>
      <ChainOfThoughtStepContent>
        {/* step output */}
      </ChainOfThoughtStepContent>
    </ChainOfThoughtStep>

    <ChainOfThoughtStep status="pending">
      <ChainOfThoughtStepTitle icon={<CodeIcon />}>
        Search codebase for API handlers
      </ChainOfThoughtStepTitle>
    </ChainOfThoughtStep>

    <ChainOfThoughtComplete label="Task complete" />
  </ChainOfThoughtContent>
</ChainOfThought>

Typical status progression per step is pending -> active -> completed (or error).

Examples

Basic

Simple timeline labels that mirror how coding agents report progress in tools like Cursor. Steps without icons hide connectors by default.

Grepped `chain-of-thought` in `nexus-ui`
Searched files `**/components/*` in `nexus-ui`
Read `registry.json` L1-206
Read `chain-of-thought.tsx` L1-80
Read `default.tsx` L1-96
No linter errors
import * as React from "react";

import {
  ChainOfThought,
  ChainOfThoughtComplete,
  ChainOfThoughtContent,
  ChainOfThoughtStep,
  ChainOfThoughtStepTitle,
  ChainOfThoughtTrigger,
} from "@/components/nexus-ui/chain-of-thought";

function ChainOfThoughtBasic() {
  return (
    <div className="w-full">
      <ChainOfThought autoCloseOnAllComplete={false}>
        <ChainOfThoughtTrigger>
          Explored 3 files, 2 searches, lints
        </ChainOfThoughtTrigger>
        <ChainOfThoughtContent>
          <ChainOfThoughtStep status="completed">
            <ChainOfThoughtStepTitle>
              Grepped `chain-of-thought` in `nexus-ui`
            </ChainOfThoughtStepTitle>
          </ChainOfThoughtStep>

          <ChainOfThoughtStep status="completed">
            <ChainOfThoughtStepTitle>
              Searched files `**/components/*` in `nexus-ui`
            </ChainOfThoughtStepTitle>
          </ChainOfThoughtStep>

          <ChainOfThoughtStep status="completed">
            <ChainOfThoughtStepTitle>
              Read `registry.json` L1-206
            </ChainOfThoughtStepTitle>
          </ChainOfThoughtStep>

          <ChainOfThoughtStep status="completed">
            <ChainOfThoughtStepTitle>
              Read `chain-of-thought.tsx` L1-80
            </ChainOfThoughtStepTitle>
          </ChainOfThoughtStep>

          <ChainOfThoughtStep status="completed">
            <ChainOfThoughtStepTitle>
              Read `default.tsx` L1-96
            </ChainOfThoughtStepTitle>
          </ChainOfThoughtStep>

          <ChainOfThoughtComplete label="No linter errors" />
        </ChainOfThoughtContent>
      </ChainOfThought>
    </div>
  );
}

export default ChainOfThoughtBasic;

With Content

Expandable step content for rich outputs like search queries, links, file matches, and analysis payloads.

3 days in lisbon best neighborhoodsmust-visit places in lisbon for first timerslisbon public transport tips 2026best sunset viewpoints lisbonfood spots alfama bairro alto chiado
Fri
Sunny
H: 24C - L: 16C
Rain: 5%
Sat
Partly cloudy
H: 23C - L: 15C
Rain: 15%
Sun
Breezy
H: 21C - L: 14C
Rain: 20%
Hotel -> Belem Tower
24 min
Tram + walk
Alfama -> Time Out Market
18 min
Metro + walk
Chiado -> Miradouro da Senhora
22 min
Taxi
Suggested pass: 24-hour transit card for day 1 and day 2.
Research complete
import * as React from "react";
import {
  AiBrain01Icon,
  AiWebBrowsingIcon,
  CheckmarkCircle01Icon,
  Globe02Icon,
  MapsIcon,
  Sun03Icon,
} from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";

import {
  ChainOfThought,
  ChainOfThoughtComplete,
  ChainOfThoughtContent,
  ChainOfThoughtStep,
  ChainOfThoughtStepContent,
  ChainOfThoughtStepTitle,
  ChainOfThoughtTrigger,
} from "@/components/nexus-ui/chain-of-thought";

const WEB_SEARCH_QUERIES = [
  "3 days in lisbon best neighborhoods",
  "must-visit places in lisbon for first timers",
  "lisbon public transport tips 2026",
  "best sunset viewpoints lisbon",
  "food spots alfama bairro alto chiado",
];

const WEB_SOURCES = [
  {
    title: "Lisbon neighborhoods guide for first-time visitors",
    domain: "visitlisboa.com",
    url: "https://www.visitlisboa.com/en/",
  },
  {
    title: "48 hours and 72 hours in Lisbon itinerary",
    domain: "lonelyplanet.com",
    url: "https://www.lonelyplanet.com/portugal/lisbon",
  },
  {
    title: "Best miradouros (viewpoints) in Lisbon",
    domain: "culturetrip.com",
    url: "https://theculturetrip.com/europe/portugal/lisbon",
  },
  {
    title: "Lisbon metro and tram guide",
    domain: "carris.pt",
    url: "https://www.carris.pt/en/",
  },
];

const WEATHER_CARDS = [
  {
    day: "Fri",
    condition: "Sunny",
    high: "24C",
    low: "16C",
    rainChance: "5%",
  },
  {
    day: "Sat",
    condition: "Partly cloudy",
    high: "23C",
    low: "15C",
    rainChance: "15%",
  },
  {
    day: "Sun",
    condition: "Breezy",
    high: "21C",
    low: "14C",
    rainChance: "20%",
  },
];

const MAP_ROUTES = [
  { from: "Hotel -> Belem Tower", eta: "24 min", mode: "Tram + walk" },
  { from: "Alfama -> Time Out Market", eta: "18 min", mode: "Metro + walk" },
  { from: "Chiado -> Miradouro da Senhora", eta: "22 min", mode: "Taxi" },
];

function ChainOfThoughtWithContent() {
  return (
    <div className="w-full">
      <ChainOfThought autoCloseOnAllComplete={false}>
        <ChainOfThoughtTrigger
          icon={
            <HugeiconsIcon
              icon={AiBrain01Icon}
              strokeWidth={1.75}
              className="size-4"
            />
          }
        >
          Planned a 3-day Lisbon itinerary
        </ChainOfThoughtTrigger>

        <ChainOfThoughtContent>
          <ChainOfThoughtStep
            status="completed"
            hasContent
            autoCloseOnComplete={false}
          >
            <ChainOfThoughtStepTitle
              icon={
                <HugeiconsIcon
                  icon={AiWebBrowsingIcon}
                  strokeWidth={1.75}
                  className="size-4"
                />
              }
            >
              Web search
            </ChainOfThoughtStepTitle>

            <ChainOfThoughtStepContent>
              <div className="mt-1 space-y-2">
                <div className="-mx-1 mt-1 no-scrollbar flex gap-1.5 overflow-x-auto overscroll-x-contain px-1 pb-0.5 [-webkit-overflow-scrolling:touch] sm:mx-0 sm:flex-wrap sm:overflow-x-visible sm:px-0 sm:pb-0">
                  {WEB_SEARCH_QUERIES.map((query) => (
                    <span
                      key={query}
                      className="inline-flex h-6.5 shrink-0 items-center gap-1 rounded-full bg-muted px-2 text-xs leading-4.5 whitespace-nowrap text-muted-foreground sm:max-w-[187.8px] sm:whitespace-normal"
                    >
                      <HugeiconsIcon
                        icon={Globe02Icon}
                        strokeWidth={1.75}
                        className="size-4 shrink-0 text-muted-foreground/50"
                      />
                      <span className="min-w-0 sm:truncate">{query}</span>
                    </span>
                  ))}
                </div>

                <div className="mt-1.5 no-scrollbar flex max-h-[180px] w-full max-w-full min-w-0 flex-col gap-2 overflow-x-hidden overflow-y-auto rounded-[12px] border border-border/50 bg-secondary p-3">
                  {WEB_SOURCES.map((source) => (
                    <a
                      key={source.url}
                      href={source.url}
                      target="_blank"
                      rel="noopener noreferrer"
                      className="grid w-full max-w-full min-w-0 grid-cols-[auto_minmax(0,1fr)_minmax(0,7.5rem)] items-center gap-2 rounded-md px-1.5 py-1 text-xs leading-4.5 transition-colors hover:bg-border/50 sm:grid-cols-[auto_minmax(0,1fr)_minmax(0,9rem)] dark:hover:bg-border/40"
                    >
                      <img
                        alt=""
                        loading="lazy"
                        width={16}
                        height={16}
                        className="size-4 shrink-0 rounded"
                        src={`https://www.google.com/s2/favicons?domain=${source.domain}&sz=128`}
                      />
                      <div className="min-w-0 truncate text-primary">
                        {source.title}
                      </div>
                      <div
                        className="min-w-0 truncate text-right text-muted-foreground tabular-nums"
                        title={source.domain}
                      >
                        {source.domain}
                      </div>
                    </a>
                  ))}
                </div>
              </div>
            </ChainOfThoughtStepContent>
          </ChainOfThoughtStep>

          <ChainOfThoughtStep
            status="completed"
            hasContent
            autoCloseOnComplete={false}
          >
            <ChainOfThoughtStepTitle
              icon={
                <HugeiconsIcon
                  icon={Sun03Icon}
                  strokeWidth={1.75}
                  className="size-4"
                />
              }
            >
              Get weather tool
            </ChainOfThoughtStepTitle>

            <ChainOfThoughtStepContent>
              <div className="mt-1.5 grid grid-cols-1 gap-2 sm:grid-cols-3">
                {WEATHER_CARDS.map((item) => (
                  <div
                    key={item.day}
                    className="rounded-[12px] border border-border/50 bg-secondary p-3"
                  >
                    <div className="text-xs text-muted-foreground">
                      {item.day}
                    </div>
                    <div className="mt-1 text-sm font-medium text-primary">
                      {item.condition}
                    </div>
                    <div className="mt-1 text-xs text-muted-foreground">
                      H: {item.high} - L: {item.low}
                    </div>
                    <div className="mt-1 text-xs text-muted-foreground">
                      Rain: {item.rainChance}
                    </div>
                  </div>
                ))}
              </div>
            </ChainOfThoughtStepContent>
          </ChainOfThoughtStep>

          <ChainOfThoughtStep
            status="completed"
            hasContent
            autoCloseOnComplete={false}
            className="animate-in"
          >
            <ChainOfThoughtStepTitle
              icon={
                <HugeiconsIcon
                  icon={MapsIcon}
                  strokeWidth={1.75}
                  className="size-4"
                />
              }
            >
              Route planner tool
            </ChainOfThoughtStepTitle>

            <ChainOfThoughtStepContent>
              <div className="mt-1.5 no-scrollbar flex max-h-[180px] w-full max-w-full min-w-0 flex-col gap-2 overflow-x-hidden overflow-y-auto rounded-[12px] border border-border/50 bg-secondary p-3">
                {MAP_ROUTES.map((route) => (
                  <div
                    key={route.from}
                    className="grid w-full max-w-full min-w-0 grid-cols-[minmax(0,1fr)_auto_auto] items-center gap-2 rounded-md px-1.5 py-1 text-xs leading-4.5"
                  >
                    <div className="min-w-0 truncate text-primary">
                      {route.from}
                    </div>
                    <div className="text-muted-foreground tabular-nums">
                      {route.eta}
                    </div>
                    <div className="text-muted-foreground">{route.mode}</div>
                  </div>
                ))}
                <div className="mt-1 rounded-md border border-border/50 bg-background/50 px-2 py-1.5 text-xs text-muted-foreground">
                  Suggested pass: 24-hour transit card for day 1 and day 2.
                </div>
              </div>
            </ChainOfThoughtStepContent>
          </ChainOfThoughtStep>

          <ChainOfThoughtComplete
            label="Research complete"
            icon={
              <HugeiconsIcon
                icon={CheckmarkCircle01Icon}
                strokeWidth={1.75}
                className="size-4"
              />
            }
          />
        </ChainOfThoughtContent>
      </ChainOfThought>
    </div>
  );
}

export default ChainOfThoughtWithContent;

Error

Show a failed step with status="error" and include contextual details in step content.

Connected to workspace
Request to api.example.com timed out after 10s. Retry with fallback endpoint or cached schema.
import * as React from "react";
import {
  AiBrain01Icon,
  AiWebBrowsingIcon,
  FolderOpenIcon,
} from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";

import {
  ChainOfThought,
  ChainOfThoughtContent,
  ChainOfThoughtStep,
  ChainOfThoughtStepContent,
  ChainOfThoughtStepTitle,
  ChainOfThoughtTrigger,
} from "@/components/nexus-ui/chain-of-thought";

function ChainOfThoughtError() {
  return (
    <div className="w-full">
      <ChainOfThought autoCloseOnAllComplete={false}>
        <ChainOfThoughtTrigger
          icon={<HugeiconsIcon icon={AiBrain01Icon} strokeWidth={1.75} className="size-4" />}
        >
          Executing plan...
        </ChainOfThoughtTrigger>

        <ChainOfThoughtContent>
          <ChainOfThoughtStep status="completed">
            <ChainOfThoughtStepTitle
              icon={<HugeiconsIcon icon={FolderOpenIcon} strokeWidth={1.75} className="size-4" />}
            >
              Connected to workspace
            </ChainOfThoughtStepTitle>
          </ChainOfThoughtStep>

          <ChainOfThoughtStep status="error" hasContent autoCloseOnComplete={false}>
            <ChainOfThoughtStepTitle
              icon={
                <HugeiconsIcon
                  icon={AiWebBrowsingIcon}
                  strokeWidth={1.75}
                  className="size-4"
                />
              }
            >
              Failed to fetch external API schema
            </ChainOfThoughtStepTitle>

            <ChainOfThoughtStepContent>
              <div className="mt-1 rounded-[12px] border border-destructive/20 bg-destructive/5 p-3 text-sm leading-4.5 text-destructive">
                Request to api.example.com timed out after 10s. Retry with fallback
                endpoint or cached schema.
              </div>
            </ChainOfThoughtStepContent>
          </ChainOfThoughtStep>
        </ChainOfThoughtContent>
      </ChainOfThought>
    </div>
  );
}

export default ChainOfThoughtError;

Without Header

Render only the step timeline by omitting ChainOfThoughtTrigger. Root stays open and autoCloseOnAllComplete is disabled.

Fetched customer chat history from CRM
Retrieved orders, refunds, and delivery events
Generated escalation summary and next actions
Escalation draft ready
import * as React from "react";
import {
  Analytics01Icon,
  CheckmarkCircle01Icon,
  FolderOpenIcon,
  Globe02Icon,
} from "@hugeicons/core-free-icons";
import { HugeiconsIcon } from "@hugeicons/react";

import {
  ChainOfThought,
  ChainOfThoughtComplete,
  ChainOfThoughtContent,
  ChainOfThoughtStep,
  ChainOfThoughtStepTitle,
} from "@/components/nexus-ui/chain-of-thought";

function ChainOfThoughtWithoutHeader() {
  return (
    <div className="w-full">
      <ChainOfThought defaultOpen autoCloseOnAllComplete={false}>
        <ChainOfThoughtContent>
          <ChainOfThoughtStep status="completed">
            <ChainOfThoughtStepTitle
              icon={
                <HugeiconsIcon
                  icon={Globe02Icon}
                  strokeWidth={1.75}
                  className="size-4"
                />
              }
            >
              Fetched customer chat history from CRM
            </ChainOfThoughtStepTitle>
          </ChainOfThoughtStep>

          <ChainOfThoughtStep status="completed">
            <ChainOfThoughtStepTitle
              icon={
                <HugeiconsIcon
                  icon={FolderOpenIcon}
                  strokeWidth={1.75}
                  className="size-4"
                />
              }
            >
              Retrieved orders, refunds, and delivery events
            </ChainOfThoughtStepTitle>
          </ChainOfThoughtStep>

          <ChainOfThoughtStep status="completed">
            <ChainOfThoughtStepTitle
              icon={
                <HugeiconsIcon
                  icon={Analytics01Icon}
                  strokeWidth={1.75}
                  className="size-4"
                />
              }
            >
              Generated escalation summary and next actions
            </ChainOfThoughtStepTitle>
          </ChainOfThoughtStep>

          <ChainOfThoughtComplete
            label="Escalation draft ready"
            icon={
              <HugeiconsIcon
                icon={CheckmarkCircle01Icon}
                strokeWidth={1.75}
                className="size-4"
              />
            }
          />
        </ChainOfThoughtContent>
      </ChainOfThought>
    </div>
  );
}

export default ChainOfThoughtWithoutHeader;

Vercel AI SDK Integration

ChainOfThought works well with useChat by mapping assistant tool parts into timeline steps.

Use it with Vercel AI SDK by:

  • deriving step status (pending/active/completed/error) from tool part state
  • rendering tool results inside ChainOfThoughtStepContent when available
  • letting root auto-close after all steps are completed

Install the AI SDK

npm install ai @ai-sdk/react

Create a chat API route that streams tool and text parts

You can use any streamText(...).toUIMessageStreamResponse() route shape used in your app. See Prompt Input docs for a minimal route scaffold.

Map assistant parts to Chain of Thought steps

"use client";

import { useChat } from "@ai-sdk/react";
import { DefaultChatTransport, type UIMessage } from "ai";
import {
  ChainOfThought,
  ChainOfThoughtTrigger,
  ChainOfThoughtContent,
  ChainOfThoughtStep,
  ChainOfThoughtStepTitle,
  ChainOfThoughtStepContent,
  ChainOfThoughtComplete,
  type ChainOfThoughtStepStatus,
} from "@/components/nexus-ui/chain-of-thought";

type StepVM = {
  id: string;
  label: string;
  status: ChainOfThoughtStepStatus;
  hasContent?: boolean;
  content?: React.ReactNode;
};

function stepsFromAssistant(message: UIMessage): StepVM[] {
  return message.parts.flatMap((part, index) => {
    // AI SDK UI emits tool-specific part types: tool-<toolName>
    if (part.type === "tool-displayWeather") {
      switch (part.state) {
        case "input-available":
          return [
            {
              id: `weather-${index}`,
              label: "Running weather tool...",
              status: "active" as const,
              hasContent: true,
              content: <div className="text-xs text-muted-foreground">Fetching forecast...</div>,
            },
          ];
        case "output-available":
          return [
            {
              id: `weather-${index}`,
              label: "Weather tool",
              status: "completed" as const,
              hasContent: true,
              content: <pre>{JSON.stringify(part.output, null, 2)}</pre>,
            },
          ];
        case "output-error":
          return [
            {
              id: `weather-${index}`,
              label: "Weather tool failed",
              status: "error" as const,
              hasContent: true,
              content: <div className="text-xs text-destructive">{part.errorText}</div>,
            },
          ];
      }
    }

    if (part.type === "tool-getStockPrice") {
      switch (part.state) {
        case "input-available":
          return [
            {
              id: `stock-${index}`,
              label: "Running stock tool...",
              status: "active" as const,
            },
          ];
        case "output-available":
          return [
            {
              id: `stock-${index}`,
              label: "Stock price tool",
              status: "completed" as const,
              hasContent: true,
              content: <pre>{JSON.stringify(part.output, null, 2)}</pre>,
            },
          ];
        case "output-error":
          return [
            {
              id: `stock-${index}`,
              label: "Stock price tool failed",
              status: "error" as const,
              hasContent: true,
              content: <div className="text-xs text-destructive">{part.errorText}</div>,
            },
          ];
      }
    }

    return [];
  });
}

export default function ChainOfThoughtWithUseChat() {
  const { messages } = useChat({
    transport: new DefaultChatTransport({ api: "/api/chat" }),
  });

  const assistant = [...messages].reverse().find((m) => m.role === "assistant");
  if (!assistant) return null;

  const steps = stepsFromAssistant(assistant);
  if (steps.length === 0) return null;

  return (
    <ChainOfThought autoCloseOnAllComplete>
      <ChainOfThoughtTrigger>Thinking...</ChainOfThoughtTrigger>
      <ChainOfThoughtContent>
        {steps.map((step) => (
          <ChainOfThoughtStep
            key={step.id}
            status={step.status}
            hasContent={step.hasContent}
          >
            <ChainOfThoughtStepTitle collapsible={Boolean(step.hasContent)}>
              {step.label}
            </ChainOfThoughtStepTitle>
            {step.hasContent ? (
              <ChainOfThoughtStepContent>{step.content}</ChainOfThoughtStepContent>
            ) : null}
          </ChainOfThoughtStep>
        ))}
        <ChainOfThoughtComplete label="Task complete" />
      </ChainOfThoughtContent>
    </ChainOfThought>
  );
}

API Reference

ChainOfThought

Root container for the thought timeline. Tracks child step statuses to drive trigger activity and optional auto-close behavior. Wraps Collapsible Root.

Prop

Type

ChainOfThoughtTrigger

Top trigger row for the thought timeline. Label shimmer is active while the flow is in progress and no step is in error. Wraps Collapsible Trigger.

Prop

Type

ChainOfThoughtContent

Content container for step rows. Wraps Collapsible Content.

Prop

Type

ChainOfThoughtStep

A single timeline step with status-aware open behavior and optional expandable content. Wraps Collapsible.

Prop

Type

ChainOfThoughtStepTitle

Step label row. Can render as static text row or collapsible trigger row. Wraps Collapsible Trigger.

Prop

Type

ChainOfThoughtStepContent

Expandable area for tool output/results. Keep result rendering consumer-defined. Wraps Collapsible Content.

Prop

Type

ChainOfThoughtComplete

Terminal row for completion state (Task complete, Done, etc.).

Prop

Type

View as markdown Edit on GitHub