Polish Agent Orchestrator landing page

This commit is contained in:
codebanditssss 2026-06-28 17:04:23 +05:30
parent 01406eddca
commit 70577534f5
28 changed files with 1698 additions and 2160 deletions

View File

@ -1,5 +1,5 @@
/*
* AO Warm Terminal theme for Fumadocs
* AO Blue Terminal theme for Fumadocs
* ====================================
* CRITICAL: Fumadocs Tailwind utilities use --color-fd-* variables (NOT --fd-*).
* We must override --color-fd-* to change colors. These are inside layer(fumadocs),
@ -12,10 +12,10 @@
*/
:root {
/* Docs primary accent = AMBER (matches dashboard Orchestrator button + active items) */
--docs-accent: var(--color-accent-amber);
--docs-accent-dim: var(--color-accent-amber-dim);
--docs-accent-border: var(--color-accent-amber-border);
/* Docs primary accent = landing blue */
--docs-accent: var(--color-accent);
--docs-accent-dim: var(--color-accent-dim);
--docs-accent-border: var(--color-accent-border);
--color-fd-background: var(--color-bg-base);
--color-fd-foreground: var(--color-text-primary);
@ -36,9 +36,9 @@
}
.dark {
--docs-accent: var(--color-accent-amber);
--docs-accent-dim: var(--color-accent-amber-dim);
--docs-accent-border: var(--color-accent-amber-border);
--docs-accent: var(--color-accent);
--docs-accent-dim: var(--color-accent-dim);
--docs-accent-border: var(--color-accent-border);
--color-fd-background: var(--color-bg-base);
--color-fd-foreground: var(--color-text-primary);
@ -80,19 +80,19 @@
}
/*
1c. SEMANTIC COLORS override fumadocs info/warning/error colors to amber
1c. SEMANTIC COLORS override fumadocs info/warning/error colors
*/
:root {
--color-fd-info: var(--docs-accent);
--color-fd-warning: var(--color-accent-amber);
--color-fd-warning: var(--docs-accent);
--color-fd-success: #16a34a;
--color-fd-error: #dc2626;
}
.dark {
--color-fd-info: var(--docs-accent);
--color-fd-warning: var(--color-accent-amber);
--color-fd-warning: var(--docs-accent);
--color-fd-success: #22c55e;
--color-fd-error: #ef4444;
}
@ -100,10 +100,10 @@
/*
1c. FORCE WARM LINK COLORS fumadocs uses text-fd-primary (accent blue)
on too many elements. Override sidebar, TOC, and breadcrumb links
to warm gray. Only prose body links stay accent.
to neutral gray. Only prose body links stay accent.
*/
/* Sidebar links: warm stone gray, not periwinkle (desktop + mobile) */
/* Sidebar links: neutral gray (desktop + mobile) */
#nd-sidebar a:not([data-active="true"]),
#nd-sidebar button,
#nd-sidebar-mobile a:not([data-active="true"]),
@ -118,13 +118,13 @@
color: var(--color-text-primary) !important;
}
/* Sidebar active link: amber accent (desktop + mobile) */
/* Sidebar active link: blue accent (desktop + mobile) */
#nd-sidebar a[data-active="true"],
#nd-sidebar-mobile a[data-active="true"] {
color: var(--docs-accent) !important;
}
/* TOC links: warm gray, not blue */
/* TOC links: neutral gray by default */
#nd-toc a,
#nd-tocnav a {
color: var(--color-text-secondary) !important;
@ -140,7 +140,7 @@
color: var(--docs-accent) !important;
}
/* Breadcrumb: warm gray */
/* Breadcrumb: neutral gray */
#nd-page nav a,
#nd-page [aria-label="breadcrumb"] a {
color: var(--color-text-secondary) !important;
@ -161,12 +161,12 @@
color: var(--color-text-primary) !important;
}
/* Step number circles: amber */
/* Step number circles: blue */
#nd-docs-layout .fd-step::before {
background: var(--docs-accent) !important;
}
/* Footer prev/next: warm gray, not blue */
/* Footer prev/next: neutral gray by default */
#nd-page footer a,
#nd-page [class*="footer"] a {
color: var(--color-text-secondary) !important;
@ -253,7 +253,7 @@
color: inherit;
}
/* Links — amber accent like dashboard */
/* Links — blue accent like landing */
#nd-docs-layout .prose a {
color: var(--docs-accent);
text-decoration: none;
@ -374,12 +374,12 @@ pre.shiki code {
font-size: 12.5px;
}
/* Force info callout to amber (fumadocs uses --color-fd-info internally) */
/* Force info callout to blue (fumadocs uses --color-fd-info internally) */
#nd-docs-layout [data-callout][data-type="info"] {
--callout-color: var(--docs-accent) !important;
}
/* Edit on GitHub button — force amber colors */
/* Edit on GitHub button — force blue colors */
#nd-docs-layout a[href*="github.com"][href*="/blob/"] {
color: var(--color-text-secondary) !important;
}
@ -456,7 +456,7 @@ pre.shiki code {
padding: 0.75rem 1.5rem;
border-bottom: 1px solid var(--color-border-subtle);
background: var(--color-bg-inset);
color: var(--color-accent-amber);
color: var(--docs-accent);
font-family: var(--font-mono);
font-size: 0.6875rem;
font-weight: 600;
@ -515,7 +515,7 @@ pre.shiki code {
}
#nd-docs-layout .docs-missing-primary {
background: var(--color-accent-amber);
background: var(--docs-accent);
color: #1a1918;
}

View File

