Components / Conversation

Branch

A pager over the versions of a reply or an edited message — previous, next, `2 / 3`.

Write a short reply about the corrected invoice.
Good news: the invoice error is resolved. We have credited $40 to your account, visible under Billing.
3 / 3
pnpm exec shadcn add @intelligo/ai-branch

Inside an app made by intelligo create, or any shadcn project that names the registry.

built on

button

npm packages it adds

lucide-react

used by these pages

Chat

Source

What shadcn add writes to components/ui/ai-branch.tsx. It is yours from then on.

"use client";

/*
 * For a thread that owns which version is on screen: this is a pager over
 * versions the thread keeps, not a component that clones its children per
 * branch.
 */

import * as React from "react";
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react";

import { Button } from "@showcase/components/ui/button";
import { cn } from "@showcase/lib/utils";

type BranchContextValue = {
  index: number;
  count: number;
  onIndexChange: (index: number) => void;
};

const BranchContext = React.createContext<BranchContextValue | null>(null);

function useBranch() {
  const context = React.useContext(BranchContext);
  if (!context) {
    throw new Error("Branch components must be used within Branch");
  }
  return context;
}

/**
 * Versions of one reply — or one edited message — with the thread in
 * charge of `index`. Renders nothing when there is only one.
 */
function Branch({
  index,
  count,
  onIndexChange,
  className,
  children,
  ...props
}: React.ComponentProps<"div"> & {
  /** Zero-based; the version on screen. */
  index: number;
  count: number;
  onIndexChange: (index: number) => void;
}) {
  const value = React.useMemo(
    () => ({ index, count, onIndexChange }),
    [index, count, onIndexChange]
  );
  if (count < 2) return null;
  return (
    <BranchContext.Provider value={value}>
      <div
        data-slot="branch"
        className={cn("flex items-center gap-0.5", className)}
        {...props}
      >
        {children}
      </div>
    </BranchContext.Provider>
  );
}

function BranchPrevious({
  label,
  className,
  ...props
}: Omit<React.ComponentProps<typeof Button>, "onClick"> & {
  /** Accessible name, e.g. "Previous version". */
  label: string;
}) {
  const { index, onIndexChange } = useBranch();
  return (
    <Button
      data-slot="branch-previous"
      type="button"
      variant="ghost"
      size="icon-xs"
      aria-label={label}
      disabled={index <= 0}
      onClick={() => onIndexChange(index - 1)}
      className={cn("text-muted-foreground", className)}
      {...props}
    >
      <ChevronLeftIcon />
    </Button>
  );
}

function BranchNext({
  label,
  className,
  ...props
}: Omit<React.ComponentProps<typeof Button>, "onClick"> & {
  /** Accessible name, e.g. "Next version". */
  label: string;
}) {
  const { index, count, onIndexChange } = useBranch();
  return (
    <Button
      data-slot="branch-next"
      type="button"
      variant="ghost"
      size="icon-xs"
      aria-label={label}
      disabled={index >= count - 1}
      onClick={() => onIndexChange(index + 1)}
      className={cn("text-muted-foreground", className)}
      {...props}
    >
      <ChevronRightIcon />
    </Button>
  );
}

/** "2 / 3". Digits only, so it needs no copy. */
function BranchPage({ className, ...props }: React.ComponentProps<"span">) {
  const { index, count } = useBranch();
  return (
    <span
      data-slot="branch-page"
      className={cn(
        "min-w-8 text-center text-xs tabular-nums text-muted-foreground",
        className
      )}
      aria-live="polite"
      {...props}
    >
      {index + 1} / {count}
    </span>
  );
}

export { Branch, BranchPrevious, BranchNext, BranchPage, useBranch };

Ready to try it? Three commands and the app runs against a stub model.