diff --git a/packages/website/e2e/mockups.spec.ts b/packages/website/e2e/mockups.spec.ts index 8b0ba65857..3be72da2f7 100644 --- a/packages/website/e2e/mockups.spec.ts +++ b/packages/website/e2e/mockups.spec.ts @@ -59,3 +59,72 @@ test("desktop mockup corners shrink in proportion on a phone", async ({ page }) await resizeToPhone(page); await expect.poll(() => cornerProportion(mockup)).toBeCloseTo(desktopCorner, 3); }); + +async function openAgentPage(page: Page, slug: string, width: number) { + await page.setViewportSize({ width, height: 900 }); + await page.goto(`/${slug}`); + await expect(page.getByRole("button", { name: "Build", exact: true })).toBeVisible(); +} + +async function expectComposerModel(page: Page, label: string) { + const desktop = page.getByRole("img", { name: desktopName, exact: true }); + await expect(desktop.getByText(label, { exact: true })).toBeVisible(); + await page.getByRole("button", { name: "Extend", exact: true }).click(); + await expect(desktop.getByText(label, { exact: true })).toHaveCount(1); + await expect(desktop.getByText(label, { exact: true })).toBeVisible(); + await viewPhones(page); + await expect( + page + .getByRole("img", { name: "Paseo agent chat", exact: true }) + .getByText(label, { exact: true }), + ).toBeVisible(); +} + +async function expectPiOverlay(page: Page) { + await expect(page).toHaveTitle("Pi Agent Mobile and Desktop App, Open Source"); + await expect(page.getByRole("heading", { level: 1 })).toHaveText( + "Open source app for the Pi coding agent", + ); + await expect(page.locator('link[rel="canonical"]')).toHaveAttribute( + "href", + "https://paseo.sh/pi", + ); + await expectComposerModel(page, "Pi · Sonnet 4.5"); + const section = page.getByRole("region", { name: "Your Pi setup, with a desktop and mobile UI" }); + await section.scrollIntoViewIfNeeded(); + await expect(section.getByRole("heading", { name: "Bring your extensions" })).toBeVisible(); + await expect( + section.getByText(/pi-subagents, pi-ask-user, pi-mcp-adapter and pi-goal-x/), + ).toBeVisible(); + await expect(section.getByRole("link", { name: "Set up Paseo" })).toHaveAttribute( + "href", + "/docs", + ); + expect(await page.evaluate(() => document.documentElement.scrollWidth)).toBeLessThanOrEqual( + await page.evaluate(() => window.innerWidth), + ); +} + +for (const width of [1440, 390]) { + test(`Pi has its own content and composer at ${width}px`, async ({ page }) => { + await openAgentPage(page, "pi", width); + await expectPiOverlay(page); + }); + + test(`an agent without an overlay keeps the shared page at ${width}px`, async ({ page }) => { + await openAgentPage(page, "opencode", width); + await expect(page.getByRole("heading", { level: 1 })).toHaveText( + "Open source app for OpenCode", + ); + await expect( + page.getByText( + "Run OpenCode on your machine, drive it from your phone or desktop. Open source on both ends, your code stays local.", + { exact: true }, + ), + ).toBeVisible(); + await expect( + page.getByRole("region", { name: "Your Pi setup, with a desktop and mobile UI" }), + ).toHaveCount(0); + await expectComposerModel(page, "GPT-5.6-Sol"); + }); +} diff --git a/packages/website/src/components/agent-route.tsx b/packages/website/src/components/agent-route.tsx index e86db49523..6a78b091bc 100644 --- a/packages/website/src/components/agent-route.tsx +++ b/packages/website/src/components/agent-route.tsx @@ -7,7 +7,14 @@ export function agentRouteOptions(slug: string) { return { head: () => pageMeta(page.metaTitle, page.metaDescription, `/${slug}`), component: function AgentLandingPage() { - return ; + return ( + + ); }, }; } diff --git a/packages/website/src/components/hero-mockup.tsx b/packages/website/src/components/hero-mockup.tsx index eeffc41d79..1911d9039d 100644 --- a/packages/website/src/components/hero-mockup.tsx +++ b/packages/website/src/components/hero-mockup.tsx @@ -6,6 +6,7 @@ import { DESIGN_WIDTH, MOCKUP_STATES, type MockupStateId, + type MockupAgent, MockupWindow, ScaledMockup, } from "~/components/mockup"; @@ -15,7 +16,7 @@ const ALT = const PILL_TRANSITION = { duration: 0.34, ease: [0.22, 0.61, 0.36, 1] as const }; -export function HeroMockup() { +export function HeroMockup({ agent }: { agent?: MockupAgent }) { const [state, setState] = useState(DEFAULT_MOCKUP_STATE); return ( @@ -39,7 +40,7 @@ export function HeroMockup() { label={ALT} className="ring-1 ring-white/10" > - + ); diff --git a/packages/website/src/components/landing-page.tsx b/packages/website/src/components/landing-page.tsx index a5b573a1d6..2411722427 100644 --- a/packages/website/src/components/landing-page.tsx +++ b/packages/website/src/components/landing-page.tsx @@ -59,7 +59,7 @@ const AGENT_LIST_GRID_STYLE = { const PHONE_PERSPECTIVE_STYLE = { minHeight: 480, perspective: 700 }; import { CursorFieldProvider } from "~/components/butterfly"; import { CommandDialog } from "~/components/command-dialog"; -import { AGENT_PAGES } from "~/data/agent-pages"; +import { AGENT_PAGES, type AgentPage } from "~/data/agent-pages"; import { appStoreUrl, playStoreUrl, @@ -91,12 +91,12 @@ import { SponsorSection, SponsorsSection } from "~/components/sponsorship"; import { SiteHeader } from "~/components/site-header"; import "~/styles.css"; -interface LandingPageProps { +interface LandingPageProps extends Pick { title: React.ReactNode; subtitle: React.ReactNode; } -export function LandingPage({ title, subtitle }: LandingPageProps) { +export function LandingPage({ title, subtitle, mockup, section }: LandingPageProps) { return ( {/* Hero section with background image */} @@ -115,13 +115,15 @@ export function LandingPage({ title, subtitle }: LandingPageProps) { className="relative px-6 md:px-8 pt-4 md:pt-8 pb-8 md:pb-16" >
- +
+ {section ? : null} + {/* Phone showcase */} - + {/* Content section */}
@@ -144,6 +146,32 @@ export function LandingPage({ title, subtitle }: LandingPageProps) { ); } +function AgentSection({ section }: { section: NonNullable }) { + return ( +
+

{section.title}

+

+ {section.description} +

+
+ {section.items.map((item) => ( +
+

{item.title}

+

{item.description}

+
+ ))} +
+ + {section.link.label} + + +
+ ); +} + function Nav() { return (
- - - + + {agent ? : MODEL_ICON} + + {EFFORT_ICON} + {ACCESS_ICON} diff --git a/packages/website/src/components/mockup/index.ts b/packages/website/src/components/mockup/index.ts index 71ffed8b8d..fe17c15684 100644 --- a/packages/website/src/components/mockup/index.ts +++ b/packages/website/src/components/mockup/index.ts @@ -7,3 +7,4 @@ export { MobileSidebar } from "./mobile/sidebar"; export { DEFAULT_MOCKUP_STATE, MOCKUP_STATES, type MockupStateId } from "./states"; export { MockupWindow } from "./window"; export { ScaledMockup } from "./scaled-mockup"; +export type { MockupAgent } from "./agent"; diff --git a/packages/website/src/components/mockup/mobile/chat.tsx b/packages/website/src/components/mockup/mobile/chat.tsx index 4d3ac156fd..964a2947dc 100644 --- a/packages/website/src/components/mockup/mobile/chat.tsx +++ b/packages/website/src/components/mockup/mobile/chat.tsx @@ -1,3 +1,4 @@ +import { MockupAgentIcon, type MockupAgent } from "../agent"; // Mobile agent chat — the counterpart of the desktop chat pane, drawn at phone // width. Maps to the "agent chat" reference screenshot: header, collapsed slash // command, muted tool-call rows, an assistant turn, the diff-stat pill, and the @@ -115,7 +116,7 @@ function Transcript() { ); } -function Composer() { +function Composer({ agent }: { agent?: MockupAgent }) { return (
@@ -129,8 +130,14 @@ function Composer() {
- - GPT-5.6-Sol + {agent ? ( + + ) : ( + + )} + + {agent ? `${agent.name} · ${agent.model ?? "GPT-5.6-Sol"}` : "GPT-5.6-Sol"} + Medium @@ -143,7 +150,7 @@ function Composer() { ); } -export function MobileChat() { +export function MobileChat({ agent }: { agent?: MockupAgent }) { return ( <>
@@ -151,7 +158,7 @@ export function MobileChat() {
- + ); } diff --git a/packages/website/src/components/mockup/window.tsx b/packages/website/src/components/mockup/window.tsx index ae95b68867..b6cea3af71 100644 --- a/packages/website/src/components/mockup/window.tsx +++ b/packages/website/src/components/mockup/window.tsx @@ -1,3 +1,4 @@ +import type { MockupAgent } from "./agent"; import { AnimatePresence, MotionConfig, motion, useReducedMotion } from "framer-motion"; import { AgentTranscript, Composer, PluginBuildTranscript } from "./chat"; import { DESIGN_HEIGHT, DESIGN_WIDTH, RIGHT_PANEL_W } from "./geometry"; @@ -26,7 +27,7 @@ function panelKey(state: MockupStateId): string { return "changes"; } -function CenterPane({ state }: { state: MockupStateId }) { +function CenterPane({ state, agent }: { state: MockupStateId; agent?: MockupAgent }) { if (state === "review") return ; return (
@@ -34,7 +35,7 @@ function CenterPane({ state }: { state: MockupStateId }) {
{state === "extend" ? : }
- +
); } @@ -59,7 +60,7 @@ function PanelCrossfade({ state }: { state: MockupStateId }) { } /** The middle content column: its own header, then the tab strip, then the pane. */ -function CenterColumn({ state }: { state: MockupStateId }) { +function CenterColumn({ state, agent }: { state: MockupStateId; agent?: MockupAgent }) { return (
@@ -73,7 +74,7 @@ function CenterColumn({ state }: { state: MockupStateId }) { exit={FADE_OUT} className="absolute inset-0 flex flex-col" > - +
@@ -96,7 +97,7 @@ function DockedPanel({ state }: { state: MockupStateId }) { ); } -export function MockupWindow({ state }: { state: MockupStateId }) { +export function MockupWindow({ state, agent }: { state: MockupStateId; agent?: MockupAgent }) { const reduceMotion = useReducedMotion(); return ( @@ -109,7 +110,7 @@ export function MockupWindow({ state }: { state: MockupStateId }) { header stops at the divider — it never spans over the right panel. */}
- + {hasRightPanel(state) ? : null} diff --git a/packages/website/src/data/agent-pages.ts b/packages/website/src/data/agent-pages.ts index ee54dc8dea..49d9393785 100644 --- a/packages/website/src/data/agent-pages.ts +++ b/packages/website/src/data/agent-pages.ts @@ -1,3 +1,6 @@ +import { PiIcon } from "~/components/agent-icons"; +import type { MockupAgent } from "~/components/mockup"; + // Source of truth for per-agent marketing landing pages. // To add a new agent, append an entry here and create a 4-line route file at // `src/routes/.tsx`. The sitemap (vite.config) reads `AGENT_PAGE_SLUGS`. @@ -9,6 +12,14 @@ export interface AgentPage { subtitle: string; metaTitle: string; metaDescription: string; + // Omit either field to keep the shared landing page unchanged. + mockup?: MockupAgent; + section?: { + title: string; + description: string; + items: readonly { title: string; description: string }[]; + link: { href: string; label: string }; + }; } export const AGENT_PAGES = [ @@ -45,9 +56,33 @@ export const AGENT_PAGES = [ { slug: "pi", name: "Pi Agent", + mockup: { name: "Pi", Icon: PiIcon, model: "Sonnet 4.5" }, + section: { + title: "Your Pi setup, with a desktop and mobile UI", + description: + "Paseo runs your installed Pi CLI. Start a session from the app, or import one you started in the terminal and continue from there.", + items: [ + { + title: "Bring your extensions", + description: + "Paseo supports Pi extensions including pi-subagents, pi-ask-user, pi-mcp-adapter and pi-goal-x. Follow subagents, answer questions and see task progress in the app. Extensions that depend on a custom terminal UI do not carry over.", + }, + { + title: "Pick from your Pi models", + description: + "The model picker reads the models available in your Pi setup and uses Pi’s configured model by default. Keep your provider configuration in Pi.", + }, + { + title: "Start with Pi on your machine", + description: + "Install and configure Pi on the machine where you run Paseo. Open a workspace in Paseo and choose Pi when you create an agent. Pair your phone to keep working on that machine when you step away.", + }, + ], + link: { href: "/docs", label: "Set up Paseo" }, + }, title: "Open source app for the Pi coding agent", subtitle: - "Run the Pi coding agent on your machine, drive it from your phone or desktop. Self-hosted and open source.", + "Use Pi from your desktop or phone, with the models and extensions you already use. Pi runs on your machine. Paseo gives you a UI for it.", metaTitle: "Pi Agent Mobile and Desktop App, Open Source", metaDescription: "Open source mobile and desktop app for the Pi coding agent. Launch sessions on your machine, monitor progress, merge from anywhere. Self-hosted.",