shadimage
elements
uiagent-statussource: ok · hit

AI Agent Status

Display current agent handling a request with status indicators, animated pulse for active states, and handoff visualization for multi-agent workflows.

npx shadcn add "https://www.tryelements.dev/r/agent-status.json"
free

Where it goes

stats

Interaction

animated-counter

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Dependencies

Bundle weight · light
iconslucide-react

Files1 · source cached 10/13/2026, 1:26:02 PM

registry/default/blocks/ai/ai-agent-status/components/elements/ai-agent-status.tsx
"use client";

import * as React from "react";

import {
  ArrowRight,
  Bot,
  Brain,
  Check,
  Clock,
  Loader2,
  Pause,
} from "lucide-react";

import { cn } from "@/lib/utils";

type AgentStatus =
  | "idle"
  | "thinking"
  | "acting"
  | "waiting"
  | "done"
  | "handoff";

interface AgentInfo {
  name: string;
  icon?: React.ReactNode;
  description?: string;
}

interface AiAgentStatusContextValue {
  agent: AgentInfo;
  status: AgentStatus;
  handoffTo?: string;
  variant: "badge" | "card";
}

const AiAgentStatusContext =
  React.createContext<AiAgentStatusContextValue | null>(null);

function useAgentStatusContext() {
  const context = React.useContext(AiAgentStatusContext);
  if (!context) {
    throw new Error(
      "AiAgentStatus components must be used within <AiAgentStatus>",
    );
  }
  return context;
}

interface AiAgentStatusProps {
  agent: AgentInfo;
  status: AgentStatus;
  handoffTo?: string;
  variant?: "badge" | "card";
  children?: React.ReactNode;
  className?: string;
}

function AiAgentStatus({
  agent,
  status,
  handoffTo,
  variant = "badge",
  children,
  className,
}: AiAgentStatusProps) {
  const contextValue = React.useMemo(
    () => ({ agent, status, handoffTo, variant }),
    [agent, status, handoffTo, variant],
  );

  if (variant === "badge") {
    return (
      <AiAgentStatusContext.Provider value={contextValue}>
        <AiAgentStatusBadge className={className}>
          {children}
        </AiAgentStatusBadge>
      </AiAgentStatusContext.Provider>
    );
  }

  return (
    <AiAgentStatusContext.Provider value={contextValue}>
      <AiAgentStatusCard className={className}>{children}</AiAgentStatusCard>
    </AiAgentStatusContext.Provider>
  );
}

interface AiAgentStatusBadgeProps {
  children?: React.ReactNode;
  className?: string;
}

function AiAgentStatusBadge({ children, className }: AiAgentStatusBadgeProps) {
  const { agent, status, handoffTo } = useAgentStatusContext();

  const statusConfig = React.useMemo(() => {
    const configs: Record<
      AgentStatus,
      { icon: React.ReactNode; label: string; className: string }
    > = {
      idle: {
        icon: <Clock className="size-3" />,
        label: "Idle",
        className: "bg-muted text-muted-foreground",
      },
      thinking: {
        icon: <Brain className="size-3 animate-pulse" />,
        label: "Thinking",
        className:
          "bg-purple-100 text-purple-700 dark:bg-purple-950 dark:text-purple-300",
      },
      acting: {
        icon: <Loader2 className="size-3 animate-spin" />,
        label: "Acting",
        className:
          "bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-300",
      },
      waiting: {
        icon: <Pause className="size-3" />,
        label: "Waiting",
        className:
          "bg-amber-100 text-amber-700 dark:bg-amber-950 dark:text-amber-300",
      },
      done: {
        icon: <Check className="size-3" />,
        label: "Done",
        className:
          "bg-green-100 text-green-700 dark:bg-green-950 dark:text-green-300",
      },
      handoff: {
        icon: <ArrowRight className="size-3 animate-pulse" />,
        label: "Handing off",
        className:
          "bg-orange-100 text-orange-700 dark:bg-orange-950 dark:text-orange-300",
      },
    };
    return configs[status];
  }, [status]);

  const isActive = status === "thinking" || status === "acting";

  return (
    <div
      data-slot="ai-agent-status"
      data-status={status}
      data-variant="badge"
      className={cn(
        "inline-flex items-center gap-2 rounded-full border px-3 py-1.5 text-sm",
        isActive && "ring-2 ring-offset-2 ring-offset-background",
        status === "thinking" && "ring-purple-500/50",
        status === "acting" && "ring-blue-500/50",
        className,
      )}
    >
      <div className="flex items-center gap-1.5">
        <span className="relative flex size-5 shrink-0 items-center justify-center">
          {agent.icon ?? <Bot className="size-4" />}
          {isActive && (
            <span className="absolute -right-0.5 -top-0.5 size-2 rounded-full bg-green-500 animate-pulse" />
          )}
        </span>
        <span className="font-medium">{agent.name}</span>
      </div>
      <span
        className={cn(
          "inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium",
          statusConfig.className,
        )}
      >
        {statusConfig.icon}
        {statusConfig.label}
      </span>
      {status === "handoff" && handoffTo && (
        <span className="inline-flex items-center gap-1 text-xs text-muted-foreground">
          <ArrowRight className="size-3" />
          <span>{handoffTo}</span>
        </span>
      )}
      {children}
    </div>
  );
}

