From b474beedce0d8d17861f25094a9e6d24aff16413 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Mon, 2 Nov 2026 10:50:12 +0700 Subject: [PATCH] Homepage shows an agent building a plugin live (#8515) * feat(website): show agents building plugins on the homepage The plugins section now leads with the workflow instead of the API list: you ask an agent for a panel and it appears, without a restart. It plays as a timeline (prompt, tool calls, result) and then slides in the panel the agent built, followed by the 400+ community plugins and their categories. It moves above the automation section. Mock tool calls on the homepage now match the app's collapsed timeline rows: same labels and icons, muted text in the UI font, and Paseo MCP tools named by action ("Create agent"). The hero pills are now nouns. * fix(website): show the finished plugin demo when motion is reduced The demo staged its items and slid the panel across its width even for visitors who ask for reduced motion. It now starts in its finished state for them, matching the hero mockup. --- packages/website/e2e/mockups.spec.ts | 41 ++- .../website/src/components/landing-page.tsx | 256 ++++++++++++++---- .../website/src/components/mockup/chat.tsx | 86 +++--- .../website/src/components/mockup/index.ts | 1 + packages/website/src/i18n/resources/en.json | 27 +- .../website/src/i18n/resources/zh-CN.json | 27 +- 6 files changed, 315 insertions(+), 123 deletions(-) diff --git a/packages/website/e2e/mockups.spec.ts b/packages/website/e2e/mockups.spec.ts index 8b0ba65857..41570439bf 100644 --- a/packages/website/e2e/mockups.spec.ts +++ b/packages/website/e2e/mockups.spec.ts @@ -6,7 +6,7 @@ const desktopName = async function openHomepage(page: Page) { await page.setViewportSize({ width: 1512, height: 930 }); await page.goto("/"); - await expect(page.getByRole("button", { name: "Build", exact: true })).toBeVisible(); + await expect(page.getByRole("button", { name: "Parallel agents", exact: true })).toBeVisible(); } async function viewPhones(page: Page) { @@ -59,3 +59,42 @@ test("desktop mockup corners shrink in proportion on a phone", async ({ page }) await resizeToPhone(page); await expect.poll(() => cornerProportion(mockup)).toBeCloseTo(desktopCorner, 3); }); + +/** Opacity as rendered: the element's own times every ancestor's. */ +async function renderedOpacity(locator: Locator) { + return locator.evaluate((element) => { + let opacity = 1; + for (let node: Element | null = element; node; node = node.parentElement) { + opacity *= Number.parseFloat(getComputedStyle(node).opacity); + } + return opacity; + }); +} + +test("the plugin demo plays through to the panel the agent built", async ({ page }) => { + await openHomepage(page); + const builtPanelRow = page.getByText("Add Gruvbox theme", { exact: true }); + expect(await renderedOpacity(builtPanelRow)).toBe(0); + + // Hydration can reset the scroll position, so keep the demo in view until it finishes. + const heading = page.getByRole("heading", { name: "Ask Paseo to change Paseo" }); + await expect(async () => { + await heading.scrollIntoViewIfNeeded(); + expect(await renderedOpacity(builtPanelRow)).toBe(1); + }).toPass({ timeout: 15_000 }); + expect(await renderedOpacity(page.getByText("Review queue is in your sidebar."))).toBe(1); +}); + +test("the plugin demo shows its finished state when motion is reduced", async ({ page }) => { + await page.emulateMedia({ reducedMotion: "reduce" }); + await openHomepage(page); + const builtPanelRow = page.getByText("Add Gruvbox theme", { exact: true }); + const panel = builtPanelRow.locator("xpath=ancestor::div[contains(@class, 'md:border-l')]"); + const heading = page.getByRole("heading", { name: "Ask Paseo to change Paseo" }); + + await expect(async () => { + await heading.scrollIntoViewIfNeeded(); + expect(await renderedOpacity(builtPanelRow)).toBe(1); + }).toPass({ timeout: 2_000 }); + expect(await panel.evaluate((element) => getComputedStyle(element).transform)).toBe("none"); +}); diff --git a/packages/website/src/components/landing-page.tsx b/packages/website/src/components/landing-page.tsx index 93d43afa26..77869e7379 100644 --- a/packages/website/src/components/landing-page.tsx +++ b/packages/website/src/components/landing-page.tsx @@ -1,4 +1,6 @@ import { PLUGINS_LINKED } from "~/plugins/launch"; +import { categoryHref } from "~/plugins/links"; +import type { CategorySlug } from "~/plugins/categories"; import * as React from "react"; import { Trans, useTranslation } from "react-i18next"; import { @@ -10,7 +12,7 @@ import { ExternalLink, Gem, GitFork, - LayoutPanelLeft, + GitPullRequest, Laptop, Lock, Merge, @@ -19,16 +21,17 @@ import { Smartphone, Terminal, Users, - Workflow, type LucideIcon, } from "lucide-react"; import { motion, AnimatePresence, useInView, + useReducedMotion, useScroll, useTransform, type Transition, + type Variants, } from "framer-motion"; // Shared motion presets — hoisted so every JSX site receives the same object @@ -86,7 +89,14 @@ import { PiIcon, } from "~/components/agent-icons"; import { DiscordIcon, GitHubIcon, SlackIcon } from "~/components/brand-icons"; -import { MobileChat, MobileDiff, MobileSidebar, PhoneFrame } from "~/components/mockup"; +import { + MobileChat, + MobileDiff, + MobileSidebar, + PhoneFrame, + type ToolCall, + ToolCallRow, +} from "~/components/mockup"; import { FAQItem } from "~/components/faq-item"; import { SiteFooter } from "~/components/site-footer"; import { SponsorSection, SponsorsSection } from "~/components/sponsorship"; @@ -132,8 +142,8 @@ export function LandingPage({ title, subtitle }: LandingPageProps) { - + @@ -703,41 +713,50 @@ function AutomationDetail({ kind }: { kind: AutomationKind }) { ); } +const MCP_AGENT_CALLS: ToolCall[] = [ + { kind: "paseo", label: "Create agent", detail: "Fix #412: retry flaky relay handshake" }, + { kind: "paseo", label: "Create agent", detail: "Fix #417: group schedules by project" }, +]; + function McpAutomationTranscript() { const { t } = useTranslation(); + return ( + + {t("landing.automation.transcriptPlan")} + + {t("landing.automation.transcriptDone")} + + ); +} + +/** A user message and the agent's reply, laid out like the app's timeline. */ +function AgentTurn({ request, children }: { request: string; children: React.ReactNode }) { return (
-
-

- {t("landing.automation.transcriptRequest")} -

-
-
- -
-

- {t("landing.automation.transcriptPlan")} -

-
- - -
-

- {t("landing.automation.transcriptDone")} -

-
-
+ {request} +
{children}
); } -function McpAgentCall({ issue, provider }: { issue: string; provider: string }) { +function UserMessage({ children }: { children: React.ReactNode }) { return ( -
- create_agent - {issue} - {provider} - worktree +
+

{children}

+
+ ); +} + +function AgentText({ children }: { children: React.ReactNode }) { + return

{children}

; +} + +function ToolCalls({ calls }: { calls: ReadonlyArray }) { + return ( +
+ {calls.map((call) => ( + + ))}
); } @@ -842,17 +861,16 @@ function ExtensibleSection() { title={t("landing.plugins.title")} description={t("landing.plugins.description")} > -
- -
- {PLUGIN_CAPABILITIES.map((capability) => ( - - ))} +
+ +
+ +

{t("landing.plugins.license")}

@@ -872,6 +890,156 @@ function ExtensibleSection() { ); } +const PLUGIN_BUILD_CALLS: ToolCall[] = [ + { kind: "read", detail: "paseo-plugin/SKILL.md" }, + { kind: "shell", detail: "paseo plugin init review-queue" }, + { kind: "write", detail: "review-queue/index.ts" }, + { kind: "write", detail: "review-queue/review-queue.client.tsx" }, + { kind: "shell", detail: "paseo plugin install ./review-queue" }, +]; + +// The demo plays once when it scrolls into view: the prompt, then each timeline +// item in turn, then the panel the agent built slides in and fills. +const DEMO_STEP_SECONDS = 0.45; +const DEMO_STEPS = { + prompt: 0, + plan: 1.4, + firstCall: 2.2, +} as const; +const DEMO_DONE_STEP = DEMO_STEPS.firstCall + PLUGIN_BUILD_CALLS.length; +const DEMO_PANEL_STEP = DEMO_DONE_STEP + 1; + +const DEMO_ITEM_VARIANTS: Variants = { + hidden: { opacity: 0, y: 6 }, + shown: (step: number) => ({ + opacity: 1, + y: 0, + transition: { delay: step * DEMO_STEP_SECONDS, duration: 0.35, ease: "easeOut" }, + }), +}; +const DEMO_PANEL_VARIANTS: Variants = { + hidden: { opacity: 0, x: "100%" }, + shown: { + opacity: 1, + x: 0, + transition: { + delay: DEMO_PANEL_STEP * DEMO_STEP_SECONDS, + duration: 0.6, + ease: [0.22, 0.61, 0.36, 1], + }, + }, +}; + +/** One piece of the demo that fades in at its step. */ +function DemoItem({ step, children }: { step: number; children: React.ReactNode }) { + return ( + + {children} + + ); +} + +/** The ask on the left, the panel it produced on the right. */ +function SelfModifyDemo() { + const { t } = useTranslation(); + // Visitors who ask for reduced motion get the finished demo, with no slide or staging. + const reduceMotion = useReducedMotion(); + return ( + +
+ + {t("landing.plugins.request")} + +
+ + {t("landing.plugins.plan")} + +
+ {PLUGIN_BUILD_CALLS.map((call, index) => ( + + + + ))} +
+ + {t("landing.plugins.done")} + +
+
+ +
+ ); +} + +const REVIEW_QUEUE = [ + { number: "#418", title: "Retry flaky relay handshake", author: "ana", age: "12m" }, + { number: "#415", title: "Group schedules by project", author: "kenji", age: "1h" }, + { number: "#409", title: "Add Gruvbox theme", author: "lea", age: "3h" }, +] as const; + +/** The panel the agent just built, docked like a real sidebar panel. */ +function ReviewQueuePreview() { + const { t } = useTranslation(); + return ( + +
+ + {t("landing.plugins.panelTitle")} + + {REVIEW_QUEUE.length} + +
+
+ {REVIEW_QUEUE.map((pr, index) => ( + +
+

{pr.title}

+

+ {pr.number} · {pr.author} · {pr.age} +

+
+
+ ))} +
+
+ ); +} + +const SHOWCASE_CATEGORIES = [ + "themes", + "sidebar", + "workspaces", + "providers", + "orchestration", + "git", +] as const satisfies ReadonlyArray; + +function PluginCategoryLinks() { + const { t } = useTranslation(); + const localize = useLocalizePath(); + return ( +
+ {SHOWCASE_CATEGORIES.map((category) => ( + + {t(`plugins.categories.${category}.label`)} + + ))} +
+ ); +} + const PLUGINS_BROWSE = PLUGINS_LINKED ? { href: "/plugins", external: false } : { href: "https://paseo.cafe", external: true }; @@ -899,12 +1067,6 @@ function PluginActions() { ); } -const PLUGIN_CAPABILITIES = [ - { icon: LayoutPanelLeft, key: "ui" }, - { icon: Workflow, key: "lifecycle" }, - { icon: Bot, key: "providers" }, -] as const; - const PRINCIPLES = [ { icon: Lock, key: "private" }, { icon: Compass, key: "independent" }, diff --git a/packages/website/src/components/mockup/chat.tsx b/packages/website/src/components/mockup/chat.tsx index f944839cf8..dc951a8517 100644 --- a/packages/website/src/components/mockup/chat.tsx +++ b/packages/website/src/components/mockup/chat.tsx @@ -1,14 +1,14 @@ import { AudioLines, - BookOpen, Brain, ChevronDown, CircleDot, Copy, - FilePenLine, + Eye, LoaderCircle, ListTodo, Mic, + Pencil, Plus, ShieldOff, Split, @@ -18,7 +18,7 @@ import { import type * as React from "react"; import { useTranslation } from "react-i18next"; import { DiffStat } from "./atoms"; -import { CodexIcon } from "./icons"; +import { CodexIcon, PaseoMark } from "./icons"; // Icons passed as props are hoisted so they are not rebuilt on every render. const MODEL_ICON = ; @@ -94,70 +94,62 @@ export function AgentTranscript() { ); } -interface PluginToolCall { - kind: "read" | "shell" | "write"; - label: string; - detail: string; +/** + * A collapsed tool call as the app's timeline shows it. Built-in tools carry a + * fixed label; Paseo MCP tools name their action, such as "Create agent". + */ +export type ToolCall = + | { kind: "read" | "write" | "shell"; detail: string } + | { kind: "paseo"; label: string; detail: string }; + +const BUILT_IN_LABELS = { read: "Read", write: "Write", shell: "Shell" } as const; + +function toolCallLabel(call: ToolCall): string { + return call.kind === "paseo" ? call.label : BUILT_IN_LABELS[call.kind]; } -const PLUGIN_TOOL_CALLS: PluginToolCall[] = [ - { kind: "read", label: "Read", detail: "paseo-plugin/SKILL.md" }, - { - kind: "shell", - label: "Shell", - detail: "paseo plugin init ~/dev/release-radar", - }, - { - kind: "write", - label: "Write", - detail: "~/dev/release-radar/index.ts", - }, - { - kind: "write", - label: "Write", - detail: "~/dev/release-radar/release-radar.client.tsx", - }, - { - kind: "shell", - label: "Shell", - detail: "npm install && npm run typecheck", - }, - { - kind: "shell", - label: "Shell", - detail: "paseo plugin install ~/dev/release-radar", - }, +const PLUGIN_TOOL_CALLS: ToolCall[] = [ + { kind: "read", detail: "paseo-plugin/SKILL.md" }, + { kind: "shell", detail: "paseo plugin init ~/dev/release-radar" }, + { kind: "write", detail: "~/dev/release-radar/index.ts" }, + { kind: "write", detail: "~/dev/release-radar/release-radar.client.tsx" }, + { kind: "shell", detail: "npm install && npm run typecheck" }, + { kind: "shell", detail: "paseo plugin install ~/dev/release-radar" }, ]; const PLUGIN_DISCOVERY_CALLS = PLUGIN_TOOL_CALLS.slice(0, 1); const PLUGIN_BUILD_CALLS = PLUGIN_TOOL_CALLS.slice(1, 4); const PLUGIN_INSTALL_CALLS = PLUGIN_TOOL_CALLS.slice(4); -function PluginToolIcon({ kind }: { kind: PluginToolCall["kind"] }) { - if (kind === "read") return ; - if (kind === "write") return ; +function ToolCallIcon({ kind }: { kind: ToolCall["kind"] }) { + if (kind === "read") return ; + if (kind === "write") return ; + if (kind === "paseo") return ; return ; } -function PluginToolRow({ call }: { call: PluginToolCall }) { +/** + * One tool call row, drawn like the app's collapsed timeline item + * (`packages/app/src/components/message.tsx`): icon, label, then the summary, + * all muted and in the UI font. + */ +export function ToolCallRow({ call }: { call: ToolCall }) { return ( -
- - - - {call.label} - - {call.detail} +
+ + + {toolCallLabel(call)} + {call.detail}
); } -function PluginToolGroup({ calls }: { calls: PluginToolCall[] }) { +function PluginToolGroup({ calls }: { calls: ToolCall[] }) { return (
{calls.map((call) => ( - + ))}
); diff --git a/packages/website/src/components/mockup/index.ts b/packages/website/src/components/mockup/index.ts index 71ffed8b8d..d07c4d1f56 100644 --- a/packages/website/src/components/mockup/index.ts +++ b/packages/website/src/components/mockup/index.ts @@ -1,4 +1,5 @@ export { ClaudeIcon, CodexIcon, SourceControlIcon } from "./icons"; +export { type ToolCall, ToolCallRow } from "./chat"; export { DESIGN_HEIGHT, DESIGN_WIDTH } from "./geometry"; export { PhoneFrame } from "./mobile/frame"; export { MobileChat } from "./mobile/chat"; diff --git a/packages/website/src/i18n/resources/en.json b/packages/website/src/i18n/resources/en.json index 055229ca47..3365a9fffa 100644 --- a/packages/website/src/i18n/resources/en.json +++ b/packages/website/src/i18n/resources/en.json @@ -375,19 +375,18 @@ "otherHostComment": "# Target another host" }, "plugins": { - "title": "Extend it with plugins", - "description": "Add new panels and functionality that work in desktop, web and mobile", - "lead": "Plugins", + "title": "Ask Paseo to change Paseo", + "description": "Agents in Paseo can change the whole UI without a restart, even on mobile.", + "request": "Add a sidebar panel with the PRs waiting on my review.", + "plan": "I'll build it as a local plugin and install it.", + "done": "Review queue is in your sidebar.", + "panelTitle": "Review queue", + "libraryTitle": "400+ community plugins", + "libraryDescription": "Built by developers on the same plugin API your agents use", "browse": "Browse plugins", "docs": "Plugin docs", "license": "Paseo is Apache 2.0.", - "fork": "Fork the repo", - "uiTitle": "UI", - "uiDescription": "Screens, workspace panels, sidebar items, slash commands, and themes", - "lifecycleTitle": "Agent lifecycle", - "lifecycleDescription": "Change config and MCP servers, answer permissions, and follow up", - "providersTitle": "Providers", - "providersDescription": "Add a coding agent as a provider, used like the built-in ones" + "fork": "Fork the repo" }, "philosophy": { "title": "Philosophy", @@ -447,10 +446,10 @@ }, "mockup": { "alt": "Paseo desktop app with coding agents, a conversation, and a code diff open side by side", - "build": "Build", - "review": "Review", - "ship": "Ship", - "extend": "Extend", + "build": "Parallel agents", + "review": "Code review", + "ship": "Pull requests", + "extend": "Plugins", "app": { "newWorkspace": "New workspace", "history": "History", diff --git a/packages/website/src/i18n/resources/zh-CN.json b/packages/website/src/i18n/resources/zh-CN.json index 048d351e4d..2cb1cf4f3f 100644 --- a/packages/website/src/i18n/resources/zh-CN.json +++ b/packages/website/src/i18n/resources/zh-CN.json @@ -375,19 +375,18 @@ "otherHostComment": "# 指定另一台主机" }, "plugins": { - "title": "用插件扩展", - "description": "添加新的面板和功能,在桌面端、网页端和移动端都能使用", - "lead": "插件", + "title": "让 Paseo 修改 Paseo", + "description": "Paseo 里的 Agent 可以修改整个界面,无需重启,在移动端也一样。", + "request": "在侧边栏加一个面板,列出等待我审查的 PR。", + "plan": "我会把它做成本地插件并安装。", + "done": "审查队列已出现在你的侧边栏。", + "panelTitle": "审查队列", + "libraryTitle": "400 多个社区插件", + "libraryDescription": "开发者用的是和你的 Agent 相同的插件 API", "browse": "浏览插件", "docs": "插件文档", "license": "Paseo 采用 Apache 2.0 许可证。", - "fork": "Fork 仓库", - "uiTitle": "界面", - "uiDescription": "页面、工作区面板、侧边栏项目、斜杠命令和主题", - "lifecycleTitle": "Agent 生命周期", - "lifecycleDescription": "修改配置和 MCP 服务器、响应权限请求、发送后续消息", - "providersTitle": "Provider", - "providersDescription": "把编程 Agent 添加为 Provider,用法与内置 Provider 相同" + "fork": "Fork 仓库" }, "philosophy": { "title": "理念", @@ -447,10 +446,10 @@ }, "mockup": { "alt": "Paseo 桌面应用,并排打开了编程 Agent、一段对话和一份代码 diff", - "build": "构建", - "review": "审查", - "ship": "发布", - "extend": "扩展", + "build": "并行 Agent", + "review": "代码审查", + "ship": "Pull Request", + "extend": "插件", "app": { "newWorkspace": "新建工作区", "history": "历史",