AI Patterns

agents

Create Agent

A two-step wizard card for naming an agent, picking its icon, and selecting which event sources activate it.

New agent

Name your agent

Give it a name, instructions, and an icon.

  • Require approvalHuman must approve before any write action
  • Read-only by defaultCannot push code or send messages directly
  • Limit to 1 action per triggerPrevents runaway loops on rapid events
"use client";

import * as React from "react";
import { AnimatePresence, motion } from "motion/react";
import {
  Antenna,
  AtSign,
  Bot,
  Brain,
  Check,
  ChevronRight,
  Code2,
  Compass,
  Cpu,
  Database,
  Eye,
  FlaskConical,
  GitBranch,
  Globe,
  Layers,
  Lightbulb,
  Plus,
  Rocket,
  Search,
  Server,
  Shield,
  ShieldAlert,
  ShieldOff,
  Sparkles,
  Star,
  Terminal,
  Webhook,
  Wrench,
  X,
  Zap,
} from "lucide-react";

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

// ── Trigger sources ──────────────────────────────────────────────────────────

interface TriggerField {
  id: string;
  label: string;
  type: "text" | "select";
  placeholder?: string;
  options?: { value: string; label: string }[];
}

export const TRIGGER_SOURCES = [
  {
    id: "github" as const,
    label: "GitHub",
    icon: GitBranch,
    description: "Issues, pull requests, and labels",
    fields: [
      {
        id: "repo",
        label: "Repository",
        type: "text" as const,
        placeholder: "e.g. carlosmarch/ai-patterns",
      },
      {
        id: "event",
        label: "Event",
        type: "select" as const,
        options: [
          { value: "issue-labeled", label: "Issue labeled" },
          { value: "pr-opened", label: "PR opened" },
          { value: "pr-merged", label: "PR merged" },
        ],
      },
    ] satisfies TriggerField[],
  },
  {
    id: "slack" as const,
    label: "Slack",
    icon: AtSign,
    description: "Mentions, messages, and reactions",
    fields: [
      {
        id: "channel",
        label: "Channel",
        type: "text" as const,
        placeholder: "e.g. #design-requests",
      },
      {
        id: "event",
        label: "Trigger on",
        type: "select" as const,
        options: [
          { value: "mention", label: "Agent mentioned" },
          { value: "keyword", label: "Keyword match" },
        ],
      },
    ] satisfies TriggerField[],
  },
  {
    id: "figma" as const,
    label: "Figma",
    icon: Layers,
    description: "Frame status changes and comments",
    fields: [
      {
        id: "scope",
        label: "Files",
        type: "select" as const,
        options: [
          { value: "any", label: "Any Figma file" },
          { value: "specific", label: "Specific file URL" },
        ],
      },
      {
        id: "event",
        label: "Event",
        type: "select" as const,
        options: [
          { value: "ready-for-dev", label: "Frame ready for dev" },
          { value: "comment", label: "New comment" },
        ],
      },
    ] satisfies TriggerField[],
  },
] as const;

export type TriggerSourceId = (typeof TRIGGER_SOURCES)[number]["id"];

// ── Agent icons ───────────────────────────────────────────────────────────────

export const AGENT_ICONS = [
  { id: "bot" as const, icon: Bot, label: "Bot" },
  { id: "sparkles" as const, icon: Sparkles, label: "Sparkles" },
  { id: "zap" as const, icon: Zap, label: "Zap" },
  { id: "brain" as const, icon: Brain, label: "Brain" },
  { id: "rocket" as const, icon: Rocket, label: "Rocket" },
] as const;

