Agent Requests Feature
A feature section where four example requests drive one Agent Session. The picked request plays in a terminal beside the screen it changes: an alert rule and its Slack message, a retention table filling cohort by cohort, revenue split by plan, events tagged one by one. A line under the row fills as each change lands, and the next request plays on its own until the visitor picks one.
"use client";
import * as React from "react";
import { motion, useReducedMotion } from "motion/react";
import { cn } from "@/lib/utils";
import {
AgentSession,
AgentSessionTarget,
useAgentSessionEffect,
type AgentSessionStep,
} from "@/components/ui/agent-session";
/* ── Content ─────────────────────────────────────────────────
* Everything you'd change to make the section yours: the copy,
* the requests a visitor can pick, and the script the agent runs
* for each. The app the agent changes is plain markup further
* down; its numbers are here.
* ─────────────────────────────────────────────────────────── */
/** The one accent: progress lines, the glow and rings in the session, and the app's highlights. */
const ACCENT = { light: "#ea580c", dark: "#fb923c" };
const BRAND = "Tern";
const HEADLINE = "Ask for what you need.";
const HEADLINE_QUIET = "Tern does the wiring.";
const SUBLINE =
"Describe the change in one sentence. Your coding agent finds the events, writes the tracking code and puts the chart where you asked, as plain files in your repo.";
const FOOTNOTE = "Every change is a file you read in review and undo with git.";
const TERMINAL_TITLE = "web — agent";
/** How long a finished request rests before the next one plays, until the visitor picks one. */
const ADVANCE_MS = 2600;
type Screen = "alerts" | "retention" | "revenue" | "events";
type Icon = "bell" | "hash" | "grid" | "bars" | "tag" | "clock";
const REQUESTS: { id: Screen; label: string; icons: Icon[]; url: string; script: AgentSessionStep[] }[] = [
{
id: "alerts",
label: "Ping #payments when checkout errors spike",
icons: ["bell", "hash"],
url: "localhost:3000/alerts",
script: [
{ prompt: "Ping #payments in Slack when checkout errors spike" },
{
run: "npx tern events list --match checkout",
ms: 800,
output: ["checkout_started checkout_failed checkout_completed"],
},
{
run: 'npx tern alert add checkout_failed --above 2% --window 10m --slack "#payments"',
ms: 1200,
pace: 380,
output: [
{ text: "+ tern/alerts/checkout-failed.yml 2% over 10 min", tone: "added", effect: "rule" },
{ text: "✓ Slack connected · #payments", tone: "success", effect: "slack" },
],
},
{
run: "npx tern alert test checkout-failed",
ms: 900,
output: [{ text: "✓ Test alert sent to #payments", tone: "success", effect: "toast" }],
},
{ say: "Done. If more than 2% of checkouts fail within ten minutes, #payments hears about it." },
],
},
{
id: "retention",
label: "Add a weekly retention table",
icons: ["grid", "clock"],
url: "localhost:3000/retention",
script: [
{ prompt: "Add a weekly retention table for the last four cohorts" },
{
run: "npx tern add retention --cohort weekly --weeks 4",
ms: 1000,
output: [
{ text: "+ app/retention/cohort-table.tsx CohortTable", tone: "added", effect: "table" },
"~ app/retention/page.tsx +1 section",
],
},
{
run: "npx tern backfill retention --since 2026-09-28",
ms: 1300,
pace: 340,
output: [
{ text: "Week of Sep 28 1,912 people", effect: "w0" },
{ text: "Week of Oct 5 2,104 people", effect: "w1" },
{ text: "Week of Oct 12 2,388 people", effect: "w2" },
{ text: "Week of Oct 19 2,241 people", effect: "w3" },
],
},
{ say: "Retention is climbing: 41% of the Oct 12 cohort came back in week one, up from 38% in September." },
],
},
{
id: "revenue",
label: "Break revenue down by plan",
icons: ["bars"],
url: "localhost:3000/revenue",
script: [
{ prompt: "Break revenue down by plan, week by week" },
{
run: 'npx tern query "sum(amount) by plan, week" --last 8w',
ms: 1100,
output: [
{ text: "plan this week vs 8w ago", tone: "muted" },
"Pro $18,420 +31%",
"Team $9,870 +9%",
"Starter $2,310 −4%",
],
},
{
run: "npx tern add chart revenue-by-plan --type stacked",
ms: 1000,
output: [{ text: "+ app/revenue/by-plan.tsx RevenueByPlan", tone: "added", effect: "chart" }],
},
{ say: "Pro is 60% of revenue now, up from 52% eight weeks ago. Starter is flat." },
],
},
{
id: "events",
label: "Tag every event with the customer's plan",
icons: ["tag"],
url: "localhost:3000/events",
script: [
{ prompt: "Tag every event with the customer's plan" },
{
run: 'npx tern properties add plan --from "user.subscription.plan"',
ms: 900,
output: [{ text: "+ tern/properties/plan.ts reads user.subscription.plan", tone: "added" }],
},
{
run: "npx tern backfill --property plan --last 24h",
ms: 1200,
pace: 320,
output: [
{ text: "signup_completed 412 events", effect: "e0" },
{ text: "checkout_started 1,036 events", effect: "e1" },
{ text: "page_viewed 18,920 events", effect: "e2" },
{ text: "invite_sent 96 events", effect: "e3" },
],
},
{ say: "Every event carries plan now, the last 24 hours included, so any chart can split by it." },
],
},
];
/* The app the agent changes. */
const NAV: { label: string; screen?: Screen }[] = [
{ label: "Overview" },
{ label: "Events", screen: "events" },
{ label: "Retention", screen: "retention" },
{ label: "Revenue", screen: "revenue" },
{ label: "Alerts", screen: "alerts" },
];
const COHORTS = [
{ effect: "w0", week: "Sep 28", people: "1,912", rates: [100, 38, 31, 27] },
{ effect: "w1", week: "Oct 5", people: "2,104", rates: [100, 40, 33] },
{ effect: "w2", week: "Oct 12", people: "2,388", rates: [100, 41] },
{ effect: "w3", week: "Oct 19", people: "2,241", rates: [100] },
];
/** Revenue per week in $k: Pro, Team, Starter. */
const REVENUE = [
[10.2, 7.9, 2.4],
[11.0, 8.1, 2.5],
[11.8, 8.4, 2.3],
[12.9, 8.6, 2.4],
[14.1, 9.0, 2.4],
[15.6, 9.2, 2.3],
[16.9, 9.5, 2.3],
[18.4, 9.9, 2.3],
];
const EVENTS = [
{ effect: "e0", name: "signup_completed", who: "maya@kiln.studio", ago: "2s", plan: "Pro" },
{ effect: "e1", name: "checkout_started", who: "theo@ferngrove.co", ago: "6s", plan: "Team" },
{ effect: "e2", name: "page_viewed", who: "/pricing", ago: "9s", plan: "Starter" },
{ effect: "e3", name: "invite_sent", who: "ana@lumenhaus.io", ago: "14s", plan: "Pro" },
];
/* ── Section ─────────────────────────────────────────────── */
const focusRing =
"outline-none focus-visible:ring-2 focus-visible:ring-(--accent) focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-zinc-950";
const effectsOf = (script: AgentSessionStep[]) => {
const names = new Set<string>();
for (const step of script) {
if ("say" in step && step.effect) names.add(step.effect);
if ("output" in step)
for (const line of step.output ?? []) if (typeof line !== "string" && line.effect) names.add(line.effect);
}
return names.size;
};
export function AgentRequestsFeature() {
const reduced = useReducedMotion();
const [active, setActive] = React.useState(0);
const [landed, setLanded] = React.useState(0);
const [done, setDone] = React.useState(false);
// Auto-advance until the visitor picks a request themselves.
const [touring, setTouring] = React.useState(true);
const timer = React.useRef<number>(0);
const tabs = React.useRef<(HTMLButtonElement | null)[]>([]);
const id = React.useId();
React.useEffect(() => () => window.clearTimeout(timer.current), []);
const show = (index: number) => {
window.clearTimeout(timer.current);
setActive(index);
setLanded(0);
setDone(false);
};
const pick = (index: number) => {
setTouring(false);
show(index);
};
const onKeyDown = (event: React.KeyboardEvent) => {
const last = REQUESTS.length - 1;
const next =
event.key === "ArrowRight" || event.key === "ArrowDown"
? active === last ? 0 : active + 1
: event.key === "ArrowLeft" || event.key === "ArrowUp"
? active === 0 ? last : active - 1
: event.key === "Home"
? 0
: event.key === "End"
? last
: null;
if (next == null) return;
event.preventDefault();
pick(next);
tabs.current[next]?.focus();
};
const request = REQUESTS[active];
const changes = effectsOf(request.script);
return (
<section
style={{ "--accent-light": ACCENT.light, "--accent-dark": ACCENT.dark } as React.CSSProperties}
className="bg-white text-zinc-950 [--accent:var(--accent-light)] dark:bg-zinc-950 dark:text-white dark:[--accent:var(--accent-dark)]"
>
<div className="mx-auto max-w-6xl px-6 py-24 sm:py-32">
<div className="grid gap-x-10 gap-y-5 lg:grid-cols-12 lg:items-end">
<h2 className="text-4xl/[1.05] font-semibold tracking-[-0.035em] text-balance sm:text-5xl/[1.02] lg:col-span-7">
{HEADLINE} <span className="block text-zinc-400 dark:text-zinc-500">{HEADLINE_QUIET}</span>
</h2>
<p className="max-w-md text-base/7 text-pretty text-zinc-600 lg:col-span-5 lg:justify-self-end dark:text-zinc-400">
{SUBLINE}
</p>
</div>
<div
role="tablist"
aria-label={`Requests to ${BRAND}`}
onKeyDown={onKeyDown}
className="mt-12 grid border-t border-zinc-950/10 sm:grid-cols-2 sm:gap-x-10 dark:border-white/10"
>
{REQUESTS.map((item, index) => {
const selected = index === active;
return (
<button
key={item.id}
ref={(el) => {
tabs.current[index] = el;
}}
type="button"
role="tab"
id={`${id}-tab-${index}`}
aria-selected={selected}
aria-controls={`${id}-panel`}
tabIndex={selected ? 0 : -1}
onClick={() => pick(index)}
className={cn(
"group relative flex h-14 min-w-0 items-center justify-between gap-4 border-b border-zinc-950/10 text-left text-sm transition-colors dark:border-white/10",
focusRing,
selected
? "text-zinc-950 dark:text-white"
: "text-zinc-500 hover:text-zinc-950 dark:text-zinc-400 dark:hover:text-white",
)}
>
<span className="min-w-0 truncate font-medium">{item.label}</span>
<span className="flex shrink-0 items-center gap-2 text-zinc-400 dark:text-zinc-500">
{selected && (
<span className="text-xs text-zinc-500 tabular-nums dark:text-zinc-400">
{done ? <CheckIcon className="size-3.5 text-(--accent)" /> : `${landed}/${changes}`}
</span>
)}
{item.icons.map((icon) => (
<RowIcon key={icon} icon={icon} className="size-3.5" />
))}
</span>
{/* Fills as each of this request's changes lands in the app. */}
{selected && (
<motion.span
aria-hidden
initial={{ transform: "scaleX(0)" }}
animate={{ transform: `scaleX(${done ? 1 : landed / Math.max(1, changes)})` }}
transition={reduced ? { duration: 0 } : { type: "spring", visualDuration: 0.45, bounce: 0 }}
className="absolute inset-x-0 -bottom-px h-px origin-left bg-(--accent)"
/>
)}
</button>
);
})}
</div>
<div id={`${id}-panel`} role="tabpanel" aria-labelledby={`${id}-tab-${active}`} className="mt-8">
<AgentSession
script={request.script}
title={TERMINAL_TITLE}
url={request.url}
onEffect={() => setLanded((count) => count + 1)}
onComplete={() => {
setDone(true);
if (!touring) return;
window.clearTimeout(timer.current);
timer.current = window.setTimeout(() => show((active + 1) % REQUESTS.length), ADVANCE_MS);
}}
className="[--agent-session-accent:var(--accent)] [--agent-session-height:21rem] [--agent-session-preview-height:19rem] sm:[--agent-session-height:23rem] dark:[--agent-session-accent:var(--accent)]"
>
<TernApp screen={request.id} />
</AgentSession>
<p className="mt-4 text-center text-sm text-zinc-500">{FOOTNOTE}</p>
</div>
</div>
</section>
);
}
/* ── The app ─────────────────────────────────────────────── */
function TernApp({ screen }: { screen: Screen }) {
return (
<div className="@container/app flex h-full text-zinc-900 dark:text-zinc-100">
<aside className="hidden w-32 shrink-0 flex-col gap-0.5 border-r border-zinc-950/[0.06] bg-zinc-50/70 p-2.5 @[26rem]/app:flex dark:border-white/[0.06] dark:bg-white/[0.02]">
<div className="mb-3 flex items-center gap-1.5 px-1.5 pt-1 text-[13px] font-semibold tracking-tight">
<TernMark className="size-3.5 text-(--accent)" />
{BRAND}
</div>
{NAV.map((item) => (
<div
key={item.label}
className={cn(
"rounded-md px-2 py-1.5 text-xs",
item.screen === screen
? "bg-white font-medium shadow-sm ring-1 ring-zinc-950/5 dark:bg-white/10 dark:ring-white/5"
: "text-zinc-500 dark:text-zinc-400",
)}
>
{item.label}
</div>
))}
</aside>
<div className="relative min-w-0 flex-1 p-4">
{screen === "alerts" && <AlertsScreen />}
{screen === "retention" && <RetentionScreen />}
{screen === "revenue" && <RevenueScreen />}
{screen === "events" && <EventsScreen />}
</div>
</div>
);
}
function ScreenTitle({ title, aside }: { title: string; aside?: React.ReactNode }) {
return (
<div className="mb-3 flex items-baseline justify-between gap-3">
<h3 className="text-sm font-semibold tracking-tight">{title}</h3>
{aside && <span className="truncate text-[11px] text-zinc-400">{aside}</span>}
</div>
);
}
const card =
"rounded-xl bg-white ring-1 ring-zinc-950/[0.08] shadow-sm dark:bg-white/[0.04] dark:ring-white/10";
function AlertsScreen() {
return (
<>
<ScreenTitle title="Alerts" aside="2 rules" />
<div className={cn(card, "divide-y divide-zinc-950/[0.06] dark:divide-white/[0.06]")}>
<AlertRow name="Signup drop" rule="−30% in 1 hour" to="Email" />
<AgentSessionTarget
effect="rule"
className="[--agent-session-ring-radius:10px]"
fallback={<p className="px-3 py-2.5 text-[11px] text-zinc-400">Nothing watches checkout yet</p>}
>
<AlertRow name="Checkout failures" rule="> 2% in 10 min" to="#payments" fresh />
</AgentSessionTarget>
</div>
<div className="mt-3 flex items-center justify-between rounded-xl px-3 py-2.5 ring-1 ring-zinc-950/[0.08] dark:ring-white/10">
<span className="text-xs font-medium">Slack</span>
<AgentSessionTarget
effect="slack"
className="justify-items-end [--agent-session-ring-radius:999px]"
fallback={<span className="text-[11px] text-zinc-400">Not connected</span>}
>
<span className="flex items-center gap-1.5 text-[11px] text-zinc-600 dark:text-zinc-300">
<span className="size-1.5 rounded-full bg-emerald-500" />
#payments
</span>
</AgentSessionTarget>
</div>
{/* The test alert, as it arrives in Slack. */}
<AgentSessionTarget
effect="toast"
className="absolute right-4 bottom-4 left-4 [--agent-session-ring-radius:14px] @[26rem]/app:left-auto @[26rem]/app:w-64"
>
<div className={cn(card, "flex gap-2.5 p-3")}>
<span className="grid size-7 shrink-0 place-items-center rounded-lg bg-zinc-950 text-white dark:bg-white dark:text-zinc-950">
<TernMark className="size-3.5" />
</span>
<div className="min-w-0">
<p className="text-[11px] font-semibold">
{BRAND} <span className="font-normal text-zinc-400">in #payments</span>
</p>
<p className="mt-0.5 text-[11px]/4 text-zinc-600 dark:text-zinc-300">
Test: checkout_failed at 2.4% over the last 10 minutes.
</p>
</div>
</div>
</AgentSessionTarget>
</>
);
}
function AlertRow({ name, rule, to, fresh = false }: { name: string; rule: string; to: string; fresh?: boolean }) {
return (
<div className="flex items-center gap-2.5 px-3 py-2.5">
<span className={cn("size-1.5 shrink-0 rounded-full", fresh ? "bg-(--accent)" : "bg-zinc-300 dark:bg-zinc-600")} />
<span className="min-w-0 flex-1 truncate text-xs font-medium">{name}</span>
<span className="text-[11px] text-zinc-500 tabular-nums dark:text-zinc-400">{rule}</span>
<span className="hidden rounded-md bg-zinc-100 px-1.5 py-0.5 text-[10px] text-zinc-600 @[22rem]/app:inline dark:bg-white/[0.06] dark:text-zinc-300">
{to}
</span>
</div>
);
}
function RetentionScreen() {
return (
<>
<ScreenTitle title="Retention" aside="Weekly cohorts" />
<AgentSessionTarget
effect="table"
className="[--agent-session-ring-radius:14px]"
fallback={
<div className="flex h-full min-h-40 flex-col items-center justify-center gap-1 rounded-xl border border-dashed border-zinc-950/15 text-center dark:border-white/15">
<p className="text-xs font-medium">No retention view yet</p>
<p className="text-[11px] text-zinc-400">Cohorts appear once events are tracked.</p>
</div>
}
>
<div className={cn(card, "p-3")}>
<div className="grid grid-cols-[minmax(0,1fr)_repeat(4,2.25rem)] gap-1 text-[10px] text-zinc-400">
<span>Cohort</span>
{["W0", "W1", "W2", "W3"].map((week) => (
<span key={week} className="text-center">
{week}
</span>
))}
</div>
<div className="mt-1.5 flex flex-col gap-1">
{COHORTS.map((cohort) => (
<CohortRow key={cohort.effect} {...cohort} />
))}
</div>
</div>
</AgentSessionTarget>
</>
);
}
function CohortRow({ effect, week, people, rates }: (typeof COHORTS)[number]) {
const landed = useAgentSessionEffect(effect);
const reduced = useReducedMotion();
return (
<AgentSessionTarget effect={effect} enter="none" className="[--agent-session-ring-radius:8px]">
<div className="grid grid-cols-[minmax(0,1fr)_repeat(4,2.25rem)] items-center gap-1">
<span className="min-w-0 truncate text-[11px]">
{week} <span className="text-zinc-400 tabular-nums">{people}</span>
</span>
{[0, 1, 2, 3].map((i) => {
const rate = rates[i];
return (
<span
key={i}
className="relative grid h-6 place-items-center overflow-hidden rounded-md bg-zinc-100 text-[10px] tabular-nums dark:bg-white/[0.05]"
>
{rate != null && (
<>
<motion.span
aria-hidden
initial={false}
animate={{ opacity: landed ? 0.15 + (rate / 100) * 0.85 : 0 }}
transition={reduced ? { duration: 0 } : { duration: 0.35, delay: landed ? i * 0.06 : 0 }}
className="absolute inset-0 bg-(--accent)"
/>
<motion.span
initial={false}
animate={{ opacity: landed ? 1 : 0 }}
transition={reduced ? { duration: 0 } : { duration: 0.25, delay: landed ? i * 0.06 : 0 }}
className={cn("relative font-medium", rate > 60 ? "text-white dark:text-zinc-950" : "")}
>
{rate}%
</motion.span>
</>
)}
</span>
);
})}
</div>
</AgentSessionTarget>
);
}
function RevenueScreen() {
const max = Math.max(...REVENUE.map((week) => week.reduce((a, b) => a + b, 0)));
return (
<>
<ScreenTitle title="Revenue" aside="$30,600 this week" />
<div className="mb-3 grid grid-cols-2 gap-2">
<Stat label="This week" value="$30.6k" delta="+8.4%" />
<Stat label="Paying teams" value="1,148" delta="+3.0%" />
</div>
<AgentSessionTarget
effect="chart"
className="[--agent-session-ring-radius:14px]"
fallback={
<div className={cn(card, "flex h-full min-h-36 items-center justify-center text-[11px] text-zinc-400")}>
Revenue isn't split by plan yet
</div>
}
>
{(landed) => (
<div className={cn(card, "p-3")}>
<div className="flex h-24 items-end gap-1.5">
{REVENUE.map((week, i) => (
<StackedBar key={i} week={week} max={max} grow={landed} index={i} />
))}
</div>
<div className="mt-2.5 flex gap-3 text-[10px] text-zinc-500 dark:text-zinc-400">
<Legend className="bg-(--accent)" label="Pro" />
<Legend className="bg-zinc-800 dark:bg-zinc-300" label="Team" />
<Legend className="bg-zinc-300 dark:bg-zinc-600" label="Starter" />
</div>
</div>
)}
</AgentSessionTarget>
</>
);
}
function StackedBar({ week, max, grow, index }: { week: number[]; max: number; grow: boolean; index: number }) {
const reduced = useReducedMotion();
const total = week.reduce((a, b) => a + b, 0);
const tones = ["bg-(--accent)", "bg-zinc-800 dark:bg-zinc-300", "bg-zinc-300 dark:bg-zinc-600"];
return (
<motion.div
initial={false}
animate={{ transform: `scaleY(${grow ? 1 : 0.04})` }}
transition={reduced ? { duration: 0 } : { type: "spring", visualDuration: 0.5, bounce: 0.08, delay: grow ? index * 0.04 : 0 }}
className="flex min-w-0 flex-1 origin-bottom flex-col-reverse overflow-hidden rounded-[3px]"
style={{ height: `${(total / max) * 100}%` }}
>
{week.map((value, i) => (
<span key={i} className={tones[i]} style={{ height: `${(value / total) * 100}%` }} />
))}
</motion.div>
);
}
function EventsScreen() {
return (
<>
<ScreenTitle
title="Live events"
aside={
<span className="flex items-center gap-1.5">
<span className="size-1.5 rounded-full bg-emerald-500" />
Streaming
</span>
}
/>
<div className={cn(card, "divide-y divide-zinc-950/[0.06] dark:divide-white/[0.06]")}>
{EVENTS.map((event) => (
<div key={event.effect} className="flex items-center gap-2.5 px-3 py-2.5">
<span className="min-w-0 flex-1">
<span className="block truncate font-mono text-[11px] font-medium">{event.name}</span>
<span className="block truncate text-[10px] text-zinc-400">
{event.who} · {event.ago} ago
</span>
</span>
<AgentSessionTarget
effect={event.effect}
className="justify-items-end [--agent-session-ring-radius:999px]"
fallback={<span className="text-[10px] text-zinc-300 dark:text-zinc-600">no plan</span>}
>
<span className="rounded-full bg-zinc-100 px-2 py-0.5 text-[10px] font-medium text-zinc-700 ring-1 ring-zinc-950/5 dark:bg-white/[0.06] dark:text-zinc-200 dark:ring-white/10">
plan: <span className="text-(--accent)">{event.plan}</span>
</span>
</AgentSessionTarget>
</div>
))}
</div>
</>
);
}
function Stat({ label, value, delta }: { label: string; value: string; delta: string }) {
return (
<div className={cn(card, "p-2.5 shadow-none")}>
<p className="text-[11px] text-zinc-500 dark:text-zinc-400">{label}</p>
<p className="mt-0.5 flex items-baseline gap-1.5">
<span className="text-base font-semibold tracking-tight tabular-nums">{value}</span>
<span className="text-[10px] font-medium text-emerald-600 dark:text-emerald-400">{delta}</span>
</p>
</div>
);
}
function Legend({ className, label }: { className: string; label: string }) {
return (
<span className="flex items-center gap-1.5">
<span className={cn("size-2 rounded-[2px]", className)} />
{label}
</span>
);
}
/* ── Icons ───────────────────────────────────────────────── */
function TernMark({ className }: { className?: string }) {
return (
<svg viewBox="0 0 24 24" className={className} aria-hidden>
<path d="M1 7.5c4.2-.9 8 1.2 11 5 3-3.8 6.8-5.9 11-5-4.4 1.4-8.1 5-11 10.5C9.1 12.5 5.4 8.9 1 7.5Z" fill="currentColor" />
</svg>
);
}
function RowIcon({ icon, className }: { icon: Icon; className?: string }) {
const paths: Record<Icon, React.ReactNode> = {
bell: <path d="M4 11.5h8l-1-1.5V7a3 3 0 0 0-6 0v3l-1 1.5ZM6.8 13.5h2.4" />,
hash: <path d="M6 2.5 5 13.5M11 2.5l-1 11M2.5 6h11M2 10h11" />,
grid: <path d="M2.5 2.5h11v11h-11zM2.5 6.2h11M2.5 9.8h11M6.2 2.5v11M9.8 2.5v11" />,
clock: <path d="M8 2.5a5.5 5.5 0 1 0 0 11 5.5 5.5 0 0 0 0-11ZM8 5v3l2 1.5" />,
bars: <path d="M3 13.5v-4M6.3 13.5V6M9.7 13.5V8M13 13.5V3" />,
tag: <path d="M2.5 2.5h5l6 6-5 5-6-6v-5ZM5.3 5.3h.01" />,
};
return (
<svg
viewBox="0 0 16 16"
className={className}
fill="none"
stroke="currentColor"
strokeWidth={1.4}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden
>
{paths[icon]}
</svg>
);
}
function CheckIcon({ className }: { className?: string }) {
return (
<svg viewBox="0 0 16 16" className={className} fill="none" stroke="currentColor" strokeWidth={1.8} aria-hidden>
<path d="m3.5 8.5 3 3 6-7" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
Installation
The CLI writes components/agent-requests-feature.tsx, the components it's built with (components/ui/agent-session.tsx) and lib/utils.ts, and installs motion, clsx and tailwind-merge. It needs the @keyframeui registry in components.json; add it once per project.
npx shadcn@latest add @keyframeui/agent-requests-featureThen put it where the section goes. The copy, links and data sit in constants at the top of the file, so making it yours starts there.
import { AgentRequestsFeature } from "@/components/agent-requests-feature";
export default function Page() {
return <AgentRequestsFeature />;
}
Built with
Each of these is a component on its own, with its own examples and props. The block installs them for you.