agents
Agent Triggers
A configuration panel for listing and toggling the external events that activate an agent, with a manual run control.
AI-Patterns
When should this agent run?
Issue labeled pattern-request in carlosmarch/ai-patterns
When AI-Patterns is mentioned in #design-requests
Frame marked ready for dev in any Figma file
"use client";
import * as React from "react";
import { AnimatePresence, motion } from "motion/react";
import { AtSign, GitBranch, GitPullRequest, Layers, Play, Plus } from "lucide-react";
import { cn } from "@/lib/utils";
export interface AgentTrigger {
id: string;
icon: React.ComponentType<{ className?: string }>;
event: string;
context: string;
enabled: boolean;
needsSetup?: boolean;
}
export interface AgentAvatar {
name: string;
src?: string;
icon?: React.ComponentType<{ className?: string }>;
}
export interface AgentTriggersProps {
title?: string;
description?: string;
agent?: AgentAvatar;
triggers?: AgentTrigger[];
onRunAgent?: () => void;
onToggle?: (id: string, enabled: boolean) => void;
onAddTrigger?: (platformId?: string) => void;
onFinishSetup?: (id: string) => void;
className?: string;
}
const QUICK_ADD_PLATFORMS = [
{ id: "github", label: "GitHub", icon: GitBranch },
{ id: "slack", label: "Slack", icon: AtSign },
{ id: "figma", label: "Figma", icon: Layers },
] as const;
export const DEFAULT_TRIGGERS: AgentTrigger[] = [
{
id: "github-issue",
icon: GitPullRequest,
event: "Issue labeled pattern-request",
context: "in carlosmarch/ai-patterns",
enabled: true,
},
{
id: "slack-mention",
icon: AtSign,
event: "When AI-Patterns is mentioned",
context: "in #design-requests",
enabled: true,
},
{
id: "figma-ready",
icon: Layers,
event: "Frame marked ready for dev",
context: "in any Figma file",
enabled: false,
needsSetup: true,
},
];
function Avatar({ agent }: { agent: AgentAvatar }) {
return (
<span className="flex size-8 shrink-0 items-center justify-center overflow-hidden rounded-full bg-muted text-muted-foreground">
{agent.src ? (
<img src={agent.src} alt={agent.name} className="size-full object-cover" />
) : agent.icon ? (
<agent.icon className="size-4" />
) : (
<span className="text-xs font-semibold">
{agent.name.split(" ").map((w) => w[0]).slice(0, 2).join("").toUpperCase()}
</span>
)}
</span>
);
}
function Toggle({ enabled, onChange }: { enabled: boolean; onChange: (v: boolean) => void }) {
return (
<button
type="button"
role="switch"
aria-checked={enabled}
onClick={() => onChange(!enabled)}
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>
);
}
export function AgentTriggers({
title = "Triggers",
description = "When should this agent run?",
agent,
triggers: initialTriggers = DEFAULT_TRIGGERS,
onRunAgent,
onToggle,
onAddTrigger,
onFinishSetup,
className,
}: AgentTriggersProps) {
const [triggers, setTriggers] = React.useState(initialTriggers);
const [running, setRunning] = React.useState(false);
function handleToggle(id: string, value: boolean) {
setTriggers((prev) => prev.map((t) => (t.id === id ? { ...t, enabled: value } : t)));
onToggle?.(id, value);
}
function handleRun() {
if (running) return;
setRunning(true);
onRunAgent?.();
setTimeout(() => setRunning(false), 2200);
}
return (
<div className={cn("w-full max-w-xs overflow-hidden rounded-2xl border bg-card", className)}>
<div className="flex items-start justify-between gap-3 px-4 pb-3 pt-4">
<div className="flex min-w-0 items-center gap-2.5">
{agent && <Avatar agent={agent} />}
<div className="min-w-0">
<p className="text-sm font-semibold">{title}</p>
<p className="text-xs text-muted-foreground">{description}</p>
</div>
</div>
<button
type="button"
onClick={handleRun}
disabled={running}
className={cn(
"flex shrink-0 items-center gap-1.5 rounded-full border px-3 py-1.5 text-xs font-medium transition-colors disabled:pointer-events-none",
running ? "border-transparent bg-foreground text-background" : "hover:bg-accent"
)}
>
<Play className={cn("size-3 transition-all", running ? "fill-current" : "fill-none stroke-current")} />
<AnimatePresence mode="wait" initial={false}>
<motion.span
key={running ? "running" : "idle"}
initial={{ opacity: 0, y: 3 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -3 }}
transition={{ duration: 0.1 }}
>
{running ? "Running…" : "Run agent"}
</motion.span>
</AnimatePresence>
</button>
</div>
<ul className="divide-y border-t">
{triggers.map((trigger) => (
<li key={trigger.id} className="flex items-center gap-3 px-4 py-2.5">
<span className="flex size-6 shrink-0 items-center justify-center rounded text-muted-foreground">
<trigger.icon className="size-3.5" />
</span>
<p className="min-w-0 flex-1 text-xs leading-snug">
<span className="font-medium">{trigger.event}</span>{" "}
<span className="text-muted-foreground">{trigger.context}</span>
</p>
<div className="flex shrink-0 items-center gap-2">
{trigger.needsSetup && !trigger.enabled && (
<button
type="button"
onClick={() => onFinishSetup?.(trigger.id)}
className="text-[11px] text-muted-foreground underline underline-offset-2 transition-colors hover:text-foreground"
>
Finish setup
</button>
)}
<Toggle enabled={trigger.enabled} onChange={(v) => handleToggle(trigger.id, v)} />
</div>
</li>
))}
</ul>
<div className="flex items-center gap-2 border-t px-4 py-2.5">
<button
type="button"
onClick={() => onAddTrigger?.()}
className="flex items-center gap-1.5 text-xs text-muted-foreground transition-colors hover:text-foreground"
>
<Plus className="size-3.5" />
Add trigger
</button>
<div className="ml-0.5 flex items-center gap-0.5">
{QUICK_ADD_PLATFORMS.map(({ id, label, icon: Icon }) => (
<button
key={id}
type="button"
aria-label={`Add ${label} trigger`}
onClick={() => onAddTrigger?.(id)}
className="flex size-5 items-center justify-center rounded text-muted-foreground/50 transition-colors hover:text-muted-foreground"
>
<Icon className="size-3.5" />
</button>
))}
</div>
</div>
</div>
);
}
A UX spec for this pattern — written for agents implementing or reusing it, not the code.
# Agent Triggers
## Summary
A configuration panel listing the external events that cause an agent to run automatically, each with an on/off toggle and an optional "Finish setup" prompt for incomplete integrations. A "Run agent" button lets the user fire the agent on demand outside of any trigger, and a footer row provides one-click entry points for adding new trigger sources.
## When to use
- In an agent or automation settings panel where users define when a workflow should start.
- When an agent can be connected to multiple external sources (GitHub, Slack, Figma, etc.) and users need to pick which ones activate it.
- Alongside an agent canvas or workflow builder, as the entry-point node configuration.
## When not to use
- For simple scheduled or time-based recurrence — use a cron or schedule picker instead; event-based and time-based triggers have different mental models and should not share this component.
- When there is only ever one possible trigger — a list of one is not a list; use a single connection widget or toggle.
- As a general settings list. This component is about activation conditions, not preferences.
## Anatomy
- Header: a title ("Triggers") and a subtitle ("When should this agent run?") scoped to the agent being configured.
- Run agent button: an outlined play button in the header that fires the agent manually, independent of any trigger. It enters a transient "Running…" state while the agent executes.
- Trigger list: one row per trigger, each containing an icon that identifies the source, an event name in medium weight followed by a muted location context ("in Slack", "in carlosmarch/ai-patterns"), and a toggle on the trailing edge.
- Finish setup label: a small underlined text link that appears before the toggle when a trigger has been added but not yet fully configured. Enables the trigger rather than completing setup inline — that flow belongs in a dedicated integration modal.
- Add trigger footer: a "+ Add trigger" text button plus icon shortcuts for common source platforms (GitHub, Slack, Figma). Both lead to the same source-picker; the icons are just accelerators for recognized integrations.
## Behavior
- Toggling a trigger on/off is immediate and optimistic — the UI updates before the server round-trip. If the save fails, revert and surface an error.
- "Finish setup" shows only when `needsSetup` is true and the trigger is currently off. Once the user enables the trigger (completing setup), the label disappears.
- Run agent is a one-shot control: clicking it disables the button and swaps its label to "Running…" until the run resolves, then restores the idle state. It does not reflect ongoing trigger-based runs.
- The trigger list is non-reorderable — triggers fire in parallel on any matching event, so order has no semantic meaning.
- Adding a new trigger appends it to the list in a needs-setup state with the toggle off; the "Finish setup" label appears immediately.
## Content guidelines
- Event names are short noun phrases or gerunds that describe the inbound event, not the agent's response to it: "Issue labeled pattern-request", "Frame marked ready for dev". Start with the noun (Issue, Frame, Message) so items scan consistently when the list grows.
- The context string names the specific location in sentence-case with no trailing period: "in carlosmarch/ai-patterns", "in any Figma file".
- Platform icons should match the brand icon for the integration, not a generic glyph. When a brand icon isn't available, use a domain-appropriate generic (a hashtag for channels, an @ for mention events).
## Accessibility
- The toggle must be a `<button role="switch" aria-checked={enabled}>` — not a styled `<div>` — so screen readers announce the state change.
- Each toggle must have an accessible label. Since the label is provided by the adjacent row text, use `aria-labelledby` pointing to the event name element, or wrap the row in a `<label>`.
- "Finish setup" must be keyboard reachable and announce where it leads: `aria-label="Finish setup for Frame marked ready for dev"`.
- The quick-add platform icons in the footer are icon-only buttons; each must carry an `aria-label` naming the platform ("Add GitHub trigger").
## Related patterns
- Flowchart: the canvas-level counterpart — Agent Triggers configures the entry-point event, while the Flowchart node describes what conditions gate the work that follows.
- Tool Approval: also governs what an agent is allowed to do, but at the per-call level rather than the activation level.
- Setup Checklist: shares the "incomplete step" affordance (the Finish setup label) — use Setup Checklist when onboarding a whole agent for the first time; use Agent Triggers when managing an already-running agent's event sources.