export const EXTRA_AGENT_ICONS = [
  { id: "cpu" as const, icon: Cpu, label: "CPU" },
  { id: "globe" as const, icon: Globe, label: "Globe" },
  { id: "terminal" as const, icon: Terminal, label: "Terminal" },
  { id: "code2" as const, icon: Code2, label: "Code" },
  { id: "wrench" as const, icon: Wrench, label: "Wrench" },
  { id: "search" as const, icon: Search, label: "Search" },
  { id: "eye" as const, icon: Eye, label: "Eye" },
  { id: "star" as const, icon: Star, label: "Star" },
  { id: "database" as const, icon: Database, label: "Database" },
  { id: "server" as const, icon: Server, label: "Server" },
  { id: "lightbulb" as const, icon: Lightbulb, label: "Lightbulb" },
  { id: "compass" as const, icon: Compass, label: "Compass" },
  { id: "flask" as const, icon: FlaskConical, label: "Flask" },
  { id: "antenna" as const, icon: Antenna, label: "Antenna" },
] as const;

export const ALL_AGENT_ICONS = [...AGENT_ICONS, ...EXTRA_AGENT_ICONS];

export type AgentIconId = (typeof ALL_AGENT_ICONS)[number]["id"];

// ── Guardrails ────────────────────────────────────────────────────────────────

export interface Guardrail {
  id: string;
  label: string;
  description: string;
  icon: React.ComponentType<{ className?: string }>;
  enabled: boolean;
}

export const DEFAULT_GUARDRAILS: Guardrail[] = [
  {
    id: "approval-required",
    label: "Require approval",
    description: "Human must approve before any write action",
    icon: ShieldAlert,
    enabled: true,
  },
  {
    id: "read-only",
    label: "Read-only by default",
    description: "Cannot push code or send messages directly",
    icon: ShieldOff,
    enabled: true,
  },
  {
    id: "rate-limit",
    label: "Limit to 1 action per trigger",
    description: "Prevents runaway loops on rapid events",
    icon: Shield,
    enabled: false,
  },
];

// ── Payload ───────────────────────────────────────────────────────────────────

export interface SelectedTrigger {
  sourceId: TriggerSourceId;
  config: Record<string, string>;
}

export interface CustomTrigger {
  id: string;
  name: string;
  event: string;
}

export interface CreateAgentPayload {
  name: string;
  instructions: string;
  iconId: AgentIconId;
  guardrails: { id: string; enabled: boolean }[];
  triggers: SelectedTrigger[];
  customTriggers: CustomTrigger[];
}

// ── Props ─────────────────────────────────────────────────────────────────────

export interface CreateAgentProps {
  onCreateAgent?: (payload: CreateAgentPayload) => void;
  className?: string;
}

// ── Component ─────────────────────────────────────────────────────────────────

