diff --git a/packages/website/src/components/landing-page.tsx b/packages/website/src/components/landing-page.tsx new file mode 100644 index 000000000..53b457483 --- /dev/null +++ b/packages/website/src/components/landing-page.tsx @@ -0,0 +1,1102 @@ +import * as React from 'react' +import { motion, AnimatePresence } from 'framer-motion' +import { CursorFieldProvider } from '~/components/butterfly' +import websitePackage from '../../package.json' +import '~/styles.css' + +const desktopVersion = websitePackage.version +const releaseBase = `https://github.com/getpaseo/paseo/releases/download/v${desktopVersion}` +const androidApkHref = `${releaseBase}/paseo-v${desktopVersion}-android.apk` + +type Platform = 'mac-silicon' | 'mac-intel' | 'windows' | 'linux' + +interface DownloadOption { + platform: Platform + label: string + href: string + icon: (props: React.SVGProps) => React.ReactElement +} + +const downloadOptions: DownloadOption[] = [ + { + platform: 'mac-silicon', + label: 'Mac', + href: `${releaseBase}/Paseo_${desktopVersion}_aarch64.dmg`, + icon: AppleIcon, + }, + { + platform: 'mac-intel', + label: 'Mac Intel', + href: `${releaseBase}/Paseo_${desktopVersion}_x86_64.dmg`, + icon: AppleIcon, + }, + { + platform: 'windows', + label: 'Windows', + href: `${releaseBase}/Paseo_${desktopVersion}_x64-setup.exe`, + icon: WindowsIcon, + }, + { + platform: 'linux', + label: 'Linux', + href: `${releaseBase}/Paseo_${desktopVersion}_amd64.AppImage`, + icon: LinuxIcon, + }, +] + +function useDetectedPlatform(): Platform { + const [platform, setPlatform] = React.useState('mac-silicon') + + React.useEffect(() => { + const ua = navigator.userAgent.toLowerCase() + if (ua.includes('win')) { + setPlatform('windows') + } else if (ua.includes('linux')) { + setPlatform('linux') + } else if (ua.includes('mac')) { + // Check for Apple Silicon vs Intel + // navigator.platform is deprecated but still the most reliable check + const isARM = /arm|aarch64/i.test(navigator.platform) || + // Chrome/Edge on Apple Silicon report x86 platform but have ARM in userAgentData + (navigator as unknown as { userAgentData?: { architecture?: string } }).userAgentData?.architecture === 'arm' + setPlatform(isARM ? 'mac-silicon' : 'mac-silicon') // Default to Silicon for modern Macs + } + }, []) + + return platform +} + +interface LandingPageProps { + title: string + subtitle: string +} + +export function LandingPage({ title, subtitle }: LandingPageProps) { + return ( + + {/* Hero section with background image */} +
+
+
+ +
+
+ + {/* Mockup - inside hero so it's above the gradient, positioned to overflow into black section */} + +
+ Paseo app showing agent management interface +
+
+
+ + {/* Content section */} +
+
+
+ + + +
+
+ +
+ + ) +} + +function Nav() { + return ( + + ) +} + +function Hero({ title, subtitle }: { title: string; subtitle: string }) { + return ( +
+ + {title} + + + {subtitle} + +
+ ) +} + +function AgentBadge({ name, icon }: { name: string; icon: React.ReactNode }) { + const [hovered, setHovered] = React.useState(false) + + return ( + setHovered(true)} + onMouseLeave={() => setHovered(false)} + > + {icon} + + {hovered && ( + + {name} + + )} + + + ) +} + +function Features() { + return ( +
+ {/* Primary differentiators - 2x2 */} +
+ {[ + { + title: 'Self-hosted', + description: 'Agents run on your machine with your full dev environment. Use your tools, your configs and your skills.', + icon: ( + + + + + + + ), + }, + { + title: 'Multi-provider', + description: 'Claude Code, Codex, and OpenCode through the same interface. Pick the right model for each job.', + icon: ( + + + + + + + ), + }, + { + title: 'Voice control', + description: 'Dictate tasks or talk through problems in voice mode. Hands-free when you need it.', + icon: ( + + + + + + ), + }, + { + title: 'Cross-device', + description: 'iOS, Android, desktop, web, and CLI. Start work at your desk, check in from your phone, script it from the terminal.', + icon: ( + + + + + ), + }, + ].map((feature, i) => ( + +
+ {feature.icon} +

{feature.title}

+
+

{feature.description}

+
+ ))} +
+ + {/* Also */} + + Multi-host + Built-in terminal + Git worktrees + E2E encrypted relay + Open source + +
+ ) +} + +function GetStarted() { + return ( + + +
+ Supports +
+ } /> + } /> + } /> +
+
+
+ ) +} + +function DownloadButton() { + const detectedPlatform = useDetectedPlatform() + const [open, setOpen] = React.useState(false) + const ref = React.useRef(null) + + React.useEffect(() => { + function handleClickOutside(e: MouseEvent) { + if (ref.current && !ref.current.contains(e.target as Node)) { + setOpen(false) + } + } + document.addEventListener('mousedown', handleClickOutside) + return () => document.removeEventListener('mousedown', handleClickOutside) + }, []) + + const primary = downloadOptions.find((o) => o.platform === detectedPlatform)! + const secondaryOptions = downloadOptions.filter((o) => o.platform !== detectedPlatform) + const PrimaryIcon = primary.icon + + return ( +
+
+ + + Download for {primary.label} + + +
+ + {open && ( + + {secondaryOptions.map((option) => { + const Icon = option.icon + return ( + setOpen(false)} + className="flex items-center gap-2 px-4 py-2 text-sm text-white/80 hover:bg-white/10 transition-colors" + > + + {option.label} + + ) + })} + + )} + +
+ ) +} + +function ServerInstallButton() { + const [open, setOpen] = React.useState(false) + const ref = React.useRef(null) + + React.useEffect(() => { + function handleClickOutside(e: MouseEvent) { + if (ref.current && !ref.current.contains(e.target as Node)) { + setOpen(false) + } + } + document.addEventListener('mousedown', handleClickOutside) + return () => document.removeEventListener('mousedown', handleClickOutside) + }, []) + + return ( +
+ + + {open && ( + <> + setOpen(false)} + /> + +
+

Run agents on a remote machine

+

+ For headless machines you want to connect to from the Paseo apps. The desktop app already includes a built-in daemon. +

+
+ npm install -g @getpaseo/cli && paseo +

+ Requires Node.js 18+. Run paseo to start the daemon. +

+
+ + )} +
+
+ ) +} + +function ClaudeCodeIcon(props: React.SVGProps) { + return ( + + ) +} + +function CodexIcon(props: React.SVGProps) { + return ( + + ) +} + +function OpenCodeIcon(props: React.SVGProps) { + return ( + + ) +} + +function AppleIcon(props: React.SVGProps) { + return ( + + ) +} + +function AndroidIcon(props: React.SVGProps) { + return ( + + ) +} + +function AppStoreIcon(props: React.SVGProps) { + return ( + + ) +} + +function WindowsIcon(props: React.SVGProps) { + return ( + + ) +} + +function LinuxIcon(props: React.SVGProps) { + return ( + + ) +} + +function TerminalIcon(props: React.SVGProps) { + return ( + + ) +} + +function ChevronDownIcon(props: React.SVGProps) { + return ( + + ) +} + +function GlobeIcon(props: React.SVGProps) { + return ( + + ) +} + +function Step({ number, children }: { number: number; children: React.ReactNode }) { + return ( +
+ + {number} + +
{children}
+
+ ) +} + +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 ( +
+
+ $ + {children} +
+ +
+ ) +} + +const bashKeywords = new Set(['while', 'do', 'done', 'if', 'then', 'fi', 'else', 'break', 'true', 'false']) +const bashCommands = new Set(['paseo', 'echo', 'jq']) + +function highlightBash(code: string): React.ReactNode { + const tokens: React.ReactNode[] = [] + let i = 0 + let key = 0 + + while (i < code.length) { + if (code[i] === '#' && (i === 0 || /[\s(]/.test(code[i - 1]))) { + const end = code.indexOf('\n', i) + const comment = end === -1 ? code.slice(i) : code.slice(i, end) + tokens.push({comment}) + i += comment.length + continue + } + + if (code[i] === '"') { + let j = i + 1 + while (j < code.length && code[j] !== '"') { + if (code[j] === '\\') j++ + j++ + } + const str = code.slice(i, j + 1) + tokens.push({str}) + i = j + 1 + continue + } + + if (code[i] === "'") { + let j = i + 1 + while (j < code.length && code[j] !== "'") j++ + const str = code.slice(i, j + 1) + tokens.push({str}) + i = j + 1 + continue + } + + if (code[i] === '$') { + if (code[i + 1] === '(') { + tokens.push($() + i += 2 + continue + } + let j = i + 1 + while (j < code.length && /\w/.test(code[j])) j++ + tokens.push({code.slice(i, j)}) + i = j + continue + } + + if (code[i] === '-' && (i === 0 || /\s/.test(code[i - 1])) && i + 1 < code.length && /[\w-]/.test(code[i + 1])) { + let j = i + if (code[j + 1] === '-') j++ + j++ + while (j < code.length && /[\w-]/.test(code[j])) j++ + tokens.push({code.slice(i, j)}) + i = j + continue + } + + if (/[a-zA-Z_]/.test(code[i])) { + let j = i + while (j < code.length && /\w/.test(code[j])) j++ + const word = code.slice(i, j) + if (bashKeywords.has(word)) { + tokens.push({word}) + } else if (bashCommands.has(word)) { + tokens.push({word}) + } else { + tokens.push(word) + key++ + } + i = j + continue + } + + if (code[i] === '|' || (code[i] === '&' && code[i + 1] === '&')) { + const op = code[i] === '|' ? '|' : '&&' + tokens.push({op}) + i += op.length + continue + } + + if (code[i] === '\\') { + tokens.push(\) + i++ + continue + } + + if (code[i] === ')') { + tokens.push()) + i++ + continue + } + + tokens.push(code[i]) + i++ + } + + return <>{tokens} +} + +function CLICodeBlock({ children }: { children: string }) { + const [copied, setCopied] = React.useState(false) + + function handleCopy() { + navigator.clipboard.writeText(children) + setCopied(true) + setTimeout(() => setCopied(false), 2000) + } + + return ( +
+ +
{highlightBash(children)}
+
+ ) +} + +interface CLIExample { + title: string + description: string + code: string +} + +const cliExamples: CLIExample[] = [ + { + title: 'Launch and monitor', + description: 'Give an agent a task and watch it work. The --worktree flag spins up an isolated git branch so you can run multiple agents on the same repo without conflicts.', + code: +`paseo run --provider claude/opus-4.6 "implement user authentication" +paseo run --provider codex/gpt-5.4 --worktree feature-x "implement feature X" + +paseo ls # list running agents +paseo attach abc123 # stream live output +paseo send abc123 "also add tests" # follow-up task`, + }, + { + title: 'Orchestration', + description: 'Agents can use the CLI too. Tell one agent to spawn others, split up the work, and pull everything together when they\'re done.', + code: +`# Spawn two agents in parallel, wait, then synthesize +paseo run --detach "implement the frontend" --name frontend +paseo run --detach "implement the API layer" --name api + +paseo wait frontend api + +paseo run "review both branches and write an integration plan"`, + }, + { + title: 'Structured output', + description: 'Pass a JSON schema and get typed data back from any agent run. No output parsing, just the structured result you asked for.', + code: +`result=$(paseo run \\ + --output-schema '{ + "type": "object", + "properties": { + "severity": { "type": "string", "enum": ["low", "medium", "high"] }, + "issues": { "type": "array", "items": { "type": "string" } } + }, + "required": ["severity", "issues"] + }' \\ + "audit this codebase for security issues") + +echo $result | jq '.severity' # "high" +echo $result | jq '.issues[0]' # "SQL injection on line 42"`, + }, + { + title: 'Remote', + description: 'Point at any daemon on your network or over the internet. Run agents on a beefy server from your laptop.', + code: +`# Set once for the session +export PASEO_HOST=workstation.local:6767 +paseo run "run the full test suite" +paseo ls + +# Or per-command +paseo --host gpu-server:6767 run "train the model" +paseo --host gpu-server:6767 attach abc123`, + }, + { + title: 'Worker-judge loops', + description: 'Have one agent do the work and another judge the result. Loop until it passes. Good for test fixes, code review, or any task with clear acceptance criteria.', + code: +`while true; do + paseo run "make all tests pass" + + verdict=$(paseo run \\ + --output-schema '{"type":"object","properties":{"passed":{"type":"boolean"}},"required":["passed"]}' \\ + "verify tests pass and the code is production-ready") + + echo "$verdict" | jq -e '.passed' && break +done`, + }, +] + +function CLISection() { + const [activeIndex, setActiveIndex] = React.useState(0) + const active = cliExamples[activeIndex] + + return ( + +
+

CLI

+

+ Everything you can do in the app, you can do from the terminal. +

+
+ +
+ {cliExamples.map((example, i) => ( + + ))} +
+ +
+

{active.description}

+ {active.code} +
+ + + Full CLI reference + + + + +
+ ) +} + +function FAQ() { + return ( + +

FAQ

+
+ + Yes. Paseo is free and open source. You need Claude Code, Codex, or OpenCode installed + with your own credentials. Voice is local-first by default and can optionally use OpenAI + speech providers if you configure them. + + + Paseo doesn't send your code anywhere. Agents run locally and talk to their own APIs + as they normally would. For remote access, you can use the optional{' '} + end-to-end encrypted relay, + connect directly over your local network, or use your own tunnel. + + + Claude Code, Codex, and OpenCode. Each agent runs as its own process using its own CLI. + Paseo doesn't modify or wrap their behavior. + + + No. You can run the daemon headless with{' '} + npm install -g @getpaseo/cli && paseo{' '} + and use the CLI, web app, or mobile app to connect. The desktop app just bundles the + daemon with a UI. + + + Voice runs locally on your device by default. You talk, the app transcribes and sends it + to your agent as text. Optionally, you can configure OpenAI speech providers for + higher-quality transcription and text-to-speech. + See the voice docs. + + + Yes. You can use the hosted relay (end-to-end encrypted, Paseo can't read your traffic), + set up your own tunnel (Tailscale, Cloudflare Tunnel, etc.), or expose the daemon port directly. + See configuration. + + + No. Paseo works in any directory. Worktrees are optional and only relevant if you use git. + You can run agents anywhere you'd normally work. + + +

We can't make promises on behalf of providers.

+

That said, Paseo launches the official first-party CLIs (Claude Code, Codex, OpenCode) as + subprocesses. It doesn't extract tokens or call inference APIs directly. From the provider's + perspective, usage through Paseo is indistinguishable from running the CLI yourself.

+

I've been using Paseo with all providers for months without issue.

+
+ + When you launch an agent with the worktree option (from the app, desktop, or CLI), Paseo + creates a git worktree and runs the agent inside it. The agent works on an isolated branch + without touching your main working directory. See the{' '} + worktrees docs. + +
+
+ ) +} + +function FAQItem({ question, children }: { question: string; children: React.ReactNode }) { + return ( +
+ + + + - + {question} + +
{children}
+
+ ) +} diff --git a/packages/website/src/routeTree.gen.ts b/packages/website/src/routeTree.gen.ts index 979d01ed1..eab06af74 100644 --- a/packages/website/src/routeTree.gen.ts +++ b/packages/website/src/routeTree.gen.ts @@ -10,7 +10,10 @@ import { Route as rootRouteImport } from './routes/__root' import { Route as PrivacyRouteImport } from './routes/privacy' +import { Route as OpencodeRouteImport } from './routes/opencode' import { Route as DocsRouteImport } from './routes/docs' +import { Route as CodexRouteImport } from './routes/codex' +import { Route as ClaudeCodeRouteImport } from './routes/claude-code' import { Route as ChangelogRouteImport } from './routes/changelog' import { Route as IndexRouteImport } from './routes/index' import { Route as DocsIndexRouteImport } from './routes/docs/index' @@ -27,11 +30,26 @@ const PrivacyRoute = PrivacyRouteImport.update({ path: '/privacy', getParentRoute: () => rootRouteImport, } as any) +const OpencodeRoute = OpencodeRouteImport.update({ + id: '/opencode', + path: '/opencode', + getParentRoute: () => rootRouteImport, +} as any) const DocsRoute = DocsRouteImport.update({ id: '/docs', path: '/docs', getParentRoute: () => rootRouteImport, } as any) +const CodexRoute = CodexRouteImport.update({ + id: '/codex', + path: '/codex', + getParentRoute: () => rootRouteImport, +} as any) +const ClaudeCodeRoute = ClaudeCodeRouteImport.update({ + id: '/claude-code', + path: '/claude-code', + getParentRoute: () => rootRouteImport, +} as any) const ChangelogRoute = ChangelogRouteImport.update({ id: '/changelog', path: '/changelog', @@ -86,7 +104,10 @@ const DocsBestPracticesRoute = DocsBestPracticesRouteImport.update({ export interface FileRoutesByFullPath { '/': typeof IndexRoute '/changelog': typeof ChangelogRoute + '/claude-code': typeof ClaudeCodeRoute + '/codex': typeof CodexRoute '/docs': typeof DocsRouteWithChildren + '/opencode': typeof OpencodeRoute '/privacy': typeof PrivacyRoute '/docs/best-practices': typeof DocsBestPracticesRoute '/docs/cli': typeof DocsCliRoute @@ -100,6 +121,9 @@ export interface FileRoutesByFullPath { export interface FileRoutesByTo { '/': typeof IndexRoute '/changelog': typeof ChangelogRoute + '/claude-code': typeof ClaudeCodeRoute + '/codex': typeof CodexRoute + '/opencode': typeof OpencodeRoute '/privacy': typeof PrivacyRoute '/docs/best-practices': typeof DocsBestPracticesRoute '/docs/cli': typeof DocsCliRoute @@ -114,7 +138,10 @@ export interface FileRoutesById { __root__: typeof rootRouteImport '/': typeof IndexRoute '/changelog': typeof ChangelogRoute + '/claude-code': typeof ClaudeCodeRoute + '/codex': typeof CodexRoute '/docs': typeof DocsRouteWithChildren + '/opencode': typeof OpencodeRoute '/privacy': typeof PrivacyRoute '/docs/best-practices': typeof DocsBestPracticesRoute '/docs/cli': typeof DocsCliRoute @@ -130,7 +157,10 @@ export interface FileRouteTypes { fullPaths: | '/' | '/changelog' + | '/claude-code' + | '/codex' | '/docs' + | '/opencode' | '/privacy' | '/docs/best-practices' | '/docs/cli' @@ -144,6 +174,9 @@ export interface FileRouteTypes { to: | '/' | '/changelog' + | '/claude-code' + | '/codex' + | '/opencode' | '/privacy' | '/docs/best-practices' | '/docs/cli' @@ -157,7 +190,10 @@ export interface FileRouteTypes { | '__root__' | '/' | '/changelog' + | '/claude-code' + | '/codex' | '/docs' + | '/opencode' | '/privacy' | '/docs/best-practices' | '/docs/cli' @@ -172,7 +208,10 @@ export interface FileRouteTypes { export interface RootRouteChildren { IndexRoute: typeof IndexRoute ChangelogRoute: typeof ChangelogRoute + ClaudeCodeRoute: typeof ClaudeCodeRoute + CodexRoute: typeof CodexRoute DocsRoute: typeof DocsRouteWithChildren + OpencodeRoute: typeof OpencodeRoute PrivacyRoute: typeof PrivacyRoute } @@ -185,6 +224,13 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof PrivacyRouteImport parentRoute: typeof rootRouteImport } + '/opencode': { + id: '/opencode' + path: '/opencode' + fullPath: '/opencode' + preLoaderRoute: typeof OpencodeRouteImport + parentRoute: typeof rootRouteImport + } '/docs': { id: '/docs' path: '/docs' @@ -192,6 +238,20 @@ declare module '@tanstack/react-router' { preLoaderRoute: typeof DocsRouteImport parentRoute: typeof rootRouteImport } + '/codex': { + id: '/codex' + path: '/codex' + fullPath: '/codex' + preLoaderRoute: typeof CodexRouteImport + parentRoute: typeof rootRouteImport + } + '/claude-code': { + id: '/claude-code' + path: '/claude-code' + fullPath: '/claude-code' + preLoaderRoute: typeof ClaudeCodeRouteImport + parentRoute: typeof rootRouteImport + } '/changelog': { id: '/changelog' path: '/changelog' @@ -292,7 +352,10 @@ const DocsRouteWithChildren = DocsRoute._addFileChildren(DocsRouteChildren) const rootRouteChildren: RootRouteChildren = { IndexRoute: IndexRoute, ChangelogRoute: ChangelogRoute, + ClaudeCodeRoute: ClaudeCodeRoute, + CodexRoute: CodexRoute, DocsRoute: DocsRouteWithChildren, + OpencodeRoute: OpencodeRoute, PrivacyRoute: PrivacyRoute, } export const routeTree = rootRouteImport diff --git a/packages/website/src/routes/claude-code.tsx b/packages/website/src/routes/claude-code.tsx new file mode 100644 index 000000000..a04721555 --- /dev/null +++ b/packages/website/src/routes/claude-code.tsx @@ -0,0 +1,25 @@ +import { createFileRoute } from '@tanstack/react-router' +import { LandingPage } from '~/components/landing-page' + +export const Route = createFileRoute('/claude-code')({ + head: () => ({ + meta: [ + { title: 'Claude Code Mobile App – Ship from your phone | Paseo' }, + { + name: 'description', + content: + 'Run Claude Code from your phone. Launch agents, check on progress, review diffs, and merge — all from your pocket. Self-hosted, your code stays on your machine.', + }, + ], + }), + component: ClaudeCodePage, +}) + +function ClaudeCodePage() { + return ( + + ) +} diff --git a/packages/website/src/routes/codex.tsx b/packages/website/src/routes/codex.tsx new file mode 100644 index 000000000..0ddb28fb6 --- /dev/null +++ b/packages/website/src/routes/codex.tsx @@ -0,0 +1,25 @@ +import { createFileRoute } from '@tanstack/react-router' +import { LandingPage } from '~/components/landing-page' + +export const Route = createFileRoute('/codex')({ + head: () => ({ + meta: [ + { title: 'Codex Mobile App – Run Codex from anywhere | Paseo' }, + { + name: 'description', + content: + 'Run OpenAI Codex from your phone. Kick off agents, monitor progress, and ship code without being at your desk. Self-hosted, your code never leaves your machine.', + }, + ], + }), + component: CodexPage, +}) + +function CodexPage() { + return ( + + ) +} diff --git a/packages/website/src/routes/index.tsx b/packages/website/src/routes/index.tsx index a93e9e3d6..dacc70a00 100644 --- a/packages/website/src/routes/index.tsx +++ b/packages/website/src/routes/index.tsx @@ -1,66 +1,5 @@ -import * as React from 'react' import { createFileRoute } from '@tanstack/react-router' -import { motion, AnimatePresence } from 'framer-motion' -import { CursorFieldProvider } from '~/components/butterfly' -import websitePackage from '../../package.json' -import '~/styles.css' - -const desktopVersion = websitePackage.version -const releaseBase = `https://github.com/getpaseo/paseo/releases/download/v${desktopVersion}` -const androidApkHref = `${releaseBase}/paseo-v${desktopVersion}-android.apk` - -type Platform = 'mac-silicon' | 'mac-intel' | 'windows' | 'linux' - -interface DownloadOption { - platform: Platform - label: string - href: string - icon: (props: React.SVGProps) => React.ReactElement -} - -const downloadOptions: DownloadOption[] = [ - { - platform: 'mac-silicon', - label: 'Mac', - href: `${releaseBase}/Paseo_${desktopVersion}_aarch64.dmg`, - icon: AppleIcon, - }, - { - platform: 'mac-intel', - label: 'Mac Intel', - href: `${releaseBase}/Paseo_${desktopVersion}_x86_64.dmg`, - icon: AppleIcon, - }, - { - platform: 'windows', - label: 'Windows', - href: `${releaseBase}/Paseo_${desktopVersion}_x64-setup.exe`, - icon: WindowsIcon, - }, - { - platform: 'linux', - label: 'Linux', - href: `${releaseBase}/Paseo_${desktopVersion}_amd64.AppImage`, - icon: LinuxIcon, - }, -] - -function useDetectedPlatform(): Platform { - const [platform, setPlatform] = React.useState('mac-silicon') - - React.useEffect(() => { - const ua = navigator.userAgent.toLowerCase() - if (ua.includes('win')) { - setPlatform('windows') - } else if (ua.includes('linux') && !ua.includes('android')) { - setPlatform('linux') - } else if (ua.includes('mac')) { - setPlatform('mac-silicon') - } - }, []) - - return platform -} +import { LandingPage } from '~/components/landing-page' export const Route = createFileRoute('/')({ head: () => ({ @@ -78,1020 +17,9 @@ export const Route = createFileRoute('/')({ function Home() { return ( - - {/* Hero section with background image */} -
-
-
- -
-
- - {/* Mockup - inside hero so it's above the gradient, positioned to overflow into black section */} - -
- Paseo app showing agent management interface -
-
-
- - {/* Content section */} -
-
-
- - - -
-
- -
- - ) -} - -function Nav() { - return ( - - ) -} - -function Hero() { - return ( -
- - One interface for all your coding agents - - - Run agents in parallel on your own machines. Ship from your phone or your desk. - -
- ) -} - -function AgentBadge({ name, icon }: { name: string; icon: React.ReactNode }) { - const [hovered, setHovered] = React.useState(false) - - return ( - setHovered(true)} - onMouseLeave={() => setHovered(false)} - > - {icon} - - {hovered && ( - - {name} - - )} - - - ) -} - -function Differentiator({ title, description }: { title: string; description: string }) { - return ( -
-

{title}

-

{description}

-
- ) -} - -function Features() { - return ( -
- {/* Primary differentiators - 2x2 */} -
- {[ - { - title: 'Self-hosted', - description: 'Agents run on your machine with your full dev environment. Use your tools, your configs and your skills.', - icon: ( - - - - - - - ), - }, - { - title: 'Multi-provider', - description: 'Claude Code, Codex, and OpenCode through the same interface. Pick the right model for each job.', - icon: ( - - - - - - - ), - }, - { - title: 'Voice control', - description: 'Dictate tasks or talk through problems in voice mode. Hands-free when you need it.', - icon: ( - - - - - - ), - }, - { - title: 'Cross-device', - description: 'iOS, Android, desktop, web, and CLI. Start work at your desk, check in from your phone, script it from the terminal.', - icon: ( - - - - - ), - }, - ].map((feature, i) => ( - -
- {feature.icon} -

{feature.title}

-
-

{feature.description}

-
- ))} -
- - {/* Also */} - - Multi-host - Built-in terminal - Git worktrees - E2E encrypted relay - Open source - -
- ) -} - -function GetStarted() { - return ( - - -
- Supports -
- } /> - } /> - } /> -
-
-
- ) -} - -function DownloadButton() { - const detectedPlatform = useDetectedPlatform() - const [open, setOpen] = React.useState(false) - const ref = React.useRef(null) - - React.useEffect(() => { - function handleClickOutside(e: MouseEvent) { - if (ref.current && !ref.current.contains(e.target as Node)) { - setOpen(false) - } - } - document.addEventListener('mousedown', handleClickOutside) - return () => document.removeEventListener('mousedown', handleClickOutside) - }, []) - - const primary = downloadOptions.find((o) => o.platform === detectedPlatform)! - const secondaryOptions = downloadOptions.filter((o) => o.platform !== detectedPlatform) - const PrimaryIcon = primary.icon - - return ( -
-
- - - Download for {primary.label} - - -
- - {open && ( - - {secondaryOptions.map((option) => { - const Icon = option.icon - return ( - setOpen(false)} - className="flex items-center gap-2 px-4 py-2 text-sm text-white/80 hover:bg-white/10 transition-colors" - > - - {option.label} - - ) - })} - - )} - -
- ) -} - -function ServerInstallButton() { - const [open, setOpen] = React.useState(false) - const ref = React.useRef(null) - - React.useEffect(() => { - function handleClickOutside(e: MouseEvent) { - if (ref.current && !ref.current.contains(e.target as Node)) { - setOpen(false) - } - } - document.addEventListener('mousedown', handleClickOutside) - return () => document.removeEventListener('mousedown', handleClickOutside) - }, []) - - return ( -
- - - {open && ( - <> - setOpen(false)} - /> - -
-

Run agents on a remote machine

-

- For headless machines you want to connect to from the Paseo apps. The desktop app already includes a built-in daemon. -

-
- npm install -g @getpaseo/cli && paseo -

- Requires Node.js 18+. Run paseo to start the daemon. -

-
- - )} -
-
- ) -} - -function ClaudeCodeIcon(props: React.SVGProps) { - return ( - - ) -} - -function CodexIcon(props: React.SVGProps) { - return ( - - ) -} - -function OpenCodeIcon(props: React.SVGProps) { - return ( - - ) -} - -function AppleIcon(props: React.SVGProps) { - return ( - - ) -} - -function AndroidIcon(props: React.SVGProps) { - return ( - - ) -} - -function AppStoreIcon(props: React.SVGProps) { - return ( - - ) -} - -function WindowsIcon(props: React.SVGProps) { - return ( - - ) -} - -function LinuxIcon(props: React.SVGProps) { - return ( - - ) -} - -function TerminalIcon(props: React.SVGProps) { - return ( - - ) -} - -function ChevronDownIcon(props: React.SVGProps) { - return ( - - ) -} - -function GlobeIcon(props: React.SVGProps) { - return ( - - ) -} - -function Step({ number, children }: { number: number; children: React.ReactNode }) { - return ( -
- - {number} - -
{children}
-
- ) -} - -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 ( -
-
- $ - {children} -
- -
- ) -} - -const bashKeywords = new Set(['while', 'do', 'done', 'if', 'then', 'fi', 'else', 'break', 'true', 'false']) -const bashCommands = new Set(['paseo', 'echo', 'jq']) - -function highlightBash(code: string): React.ReactNode { - const tokens: React.ReactNode[] = [] - let i = 0 - let key = 0 - - while (i < code.length) { - // Comment (# not inside a word) - if (code[i] === '#' && (i === 0 || /[\s(]/.test(code[i - 1]))) { - const end = code.indexOf('\n', i) - const comment = end === -1 ? code.slice(i) : code.slice(i, end) - tokens.push({comment}) - i += comment.length - continue - } - - // Double-quoted string - if (code[i] === '"') { - let j = i + 1 - while (j < code.length && code[j] !== '"') { - if (code[j] === '\\') j++ - j++ - } - const str = code.slice(i, j + 1) - tokens.push({str}) - i = j + 1 - continue - } - - // Single-quoted string - if (code[i] === "'") { - let j = i + 1 - while (j < code.length && code[j] !== "'") j++ - const str = code.slice(i, j + 1) - tokens.push({str}) - i = j + 1 - continue - } - - // $( subshell or $variable - if (code[i] === '$') { - if (code[i + 1] === '(') { - tokens.push($() - i += 2 - continue - } - let j = i + 1 - while (j < code.length && /\w/.test(code[j])) j++ - tokens.push({code.slice(i, j)}) - i = j - continue - } - - // Flags: --long-flag or -f (preceded by whitespace) - if (code[i] === '-' && (i === 0 || /\s/.test(code[i - 1])) && i + 1 < code.length && /[\w-]/.test(code[i + 1])) { - let j = i - if (code[j + 1] === '-') j++ - j++ - while (j < code.length && /[\w-]/.test(code[j])) j++ - tokens.push({code.slice(i, j)}) - i = j - continue - } - - // Words - if (/[a-zA-Z_]/.test(code[i])) { - let j = i - while (j < code.length && /\w/.test(code[j])) j++ - const word = code.slice(i, j) - if (bashKeywords.has(word)) { - tokens.push({word}) - } else if (bashCommands.has(word)) { - tokens.push({word}) - } else { - tokens.push(word) - key++ - } - i = j - continue - } - - // Pipe, &&, backslash - if (code[i] === '|' || (code[i] === '&' && code[i + 1] === '&')) { - const op = code[i] === '|' ? '|' : '&&' - tokens.push({op}) - i += op.length - continue - } - - if (code[i] === '\\') { - tokens.push(\) - i++ - continue - } - - // Closing paren (matches $( ... )) - if (code[i] === ')') { - tokens.push()) - i++ - continue - } - - // Everything else - tokens.push(code[i]) - i++ - } - - return <>{tokens} -} - -function CLICodeBlock({ children }: { children: string }) { - const [copied, setCopied] = React.useState(false) - - function handleCopy() { - navigator.clipboard.writeText(children) - setCopied(true) - setTimeout(() => setCopied(false), 2000) - } - - return ( -
- -
{highlightBash(children)}
-
- ) -} - -interface CLIExample { - title: string - description: string - code: string -} - -const cliExamples: CLIExample[] = [ - { - title: 'Launch and monitor', - description: 'Give an agent a task and watch it work. The --worktree flag spins up an isolated git branch so you can run multiple agents on the same repo without conflicts.', - code: -`paseo run --provider claude/opus-4.6 "implement user authentication" -paseo run --provider codex/gpt-5.4 --worktree feature-x "implement feature X" - -paseo ls # list running agents -paseo attach abc123 # stream live output -paseo send abc123 "also add tests" # follow-up task`, - }, - { - title: 'Orchestration', - description: 'Agents can use the CLI too. Tell one agent to spawn others, split up the work, and pull everything together when they\'re done.', - code: -`# Spawn two agents in parallel, wait, then synthesize -paseo run --detach "implement the frontend" --name frontend -paseo run --detach "implement the API layer" --name api - -paseo wait frontend api - -paseo run "review both branches and write an integration plan"`, - }, - { - title: 'Structured output', - description: 'Pass a JSON schema and get typed data back from any agent run. No output parsing, just the structured result you asked for.', - code: -`result=$(paseo run \\ - --output-schema '{ - "type": "object", - "properties": { - "severity": { "type": "string", "enum": ["low", "medium", "high"] }, - "issues": { "type": "array", "items": { "type": "string" } } - }, - "required": ["severity", "issues"] - }' \\ - "audit this codebase for security issues") - -echo $result | jq '.severity' # "high" -echo $result | jq '.issues[0]' # "SQL injection on line 42"`, - }, - { - title: 'Remote', - description: 'Point at any daemon on your network or over the internet. Run agents on a beefy server from your laptop.', - code: -`# Set once for the session -export PASEO_HOST=workstation.local:6767 -paseo run "run the full test suite" -paseo ls - -# Or per-command -paseo --host gpu-server:6767 run "train the model" -paseo --host gpu-server:6767 attach abc123`, - }, - { - title: 'Worker-judge loops', - description: 'Have one agent do the work and another judge the result. Loop until it passes. Good for test fixes, code review, or any task with clear acceptance criteria.', - code: -`while true; do - paseo run "make all tests pass" - - verdict=$(paseo run \\ - --output-schema '{"type":"object","properties":{"passed":{"type":"boolean"}},"required":["passed"]}' \\ - "verify tests pass and the code is production-ready") - - echo "$verdict" | jq -e '.passed' && break -done`, - }, -] - -function CLISection() { - const [activeIndex, setActiveIndex] = React.useState(0) - const active = cliExamples[activeIndex] - - return ( - -
-

CLI

-

- Everything you can do in the app, you can do from the terminal. -

-
- -
- {cliExamples.map((example, i) => ( - - ))} -
- -
-

{active.description}

- {active.code} -
- - - Full CLI reference - - - - -
- ) -} - -function FAQ() { - return ( - -

FAQ

-
- - Yes. Paseo is free and open source. You need Claude Code, Codex, or OpenCode installed - with your own credentials. Voice is local-first by default and can optionally use OpenAI - speech providers if you configure them. - - - Paseo doesn't send your code anywhere. Agents run locally and talk to their own APIs - as they normally would. For remote access, you can use the optional{' '} - end-to-end encrypted relay, - connect directly over your local network, or use your own tunnel. - - - Claude Code, Codex, and OpenCode. Each agent runs as its own process using its own CLI. - Paseo doesn't modify or wrap their behavior. - - - No. You can run the daemon headless with{' '} - npm install -g @getpaseo/cli && paseo{' '} - and use the CLI, web app, or mobile app to connect. The desktop app just bundles the - daemon with a UI. - - - Voice runs locally on your device by default. You talk, the app transcribes and sends it - to your agent as text. Optionally, you can configure OpenAI speech providers for - higher-quality transcription and text-to-speech. - See the voice docs. - - - Yes. You can use the hosted relay (end-to-end encrypted, Paseo can't read your traffic), - set up your own tunnel (Tailscale, Cloudflare Tunnel, etc.), or expose the daemon port directly. - See configuration. - - - No. Paseo works in any directory. Worktrees are optional and only relevant if you use git. - You can run agents anywhere you'd normally work. - - -

We can't make promises on behalf of providers.

-

That said, Paseo launches the official first-party CLIs (Claude Code, Codex, OpenCode) as - subprocesses. It doesn't extract tokens or call inference APIs directly. From the provider's - perspective, usage through Paseo is indistinguishable from running the CLI yourself.

-

I've been using Paseo with all providers for months without issue.

-
- - When you launch an agent with the worktree option (from the app, desktop, or CLI), Paseo - creates a git worktree and runs the agent inside it. The agent works on an isolated branch - without touching your main working directory. See the{' '} - worktrees docs. - -
-
- ) -} - -function FAQItem({ question, children }: { question: string; children: React.ReactNode }) { - return ( -
- - + - - - {question} - -
{children}
-
+ ) } diff --git a/packages/website/src/routes/opencode.tsx b/packages/website/src/routes/opencode.tsx new file mode 100644 index 000000000..e55ce5dcc --- /dev/null +++ b/packages/website/src/routes/opencode.tsx @@ -0,0 +1,25 @@ +import { createFileRoute } from '@tanstack/react-router' +import { LandingPage } from '~/components/landing-page' + +export const Route = createFileRoute('/opencode')({ + head: () => ({ + meta: [ + { title: 'OpenCode Mobile App – Code from anywhere | Paseo' }, + { + name: 'description', + content: + 'Run OpenCode from your phone. Launch agents, watch them work, and ship code from wherever you are. Self-hosted, open source, your code stays local.', + }, + ], + }), + component: OpenCodePage, +}) + +function OpenCodePage() { + return ( + + ) +}