shadimage
elements
uiagent-rostersource: ok · hit

AI Agent Roster

Display a directory of AI agents with status indicators, match patterns, and selection support. Supports grid and list layouts for multi-agent orchestration UIs.

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

Where it goes

stats

Interaction

animated-counterdropdown

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/9/2026, 11:08:10 PM

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

import * as React from "react";

import {
  Bot,
  Check,
  ChevronRight,
  Clock,
  Cpu,
  Loader2,
  Power,
} from "lucide-react";

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

type AgentStatus = "active" | "idle" | "busy" | "offline";

interface RosterAgent {
  id: string;
  name: string;
  description?: string;
  matchOn?: string[];
  status: AgentStatus;
  icon?: React.ReactNode;
  model?: string;
}

interface AiAgentRosterContextValue {
  agents: RosterAgent[];
  activeAgentId?: string;
  layout: "grid" | "list";
  onSelect?: (agentId: string) => void;
}

const AiAgentRosterContext =
  React.createContext<AiAgentRosterContextValue | null>(null);

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

interface AiAgentRosterProps {
  agents: RosterAgent[];
  activeAgentId?: string;
  onSelect?: (agentId: string) => void;
  layout?: "grid" | "list";
  children?: React.ReactNode;
  className?: string;
}

function AiAgentRoster({
  agents,
  activeAgentId,
  onSelect,
  layout = "grid",
  children,
  className,
}: AiAgentRosterProps) {
  const contextValue = React.useMemo(
    () => ({ agents, activeAgentId, layout, onSelect }),
    [agents, activeAgentId, layout, onSelect],
  );

  return (
    <AiAgentRosterContext.Provider value={contextValue}>
      <div
        data-slot="ai-agent-roster"
        data-layout={layout}
        className={cn(
          "rounded-lg border border-border bg-card text-card-foreground",
          className,
        )}
      >
        {children || <AiAgentRosterContent />}
      </div>
    </AiAgentRosterContext.Provider>
  );
}

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

function AiAgentRosterHeader({
  children,
  className,
}: AiAgentRosterHeaderProps) {
  const { agents } = useAgentRosterContext();

  const activeCount = React.useMemo(
    () =>
      agents.filter((a) => a.status === "active" || a.status === "busy").length,
    [agents],
  );

  return (
    <div
      data-slot="ai-agent-roster-header"
      className={cn(
        "flex items-center justify-between border-b border-border px-4 py-3",
        className,
      )}
    >
      <div className="flex items-center gap-2">
        <div className="flex size-8 shrink-0 items-center justify-center rounded-md bg-muted">
          <Bot className="size-4 text-muted-foreground" />
        </div>
        <div>
          <h3 className="font-semibold text-sm">
            {children || "Agent Roster"}
          </h3>
          <p className="text-xs text-muted-foreground">
            {activeCount} of {agents.length} active
          </p>
        </div>
      </div>
    </div>
  );
}

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

function AiAgentRosterContent({
  children,
  className,
}: AiAgentRosterContentProps) {
  const { agents, layout } = useAgentRosterContext();

  return (
    <div
      data-slot="ai-agent-roster-content"
      className={cn(
        "p-4",
        layout === "grid" ? "grid gap-3 sm:grid-cols-2" : "flex flex-col gap-2",
        className,
      )}
    >
      {children ||
        agents.map((agent) => <AiAgentCard key={agent.id} agent={agent} />)}
    </div>
  );
}

interface AiAgentCardProps {
  agent: RosterAgent;
  className?: string;
}

