Agent Session Hero
A hero for a developer tool that coding agents use. Under the headline an Agent Session plays: the agent adds a signup funnel to a live dashboard, each bar filling as its tracking call is written. Your mark's orbit is printed in glyphs around the headline and inks under the pointer, a Release Pill sits above it, and the nav docks into a pill that catches the hero's button as you scroll.
Hero Section
Built with Agent Session, Glyph Field, Handoff Nav and Release Pill
Uses motion, clsx, tailwind-merge
"use client";
import * as React from "react";
import { motion, useReducedMotion } from "motion/react";
import {
AgentSession,
AgentSessionTarget,
useAgentSessionEffect,
type AgentSessionStep,
} from "@/components/ui/agent-session";
import { GlyphField } from "@/components/ui/glyph-field";
import { HandoffNav, HandoffNavCta, HandoffNavProvider } from "@/components/ui/handoff-nav";
import { ReleasePill, type Release } from "@/components/ui/release-pill";
/* ── Content ─────────────────────────────────────────────────
* Everything you'd change to make the hero yours: the nav, the
* release notes in the pill, the copy, and the script the agent
* runs under the headline. The dashboard it changes is plain
* markup further down; its numbers are here too.
* ─────────────────────────────────────────────────────────── */
/** The one accent: glyph ink, the session's glow and rings, and the dashboard's highlights. */
const ACCENT = { light: "#ea580c", dark: "#fb923c" };
const BRAND = { name: "Tern", href: "#" };
const NAV_LINKS = [
{ label: "Product", href: "#product" },
{ label: "Docs", href: "#docs" },
{ label: "Pricing", href: "#pricing" },
{ label: "Changelog", href: "#changelog" },
];
const SIGN_IN = { label: "Log in", href: "#login" };
const RELEASES: Release[] = [
{
version: "v2.4",
title: "Funnels from a prompt",
date: "Oct 28",
href: "#changelog-v2-4",
summary: "Ask your agent for a funnel and it writes the chart and every tracking call behind it.",
},
{ version: "v2.3", title: "Slack alerts on any event", date: "Oct 14", href: "#changelog-v2-3" },
{ version: "v2.2", title: "Weekly retention cohorts", date: "Sep 30", href: "#changelog-v2-2" },
{ version: "v2.1", title: "Keys stay in your terminal", date: "Sep 16", href: "#changelog-v2-1" },
];
const CHANGELOG_HREF = "#changelog";
const HEADLINE = "Ship the feature.";
const HEADLINE_QUIET = "Your agent tracks it.";
const SUBLINE =
"Tern is product analytics for teams that build with coding agents. Ask in a sentence: the agent finds the events, writes the tracking code and puts the chart on your dashboard.";
const PRIMARY_CTA = { label: "Start free", href: "#start" };
const SECONDARY_CTA = { label: "Set up your agent", href: "#agent-setup" };
const FOOTNOTE = "Free up to 10,000 events a month. Works with any agent that can run a command.";
/** Under the session: who uses it. Wordmarks set in type; swap in your customers' logos. */
const CUSTOMERS_LABEL = "Teams that ship with their agents";
const CUSTOMERS: { name: string; className: string }[] = [
{ name: "Kiln", className: "font-semibold tracking-tight" },
{ name: "Ferngrove", className: "font-serif italic" },
{ name: "LUMENHAUS", className: "font-mono text-[13px] tracking-[0.2em]" },
{ name: "Okto", className: "font-bold tracking-[-0.04em]" },
{ name: "Field & Fold", className: "font-serif" },
{ name: "harbor", className: "font-medium lowercase tracking-wide" },
];
/* The session under the headline, and the dashboard it changes. */
const TERMINAL_TITLE = "web — agent";
const APP_URL = "localhost:3000/dashboard";
const SCRIPT: AgentSessionStep[] = [
{ prompt: "Add a signup funnel to the dashboard and track each step" },
{
run: "npx tern events list --match signup",
ms: 900,
output: [
"signup_viewed page /signup",
"signup_submitted form #signup-form",
"email_verified server POST /api/verify",
],
more: 4,
},
{
run: "npx tern add funnel signup --to app/dashboard",
ms: 1100,
output: [
{ text: "+ app/dashboard/signup-funnel.tsx SignupFunnel", tone: "added", effect: "funnel" },
{ text: "~ app/dashboard/layout.tsx Funnels (1)", effect: "nav" },
],
},
{
run: "npx tern track signup_viewed signup_submitted email_verified",
ms: 1200,
pace: 420,
output: [
{ text: '+ app/signup/page.tsx track("signup_viewed")', tone: "added", effect: "step-1" },
{ text: '+ app/signup/form.tsx track("signup_submitted")', tone: "added", effect: "step-2" },
{ text: '+ app/api/verify/route.ts track("email_verified")', tone: "added", effect: "step-3" },
],
},
{
say: "Added a three-step signup funnel. Last week 1,204 of 12,480 visitors finished, and most of the drop happens before the form is sent.",
},
];
const DASHBOARD_NAV = ["Overview", "Funnels", "Events", "Retention", "Alerts"];
const STATS = [
{ label: "Visitors", value: "12,480", delta: "+8.2%" },
{ label: "Signups", value: "1,204", delta: "+3.1%" },
{ label: "Active today", value: "3,906", delta: "+1.4%" },
];
const FUNNEL = [
{ effect: "step-1", label: "Viewed signup", count: "12,480", share: 1 },
{ effect: "step-2", label: "Sent the form", count: "2,911", share: 0.233 },
{ effect: "step-3", label: "Verified email", count: "1,204", share: 0.096 },
];
/* ── Hero ────────────────────────────────────────────────── */
const focusRing =
"focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-zinc-950 dark:focus-visible:outline-white";
export function AgentSessionHero() {
return (
<section
style={{ "--accent-light": ACCENT.light, "--accent-dark": ACCENT.dark } as React.CSSProperties}
// The Release Pill draws with the shadcn theme colors; point them at this palette.
className="relative isolate overflow-x-clip bg-zinc-50 text-zinc-950 [--accent:var(--accent-light)] [--background:#fafafa] [--foreground:#09090b] [--muted-foreground:#71717a] dark:bg-zinc-950 dark:text-white dark:[--accent:var(--accent-dark)] dark:[--background:#09090b] dark:[--foreground:#fafafa] dark:[--muted-foreground:#a1a1aa]"
>
<HandoffNavProvider>
<HandoffNav
logo={
<a href={BRAND.href} className={`flex items-center gap-2 rounded-md text-[15px] font-semibold tracking-tight ${focusRing}`}>
<TernMark className="size-5 text-(--accent)" />
{BRAND.name}
</a>
}
links={NAV_LINKS}
action={SIGN_IN}
className="[--handoff-nav-width:72rem]"
/>
{/* "cover" sizes the orbit to the width, so its band sweeps around the copy, not through it. */}
<GlyphField
fit="cover"
glyphClassName="text-zinc-300 dark:text-zinc-700"
accentClassName="text-(--accent)"
canvasClassName="opacity-60 sm:opacity-100"
className="px-6 pt-10 pb-12 text-center sm:pt-14"
>
<ReleasePill releases={RELEASES} changelogHref={CHANGELOG_HREF} />
<h1 className="mx-auto mt-6 max-w-[15ch] text-[2.75rem]/[1.02] font-semibold tracking-[-0.04em] text-balance sm:text-6xl/[1] lg:text-7xl/[0.98]">
{HEADLINE} <span className="block text-zinc-400 dark:text-zinc-500">{HEADLINE_QUIET}</span>
</h1>
<p className="mx-auto mt-6 max-w-xl text-base/7 text-pretty text-zinc-600 sm:text-lg/8 dark:text-zinc-400">
{SUBLINE}
</p>
<div className="mt-8 flex flex-wrap items-center justify-center gap-x-6 gap-y-3">
<a
href={SECONDARY_CTA.href}
className={`group inline-flex items-center gap-1 rounded-md text-sm font-medium text-zinc-700 transition-colors hover:text-zinc-950 dark:text-zinc-300 dark:hover:text-white ${focusRing}`}
>
{SECONDARY_CTA.label}
<ArrowIcon className="size-3.5 transition-transform duration-200 group-hover:translate-x-0.5" />
</a>
<HandoffNavCta href={PRIMARY_CTA.href}>
{PRIMARY_CTA.label}
<ArrowIcon className="size-3.5" />
</HandoffNavCta>
</div>
<p className="mt-5 text-sm text-zinc-500">{FOOTNOTE}</p>
</GlyphField>
<div className="mx-auto max-w-6xl px-6 pb-20 sm:pb-24">
<AgentSession
script={SCRIPT}
title={TERMINAL_TITLE}
url={APP_URL}
className="[--agent-session-accent:var(--accent)] [--agent-session-height:22rem] [--agent-session-preview-height:20.5rem] sm:[--agent-session-height:26rem] dark:[--agent-session-accent:var(--accent)]"
>
<Dashboard />
</AgentSession>
<div className="mt-14 text-center sm:mt-16">
<p className="text-sm text-zinc-500">{CUSTOMERS_LABEL}</p>
<ul className="mt-5 flex flex-wrap items-center justify-center gap-x-10 gap-y-4 text-lg text-zinc-400 dark:text-zinc-500">
{CUSTOMERS.map((customer) => (
<li key={customer.name} className={customer.className}>
{customer.name}
</li>
))}
</ul>
</div>
</div>
</HandoffNavProvider>
</section>
);
}
/* ── The dashboard the agent changes ─────────────────────── */
function Dashboard() {
return (
<div className="@container/app flex h-full text-zinc-900 dark:text-zinc-100">
<aside className="hidden w-36 shrink-0 flex-col gap-0.5 border-r border-zinc-950/[0.06] bg-zinc-50/70 p-2.5 @[28rem]/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.name}
</div>
{DASHBOARD_NAV.map((item, i) => (
<div
key={item}
className={
i === 0
? "flex items-center justify-between rounded-md bg-white px-2 py-1.5 text-xs font-medium shadow-sm ring-1 ring-zinc-950/5 dark:bg-white/10 dark:ring-white/5"
: "flex items-center justify-between rounded-md px-2 py-1.5 text-xs text-zinc-500 dark:text-zinc-400"
}
>
{item}
{item === "Funnels" && (
<AgentSessionTarget effect="nav" className="[--agent-session-ring-radius:999px]">
<span className="rounded-full bg-(--accent) px-1.5 text-[10px] leading-4 font-semibold text-white tabular-nums dark:text-zinc-950">
1
</span>
</AgentSessionTarget>
)}
</div>
))}
</aside>
<div className="flex min-w-0 flex-1 flex-col gap-3 p-4">
<div className="flex items-baseline justify-between">
<h3 className="text-sm font-semibold tracking-tight">Overview</h3>
<span className="text-[11px] text-zinc-400">Last 7 days</span>
</div>
<div className="grid grid-cols-2 gap-2 @[34rem]/app:grid-cols-3">
{STATS.map((stat, i) => (
<div
key={stat.label}
className={`rounded-xl bg-white p-2.5 ring-1 ring-zinc-950/[0.08] dark:bg-white/[0.04] dark:ring-white/10 ${i === 2 ? "hidden @[34rem]/app:block" : ""}`}
>
<p className="text-[11px] text-zinc-500 dark:text-zinc-400">{stat.label}</p>
<p className="mt-0.5 flex items-baseline gap-1.5">
<span className="text-base font-semibold tracking-tight tabular-nums">{stat.value}</span>
<span className="text-[10px] font-medium text-emerald-600 dark:text-emerald-400">{stat.delta}</span>
</p>
</div>
))}
</div>
<AgentSessionTarget
effect="funnel"
className="[--agent-session-ring-radius:14px]"
fallback={
<div className="flex h-full min-h-36 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 funnels yet</p>
<p className="text-[11px] text-zinc-400">Ask your agent for one.</p>
</div>
}
>
<div className="rounded-xl bg-white p-3 shadow-sm ring-1 ring-zinc-950/[0.08] dark:bg-white/[0.04] dark:ring-white/10">
<div className="mb-2.5 flex items-baseline justify-between">
<p className="text-xs font-semibold">Signup funnel</p>
<p className="text-[11px] text-zinc-400 tabular-nums">9.6% convert</p>
</div>
<div className="flex flex-col gap-2">
{FUNNEL.map((step) => (
<FunnelRow key={step.effect} {...step} />
))}
</div>
</div>
</AgentSessionTarget>
</div>
</div>
);
}
// Each bar fills once the tracking call for its step is written.
function FunnelRow({ effect, label, count, share }: (typeof FUNNEL)[number]) {
const landed = useAgentSessionEffect(effect);
const reduced = useReducedMotion();
return (
<AgentSessionTarget effect={effect} enter="none" className="[--agent-session-ring-radius:8px]">
<div className="flex flex-col gap-1">
<div className="flex items-baseline justify-between text-[11px]">
<span className="text-zinc-600 dark:text-zinc-300">{label}</span>
<span className={`font-medium tabular-nums ${landed ? "" : "text-zinc-300 dark:text-zinc-600"}`}>
{landed ? count : "—"}
</span>
</div>
<div className="h-1.5 overflow-hidden rounded-full bg-zinc-100 dark:bg-white/[0.07]">
<motion.div
initial={false}
animate={{ transform: `scaleX(${landed ? share : 0})` }}
transition={reduced ? { duration: 0 } : { type: "spring", visualDuration: 0.6, bounce: 0 }}
className="h-full origin-left rounded-full bg-(--accent)"
/>
</div>
</div>
</AgentSessionTarget>
);
}
/* ── 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 ArrowIcon({ className }: { className?: string }) {
return (
<svg viewBox="0 0 16 16" className={className} fill="none" stroke="currentColor" strokeWidth={1.6} aria-hidden>
<path d="M3.5 8h9m-3.5-3.5L12.5 8 9 11.5" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
Installation
The CLI writes components/agent-session-hero.tsx, the components it's built with (components/ui/agent-session.tsx, components/ui/glyph-field.tsx, components/ui/handoff-nav.tsx and components/ui/release-pill.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-session-heroThen 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 { AgentSessionHero } from "@/components/agent-session-hero";
export default function Page() {
return <AgentSessionHero />;
}
Built with
Each of these is a component on its own, with its own examples and props. The block installs them for you.
- Agent Session A coding agent at work in its terminal, beside the app it's changing. The prompt types itself, each command runs with a live timer and lands its output in one burst, and every line that writes a file shows up in the app a beat later, ringed. Point at a change on either side and its other half lights up. @keyframeui/agent-session
- Glyph Field Your mark drawn in type. A word, an SVG path or a photo is printed as a grid of monospace glyphs that run from bare paper through dots to a dense @. It prints left to right the first time it scrolls into view, and the pointer inks the glyphs it passes over in your accent, which then dry back to their resting weight. @keyframeui/glyph-field
- Handoff Nav A navbar that tightens into a floating pill once the page scrolls, and catches your hero's call to action: as the hero button rides up behind the pill it flies into a slot at the end, and when you scroll back it flies home. On narrow screens the pill grows down into a menu. @keyframeui/handoff-nav
- Release Pill The "New" badge above your headline, made useful. Hover it and the pill grows into a small changelog with your last few releases, then folds back into the pill when you leave. @keyframeui/release-pill