mirror of
https://github.com/getpaseo/paseo.git
synced 2026-10-30 05:02:58 +00:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
494d88093b |
@@ -59,3 +59,72 @@ test("desktop mockup corners shrink in proportion on a phone", async ({ page })
|
|||||||
await resizeToPhone(page);
|
await resizeToPhone(page);
|
||||||
await expect.poll(() => cornerProportion(mockup)).toBeCloseTo(desktopCorner, 3);
|
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");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,7 +7,14 @@ export function agentRouteOptions(slug: string) {
|
|||||||
return {
|
return {
|
||||||
head: () => pageMeta(page.metaTitle, page.metaDescription, `/${slug}`),
|
head: () => pageMeta(page.metaTitle, page.metaDescription, `/${slug}`),
|
||||||
component: function AgentLandingPage() {
|
component: function AgentLandingPage() {
|
||||||
return <LandingPage title={page.title} subtitle={page.subtitle} />;
|
return (
|
||||||
|
<LandingPage
|
||||||
|
title={page.title}
|
||||||
|
subtitle={page.subtitle}
|
||||||
|
mockup={page.mockup}
|
||||||
|
section={page.section}
|
||||||
|
/>
|
||||||
|
);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
DESIGN_WIDTH,
|
DESIGN_WIDTH,
|
||||||
MOCKUP_STATES,
|
MOCKUP_STATES,
|
||||||
type MockupStateId,
|
type MockupStateId,
|
||||||
|
type MockupAgent,
|
||||||
MockupWindow,
|
MockupWindow,
|
||||||
ScaledMockup,
|
ScaledMockup,
|
||||||
} from "~/components/mockup";
|
} 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 };
|
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<MockupStateId>(DEFAULT_MOCKUP_STATE);
|
const [state, setState] = useState<MockupStateId>(DEFAULT_MOCKUP_STATE);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -39,7 +40,7 @@ export function HeroMockup() {
|
|||||||
label={ALT}
|
label={ALT}
|
||||||
className="ring-1 ring-white/10"
|
className="ring-1 ring-white/10"
|
||||||
>
|
>
|
||||||
<MockupWindow state={state} />
|
<MockupWindow state={state} agent={agent} />
|
||||||
</ScaledMockup>
|
</ScaledMockup>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ const AGENT_LIST_GRID_STYLE = {
|
|||||||
const PHONE_PERSPECTIVE_STYLE = { minHeight: 480, perspective: 700 };
|
const PHONE_PERSPECTIVE_STYLE = { minHeight: 480, perspective: 700 };
|
||||||
import { CursorFieldProvider } from "~/components/butterfly";
|
import { CursorFieldProvider } from "~/components/butterfly";
|
||||||
import { CommandDialog } from "~/components/command-dialog";
|
import { CommandDialog } from "~/components/command-dialog";
|
||||||
import { AGENT_PAGES } from "~/data/agent-pages";
|
import { AGENT_PAGES, type AgentPage } from "~/data/agent-pages";
|
||||||
import {
|
import {
|
||||||
appStoreUrl,
|
appStoreUrl,
|
||||||
playStoreUrl,
|
playStoreUrl,
|
||||||
@@ -91,12 +91,12 @@ import { SponsorSection, SponsorsSection } from "~/components/sponsorship";
|
|||||||
import { SiteHeader } from "~/components/site-header";
|
import { SiteHeader } from "~/components/site-header";
|
||||||
import "~/styles.css";
|
import "~/styles.css";
|
||||||
|
|
||||||
interface LandingPageProps {
|
interface LandingPageProps extends Pick<AgentPage, "mockup" | "section"> {
|
||||||
title: React.ReactNode;
|
title: React.ReactNode;
|
||||||
subtitle: React.ReactNode;
|
subtitle: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function LandingPage({ title, subtitle }: LandingPageProps) {
|
export function LandingPage({ title, subtitle, mockup, section }: LandingPageProps) {
|
||||||
return (
|
return (
|
||||||
<CursorFieldProvider>
|
<CursorFieldProvider>
|
||||||
{/* Hero section with background image */}
|
{/* 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"
|
className="relative px-6 md:px-8 pt-4 md:pt-8 pb-8 md:pb-16"
|
||||||
>
|
>
|
||||||
<div className="max-w-7xl mx-auto">
|
<div className="max-w-7xl mx-auto">
|
||||||
<HeroMockup />
|
<HeroMockup agent={mockup} />
|
||||||
</div>
|
</div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{section ? <AgentSection section={section} /> : null}
|
||||||
|
|
||||||
{/* Phone showcase */}
|
{/* Phone showcase */}
|
||||||
<PhoneShowcase />
|
<PhoneShowcase agent={mockup} />
|
||||||
|
|
||||||
{/* Content section */}
|
{/* Content section */}
|
||||||
<div className="landing-content bg-background">
|
<div className="landing-content bg-background">
|
||||||
@@ -144,6 +146,32 @@ export function LandingPage({ title, subtitle }: LandingPageProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function AgentSection({ section }: { section: NonNullable<AgentPage["section"]> }) {
|
||||||
|
return (
|
||||||
|
<section aria-label={section.title} className="mx-auto max-w-5xl px-6 py-16 md:py-24">
|
||||||
|
<h2 className="max-w-2xl text-3xl font-medium tracking-tight md:text-4xl">{section.title}</h2>
|
||||||
|
<p className="mt-4 max-w-2xl text-base leading-relaxed text-muted-foreground">
|
||||||
|
{section.description}
|
||||||
|
</p>
|
||||||
|
<div className="mt-10 grid gap-8 md:grid-cols-3">
|
||||||
|
{section.items.map((item) => (
|
||||||
|
<div key={item.title} className="border-t border-white/10 pt-5">
|
||||||
|
<h3 className="text-lg font-medium">{item.title}</h3>
|
||||||
|
<p className="mt-3 text-sm leading-relaxed text-muted-foreground">{item.description}</p>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href={section.link.href}
|
||||||
|
className="mt-8 inline-flex items-center gap-2 text-sm text-white/80 hover:text-white"
|
||||||
|
>
|
||||||
|
{section.link.label}
|
||||||
|
<ArrowRight className="h-4 w-4" />
|
||||||
|
</a>
|
||||||
|
</section>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function Nav() {
|
function Nav() {
|
||||||
return (
|
return (
|
||||||
<nav className="mb-20 md:mb-24">
|
<nav className="mb-20 md:mb-24">
|
||||||
@@ -1080,7 +1108,7 @@ function ServerInstallButton() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function PhoneShowcase() {
|
function PhoneShowcase({ agent }: { agent?: AgentPage["mockup"] }) {
|
||||||
const containerRef = React.useRef<HTMLDivElement>(null);
|
const containerRef = React.useRef<HTMLDivElement>(null);
|
||||||
const textInView = useInView(containerRef, { once: true, margin: "-80px" });
|
const textInView = useInView(containerRef, { once: true, margin: "-80px" });
|
||||||
|
|
||||||
@@ -1174,7 +1202,7 @@ function PhoneShowcase() {
|
|||||||
aria-label="Paseo agent chat"
|
aria-label="Paseo agent chat"
|
||||||
>
|
>
|
||||||
<PhoneFrame time="18:53">
|
<PhoneFrame time="18:53">
|
||||||
<MobileChat />
|
<MobileChat agent={agent} />
|
||||||
</PhoneFrame>
|
</PhoneFrame>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import type { ComponentType, SVGProps } from "react";
|
||||||
|
|
||||||
|
/** Optional branding for the shared desktop and phone composer mockups. */
|
||||||
|
export interface MockupAgent {
|
||||||
|
name: string;
|
||||||
|
Icon: ComponentType<SVGProps<SVGSVGElement>>;
|
||||||
|
model?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MockupAgentIcon({ agent, size }: { agent: MockupAgent; size: number }) {
|
||||||
|
return <agent.Icon width={size} height={size} className="shrink-0" />;
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { MockupAgentIcon, type MockupAgent } from "./agent";
|
||||||
import {
|
import {
|
||||||
AudioLines,
|
AudioLines,
|
||||||
BookOpen,
|
BookOpen,
|
||||||
@@ -188,10 +189,10 @@ export function PluginBuildTranscript() {
|
|||||||
|
|
||||||
// --- composer ---------------------------------------------------------------
|
// --- composer ---------------------------------------------------------------
|
||||||
|
|
||||||
function ControlPill({ icon, label }: { icon: React.ReactNode; label: string }) {
|
function ControlPill({ children, label }: { children: React.ReactNode; label: string }) {
|
||||||
return (
|
return (
|
||||||
<span className="flex items-center gap-[6px] text-[12px] text-mock-fg-muted">
|
<span className="flex items-center gap-[6px] text-[12px] text-mock-fg-muted">
|
||||||
{icon}
|
{children}
|
||||||
{label}
|
{label}
|
||||||
<ChevronDown size={11} className="text-mock-fg-xmuted" />
|
<ChevronDown size={11} className="text-mock-fg-xmuted" />
|
||||||
</span>
|
</span>
|
||||||
@@ -213,9 +214,11 @@ function BusyRing() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Composer({
|
export function Composer({
|
||||||
|
agent,
|
||||||
showProgress = true,
|
showProgress = true,
|
||||||
busy = true,
|
busy = true,
|
||||||
}: {
|
}: {
|
||||||
|
agent?: MockupAgent;
|
||||||
showProgress?: boolean;
|
showProgress?: boolean;
|
||||||
busy?: boolean;
|
busy?: boolean;
|
||||||
}) {
|
}) {
|
||||||
@@ -243,9 +246,13 @@ export function Composer({
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-[17px] pt-[26px]">
|
<div className="flex items-center gap-[17px] pt-[26px]">
|
||||||
<Plus size={15} className="text-mock-fg-muted" />
|
<Plus size={15} className="text-mock-fg-muted" />
|
||||||
<ControlPill icon={MODEL_ICON} label="GPT-5.6-Sol" />
|
<ControlPill
|
||||||
<ControlPill icon={EFFORT_ICON} label="Medium" />
|
label={agent ? `${agent.name} · ${agent.model ?? "GPT-5.6-Sol"}` : "GPT-5.6-Sol"}
|
||||||
<ControlPill icon={ACCESS_ICON} label="Full access" />
|
>
|
||||||
|
{agent ? <MockupAgentIcon agent={agent} size={13} /> : MODEL_ICON}
|
||||||
|
</ControlPill>
|
||||||
|
<ControlPill label="Medium">{EFFORT_ICON}</ControlPill>
|
||||||
|
<ControlPill label="Full access">{ACCESS_ICON}</ControlPill>
|
||||||
<Zap size={14} className="text-mock-fg-muted" />
|
<Zap size={14} className="text-mock-fg-muted" />
|
||||||
<ListTodo size={14} className="text-mock-fg-muted" />
|
<ListTodo size={14} className="text-mock-fg-muted" />
|
||||||
<span className="flex-1" />
|
<span className="flex-1" />
|
||||||
|
|||||||
@@ -7,3 +7,4 @@ export { MobileSidebar } from "./mobile/sidebar";
|
|||||||
export { DEFAULT_MOCKUP_STATE, MOCKUP_STATES, type MockupStateId } from "./states";
|
export { DEFAULT_MOCKUP_STATE, MOCKUP_STATES, type MockupStateId } from "./states";
|
||||||
export { MockupWindow } from "./window";
|
export { MockupWindow } from "./window";
|
||||||
export { ScaledMockup } from "./scaled-mockup";
|
export { ScaledMockup } from "./scaled-mockup";
|
||||||
|
export type { MockupAgent } from "./agent";
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { MockupAgentIcon, type MockupAgent } from "../agent";
|
||||||
// Mobile agent chat — the counterpart of the desktop chat pane, drawn at phone
|
// Mobile agent chat — the counterpart of the desktop chat pane, drawn at phone
|
||||||
// width. Maps to the "agent chat" reference screenshot: header, collapsed slash
|
// 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
|
// 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 (
|
return (
|
||||||
<div className="shrink-0 px-[18px] pt-[10px] pb-[30px]">
|
<div className="shrink-0 px-[18px] pt-[10px] pb-[30px]">
|
||||||
<div className="mb-[16px]">
|
<div className="mb-[16px]">
|
||||||
@@ -129,8 +130,14 @@ function Composer() {
|
|||||||
<div className="flex items-center gap-[13px] pt-[26px]">
|
<div className="flex items-center gap-[13px] pt-[26px]">
|
||||||
<Plus size={20} className="shrink-0 text-mock-fg-muted" strokeWidth={1.9} />
|
<Plus size={20} className="shrink-0 text-mock-fg-muted" strokeWidth={1.9} />
|
||||||
<span className="flex shrink-0 items-center gap-[5px]">
|
<span className="flex shrink-0 items-center gap-[5px]">
|
||||||
<CodexIcon size={19} className="shrink-0 text-mock-fg" />
|
{agent ? (
|
||||||
<span className="whitespace-nowrap text-[16px] text-mock-fg">GPT-5.6-Sol</span>
|
<MockupAgentIcon agent={agent} size={19} />
|
||||||
|
) : (
|
||||||
|
<CodexIcon size={19} className="shrink-0 text-mock-fg" />
|
||||||
|
)}
|
||||||
|
<span className="whitespace-nowrap text-[16px] text-mock-fg">
|
||||||
|
{agent ? `${agent.name} · ${agent.model ?? "GPT-5.6-Sol"}` : "GPT-5.6-Sol"}
|
||||||
|
</span>
|
||||||
<span className="whitespace-nowrap text-[16px] text-mock-fg-muted">Medium</span>
|
<span className="whitespace-nowrap text-[16px] text-mock-fg-muted">Medium</span>
|
||||||
</span>
|
</span>
|
||||||
<span className="flex-1" />
|
<span className="flex-1" />
|
||||||
@@ -143,7 +150,7 @@ function Composer() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MobileChat() {
|
export function MobileChat({ agent }: { agent?: MockupAgent }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Header />
|
<Header />
|
||||||
@@ -151,7 +158,7 @@ export function MobileChat() {
|
|||||||
<div className="min-h-0 flex-1 overflow-hidden pt-[6px]">
|
<div className="min-h-0 flex-1 overflow-hidden pt-[6px]">
|
||||||
<Transcript />
|
<Transcript />
|
||||||
</div>
|
</div>
|
||||||
<Composer />
|
<Composer agent={agent} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { MockupAgent } from "./agent";
|
||||||
import { AnimatePresence, MotionConfig, motion, useReducedMotion } from "framer-motion";
|
import { AnimatePresence, MotionConfig, motion, useReducedMotion } from "framer-motion";
|
||||||
import { AgentTranscript, Composer, PluginBuildTranscript } from "./chat";
|
import { AgentTranscript, Composer, PluginBuildTranscript } from "./chat";
|
||||||
import { DESIGN_HEIGHT, DESIGN_WIDTH, RIGHT_PANEL_W } from "./geometry";
|
import { DESIGN_HEIGHT, DESIGN_WIDTH, RIGHT_PANEL_W } from "./geometry";
|
||||||
@@ -26,7 +27,7 @@ function panelKey(state: MockupStateId): string {
|
|||||||
return "changes";
|
return "changes";
|
||||||
}
|
}
|
||||||
|
|
||||||
function CenterPane({ state }: { state: MockupStateId }) {
|
function CenterPane({ state, agent }: { state: MockupStateId; agent?: MockupAgent }) {
|
||||||
if (state === "review") return <DiffReviewPane />;
|
if (state === "review") return <DiffReviewPane />;
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-0 flex-1 flex-col bg-mock-surface0">
|
<div className="flex min-h-0 flex-1 flex-col bg-mock-surface0">
|
||||||
@@ -34,7 +35,7 @@ function CenterPane({ state }: { state: MockupStateId }) {
|
|||||||
<div className="flex min-h-0 flex-1 flex-col justify-end overflow-hidden px-[27px] pb-[6px]">
|
<div className="flex min-h-0 flex-1 flex-col justify-end overflow-hidden px-[27px] pb-[6px]">
|
||||||
{state === "extend" ? <PluginBuildTranscript /> : <AgentTranscript />}
|
{state === "extend" ? <PluginBuildTranscript /> : <AgentTranscript />}
|
||||||
</div>
|
</div>
|
||||||
<Composer showProgress={state !== "extend"} busy={state !== "extend"} />
|
<Composer agent={agent} showProgress={state !== "extend"} busy={state !== "extend"} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -59,7 +60,7 @@ function PanelCrossfade({ state }: { state: MockupStateId }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** The middle content column: its own header, then the tab strip, then the pane. */
|
/** 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 (
|
return (
|
||||||
<div className="flex min-w-0 flex-1 flex-col">
|
<div className="flex min-w-0 flex-1 flex-col">
|
||||||
<TitleBar state={state} />
|
<TitleBar state={state} />
|
||||||
@@ -73,7 +74,7 @@ function CenterColumn({ state }: { state: MockupStateId }) {
|
|||||||
exit={FADE_OUT}
|
exit={FADE_OUT}
|
||||||
className="absolute inset-0 flex flex-col"
|
className="absolute inset-0 flex flex-col"
|
||||||
>
|
>
|
||||||
<CenterPane state={state} />
|
<CenterPane state={state} agent={agent} />
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
</div>
|
</div>
|
||||||
@@ -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();
|
const reduceMotion = useReducedMotion();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -109,7 +110,7 @@ export function MockupWindow({ state }: { state: MockupStateId }) {
|
|||||||
header stops at the divider — it never spans over the right panel. */}
|
header stops at the divider — it never spans over the right panel. */}
|
||||||
<Sidebar state={state} />
|
<Sidebar state={state} />
|
||||||
<div className="w-px shrink-0 bg-mock-border" />
|
<div className="w-px shrink-0 bg-mock-border" />
|
||||||
<CenterColumn state={state} />
|
<CenterColumn state={state} agent={agent} />
|
||||||
<AnimatePresence initial={false}>
|
<AnimatePresence initial={false}>
|
||||||
{hasRightPanel(state) ? <DockedPanel key="right-panel" state={state} /> : null}
|
{hasRightPanel(state) ? <DockedPanel key="right-panel" state={state} /> : null}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
|
|||||||
@@ -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.
|
// 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
|
// To add a new agent, append an entry here and create a 4-line route file at
|
||||||
// `src/routes/<slug>.tsx`. The sitemap (vite.config) reads `AGENT_PAGE_SLUGS`.
|
// `src/routes/<slug>.tsx`. The sitemap (vite.config) reads `AGENT_PAGE_SLUGS`.
|
||||||
@@ -9,6 +12,14 @@ export interface AgentPage {
|
|||||||
subtitle: string;
|
subtitle: string;
|
||||||
metaTitle: string;
|
metaTitle: string;
|
||||||
metaDescription: 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 = [
|
export const AGENT_PAGES = [
|
||||||
@@ -45,9 +56,33 @@ export const AGENT_PAGES = [
|
|||||||
{
|
{
|
||||||
slug: "pi",
|
slug: "pi",
|
||||||
name: "Pi Agent",
|
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",
|
title: "Open source app for the Pi coding agent",
|
||||||
subtitle:
|
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",
|
metaTitle: "Pi Agent Mobile and Desktop App, Open Source",
|
||||||
metaDescription:
|
metaDescription:
|
||||||
"Open source mobile and desktop app for the Pi coding agent. Launch sessions on your machine, monitor progress, merge from anywhere. Self-hosted.",
|
"Open source mobile and desktop app for the Pi coding agent. Launch sessions on your machine, monitor progress, merge from anywhere. Self-hosted.",
|
||||||
|
|||||||
Reference in New Issue
Block a user