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
+
+ );
+}
+
+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 (
+
+ );
+}
+
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": "历史",