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": "历史",