website: add /cloud design partners page and shared shell

- New /cloud page with signup form (email, name, company, role, message)
  posting to a Discord webhook via a TanStack Start server function on
  the Cloudflare Worker. Webhook URL read from DISCORD_WEBHOOK_URL secret.
- Extract SiteShell, SiteFooter, FAQItem from landing-page.
- Migrate /privacy, /download, /blog, /changelog, /cloud to SiteShell so
  they share header + footer with consistent width.
- Add /cloud to header and footer nav.
- Make /docs sidebar full-width; bump footer text from xs to sm.
- Add Cloudflare Workers types and ignore .dev.vars.
This commit is contained in:
Mohamed Boudra
2026-05-06 10:45:14 +07:00
parent ea4d6bb7ce
commit 0eacc98e17
16 changed files with 730 additions and 431 deletions
+2
View File
@@ -13,6 +13,8 @@ out/
.env.local
.env.test.local
.env*.local
.dev.vars
**/.dev.vars
# Logs
*.log
+84
View File
@@ -0,0 +1,84 @@
import { env } from "cloudflare:workers";
import { createServerFn } from "@tanstack/react-start";
export interface CloudSignupInput {
email: string;
name?: string;
company?: string;
role?: string;
message: string;
honeypot?: string;
}
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
function asString(v: unknown, max: number): string {
if (typeof v !== "string") throw new Error("expected string");
const trimmed = v.trim();
if (trimmed.length > max) throw new Error("field too long");
return trimmed;
}
function validate(raw: unknown): CloudSignupInput {
if (typeof raw !== "object" || raw === null) throw new Error("invalid input");
const r = raw as Record<string, unknown>;
const email = asString(r.email, 320);
if (!EMAIL_RE.test(email)) throw new Error("invalid email");
const message = asString(r.message, 4000);
if (message.length === 0) throw new Error("message required");
const name = r.name === undefined || r.name === "" ? undefined : asString(r.name, 200);
const company =
r.company === undefined || r.company === "" ? undefined : asString(r.company, 200);
const role = r.role === undefined || r.role === "" ? undefined : asString(r.role, 200);
const honeypot = typeof r.honeypot === "string" ? r.honeypot : "";
return { email, name, company, role, message, honeypot };
}
function truncate(s: string, max: number): string {
return s.length <= max ? s : `${s.slice(0, max - 1)}…`;
}
function buildEmbed(input: CloudSignupInput) {
const fields: Array<{ name: string; value: string; inline?: boolean }> = [
{ name: "Email", value: truncate(input.email, 1024), inline: true },
];
if (input.name) {
fields.push({ name: "Name", value: truncate(input.name, 1024), inline: true });
}
if (input.company) {
fields.push({ name: "Company", value: truncate(input.company, 1024), inline: true });
}
if (input.role) {
fields.push({ name: "Role", value: truncate(input.role, 1024), inline: true });
}
fields.push({ name: "Message", value: truncate(input.message, 1024) });
return {
title: "Paseo Cloud signup",
color: 0x5865f2,
fields,
timestamp: new Date().toISOString(),
};
}
export const submitCloudSignup = createServerFn({ method: "POST" })
.inputValidator(validate)
.handler(async ({ data }) => {
if (data.honeypot) return { ok: true };
const url = (env as { DISCORD_WEBHOOK_URL?: string }).DISCORD_WEBHOOK_URL;
if (!url) throw new Error("webhook not configured");
const res = await fetch(url, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ embeds: [buildEmbed(data)] }),
});
if (!res.ok) throw new Error(`webhook ${res.status}`);
return { ok: true };
});
@@ -0,0 +1,14 @@
import type { ReactNode } from "react";
export function FAQItem({ question, children }: { question: string; children: ReactNode }) {
return (
<details className="group">
<summary className="font-medium text-sm cursor-pointer list-none flex items-start gap-2 -ml-4">
<span className="font-mono text-white/40 flex-shrink-0 group-open:hidden">+</span>
<span className="font-mono text-white/40 flex-shrink-0 hidden group-open:inline">−</span>
{question}
</summary>
<div className="text-sm text-muted-foreground space-y-2 mt-2 prose">{children}</div>
</details>
);
}
@@ -48,6 +48,8 @@ import { useRelease } from "~/routes/__root";
import { Mic } from "lucide-react";
import { HeroMockup } from "~/components/hero-mockup";
import { ClaudeIcon } from "~/components/mockup";
import { FAQItem } from "~/components/faq-item";
import { SiteFooter } from "~/components/site-footer";
import { SiteHeader } from "~/components/site-header";
import "~/styles.css";
@@ -97,126 +99,7 @@ export function LandingPage({ title, subtitle }: LandingPageProps) {
<SponsorCTA />
</div>
</main>
<footer className="p-6 md:p-20 md:pt-0 max-w-5xl mx-auto">
<div className="border-t border-white/10 pt-8 pb-4 grid grid-cols-2 sm:grid-cols-4 gap-8 text-xs">
<div className="space-y-3">
<p className="text-white/60 font-medium">Product</p>
<div className="space-y-2">
<a
href="/blog"
className="block text-white/40 hover:text-white/60 transition-colors"
>
Blog
</a>
<a
href="/docs"
className="block text-white/40 hover:text-white/60 transition-colors"
>
Docs
</a>
<a
href="/changelog"
className="block text-white/40 hover:text-white/60 transition-colors"
>
Changelog
</a>
<a
href="/docs/cli"
className="block text-white/40 hover:text-white/60 transition-colors"
>
CLI
</a>
<a
href="/privacy"
className="block text-white/40 hover:text-white/60 transition-colors"
>
Privacy
</a>
</div>
</div>
<div className="space-y-3">
<p className="text-white/60 font-medium">Agents</p>
<div className="space-y-2">
<a
href="/claude-code"
className="block text-white/40 hover:text-white/60 transition-colors"
>
Claude Code
</a>
<a
href="/codex"
className="block text-white/40 hover:text-white/60 transition-colors"
>
Codex
</a>
<a
href="/opencode"
className="block text-white/40 hover:text-white/60 transition-colors"
>
OpenCode
</a>
</div>
</div>
<div className="space-y-3">
<p className="text-white/60 font-medium">Community</p>
<div className="space-y-2">
<a
href="https://discord.gg/jz8T2uahpH"
target="_blank"
rel="noopener noreferrer"
className="block text-white/40 hover:text-white/60 transition-colors"
>
Discord
</a>
<a
href="https://github.com/getpaseo/paseo"
target="_blank"
rel="noopener noreferrer"
className="block text-white/40 hover:text-white/60 transition-colors"
>
GitHub
</a>
</div>
</div>
<div className="space-y-3">
<p className="text-white/60 font-medium">Download</p>
<div className="space-y-2">
<a
href={appStoreUrl}
target="_blank"
rel="noopener noreferrer"
className="block text-white/40 hover:text-white/60 transition-colors"
>
App Store
</a>
<a
href={playStoreUrl}
target="_blank"
rel="noopener noreferrer"
className="block text-white/40 hover:text-white/60 transition-colors"
>
Google Play
</a>
<a
href="https://github.com/getpaseo/paseo/releases"
target="_blank"
rel="noopener noreferrer"
className="block text-white/40 hover:text-white/60 transition-colors"
>
Desktop
</a>
<a
href={webAppUrl}
target="_blank"
rel="noopener noreferrer"
className="block text-white/40 hover:text-white/60 transition-colors"
>
Web App
</a>
</div>
</div>
</div>
</footer>
<SiteFooter />
</div>
</CursorFieldProvider>
);
@@ -1685,16 +1568,3 @@ function SponsorCTA() {
</motion.div>
);
}
function FAQItem({ question, children }: { question: string; children: React.ReactNode }) {
return (
<details className="group">
<summary className="font-medium text-sm cursor-pointer list-none flex items-start gap-2 -ml-4">
<span className="font-mono text-white/40 flex-shrink-0 group-open:hidden">+</span>
<span className="font-mono text-white/40 flex-shrink-0 hidden group-open:inline">−</span>
{question}
</summary>
<div className="text-sm text-muted-foreground space-y-2 mt-2 prose">{children}</div>
</details>
);
}
@@ -0,0 +1,126 @@
import { appStoreUrl, playStoreUrl, webAppUrl } from "~/downloads";
interface SiteFooterProps {
width?: "default" | "prose";
}
export function SiteFooter({ width = "default" }: SiteFooterProps) {
const widthClasses =
width === "prose" ? "max-w-prose p-6 md:p-12 md:pt-0" : "max-w-5xl p-6 md:p-20 md:pt-0";
return (
<footer className={`${widthClasses} mx-auto`}>
<div className="border-t border-white/10 pt-8 pb-4 grid grid-cols-2 sm:grid-cols-4 gap-8 text-sm">
<div className="space-y-3">
<p className="text-white/60 font-medium">Product</p>
<div className="space-y-2">
<a href="/blog" className="block text-white/40 hover:text-white/60 transition-colors">
Blog
</a>
<a href="/docs" className="block text-white/40 hover:text-white/60 transition-colors">
Docs
</a>
<a
href="/changelog"
className="block text-white/40 hover:text-white/60 transition-colors"
>
Changelog
</a>
<a href="/cloud" className="block text-white/40 hover:text-white/60 transition-colors">
Cloud
</a>
<a
href="/docs/cli"
className="block text-white/40 hover:text-white/60 transition-colors"
>
CLI
</a>
<a
href="/privacy"
className="block text-white/40 hover:text-white/60 transition-colors"
>
Privacy
</a>
</div>
</div>
<div className="space-y-3">
<p className="text-white/60 font-medium">Agents</p>
<div className="space-y-2">
<a
href="/claude-code"
className="block text-white/40 hover:text-white/60 transition-colors"
>
Claude Code
</a>
<a href="/codex" className="block text-white/40 hover:text-white/60 transition-colors">
Codex
</a>
<a
href="/opencode"
className="block text-white/40 hover:text-white/60 transition-colors"
>
OpenCode
</a>
</div>
</div>
<div className="space-y-3">
<p className="text-white/60 font-medium">Community</p>
<div className="space-y-2">
<a
href="https://discord.gg/jz8T2uahpH"
target="_blank"
rel="noopener noreferrer"
className="block text-white/40 hover:text-white/60 transition-colors"
>
Discord
</a>
<a
href="https://github.com/getpaseo/paseo"
target="_blank"
rel="noopener noreferrer"
className="block text-white/40 hover:text-white/60 transition-colors"
>
GitHub
</a>
</div>
</div>
<div className="space-y-3">
<p className="text-white/60 font-medium">Download</p>
<div className="space-y-2">
<a
href={appStoreUrl}
target="_blank"
rel="noopener noreferrer"
className="block text-white/40 hover:text-white/60 transition-colors"
>
App Store
</a>
<a
href={playStoreUrl}
target="_blank"
rel="noopener noreferrer"
className="block text-white/40 hover:text-white/60 transition-colors"
>
Google Play
</a>
<a
href="https://github.com/getpaseo/paseo/releases"
target="_blank"
rel="noopener noreferrer"
className="block text-white/40 hover:text-white/60 transition-colors"
>
Desktop
</a>
<a
href={webAppUrl}
target="_blank"
rel="noopener noreferrer"
className="block text-white/40 hover:text-white/60 transition-colors"
>
Web App
</a>
</div>
</div>
</div>
</footer>
);
}
@@ -28,6 +28,12 @@ export function SiteHeader() {
>
Changelog
</a>
<a
href="/cloud"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
Cloud
</a>
<a
href="/download"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
@@ -0,0 +1,24 @@
import type { ReactNode } from "react";
import { SiteFooter } from "~/components/site-footer";
import { SiteHeader } from "~/components/site-header";
interface SiteShellProps {
children: ReactNode;
width?: "default" | "prose";
}
export function SiteShell({ children, width = "default" }: SiteShellProps) {
const mainClasses =
width === "prose" ? "max-w-prose p-6 md:p-12 mx-auto" : "max-w-5xl p-6 md:p-20 mx-auto";
return (
<div className="min-h-screen bg-background">
<main className={mainClasses}>
<div className="mb-12">
<SiteHeader />
</div>
{children}
</main>
<SiteFooter width={width} />
</div>
);
}
+21
View File
@@ -14,6 +14,7 @@ import { Route as OpencodeRouteImport } from "./routes/opencode";
import { Route as DownloadRouteImport } from "./routes/download";
import { Route as DocsRouteImport } from "./routes/docs";
import { Route as CodexRouteImport } from "./routes/codex";
import { Route as CloudRouteImport } from "./routes/cloud";
import { Route as ClaudeCodeRouteImport } from "./routes/claude-code";
import { Route as ChangelogRouteImport } from "./routes/changelog";
import { Route as BlogRouteImport } from "./routes/blog";
@@ -48,6 +49,11 @@ const CodexRoute = CodexRouteImport.update({
path: "/codex",
getParentRoute: () => rootRouteImport,
} as any);
const CloudRoute = CloudRouteImport.update({
id: "/cloud",
path: "/cloud",
getParentRoute: () => rootRouteImport,
} as any);
const ClaudeCodeRoute = ClaudeCodeRouteImport.update({
id: "/claude-code",
path: "/claude-code",
@@ -94,6 +100,7 @@ export interface FileRoutesByFullPath {
"/blog": typeof BlogRouteWithChildren;
"/changelog": typeof ChangelogRoute;
"/claude-code": typeof ClaudeCodeRoute;
"/cloud": typeof CloudRoute;
"/codex": typeof CodexRoute;
"/docs": typeof DocsRouteWithChildren;
"/download": typeof DownloadRoute;
@@ -108,6 +115,7 @@ export interface FileRoutesByTo {
"/": typeof IndexRoute;
"/changelog": typeof ChangelogRoute;
"/claude-code": typeof ClaudeCodeRoute;
"/cloud": typeof CloudRoute;
"/codex": typeof CodexRoute;
"/download": typeof DownloadRoute;
"/opencode": typeof OpencodeRoute;
@@ -123,6 +131,7 @@ export interface FileRoutesById {
"/blog": typeof BlogRouteWithChildren;
"/changelog": typeof ChangelogRoute;
"/claude-code": typeof ClaudeCodeRoute;
"/cloud": typeof CloudRoute;
"/codex": typeof CodexRoute;
"/docs": typeof DocsRouteWithChildren;
"/download": typeof DownloadRoute;
@@ -140,6 +149,7 @@ export interface FileRouteTypes {
| "/blog"
| "/changelog"
| "/claude-code"
| "/cloud"
| "/codex"
| "/docs"
| "/download"
@@ -154,6 +164,7 @@ export interface FileRouteTypes {
| "/"
| "/changelog"
| "/claude-code"
| "/cloud"
| "/codex"
| "/download"
| "/opencode"
@@ -168,6 +179,7 @@ export interface FileRouteTypes {
| "/blog"
| "/changelog"
| "/claude-code"
| "/cloud"
| "/codex"
| "/docs"
| "/download"
@@ -184,6 +196,7 @@ export interface RootRouteChildren {
BlogRoute: typeof BlogRouteWithChildren;
ChangelogRoute: typeof ChangelogRoute;
ClaudeCodeRoute: typeof ClaudeCodeRoute;
CloudRoute: typeof CloudRoute;
CodexRoute: typeof CodexRoute;
DocsRoute: typeof DocsRouteWithChildren;
DownloadRoute: typeof DownloadRoute;
@@ -228,6 +241,13 @@ declare module "@tanstack/react-router" {
preLoaderRoute: typeof CodexRouteImport;
parentRoute: typeof rootRouteImport;
};
"/cloud": {
id: "/cloud";
path: "/cloud";
fullPath: "/cloud";
preLoaderRoute: typeof CloudRouteImport;
parentRoute: typeof rootRouteImport;
};
"/claude-code": {
id: "/claude-code";
path: "/claude-code";
@@ -316,6 +336,7 @@ const rootRouteChildren: RootRouteChildren = {
BlogRoute: BlogRouteWithChildren,
ChangelogRoute: ChangelogRoute,
ClaudeCodeRoute: ClaudeCodeRoute,
CloudRoute: CloudRoute,
CodexRoute: CodexRoute,
DocsRoute: DocsRouteWithChildren,
DownloadRoute: DownloadRoute,
+4 -9
View File
@@ -1,5 +1,5 @@
import { createFileRoute, Outlet } from "@tanstack/react-router";
import { SiteHeader } from "~/components/site-header";
import { SiteShell } from "~/components/site-shell";
export const Route = createFileRoute("/blog")({
component: BlogLayout,
@@ -7,13 +7,8 @@ export const Route = createFileRoute("/blog")({
function BlogLayout() {
return (
<div className="min-h-screen bg-background">
<div className="max-w-prose mx-auto p-6 md:p-12">
<div className="mb-8">
<SiteHeader />
</div>
<Outlet />
</div>
</div>
<SiteShell>
<Outlet />
</SiteShell>
);
}
+6 -11
View File
@@ -1,8 +1,8 @@
import { createFileRoute } from "@tanstack/react-router";
import ReactMarkdown from "react-markdown";
import changelogMarkdown from "../../../../CHANGELOG.md?raw";
import { SiteShell } from "~/components/site-shell";
import { pageMeta } from "~/meta";
import { SiteHeader } from "~/components/site-header";
export const Route = createFileRoute("/changelog")({
head: () => ({
@@ -16,15 +16,10 @@ export const Route = createFileRoute("/changelog")({
function Changelog() {
return (
<div className="min-h-screen bg-background">
<div className="max-w-4xl mx-auto p-6 md:p-12">
<div className="mb-8">
<SiteHeader />
</div>
<article className="changelog-markdown rounded-xl border border-border bg-card/40 p-6 md:p-8">
<ReactMarkdown>{changelogMarkdown}</ReactMarkdown>
</article>
</div>
</div>
<SiteShell>
<article className="changelog-markdown rounded-xl border border-border bg-card/40 p-6 md:p-8">
<ReactMarkdown>{changelogMarkdown}</ReactMarkdown>
</article>
</SiteShell>
);
}
+216
View File
@@ -0,0 +1,216 @@
import { createFileRoute } from "@tanstack/react-router";
import { useCallback, useState, type FormEvent } from "react";
import { submitCloudSignup, type CloudSignupInput } from "~/cloud-signup";
import { FAQItem } from "~/components/faq-item";
import { SiteShell } from "~/components/site-shell";
import { pageMeta } from "~/meta";
export const Route = createFileRoute("/cloud")({
head: () => ({
meta: pageMeta(
"Paseo Cloud - Design Partners",
"Paseo across machines, with a team, or inside a company. Looking for design partners.",
),
}),
component: Cloud,
});
const INPUT_CLASS =
"block w-full rounded-md bg-white/5 border border-white/10 px-3.5 py-2.5 text-sm text-white placeholder:text-white/30 focus:outline-none focus:border-white/30 transition-colors";
type Status = "idle" | "submitting" | "success" | "error";
function Cloud() {
return (
<SiteShell>
<h1 className="text-3xl font-medium mb-3">Paseo Cloud</h1>
<p className="text-white/70 leading-relaxed mb-10">
For using Paseo across machines, with a team, or inside a company. Looking for design
partners.
</p>
<div className="space-y-20">
<SignupForm />
<FaqSection />
</div>
</SiteShell>
);
}
function SignupForm() {
const [status, setStatus] = useState<Status>("idle");
const [error, setError] = useState<string | null>(null);
const handleSubmit = useCallback(async (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
setStatus("submitting");
setError(null);
const form = new FormData(event.currentTarget);
const data: CloudSignupInput = {
email: String(form.get("email") ?? ""),
name: form.get("name") ? String(form.get("name")) : undefined,
company: form.get("company") ? String(form.get("company")) : undefined,
role: form.get("role") ? String(form.get("role")) : undefined,
message: String(form.get("message") ?? ""),
honeypot: form.get("website") ? String(form.get("website")) : "",
};
try {
await submitCloudSignup({ data });
setStatus("success");
} catch (err) {
setStatus("error");
setError(err instanceof Error ? err.message : "Something went wrong.");
}
}, []);
if (status === "success") {
return (
<section className="space-y-3">
<p className="text-white/70">
Got it. I&apos;ll be in touch. If you don&apos;t hear back within a week, ping me on{" "}
<a
href="https://discord.gg/jz8T2uahpH"
target="_blank"
rel="noopener noreferrer"
className="underline hover:text-white/80"
>
Discord
</a>
.
</p>
</section>
);
}
const submitting = status === "submitting";
return (
<form onSubmit={handleSubmit} className="space-y-6" noValidate>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
<Field label="Email" required>
<input type="email" name="email" required autoComplete="email" className={INPUT_CLASS} />
</Field>
<Field label="Name">
<input type="text" name="name" autoComplete="name" className={INPUT_CLASS} />
</Field>
<Field label="Company">
<input type="text" name="company" autoComplete="organization" className={INPUT_CLASS} />
</Field>
<Field label="Role">
<input
type="text"
name="role"
autoComplete="organization-title"
className={INPUT_CLASS}
/>
</Field>
</div>
<Field label="Message" required>
<textarea
name="message"
required
rows={5}
placeholder="A bit about you and what you'd want from Paseo Cloud."
className={`${INPUT_CLASS} resize-y`}
/>
</Field>
<input
type="text"
name="website"
tabIndex={-1}
autoComplete="off"
aria-hidden="true"
className="absolute left-[-9999px] w-px h-px opacity-0"
/>
{error && (
<p className="text-sm text-red-400">
{error === "webhook not configured"
? "The form isn't wired up yet. Try Discord for now."
: "Something went wrong. Try again or DM me on Discord."}
</p>
)}
<div className="flex items-center gap-4 pt-4">
<button
type="submit"
disabled={submitting}
className="rounded-md bg-white text-black px-4 py-2 text-sm font-medium hover:bg-white/90 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{submitting ? "Sending…" : "Send"}
</button>
<p className="text-sm text-white/50">
Or{" "}
<a
href="https://discord.gg/jz8T2uahpH"
target="_blank"
rel="noopener noreferrer"
className="underline hover:text-white/80"
>
DM me on Discord
</a>
.
</p>
</div>
</form>
);
}
function Field({
label,
required,
children,
}: {
label: string;
required?: boolean;
children: React.ReactNode;
}) {
return (
<label className="block">
<span className="block text-sm font-medium text-white/80 pb-2">
{label}
{required && <span className="text-white/40"> *</span>}
</span>
{children}
</label>
);
}
function FaqSection() {
return (
<section className="space-y-6">
<h2 className="text-3xl font-medium">FAQ</h2>
<div className="space-y-6">
<FAQItem question="What is Paseo Cloud?">
An optional layer on top of Paseo for running daemons across machines, syncing config
between them, and using Paseo with a team or company. Think shared runners, permissions,
audit, managed daemons, and org controls.
</FAQItem>
<FAQItem question="Will Paseo stay free and open source?">
Yes. The whole stack stays free and open source: app, daemon, CLI, protocols, and the
Cloud control plane. Managed Cloud is the optional paid layer for people who don&apos;t
want to host it themselves.
</FAQItem>
<FAQItem question="Self-hosted or managed?">
Both. The control plane will live in the Paseo monorepo so you can host it yourself.
Managed is for people and teams who don&apos;t want to.
</FAQItem>
<FAQItem question="Does my code go through Paseo?">
No. Daemons run on your machines and talk to agent providers directly. Cloud handles
registration, config sync, permissions, and orchestration. Code and model traffic stay on
your machines.
</FAQItem>
<FAQItem question="When will it be available?">
Early access for design partners now. No public date. The app and daemon come first.
</FAQItem>
<FAQItem question="How does pricing work?">
Not set yet. Design partners help shape it.
</FAQItem>
</div>
</section>
);
}
+2 -2
View File
@@ -39,14 +39,14 @@ function DocsLayout() {
</nav>
</header>
<div className="max-w-5xl mx-auto flex">
<div className="flex">
{/* Desktop sidebar */}
<aside className="hidden md:block w-56 shrink-0 border-r border-border p-6 sticky top-0 h-screen">
<Link to="/" className="flex items-center gap-3 mb-8">
<img src="/logo.svg" alt="Paseo" className="w-6 h-6" />
<span className="text-lg font-medium">Paseo</span>
</Link>
<nav className="space-y-1">
<nav className="space-y-1 -ml-3">
{navigation.map((item) => (
<Link
key={item.href}
+152 -207
View File
@@ -1,5 +1,6 @@
import { Link, createFileRoute } from "@tanstack/react-router";
import { createFileRoute } from "@tanstack/react-router";
import { CodeBlock } from "~/components/code-block";
import { SiteShell } from "~/components/site-shell";
import { pageMeta } from "~/meta";
import {
downloadUrls,
@@ -32,232 +33,176 @@ function Download() {
const urls = downloadUrls(release);
return (
<div className="min-h-screen bg-background">
<div className="max-w-3xl mx-auto p-6 md:p-12">
<header className="flex items-center justify-between gap-4 mb-12">
<Link to="/" className="flex items-center gap-3">
<img src="/logo.svg" alt="Paseo" className="w-6 h-6" />
<span className="text-lg font-medium">Paseo</span>
</Link>
<div className="flex items-center gap-4">
<Link
to="/docs"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
Docs
</Link>
<Link
to="/changelog"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
Changelog
</Link>
<a
href="https://discord.gg/jz8T2uahpH"
target="_blank"
rel="noopener noreferrer"
aria-label="Discord"
className="text-muted-foreground hover:text-foreground transition-colors inline-flex items-center"
>
<svg
role="img"
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z" />
</svg>
</a>
<a
href="https://github.com/getpaseo/paseo"
target="_blank"
rel="noopener noreferrer"
aria-label="GitHub"
className="text-muted-foreground hover:text-foreground transition-colors inline-flex items-center"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M12 0C5.37 0 0 5.484 0 12.252c0 5.418 3.438 10.013 8.205 11.637.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.738-4.042-1.61-4.042-1.61-.546-1.403-1.333-1.776-1.333-1.776-1.089-.756.084-.741.084-.741 1.205.087 1.838 1.262 1.838 1.262 1.07 1.87 2.809 1.33 3.495 1.017.108-.79.417-1.33.76-1.636-2.665-.31-5.467-1.35-5.467-6.005 0-1.327.465-2.413 1.235-3.262-.124-.31-.535-1.556.117-3.243 0 0 1.008-.33 3.3 1.248a11.2 11.2 0 0 1 3.003-.404c1.02.005 2.045.138 3.003.404 2.29-1.578 3.297-1.248 3.297-1.248.653 1.687.242 2.933.118 3.243.77.85 1.233 1.935 1.233 3.262 0 4.667-2.807 5.692-5.48 5.995.43.38.823 1.133.823 2.285 0 1.65-.015 2.98-.015 3.386 0 .315.218.694.825.576C20.565 22.26 24 17.667 24 12.252 24 5.484 18.627 0 12 0z" />
</svg>
</a>
<SiteShell>
<h1 className="text-3xl md:text-4xl font-semibold tracking-tight mb-2">Download</h1>
<p className="text-muted-foreground mb-10">v{version}</p>
{/* Desktop */}
<section className="rounded-xl border border-border bg-card/40 p-6 md:p-8 mb-6">
<div className="flex items-start justify-between mb-8">
<div>
<h2 className="text-2xl font-semibold">Desktop</h2>
<p className="text-sm text-muted-foreground mt-1">
Recommended, bundles everything you need
</p>
</div>
</header>
<MonitorIcon className="h-5 w-5 text-muted-foreground mt-1.5" />
</div>
<h1 className="text-3xl md:text-4xl font-semibold tracking-tight mb-2">Download</h1>
<p className="text-muted-foreground mb-10">v{version}</p>
{/* Desktop */}
<section className="rounded-xl border border-border bg-card/40 p-6 md:p-8 mb-6">
<div className="flex items-start justify-between mb-8">
<div>
<h2 className="text-2xl font-semibold">Desktop</h2>
<p className="text-sm text-muted-foreground mt-1">Recommended, bundles the server</p>
<div className="divide-y divide-border">
{/* macOS */}
<div className="flex flex-col gap-3 py-5 first:pt-0 last:pb-0 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3">
<AppleIcon className="h-5 w-5 text-foreground" />
<span className="font-medium">macOS</span>
</div>
<div className="flex flex-wrap items-center gap-2">
<DownloadPill href={urls.macAppleSilicon} label="Apple Silicon" />
<DownloadPill href={urls.macIntel} label="Intel" />
</div>
<MonitorIcon className="h-5 w-5 text-muted-foreground mt-1.5" />
</div>
<div className="divide-y divide-border">
{/* macOS */}
<div className="flex flex-col gap-3 py-5 first:pt-0 last:pb-0 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3">
<AppleIcon className="h-5 w-5 text-foreground" />
<span className="font-medium">macOS</span>
</div>
<div className="flex flex-wrap items-center gap-2">
<DownloadPill href={urls.macAppleSilicon} label="Apple Silicon" />
<DownloadPill href={urls.macIntel} label="Intel" />
</div>
{/* Homebrew */}
<div className="flex flex-col gap-3 py-5 first:pt-0 last:pb-0 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3">
<TerminalIcon className="h-5 w-5 text-foreground" />
<span className="font-medium">Homebrew</span>
</div>
{/* Homebrew */}
<div className="flex flex-col gap-3 py-5 first:pt-0 last:pb-0 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3">
<TerminalIcon className="h-5 w-5 text-foreground" />
<span className="font-medium">Homebrew</span>
</div>
<CodeBlock size="sm">brew install --cask paseo</CodeBlock>
</div>
{/* Windows */}
<div className="flex flex-col gap-3 py-5 first:pt-0 last:pb-0 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3">
<WindowsIcon className="h-5 w-5 text-foreground" />
<span className="font-medium">Windows</span>
</div>
<div className="flex flex-wrap gap-2">
<DownloadPill
href={urls.windowsExeX64}
label={urls.windowsExeArm64 ? "Intel / x64" : "Download"}
/>
{urls.windowsExeArm64 && <DownloadPill href={urls.windowsExeArm64} label="ARM64" />}
</div>
</div>
{/* Linux */}
<div className="flex flex-col gap-3 py-5 first:pt-0 last:pb-0 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3">
<LinuxIcon className="h-5 w-5 text-foreground" />
<span className="font-medium">Linux</span>
</div>
<div className="flex flex-wrap gap-2">
<DownloadPill href={urls.linuxAppImage} label="AppImage" />
<DownloadPill href={urls.linuxDeb} label="DEB" />
<DownloadPill href={urls.linuxRpm} label="RPM" />
</div>
</div>
</div>
</section>
{/* Mobile */}
<section className="rounded-xl border border-border bg-card/40 p-6 md:p-8 mb-6">
<div className="flex items-center justify-between mb-8">
<h2 className="text-2xl font-semibold">Mobile</h2>
<PhoneIcon className="h-5 w-5 text-muted-foreground" />
<CodeBlock size="sm">brew install --cask paseo</CodeBlock>
</div>
<div className="divide-y divide-border">
{/* Android */}
<div className="flex flex-col gap-3 py-5 first:pt-0 last:pb-0 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3">
<AndroidIcon className="h-5 w-5 text-foreground" />
<span className="font-medium">Android</span>
</div>
<div className="flex flex-wrap gap-2">
<DownloadPill href={playStoreUrl} label="Play Store" external />
<DownloadPill href={urls.androidApk} label="APK" />
</div>
{/* Windows */}
<div className="flex flex-col gap-3 py-5 first:pt-0 last:pb-0 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3">
<WindowsIcon className="h-5 w-5 text-foreground" />
<span className="font-medium">Windows</span>
</div>
{/* iOS */}
<div className="flex flex-col gap-3 py-5 first:pt-0 last:pb-0 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3">
<AppleIcon className="h-5 w-5 text-foreground" />
<span className="font-medium">iOS</span>
</div>
<div className="flex flex-wrap gap-2">
<DownloadPill href={appStoreUrl} label="App Store" external />
</div>
<div className="flex flex-wrap gap-2">
<DownloadPill
href={urls.windowsExeX64}
label={urls.windowsExeArm64 ? "Intel / x64" : "Download"}
/>
{urls.windowsExeArm64 && <DownloadPill href={urls.windowsExeArm64} label="ARM64" />}
</div>
</div>
</section>
{/* Web */}
<section className="rounded-xl border border-border bg-card/40 p-6 md:p-8 mb-6">
<div className="flex items-start justify-between mb-8">
<div>
<h2 className="text-2xl font-semibold">Web</h2>
<p className="text-sm text-muted-foreground mt-1">
Connect to a server from any browser
</p>
{/* Linux */}
<div className="flex flex-col gap-3 py-5 first:pt-0 last:pb-0 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3">
<LinuxIcon className="h-5 w-5 text-foreground" />
<span className="font-medium">Linux</span>
</div>
<GlobeIcon className="h-5 w-5 text-muted-foreground mt-1.5" />
</div>
<div className="divide-y divide-border">
<div className="flex flex-col gap-3 py-5 first:pt-0 last:pb-0 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3">
<GlobeIcon className="h-5 w-5 text-foreground" />
<span className="font-medium">Web App</span>
</div>
<div className="flex flex-wrap gap-2">
<DownloadPill href={webAppUrl} label="Open" external />
</div>
<div className="flex flex-wrap gap-2">
<DownloadPill href={urls.linuxAppImage} label="AppImage" />
<DownloadPill href={urls.linuxDeb} label="DEB" />
<DownloadPill href={urls.linuxRpm} label="RPM" />
</div>
</div>
</section>
</div>
</section>
{/* Server */}
<section className="rounded-xl border border-border bg-card/40 p-6 md:p-8">
<div className="flex items-start justify-between mb-8">
<div>
<h2 className="text-2xl font-semibold">Server</h2>
<p className="text-sm text-muted-foreground mt-1">
Run the Paseo server anywhere, connect from any client
</p>
{/* Mobile */}
<section className="rounded-xl border border-border bg-card/40 p-6 md:p-8 mb-6">
<div className="flex items-center justify-between mb-8">
<h2 className="text-2xl font-semibold">Mobile</h2>
<PhoneIcon className="h-5 w-5 text-muted-foreground" />
</div>
<div className="divide-y divide-border">
{/* Android */}
<div className="flex flex-col gap-3 py-5 first:pt-0 last:pb-0 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3">
<AndroidIcon className="h-5 w-5 text-foreground" />
<span className="font-medium">Android</span>
</div>
<TerminalIcon className="h-5 w-5 text-muted-foreground mt-1.5" />
</div>
<div className="divide-y divide-border">
<div className="flex flex-col gap-3 py-5 first:pt-0 last:pb-0 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3">
<TerminalIcon className="h-5 w-5 text-foreground" />
<span className="font-medium">npm</span>
</div>
<CodeBlock size="sm">npm install -g @getpaseo/cli && paseo</CodeBlock>
</div>
<div className="flex flex-col gap-3 py-5 first:pt-0 last:pb-0 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3">
<TerminalIcon className="h-5 w-5 text-foreground" />
<span className="font-medium">Nix</span>
</div>
<CodeBlock size="sm">nix run github:getpaseo/paseo</CodeBlock>
<div className="flex flex-wrap gap-2">
<DownloadPill href={playStoreUrl} label="Play Store" external />
<DownloadPill href={urls.androidApk} label="APK" />
</div>
</div>
</section>
<p className="text-center text-xs text-muted-foreground mt-8">
All releases are available on{" "}
<a
href="https://github.com/getpaseo/paseo/releases"
target="_blank"
rel="noopener noreferrer"
className="underline hover:text-foreground transition-colors"
>
GitHub
</a>
.
</p>
</div>
</div>
{/* iOS */}
<div className="flex flex-col gap-3 py-5 first:pt-0 last:pb-0 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3">
<AppleIcon className="h-5 w-5 text-foreground" />
<span className="font-medium">iOS</span>
</div>
<div className="flex flex-wrap gap-2">
<DownloadPill href={appStoreUrl} label="App Store" external />
</div>
</div>
</div>
</section>
{/* Web */}
<section className="rounded-xl border border-border bg-card/40 p-6 md:p-8 mb-6">
<div className="flex items-start justify-between mb-8">
<div>
<h2 className="text-2xl font-semibold">Web</h2>
<p className="text-sm text-muted-foreground mt-1">
Connect to a server from any browser
</p>
</div>
<GlobeIcon className="h-5 w-5 text-muted-foreground mt-1.5" />
</div>
<div className="divide-y divide-border">
<div className="flex flex-col gap-3 py-5 first:pt-0 last:pb-0 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3">
<GlobeIcon className="h-5 w-5 text-foreground" />
<span className="font-medium">Web App</span>
</div>
<div className="flex flex-wrap gap-2">
<DownloadPill href={webAppUrl} label="Open" external />
</div>
</div>
</div>
</section>
{/* Server */}
<section className="rounded-xl border border-border bg-card/40 p-6 md:p-8">
<div className="flex items-start justify-between mb-8">
<div>
<h2 className="text-2xl font-semibold">Server</h2>
<p className="text-sm text-muted-foreground mt-1">
Run the Paseo server anywhere, connect from any client
</p>
</div>
<TerminalIcon className="h-5 w-5 text-muted-foreground mt-1.5" />
</div>
<div className="divide-y divide-border">
<div className="flex flex-col gap-3 py-5 first:pt-0 last:pb-0 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3">
<TerminalIcon className="h-5 w-5 text-foreground" />
<span className="font-medium">npm</span>
</div>
<CodeBlock size="sm">npm install -g @getpaseo/cli && paseo</CodeBlock>
</div>
<div className="flex flex-col gap-3 py-5 first:pt-0 last:pb-0 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3">
<TerminalIcon className="h-5 w-5 text-foreground" />
<span className="font-medium">Nix</span>
</div>
<CodeBlock size="sm">nix run github:getpaseo/paseo</CodeBlock>
</div>
</div>
</section>
<p className="text-center text-xs text-muted-foreground mt-8">
All releases are available on{" "}
<a
href="https://github.com/getpaseo/paseo/releases"
target="_blank"
rel="noopener noreferrer"
className="underline hover:text-foreground transition-colors"
>
GitHub
</a>
.
</p>
</SiteShell>
);
}
+68 -69
View File
@@ -1,4 +1,5 @@
import { createFileRoute } from "@tanstack/react-router";
import { SiteShell } from "~/components/site-shell";
import { pageMeta } from "~/meta";
export const Route = createFileRoute("/privacy")({
@@ -13,83 +14,81 @@ export const Route = createFileRoute("/privacy")({
function Privacy() {
return (
<div className="min-h-screen bg-background">
<div className="max-w-3xl mx-auto p-6 md:p-12">
<h1 className="text-3xl font-medium mb-8">Privacy Policy</h1>
<SiteShell>
<h1 className="text-3xl font-medium mb-8">Privacy Policy</h1>
<div className="space-y-6 text-white/70 leading-relaxed">
<div className="space-y-6 text-white/70 leading-relaxed">
<p>
Paseo is a self-hosted tool for managing coding agents. Your code and data stay on your
machine.
</p>
<section className="space-y-3">
<h2 className="text-xl font-medium text-white">What we collect</h2>
<p>Nothing. Paseo runs on your machine and doesn&apos;t send us any data.</p>
</section>
<section className="space-y-3">
<h2 className="text-xl font-medium text-white">The relay server</h2>
<p>
Paseo is a self-hosted tool for managing coding agents. Your code and data stay on your
machine.
If you use the optional encrypted relay to connect your phone to your daemon, the relay
sees:
</p>
<ul className="list-disc list-inside space-y-1 ml-4">
<li>IP addresses and connection timing</li>
<li>Message sizes</li>
<li>Session IDs</li>
</ul>
<p>
All messages between your phone and daemon are end-to-end encrypted with AES-256-GCM.
The relay cannot read your messages, see your code, or decrypt your traffic.
</p>
</section>
<section className="space-y-3">
<h2 className="text-xl font-medium text-white">What we collect</h2>
<p>Nothing. Paseo runs on your machine and doesn&apos;t send us any data.</p>
</section>
<section className="space-y-3">
<h2 className="text-xl font-medium text-white">Analytics and tracking</h2>
<p>
We don&apos;t use analytics, tracking pixels, cookies, or ads. The app doesn&apos;t
phone home.
</p>
</section>
<section className="space-y-3">
<h2 className="text-xl font-medium text-white">The relay server</h2>
<p>
If you use the optional encrypted relay to connect your phone to your daemon, the
relay sees:
</p>
<ul className="list-disc list-inside space-y-1 ml-4">
<li>IP addresses and connection timing</li>
<li>Message sizes</li>
<li>Session IDs</li>
</ul>
<p>
All messages between your phone and daemon are end-to-end encrypted with AES-256-GCM.
The relay cannot read your messages, see your code, or decrypt your traffic.
</p>
</section>
<section className="space-y-3">
<h2 className="text-xl font-medium text-white">Third-party services</h2>
<p>
Paseo wraps agent providers like Claude Code, Codex, and OpenCode. Those tools
communicate with their own APIs (Anthropic, OpenAI, etc.) using your credentials. Paseo
doesn&apos;t manage or intercept those API calls.
</p>
<p>
If you use voice features with cloud providers (OpenAI speech), your voice data is sent
to those services according to their privacy policies.
</p>
</section>
<section className="space-y-3">
<h2 className="text-xl font-medium text-white">Analytics and tracking</h2>
<p>
We don&apos;t use analytics, tracking pixels, cookies, or ads. The app doesn&apos;t
phone home.
</p>
</section>
<section className="space-y-3">
<h2 className="text-xl font-medium text-white">We don&apos;t sell your data</h2>
<p>We don&apos;t have your data to sell. Paseo is self-hosted and local-first.</p>
</section>
<section className="space-y-3">
<h2 className="text-xl font-medium text-white">Third-party services</h2>
<p>
Paseo wraps agent providers like Claude Code, Codex, and OpenCode. Those tools
communicate with their own APIs (Anthropic, OpenAI, etc.) using your credentials.
Paseo doesn&apos;t manage or intercept those API calls.
</p>
<p>
If you use voice features with cloud providers (OpenAI speech), your voice data is
sent to those services according to their privacy policies.
</p>
</section>
<section className="space-y-3">
<h2 className="text-xl font-medium text-white">Questions</h2>
<p>
If you have questions about privacy, open an issue on{" "}
<a
href="https://github.com/getpaseo/paseo"
target="_blank"
rel="noopener noreferrer"
className="underline hover:text-white/90"
>
GitHub
</a>
.
</p>
</section>
<section className="space-y-3">
<h2 className="text-xl font-medium text-white">We don&apos;t sell your data</h2>
<p>We don&apos;t have your data to sell. Paseo is self-hosted and local-first.</p>
</section>
<section className="space-y-3">
<h2 className="text-xl font-medium text-white">Questions</h2>
<p>
If you have questions about privacy, open an issue on{" "}
<a
href="https://github.com/getpaseo/paseo"
target="_blank"
rel="noopener noreferrer"
className="underline hover:text-white/90"
>
GitHub
</a>
.
</p>
</section>
<p className="text-sm text-white/50 pt-6">Last updated: February 2025</p>
</div>
<p className="text-sm text-white/50 pt-6">Last updated: February 2025</p>
</div>
</div>
</SiteShell>
);
}
+1
View File
@@ -11,6 +11,7 @@
"esModuleInterop": true,
"resolveJsonModule": true,
"isolatedModules": true,
"types": ["@cloudflare/workers-types"],
"paths": {
"~/*": ["./src/*"]
}
+1
View File
@@ -35,6 +35,7 @@ const sitemapPages = [
"/",
"/changelog",
"/claude-code",
"/cloud",
"/codex",
"/download",
"/opencode",