interface AiAgentStatusCardProps {
  children?: React.ReactNode;
  className?: string;
}

function AiAgentStatusCard({ children, className }: AiAgentStatusCardProps) {
  const { agent, status, handoffTo } = useAgentStatusContext();

  const statusConfig = React.useMemo(() => {
    const configs: Record<
      AgentStatus,
      {
        icon: React.ReactNode;
        label: string;
        className: string;
        borderClassName: string;
      }
    > = {
      idle: {
        icon: <Clock className="size-4" />,
        label: "Idle",
        className: "bg-muted text-muted-foreground",
        borderClassName: "border-border",
      },
      thinking: {
        icon: <Brain className="size-4 animate-pulse" />,
        label: "Thinking",
        className:
          "bg-purple-100 text-purple-700 dark:bg-purple-950 dark:text-purple-300",
        borderClassName: "border-purple-300 dark:border-purple-800",
      },
      acting: {
        icon: <Loader2 className="size-4 animate-spin" />,
        label: "Acting",
        className:
          "bg-blue-100 text-blue-700 dark:bg-blue-950 dark:text-blue-300",
        borderClassName: "border-blue-300 dark:border-blue-800",
      },
      waiting: {
        icon: <Pause className="size-4" />,
        label: "Waiting",
        className:
          "bg-amber-100 text-amber-700 dark:bg-amber-950 dark:text-amber-300",
        borderClassName: "border-amber-300 dark:border-amber-800",
      },
      done: {
        icon: <Check className="size-4" />,
        label: "Done",
        className:
          "bg-green-100 text-green-700 dark:bg-green-950 dark:text-green-300",
        borderClassName: "border-green-300 dark:border-green-800",
      },
      handoff: {
        icon: <ArrowRight className="size-4 animate-pulse" />,
        label: "Handing off",
        className:
          "bg-orange-100 text-orange-700 dark:bg-orange-950 dark:text-orange-300",
        borderClassName: "border-orange-300 dark:border-orange-800",
      },
    };
    return configs[status];
  }, [status]);

  const isActive = status === "thinking" || status === "acting";

  return (
    <div
      data-slot="ai-agent-status"
      data-status={status}
      data-variant="card"
      className={cn(
        "rounded-lg border bg-card p-4 text-card-foreground transition-colors",
        statusConfig.borderClassName,
        className,
      )}
    >
      <div className="flex items-start gap-3">
        <div className="relative flex size-10 shrink-0 items-center justify-center rounded-lg bg-muted">
          {agent.icon ?? <Bot className="size-5 text-muted-foreground" />}
          {isActive && (
            <span className="absolute -right-1 -top-1 size-3 rounded-full border-2 border-background bg-green-500 animate-pulse" />
          )}
        </div>
        <div className="flex-1 min-w-0">
          <div className="flex items-center gap-2">
            <h3 className="font-semibold text-sm">{agent.name}</h3>
            <span
              className={cn(
                "inline-flex items-center gap-1 rounded-full px-2 py-0.5 text-xs font-medium",
                statusConfig.className,
              )}
            >
              {statusConfig.icon}
              {statusConfig.label}
            </span>
          </div>
          {agent.description && (
            <p className="mt-1 text-xs text-muted-foreground line-clamp-2">
              {agent.description}
            </p>
          )}
          {status === "handoff" && handoffTo && (
            <div className="mt-2 flex items-center gap-2 text-xs text-muted-foreground">
              <ArrowRight className="size-3" />
              <span>
                Transferring to <span className="font-medium">{handoffTo}</span>
              </span>
            </div>
          )}
        </div>
      </div>
      {children && <div className="mt-3 border-t pt-3">{children}</div>}
    </div>
  );
}

interface AiAgentStatusActionsProps {
  children?: React.ReactNode;
  className?: string;
}

function AiAgentStatusActions({
  children,
  className,
}: AiAgentStatusActionsProps) {
  return (
    <div
      data-slot="ai-agent-status-actions"
      className={cn("flex items-center gap-2", className)}
    >
      {children}
    </div>
  );
}

export {
  AiAgentStatus,
  AiAgentStatusBadge,
  AiAgentStatusCard,
  AiAgentStatusActions,
};
export type { AiAgentStatusProps, AgentInfo, AgentStatus };

More from elements

Browse all components in this library →