@ -1,49 +1,31 @@
import { LandingNav } from "../../components/LandingNav";
import { LandingHero } from "../../components/LandingHero";
import { LandingAbout } from "../../components/LandingAbout";
import { LandingAgentsBar } from "../../components/LandingAgentsBar";
import { LandingStats } from "../../components/LandingStats";
import { LandingVideo } from "../../components/LandingVideo";
import { LandingFeatures } from "../../components/LandingFeatures";
import { LandingWorkflow } from "../../components/LandingWorkflow";
import { LandingUseCases } from "../../components/LandingUseCases";
import { LandingTestimonials } from "../../components/LandingTestimonials";
import { LandingHowItWorks } from "../../components/LandingHowItWorks";
import { LandingQuickStart } from "../../components/LandingQuickStart";
import { LandingArchitecture } from "../../components/LandingArchitecture";
import { LandingLiveDemo } from "../../components/LandingLiveDemo";
import { LandingSocialProof } from "../../components/LandingSocialProof";
import { LandingCTA } from "../../components/LandingCTA";
import { LandingFooter } from "../../components/LandingFooter";
import { ScrollRevealProvider } from "../../components/ScrollRevealProvider";
import { PageConstellation } from "../../components/PageConstellation";
import { formatCompactNumber, getGitHubRepoStats } from "../../lib/github-repo";
export default async function LandingPage() {
const githubStats = await getGitHubRepoStats();
export default function LandingPage() {
return (
<ScrollRevealProvider>
<PageConstellation />
<div className="relative z-10">
<LandingNav />
<LandingHero starsLabel={formatCompactNumber(githubStats.stars)} />
<LandingAbout />
<LandingHero />
<LandingAgentsBar />
<LandingFeatures />
<div id="workflow">
<LandingWorkflow />
</div>
<div id="usecases">
<LandingUseCases />
</div>
<LandingHowItWorks />
<LandingVideo />
<LandingStats stats={githubStats} />
<LandingTestimonials />
<div id="quickstart">
<LandingQuickStart />
</div>
<LandingFeatures />
<LandingHowItWorks />
<LandingArchitecture />
<LandingLiveDemo />
<LandingSocialProof />
<LandingCTA />
<footer className="py-12 px-8 text-center text-[var(--landing-muted)] opacity-30 text-[0.8125rem] border-t border-white/[0.04]">
MIT Licensed · Open Source
</footer>
<LandingFooter />
</div>
</ScrollRevealProvider>
);

View File

@ -6,9 +6,30 @@ export const metadata: Metadata = {
description: "Open-source platform for running parallel AI coding agents.",
};
const themeScript = `
(() => {
try {
const stored = window.localStorage.getItem("ao-theme");
const theme = stored === "light" || stored === "dark"
? stored
: window.matchMedia("(prefers-color-scheme: light)").matches ? "light" : "dark";
document.documentElement.dataset.theme = theme;
document.documentElement.classList.toggle("dark", theme === "dark");
document.documentElement.style.colorScheme = theme;
} catch {
document.documentElement.dataset.theme = "dark";
document.documentElement.classList.add("dark");
document.documentElement.style.colorScheme = "dark";
}
})();
`;
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<script dangerouslySetInnerHTML={{ __html: themeScript }} />
</head>
<body>{children}</body>
</html>
);

View File

@ -1,60 +0,0 @@
export function LandingAbout() {
return (
<div className="bg-[radial-gradient(ellipse_at_top,rgba(255,240,220,0.015)_0%,transparent_70%)]">
<section className="landing-reveal py-[100px] px-6 max-w-[72rem] mx-auto">
<div className="text-xs tracking-[0.15em] uppercase text-[var(--landing-muted-dim)] mb-6 font-mono">
The problem
</div>
<h2 className="font-sans font-[680] text-[clamp(1.375rem,3vw,2rem)] leading-[1.1] tracking-[-1.5px] mb-10 max-w-[48rem]">
You&apos;re running AI agents in 10 browser tabs.{" "}
<span className="text-[var(--landing-muted)]">
Checking if PRs landed. Re-running failed CI. Copy-pasting error logs.
</span>
</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-8 items-start">
<p className="text-[0.9375rem] text-[var(--landing-muted)] leading-[1.8] max-w-[28rem]">
Agent Orchestrator replaces that with one YAML file. Point it at your GitHub issues, pick your agents, and
walk away. Each agent spawns in its own git worktree, creates PRs, fixes CI failures, addresses review
comments, and moves toward merge. If you are new, start with the{" "}
<a
href="/docs/"
className="underline decoration-[var(--landing-border-default)] underline-offset-4 hover:text-white"
>
docs quickstart and configuration guides
</a>
.
</p>
{/* Config preview — show how simple setup is */}
<div className="landing-card rounded-2xl overflow-hidden">
<div className="flex items-center gap-2 px-4 py-2.5 border-b border-[var(--landing-border-subtle)]">
<div className="w-2 h-2 rounded-full bg-[rgba(255,240,220,0.12)]" />
<div className="w-2 h-2 rounded-full bg-[rgba(255,240,220,0.12)]" />
<div className="w-2 h-2 rounded-full bg-[rgba(255,240,220,0.12)]" />
<span className="ml-1.5 font-mono text-[0.5625rem] text-[var(--landing-muted-dim)]">
agent-orchestrator.yaml
</span>
</div>
<pre className="px-5 py-4 font-mono text-[0.75rem] leading-[1.9] overflow-x-auto">
<span className="text-[var(--landing-muted-dim)]">agent:</span>{" "}
<span className="text-[var(--landing-fg)]">claude-code</span>
{"\n"}
<span className="text-[var(--landing-muted-dim)]">tracker:</span>{" "}
<span className="text-[var(--landing-fg)]">github</span>
{"\n"}
<span className="text-[var(--landing-muted-dim)]">workspace:</span>{" "}
<span className="text-[var(--landing-fg)]">worktree</span>
{"\n"}
<span className="text-[var(--landing-muted-dim)]">runtime:</span>{" "}
<span className="text-[var(--landing-fg)]">tmux</span>
{"\n"}
<span className="text-[var(--landing-muted-dim)]">notifier:</span>{" "}
<span className="text-[var(--landing-fg)]">slack</span>
</pre>
</div>
</div>
</section>
</div>
);
}

View File

@ -1,45 +1,55 @@
const agents = [
{
name: "Claude Code",
src: "/docs/logos/claude-code.svg",
alt: "Anthropic",
},
{
name: "Codex",
src: "/docs/logos/codex.svg",
alt: "OpenAI",
},
{
name: "Cursor",
src: "/docs/logos/cursor.svg",
alt: "Cursor",
},
{
name: "Aider",
src: "https://aider.chat/assets/logo.svg",
alt: "Aider",
},
{ name: "Claude Code", src: "/docs/logos/claude-code.svg", alt: "Claude Code", className: "h-7 sm:h-8 lg:h-10" },
{ name: "Codex", src: "/docs/logos/codex.svg", alt: "Codex", className: "h-7 sm:h-8 lg:h-10 rounded-md" },
{ name: "Cursor", src: "/docs/logos/cursor.svg", alt: "Cursor", className: "agent-logo-contrast h-7 sm:h-8 lg:h-10" },
{ name: "Aider", src: "/docs/logos/aider.png", alt: "Aider", className: "h-6 sm:h-7 lg:h-8" },
{
name: "OpenCode",
src: "/docs/logos/opencode.svg",
alt: "OpenCode",
className: "agent-logo-contrast h-7 sm:h-8 lg:h-10",
},
];
export function LandingAgentsBar() {
return (
<div className="landing-reveal text-center px-6 pt-[60px]">
<div className="text-[0.6875rem] tracking-[0.15em] uppercase text-[var(--landing-muted)] opacity-40 mb-5">
Works with your favorite AI agents
</div>
<div className="flex items-center justify-center gap-6 flex-wrap">
{agents.map((agent) => (
<div key={agent.name} className="flex flex-col items-center gap-2">
<img src={agent.src} alt={agent.alt} className="w-8 h-8 rounded-md object-contain" />
<div className="text-[0.6875rem] font-mono text-[var(--landing-muted)] opacity-50">{agent.name}</div>
<section
id="agents"
data-testid="agents-marquee"
className="relative overflow-hidden border-y border-[color:var(--border)] bg-[color:var(--bg-deep)]"
>
<div className="container-page py-7">
<div className="mx-auto flex max-w-[1280px] flex-wrap items-baseline justify-between gap-5">
<div className="flex flex-wrap items-baseline gap-x-4 gap-y-2">
<span className="serial-num font-mono text-xs">01 - coverage</span>
<h2 className="font-display text-2xl font-bold leading-none tracking-tight text-[color:var(--fg)] sm:text-3xl">
One daemon. <span className="text-[color:var(--fg-muted)]">Twenty-three agent harnesses.</span>
</h2>
</div>
))}
<p className="max-w-md font-mono text-xs leading-relaxed text-[color:var(--fg-dim)]">
Swap harnesses per project. The daemon does not care which CLI is in the pane - adapters obey one port.
</p>
</div>
</div>
</div>
<div className="container-page pb-6">
<div className="mx-auto grid max-w-2xl grid-cols-2 items-end justify-items-center gap-x-0 gap-y-4 sm:grid-cols-3 lg:grid-cols-5">
{agents.map((agent) => (
<div key={agent.name} className="group flex min-h-[60px] w-full flex-col items-center justify-end gap-2">
<div className="flex h-9 items-end justify-center sm:h-10 lg:h-11">
<img
src={agent.src}
alt={agent.alt}
className={`${agent.className} max-w-[56px] object-contain transition-transform duration-300 group-hover:-translate-y-0.5`}
/>
</div>
<div className="font-mono text-[14px] leading-none tracking-[0.06em] text-[color:var(--fg-dim)] sm:text-[16px] lg:text-[18px]">
{agent.name}
</div>
</div>
))}
</div>
</div>
</section>
);
}

View File

@ -0,0 +1,145 @@
const ports = [
{ name: "Agent", impls: "claude-code · codex · cursor · +20", accent: true },
{ name: "Runtime", impls: "tmux · process · conpty" },
{ name: "Workspace", impls: "git worktree" },
{ name: "SCM", impls: "GitHub" },
{ name: "Tracker", impls: "GitHub", muted: true },
{ name: "Reviewer", impls: "claude-code" },
];
export function LandingArchitecture() {
return (
<section
id="architecture"
data-testid="architecture-diagram"
className="relative border-t border-[color:var(--border)] py-24 sm:py-32"
>
<div className="container-page">
<div className="mb-14 grid items-end gap-8 lg:grid-cols-12">
<div className="lg:col-span-7">
<div className="serial-num mb-3 font-mono text-xs">04 - architecture</div>
<h2
className="font-display font-bold leading-[1.02] tracking-tight text-[color:var(--fg)]"
style={{ fontSize: "clamp(32px, 4.8vw, 60px)" }}
>
A daemon at the center.{" "}
<span className="font-editorial font-medium italic text-[color:var(--accent)]">Ports at the edges.</span>
</h2>
</div>
<div className="lg:col-span-5">
<p className="text-[15px] leading-relaxed text-[color:var(--fg-muted)]">
Hexagonal architecture. Inbound/outbound port contracts make every external system - agent, runtime,
workspace, SCM - a swappable adapter.
</p>
</div>
</div>
<div className="surface relative overflow-hidden">
<div className="dotgrid relative px-6 py-12 sm:px-10 sm:py-16">
<div className="mb-8 flex flex-wrap justify-center gap-6 sm:gap-12">
<ClientNode label="ao CLI" sub="thin daemon client" />
<ClientNode label="Electron app" sub="desktop supervisor" />
</div>
<Wires variant="clients" />
<div className="mb-10 flex justify-center">
<div className="relative">
<div className="absolute -inset-px rounded-xl bg-gradient-to-br from-[color:var(--accent)] to-transparent opacity-40 blur-sm" />
<div className="glow-accent relative rounded-xl border border-[color:var(--accent)] bg-[color:var(--bg-deep)] px-8 py-6 text-[color:var(--fg)] sm:px-14 sm:py-8">
<div className="mb-2 flex items-center gap-2">
<span className="pulse-dot h-1.5 w-1.5 rounded-full bg-[color:var(--accent)]" />
<span className="font-mono text-[10px] uppercase tracking-[0.22em] text-[color:var(--accent)]">
127.0.0.1 · loopback only
</span>
</div>
<div className="font-display text-3xl font-bold tracking-tight sm:text-4xl">Go daemon</div>
<div className="mt-4 grid grid-cols-2 gap-2 sm:grid-cols-4">
{["HTTP API", "Lifecycle mgr", "CDC stream", "SQLite store"].map((item) => (
<div
key={item}
className="rounded border border-[color:var(--border-strong)] bg-[color:var(--bg-card)] px-2.5 py-1.5 text-center font-mono text-[10px] uppercase tracking-wider text-[color:var(--fg-muted)]"
>
{item}
</div>
))}
</div>
</div>
</div>
</div>
<Wires variant="ports" />
<div className="grid grid-cols-2 gap-3 sm:grid-cols-3 lg:grid-cols-6">
{ports.map((port) => (
<Port key={port.name} port={port} />
))}
</div>
</div>
<div className="flex flex-wrap items-center justify-between gap-3 border-t border-[color:var(--border)] bg-[color:var(--bg-chrome)] px-6 py-4 font-mono text-[10px] uppercase tracking-[0.22em] text-[color:var(--fg-dim)] sm:px-10">
<span>
<span className="text-[color:var(--fg-muted)]">ports/</span> backend/internal/ports/
</span>
<span>
<span className="text-[color:var(--fg-muted)]">adapters/</span> registered at boot
</span>
<span>
<span className="text-[color:var(--fg-muted)]">events/</span> sse fan-out
</span>
</div>
</div>
</div>
</section>
);
}
function ClientNode({ label, sub }: { label: string; sub: string }) {
return (
<div className="text-center">
<div className="surface-elev lift inline-block px-6 py-3">
<div className="font-display text-lg font-bold tracking-tight text-[color:var(--fg)]">{label}</div>
</div>
<div className="mt-2 font-mono text-[10px] uppercase tracking-[0.22em] text-[color:var(--fg-dim)]">{sub}</div>
</div>
);
}
function Port({ port }: { port: (typeof ports)[number] }) {
return (
<div
className={`surface lift px-3 py-3 ${port.muted ? "opacity-60" : ""} ${port.accent ? "border-[color:var(--accent)]" : ""}`}
>
<div className="mb-1 flex items-center gap-1.5">
<span
className="h-1 w-1 rounded-full"
style={{ background: port.accent ? "var(--accent)" : "var(--fg-dim)" }}
/>
<span className="font-mono text-[9px] uppercase tracking-[0.22em] text-[color:var(--fg-dim)]">port</span>
</div>
<div className="font-display text-[15px] font-bold tracking-tight text-[color:var(--fg)]">{port.name}</div>
<div className="mt-1 font-mono text-[10px] leading-snug text-[color:var(--fg-muted)]">{port.impls}</div>
</div>
);
}
function Wires({ variant }: { variant: "clients" | "ports" }) {
const paths =
variant === "clients"
? ["M260 0 L300 60", "M340 0 L300 60"]
: ["M300 0 L80 60", "M300 0 L200 60", "M300 0 L300 60", "M300 0 L400 60", "M300 0 L520 60"];
return (
<div className="relative mb-2 flex h-12 justify-center sm:h-16">
<svg viewBox="0 0 600 60" className="h-full w-full max-w-[760px]">
{paths.map((d, i) => (
<g key={d}>
<path d={d} stroke="var(--wire)" strokeWidth="1.4" fill="none" />
<path
d={d}
stroke="var(--accent)"
strokeWidth="1.8"
fill="none"
strokeDasharray="16 200"
style={{ animation: `wire-pulse 2.6s ease-in-out ${i * 0.3}s infinite` }}
/>
</g>
))}
</svg>
</div>
);
}

View File

@ -1,31 +1,81 @@
function GithubIcon({ className = "" }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M12 .5C5.65.5.5 5.65.5 12c0 5.08 3.29 9.38 7.86 10.9.58.1.79-.25.79-.56v-2.15c-3.2.7-3.88-1.37-3.88-1.37-.52-1.34-1.28-1.7-1.28-1.7-1.05-.72.08-.7.08-.7 1.16.08 1.77 1.2 1.77 1.2 1.03 1.76 2.7 1.25 3.36.96.1-.75.4-1.25.73-1.54-2.56-.29-5.26-1.28-5.26-5.7 0-1.26.45-2.29 1.19-3.1-.12-.3-.52-1.47.11-3.05 0 0 .97-.31 3.18 1.18A10.96 10.96 0 0 1 12 5.99c.98 0 1.97.13 2.9.38 2.2-1.49 3.17-1.18 3.17-1.18.63 1.58.23 2.75.11 3.05.74.81 1.19 1.84 1.19 3.1 0 4.43-2.7 5.4-5.27 5.69.41.36.78 1.07.78 2.16v3.2c0 .31.21.67.8.55A11.51 11.51 0 0 0 23.5 12C23.5 5.65 18.35.5 12 .5Z" />
</svg>
);
}
function ArrowRightIcon({ className = "" }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
<path d="M5 12h14" />
<path d="m12 5 7 7-7 7" />
</svg>
);
}
function BookIcon({ className = "" }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
<path d="M12 7v14" />
<path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z" />
</svg>
);
}
export function LandingCTA() {
return (
<section className="text-center py-40 px-6 bg-[radial-gradient(ellipse_at_center,rgba(255,255,255,0.015)_0%,transparent_60%)]">
<div className="landing-reveal">
<p className="text-[var(--landing-muted)] opacity-50 text-2xl font-sans font-[680] tracking-tight mb-4">
Stop babysitting.
</p>
<h2 className="font-sans font-[680] tracking-tight font-normal text-[clamp(1.375rem,3vw,2rem)] leading-[1.05] tracking-[-2px] mb-4">
Start <em className="italic text-[var(--landing-muted)]">orchestrating.</em>
</h2>
<div className="landing-card inline-flex items-center gap-3 rounded-lg px-6 py-3 font-mono text-[0.9375rem] text-white mb-8">
<span className="text-[var(--landing-muted)] opacity-40">$</span> npm i -g @aoagents/ao
</div>
<div className="flex items-center justify-center gap-4 flex-wrap">
<a
href="/docs"
className="landing-card rounded-lg px-6 py-3 text-[0.9375rem] text-[var(--landing-muted)] no-underline transition-colors hover:text-white"
<section
id="cta"
data-testid="cta-section"
className="relative overflow-hidden border-t border-[color:var(--border)] py-24 sm:py-32"
>
<div className="container-page relative">
<div className="surface-elev px-8 py-14 text-center sm:px-14 sm:py-20">
<div className="mb-8 inline-flex items-center gap-2 rounded-full border border-[color:var(--border-strong)] bg-[color:var(--bg-deep)] px-3 py-1">
<span className="font-mono text-[10px] uppercase tracking-[0.22em] text-[color:var(--accent)]">
$ ao spawn --project your-repo --prompt &quot;ship it&quot;
</span>
</div>
<h2
data-testid="cta-headline"
className="font-display font-bold leading-[1.02] tracking-tight text-[color:var(--fg)]"
style={{ fontSize: "clamp(36px, 6vw, 76px)" }}
>
Read Docs
</a>
<a
href="https://github.com/ComposioHQ/agent-orchestrator"
target="_blank"
rel="noopener noreferrer"
className="liquid-glass-solid rounded-lg px-6 py-3 text-[0.9375rem] no-underline transition-transform hover:scale-[1.03]"
>
View on GitHub
</a>
Stop babysitting one agent.
<br />
<span className="font-editorial font-medium italic text-[color:var(--accent)]">Start orchestrating.</span>
</h2>
<p className="mx-auto mt-6 max-w-2xl text-[16px] leading-relaxed text-[color:var(--fg-muted)] sm:text-[17px]">
Free, Apache 2.0 licensed, runs on your laptop. The whole repo is on GitHub - read the source, fork it, and
ship your first parallel agent in five minutes.
</p>
<div className="mt-10 flex flex-wrap items-center justify-center gap-3">
<a
href="https://github.com/AgentWrapper/agent-orchestrator"
target="_blank"
rel="noreferrer"
data-testid="cta-github-btn"
className="group inline-flex items-center gap-2 rounded-lg bg-[color:var(--accent)] px-6 py-3.5 text-[15px] font-semibold text-white shadow-[0_0_0_1px_rgba(255,255,255,0.1)_inset,0_8px_24px_-8px_rgba(77,141,255,0.6)] transition-all hover:brightness-110"
style={{ color: "#fff" }}
>
<GithubIcon className="h-4 w-4" />
Star on GitHub · 7.7k
<ArrowRightIcon className="h-4 w-4 transition-transform group-hover:translate-x-0.5" />
</a>
<a
href="/docs/architecture"
data-testid="cta-docs-btn"
className="inline-flex items-center gap-2 rounded-lg border border-[color:var(--border-strong)] bg-[color:var(--bg-deep)] px-6 py-3.5 text-[15px] font-semibold text-[color:var(--fg)] transition-colors hover:bg-[color:var(--bg-card-hover)]"
>
<BookIcon className="h-4 w-4" />
Read the architecture
</a>
</div>
</div>
</div>
</section>

View File

@ -1,520 +1,208 @@
"use client";
import { useEffect, useRef, useState } from "react";
type DemoKind = "parallel" | "recovery" | "plugins" | "dashboard";
const features: { n: string; title: string; desc: string; demo: DemoKind }[] = [
const features = [
{
n: "01",
title: "Multi-agent execution",
desc: "Run Claude Code, Codex, Cursor, Aider, and OpenCode in parallel. Each agent in its own git worktree, branch, and context.",
demo: "parallel",
kicker: "the substrate",
title: "An operating system, not a wrapper.",
desc: "Inbound and outbound port contracts. Swappable adapters. A CDC stream. The kind of substrate that survives the next model upgrade - and the one after that.",
visual: "ports",
accent: true,
span: "lg:col-span-2",
},
{
n: "02",
title: "Autonomous CI + review handling",
desc: "CI fails? The agent reads the logs and pushes a fix. Review comments land? The agent addresses them. You sleep, your agents ship.",
demo: "recovery",
kicker: "isolation",
title: "Every agent gets a worktree.",
desc: "No branch collisions. No stash gymnastics. Each session lives in its own git worktree with its own attachable pane.",
visual: "branch",
span: "lg:col-span-1",
},
{
n: "03",
title: "Seven swappable slots",
desc: "Runtime, Agent, Workspace, Tracker, SCM, Notifier, Terminal. Use tmux or process. GitHub or GitLab. Slack or webhooks.",
demo: "plugins",
kicker: "feedback loop",
title: "PRs watched. Agents nudged.",
desc: "CI failure, requested change, merge conflict - the lifecycle manager routes each fact back to the owning agent automatically.",
visual: "pr",
span: "lg:col-span-1",
},
{
n: "04",
title: "Real-time Kanban + terminal",
desc: "Every agent's state in one view. Attach to any terminal via the browser. SSE updates every 5 seconds. WebSocket for live I/O.",
demo: "dashboard",
kicker: "durability",
title: "Durable facts. Derived status.",
desc: "SQLite stores a small set of session facts. Display state is computed at read time. Triggers append to change_log; CDC fans events out via SSE.",
visual: "log",
span: "lg:col-span-2",
},
{
kicker: "trust model",
title: "Bound to 127.0.0.1.",
desc: "No auth, no CORS, no TLS. No SaaS in the loop. Your threat model fits on a sticky note.",
span: "lg:col-span-1",
},
{
kicker: "lifecycle",
title: "Lifecycle manager + reaper.",
desc: "Reduces runtime, activity and PR observations into durable state. Crash-safe reconcile on every boot.",
span: "lg:col-span-1",
},
{
kicker: "interfaces",
title: "ao CLI and Electron app.",
desc: "Both drive the same daemon over loopback. Spawn from a terminal; supervise in a desktop kanban.",
span: "lg:col-span-1",
},
];
// The feature's animated demo — the stacked back panel + a smaller front peek,
// reused as-is from the original switcher so each card stays rich.
function FeatureDemo({ kind }: { kind: DemoKind }) {
return (
<div className="relative h-[460px]">
<div className="absolute top-0 right-0 w-[90%] h-[390px] bg-[rgba(255,240,220,0.04)] rounded-2xl border border-[var(--landing-border-subtle)] overflow-hidden landing-feat-card">
{kind === "parallel" && <ParallelBack />}
{kind === "recovery" && <RecoveryBack />}
{kind === "plugins" && <PluginsBack />}
{kind === "dashboard" && <DashboardBack />}
</div>
<div className="absolute bottom-0 left-0 w-[58%] h-[230px] bg-[rgba(255,240,220,0.06)] rounded-2xl border border-[var(--landing-border-default)] overflow-hidden landing-feat-card-front">
{kind === "parallel" && <ParallelFront />}
{kind === "recovery" && <RecoveryFront />}
{kind === "plugins" && <PluginsFront />}
{kind === "dashboard" && <DashboardFront />}
</div>
</div>
);
}
// Sticky offset from the top of the viewport where each card pins (leaves room
// for the fixed nav); each successive card pins STACK_GAP lower so the tops peek.
const BASE_TOP = 120;
const STACK_GAP = 26;
export function LandingFeatures() {
const cardRefs = useRef<(HTMLDivElement | null)[]>([]);
const [stack, setStack] = useState(false);
// Scroll-stack only on desktop; on narrow screens cards read as a plain list.
useEffect(() => {
const mq = window.matchMedia("(min-width: 768px)");
const apply = () => setStack(mq.matches);
apply();
mq.addEventListener("change", apply);
return () => mq.removeEventListener("change", apply);
}, []);
// As later cards pin on top, shrink + dim the cards beneath them so the deck
// reads as a stack. CSS transition smooths the steps; rAF throttles scroll.
useEffect(() => {
const els = cardRefs.current;
if (!stack) {
els.forEach((el) => {
if (el) {
el.style.transform = "";
el.style.opacity = "";
}
});
return;
}
let raf = 0;
const update = () => {
raf = 0;
els.forEach((el, i) => {
if (!el) return;
let depth = 0;
for (let j = i + 1; j < els.length; j++) {
const ej = els[j];
if (ej && ej.getBoundingClientRect().top <= BASE_TOP + j * STACK_GAP + 0.5) {
depth += 1;
}
}
el.style.transform = `scale(${1 - depth * 0.05})`;
el.style.opacity = `${Math.max(1 - depth * 0.16, 0.55)}`;
});
};
const onScroll = () => {
if (!raf) raf = requestAnimationFrame(update);
};
update();
window.addEventListener("scroll", onScroll, { passive: true });
window.addEventListener("resize", onScroll);
return () => {
window.removeEventListener("scroll", onScroll);
window.removeEventListener("resize", onScroll);
if (raf) cancelAnimationFrame(raf);
};
}, [stack]);
return (
<section className="py-[96px] px-6 md:px-16 max-w-[68rem] mx-auto" id="features">
<div className="landing-reveal text-center">
<span className="inline-block border border-[var(--landing-border-strong)] rounded-full px-4 py-[5px] text-[13px] text-[var(--landing-muted)] mb-5">
Features
</span>
</div>
<h2
className="landing-reveal text-center mx-auto mb-[72px] max-w-[36rem] text-[var(--landing-fg)]"
style={{
fontFamily: "var(--font-instrument-serif), ui-serif, Georgia, serif",
fontSize: "clamp(2.25rem, 5vw, 3.5rem)",
lineHeight: 1.08,
fontWeight: 400,
}}
>
A unified orchestrator <em className="italic text-[var(--landing-muted)]">that scales.</em>
</h2>
<div className="relative">
{features.map((f, i) => (
<div
key={f.n}
ref={(el) => {
cardRefs.current[i] = el;
}}
className="landing-card rounded-2xl grid grid-cols-1 md:grid-cols-2 gap-8 md:gap-12 items-center overflow-hidden"
style={{
padding: "clamp(1.5rem, 3vw, 2.5rem)",
marginBottom: "1.5rem",
transformOrigin: "center top",
transition: "transform 0.4s ease, opacity 0.4s ease, border-color 0.2s ease",
...(stack ? { position: "sticky", top: `${BASE_TOP + i * STACK_GAP}px`, zIndex: i + 1 } : null),
}}
>
<div>
<div className="font-mono text-xs tracking-[0.1em] text-[var(--landing-muted)] opacity-50 mb-4">
{f.n}
</div>
<h3 className="font-sans font-[680] tracking-tight text-[1.375rem] mb-4">{f.title}</h3>
<p className="text-[var(--landing-muted)] text-[0.9375rem] leading-[1.7] max-w-[28rem]">{f.desc}</p>
</div>
<FeatureDemo kind={f.demo} />
<section id="features" data-testid="features-grid" className="relative py-24 sm:py-32">
<div className="container-page">
<div className="mb-14 grid items-end gap-8 lg:grid-cols-12">
<div className="lg:col-span-7">
<div className="serial-num mb-3 font-mono text-xs">02 - what&apos;s inside</div>
<h2
className="font-display font-bold leading-[1.05] tracking-tight text-[color:var(--fg)]"
style={{ fontSize: "clamp(32px, 4.5vw, 56px)" }}
>
Built like an operating system,{" "}
<span className="font-editorial font-medium italic text-[color:var(--fg-muted)]">not a wrapper.</span>
</h2>
</div>
))}
<div className="lg:col-span-5">
<p className="text-[15px] leading-relaxed text-[color:var(--fg-muted)]">
Inbound/outbound port contracts. Swappable adapters. A CDC stream - the kind of substrate that survives
the next model upgrade. And the one after that.
</p>
</div>
</div>
<div className="grid gap-4 lg:grid-cols-3">
{features.map((feature, i) => (
<FeatureCard key={feature.title} feature={feature} index={i} />
))}
</div>
</div>
</section>
);
}
/* ──────── 01 · Parallel ──────── */
function ParallelBack() {
const agents = [
{ name: "claude-code", task: "#42 auth", color: "rgba(255,159,102,0.7)", dur: 3.4, delay: 0 },
{ name: "codex", task: "#43 pagination", color: "rgba(134,239,172,0.65)", dur: 4.2, delay: 0.5 },
{ name: "aider", task: "#44 rate limit", color: "rgba(167,139,250,0.65)", dur: 3.6, delay: 1.0 },
{ name: "opencode", task: "#46 db refactor", color: "rgba(96,165,250,0.65)", dur: 4.8, delay: 0.3 },
];
function FeatureCard({ feature, index }: { feature: (typeof features)[number]; index: number }) {
return (
<div className="p-5 h-full flex flex-col">
<div className="flex items-center justify-between mb-4">
<span className="font-mono text-[0.625rem] tracking-[0.12em] uppercase text-[var(--landing-muted-dim)]">
4 sessions · parallel
</span>
<span className="font-mono text-[0.5625rem] text-[var(--landing-muted-dim)] flex items-center gap-1.5">
<span className="w-1 h-1 rounded-full bg-[rgba(134,239,172,0.7)] landing-sse-pulse" />
live
</span>
</div>
<div className="grid grid-cols-2 gap-2.5 flex-1">
{agents.map((a) => (
<div
key={a.name}
className="bg-[rgba(255,240,220,0.035)] border border-[var(--landing-border-subtle)] rounded-xl p-3 flex flex-col"
>
<div className="flex items-center gap-1.5 mb-1">
<span className="w-1.5 h-1.5 rounded-full shrink-0" style={{ background: a.color }} />
<span className="font-mono text-[0.6875rem] text-[var(--landing-fg)]/85 truncate">{a.name}</span>
</div>
<div className="font-mono text-[0.625rem] text-[var(--landing-muted)] opacity-65 mb-auto truncate">
{a.task}
</div>
<div className="h-[3px] rounded-full bg-[var(--landing-border-subtle)] overflow-hidden mt-2">
<div
className="h-full landing-feature-bar"
style={{
background: a.color,
animationDuration: `${a.dur}s`,
animationDelay: `${a.delay}s`,
}}
/>
</div>
</div>
))}
</div>
</div>
);
}
function ParallelFront() {
const fleet = [
{ name: "claude-code", color: "rgba(255,159,102,0.85)" },
{ name: "codex", color: "rgba(134,239,172,0.75)" },
{ name: "aider", color: "rgba(167,139,250,0.75)" },
{ name: "opencode", color: "rgba(96,165,250,0.75)" },
{ name: "cursor", color: "rgba(244,114,182,0.65)" },
];
return (
<div className="flex flex-col p-5 w-full">
<div className="font-mono text-[0.5625rem] tracking-[0.12em] uppercase text-[var(--landing-muted-dim)] mb-2.5">
Fleet · 5 agents
</div>
<div className="flex flex-col gap-1.5">
{fleet.map((a) => (
<div
key={a.name}
className="flex items-center gap-2 bg-[rgba(255,240,220,0.04)] border border-[var(--landing-border-subtle)] rounded-md px-2.5 py-1.5"
>
<span className="w-1.5 h-1.5 rounded-full shrink-0" style={{ background: a.color }} />
<span className="font-mono text-[0.6875rem] text-[var(--landing-fg)]/85 truncate">{a.name}</span>
</div>
))}
</div>
</div>
);
}
/* ──────── 02 · Recovery ──────── */
const recoveryStages: { time: string; text: string; kind: "info" | "fail" | "fix" | "ok" }[] = [
{ time: "10:42", text: "agent.spawn → s-312", kind: "info" },
{ time: "10:43", text: "✗ tests/auth failed", kind: "fail" },
{ time: "10:44", text: "agent.investigate()", kind: "info" },
{ time: "10:44", text: "patch · re-running ci", kind: "fix" },
{ time: "10:45", text: "✓ tests/auth (48/48)", kind: "ok" },
{ time: "10:45", text: "✗ lint failed", kind: "fail" },
{ time: "10:46", text: "patch · eslint --fix", kind: "fix" },
{ time: "10:47", text: "✓ lint passed", kind: "ok" },
{ time: "10:47", text: "● ready to merge", kind: "ok" },
];
function RecoveryBack() {
const [count, setCount] = useState(3);
useEffect(() => {
const id = setInterval(() => {
setCount((c) => (c >= recoveryStages.length ? 3 : c + 1));
}, 1000);
return () => clearInterval(id);
}, []);
const visible = recoveryStages.slice(0, count);
return (
<div className="p-5 h-full flex flex-col font-mono text-[0.6875rem]">
<div className="flex items-center justify-between mb-3 pb-3 border-b border-[var(--landing-border-subtle)]">
<span className="text-[var(--landing-fg)]/80">PR #312 · feat/user-auth</span>
<span className="text-[0.5625rem] uppercase tracking-[0.1em] text-[var(--landing-muted-dim)]">healing</span>
</div>
<div className="flex-1 space-y-1.5 overflow-hidden">
{visible.map((s, i) => {
const isLast = i === visible.length - 1;
const color =
s.kind === "fail"
? "text-[rgba(248,113,113,0.85)]"
: s.kind === "ok"
? "text-[rgba(134,239,172,0.85)]"
: s.kind === "fix"
? "text-[rgba(251,191,36,0.85)]"
: "text-[var(--landing-muted)]";
return (
<div
key={`${i}-${s.text}`}
className={`flex items-baseline gap-2.5 ${isLast ? "landing-stream-line" : ""}`}
>
<span className="text-[var(--landing-muted-dim)] opacity-50 w-9 shrink-0">{s.time}</span>
<span className={`${color} truncate`}>{s.text}</span>
</div>
);
})}
</div>
</div>
);
}
function RecoveryFront() {
return (
<div className="grid grid-cols-2 gap-2.5 p-5 w-full h-full items-stretch">
<div className="bg-[rgba(248,113,113,0.05)] border border-[rgba(248,113,113,0.18)] rounded-xl p-3 flex flex-col items-center justify-center gap-1">
<span className="font-mono text-[0.5rem] tracking-[0.12em] uppercase text-[rgba(248,113,113,0.7)]">before</span>
<span className="text-[1.75rem] leading-none text-[rgba(248,113,113,0.85)]"></span>
<span className="font-mono text-[0.625rem] text-[var(--landing-fg)]/70">12/48</span>
</div>
<div className="bg-[rgba(134,239,172,0.05)] border border-[rgba(134,239,172,0.2)] rounded-xl p-3 flex flex-col items-center justify-center gap-1">
<span className="font-mono text-[0.5rem] tracking-[0.12em] uppercase text-[rgba(134,239,172,0.7)]">after</span>
<span className="text-[1.75rem] leading-none text-[rgba(134,239,172,0.85)]"></span>
<span className="font-mono text-[0.625rem] text-[var(--landing-fg)]/70">48/48</span>
</div>
</div>
);
}
/* ──────── 03 · Plugins ──────── */
function PluginsBack() {
const slots = [
{ slot: "agent", values: ["claude-code", "codex", "aider", "opencode"] },
{ slot: "tracker", values: ["github", "linear", "gitlab"] },
{ slot: "runtime", values: ["tmux", "process"] },
{ slot: "workspace", values: ["worktree", "clone"] },
{ slot: "scm", values: ["github", "gitlab"] },
{ slot: "notifier", values: ["slack", "webhook", "desktop"] },
{ slot: "terminal", values: ["iterm2", "web"] },
];
const [tick, setTick] = useState(0);
useEffect(() => {
const id = setInterval(() => setTick((t) => t + 1), 1600);
return () => clearInterval(id);
}, []);
return (
<div className="p-5 h-full flex flex-col">
<div className="flex items-center justify-between mb-3 pb-3 border-b border-[var(--landing-border-subtle)]">
<span className="font-mono text-[0.6875rem] text-[var(--landing-fg)]/80">agent-orchestrator.yaml</span>
<span className="font-mono text-[0.5625rem] tracking-[0.1em] uppercase text-[var(--landing-muted-dim)]">
7 slots
</span>
</div>
<div className="flex flex-col gap-1.5 font-mono text-[0.6875rem]">
{slots.map((s, i) => {
const val = s.values[(tick + i) % s.values.length];
return (
<div key={s.slot} className="flex items-center gap-3">
<span className="text-[var(--landing-muted-dim)] w-[4.5rem] shrink-0">{s.slot}:</span>
<span
key={val}
className="landing-chip-swap inline-block px-2 py-[1px] rounded-md bg-[rgba(255,240,220,0.05)] text-[var(--landing-fg)]/85 border border-[var(--landing-border-subtle)]"
>
{val}
</span>
</div>
);
})}
</div>
</div>
);
}
function PluginsFront() {
const pairs = [
{ from: "tmux", to: "process" },
{ from: "github", to: "linear" },
{ from: "slack", to: "webhook" },
{ from: "worktree", to: "clone" },
];
const [idx, setIdx] = useState(0);
useEffect(() => {
const id = setInterval(() => setIdx((i) => (i + 1) % pairs.length), 1800);
return () => clearInterval(id);
}, []);
const p = pairs[idx];
return (
<div className="flex flex-col items-center justify-center gap-3 p-5 w-full h-full">
<span className="font-mono text-[0.5625rem] tracking-[0.12em] uppercase text-[var(--landing-muted-dim)]">
swap
</span>
<div className="flex items-center gap-3">
<span
key={`from-${idx}`}
className="landing-chip-swap font-mono text-[0.8125rem] px-2.5 py-1 rounded-md bg-[rgba(255,240,220,0.05)] text-[var(--landing-fg)]/85 border border-[var(--landing-border-subtle)]"
<article
data-testid={`feature-article-${String(index + 1).padStart(2, "0")}`}
className={`surface lift group relative overflow-hidden p-6 ${feature.span || ""} ${
feature.accent ? "bg-gradient-to-br from-[color:var(--bg-card)] to-[#0d1220]" : ""
}`}
>
<div className="mb-5 flex items-center gap-2">
<div
className={`flex h-8 w-8 items-center justify-center rounded-md border font-mono text-xs ${
feature.accent
? "border-[color:var(--accent)] bg-[color:var(--accent-soft)] text-[color:var(--accent)]"
: "border-[color:var(--border-strong)] bg-[color:var(--bg-deep)] text-[color:var(--fg-muted)]"
}`}
>
{p.from}
{String(index + 1).padStart(2, "0")}
</div>
<span className="font-mono text-[10px] uppercase tracking-[0.22em] text-[color:var(--fg-dim)]">
{feature.kicker}
</span>
<span className="text-[var(--landing-muted)] text-base"></span>
<span
key={`to-${idx}`}
className="landing-chip-swap font-mono text-[0.8125rem] px-2.5 py-1 rounded-md bg-[rgba(255,240,220,0.08)] text-[var(--landing-fg)]/90 border border-[var(--landing-border-default)]"
</div>
<h3 className="font-display mb-2.5 text-[19px] font-bold leading-snug tracking-tight text-[color:var(--fg)] sm:text-[21px]">
{feature.title}
</h3>
<p className="text-[14px] leading-relaxed text-[color:var(--fg-muted)]">{feature.desc}</p>
{feature.visual === "ports" && <PortsVisual />}
{feature.visual === "branch" && <BranchVisual />}
{feature.visual === "pr" && <PrVisual />}
{feature.visual === "log" && <LogVisual />}
</article>
);
}
function PortsVisual() {
return (
<div className="mt-6 grid grid-cols-3 gap-2 border-t border-[color:var(--border)] pt-5">
{["Agent", "Runtime", "Workspace", "SCM", "Tracker", "Reviewer"].map((port, i) => (
<div
key={port}
className="flex items-center gap-1.5 py-1 font-mono text-[10px] uppercase tracking-wider text-[color:var(--fg-muted)]"
>
{p.to}
</span>
<span className="h-1 w-1 rounded-full" style={{ background: i === 0 ? "var(--accent)" : "var(--fg-dim)" }} />
{port}
</div>
))}
</div>
);
}
function BranchVisual() {
return (
<svg viewBox="0 0 200 60" className="mt-5 h-auto w-full opacity-90">
<path d="M10 30 L60 30" stroke="rgba(255,255,255,0.25)" strokeWidth="1.5" fill="none" />
<path d="M60 30 L60 10 L190 10" stroke="var(--accent)" strokeWidth="1.5" fill="none" />
<path d="M60 30 L190 30" stroke="rgba(255,255,255,0.25)" strokeWidth="1.5" fill="none" />
<path d="M60 30 L60 50 L190 50" stroke="var(--accent)" strokeWidth="1.5" fill="none" />
<circle cx="10" cy="30" r="3" fill="var(--fg-muted)" />
<circle cx="60" cy="30" r="3" fill="var(--fg)" />
<circle cx="190" cy="10" r="3.5" fill="var(--accent)" />
<circle cx="190" cy="30" r="3.5" fill="var(--fg-muted)" />
<circle cx="190" cy="50" r="3.5" fill="var(--accent)" />
</svg>
);
}
function PrVisual() {
return (
<div className="mt-5 space-y-1 font-mono text-[11px]">
<Row color="var(--status-ok)" label="lint" status="pass" />
<Row color="var(--status-ok)" label="unit" status="pass" />
<Row color="var(--status-fail)" label="e2e" status="fail" highlight />
<Row color="var(--fg-muted)" label="review" status="requested" />
<div className="mt-2 border-t border-[color:var(--border)] pt-2 text-[10px] uppercase tracking-wider text-[color:var(--accent)]">
-&gt; nudge -&gt; sess_8f2
</div>
</div>
);
}
/* ──────── 04 · Dashboard ──────── */
type KanbanCard = {
id: number;
col: 0 | 1 | 2;
title: string;
agent: string;
function Row({
color,
label,
status,
highlight,
}: {
color: string;
};
function DashboardBack() {
const [cards, setCards] = useState<KanbanCard[]>([
{ id: 1, col: 0, title: "Add user auth", agent: "claude-code", color: "rgba(255,159,102,0.7)" },
{ id: 2, col: 0, title: "Fix pagination", agent: "codex", color: "rgba(134,239,172,0.65)" },
{ id: 3, col: 1, title: "Add rate limit", agent: "aider", color: "rgba(167,139,250,0.65)" },
{ id: 4, col: 2, title: "Refactor DB", agent: "opencode", color: "rgba(96,165,250,0.65)" },
]);
useEffect(() => {
const id = setInterval(() => {
setCards((prev) => {
const advanceable = prev.filter((c) => c.col < 2);
if (advanceable.length === 0) {
return prev.map((c) => ({ ...c, col: 0 as 0 | 1 | 2 }));
}
const oldest = advanceable[0];
return prev.map((c) => (c.id === oldest.id ? { ...c, col: (c.col + 1) as 0 | 1 | 2 } : c));
});
}, 2400);
return () => clearInterval(id);
}, []);
const cols = ["Working", "Review", "Merged"];
label: string;
status: string;
highlight?: boolean;
}) {
return (
<div className="p-5 h-full flex flex-col">
<div className="flex items-center justify-between mb-3 pb-3 border-b border-[var(--landing-border-subtle)]">
<span className="font-mono text-[0.6875rem] text-[var(--landing-fg)]/80">my-saas-app · 4 sessions</span>
<span className="font-mono text-[0.5625rem] tracking-[0.1em] uppercase text-[var(--landing-muted-dim)] flex items-center gap-1.5">
<span className="w-1 h-1 rounded-full bg-[rgba(134,239,172,0.7)] landing-sse-pulse" />
sse
</span>
</div>
<div className="grid grid-cols-3 gap-2 flex-1">
{cols.map((name, col) => (
<div key={name} className="space-y-1.5">
<div className="font-mono text-[0.5rem] tracking-[0.12em] uppercase text-[var(--landing-muted-dim)] mb-1.5">
{name}
</div>
{cards
.filter((c) => c.col === col)
.map((c) => (
<div
key={c.id}
className="bg-[rgba(255,240,220,0.035)] border border-[var(--landing-border-subtle)] rounded-lg p-2 text-[0.625rem] leading-tight landing-stream-line"
>
<div className="text-[var(--landing-fg)]/85 truncate mb-1">{c.title}</div>
<div className="flex items-center gap-1">
<span className="w-1 h-1 rounded-full shrink-0" style={{ background: c.color }} />
<span className="font-mono text-[0.5rem] text-[var(--landing-muted-dim)] truncate">{c.agent}</span>
</div>
</div>
))}
</div>
))}
</div>
<div className="flex items-center justify-between">
<span className="flex items-center gap-1.5 text-[color:var(--fg)]">
<span className="h-1.5 w-1.5 rounded-full" style={{ background: color }} />
{label}
</span>
<span
className="text-[10px] uppercase tracking-wider"
style={{ color: highlight ? "var(--status-fail)" : "var(--fg-muted)" }}
>
{status}
</span>
</div>
);
}
const streamPool = [
"tests/auth.py::test_login",
"tests/api.py::test_pagination",
"tests/db.py::test_migration",
"tests/queue.py::test_dequeue",
"tests/auth.py::test_logout",
"tests/api.py::test_cursor",
"tests/db.py::test_index",
"tests/queue.py::test_retry",
];
function DashboardFront() {
const [stream, setStream] = useState(() =>
streamPool.slice(0, 4).map((text, i) => ({ id: i, text, exiting: false })),
);
const nextRef = useRef(4);
useEffect(() => {
const id = setInterval(() => {
setStream((prev) => {
const marked = prev.map((l, i) => (i === 0 ? { ...l, exiting: true } : l));
const next = [
...marked,
{
id: nextRef.current,
text: streamPool[nextRef.current % streamPool.length],
exiting: false,
},
];
nextRef.current += 1;
return next;
});
setTimeout(() => {
setStream((prev) => prev.filter((l) => !l.exiting));
}, 240);
}, 1300);
return () => clearInterval(id);
}, []);
function LogVisual() {
return (
<div className="p-4 w-full h-full font-mono flex flex-col">
<div className="flex items-center justify-between mb-2 pb-1.5 border-b border-[var(--landing-border-subtle)]">
<span className="text-[0.6875rem] text-[var(--landing-fg)]/80">s-003 · attached</span>
<span className="text-[0.5rem] text-[var(--landing-muted-dim)]">tail -f</span>
</div>
<div className="space-y-[2px] text-[0.625rem] text-[var(--landing-muted)] leading-[1.6] flex-1 overflow-hidden">
{stream.map((l) => (
<div
key={l.id}
className={`truncate transition-opacity duration-200 ${l.exiting ? "opacity-0" : "landing-stream-line"}`}
>
<span className="text-[rgba(134,239,172,0.7)]"></span> <span className="opacity-70">{l.text}</span>
</div>
))}
</div>
<div className="mt-5 space-y-0.5 font-mono text-[11px]">
{[
["0x1f", "sess.spawn", "var(--fg-muted)"],
["0x20", "pr.opened", "var(--fg-muted)"],
["0x21", "ci.fail -> nudge", "var(--status-fail)"],
["0x22", "agent.resume", "var(--fg-muted)"],
["0x23", "pr.merged", "var(--status-ok)"],
].map(([hex, label, color]) => (
<div key={hex} className="flex justify-between">
<span style={{ color }}>{label}</span>
<span className="text-[color:var(--fg-dim)]">{hex}</span>
</div>
))}
</div>
);
}

View File

@ -0,0 +1,93 @@
const LOGO_URL = "/ao-logo.svg";
function GithubIcon({ className = "" }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M12 .5C5.65.5.5 5.65.5 12c0 5.08 3.29 9.38 7.86 10.9.58.1.79-.25.79-.56v-2.15c-3.2.7-3.88-1.37-3.88-1.37-.52-1.34-1.28-1.7-1.28-1.7-1.05-.72.08-.7.08-.7 1.16.08 1.77 1.2 1.77 1.2 1.03 1.76 2.7 1.25 3.36.96.1-.75.4-1.25.73-1.54-2.56-.29-5.26-1.28-5.26-5.7 0-1.26.45-2.29 1.19-3.1-.12-.3-.52-1.47.11-3.05 0 0 .97-.31 3.18 1.18A10.96 10.96 0 0 1 12 5.99c.98 0 1.97.13 2.9.38 2.2-1.49 3.17-1.18 3.17-1.18.63 1.58.23 2.75.11 3.05.74.81 1.19 1.84 1.19 3.1 0 4.43-2.7 5.4-5.27 5.69.41.36.78 1.07.78 2.16v3.2c0 .31.21.67.8.55A11.51 11.51 0 0 0 23.5 12C23.5 5.65 18.35.5 12 .5Z" />
</svg>
);
}
const productLinks = [
{ label: "Features", href: "#features" },
{ label: "How it works", href: "#how" },
{ label: "Architecture", href: "#architecture" },
{ label: "Quickstart", href: "#quickstart" },
];
const resourceLinks = [
{ label: "GitHub", href: "https://github.com/AgentWrapper/agent-orchestrator" },
{ label: "Architecture docs", href: "/docs/architecture" },
{ label: "CLI reference", href: "/docs/cli" },
{ label: "Releases", href: "https://github.com/AgentWrapper/agent-orchestrator/releases" },
];
const communityLinks = [
{ label: "Contributors", href: "https://github.com/AgentWrapper/agent-orchestrator/graphs/contributors" },
{ label: "Issues", href: "https://github.com/AgentWrapper/agent-orchestrator/issues" },
{ label: "Pull requests", href: "https://github.com/AgentWrapper/agent-orchestrator/pulls" },
{ label: "ao-agents.com", href: "https://ao-agents.com" },
];
export function LandingFooter() {
return (
<footer data-testid="footer" className="border-t border-[color:var(--border)] bg-[color:var(--bg-deep)]">
<div className="container-page py-16">
<div className="grid gap-10 md:grid-cols-12">
<div className="md:col-span-5">
<div className="mb-4 inline-flex h-10 items-center gap-2.5">
<img src={LOGO_URL} alt="Agent Orchestrator" className="block h-10 w-10 shrink-0 object-contain" />
<span className="font-display text-lg font-bold leading-none tracking-tight text-[color:var(--fg)]">
Agent Orchestrator
</span>
</div>
<p className="max-w-sm text-[14px] leading-relaxed text-[color:var(--fg-muted)]">
The open-source orchestration layer for parallel AI coding agents. Loopback-only, Apache 2.0 licensed,
runs on your laptop.
</p>
</div>
<FooterCol title="Product" links={productLinks} />
<FooterCol title="Resources" links={resourceLinks} />
<FooterCol title="Community" links={communityLinks} />
</div>
<div className="mt-14 flex flex-col items-start justify-between gap-3 border-t border-[color:var(--border)] pt-6 font-mono text-[10px] uppercase tracking-[0.22em] text-[color:var(--fg-dim)] sm:flex-row sm:items-center">
<div>Built by the open-source community.</div>
<a
href="https://github.com/AgentWrapper/agent-orchestrator"
target="_blank"
rel="noreferrer"
className="inline-flex items-center gap-1.5 transition-colors hover:text-[color:var(--accent)]"
data-testid="footer-github-link"
>
<GithubIcon className="h-3 w-3" />
AgentWrapper/agent-orchestrator
</a>
</div>
</div>
</footer>
);
}
function FooterCol({ title, links }: { title: string; links: Array<{ label: string; href: string }> }) {
return (
<div className="md:col-span-2 lg:col-span-2">
<h4 className="mb-4 font-mono text-[10px] uppercase tracking-[0.22em] text-[color:var(--fg-dim)]">{title}</h4>
<ul className="space-y-2.5">
{links.map((link) => (
<li key={link.label}>
<a
href={link.href}
target={link.href.startsWith("#") || link.href.startsWith("/") ? undefined : "_blank"}
rel={link.href.startsWith("#") || link.href.startsWith("/") ? undefined : "noreferrer"}
className="text-[13.5px] text-[color:var(--fg-muted)] transition-colors hover:text-[color:var(--fg)]"
>
{link.label}
</a>
</li>
))}
</ul>
</div>
);
}

View File

@ -1,101 +1,134 @@
interface LandingHeroProps {
starsLabel: string;
}
export function LandingHero({ starsLabel }: LandingHeroProps) {
function GithubIcon({ className = "" }: { className?: string }) {
return (
<div className="relative min-h-screen overflow-hidden">
<section className="relative z-10 flex flex-col items-center justify-center text-center px-6 pt-32 pb-20 min-h-screen">
<div className="landing-fade-rise landing-card inline-flex items-center gap-2 rounded-lg px-3 py-1.5 text-xs text-[var(--landing-muted)] mb-8">
<span className="w-1.5 h-1.5 rounded-full bg-[rgba(134,239,172,0.7)]" />
Open Source · MIT Licensed · {starsLabel} GitHub Stars
</div>
<h1 className="landing-fade-rise font-sans font-[680] text-[clamp(1.75rem,4vw,2.75rem)] leading-[1] tracking-[-2px] max-w-[56rem]">
Run 30 AI agents in parallel.
<br />
<span className="text-[var(--landing-muted)]">One dashboard.</span>
</h1>
<p className="landing-fade-rise-d1 text-[var(--landing-muted)] text-[0.9375rem] max-w-[38rem] mt-6 leading-[1.7]">
Agent Orchestrator spawns Claude Code, Codex, Cursor, Aider, and OpenCode in isolated git worktrees. Each
agent gets its own branch, creates PRs, fixes CI, and addresses reviews autonomously.
</p>
<div className="landing-fade-rise-d2 flex items-center gap-3 mt-10 flex-wrap justify-center">
<div className="landing-card rounded-lg px-6 py-3 font-mono text-sm">
<span className="text-[var(--landing-muted)] opacity-40">$</span> npx @aoagents/ao start
</div>
<a
href="/docs"
className="landing-card rounded-lg px-6 py-3 text-sm no-underline transition-colors hover:text-white"
>
Read Docs
</a>
<a
href="https://github.com/ComposioHQ/agent-orchestrator"
target="_blank"
rel="noopener noreferrer"
className="liquid-glass-solid rounded-lg px-6 py-3 text-sm no-underline transition-colors"
>
View on GitHub
</a>
</div>
<div className="landing-fade-rise-d2 w-full max-w-[72rem] mt-16">
<div style={{ maxWidth: "62rem", margin: "0 auto" }}>
{/* Laptop screen / lid */}
<div
className="rounded-[14px]"
style={{
padding: 10,
background: "linear-gradient(180deg, #211f1c 0%, #161513 100%)",
border: "1px solid var(--landing-border-default)",
boxShadow: "0 30px 70px -24px rgba(0,0,0,0.65)",
}}
>
<div
className="overflow-hidden"
style={{
borderRadius: 6,
aspectRatio: "16 / 10",
background: "#0c0b0a",
}}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src="/hero-dashboard.png"
alt="Agent Orchestrator dashboard — live agent sessions flowing from work to review to merge"
className="w-full h-full"
style={{ objectFit: "cover", objectPosition: "top", display: "block" }}
/>
</div>
</div>
{/* Laptop base / hinge */}
<div style={{ width: "112%", marginLeft: "-6%" }}>
<div
style={{
height: 16,
background: "linear-gradient(180deg, #2a2823 0%, #131210 100%)",
borderRadius: "0 0 12px 12px",
borderTop: "1px solid var(--landing-border-default)",
position: "relative",
}}
>
<div
style={{
position: "absolute",
top: 0,
left: "50%",
transform: "translateX(-50%)",
width: "15%",
height: 6,
background: "#0c0b0a",
borderRadius: "0 0 8px 8px",
}}
/>
</div>
</div>
</div>
</div>
</section>
</div>
<svg className={className} viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M12 .5C5.65.5.5 5.65.5 12c0 5.08 3.29 9.38 7.86 10.9.58.1.79-.25.79-.56v-2.15c-3.2.7-3.88-1.37-3.88-1.37-.52-1.34-1.28-1.7-1.28-1.7-1.05-.72.08-.7.08-.7 1.16.08 1.77 1.2 1.77 1.2 1.03 1.76 2.7 1.25 3.36.96.1-.75.4-1.25.73-1.54-2.56-.29-5.26-1.28-5.26-5.7 0-1.26.45-2.29 1.19-3.1-.12-.3-.52-1.47.11-3.05 0 0 .97-.31 3.18 1.18A10.96 10.96 0 0 1 12 5.99c.98 0 1.97.13 2.9.38 2.2-1.49 3.17-1.18 3.17-1.18.63 1.58.23 2.75.11 3.05.74.81 1.19 1.84 1.19 3.1 0 4.43-2.7 5.4-5.27 5.69.41.36.78 1.07.78 2.16v3.2c0 .31.21.67.8.55A11.51 11.51 0 0 0 23.5 12C23.5 5.65 18.35.5 12 .5Z" />
</svg>
);
}
function ArrowRightIcon({ className = "" }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
<path d="M5 12h14" />
<path d="m12 5 7 7-7 7" />
</svg>
);
}
function BookIcon({ className = "" }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
<path d="M12 7v14" />
<path d="M3 18a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h5a4 4 0 0 1 4 4 4 4 0 0 1 4-4h5a1 1 0 0 1 1 1v13a1 1 0 0 1-1 1h-6a3 3 0 0 0-3 3 3 3 0 0 0-3-3z" />
</svg>
);
}
export function LandingHero() {
return (
<section
data-testid="hero-section"
id="top"
className="relative overflow-hidden border-b border-[color:var(--border)] pb-20 pt-14 sm:pt-16 lg:pb-24"
>
<div
className="pointer-events-none absolute inset-0 opacity-[0.24]"
style={{
backgroundImage:
"linear-gradient(var(--border) 1px, transparent 1px), linear-gradient(90deg, var(--border) 1px, transparent 1px)",
backgroundSize: "44px 44px",
maskImage: "radial-gradient(ellipse at 52% 42%, black 0%, transparent 68%)",
WebkitMaskImage: "radial-gradient(ellipse at 52% 42%, black 0%, transparent 68%)",
}}
/>
<div className="relative z-10 mx-auto w-full max-w-[1680px] px-5 sm:px-8 lg:px-12 xl:px-16">
<div className="grid items-center gap-10 lg:grid-cols-[0.9fr_1.1fr] lg:gap-10 xl:gap-14">
<div className="max-w-[760px] text-left">
<div className="mb-5 font-mono text-[11px] font-semibold uppercase tracking-[0.22em] text-[color:var(--accent)]">
Agent work, from issue to merge
</div>
<h1
data-testid="hero-headline"
className="font-display font-bold leading-[0.94] tracking-tight text-[color:var(--fg)]"
style={{ fontSize: "clamp(52px, 5.05vw, 96px)" }}
>
<span className="block 2xl:whitespace-nowrap">Review the work,</span>
<span className="block text-[color:var(--accent)] 2xl:whitespace-nowrap">Not the agents.</span>
</h1>
<p
data-testid="hero-subtitle"
className="mt-6 max-w-[720px] text-[18px] font-semibold leading-[1.62] text-[color:var(--fg-muted)] sm:text-[20px]"
>
Every issue gets its own checkout, session, branch, PR, checks, and review thread. When something breaks,
the right context goes back to the right agent.
</p>
<div className="mt-8 flex flex-wrap items-center gap-3">
<a
href="https://github.com/AgentWrapper/agent-orchestrator"
target="_blank"
rel="noreferrer"
data-testid="hero-primary-cta"
className="group inline-flex items-center gap-2 rounded-lg bg-[color:var(--accent)] px-5 py-3 text-[14px] font-semibold text-white shadow-[0_0_0_1px_rgba(255,255,255,0.1)_inset] transition-all hover:brightness-110"
style={{ color: "#fff" }}
>
<GithubIcon className="h-4 w-4" />
Install Agent Orchestrator
<ArrowRightIcon className="h-4 w-4 transition-transform group-hover:translate-x-0.5" />
</a>
<a
href="/docs"
data-testid="hero-secondary-cta"
className="inline-flex items-center gap-2 rounded-lg border border-[color:var(--border-strong)] bg-[color:var(--bg-card)] px-5 py-3 text-[14px] font-semibold text-[color:var(--fg)] transition-colors hover:bg-[color:var(--bg-card-hover)]"
>
<BookIcon className="h-4 w-4" />
Read the docs
</a>
</div>
<div className="mt-8 border-l border-[color:var(--border-strong)] pl-4 font-mono text-[11px] uppercase tracking-[0.16em] text-[color:var(--fg-dim)]">
issue -&gt; worktree -&gt; session -&gt; pull request -&gt; review loop
</div>
</div>
<div className="relative min-w-0 overflow-visible lg:pl-2" data-testid="hero-screenshot">
<div className="relative ml-auto w-full max-w-[1080px] pr-[clamp(70px,8vw,150px)] sm:pr-[clamp(95px,10vw,180px)] lg:pr-[clamp(110px,8vw,190px)]">
<div className="relative rounded-[18px] border border-[color:var(--border)] bg-[color:var(--bg-elevated)] p-1">
<div className="overflow-hidden rounded-[13px] bg-[color:var(--bg-deep)]">
<img
src="/hero-dashboard.png"
alt="Agent Orchestrator dashboard board view"
className="theme-dark-only block w-full"
draggable="false"
/>
<img
src="/hero-dashboard-light.png"
alt="Agent Orchestrator dashboard board view in light theme"
className="theme-light-only hidden w-full"
draggable="false"
/>
</div>
</div>
<div className="absolute bottom-0 right-0 hidden w-[26%] min-w-[170px] translate-y-[12%] sm:block">
<div className="rounded-[24px] border border-[color:var(--border)] bg-[color:var(--bg-elevated)] p-1 shadow-[0_24px_80px_-48px_rgba(0,0,0,0.95)]">
<div className="relative overflow-hidden rounded-[19px] bg-[color:var(--bg-deep)]">
<img
src="/hero-new-task.png"
alt="Agent Orchestrator mobile workflow preview"
className="theme-dark-only block aspect-[9/16] h-auto w-full object-cover object-center"
draggable="false"
/>
<img
src="/hero-dashboard-light.png"
alt="Agent Orchestrator mobile workflow preview in light theme"
className="theme-light-only hidden aspect-[9/16] h-auto w-full object-cover object-center"
draggable="false"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
);
}

View File

@ -1,319 +1,127 @@
"use client";
import { useEffect, useRef, useState } from "react";
const DURATION_MS = 3000;
const steps = [
{
n: "01",
title: "Configure & assign",
titleEm: "assign",
desc: "Point Agent Orchestrator at your repo with a YAML config. Choose your agent, set up trackers and notifiers. One file, full control.",
tags: ["YAML", "Plugins", "Trackers"],
kind: "cli" as const,
label: "register",
title: "Tell ao about your repo",
desc: "Point the daemon at a local git repo. Worker and orchestrator agents are picked per project - no global setting wars.",
code: `ao project add --path . \\
--worker-agent codex \\
--orchestrator-agent claude-code`,
out: `ok project "your-repo" registered
ok data dir -> ~/.ao/`,
},
{
n: "02",
title: "Agents work",
titleEm: "work",
desc: "Each agent spawns in an isolated worktree. They write code, create PRs, run tests, and fix failures. Monitor everything from the live dashboard, or let them run.",
tags: ["Worktrees", "Live dashboard", "Parallel"],
kind: "dashboard" as const,
label: "spawn",
title: "Carve out a worktree, attach a pane",
desc: "Every spawn creates its own git worktree and an attachable runtime session. Multiple sessions, zero collisions.",
code: `ao spawn --project your-repo --prompt \\
"Add SSO via Okta to /auth"`,
out: `ok session sess_8f2 spawned
ok worktree wt-add-sso-okta
ok pane attached · streaming activity`,
},
{
n: "03",
title: "PRs land",
titleEm: "land",
desc: "Agents create pull requests, address review comments, fix CI failures, and get them to mergeable state. Your morning starts with merged PRs, not a backlog.",
tags: ["Pull requests", "CI fixes", "Review"],
kind: "prs" as const,
label: "ship",
title: "Agent pushes the PR. You go for coffee.",
desc: "The agent develops, tests, and opens a PR from inside its worktree. Activity streams back to your terminal or the desktop app.",
code: `git push -u origin add-sso-okta
gh pr create --fill`,
out: `PR #482 opened
checks: queued · 0/4 complete`,
},
{
n: "04",
label: "react",
title: "Feedback routes itself",
desc: "The SCM observer watches the PR. CI failure, requested change, merge conflict - all become nudges to the owning agent.",
code: `[scm/github] PR #482 · lint -> fail
[lcm] derive nudge for sess_8f2
[agent/codex] received nudge · fix in progress`,
out: `ok lint passing · pushed fixup
ok pr.merged -> main`,
accent: true,
},
];
export function LandingHowItWorks() {
const [active, setActive] = useState(0);
const [progress, setProgress] = useState(0);
const [isDesktop, setIsDesktop] = useState(true);
const pausedRef = useRef(false);
const startRef = useRef<number | null>(null);
useEffect(() => {
const mq = window.matchMedia("(min-width: 768px)");
const apply = () => setIsDesktop(mq.matches);
apply();
mq.addEventListener("change", apply);
return () => mq.removeEventListener("change", apply);
}, []);
useEffect(() => {
let raf = 0;
const tick = (now: number) => {
if (startRef.current === null) startRef.current = now;
if (!pausedRef.current) {
const p = Math.min((now - startRef.current) / DURATION_MS, 1);
setProgress(p);
if (p >= 1) {
startRef.current = now;
setActive((a) => (a + 1) % steps.length);
setProgress(0);
}
} else {
startRef.current = now - progress * DURATION_MS;
}
raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [active]);
const select = (i: number) => {
if (i === active) return;
startRef.current = null;
setProgress(0);
setActive(i);
};
return (
<section className="py-[120px] px-6 max-w-[72rem] mx-auto" id="how">
<div className="landing-reveal">
<div className="text-xs tracking-[0.15em] uppercase text-[var(--landing-muted)] opacity-60 mb-6">Process</div>
<h2 className="font-sans font-[680] tracking-tight font-normal text-[clamp(1.375rem,3vw,2rem)] leading-[1.05] tracking-[-1.5px] mb-6">
Three steps to <em className="italic text-[var(--landing-muted)]">orchestration</em>
</h2>
</div>
<div
className="landing-reveal mt-16 flex flex-col md:flex-row"
style={isDesktop ? { minHeight: 540 } : undefined}
onMouseEnter={() => (pausedRef.current = true)}
onMouseLeave={() => (pausedRef.current = false)}
>
{steps.map((step, i) => {
const isActive = i === active;
return (
<div
key={step.n}
role="button"
tabIndex={0}
aria-expanded={isActive}
onClick={() => select(i)}
onKeyDown={(e) => {
if (e.key === "Enter" || e.key === " ") {
e.preventDefault();
select(i);
}
}}
className="relative min-w-0 cursor-pointer overflow-hidden border-l border-[var(--landing-border-subtle)] pl-7 pr-5 py-2 first:border-l-0 first:pl-0 md:first:pl-7"
style={{
flex: isDesktop ? (isActive ? "1 1 0%" : "0 1 15rem") : "0 0 auto",
transition: "flex 0.6s cubic-bezier(0.22,1,0.36,1)",
}}
<section
id="how"
data-testid="how-it-works"
className="relative border-t border-[color:var(--border)] bg-[color:var(--bg-deep)] py-24 sm:py-32"
>
<div className="container-page">
<div className="mb-14 grid items-end gap-8 lg:grid-cols-12">
<div className="lg:col-span-7">
<div className="serial-num mb-3 font-mono text-xs">03 - how it works</div>
<h2
className="font-display font-bold leading-[1.02] tracking-tight text-[color:var(--fg)]"
style={{ fontSize: "clamp(32px, 4.8vw, 60px)" }}
>
{/* Header — always visible */}
<div
className="font-mono text-[2.75rem] leading-none font-[680] tracking-tight mb-5"
style={{
color: "var(--landing-muted)",
opacity: isActive ? 0.85 : 0.32,
transition: "opacity 0.4s ease",
}}
>
{step.n}
</div>
<h3
className="font-sans font-[680] tracking-tight text-[1.375rem] leading-[1.15]"
style={{
color: isActive ? "var(--landing-fg)" : "var(--landing-muted)",
transition: "color 0.4s ease",
maxWidth: isActive ? "100%" : "11rem",
}}
>
{step.title.replace(` ${step.titleEm}`, "")}{" "}
<em className="italic text-[var(--landing-muted)]">{step.titleEm}</em>
</h3>
Four commands.{" "}
<span className="font-editorial font-medium italic text-[color:var(--accent)]">A fleet at work.</span>
</h2>
</div>
<div className="lg:col-span-5">
<p className="text-[15px] leading-relaxed text-[color:var(--fg-muted)]">
No control plane. No SaaS account. No Docker network to debug. One Go binary, your favorite agent CLI, and
the orchestrator runs on loopback.
</p>
</div>
</div>
{/* Expanding body */}
<div
aria-hidden={!isActive}
style={{
opacity: isActive ? 1 : 0,
maxHeight: isActive ? (isDesktop ? 1000 : 900) : 0,
transition: isActive
? "opacity 0.5s ease 0.15s, max-height 0.6s ease"
: "opacity 0.25s ease, max-height 0.4s ease",
overflow: "hidden",
}}
>
<div className="flex gap-5 pt-7" style={{ minWidth: isDesktop ? "30rem" : undefined }}>
{/* Vertical progress bar */}
<div
className="shrink-0 w-[3px] rounded-full overflow-hidden self-stretch"
style={{ background: "var(--landing-border-default)" }}
>
<div
style={{
height: `${(isActive ? progress : 0) * 100}%`,
background: "var(--landing-accent)",
transition: "height 0.08s linear",
}}
/>
</div>
<div className="min-w-0">
<p className="text-[var(--landing-muted)] text-[0.9375rem] leading-[1.7] max-w-[30rem] mb-6">
{step.desc}
</p>
<div className="mb-6">
{step.kind === "cli" && <CliDemo />}
{step.kind === "dashboard" && <DashboardDemo />}
{step.kind === "prs" && <PrsDemo />}
</div>
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 font-mono text-[0.6875rem] tracking-[0.05em] uppercase text-[var(--landing-muted)] opacity-60">
{step.tags.map((t, ti) => (
<span key={t} className="flex items-center gap-3">
{ti > 0 && <span className="opacity-40">·</span>}
{t}
</span>
))}
</div>
</div>
</div>
</div>
</div>
);
})}
<div className="relative space-y-5 lg:space-y-0">
{steps.map((step, i) => (
<Step key={step.n} step={step} index={i} />
))}
</div>
</div>
</section>
);
}
function CliDemo() {
function Step({ step, index }: { step: (typeof steps)[number]; index: number }) {
return (
<div className="bg-black/40 rounded-xl overflow-hidden font-mono text-[0.8125rem]">
<div className="flex items-center gap-2 px-4 py-3 bg-[var(--landing-surface)]">
<div className="w-2.5 h-2.5 rounded-full bg-[rgba(255,240,220,0.12)]" />
<div className="w-2.5 h-2.5 rounded-full bg-[rgba(255,240,220,0.12)]" />
<div className="w-2.5 h-2.5 rounded-full bg-[rgba(255,240,220,0.12)]" />
</div>
<div className="px-5 py-4 leading-[1.8]">
<div>
<span className="text-[var(--landing-muted)]">$</span>{" "}
<span className="text-white">ao batch-spawn 42 43 44 45 46</span>
</div>
<div className="text-[var(--landing-muted)] opacity-60">&nbsp;</div>
<div className="text-[var(--landing-muted)] opacity-60"> Loading config from agent-orchestrator.yaml</div>
<div className="text-[var(--landing-muted)] opacity-60"> Resolving 5 issues from GitHub</div>
<div className="text-[var(--landing-muted)] opacity-60"> Spawning sessions in worktrees...</div>
<div className="text-[rgba(134,239,172,0.8)]"> Session s-001 spawned issue #42</div>
<div className="text-[rgba(134,239,172,0.8)]"> Session s-002 spawned issue #43</div>
<div className="text-[rgba(134,239,172,0.8)]"> Session s-003 spawned issue #44</div>
<div className="text-[rgba(134,239,172,0.8)]"> Session s-004 spawned issue #45</div>
<div className="text-[rgba(134,239,172,0.8)]"> Session s-005 spawned issue #46</div>
<div className="text-[var(--landing-muted)] opacity-60">&nbsp;</div>
<div>
<span className="landing-agent-dot mr-1.5" />
<span className="text-[var(--landing-muted)] opacity-60">
5 agents working · Dashboard http://localhost:3000
<article
data-testid={`step-${step.n}`}
style={{ top: `calc(88px + ${index * 18}px)`, zIndex: 10 + index }}
className={`surface grid overflow-hidden lg:sticky lg:mb-6 lg:grid-cols-12 ${step.accent ? "glow-accent" : ""}`}
>
<div className="border-b border-[color:var(--border)] p-6 sm:p-8 lg:col-span-5 lg:border-b-0 lg:border-r">
<div className="mb-4 flex items-center gap-3">
<span className="font-mono text-[11px] uppercase tracking-[0.25em] text-[color:var(--fg-dim)]">
step {step.n}
</span>
<span className="h-px flex-1 bg-[color:var(--border)]" />
<span
className={`inline-block rounded px-2 py-0.5 font-mono text-[10px] uppercase tracking-[0.22em] ${
step.accent
? "bg-[color:var(--accent-soft)] text-[color:var(--accent)]"
: "border border-[color:var(--border-strong)] bg-[color:var(--bg-deep)] text-[color:var(--fg-muted)]"
}`}
>
{step.label}
</span>
</div>
<h3 className="font-display mb-3 text-[22px] font-bold leading-tight tracking-tight text-[color:var(--fg)] sm:text-[26px]">
{step.title}
</h3>
<p className="text-[14.5px] leading-relaxed text-[color:var(--fg-muted)]">{step.desc}</p>
</div>
</div>
);
}
function DashboardDemo() {
return (
<div className="rounded-2xl overflow-hidden bg-black/30">
<div className="flex items-center gap-2 px-4 py-2.5 bg-[var(--landing-card-bg)] border-b border-[var(--landing-border-subtle)]">
<div className="w-2.5 h-2.5 rounded-full bg-[rgba(255,240,220,0.12)]" />
<div className="w-2.5 h-2.5 rounded-full bg-[rgba(255,240,220,0.12)]" />
<div className="w-2.5 h-2.5 rounded-full bg-[rgba(255,240,220,0.12)]" />
<span className="text-[0.6875rem] text-[var(--landing-muted)] opacity-50 ml-2">my-saas-app · 5 sessions</span>
</div>
<div className="grid grid-cols-4 gap-2 p-3">
<DashColumn
title="Working"
cards={[
{ title: "Add user auth flow", meta: "#42 · feat/auth", agent: "claude-code" },
{ title: "Fix pagination bug", meta: "#43 · fix/pagination", agent: "codex" },
]}
/>
<DashColumn title="Pending" cards={[{ title: "Add rate limiting", meta: "#44 · PR #312", agent: "aider" }]} />
<DashColumn
title="Review"
cards={[{ title: "Update API tests", meta: "#45 · PR #310", agent: "claude-code", amber: true }]}
/>
<DashColumn
title="Merged"
cards={[{ title: "Refactor DB layer", meta: "#46 · PR #308", agent: "opencode", done: true }]}
/>
</div>
</div>
);
}
function PrsDemo() {
return (
<div className="flex flex-col gap-2.5">
{[
{ branch: "feat/user-auth", title: "Add user authentication flow" },
{ branch: "fix/pagination-offset", title: "Fix off-by-one in cursor pagination" },
{ branch: "feat/rate-limiting", title: "Add Redis-backed rate limiter" },
{ branch: "refactor/db-layer", title: "Extract repository pattern from services" },
].map((pr) => (
<div
key={pr.branch}
className="bg-[var(--landing-surface)] border border-[var(--landing-border-subtle)] rounded-xl px-5 py-4 flex items-center justify-between"
>
<div className="flex flex-col gap-1">
<div className="font-mono text-xs text-[var(--landing-fg)]/70">{pr.branch}</div>
<div className="text-[0.8125rem] text-[var(--landing-muted)]">{pr.title}</div>
</div>
<div className="font-mono text-[0.625rem] tracking-[0.05em] px-3 py-1 rounded-full bg-[rgba(134,239,172,0.08)] text-[rgba(134,239,172,0.7)]">
Merged
</div>
<div className="border-l border-[color:var(--border)] bg-[color:var(--code-bg)] p-6 font-mono text-[12.5px] leading-relaxed sm:p-8 lg:col-span-7">
<div className="mb-3 flex items-center gap-2 text-[10px] uppercase tracking-[0.22em] text-[color:var(--code-muted)]">
<span className="h-1.5 w-1.5 rounded-full bg-[color:var(--accent)]" />
<span>~/projects/your-repo</span>
<span className="ml-auto">{step.label}.sh</span>
</div>
))}
</div>
);
}
interface DashCardData {
title: string;
meta: string;
agent: string;
amber?: boolean;
done?: boolean;
}
function DashColumn({ title, cards }: { title: string; cards: DashCardData[] }) {
return (
<div>
<div className="font-mono text-[0.625rem] tracking-[0.1em] uppercase text-[var(--landing-muted)] opacity-40 px-2 mb-1">
{title}
</div>
{cards.map((card) => (
<div
key={card.meta}
className="bg-[var(--landing-surface)] border border-[var(--landing-border-subtle)] rounded-lg p-2.5 mb-1.5 text-[0.6875rem]"
>
<div className="text-[var(--landing-fg)]/70 mb-1">{card.title}</div>
<div className="font-mono text-[0.5625rem] text-[var(--landing-muted)] opacity-50">{card.meta}</div>
<div className="flex items-center gap-1 mt-1 font-mono text-[0.5625rem] text-[var(--landing-muted)] opacity-60">
{card.done ? (
<span></span>
) : (
<span
className="inline-block w-1.5 h-1.5 rounded-full"
style={{ background: card.amber ? "rgba(251,191,36,0.7)" : "rgba(134,239,172,0.7)" }}
/>
)}
{card.agent}
</div>
<pre className="whitespace-pre-wrap break-words text-[color:var(--code-fg)]">{step.code}</pre>
<div className="mt-3 border-t border-[color:var(--border)] pt-3 text-[color:var(--code-muted)]">
<pre className="whitespace-pre-wrap break-words">{step.out}</pre>
</div>
))}
</div>
</div>
</article>
);
}

View File

@ -0,0 +1,202 @@
"use client";
import { useState } from "react";
const tabs = [
{
id: "install",
label: "Install",
lines: [
"# Requires Go 1.25+, tmux on PATH",
"$ cd backend && go build -o /tmp/ao ./cmd/ao",
"",
"# Start the daemon and wait for /readyz",
"$ /tmp/ao start",
"✓ daemon up on 127.0.0.1:3001",
"✓ pid 4821 · ready in 184ms",
"",
"$ /tmp/ao doctor",
"✓ git found · 2.43.0",
"✓ tmux found · 3.5a",
"✓ data dir ~/.ao",
"✓ all checks passing",
],
},
{
id: "spawn",
label: "Spawn agents",
lines: [
"# Register a local repo with worker + orchestrator",
"$ ao project add --path /path/to/repo --id myrepo \\",
" --worker-agent codex \\",
" --orchestrator-agent claude-code",
'✓ project "myrepo" registered',
"",
"# Fan a task out across parallel sessions",
"$ ao spawn --project myrepo \\",
' --prompt "Refactor auth to use JWT"',
"✓ session sess_8f2 · worktree wt-jwt",
"",
"$ ao session ls --project myrepo",
" sess_8f2 codex wt-jwt active",
" sess_a13 claude-code wt-add-sso active",
" sess_c01 cursor wt-webhooks active",
],
},
{
id: "ci",
label: "Auto-nudge on CI",
lines: [
"# the SCM observer is already running. no flags needed.",
"# when GitHub Actions fails:",
"",
'[scm/github] PR #482 · check "lint" → fail',
"[lcm] derive nudge for sess_8f2",
"[agent/codex] received nudge · resuming pane",
"",
"# the agent re-opens the worktree, fixes the lint,",
"# pushes a new commit, and CI re-runs.",
"# you do nothing.",
"",
'[scm/github] PR #482 · check "lint" → pass',
"[scm/github] PR #482 · merged → main",
"✓ ship it.",
],
},
];
function classify(line: string) {
if (!line) return "blank";
if (line.startsWith("✓")) return "ok";
if (line.startsWith("$")) return "cmd";
if (line.startsWith("#")) return "comment";
if (line.startsWith("[")) return "log";
return "out";
}
function CommandLines({ lines }: { lines: string[] }) {
const colorFor = (kind: string) =>
kind === "ok"
? "text-[color:var(--status-ok)]"
: kind === "cmd"
? "text-[color:var(--code-fg)]"
: "text-[color:var(--code-muted)]";
return (
<div className="min-h-[380px] font-mono text-[13px] leading-[1.8]">
{lines.map((line, index) => (
<div key={`${line}-${index}`} className={`${colorFor(classify(line))} whitespace-pre-wrap`}>
{line || "\u00A0"}
</div>
))}
</div>
);
}
async function copyText(text: string) {
if (navigator.clipboard?.writeText) {
try {
await navigator.clipboard.writeText(text);
return true;
} catch {
// Fall through to the textarea path for browsers that block Clipboard API.
}
}
const textarea = document.createElement("textarea");
textarea.value = text;
textarea.setAttribute("readonly", "");
textarea.style.position = "fixed";
textarea.style.left = "-9999px";
textarea.style.top = "0";
document.body.appendChild(textarea);
textarea.select();
try {
return document.execCommand("copy");
} finally {
document.body.removeChild(textarea);
}
}
export function LandingLiveDemo() {
const [active, setActive] = useState("install");
const [copyState, setCopyState] = useState<"idle" | "copied" | "failed">("idle");
const current = tabs.find((tab) => tab.id === active) ?? tabs[0];
const onCopy = async () => {
const copied = await copyText(current.lines.join("\n"));
setCopyState(copied ? "copied" : "failed");
window.setTimeout(() => setCopyState("idle"), 1600);
};
return (
<section
id="quickstart"
data-testid="live-demo-terminal"
className="relative border-t border-[color:var(--border)] bg-[color:var(--bg-deep)] py-24 sm:py-32"
>
<div className="container-page">
<div className="mb-14 grid items-end gap-8 lg:grid-cols-12">
<div className="lg:col-span-7">
<div className="serial-num mb-3 font-mono text-xs">05 - quickstart</div>
<h2
className="font-display font-bold leading-[1.02] tracking-tight text-[color:var(--fg)]"
style={{ fontSize: "clamp(32px, 4.8vw, 60px)" }}
>
From zero to a fleet -{" "}
<span className="font-editorial font-medium italic text-[color:var(--accent)]">in three commands.</span>
</h2>
</div>
<div className="lg:col-span-5">
<p className="text-[15px] leading-relaxed text-[color:var(--fg-muted)]">
Live transcript. Click a tab - the daemon types it back.
</p>
</div>
</div>
<div className="terminal-window">
<div className="terminal-header flex items-center justify-between px-3 py-2">
<div className="flex items-center gap-1.5">
<span className="h-3 w-3 rounded-full bg-[color:var(--dot-red)]" />
<span className="h-3 w-3 rounded-full bg-[color:var(--dot-yellow)]" />
<span className="h-3 w-3 rounded-full bg-[color:var(--dot-green)]" />
</div>
<span className="font-mono text-[10px] uppercase tracking-[0.22em] text-[color:var(--code-muted)]">
ao - {current.label.toLowerCase()}
</span>
<button
type="button"
onClick={onCopy}
data-testid="demo-copy-btn"
className="rounded border border-[color:var(--border-strong)] px-2 py-1 font-mono text-[10px] uppercase tracking-[0.18em] text-[color:var(--fg-muted)] transition hover:border-[color:var(--border-bright)] hover:text-[color:var(--fg)]"
>
{copyState === "copied" ? "Copied" : copyState === "failed" ? "Failed" : "Copy"}
</button>
</div>
<div className="flex items-center gap-1 border-b border-[color:var(--border)] bg-[color:var(--code-chrome)] px-3 pt-3">
{tabs.map((tab) => {
const isActive = tab.id === active;
return (
<button
key={tab.id}
onClick={() => setActive(tab.id)}
className={`-mb-px rounded-t-md border-x border-t px-3 py-1.5 font-mono text-[11px] uppercase tracking-[0.18em] transition-colors ${
isActive
? "border-[color:var(--border-strong)] bg-[color:var(--code-bg)] text-[color:var(--code-fg)]"
: "border-transparent text-[color:var(--code-muted)] hover:text-[color:var(--code-fg)]"
}`}
>
{tab.label}
</button>
);
})}
</div>
<div data-testid="demo-code-block" className="bg-[color:var(--code-bg)] p-5 sm:p-7">
<CommandLines lines={current.lines} />
</div>
</div>
</div>
</section>
);
}

View File

@ -1,104 +1,170 @@
"use client";
import Image from "next/image";
import Link from "next/link";
import { useEffect, useState } from "react";
function XIcon() {
const navItems = [
{ label: "Features", href: "#features" },
{ label: "How it works", href: "#how" },
{ label: "Architecture", href: "#architecture" },
{ label: "Quickstart", href: "#quickstart" },
];
function GithubIcon({ className = "" }: { className?: string }) {
return (
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z" />
<svg className={className} viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M12 .5C5.65.5.5 5.65.5 12c0 5.08 3.29 9.38 7.86 10.9.58.1.79-.25.79-.56v-2.15c-3.2.7-3.88-1.37-3.88-1.37-.52-1.34-1.28-1.7-1.28-1.7-1.05-.72.08-.7.08-.7 1.16.08 1.77 1.2 1.77 1.2 1.03 1.76 2.7 1.25 3.36.96.1-.75.4-1.25.73-1.54-2.56-.29-5.26-1.28-5.26-5.7 0-1.26.45-2.29 1.19-3.1-.12-.3-.52-1.47.11-3.05 0 0 .97-.31 3.18 1.18A10.96 10.96 0 0 1 12 5.99c.98 0 1.97.13 2.9.38 2.2-1.49 3.17-1.18 3.17-1.18.63 1.58.23 2.75.11 3.05.74.81 1.19 1.84 1.19 3.1 0 4.43-2.7 5.4-5.27 5.69.41.36.78 1.07.78 2.16v3.2c0 .31.21.67.8.55A11.51 11.51 0 0 0 23.5 12C23.5 5.65 18.35.5 12 .5Z" />
</svg>
);
}
function DiscordIcon() {
function ArrowUpRightIcon({ className = "" }: { className?: string }) {
return (
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M20.317 4.37a19.791 19.791 0 0 0-4.885-1.515.074.074 0 0 0-.079.037c-.21.375-.444.864-.608 1.25a18.27 18.27 0 0 0-5.487 0 12.64 12.64 0 0 0-.617-1.25.077.077 0 0 0-.079-.037A19.736 19.736 0 0 0 3.677 4.37a.07.07 0 0 0-.032.027C.533 9.046-.32 13.58.099 18.057a.082.082 0 0 0 .031.057 19.9 19.9 0 0 0 5.993 3.03.078.078 0 0 0 .084-.028 14.09 14.09 0 0 0 1.226-1.994.076.076 0 0 0-.041-.106 13.107 13.107 0 0 1-1.872-.892.077.077 0 0 1-.008-.128 10.2 10.2 0 0 0 .372-.292.074.074 0 0 1 .077-.01c3.928 1.793 8.18 1.793 12.062 0a.074.074 0 0 1 .078.01c.12.098.246.198.373.292a.077.077 0 0 1-.006.127 12.299 12.299 0 0 1-1.873.892.077.077 0 0 0-.041.107c.36.698.772 1.362 1.225 1.993a.076.076 0 0 0 .084.028 19.839 19.839 0 0 0 6.002-3.03.077.077 0 0 0 .032-.054c.5-5.177-.838-9.674-3.549-13.66a.061.061 0 0 0-.031-.03zM8.02 15.33c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.956-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.956 2.418-2.157 2.418zm7.975 0c-1.183 0-2.157-1.085-2.157-2.419 0-1.333.955-2.419 2.157-2.419 1.21 0 2.176 1.096 2.157 2.42 0 1.333-.946 2.418-2.157 2.418z" />
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
<path d="M7 7h10v10" />
<path d="M7 17 17 7" />
</svg>
);
}
function GithubIcon() {
function MenuIcon({ className = "" }: { className?: string }) {
return (
<svg width="16" height="16" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M12 0c-6.626 0-12 5.373-12 12 0 5.302 3.438 9.8 8.207 11.387.599.111.793-.261.793-.577v-2.234c-3.338.726-4.033-1.416-4.033-1.416-.546-1.387-1.333-1.756-1.333-1.756-1.089-.745.083-.729.083-.729 1.205.084 1.839 1.237 1.839 1.237 1.07 1.834 2.807 1.304 3.492.997.107-.775.418-1.305.762-1.604-2.665-.305-5.467-1.334-5.467-5.931 0-1.311.469-2.381 1.236-3.221-.124-.303-.535-1.524.117-3.176 0 0 1.008-.322 3.301 1.23.957-.266 1.983-.399 3.003-.404 1.02.005 2.047.138 3.006.404 2.291-1.552 3.297-1.23 3.297-1.23.653 1.653.242 2.874.118 3.176.77.84 1.235 1.911 1.235 3.221 0 4.609-2.807 5.624-5.479 5.921.43.372.823 1.102.823 2.222v3.293c0 .319.192.694.801.576 4.765-1.589 8.199-6.086 8.199-11.386 0-6.627-5.373-12-12-12z" />
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
<path d="M4 6h16" />
<path d="M4 12h16" />
<path d="M4 18h16" />
</svg>
);
}
function XIcon({ className = "" }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
<path d="M18 6 6 18" />
<path d="m6 6 12 12" />
</svg>
);
}
function SunIcon({ className = "" }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
<circle cx="12" cy="12" r="4" />
<path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M6.34 17.66l-1.41 1.41M19.07 4.93l-1.41 1.41" />
</svg>
);
}
function MoonIcon({ className = "" }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
<path d="M20.99 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 20.99 12.79Z" />
</svg>
);
}
export function LandingNav() {
const [open, setOpen] = useState(false);
const [theme, setTheme] = useState("dark");
const [mounted, setMounted] = useState(false);
const isLight = theme === "light";
useEffect(() => {
const current = document.documentElement.dataset.theme;
setTheme(current === "light" ? "light" : "dark");
setMounted(true);
}, []);
useEffect(() => {
if (!mounted) return;
document.documentElement.dataset.theme = theme;
document.documentElement.classList.toggle("dark", theme === "dark");
document.documentElement.style.colorScheme = theme;
window.localStorage.setItem("ao-theme", theme);
}, [mounted, theme]);
return (
<nav className="fixed top-0 left-0 right-0 z-50 flex items-center justify-between px-8 py-6 max-w-[80rem] mx-auto bg-[var(--landing-bg)]/90 backdrop-blur-sm">
<a
href="#"
className="inline-flex items-center gap-2 text-base font-semibold text-white no-underline font-sans font-[680] tracking-tight"
>
<Image
src="/ao-logo.png"
alt=""
aria-hidden
width={28}
height={28}
className="h-7 w-7 rounded-md object-cover"
/>
Agent Orchestrator
</a>
<ul className="hidden md:flex items-center gap-8 list-none">
<li>
<Link
href="/docs"
className="text-sm text-[var(--landing-muted)] no-underline hover:text-white transition-colors"
>
Docs
</Link>
</li>
<li>
<header
data-testid="site-nav"
className="sticky top-0 z-40 border-b border-[color:var(--border)] bg-[color:var(--nav-bg)] backdrop-blur-xl"
>
<div className="container-page flex h-16 items-center justify-between">
<a href="#top" data-testid="nav-logo" className="group inline-flex h-10 items-center gap-2.5">
<img src="/ao-logo.svg" alt="Agent Orchestrator" className="block h-10 w-10 shrink-0 object-contain" />
<span className="font-display text-[15px] font-bold leading-none tracking-tight text-[color:var(--fg)]">
Agent Orchestrator
</span>
</a>
<nav className="hidden items-center gap-7 md:flex">
{navItems.map((item) => (
<a
key={item.label}
href={item.href}
className="text-[13px] font-medium text-[color:var(--fg-muted)] transition-colors hover:text-[color:var(--fg)]"
>
{item.label}
</a>
))}
</nav>
<div className="flex items-center gap-2">
<a
href="#features"
className="text-sm text-[var(--landing-muted)] no-underline hover:text-white transition-colors"
href="https://github.com/AgentWrapper/agent-orchestrator"
target="_blank"
rel="noreferrer"
data-testid="nav-star-btn"
className="hidden items-center gap-1.5 rounded-md border border-[color:var(--border-strong)] px-2.5 py-1.5 text-[12px] font-medium text-[color:var(--fg-muted)] transition-colors hover:border-[color:var(--border-bright)] hover:text-[color:var(--fg)] sm:inline-flex"
>
Features
<GithubIcon className="h-3.5 w-3.5" />
<span className="font-mono">7.7k</span>
</a>
</li>
<li>
<button
type="button"
onClick={() => setTheme(isLight ? "dark" : "light")}
data-testid="theme-toggle"
className="inline-flex h-8 w-8 items-center justify-center rounded-md border border-[color:var(--border-strong)] text-[color:var(--fg-muted)] transition-colors hover:border-[color:var(--border-bright)] hover:text-[color:var(--fg)]"
aria-label={isLight ? "Switch to dark theme" : "Switch to light theme"}
title={isLight ? "Dark theme" : "Light theme"}
>
{isLight ? <MoonIcon className="h-4 w-4" /> : <SunIcon className="h-4 w-4" />}
</button>
<a
href="#how"
className="text-sm text-[var(--landing-muted)] no-underline hover:text-white transition-colors"
href="https://github.com/AgentWrapper/agent-orchestrator"
target="_blank"
rel="noreferrer"
data-testid="nav-cta-btn"
className="inline-flex items-center gap-1.5 rounded-md bg-[color:var(--accent)] px-3.5 py-1.5 text-[13px] font-semibold text-white transition-all hover:brightness-110"
style={{ color: "#fff" }}
>
How It Works
Install
<ArrowUpRightIcon className="h-3.5 w-3.5" />
</a>
</li>
</ul>
<div className="flex items-center gap-2">
<a
href="https://x.com/aoagents"
target="_blank"
rel="noopener noreferrer"
aria-label="X (Twitter)"
className="inline-flex h-9 w-9 items-center justify-center rounded-md text-white/80 transition-colors hover:text-white"
>
<XIcon />
</a>
<a
href="https://discord.gg/UZv7JjxbwG"
target="_blank"
rel="noopener noreferrer"
aria-label="Discord"
className="inline-flex h-9 w-9 items-center justify-center rounded-md text-white/80 transition-colors hover:text-white"
>
<DiscordIcon />
</a>
<a
href="https://github.com/ComposioHQ/agent-orchestrator"
target="_blank"
rel="noopener noreferrer"
aria-label="GitHub"
className="inline-flex h-9 w-9 items-center justify-center rounded-md text-white/80 transition-colors hover:text-white"
>
<GithubIcon />
</a>
<button
onClick={() => setOpen(!open)}
className="rounded-md border border-[color:var(--border-strong)] p-1.5 text-[color:var(--fg)] md:hidden"
data-testid="nav-mobile-toggle"
aria-label="menu"
>
{open ? <XIcon className="h-4 w-4" /> : <MenuIcon className="h-4 w-4" />}
</button>
</div>
</div>
</nav>
{open && (
<div className="border-t border-[color:var(--border)] bg-[color:var(--bg-card)] md:hidden">
<div className="flex flex-col gap-3.5 px-5 py-4">
{navItems.map((item) => (
<a
key={item.label}
href={item.href}
onClick={() => setOpen(false)}
className="text-sm font-medium text-[color:var(--fg-muted)]"
>
{item.label}
</a>
))}
</div>
</div>
)}
</header>
);
}

View File

@ -1,52 +0,0 @@
const steps = [
{
num: "STEP 01",
title: "Install",
desc: "One command. No dependencies beyond Node.js.",
cmd: "npm i -g @aoagents/ao",
},
{
num: "STEP 02",
title: "Configure",
desc: "Create an agent-orchestrator.yaml. Pick your agents, tracker, and notifiers.",
cmd: "ao start",
},
{ num: "STEP 03", title: "Launch", desc: "Assign issues and watch agents spawn.", cmd: "ao batch-spawn 1 2 3" },
];
export function LandingQuickStart() {
return (
<section className="py-[120px] px-6 max-w-[72rem] mx-auto bg-[radial-gradient(ellipse_at_bottom,rgba(255,255,255,0.015)_0%,transparent_60%)]">
<div className="landing-reveal">
<div className="text-xs tracking-[0.15em] uppercase text-[var(--landing-muted)] opacity-60 mb-6">
Get started in 60 seconds
</div>
<h2 className="font-sans font-[680] tracking-tight font-normal text-[clamp(1.375rem,3vw,2rem)] leading-[1.05] tracking-[-1.5px] mb-6">
Three commands to <em className="italic text-[var(--landing-muted)]">launch</em>
</h2>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-5 mt-12">
{steps.map((s) => (
<div key={s.num} className="landing-reveal landing-card rounded-2xl p-7">
<div className="font-mono text-[0.625rem] tracking-[0.1em] text-[var(--landing-muted)] opacity-40 mb-3">
{s.num}
</div>
<h3 className="font-sans font-[680] tracking-tight text-xl mb-2 tracking-tight">{s.title}</h3>
<p className="text-[var(--landing-muted)] text-[0.8125rem] leading-[1.6] mb-4">{s.desc}</p>
<div className="font-mono text-xs text-[var(--landing-fg)]/70 bg-black/30 px-3.5 py-2.5 rounded-lg">
<span className="text-[var(--landing-muted)] opacity-40">$</span> {s.cmd}
</div>
</div>
))}
</div>
<div className="landing-reveal mt-8 text-center">
<a
href="/docs/"
className="landing-card inline-flex rounded-lg px-4 py-2 text-[0.8125rem] text-[var(--landing-muted)] no-underline hover:text-white transition-colors"
>
Explore docs for setup and workflows
</a>
</div>
</section>
);
}

View File

@ -0,0 +1,211 @@
"use client";
import { useEffect, useState } from "react";
declare global {
interface Window {
twttr?: {
widgets?: {
load?: () => void;
};
};
}
}
const posts = [
{
handle: "Teknium",
statusIdParts: ["204231", "894145", "7170790"],
label: "Signal",
author: "Teknium",
note: "Most important outside validation.",
},
{
handle: "facito0",
statusIdParts: ["203638", "079647", "5547760"],
label: "Mood",
author: "FacitoO",
note: "A lightweight social proof hit from daily AO usage.",
},
{
handle: "buchireddy",
statusIdParts: ["206410", "814460", "7760628"],
label: "Builder",
author: "Buchi Reddy B",
note: "Went all-in early on the AO building blocks.",
},
{
handle: "oxwizzdom",
statusIdParts: ["204349", "124837", "6336484"],
label: "Code read",
author: "oxwizzdom",
note: "Weekend codebase teardown and minimal rebuild.",
},
{
handle: "addddiiie",
statusIdParts: ["203717", "443270", "0211408"],
label: "Use case",
author: "Adi",
note: "Parallel dev agents framed in one clean line.",
},
{
handle: "aoagents",
statusIdParts: ["205420", "723754", "8302804"],
label: "Official",
author: "Agent Orchestrator",
note: "A short official signal from the AO account.",
},
];
function postUrl(post: (typeof posts)[number]) {
return `https://twitter.com/${post.handle}/status/${post.statusIdParts.join("")}`;
}
function ArrowUpRightIcon({ className = "" }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
<path d="M7 7h10v10" />
<path d="M7 17 17 7" />
</svg>
);
}
function MessageCircleIcon({ className = "" }: { className?: string }) {
return (
<svg className={className} viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" aria-hidden="true">
<path d="M21 11.5a8.38 8.38 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.38 8.38 0 0 1-3.8-.9L3 21l1.9-5.7a8.38 8.38 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.38 8.38 0 0 1 3.8-.9h.5a8.48 8.48 0 0 1 8 8v.5Z" />
</svg>
);
}
function loadTwitterWidgets() {
if (window.twttr?.widgets) {
window.twttr.widgets.load?.();
return;
}
const existing = document.getElementById("twitter-wjs");
if (existing) {
existing.addEventListener("load", () => window.twttr?.widgets?.load?.(), { once: true });
return;
}
const script = document.createElement("script");
script.id = "twitter-wjs";
script.src = "https://platform.twitter.com/widgets.js";
script.async = true;
script.charset = "utf-8";
script.onload = () => window.twttr?.widgets?.load?.();
document.body.appendChild(script);
}
function usePageTheme() {
const [theme, setTheme] = useState("dark");
useEffect(() => {
setTheme(document.documentElement.dataset.theme || "dark");
const observer = new MutationObserver(() => {
setTheme(document.documentElement.dataset.theme || "dark");
});
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ["data-theme"],
});
return () => observer.disconnect();
}, []);
return theme;
}
export function LandingSocialProof() {
const theme = usePageTheme();
useEffect(() => {
loadTwitterWidgets();
}, [theme]);
return (
<section
id="testimonials"
data-testid="social-proof"
className="relative overflow-hidden border-t border-[color:var(--border)] py-24 sm:py-32"
>
<div className="container-page">
<div className="mx-auto max-w-[1320px]">
<div className="mb-12 grid items-end gap-8 lg:grid-cols-12">
<div className="lg:col-span-7">
<div className="serial-num mb-3 font-mono text-xs">06 - in the wild</div>
<h2
className="font-display font-bold leading-[1.02] tracking-tight text-[color:var(--fg)]"
style={{ fontSize: "clamp(32px, 4.8vw, 60px)" }}
>
People are already{" "}
<span className="font-editorial font-medium italic text-[color:var(--accent)]">
building around it.
</span>
</h2>
</div>
<div className="lg:col-span-5">
<p className="text-[15px] leading-relaxed text-[color:var(--fg-muted)]">
Real posts from builders, researchers, and early users, embedded directly from X.
</p>
</div>
</div>
<div className="tweet-masonry">
{posts.map((post, index) => (
<TweetCard key={`${theme}-${post.handle}-${index}`} post={post} index={index} theme={theme} />
))}
</div>
</div>
</div>
</section>
);
}
function TweetCard({ post, index, theme }: { post: (typeof posts)[number]; index: number; theme: string }) {
const url = postUrl(post);
return (
<article
data-testid={`tweet-card-${index}`}
className="surface mb-5 inline-block w-full break-inside-avoid overflow-hidden transition duration-300 hover:-translate-y-0.5 hover:border-[color:var(--accent-soft)]"
>
<div className="flex items-center justify-between gap-3 border-b border-[color:var(--border)] bg-[color:var(--bg-chrome)] px-4 py-3">
<div className="flex min-w-0 items-center gap-2">
<MessageCircleIcon className="h-4 w-4 shrink-0 text-[color:var(--accent)]" />
<div className="min-w-0">
<div className="font-mono text-[10px] uppercase tracking-[0.2em] text-[color:var(--fg-dim)]">
{post.label}
</div>
<div className="truncate text-[13px] font-semibold text-[color:var(--fg)]">{post.author}</div>
</div>
</div>
<a
href={url}
target="_blank"
rel="noreferrer"
aria-label={`Open ${post.author} post`}
className="inline-flex h-8 w-8 items-center justify-center rounded-md border border-[color:var(--border-strong)] text-[color:var(--fg-muted)] transition-colors hover:text-[color:var(--accent)]"
>
<ArrowUpRightIcon className="h-4 w-4" />
</a>
</div>
<div className="px-3 pb-4 pt-3">
<p className="mb-3 px-1 text-[13px] leading-relaxed text-[color:var(--fg-muted)]">{post.note}</p>
<div className="tweet-shell [&_.twitter-tweet]:mx-auto [&_.twitter-tweet]:max-w-full">
<blockquote
className="twitter-tweet"
data-theme={theme === "light" ? "light" : "dark"}
data-dnt="true"
data-conversation="none"
data-width="420"
>
<a href={url}>View post on X</a>
</blockquote>
</div>
</div>
</article>
);
}

View File

@ -1,46 +0,0 @@
import type { GitHubRepoStats } from "@/lib/github-repo";
interface LandingStatsProps {
stats: GitHubRepoStats;
}
export function LandingStats({ stats }: LandingStatsProps) {
const cards = [
{ number: stats.stars.toLocaleString(), label: "GitHub Stars" },
{ number: stats.forks.toLocaleString(), label: "Forks" },
{ number: stats.openIssues.toLocaleString(), label: "Open Issues" },
{ number: stats.watchers.toLocaleString(), label: "Watchers" },
];
return (
<section className="py-20 px-6 max-w-[72rem] mx-auto">
<div className="landing-reveal grid grid-cols-2 md:grid-cols-4 gap-5">
{cards.map((stat) => (
<div key={stat.label} className="landing-card rounded-2xl py-8 px-6 text-center">
<div className="font-sans font-[680] tracking-tight text-[clamp(2rem,4vw,3rem)] tracking-tight mb-1">
{stat.number}
</div>
<div className="text-xs text-[var(--landing-muted)] opacity-60">{stat.label}</div>
</div>
))}
</div>
<div className="landing-reveal text-center mt-8">
<a
href="https://github.com/ComposioHQ/agent-orchestrator"
target="_blank"
rel="noopener noreferrer"
className="landing-card inline-flex items-center gap-2 rounded-lg px-4 py-2 text-[0.8125rem] text-[var(--landing-muted)] no-underline transition-all hover:text-white mb-3"
>
<span className="text-[rgba(251,191,36,0.7)] text-sm"></span>
<span className="font-mono text-xs text-[var(--landing-fg)] opacity-80">{stats.stars.toLocaleString()}</span>
<span>stars on GitHub</span>
</a>
<br />
<div className="landing-card inline-flex items-center gap-2 rounded-lg px-4 py-2 text-[0.8125rem] text-[var(--landing-muted)]">
<span className="w-2 h-2 rounded-full bg-[rgba(134,239,172,0.7)] animate-pulse" />
Built with itself this repo is managed by Agent Orchestrator
</div>
</div>
</section>
);
}

View File

@ -1,157 +0,0 @@
"use client";
import { useEffect, useState } from "react";
const testimonials = [
{
quote: "Set up 12 agents on our backlog before lunch. By end of day, 8 PRs were merged.",
img: "https://i.pravatar.cc/120?img=13",
name: "Staff Engineer",
role: "Series B Startup",
},
{
quote:
"The auto CI recovery alone saves me hours a week. Agents fix their own broken tests. I just review and merge.",
img: "https://i.pravatar.cc/120?img=32",
name: "Solo Founder",
role: "Indie SaaS",
},
{
quote:
"We went from 3 PRs/day to 15 PRs/day. The plugin system means we swapped in GitLab and Linear without changing our workflow.",
img: "https://i.pravatar.cc/120?img=8",
name: "Eng Lead",
role: "20-person team",
},
];
const ROTATE_MS = 5500;
export function LandingTestimonials() {
const [active, setActive] = useState(0);
const [show, setShow] = useState(true);
const [paused, setPaused] = useState(false);
const change = (next: number) => {
if (next === active) return;
setShow(false);
window.setTimeout(() => {
setActive(next);
setShow(true);
}, 240);
};
useEffect(() => {
if (paused) return;
const t = window.setTimeout(() => change((active + 1) % testimonials.length), ROTATE_MS);
return () => window.clearTimeout(t);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [active, paused]);
const t = testimonials[active];
return (
<section className="py-20 px-6 pb-[120px] max-w-[72rem] mx-auto">
<div className="landing-reveal">
<div className="text-xs tracking-[0.15em] uppercase text-[var(--landing-muted)] opacity-60 mb-6">
What engineers say
</div>
<h2 className="font-sans font-[680] tracking-tight font-normal text-[clamp(1.375rem,3vw,2rem)] leading-[1.05] tracking-[-1.5px]">
Trusted by <em className="italic text-[var(--landing-muted)]">builders</em>
</h2>
</div>
<div className="landing-reveal mt-16" onMouseEnter={() => setPaused(true)} onMouseLeave={() => setPaused(false)}>
{/* Quote — fades on change */}
<div className="min-h-[8rem] max-w-[58rem]">
<blockquote
style={{
opacity: show ? 1 : 0,
transform: show ? "translateY(0)" : "translateY(8px)",
transition: "opacity 0.4s ease, transform 0.4s cubic-bezier(0.22,1,0.36,1)",
}}
className="font-sans font-[680] tracking-tight text-[clamp(1.5rem,3.2vw,2.375rem)] leading-[1.3] tracking-[-0.75px] text-[var(--landing-fg)]"
>
&ldquo;{t.quote}&rdquo;
</blockquote>
</div>
{/* Bottom row — author cluster on the left, step counter on the right */}
<div className="flex items-center justify-between gap-6 mt-14 flex-wrap">
<div className="flex items-center gap-5">
<div className="flex items-center">
{testimonials.map((item, i) => {
const isActive = i === active;
const size = isActive ? 56 : 44;
return (
<button
key={item.name}
onClick={() => change(i)}
aria-label={`Show testimonial from ${item.name}`}
aria-pressed={isActive}
className="rounded-full overflow-hidden cursor-pointer shrink-0 p-0"
style={{
width: size,
height: size,
marginLeft: i === 0 ? 0 : -14,
zIndex: isActive ? 30 : 10 - i,
border: `2px solid ${isActive ? "var(--landing-accent)" : "var(--landing-card-bg)"}`,
opacity: isActive ? 1 : 0.7,
boxShadow: isActive ? "0 4px 16px rgba(0,0,0,0.35)" : "none",
transition:
"width 0.4s cubic-bezier(0.22,1,0.36,1), height 0.4s cubic-bezier(0.22,1,0.36,1), opacity 0.4s ease, border-color 0.4s ease",
}}
>
{/* eslint-disable-next-line @next/next/no-img-element */}
<img
src={item.img}
alt={item.name}
width={size}
height={size}
className="w-full h-full object-cover"
style={{
filter: isActive ? "grayscale(0)" : "grayscale(1)",
transition: "filter 0.4s ease",
}}
/>
</button>
);
})}
</div>
{/* Vertical divider */}
<div className="w-px h-10 shrink-0" style={{ background: "var(--landing-border-default)" }} />
{/* Author — fades on change */}
<div
style={{
opacity: show ? 1 : 0,
transition: "opacity 0.4s ease 0.05s",
}}
>
<div className="text-[0.9375rem] font-medium text-[var(--landing-fg)]">{t.name}</div>
<div className="text-[0.8125rem] text-[var(--landing-muted)] opacity-60">{t.role}</div>
</div>
</div>
{/* Step counter — fills the right side */}
<div className="flex items-baseline gap-2 font-mono">
<span
className="text-[clamp(2.25rem,5vw,3.5rem)] font-[680] tracking-tight leading-none tabular-nums"
style={{
color: "var(--landing-fg)",
opacity: show ? 1 : 0.4,
transition: "opacity 0.3s ease",
}}
>
{String(active + 1).padStart(2, "0")}
</span>
<span className="text-[var(--landing-muted)] opacity-40 text-lg">
/ {String(testimonials.length).padStart(2, "0")}
</span>
</div>
</div>
</div>
</section>
);
}

View File

@ -1,226 +0,0 @@
"use client";
import { useEffect, useRef, type PointerEvent as ReactPointerEvent } from "react";
const dim = "text-[var(--landing-muted-dim)]";
const fg = "text-[var(--landing-fg)]/80";
const ok = "text-[rgba(134,239,172,0.8)]";
type UseCase = {
eyebrow: string;
title: string;
desc: string;
prefix: "$" | "⟡";
cmd: string;
outcome: string;
};
// Real, grounded use cases — real ao commands, reaction keys, and lifecycle states.
const cases: UseCase[] = [
{
eyebrow: "Backlog",
title: "Clear it overnight",
desc: "One agent per issue, each in its own git worktree, all running at once.",
prefix: "$",
cmd: "ao batch-spawn 142 143 144 145",
outcome: "4 worktrees · 4 PRs",
},
{
eyebrow: "CI recovery",
title: "Self-healing builds",
desc: "A check goes red; the agent reads the logs, pushes a fix, and waits for green.",
prefix: "⟡",
cmd: "reaction · ci-failed",
outcome: "ci_failed → mergeable",
},
{
eyebrow: "Review loop",
title: "Answers its own reviews",
desc: "Comments land; the agent addresses each one and re-requests review.",
prefix: "⟡",
cmd: "reaction · changes-requested",
outcome: "changes_requested → approved",
},
{
eyebrow: "Migration",
title: "Grinds through the long ones",
desc: "Hand one agent a sweeping change and let it work file by file until tests pass.",
prefix: "$",
cmd: "ao spawn 305 --agent claude-code",
outcome: "23 files · tests green",
},
{
eyebrow: "Per-role",
title: "Right model per job",
desc: "Claude Code orchestrates, Codex does the work. Pick the tool per task.",
prefix: "$",
cmd: "ao spawn 88 --agent codex",
outcome: "codex #88 · claude-code #91",
},
{
eyebrow: "Multi-project",
title: "Every repo, one screen",
desc: "Register all your repos and supervise their agents from a single dashboard.",
prefix: "$",
cmd: "ao start",
outcome: "3 projects · one dashboard",
},
];
const N = cases.length;
const THETA = 360 / N;
const RADIUS = 440;
const CARD_W = 360;
const CARD_H = 440;
export function LandingUseCases() {
const viewportRef = useRef<HTMLDivElement>(null);
const ringRef = useRef<HTMLDivElement>(null);
const cardRefs = useRef<(HTMLDivElement | null)[]>([]);
const angle = useRef(0);
const dragging = useRef(false);
const paused = useRef(false);
const reduced = useRef(false);
const start = useRef({ x: 0, a: 0 });
// rAF loop — rotate the ring and fade/scale each card by how far it faces the
// camera. Imperative (no setState) so 60fps stays smooth and re-render-free.
useEffect(() => {
reduced.current = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
let raf = 0;
const loop = () => {
if (!dragging.current && !paused.current && !reduced.current) {
angle.current += 0.12;
}
const a = angle.current;
if (ringRef.current) {
ringRef.current.style.transform = `translateZ(-${RADIUS}px) rotateY(${a}deg)`;
}
cardRefs.current.forEach((el, i) => {
if (!el) return;
const facing = Math.cos(((i * THETA + a) * Math.PI) / 180);
const vis = Math.max(facing, 0);
el.style.opacity = `${0.2 + 0.8 * vis}`;
el.style.transform = `rotateY(${i * THETA}deg) translateZ(${RADIUS}px) scale(${0.9 + 0.1 * vis})`;
});
raf = requestAnimationFrame(loop);
};
raf = requestAnimationFrame(loop);
return () => cancelAnimationFrame(raf);
}, []);
const onPointerDown = (e: ReactPointerEvent<HTMLDivElement>) => {
dragging.current = true;
start.current = { x: e.clientX, a: angle.current };
e.currentTarget.setPointerCapture(e.pointerId);
if (viewportRef.current) viewportRef.current.style.cursor = "grabbing";
};
const onPointerMove = (e: ReactPointerEvent<HTMLDivElement>) => {
if (!dragging.current) return;
angle.current = start.current.a + (e.clientX - start.current.x) * 0.4;
};
const onPointerUp = () => {
dragging.current = false;
if (viewportRef.current) viewportRef.current.style.cursor = "grab";
};
return (
<section className="py-[100px] px-6 max-w-[72rem] mx-auto">
<div className="landing-reveal text-center">
<div className="text-xs tracking-[0.15em] uppercase text-[var(--landing-muted-dim)] mb-6 font-mono">
Use cases
</div>
<h2 className="font-sans font-[680] text-[clamp(1.375rem,3vw,2rem)] leading-[1.1] tracking-[-1.5px] mb-4">
One orchestrator, many jobs
</h2>
<p className="text-[var(--landing-muted)] text-[0.9375rem] leading-[1.6] max-w-[34rem] mx-auto mb-12">
Point AO at the work and walk away drag to explore what a single run can do.
</p>
</div>
<div
ref={viewportRef}
onMouseEnter={() => (paused.current = true)}
onMouseLeave={() => {
paused.current = false;
onPointerUp();
}}
onPointerDown={onPointerDown}
onPointerMove={onPointerMove}
onPointerUp={onPointerUp}
className="landing-reveal relative mx-auto select-none"
style={{
perspective: "1900px",
height: `${CARD_H + 80}px`,
maxWidth: "1120px",
cursor: "grab",
touchAction: "pan-y",
WebkitMaskImage: "linear-gradient(to right, transparent, #000 16%, #000 84%, transparent)",
maskImage: "linear-gradient(to right, transparent, #000 16%, #000 84%, transparent)",
}}
>
<div
ref={ringRef}
style={{
position: "absolute",
inset: 0,
transformStyle: "preserve-3d",
}}
>
{cases.map((c, i) => (
<div
key={c.eyebrow}
ref={(el) => {
cardRefs.current[i] = el;
}}
style={{
position: "absolute",
left: "50%",
top: "50%",
width: `${CARD_W}px`,
height: `${CARD_H}px`,
marginLeft: `-${CARD_W / 2}px`,
marginTop: `-${CARD_H / 2}px`,
backfaceVisibility: "hidden",
}}
>
<div
className="landing-card rounded-2xl"
style={{
width: "100%",
height: "100%",
padding: "1.875rem",
display: "flex",
flexDirection: "column",
}}
>
<div className="font-mono text-[0.6875rem] tracking-[0.12em] uppercase text-[var(--landing-accent)] opacity-80">
{c.eyebrow}
</div>
<h3 className="font-sans font-[680] text-[1.3125rem] tracking-tight" style={{ marginTop: "1rem" }}>
{c.title}
</h3>
<p
className="text-[var(--landing-muted)] text-[0.9375rem] leading-[1.65]"
style={{ marginTop: "0.625rem" }}
>
{c.desc}
</p>
<div
className="font-mono text-[0.6875rem] leading-[2] bg-black/30 rounded-lg overflow-hidden"
style={{ marginTop: "auto", padding: "0.75rem 0.875rem" }}
>
<div className="whitespace-nowrap overflow-hidden text-ellipsis">
<span className={dim}>{c.prefix}</span> <span className={fg}>{c.cmd}</span>
</div>
<div className={`whitespace-nowrap overflow-hidden text-ellipsis ${ok}`}> {c.outcome}</div>
</div>
</div>
</div>
))}
</div>
</div>
</section>
);
}

View File

@ -1,19 +1,37 @@
export function LandingVideo() {
return (
<section className="landing-reveal px-6 pb-[120px] pt-10 max-w-[72rem] mx-auto">
<div className="text-center mb-5">
<span className="text-[0.6875rem] tracking-[0.12em] uppercase text-[var(--landing-muted)] opacity-50">
See it in action
</span>
</div>
<div className="landing-card rounded-2xl overflow-hidden aspect-video">
<iframe
src="https://www.youtube.com/embed/QdwaeEXOmDs?autoplay=0&rel=0&modestbranding=1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture"
allowFullScreen
className="w-full h-full border-none"
title="Agent Orchestrator Launch Demo"
/>
<section
id="see-it"
data-testid="video-section"
className="relative border-t border-[color:var(--border)] py-24 sm:py-32"
>
<div className="container-page">
<div className="mb-10 text-center">
<div className="serial-num mb-3 font-mono text-xs opacity-70">see it in action</div>
<h2
className="inline-block font-display font-bold leading-[1.02] tracking-tight text-[color:var(--fg)]"
style={{ fontSize: "clamp(28px, 3.6vw, 44px)" }}
>
Watch the founder walk through it -{" "}
<span className="font-editorial font-medium italic text-[color:var(--accent)]">100 PRs in 6 days.</span>
</h2>
</div>
<div className="relative mx-auto w-full max-w-[1180px]">
<div className="pointer-events-none absolute -inset-3 rounded-3xl bg-[color:var(--accent)] opacity-[0.045] blur-2xl" />
<div
data-testid="video-frame"
className="glow-accent relative aspect-video overflow-hidden rounded-2xl border border-[color:var(--border-strong)] bg-black"
>
<iframe
src="https://www.youtube-nocookie.com/embed/QdwaeEXOmDs?autoplay=0&rel=0&modestbranding=1&playsinline=1"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowFullScreen
className="absolute inset-0 h-full w-full border-none"
title="Agent Orchestrator Launch Demo"
/>
</div>
</div>
</div>
</section>
);

View File

@ -1,359 +0,0 @@
"use client";
import { useEffect, useRef, useState } from "react";
// The session lifecycle as a scrubbing ruler-dial. One agent's journey from a
// fresh issue to a merged PR — the real canonical states. The strip slides so
// the active milestone sits under the fixed center line; auto-loops, draggable,
// arrow-key navigable.
type Milestone = {
key: string;
label: string;
desc: string;
icon: "spawn" | "work" | "pr" | "review" | "mergeable" | "merged";
};
const milestones: Milestone[] = [
{
key: "spawning",
label: "Spawn",
icon: "spawn",
desc: "Each issue spawns an agent in its own git worktree — isolated branch, isolated context.",
},
{
key: "working",
label: "Work",
icon: "work",
desc: "The agent writes code, runs the test suite, and commits. Watch it live or let it run.",
},
{
key: "pr_open",
label: "Open PR",
icon: "pr",
desc: "Work is pushed and a pull request opens against main with a summary of the changes.",
},
{
key: "review",
label: "CI & review",
icon: "review",
desc: "CI fails? It reads the logs and pushes a fix. Review comments land? It addresses them.",
},
{
key: "mergeable",
label: "Mergeable",
icon: "mergeable",
desc: "Green checks, approvals in. The PR settles into a clean, mergeable state.",
},
{
key: "merged",
label: "Merged",
icon: "merged",
desc: "It lands on main, the worktree is archived, and the session is marked done.",
},
];
// Ruler geometry (viewBox units)
const W = 760;
const H = 150;
const CENTER_X = W / 2;
const TICKS_PER_STEP = 16;
const TICK_GAP = 11;
const PEAK_Y = 34;
const CURV = 0.00026;
const MINOR_LEN = 13;
const MAJOR_LEN = 26;
const MAX_K = (milestones.length - 1) * TICKS_PER_STEP;
const STEP_MS = 2800;
const arcTop = (dx: number) => PEAK_Y + CURV * dx * dx;
export function LandingWorkflow() {
const [active, setActive] = useState(0);
const [pos, setPos] = useState(0); // current center position in tick units
const [show, setShow] = useState(true);
const [paused, setPaused] = useState(false);
const [inView, setInView] = useState(false);
const wrapRef = useRef<HTMLDivElement>(null);
const svgWrapRef = useRef<HTMLDivElement>(null);
const posRef = useRef(0);
const rafRef = useRef(0);
const drag = useRef<{ active: boolean; startX: number; startPos: number; moved: boolean }>({
active: false,
startX: 0,
startPos: 0,
moved: false,
});
useEffect(() => {
const el = wrapRef.current;
if (!el) return;
const ob = new IntersectionObserver(([entry]) => entry.isIntersecting && setInView(true), { threshold: 0.25 });
ob.observe(el);
return () => ob.disconnect();
}, []);
const tweenTo = (target: number) => {
cancelAnimationFrame(rafRef.current);
const start = posRef.current;
const dist = target - start;
if (Math.abs(dist) < 0.001) return;
const dur = 600;
let t0: number | null = null;
const step = (now: number) => {
if (t0 === null) t0 = now;
const p = Math.min((now - t0) / dur, 1);
const e = 1 - Math.pow(1 - p, 3);
const v = start + dist * e;
posRef.current = v;
setPos(v);
if (p < 1) rafRef.current = requestAnimationFrame(step);
};
rafRef.current = requestAnimationFrame(step);
};
// Slide to the active milestone whenever it changes
useEffect(() => {
tweenTo(active * TICKS_PER_STEP);
setShow(false);
const id = window.setTimeout(() => setShow(true), 200);
return () => window.clearTimeout(id);
}, [active]);
// Auto-loop
useEffect(() => {
if (!inView || paused) return;
const t = window.setTimeout(() => setActive((a) => (a + 1) % milestones.length), STEP_MS);
return () => window.clearTimeout(t);
}, [active, paused, inView]);
const settle = (rawPos: number) => {
const nearest = Math.max(0, Math.min(milestones.length - 1, Math.round(rawPos / TICKS_PER_STEP)));
if (nearest === active) tweenTo(nearest * TICKS_PER_STEP);
setActive(nearest);
};
// Drag to scrub
const onPointerDown = (e: React.PointerEvent) => {
drag.current = { active: true, startX: e.clientX, startPos: posRef.current, moved: false };
(e.target as HTMLElement).setPointerCapture?.(e.pointerId);
setPaused(true);
cancelAnimationFrame(rafRef.current);
};
const onPointerMove = (e: React.PointerEvent) => {
if (!drag.current.active) return;
const widthPx = svgWrapRef.current?.clientWidth ?? W;
const scale = W / widthPx;
const dxTicks = ((e.clientX - drag.current.startX) * scale) / TICK_GAP;
if (Math.abs(dxTicks) > 0.4) drag.current.moved = true;
const v = Math.max(-2, Math.min(MAX_K + 2, drag.current.startPos - dxTicks));
posRef.current = v;
setPos(v);
};
const onPointerUp = () => {
if (!drag.current.active) return;
drag.current.active = false;
settle(posRef.current);
setPaused(false);
};
const onKeyDown = (e: React.KeyboardEvent) => {
if (e.key === "ArrowRight") {
e.preventDefault();
setActive((a) => Math.min(milestones.length - 1, a + 1));
} else if (e.key === "ArrowLeft") {
e.preventDefault();
setActive((a) => Math.max(0, a - 1));
}
};
// Build the visible ticks
const ticks: { k: number; x: number; yTop: number; len: number; major: boolean; opacity: number }[] = [];
const lo = Math.floor(pos) - 40;
const hi = Math.ceil(pos) + 40;
for (let k = lo; k <= hi; k++) {
if (k < 0 || k > MAX_K) continue;
const x = CENTER_X + (k - pos) * TICK_GAP;
if (x < 24 || x > W - 24) continue;
const dx = x - CENTER_X;
const major = k % TICKS_PER_STEP === 0;
const edgeFade = Math.max(0.08, 1 - Math.abs(dx) / (W / 2 + 40));
ticks.push({ k, x, yTop: arcTop(dx), len: major ? MAJOR_LEN : MINOR_LEN, major, opacity: edgeFade });
}
const cur = milestones[active];
return (
<section ref={wrapRef} className="py-[100px] px-6 max-w-[72rem] mx-auto">
<div className="landing-reveal">
<div className="text-xs tracking-[0.15em] uppercase text-[var(--landing-muted-dim)] mb-6 font-mono">
Lifecycle
</div>
<h2 className="font-sans font-[680] text-[clamp(1.375rem,3vw,2rem)] leading-[1.1] tracking-[-1.5px] mb-4">
From issue to merged PR
</h2>
<p className="text-[var(--landing-muted)] text-[0.9375rem] leading-[1.6] max-w-[34rem] mb-12">
Every session walks the same path spawned in isolation, working in parallel, landing on{" "}
<span className="font-mono text-[var(--landing-fg)]">main</span> on its own.
</p>
</div>
<div
className="landing-card rounded-2xl border-[var(--landing-border-subtle)] px-6 sm:px-10 pt-10 pb-12 select-none outline-none"
tabIndex={0}
role="group"
aria-label="Session lifecycle timeline"
onKeyDown={onKeyDown}
onMouseEnter={() => setPaused(true)}
onMouseLeave={() => setPaused(false)}
>
{/* Active icon + label */}
<div
className="flex flex-col items-center text-center mb-2"
style={{ opacity: show ? 1 : 0, transition: "opacity 0.35s ease" }}
>
<LifecycleIcon kind={cur.icon} />
<div className="font-sans font-[680] tracking-tight text-[1.5rem] tracking-[-0.5px] mt-3">{cur.label}</div>
<div className="font-mono text-[0.6875rem] tracking-[0.08em] text-[var(--landing-accent)] opacity-80 mt-1">
{cur.key}
</div>
</div>
{/* Ruler */}
<div
ref={svgWrapRef}
className="cursor-grab active:cursor-grabbing touch-none"
onPointerDown={onPointerDown}
onPointerMove={onPointerMove}
onPointerUp={onPointerUp}
onPointerCancel={onPointerUp}
>
<svg viewBox={`0 0 ${W} ${H}`} className="w-full h-auto" role="img" aria-label="Lifecycle scrubber">
{/* center indicator */}
<line
x1={CENTER_X}
y1={4}
x2={CENTER_X}
y2={H - 8}
stroke="var(--landing-accent)"
strokeWidth={2}
strokeLinecap="round"
/>
<circle cx={CENTER_X} cy={arcTop(0)} r={4} fill="var(--landing-accent)" />
{/* ticks */}
{ticks.map((t) => (
<line
key={t.k}
x1={t.x}
y1={t.yTop}
x2={t.x}
y2={t.yTop + t.len}
stroke={t.major ? "var(--landing-border-strong)" : "var(--landing-border-default)"}
strokeWidth={t.major ? 1.6 : 1}
strokeLinecap="round"
style={{ opacity: t.opacity }}
/>
))}
{/* milestone labels under their major tick (except the centered one) */}
{ticks
.filter((t) => t.major)
.map((t) => {
const idx = t.k / TICKS_PER_STEP;
const isActive = idx === active;
return (
<text
key={`lbl-${t.k}`}
x={t.x}
y={t.yTop + MAJOR_LEN + 16}
textAnchor="middle"
className="font-mono"
fontSize={9}
fill="var(--landing-muted)"
style={{ opacity: isActive ? 0 : t.opacity * 0.7, transition: "opacity 0.3s ease" }}
>
{milestones[idx].label}
</text>
);
})}
</svg>
</div>
{/* Active description */}
<div
className="text-center max-w-[34rem] mx-auto mt-2"
style={{ opacity: show ? 1 : 0, transition: "opacity 0.35s ease" }}
>
<p className="text-[var(--landing-muted)] text-[0.9375rem] leading-[1.6]">{cur.desc}</p>
</div>
</div>
</section>
);
}
function LifecycleIcon({ kind }: { kind: Milestone["icon"] }) {
const common = {
width: 30,
height: 30,
viewBox: "0 0 24 24",
fill: "none",
stroke: "var(--landing-accent)",
strokeWidth: 1.6,
strokeLinecap: "round" as const,
strokeLinejoin: "round" as const,
};
switch (kind) {
case "spawn":
return (
<svg {...common}>
<path d="M12 20v-7" />
<path d="M12 13c0-3 2-5 5-5 0 3-2 5-5 5Z" />
<path d="M12 14c0-2.5-1.8-4.2-4.2-4.2C7.8 12.3 9.6 14 12 14Z" />
</svg>
);
case "work":
return (
<svg {...common}>
<rect x="3" y="4" width="18" height="16" rx="2" />
<path d="M7 9l3 3-3 3" />
<path d="M13 15h4" />
</svg>
);
case "pr":
return (
<svg {...common}>
<circle cx="6" cy="18" r="2.5" />
<circle cx="6" cy="6" r="2.5" />
<circle cx="18" cy="8" r="2.5" />
<path d="M6 8.5v7" />
<path d="M18 10.5c0 4-6 2.5-6 6" />
<path d="M18 5.5V3.5M16.5 5h3" />
</svg>
);
case "review":
return (
<svg {...common}>
<path d="M2 12s3.5-6 10-6 10 6 10 6-3.5 6-10 6-10-6-10-6Z" />
<circle cx="12" cy="12" r="2.5" />
</svg>
);
case "mergeable":
return (
<svg {...common}>
<circle cx="12" cy="12" r="9" />
<path d="M8 12l2.5 2.5L16 9" />
</svg>
);
case "merged":
return (
<svg {...common}>
<circle cx="6" cy="6" r="2.5" />
<circle cx="6" cy="18" r="2.5" />
<circle cx="18" cy="12" r="2.5" />
<path d="M6 8.5v7" />
<path d="M8.5 6.5c5 0 2 5.5 7 5.5" />
</svg>
);
}
}

View File

@ -1,150 +0,0 @@
"use client";
import { useEffect, useRef } from "react";
export function PageConstellation() {
const canvasRef = useRef<HTMLCanvasElement>(null);
useEffect(() => {
const canvas = canvasRef.current;
if (!canvas) return;
const ctx = canvas.getContext("2d");
if (!ctx) return;
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
const dpr = Math.min(window.devicePixelRatio || 1, 2);
type Dot = { x: number; y: number; vx: number; vy: number };
let dots: Dot[] = [];
let width = 0;
let height = 0;
const mouse = { x: -10000, y: -10000, active: false };
const seed = () => {
const target = Math.min(140, Math.max(40, Math.floor((width * height) / 18000)));
dots = Array.from({ length: target }, () => ({
x: Math.random() * width,
y: Math.random() * height,
vx: (Math.random() - 0.5) * 0.1,
vy: (Math.random() - 0.5) * 0.1,
}));
};
const resize = () => {
width = window.innerWidth;
height = window.innerHeight;
canvas.width = Math.floor(width * dpr);
canvas.height = Math.floor(height * dpr);
canvas.style.width = `${width}px`;
canvas.style.height = `${height}px`;
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
seed();
};
resize();
const onMove = (e: MouseEvent) => {
mouse.x = e.clientX;
mouse.y = e.clientY;
mouse.active = true;
};
const onLeave = () => {
mouse.active = false;
mouse.x = -10000;
mouse.y = -10000;
};
window.addEventListener("resize", resize);
window.addEventListener("mousemove", onMove);
document.addEventListener("mouseleave", onLeave);
window.addEventListener("blur", onLeave);
const LINK_DIST = 105;
const MOUSE_DIST = 165;
const draw = () => {
ctx.clearRect(0, 0, width, height);
for (const d of dots) {
d.x += d.vx;
d.y += d.vy;
if (d.x < 0 || d.x > width) d.vx *= -1;
if (d.y < 0 || d.y > height) d.vy *= -1;
}
ctx.lineWidth = 0.5;
for (let i = 0; i < dots.length; i++) {
for (let j = i + 1; j < dots.length; j++) {
const a = dots[i];
const b = dots[j];
const dx = a.x - b.x;
const dy = a.y - b.y;
const distSq = dx * dx + dy * dy;
if (distSq < LINK_DIST * LINK_DIST) {
const dist = Math.sqrt(distSq);
const op = (1 - dist / LINK_DIST) * 0.05;
ctx.strokeStyle = `rgba(240, 236, 232, ${op})`;
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.lineTo(b.x, b.y);
ctx.stroke();
}
}
}
if (mouse.active) {
ctx.lineWidth = 0.6;
for (const d of dots) {
const dx = d.x - mouse.x;
const dy = d.y - mouse.y;
const distSq = dx * dx + dy * dy;
if (distSq < MOUSE_DIST * MOUSE_DIST) {
const dist = Math.sqrt(distSq);
const op = (1 - dist / MOUSE_DIST) * 0.15;
ctx.strokeStyle = `rgba(240, 236, 232, ${op})`;
ctx.beginPath();
ctx.moveTo(d.x, d.y);
ctx.lineTo(mouse.x, mouse.y);
ctx.stroke();
}
}
}
for (const d of dots) {
let op = 0.11;
if (mouse.active) {
const dx = d.x - mouse.x;
const dy = d.y - mouse.y;
const distSq = dx * dx + dy * dy;
if (distSq < MOUSE_DIST * MOUSE_DIST) {
op += (1 - Math.sqrt(distSq) / MOUSE_DIST) * 0.22;
}
}
ctx.fillStyle = `rgba(240, 236, 232, ${op})`;
ctx.beginPath();
ctx.arc(d.x, d.y, 1.0, 0, Math.PI * 2);
ctx.fill();
}
};
let raf = 0;
const loop = () => {
draw();
raf = requestAnimationFrame(loop);
};
if (reducedMotion) draw();
else loop();
return () => {
cancelAnimationFrame(raf);
window.removeEventListener("resize", resize);
window.removeEventListener("mousemove", onMove);
document.removeEventListener("mouseleave", onLeave);
window.removeEventListener("blur", onLeave);
};
}, []);
return (
<canvas ref={canvasRef} className="fixed inset-0 pointer-events-none" style={{ zIndex: 0 }} aria-hidden="true" />
);
}

View File

@ -48,7 +48,7 @@ If you plan to use GitLab or Linear, install and authenticate their CLIs or cred
<Tab value="pnpm">```bash pnpm add -g @aoagents/ao ```</Tab>
<Tab value="yarn">```bash yarn global add @aoagents/ao ```</Tab>
<Tab value="from source">
```bash git clone https://github.com/ComposioHQ/agent-orchestrator cd agent-orchestrator pnpm install pnpm build
```bash git clone https://github.com/AgentWrapper/agent-orchestrator cd agent-orchestrator pnpm install pnpm build
pnpm --filter @aoagents/ao link --global ```
</Tab>
</Tabs>

Binary file not shown.

After

Width:  |  Height:  |  Size: 934 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 MiB

View File

@ -5,48 +5,52 @@
--font-mono: var(--font-jetbrains-mono), ui-monospace, "SFMono-Regular", Menlo, monospace;
/* ── Light mode (default) ─────────────────────────────────────── */
--color-bg-base: #f5f3f0;
--color-bg-base: #f8fafc;
--color-bg-surface: #ffffff;
--color-bg-elevated: #f9f7f5;
--color-bg-subtle: rgba(120, 100, 80, 0.06);
--color-bg-inset: #f0ede9;
--color-bg-sidebar: #f0ede9;
--color-bg-elevated: #f9fafb;
--color-bg-subtle: rgba(37, 99, 235, 0.06);
--color-bg-inset: #f1f5f9;
--color-bg-sidebar: #f3f4f6;
--color-text-primary: #1c1917;
--color-text-secondary: #57534e;
--color-text-tertiary: #78716c;
--color-text-primary: #18191d;
--color-text-secondary: #62646c;
--color-text-tertiary: #8a8a91;
--color-border-default: #d6d3d1;
--color-border-subtle: rgba(0, 0, 0, 0.06);
--color-border-default: rgba(28, 28, 31, 0.16);
--color-border-subtle: rgba(28, 28, 31, 0.09);
--color-accent: #5c64b5;
--color-accent-amber: #d97706;
--color-accent-amber-dim: rgba(217, 119, 6, 0.1);
--color-accent-amber-border: rgba(217, 119, 6, 0.3);
--color-accent: #2563eb;
--color-accent-dim: rgba(37, 99, 235, 0.1);
--color-accent-border: rgba(37, 99, 235, 0.28);
--color-accent-amber: var(--color-accent);
--color-accent-amber-dim: var(--color-accent-dim);
--color-accent-amber-border: var(--color-accent-border);
--color-scrollbar: rgba(0, 0, 0, 0.08);
}
/* ── Dark mode ────────────────────────────────────────────────── */
.dark {
--color-bg-base: #121110;
--color-bg-surface: #1a1918;
--color-bg-elevated: #242220;
--color-bg-subtle: #2b2826;
--color-bg-inset: #161514;
--color-bg-sidebar: #161514;
--color-bg-base: #0a0a0b;
--color-bg-surface: #0c0c0e;
--color-bg-elevated: #111113;
--color-bg-subtle: #101013;
--color-bg-inset: #070708;
--color-bg-sidebar: #080809;
--color-text-primary: #f0ece8;
--color-text-secondary: #a8a29e;
--color-text-tertiary: #8b8682;
--color-text-primary: #f5f5f4;
--color-text-secondary: #8b8b89;
--color-text-tertiary: #5b5b58;
--color-border-default: rgba(255, 240, 220, 0.14);
--color-border-subtle: rgba(255, 240, 220, 0.08);
--color-border-default: rgba(255, 255, 255, 0.12);
--color-border-subtle: rgba(255, 255, 255, 0.06);
--color-accent: #8b9cf7;
--color-accent-amber: #f97316;
--color-accent-amber-dim: rgba(249, 115, 22, 0.12);
--color-accent-amber-border: rgba(249, 115, 22, 0.4);
--color-accent: #4d8dff;
--color-accent-dim: rgba(77, 141, 255, 0.14);
--color-accent-border: rgba(77, 141, 255, 0.28);
--color-accent-amber: var(--color-accent);
--color-accent-amber-dim: var(--color-accent-dim);
--color-accent-amber-border: var(--color-accent-border);
--color-scrollbar: rgba(255, 240, 220, 0.15);
}
@ -62,33 +66,90 @@ body {
min-height: 100%;
}
html,
body {
font-family: var(--font-sans);
background: var(--color-bg-base);
color: var(--color-text-primary);
scrollbar-width: none;
}
a {
color: inherit;
}
html::-webkit-scrollbar,
body::-webkit-scrollbar,
.landing-page::-webkit-scrollbar {
display: none;
}
.landing-page {
scrollbar-width: none;
}
/* ── Landing Page ─────────────────────────────────────────────────── */
.landing-page {
--landing-bg: #121110;
--landing-fg: #f0ece8;
--landing-muted: #a8a29e;
--landing-muted-dim: #57534e;
--landing-accent: #f97316;
--landing-border-subtle: rgba(255, 240, 220, 0.07);
--landing-border-default: rgba(255, 240, 220, 0.13);
--landing-border-strong: rgba(255, 240, 220, 0.24);
--landing-surface: #1a1918;
--landing-card-bg: #1c1b19;
background: var(--landing-bg);
color: var(--landing-fg);
--bg: var(--color-bg-base);
--bg-card: var(--color-bg-surface);
--bg-card-hover: var(--color-bg-elevated);
--bg-elevated: var(--color-bg-elevated);
--bg-deep: var(--color-bg-inset);
--bg-chrome: var(--color-bg-sidebar);
--nav-bg: color-mix(in srgb, var(--color-bg-base) 84%, transparent);
--code-bg: var(--color-bg-inset);
--code-chrome: var(--color-bg-sidebar);
--code-fg: var(--color-text-primary);
--code-muted: var(--color-text-tertiary);
--border: var(--color-border-subtle);
--border-strong: var(--color-border-default);
--border-bright: var(--color-accent-border);
--fg: var(--color-text-primary);
--fg-muted: var(--color-text-secondary);
--fg-dim: var(--color-text-tertiary);
--accent: var(--color-accent);
--accent-soft: var(--color-accent-dim);
--accent-glow: var(--color-accent-border);
--status-warn: var(--color-accent);
--status-fail: #dc2626;
--status-ok: #16a34a;
--wire: var(--color-border-default);
--dot-red: #ff5f57;
--dot-yellow: #ffbd2e;
--dot-green: #28c840;
--landing-bg: var(--bg);
--landing-fg: var(--fg);
--landing-muted: var(--fg-muted);
--landing-muted-dim: var(--fg-dim);
--landing-accent: var(--accent);
--landing-border-subtle: var(--border);
--landing-border-default: var(--border-strong);
--landing-border-strong: var(--border-bright);
--landing-surface: var(--bg-elevated);
--landing-card-bg: var(--bg-card);
min-height: 100vh;
background: var(--bg);
color: var(--fg);
font-family: var(--font-sans, -apple-system, "SF Pro Text", system-ui, sans-serif);
}
.dark .landing-page {
--status-fail: #ef4444;
--status-ok: #22c55e;
}
[data-theme="light"] .theme-dark-only {
display: none;
}
[data-theme="light"] .theme-light-only {
display: block;
}
[data-theme="light"] .agent-logo-contrast {
filter: invert(1) contrast(1.1);
}
.liquid-glass-solid {
background: var(--landing-accent);
color: #121110;
@ -108,6 +169,181 @@ a {
background 0.2s;
}
.container-page {
width: 100%;
max-width: 1680px;
margin-left: auto;
margin-right: auto;
padding-left: clamp(1.25rem, 3.8vw, 4rem);
padding-right: clamp(1.25rem, 3.8vw, 4rem);
}
.font-display {
font-family: var(--font-sans), ui-sans-serif, system-ui, sans-serif;
letter-spacing: -0.025em;
}
.font-editorial {
font-family: ui-serif, Georgia, serif;
letter-spacing: 0;
}
.surface {
background: var(--bg-card);
border: 1px solid var(--border);
border-radius: 12px;
transition:
background-color 0.2s ease,
border-color 0.2s ease;
}
.surface:hover {
background: var(--bg-card-hover);
border-color: var(--border-strong);
}
.tweet-shell {
width: 100%;
min-width: 0;
overflow: hidden;
border-radius: 14px;
background: var(--bg-deep);
}
.tweet-shell .twitter-tweet,
.tweet-shell iframe {
width: 100% !important;
max-width: 100% !important;
min-width: 0 !important;
margin-left: auto !important;
margin-right: auto !important;
overflow: hidden !important;
border-radius: 14px !important;
background: var(--bg-deep) !important;
color-scheme: dark;
}
.tweet-masonry {
column-count: 1;
column-gap: 1.25rem;
}
@media (min-width: 768px) {
.tweet-masonry {
column-count: 2;
}
}
@media (min-width: 1024px) {
.tweet-masonry {
column-count: 3;
}
}
.surface-elev {
background: var(--bg-elevated);
border: 1px solid var(--border);
border-radius: 12px;
}
.lift {
transition:
transform 0.18s ease,
border-color 0.18s ease,
background 0.18s ease;
}
.lift:hover {
transform: translateY(-2px);
border-color: var(--border-strong);
}
.serial-num {
color: var(--fg-dim);
font-feature-settings: "tnum" on;
font-weight: 650;
letter-spacing: 0.05em;
}
.glow-accent {
box-shadow:
0 0 0 1px var(--border-strong),
0 16px 48px -34px var(--accent-glow);
}
.dotgrid {
background-image: radial-gradient(var(--border) 1px, transparent 1px);
background-size: 20px 20px;
}
.terminal-window {
overflow: hidden;
border: 1px solid var(--border);
border-radius: 10px;
background: var(--code-bg);
box-shadow:
0 0 0 1px rgba(255, 255, 255, 0.02) inset,
0 12px 40px -16px rgba(0, 0, 0, 0.8),
0 18px 56px -34px var(--accent-glow);
}
.terminal-header {
border-bottom: 1px solid var(--border);
background: var(--code-chrome);
}
@keyframes pulse-dot {
0%,
100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.45;
transform: scale(0.85);
}
}
.pulse-dot {
animation: pulse-dot 2s ease-in-out infinite;
}
@keyframes caret-blink {
0%,
49% {
opacity: 1;
}
50%,
100% {
opacity: 0;
}
}
.caret {
display: inline-block;
width: 0.5em;
height: 1em;
margin-left: 2px;
animation: caret-blink 1.05s steps(1) infinite;
background: var(--accent);
vertical-align: -2px;
}
@keyframes wire-pulse {
0% {
opacity: 0;
stroke-dashoffset: 200;
}
15%,
85% {
opacity: 1;
}
100% {
opacity: 0;
stroke-dashoffset: 0;
}
}
.landing-card:hover {
border-color: var(--landing-border-default);
}