Files
paseo/packages/website/src/routes/index.tsx
T

404 lines
16 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import * as React from 'react'
import { createFileRoute } from '@tanstack/react-router'
import { CursorFieldProvider } from '~/components/butterfly'
import '~/styles.css'
export const Route = createFileRoute('/')({
head: () => ({
meta: [
{ title: 'Paseo – Manage coding agents from your phone and desktop' },
{
name: 'description',
content:
'A self-hosted daemon for Claude Code, Codex, and OpenCode. Agents run on your machine with your full dev environment. Connect from phone, desktop, or web.',
},
],
}),
component: Home,
})
function Home() {
return (
<CursorFieldProvider>
{/* Hero section with background image */}
<div
className="relative bg-cover bg-center bg-no-repeat"
style={{ backgroundImage: 'url(/hero-bg.jpg)' }}
>
<div className="absolute inset-0 bg-background/90" />
<div className="absolute inset-x-0 bottom-0 h-64 bg-gradient-to-t from-black to-transparent" />
<div className="relative p-6 pb-10 md:px-20 md:pt-20 md:pb-12 max-w-3xl mx-auto">
<Nav />
<Hero />
<GetStarted />
</div>
{/* Mockup - inside hero so it's above the gradient, positioned to overflow into black section */}
<div className="relative px-6 md:px-8 pb-8 md:pb-16">
<div className="max-w-6xl mx-auto">
<img
src="/paseo-mockup.png"
alt="Paseo app showing agent management interface"
className="w-full rounded-lg shadow-2xl"
/>
</div>
</div>
</div>
{/* Content section */}
<div className="bg-black">
<main className="p-6 md:p-20 md:pt-8 max-w-3xl mx-auto">
<Features />
<Story />
<FAQ />
</main>
<footer className="p-6 md:p-20 md:pt-0 max-w-3xl mx-auto">
<div className="border-t border-white/10 pt-6">
<a
href="/privacy"
className="text-xs text-white/40 hover:text-white/60 transition-colors"
>
Privacy
</a>
</div>
</footer>
</div>
</CursorFieldProvider>
)
}
function Nav() {
return (
<nav className="flex items-center justify-between mb-16">
<div className="flex items-center gap-3">
<img src="/logo.svg" alt="Paseo" className="w-7 h-7" />
<span className="text-lg font-medium">Paseo</span>
</div>
<div className="flex items-center gap-4">
<a
href="/docs"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
Docs
</a>
<a
href="https://github.com/getpaseo/paseo"
target="_blank"
rel="noopener noreferrer"
className="text-sm text-muted-foreground hover:text-foreground transition-colors flex items-center gap-1.5"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
viewBox="0 -0.5 25 25"
>
<path d="m12.301 0h.093c2.242 0 4.34.613 6.137 1.68l-.055-.031c1.871 1.094 3.386 2.609 4.449 4.422l.031.058c1.04 1.769 1.654 3.896 1.654 6.166 0 5.406-3.483 10-8.327 11.658l-.087.026c-.063.02-.135.031-.209.031-.162 0-.312-.054-.433-.144l.002.001c-.128-.115-.208-.281-.208-.466 0-.005 0-.01 0-.014v.001q0-.048.008-1.226t.008-2.154c.007-.075.011-.161.011-.249 0-.792-.323-1.508-.844-2.025.618-.061 1.176-.163 1.718-.305l-.076.017c.573-.16 1.073-.373 1.537-.642l-.031.017c.508-.28.938-.636 1.292-1.058l.006-.007c.372-.476.663-1.036.84-1.645l.009-.035c.209-.683.329-1.468.329-2.281 0-.045 0-.091-.001-.136v.007c0-.022.001-.047.001-.072 0-1.248-.482-2.383-1.269-3.23l.003.003c.168-.44.265-.948.265-1.479 0-.649-.145-1.263-.404-1.814l.011.026c-.115-.022-.246-.035-.381-.035-.334 0-.649.078-.929.216l.012-.005c-.568.21-1.054.448-1.512.726l.038-.022-.609.384c-.922-.264-1.981-.416-3.075-.416s-2.153.152-3.157.436l.081-.02q-.256-.176-.681-.433c-.373-.214-.814-.421-1.272-.595l-.066-.022c-.293-.154-.64-.244-1.009-.244-.124 0-.246.01-.364.03l.013-.002c-.248.524-.393 1.139-.393 1.788 0 .531.097 1.04.275 1.509l-.01-.029c-.785.844-1.266 1.979-1.266 3.227 0 .025 0 .051.001.076v-.004c-.001.039-.001.084-.001.13 0 .809.12 1.591.344 2.327l-.015-.057c.189.643.476 1.202.85 1.693l-.009-.013c.354.435.782.793 1.267 1.062l.022.011c.432.252.933.465 1.46.614l.046.011c.466.125 1.024.227 1.595.284l.046.004c-.431.428-.718 1-.784 1.638l-.001.012c-.207.101-.448.183-.699.236l-.021.004c-.256.051-.549.08-.85.08-.022 0-.044 0-.066 0h.003c-.394-.008-.756-.136-1.055-.348l.006.004c-.371-.259-.671-.595-.881-.986l-.007-.015c-.198-.336-.459-.614-.768-.827l-.009-.006c-.225-.169-.49-.301-.776-.38l-.016-.004-.32-.048c-.023-.002-.05-.003-.077-.003-.14 0-.273.028-.394.077l.007-.003q-.128.072-.08.184c.039.086.087.16.145.225l-.001-.001c.061.072.13.135.205.19l.003.002.112.08c.283.148.516.354.693.603l.004.006c.191.237.359.505.494.792l.01.024.16.368c.135.402.38.738.7.981l.005.004c.3.234.662.402 1.057.478l.016.002c.33.064.714.104 1.106.112h.007c.045.002.097.002.15.002.261 0 .517-.021.767-.062l-.027.004.368-.064q0 .609.008 1.418t.008.873v.014c0 .185-.08.351-.208.466h-.001c-.119.089-.268.143-.431.143-.075 0-.147-.011-.214-.032l.005.001c-4.929-1.689-8.409-6.283-8.409-11.69 0-2.268.612-4.393 1.681-6.219l-.032.058c1.094-1.871 2.609-3.386 4.422-4.449l.058-.031c1.739-1.034 3.835-1.645 6.073-1.645h.098-.005zm-7.64 17.666q.048-.112-.112-.192-.16-.048-.208.032-.048.112.112.192.144.096.208-.032zm.497.545q.112-.08-.032-.256-.16-.144-.256-.048-.112.08.032.256.159.157.256.047zm.48.72q.144-.112 0-.304-.128-.208-.272-.096-.144.08 0 .288t.272.112zm.672.673q.128-.128-.064-.304-.192-.192-.32-.048-.144.128.064.304.192.192.32.044zm.913.4q.048-.176-.208-.256-.24-.064-.304.112t.208.24q.24.097.304-.096zm1.009.08q0-.208-.272-.176-.256 0-.256.176 0 .208.272.176.256.001.256-.175zm.929-.16q-.032-.176-.288-.144-.256.048-.224.24t.288.128.225-.224z" />
</svg>
GitHub
</a>
</div>
</nav>
)
}
function Hero() {
return (
<div className="space-y-6">
<h1 className="text-3xl md:text-5xl font-medium tracking-tight">
Run Codex and Claude Code locally from your phone and desktop
</h1>
<p className="text-white/70 text-lg leading-relaxed">
Bring your dev environment anywhere, with voice support built in.
</p>
</div>
)
}
function Differentiator({
title,
description,
}: {
title: string
description: string
}) {
return (
<div>
<p className="font-medium text-sm">{title}</p>
<p className="text-sm text-muted-foreground">{description}</p>
</div>
)
}
function Features() {
return (
<div className="space-y-6">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-6">
<Feature
title="Self-hosted"
description="The daemon runs on your laptop, home server, or VPS. Allowing you to take full advantage of your dev environment."
/>
<Feature
title="Multi-provider"
description="Works with existing agent harnesses like Claude Code, Codex, and OpenCode from one interface."
/>
<Feature
title="Multi-host"
description="Connect to multiple daemons and see all your agents in one place."
/>
<Feature
title="First-class voice"
description="Real-time voice conversations and dictation. Talk to your agent, hear responses, and orchestrate work hands-free."
/>
<Feature
title="Optional relay"
description="Use the hosted end-to-end encrypted relay for remote access, or connect directly over your network."
/>
<Feature
title="Cross-device"
description="Jump seamlessly between iOS, Android, desktop, web, and CLI."
/>
<Feature
title="Git integration"
description="Manage agents in isolated worktrees. Review diffs and ship directly from the app."
/>
<Feature
title="Open source"
description="Free and open source. Run it yourself, fork it, contribute."
/>
</div>
</div>
)
}
function Feature({
title,
description,
}: {
title: string
description: string
}) {
return (
<div className="space-y-1">
<p className="font-medium text-base">{title}</p>
<p className="text-sm text-white/60">{description}</p>
</div>
)
}
function GetStarted() {
return (
<div className="pt-10 space-y-4">
<CodeBlock>npm install -g @getpaseo/cli && paseo</CodeBlock>
<div className="flex flex-col sm:flex-row gap-3">
<a
href="https://github.com/getpaseo/paseo/releases/download/v0.1.2/Paseo_0.1.2_aarch64.dmg"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center gap-2 rounded-lg border border-white/20 px-4 py-2 text-sm font-medium text-white hover:bg-white/10 transition-colors"
>
<AppleIcon className="h-4 w-4" />
Download for Mac
</a>
<a
href="https://app.paseo.sh"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center gap-2 rounded-lg border border-white/20 px-4 py-2 text-sm font-medium text-white hover:bg-white/10 transition-colors"
>
<GlobeIcon className="h-4 w-4" />
Launch Web App
</a>
</div>
</div>
)
}
function AppleIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 1408 1664"
fill="currentColor"
aria-hidden="true"
{...props}
>
<path d="M1393 1215q-39 125-123 250q-129 196-257 196q-49 0-140-32q-86-32-151-32q-61 0-142 33q-81 34-132 34q-152 0-301-259Q0 1144 0 902q0-228 113-374q113-144 284-144q72 0 177 30q104 30 138 30q45 0 143-34q102-34 173-34q119 0 213 65q52 36 104 100q-79 67-114 118q-65 94-65 207q0 124 69 223t158 126M1017 42q0 61-29 136q-30 75-93 138q-54 54-108 72q-37 11-104 17q3-149 78-257Q835 41 1011 0q1 3 2.5 11t2.5 11q0 4 .5 10t.5 10" />
</svg>
)
}
function GlobeIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
{...props}
>
<circle cx="12" cy="12" r="9" />
<path d="M3 12h18" />
<path d="M12 3a15 15 0 0 1 0 18" />
<path d="M12 3a15 15 0 0 0 0 18" />
</svg>
)
}
function Step({
number,
children,
}: {
number: number
children: React.ReactNode
}) {
return (
<div className="flex gap-4">
<span className="flex-shrink-0 w-6 h-6 rounded-full bg-white/20 flex items-center justify-center text-xs font-medium">
{number}
</span>
<div className="space-y-2 flex-1">{children}</div>
</div>
)
}
function CodeBlock({ children }: { children: React.ReactNode }) {
const [copied, setCopied] = React.useState(false)
const text = typeof children === 'string' ? children : ''
function handleCopy() {
navigator.clipboard.writeText(text)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
return (
<div className="bg-black/30 backdrop-blur-sm rounded-lg p-3 md:p-4 font-mono text-sm flex items-center justify-between gap-2">
<div>
<span className="text-muted-foreground select-none">$ </span>
<span className="text-foreground">{children}</span>
</div>
<button
onClick={handleCopy}
className="text-muted-foreground hover:text-foreground transition-colors p-1"
title="Copy to clipboard"
>
{copied ? (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
viewBox="0 0 256 256"
>
<path d="M229.66,77.66l-128,128a8,8,0,0,1-11.32,0l-56-56a8,8,0,0,1,11.32-11.32L96,188.69,218.34,66.34a8,8,0,0,1,11.32,11.32Z" />
</svg>
) : (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
viewBox="0 0 256 256"
>
<path d="M216,28H88A20,20,0,0,0,68,48V76H40A20,20,0,0,0,20,96V216a20,20,0,0,0,20,20H168a20,20,0,0,0,20-20V188h28a20,20,0,0,0,20-20V48A20,20,0,0,0,216,28ZM164,212H44V100H164Zm48-48H188V96a20,20,0,0,0-20-20H92V52H212Z" />
</svg>
)}
</button>
</div>
)
}
function Story() {
return (
<div className="pt-16 space-y-4">
<h2 className="text-2xl font-medium">Background</h2>
<div className="space-y-4 text-sm text-white/60">
<p>
I started using Claude Code soon after it launched, often on my phone
while going on walks to spend less time at my desk. I'd SSH into Tmux
from my phone. It worked, but the UX was rough. Dictation was bad, the
virtual keyboard was awkward, and the TUI would randomly start
flickering, which forced me to start over very often.
</p>
<p>
I started building a simple app to manage agents via voice. I continued
adding features as I needed them, and it slowly turned into what Paseo
is today.
</p>
<p>
Anthropic and OpenAI added coding agents to their mobile apps since I
started working on this, but they force you into cloud sandboxes where
you lose your whole setup. I also like testing different agents, so
locking myself to a single harness or model wasn't an option.
</p>
</div>
</div>
)
}
function FAQ() {
return (
<div className="pt-16 space-y-6">
<h2 className="text-2xl font-medium">FAQ</h2>
<div className="space-y-6">
<FAQItem question="Is this free?">
Paseo is free and open source. It wraps CLI tools like Claude Code and
Codex, which you'll need to have installed and configured with your
own credentials. Voice is local-first by default and can optionally use
OpenAI speech providers if you configure them.
</FAQItem>
<FAQItem question="Does my code leave my machine?">
Paseo itself doesn't send your code anywhere. Agents run locally and
communicate with their own APIs as they normally would. We provide an
optional end-to-end encrypted relay for remote access, but you can
also connect directly over your local network or use your own tunnel.
</FAQItem>
<FAQItem question="What agents does it support?">
Claude Code, Codex, and OpenCode.
</FAQItem>
<FAQItem question="What's the business model?">There isn't one.</FAQItem>
<FAQItem question="Isn't this just more screen time?">
I won't pretend this can't be misused to squeeze every minute of your
day into work. But for me it means less time at my desk, not more. I
brainstorm whole features with voice. I kick off work at my desk, then
check in from my phone during a walk. I see what an agent needs, send
a voice reply, and put my phone away.
</FAQItem>
<FAQItem question="What does Paseo mean?">
Stroll, in Spanish. 🚶‍♂️
</FAQItem>
</div>
</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">
<span className="font-mono text-white/40 group-open:hidden">+</span>
<span className="font-mono text-white/40 hidden group-open:inline">
-
</span>
{question}
</summary>
<div className="text-sm text-white/60 space-y-2 mt-2 ml-4">
{children}
</div>
</details>
)
}