Polish Agent Orchestrator landing page
This commit is contained in:
parent
01406eddca
commit
70577534f5
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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'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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 "ship it"
|
||||
</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>
|
||||
|
|
|
|||
|
|
@ -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'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)]">
|
||||
-> nudge -> 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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 -> worktree -> session -> pull request -> 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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"> </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"> </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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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)]"
|
||||
>
|
||||
“{t.quote}”
|
||||
</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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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" />
|
||||
);
|
||||
}
|
||||
|
|
@ -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 |
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue