+
+
+
+ Agent Orchestrator
+
+
+
+
+ {navItems.map((item) => (
+
+ {item.label}
+
+ ))}
+
+
+
- Features
+
+ 7.7k
-
-
+ 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 ? : }
+
- How It Works
+ Install
+
-
-
-
-
-
-
-
-
-
-
-
-
+
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 ? : }
+
+
-
+ {open && (
+
+ )}
+
);
}
diff --git a/frontend/src/landing/components/LandingQuickStart.tsx b/frontend/src/landing/components/LandingQuickStart.tsx
deleted file mode 100644
index 651559951..000000000
--- a/frontend/src/landing/components/LandingQuickStart.tsx
+++ /dev/null
@@ -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 (
-
-
-
- Get started in 60 seconds
-
-
- Three commands to launch
-
-
-
- {steps.map((s) => (
-
-
- {s.num}
-
-
{s.title}
-
{s.desc}
-
- $ {s.cmd}
-
-
- ))}
-
-
-
- );
-}
diff --git a/frontend/src/landing/components/LandingSocialProof.tsx b/frontend/src/landing/components/LandingSocialProof.tsx
new file mode 100644
index 000000000..5f9ed3388
--- /dev/null
+++ b/frontend/src/landing/components/LandingSocialProof.tsx
@@ -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 (
+
+
+
+
+ );
+}
+
+function MessageCircleIcon({ className = "" }: { className?: string }) {
+ return (
+
+
+
+ );
+}
+
+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 (
+
+
+
+
+
+
06 - in the wild
+
+ People are already{" "}
+
+ building around it.
+
+
+
+
+
+ Real posts from builders, researchers, and early users, embedded directly from X.
+
+
+
+
+
+ {posts.map((post, index) => (
+
+ ))}
+
+
+
+
+ );
+}
+
+function TweetCard({ post, index, theme }: { post: (typeof posts)[number]; index: number; theme: string }) {
+ const url = postUrl(post);
+
+ return (
+
+
+
+
+
+
+ {post.label}
+
+
{post.author}
+
+
+
+
+
+
+
+
+
+ );
+}
diff --git a/frontend/src/landing/components/LandingStats.tsx b/frontend/src/landing/components/LandingStats.tsx
deleted file mode 100644
index c2fab551e..000000000
--- a/frontend/src/landing/components/LandingStats.tsx
+++ /dev/null
@@ -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 (
-
-
- {cards.map((stat) => (
-
-
- {stat.number}
-
-
{stat.label}
-
- ))}
-
-
-
- );
-}
diff --git a/frontend/src/landing/components/LandingTestimonials.tsx b/frontend/src/landing/components/LandingTestimonials.tsx
deleted file mode 100644
index 982e7c414..000000000
--- a/frontend/src/landing/components/LandingTestimonials.tsx
+++ /dev/null
@@ -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 (
-
-
-
- What engineers say
-
-
- Trusted by builders
-
-
-
- setPaused(true)} onMouseLeave={() => setPaused(false)}>
- {/* Quote — fades on change */}
-
-
- {/* Bottom row — author cluster on the left, step counter on the right */}
-
-
-
- {testimonials.map((item, i) => {
- const isActive = i === active;
- const size = isActive ? 56 : 44;
- return (
-
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 */}
-
-
- );
- })}
-
-
- {/* Vertical divider */}
-
-
- {/* Author — fades on change */}
-
-
-
- {/* Step counter — fills the right side */}
-
-
- {String(active + 1).padStart(2, "0")}
-
-
- / {String(testimonials.length).padStart(2, "0")}
-
-
-
-
-
- );
-}
diff --git a/frontend/src/landing/components/LandingUseCases.tsx b/frontend/src/landing/components/LandingUseCases.tsx
deleted file mode 100644
index d3de93ae4..000000000
--- a/frontend/src/landing/components/LandingUseCases.tsx
+++ /dev/null
@@ -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
(null);
- const ringRef = useRef(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) => {
- 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) => {
- 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 (
-
-
-
- Use cases
-
-
- One orchestrator, many jobs
-
-
- Point AO at the work and walk away — drag to explore what a single run can do.
-
-
-
- (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)",
- }}
- >
-
- {cases.map((c, i) => (
-
{
- 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",
- }}
- >
-
-
- {c.eyebrow}
-
-
- {c.title}
-
-
- {c.desc}
-
-
-
- {c.prefix} {c.cmd}
-
-
→ {c.outcome}
-
-
-
- ))}
-
-
-
- );
-}
diff --git a/frontend/src/landing/components/LandingVideo.tsx b/frontend/src/landing/components/LandingVideo.tsx
index 132537b6d..a027f291e 100644
--- a/frontend/src/landing/components/LandingVideo.tsx
+++ b/frontend/src/landing/components/LandingVideo.tsx
@@ -1,19 +1,37 @@
export function LandingVideo() {
return (
-
-
-
- See it in action
-
-
-
-
VIDEO
+
+
+
+
see it in action
+
+ Watch the founder walk through it -{" "}
+ 100 PRs in 6 days.
+
+
+
+
);
diff --git a/frontend/src/landing/components/LandingWorkflow.tsx b/frontend/src/landing/components/LandingWorkflow.tsx
deleted file mode 100644
index 58b15e88a..000000000
--- a/frontend/src/landing/components/LandingWorkflow.tsx
+++ /dev/null
@@ -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
(null);
- const svgWrapRef = useRef(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 (
-
-
-
- Lifecycle
-
-
- From issue to merged PR
-
-
- Every session walks the same path — spawned in isolation, working in parallel, landing on{" "}
- main on its own.
-
-
-
- setPaused(true)}
- onMouseLeave={() => setPaused(false)}
- >
- {/* Active icon + label */}
-
-
-
{cur.label}
-
- {cur.key}
-
-
-
- {/* Ruler */}
-
-
- {/* center indicator */}
-
-
-
- {/* ticks */}
- {ticks.map((t) => (
-
- ))}
-
- {/* 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 (
-
- {milestones[idx].label}
-
- );
- })}
-
-
-
- {/* Active description */}
-
-
-
- );
-}
-
-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 (
-
-
-
-
-
- );
- case "work":
- return (
-
-
-
-
-
- );
- case "pr":
- return (
-
-
-
-
-
-
-
-
- );
- case "review":
- return (
-
-
-
-
- );
- case "mergeable":
- return (
-
-
-
-
- );
- case "merged":
- return (
-
-
-
-
-
-
-
- );
- }
-}
diff --git a/frontend/src/landing/components/PageConstellation.tsx b/frontend/src/landing/components/PageConstellation.tsx
deleted file mode 100644
index 765e96163..000000000
--- a/frontend/src/landing/components/PageConstellation.tsx
+++ /dev/null
@@ -1,150 +0,0 @@
-"use client";
-
-import { useEffect, useRef } from "react";
-
-export function PageConstellation() {
- const canvasRef = useRef(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 (
-
- );
-}
diff --git a/frontend/src/landing/content/docs/installation.mdx b/frontend/src/landing/content/docs/installation.mdx
index 1fffc19c0..7c3e774fe 100644
--- a/frontend/src/landing/content/docs/installation.mdx
+++ b/frontend/src/landing/content/docs/installation.mdx
@@ -48,7 +48,7 @@ If you plan to use GitLab or Linear, install and authenticate their CLIs or cred
```bash pnpm add -g @aoagents/ao ```
```bash yarn global add @aoagents/ao ```
- ```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 ```
diff --git a/frontend/src/landing/public/ao-logo-transparent.png b/frontend/src/landing/public/ao-logo-transparent.png
new file mode 100644
index 000000000..ae1f82f99
Binary files /dev/null and b/frontend/src/landing/public/ao-logo-transparent.png differ
diff --git a/frontend/src/landing/public/ao-logo.svg b/frontend/src/landing/public/ao-logo.svg
new file mode 100644
index 000000000..cfe33f5df
--- /dev/null
+++ b/frontend/src/landing/public/ao-logo.svg
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/landing/public/hero-dashboard-light.png b/frontend/src/landing/public/hero-dashboard-light.png
new file mode 100644
index 000000000..52be34bac
Binary files /dev/null and b/frontend/src/landing/public/hero-dashboard-light.png differ
diff --git a/frontend/src/landing/public/hero-new-task.png b/frontend/src/landing/public/hero-new-task.png
new file mode 100644
index 000000000..b5f1b6c5f
Binary files /dev/null and b/frontend/src/landing/public/hero-new-task.png differ
diff --git a/frontend/src/landing/public/landing-page-demo.gif b/frontend/src/landing/public/landing-page-demo.gif
new file mode 100644
index 000000000..03edf761f
Binary files /dev/null and b/frontend/src/landing/public/landing-page-demo.gif differ
diff --git a/frontend/src/landing/styles/globals.css b/frontend/src/landing/styles/globals.css
index f1e029850..6ffb6b369 100644
--- a/frontend/src/landing/styles/globals.css
+++ b/frontend/src/landing/styles/globals.css
@@ -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);
}