export function CreateAgent({ onCreateAgent, className }: CreateAgentProps) {
  const [step, setStep] = React.useState<0 | 1>(0);
  const [direction, setDirection] = React.useState<1 | -1>(1);
  const [name, setName] = React.useState("");
  const [instructions, setInstructions] = React.useState("");
  const [iconId, setIconId] = React.useState<AgentIconId>("bot");
  const [selectedSources, setSelectedSources] = React.useState<TriggerSourceId[]>([]);
  const [triggerConfigs, setTriggerConfigs] = React.useState<
    Partial<Record<TriggerSourceId, Record<string, string>>>
  >({});
  const [customTriggers, setCustomTriggers] = React.useState<CustomTrigger[]>([]);
  const [showMoreIcons, setShowMoreIcons] = React.useState(false);
  const [guardrailStates, setGuardrailStates] = React.useState<Record<string, boolean>>(
    Object.fromEntries(DEFAULT_GUARDRAILS.map((g) => [g.id, g.enabled]))
  );

  const selectedIcon = ALL_AGENT_ICONS.find((i) => i.id === iconId)!;
  const SelectedIconComp = selectedIcon.icon;

  function goToStep1() {
    setDirection(1);
    setStep(1);
  }

  function goToStep0() {
    setDirection(-1);
    setStep(0);
  }

  function toggleSource(id: TriggerSourceId) {
    setSelectedSources((prev) =>
      prev.includes(id) ? prev.filter((s) => s !== id) : [...prev, id]
    );
  }

  function setTriggerField(sourceId: TriggerSourceId, fieldId: string, value: string) {
    setTriggerConfigs((prev) => ({
      ...prev,
      [sourceId]: { ...prev[sourceId], [fieldId]: value },
    }));
  }

  function addCustomTrigger() {
    setCustomTriggers((prev) => [
      ...prev,
      { id: `custom-${Date.now()}`, name: "", event: "" },
    ]);
  }

  function updateCustomTrigger(id: string, field: keyof Omit<CustomTrigger, "id">, value: string) {
    setCustomTriggers((prev) =>
      prev.map((t) => (t.id === id ? { ...t, [field]: value } : t))
    );
  }

  function removeCustomTrigger(id: string) {
    setCustomTriggers((prev) => prev.filter((t) => t.id !== id));
  }

  function toggleGuardrail(id: string) {
    setGuardrailStates((prev) => ({ ...prev, [id]: !prev[id] }));
  }

  function buildPayload(): CreateAgentPayload {
    return {
      name: name.trim() || "Unnamed Agent",
      instructions: instructions.trim(),
      iconId,
      guardrails: DEFAULT_GUARDRAILS.map((g) => ({ id: g.id, enabled: guardrailStates[g.id] ?? g.enabled })),
      triggers: selectedSources.map((sourceId) => ({
        sourceId,
        config: triggerConfigs[sourceId] ?? {},
      })),
      customTriggers,
    };
  }

  const variants = {
    enter: (dir: number) => ({ x: dir * 16, opacity: 0 }),
    center: { x: 0, opacity: 1 },
    exit: (dir: number) => ({ x: dir * -16, opacity: 0 }),
  };

  return (
    <div suppressHydrationWarning className={cn("w-full max-w-xs overflow-hidden rounded-2xl border bg-card", className)}>
      {/* Header preview */}
      <div className="flex items-center justify-between gap-3 border-b px-4 py-3">
        <div className="flex min-w-0 items-center gap-2">
          <span className="flex size-7 shrink-0 items-center justify-center rounded-full bg-muted text-muted-foreground">
            <SelectedIconComp className="size-3.5" />
          </span>
          <p className="truncate text-sm font-semibold text-foreground">
            {name.trim() || "New agent"}
          </p>
        </div>
        <div className="flex shrink-0 items-center gap-1" aria-hidden>
          {[0, 1].map((i) => (
            <span
              key={i}
              className={cn(
                "block h-1.5 rounded-full transition-all duration-200",
                step === i ? "w-4 bg-foreground" : "w-1.5 bg-muted-foreground/30"
              )}
            />
          ))}
        </div>
      </div>

      {/* Steps */}
      <div className="relative overflow-hidden">
        <AnimatePresence initial={false} custom={direction} mode="wait">
          {step === 0 ? (
            <motion.div
              key="step-identity"
              custom={direction}
              variants={variants}
              initial="enter"
              animate="center"
              exit="exit"
              transition={{ duration: 0.15, ease: "easeInOut" }}
              className="px-4 pb-4 pt-3"
            >
              <p className="text-sm font-semibold">Name your agent</p>
              <p className="mt-0.5 text-xs text-muted-foreground">
                Give it a name, instructions, and an icon.
              </p>

              <input
                type="text"
                value={name}
                onChange={(e) => setName(e.target.value)}
                placeholder="e.g. AI-Patterns"
                aria-label="Agent name"
                className="mt-3 w-full rounded-lg border bg-background px-3 py-2 text-sm outline-none ring-ring placeholder:text-muted-foreground/50 focus-visible:ring-2"
              />

              <textarea
                value={instructions}
                onChange={(e) => setInstructions(e.target.value)}
                placeholder="e.g. When a GitHub issue is labeled pattern-request in carlosmarch/ai-patterns, triage it and suggest a matching component."
                aria-label="Agent instructions"
                rows={3}
                className="mt-2 w-full resize-none rounded-lg border bg-background px-3 py-2 text-sm outline-none ring-ring placeholder:text-muted-foreground/50 focus-visible:ring-2"
              />

              <ul className="mt-3 divide-y rounded-xl border">
                {DEFAULT_GUARDRAILS.map(({ id, label, description, icon: Icon }) => {
                  const enabled = guardrailStates[id] ?? false;
                  return (
                    <li key={id} className="flex items-center gap-3 px-3 py-2.5">
                      <span className={cn("flex size-6 shrink-0 items-center justify-center rounded text-muted-foreground")}>
                        <Icon className="size-3.5" />
                      </span>
                      <span className="min-w-0 flex-1">
                        <span className="block text-xs font-medium">{label}</span>
                        <span className="block text-[11px] text-muted-foreground">{description}</span>
                      </span>
                      <button
                        type="button"
                        role="switch"
                        aria-checked={enabled}
                        aria-label={label}
                        onClick={() => toggleGuardrail(id)}
                        className={cn(
                          "relative inline-flex h-5 w-9 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
                          enabled ? "bg-foreground" : "bg-muted"
                        )}
                      >
                        <motion.span
                          aria-hidden
                          className="pointer-events-none inline-block h-4 w-4 rounded-full bg-background shadow-sm"
                          animate={{ x: enabled ? 16 : 0 }}
                          transition={{ type: "spring", stiffness: 500, damping: 30 }}
                        />
                      </button>
                    </li>
                  );
                })}
              </ul>

              <div className="mt-3 space-y-1.5">
                <div className="flex items-center gap-1.5">
                  {AGENT_ICONS.map(({ id, icon: Icon, label }) => (
                    <button
                      key={id}
                      type="button"
                      aria-label={label}
                      aria-pressed={iconId === id}
                      onClick={() => setIconId(id)}
                      className={cn(
                        "flex size-8 items-center justify-center rounded-lg transition-colors",
                        iconId === id
                          ? "bg-foreground text-background"
                          : "bg-muted text-muted-foreground hover:text-foreground"
                      )}
                    >
                      <Icon className="size-4" />
                    </button>
                  ))}
                  <button
                    type="button"
                    aria-label={showMoreIcons ? "Show fewer icons" : "Show more icons"}
                    aria-expanded={showMoreIcons}
                    onClick={() => setShowMoreIcons((v) => !v)}
                    className={cn(
                      "flex size-8 items-center justify-center rounded-lg transition-colors",
                      showMoreIcons
                        ? "bg-muted text-foreground"
                        : "bg-muted text-muted-foreground hover:text-foreground"
                    )}
                  >
                    <Plus className={cn("size-4 transition-transform duration-150", showMoreIcons && "rotate-45")} />
                  </button>
                </div>

                <AnimatePresence initial={false}>
                  {showMoreIcons && (
                    <motion.div
                      initial={{ height: 0, opacity: 0 }}
                      animate={{ height: "auto", opacity: 1 }}
                      exit={{ height: 0, opacity: 0 }}
                      transition={{ duration: 0.15, ease: "easeInOut" }}
                      className="overflow-hidden"
                    >
                      <div className="flex flex-wrap gap-1.5 pt-0.5">
                        {EXTRA_AGENT_ICONS.map(({ id, icon: Icon, label }) => (
                          <button
                            key={id}
                            type="button"
                            aria-label={label}
                            aria-pressed={iconId === id}
                            onClick={() => { setIconId(id); setShowMoreIcons(false); }}
                            className={cn(
                              "flex size-8 items-center justify-center rounded-lg transition-colors",
                              iconId === id
                                ? "bg-foreground text-background"
                                : "bg-muted text-muted-foreground hover:text-foreground"
                            )}
                          >
                            <Icon className="size-4" />
                          </button>
                        ))}
                      </div>
                    </motion.div>
                  )}
                </AnimatePresence>
              </div>

              <button
                type="button"
                onClick={goToStep1}
                className="mt-4 flex w-full items-center justify-center gap-1.5 rounded-lg bg-foreground px-4 py-2 text-sm font-medium text-background transition-opacity hover:opacity-90"
              >
                Choose triggers
                <ChevronRight className="size-3.5" />
              </button>

              <button
                type="button"
                onClick={() => onCreateAgent?.(buildPayload())}
                className="mt-2 w-full text-center text-xs text-muted-foreground transition-colors hover:text-foreground"
              >
                Skip triggers
              </button>
            </motion.div>
          ) : (
            <motion.div
              key="step-triggers"
              custom={direction}
              variants={variants}
              initial="enter"
              animate="center"
              exit="exit"
              transition={{ duration: 0.15, ease: "easeInOut" }}
              className="px-4 pb-4 pt-3"
            >
              <p className="text-sm font-semibold">Choose triggers</p>
              <p className="mt-0.5 text-xs text-muted-foreground">
                Select the sources that activate this agent.
              </p>

              <ul className="mt-3 flex flex-col gap-2">
                {TRIGGER_SOURCES.map(({ id, label, icon: Icon, description, fields }) => {
                  const isSelected = selectedSources.includes(id);
                  const config = triggerConfigs[id] ?? {};

                  return (
                    <li key={id} className={cn("rounded-xl border transition-colors", isSelected ? "border-foreground/20 bg-muted/40" : "")}>
                      {/* Toggle row */}
                      <button
                        type="button"
                        aria-pressed={isSelected}
                        onClick={() => toggleSource(id)}
                        className="flex w-full items-center gap-3 px-3 py-2.5 text-left"
                      >
                        <span className="flex size-7 shrink-0 items-center justify-center rounded-md border bg-background text-muted-foreground">
                          <Icon className="size-3.5" />
                        </span>
                        <span className="min-w-0 flex-1">
                          <span className="block text-xs font-medium">{label}</span>
                          <span className="block text-[11px] text-muted-foreground">{description}</span>
                        </span>
                        <span
                          className={cn(
                            "flex size-4 shrink-0 items-center justify-center rounded-full border transition-colors",
                            isSelected
                              ? "border-foreground bg-foreground text-background"
                              : "border-muted-foreground/30"
                          )}
                        >
                          {isSelected && <Check className="size-2.5" strokeWidth={3} />}
                        </span>
                      </button>

                      {/* Expanded config */}
                      <AnimatePresence initial={false}>
                        {isSelected && (
                          <motion.div
                            initial={{ height: 0, opacity: 0 }}
                            animate={{ height: "auto", opacity: 1 }}
                            exit={{ height: 0, opacity: 0 }}
                            transition={{ duration: 0.15, ease: "easeInOut" }}
                            className="overflow-hidden"
                          >
                            <div className="flex flex-col gap-2 border-t px-3 pb-3 pt-2.5">
                              {fields.map((field) => (
                                <div key={field.id} className="flex flex-col gap-1">
                                  <label className="text-[11px] font-medium text-muted-foreground">
                                    {field.label}
                                  </label>
                                  {field.type === "text" ? (
                                    <input
                                      type="text"
                                      value={config[field.id] ?? ""}
                                      onChange={(e) => setTriggerField(id, field.id, e.target.value)}
                                      placeholder={field.placeholder}
                                      className="w-full rounded-md border bg-background px-2.5 py-1.5 text-xs outline-none ring-ring placeholder:text-muted-foreground/40 focus-visible:ring-2"
                                    />
                                  ) : (
                                    <select
                                      value={config[field.id] ?? field.options?.[0]?.value ?? ""}
                                      onChange={(e) => setTriggerField(id, field.id, e.target.value)}
                                      className="w-full rounded-md border bg-background px-2.5 py-1.5 text-xs outline-none ring-ring focus-visible:ring-2"
                                    >
                                      {field.options?.map((opt) => (
                                        <option key={opt.value} value={opt.value}>
                                          {opt.label}
                                        </option>
                                      ))}
                                    </select>
                                  )}
                                </div>
                              ))}
                            </div>
                          </motion.div>
                        )}
                      </AnimatePresence>
                    </li>
                  );
                })}
              </ul>

              {/* Custom triggers */}
              <AnimatePresence initial={false}>
                {customTriggers.map((ct) => (
                  <motion.div
                    key={ct.id}
                    initial={{ height: 0, opacity: 0 }}
                    animate={{ height: "auto", opacity: 1 }}
                    exit={{ height: 0, opacity: 0 }}
                    transition={{ duration: 0.15, ease: "easeInOut" }}
                    className="overflow-hidden"
                  >
                    <div className="mt-2 rounded-xl border border-foreground/20 bg-muted/40">
                      <div className="flex items-center gap-2 px-3 py-2.5">
                        <span className="flex size-7 shrink-0 items-center justify-center rounded-md border bg-background text-muted-foreground">
                          <Webhook className="size-3.5" />
                        </span>
                        <span className="flex-1 text-xs font-medium text-muted-foreground">
                          Custom trigger
                        </span>
                        <button
                          type="button"
                          aria-label="Remove custom trigger"
                          onClick={() => removeCustomTrigger(ct.id)}
                          className="flex size-5 items-center justify-center rounded text-muted-foreground/50 transition-colors hover:text-foreground"
                        >
                          <X className="size-3.5" />
                        </button>
                      </div>
                      <div className="flex flex-col gap-2 border-t px-3 pb-3 pt-2.5">
                        <div className="flex flex-col gap-1">
                          <label className="text-[11px] font-medium text-muted-foreground">
                            Name
                          </label>
                          <input
                            type="text"
                            value={ct.name}
                            onChange={(e) => updateCustomTrigger(ct.id, "name", e.target.value)}
                            placeholder="e.g. Webhook received"
                            className="w-full rounded-md border bg-background px-2.5 py-1.5 text-xs outline-none ring-ring placeholder:text-muted-foreground/40 focus-visible:ring-2"
                          />
                        </div>
                        <div className="flex flex-col gap-1">
                          <label className="text-[11px] font-medium text-muted-foreground">
                            Event source
                          </label>
                          <input
                            type="text"
                            value={ct.event}
                            onChange={(e) => updateCustomTrigger(ct.id, "event", e.target.value)}
                            placeholder="e.g. POST /webhooks/my-agent"
                            className="w-full rounded-md border bg-background px-2.5 py-1.5 text-xs outline-none ring-ring placeholder:text-muted-foreground/40 focus-visible:ring-2"
                          />
                        </div>
                      </div>
                    </div>
                  </motion.div>
                ))}
              </AnimatePresence>

              <button
                type="button"
                onClick={addCustomTrigger}
                className="mt-2 flex w-full items-center gap-1.5 rounded-lg border border-dashed px-3 py-2 text-xs text-muted-foreground transition-colors hover:border-foreground/30 hover:text-foreground"
              >
                <Plus className="size-3.5" />
                Add custom trigger
              </button>

              <div className="mt-3 flex gap-2">
                <button
                  type="button"
                  onClick={goToStep0}
                  className="flex-1 rounded-lg border px-4 py-2 text-sm font-medium transition-colors hover:bg-muted"
                >
                  Back
                </button>
                <button
                  type="button"
                  onClick={() => onCreateAgent?.(buildPayload())}
                  className="flex-1 rounded-lg bg-foreground px-4 py-2 text-sm font-medium text-background transition-opacity hover:opacity-90"
                >
                  Create agent
                </button>
              </div>
            </motion.div>
          )}
        </AnimatePresence>
      </div>
    </div>
  );
}

A UX spec for this pattern — written for agents implementing or reusing it, not the code.

# Create Agent

## Summary
A 2-step wizard card for creating a named, icon-identified agent and selecting which external event sources activate it. Step 1 captures identity (name and icon); Step 2 captures trigger sources. On completion it emits a typed payload that maps directly onto the Agent Triggers panel.

## When to use
- First-time agent creation where no agent exists yet.
- Onboarding flows that walk users through setting up their first automation.
- An agent catalog screen with an "Add agent" entry point that opens an inline or sheet-level wizard.

## When not to use
- Editing an existing agent's trigger sources — use the Agent Triggers pattern directly, which has per-trigger toggles and a "Finish setup" inline action.
- When only one trigger source is possible. Presenting a multi-select of one option adds unnecessary friction; skip to a single confirmation step instead.
- When agent creation is complex enough to require a full-page form (multiple steps beyond identity and triggers, required integrations, permissions review, etc.).

## Anatomy
1. **Header preview row** — always visible across both steps. Shows the currently selected icon in a rounded avatar, the live agent name (or "New agent" placeholder), and two step-progress dots.
2. **Step 1 — Identity** — a title/subtitle label pair, a text input for the agent name, a row of five icon-picker buttons (Bot, Sparkles, Zap, Brain, Rocket), and a full-width "Choose triggers →" CTA.
3. **Step 2 — Triggers** — a title/subtitle label pair, a `<ul>` of trigger source cards (each with a bordered icon square, a label and description, and a trailing checkmark circle), and a two-button footer with "Back" (outline) and "Create agent" (filled).