function AiAgentCard({ agent, className }: AiAgentCardProps) {
  const { activeAgentId, onSelect } = useAgentRosterContext();

  const isActive = activeAgentId === agent.id;

  const statusConfig = React.useMemo(() => {
    const configs: Record<
      AgentStatus,
      {
        icon: React.ReactNode;
        label: string;
        className: string;
        dotClassName: string;
      }
    > = {
      active: {
        icon: <Check className="size-3" />,
        label: "Active",
        className: "text-green-700 dark:text-green-400",
        dotClassName: "bg-green-500",
      },
      idle: {
        icon: <Clock className="size-3" />,
        label: "Idle",
        className: "text-muted-foreground",
        dotClassName: "bg-muted-foreground",
      },
      busy: {
        icon: <Loader2 className="size-3 animate-spin" />,
        label: "Busy",
        className: "text-blue-700 dark:text-blue-400",
        dotClassName: "bg-blue-500 animate-pulse",
      },
      offline: {
        icon: <Power className="size-3" />,
        label: "Offline",
        className: "text-muted-foreground/50",
        dotClassName: "bg-muted-foreground/50",
      },
    };
    return configs[agent.status];
  }, [agent.status]);

  const handleClick = React.useCallback(() => {
    if (onSelect && agent.status !== "offline") {
      onSelect(agent.id);
    }
  }, [onSelect, agent.id, agent.status]);

  const handleKeyDown = React.useCallback(
    (e: React.KeyboardEvent) => {
      if (
        (e.key === "Enter" || e.key === " ") &&
        onSelect &&
        agent.status !== "offline"
      ) {
        e.preventDefault();
        onSelect(agent.id);
      }
    },
    [onSelect, agent.id, agent.status],
  );

  return (
    <div
      data-slot="ai-agent-card"
      data-status={agent.status}
      data-active={isActive}
      role={onSelect ? "button" : undefined}
      tabIndex={onSelect && agent.status !== "offline" ? 0 : undefined}
      onClick={handleClick}
      onKeyDown={handleKeyDown}
      className={cn(
        "group relative flex flex-col gap-2 rounded-lg border bg-background p-3 transition-all",
        isActive
          ? "border-primary ring-2 ring-primary/20"
          : "border-border hover:border-muted-foreground/30",
        onSelect && agent.status !== "offline" && "cursor-pointer",
        agent.status === "offline" && "opacity-60",
        className,
      )}
    >
      <div className="flex items-start justify-between gap-2">
        <div className="flex items-center gap-2">
          <div
            className={cn(
              "flex size-9 shrink-0 items-center justify-center rounded-md transition-colors",
              isActive
                ? "bg-primary text-primary-foreground"
                : "bg-muted text-muted-foreground",
            )}
          >
            {agent.icon || <Cpu className="size-4" />}
          </div>
          <div className="min-w-0">
            <h4 className="font-medium text-sm truncate">{agent.name}</h4>
            {agent.model && (
              <p className="text-xs text-muted-foreground font-mono truncate">
                {agent.model}
              </p>
            )}
          </div>
        </div>
        <div
          className={cn(
            "flex items-center gap-1.5 text-xs font-medium",
            statusConfig.className,
          )}
        >
          <span
            className={cn("size-2 rounded-full", statusConfig.dotClassName)}
          />
          <span className="hidden sm:inline">{statusConfig.label}</span>
        </div>
      </div>

      {agent.description && (
        <p className="text-xs text-muted-foreground line-clamp-2">
          {agent.description}
        </p>
      )}

      {agent.matchOn && agent.matchOn.length > 0 && (
        <div className="flex flex-wrap gap-1">
          {agent.matchOn.slice(0, 3).map((pattern, index) => (
            <span
              key={index}
              className="inline-flex items-center rounded-md bg-muted px-1.5 py-0.5 text-[10px] font-mono text-muted-foreground"
            >
              {pattern}
            </span>
          ))}
          {agent.matchOn.length > 3 && (
            <span className="inline-flex items-center rounded-md bg-muted px-1.5 py-0.5 text-[10px] text-muted-foreground">
              +{agent.matchOn.length - 3}
            </span>
          )}
        </div>
      )}

      {onSelect && agent.status !== "offline" && (
        <ChevronRight
          className={cn(
            "absolute right-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground opacity-0 transition-all group-hover:opacity-100 group-hover:translate-x-0.5",
            isActive && "opacity-100 text-primary",
          )}
        />
      )}
    </div>
  );
}

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

function AiAgentRosterEmpty({ children, className }: AiAgentRosterEmptyProps) {
  return (
    <div
      data-slot="ai-agent-roster-empty"
      className={cn(
        "flex flex-col items-center justify-center py-8 text-center",
        className,
      )}
    >
      <div className="flex size-12 items-center justify-center rounded-full bg-muted mb-3">
        <Bot className="size-6 text-muted-foreground" />
      </div>
      <p className="text-sm text-muted-foreground">
        {children || "No agents available"}
      </p>
    </div>
  );
}

export {
  AiAgentRoster,
  AiAgentRosterHeader,
  AiAgentRosterContent,
  AiAgentCard,
  AiAgentRosterEmpty,
};
export type { AiAgentRosterProps, RosterAgent, AgentStatus };

More from elements

Browse all components in this library →