For the complete documentation index, see llms.txt. Markdown variants are available by appending .md to documentation URLs. Homepage markdown is available at /llms.md/content.md. An agent skill is available at /.well-known/agent-skills/site-skill.md.

Agent Progress 01

A collapsible progress panel for a sequence of tasks.

Installation

Set up the @cobalt registry first, as described in Installation, then add the block:

$ pnpm dlx shadcn@latest add @cobalt/agent-progress-01

Pass an ordered array of labels as steps. stepDuration controls the time per step, and completionDelay sets the delay before onFinished is called. Both durations are in milliseconds. Remount the component to start a new run.

Source

"use client";

import { RiCheckLine, RiSubtractLine, RiAddLine } from "@remixicon/react";
import { cn } from "cn";
import * as React from "react";

import { Button } from "@/components/ui/button";
import {
  Collapsible,
  CollapsibleContent,
} from "@/components/ui/collapsible";

export interface AgentProgressProps {
  steps: string[];
  stepDuration?: number;
  completionDelay?: number;
  onFinished?: () => void;
  className?: string;
}

export function AgentProgress({
  steps,
  stepDuration = 2400,
  completionDelay = 1000,
  onFinished,
  className,
}: AgentProgressProps) {
  const [completed, setCompleted] = React.useState(0);
  const [open, setOpen] = React.useState(true);
  const listId = React.useId();
  const [started, setStarted] = React.useState(false);
  const finishedRef = React.useRef(onFinished);
  finishedRef.current = onFinished;
  const duration = Math.max(1, stepDuration);
  const count = Math.min(completed, steps.length);
  const remaining = steps.length - count;

  React.useEffect(() => {
    if (count >= steps.length) {
      return;
    }
    setStarted(false);
    const frame = requestAnimationFrame(() => setStarted(true));
    const timer = window.setTimeout(
      () => setCompleted((value) => value + 1),
      duration
    );
    return () => {
      cancelAnimationFrame(frame);
      clearTimeout(timer);
    };
  }, [count, duration, steps.length]);

  React.useEffect(() => {
    if (count < steps.length) {
      return;
    }
    const timer = window.setTimeout(
      () => finishedRef.current?.(),
      Math.max(0, completionDelay)
    );
    return () => clearTimeout(timer);
  }, [count, steps.length, completionDelay]);

  return (
    <section
      className={cn(
        "w-85 max-w-full rounded-[14px] border border-border bg-popover p-2 text-popover-foreground shadow-xs",
        className
      )}
      aria-label="Task progress"
    >
      <div className="flex h-7 items-center gap-2 px-1">
        <span
          className="relative flex size-4 shrink-0 items-center justify-center"
          aria-hidden="true"
        >
          {remaining === 0 ? (
            <RiCheckLine className="size-4" />
          ) : (
            <span className="size-4 rounded-full border-2 border-muted-foreground/25 border-t-muted-foreground motion-safe:animate-spin" />
          )}
        </span>
        <span
          className="flex-1 text-sm leading-5 font-medium text-muted-foreground"
          aria-live="polite"
        >
          {remaining === 0
            ? "All steps complete"
            : `${remaining} ${remaining === 1 ? "step" : "steps"} left`}
        </span>
        <Button
          variant="filled"
          size="icon-2xs"
          radius={6}
          tooltip={open ? "Collapse steps" : "Expand steps"}
          onClick={() => setOpen(!open)}
          aria-expanded={open}
          aria-controls={listId}
          aria-label={open ? "Collapse steps" : "Expand steps"}
        >
          {open ? <RiSubtractLine /> : <RiAddLine />}
        </Button>
      </div>
      <Collapsible open={open} onOpenChange={setOpen}>
        <CollapsibleContent id={listId}>
          <ol className="flex flex-col gap-1.5 pt-2">
            {steps.map((step, index) => {
              const done = index < count;
              const active = index === count && remaining > 0;
              return (
                <li
                  key={`${index}-${step}`}
                  className={cn(
                    "flex min-h-8 items-center gap-2 rounded-[14px] border border-transparent px-2 text-sm leading-5 text-muted-foreground",
                    active && "border-border text-foreground"
                  )}
                >
                  <span
                    className="relative flex size-4 shrink-0 items-center justify-center"
                    aria-hidden="true"
                  >
                    {done ? (
                      <span className="flex size-4 items-center justify-center rounded-full bg-muted-foreground/35">
                        <RiCheckLine className="size-3 text-popover" />
                      </span>
                    ) : active ? (
                      <svg viewBox="0 0 16 16" className="size-4 -rotate-90">
                        <circle
                          cx="8"
                          cy="8"
                          r="6"
                          fill="none"
                          stroke="currentColor"
                          strokeOpacity=".2"
                          strokeWidth="2"
                        />
                        <circle
                          cx="8"
                          cy="8"
                          r="6"
                          fill="none"
                          stroke="currentColor"
                          strokeWidth="2"
                          strokeDasharray="37.7"
                          strokeDashoffset={started ? 0 : 37.7}
                          style={
                            {
                              "--step-duration": `${duration}ms`,
                            } as React.CSSProperties
                          }
                          className={cn(
                            "transition-none motion-reduce:transition-none",
                            started &&
                              "transition-all duration-(--step-duration) ease-linear motion-reduce:transition-none"
                          )}
                        />
                      </svg>
                    ) : (
                      <span className="size-4 rounded-full border border-dashed border-muted-foreground/30" />
                    )}
                  </span>
                  <span className={cn(done && "line-through")}>{step}</span>
                </li>
              );
            })}
          </ol>
        </CollapsibleContent>
      </Collapsible>
    </section>
  );
}