## Behavior
- The header preview row updates in real time: the name reflects every keystroke; the icon swaps immediately on selection. This gives users continuous feedback on how the finished agent will appear elsewhere in the product.
- Icon selection is immediate with no confirm step — clicking a picker button applies it at once.
- Trigger source cards are multi-select toggle buttons. The user may select zero, one, or all sources; there is no minimum enforcement in the component (enforce constraints in the consuming view if needed).
- Navigating from Step 1 to Step 2 slides content in from the right; navigating back slides from the left. AnimatePresence with `mode="wait"` ensures the outgoing step fully exits before the incoming step enters.
- "Create agent" is the terminal action. It fires `onCreateAgent` with a payload of `{ name, iconId, triggerSources }` and hands control entirely to the parent. The component itself has no post-creation state.
- The "Back" button does not reset Step 1 state — name and icon choices are preserved across forward/back navigation.

## Content guidelines
- Agent names should be short proper nouns or noun phrases that describe the agent's role, not its mechanism: "Pattern Bot", "Deploy Guard", "Design Review" — not "github-trigger-agent-v2".
- The placeholder text "e.g. Pattern Bot" demonstrates the expected format; replace it with a placeholder appropriate to the product domain.
- Trigger source labels name the platform (GitHub, Slack, Figma). Descriptions name the event type that fires the agent, not the agent's response: "Issues, pull requests, and labels" not "Monitors your repo and responds to issues".
- The "Create agent" button label should always say exactly that — avoid "Save", "Done", or "Finish", which imply the agent already exists.

## Accessibility
- The name input has a visible `<p>` label ("Name your agent") immediately above it and an `aria-label` attribute for programmatic association.
- Each icon-picker button has an `aria-label` naming the icon (e.g. "Sparkles") and `aria-pressed` reflecting selection state.
- Each trigger source card is a `<button>` with `aria-pressed` reflecting whether it is selected.
- The step-progress dots in the header are `aria-hidden` — they are a decorative visual indicator, not a navigation control.
- Step transitions use motion that respects `prefers-reduced-motion` via Motion's default behavior; the `duration: 0.15` transitions are brief enough that they cause minimal disruption even without the media-query guard.

## Related patterns
- **Agent Triggers** — the panel displayed after a successful `onCreateAgent` callback. The payload from Create Agent maps directly to Agent Triggers' `title`, `agent`, and `triggers` props.
- **Setup Checklist** — use for multi-step onboarding of a whole product or feature area when several independent tasks must be completed, not a single wizard with two sequential screens.
- **Invite Members** — shares the "create something new" modal shape: header preview, multi-step form, terminal action. Reference for visual consistency when both patterns appear in the same product.