import * as React from "react";
import {
motion,
AnimatePresence,
useInView,
useScroll,
useTransform,
type Transition,
} from "framer-motion";
// Shared motion presets — hoisted so every JSX site receives the same object
// reference and doesn't trigger jsx-no-new-object-as-prop.
const FADE_IN_UP = { opacity: 0, y: 20 };
const FADE_IN = { opacity: 1, y: 0 };
const FADE_IN_UP_TINY = { opacity: 0, y: -10 };
const FADE_IN_UP_XL = { opacity: 0, y: 30 };
const FADE_IN_UP_40 = { opacity: 0, y: 40 };
const FADE_IN_UP_4 = { opacity: 0, y: 4 };
const FADE_OUT_UP_4 = { opacity: 0, y: 4 };
const EASE_OUT_06_DELAY_01: Transition = { duration: 0.6, delay: 0.1, ease: "easeOut" };
const EASE_OUT_08_DELAY_05: Transition = { duration: 0.8, delay: 0.5, ease: "easeOut" };
const EASE_OUT_05: Transition = { duration: 0.5, ease: "easeOut" };
const EASE_OUT_015: Transition = { duration: 0.15, ease: "easeOut" };
const DURATION_05: Transition = { duration: 0.5 };
const VIEWPORT_60 = { once: true, margin: "-60px" };
const SVG_OVERFLOW_VISIBLE_STYLE = { overflow: "visible" as const };
const PHONE_PERSPECTIVE_STYLE = { minHeight: 480, perspective: 1200 };
import { CursorFieldProvider } from "~/components/butterfly";
import { CommandDialog } from "~/components/command-dialog";
import { AGENT_PAGES } from "~/data/agent-pages";
import {
appStoreUrl,
playStoreUrl,
webAppUrl,
getDownloadOptions,
useDetectedPlatform,
AppleIcon,
PlayStoreIcon,
TerminalIcon,
GlobeIcon,
} from "~/downloads";
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";
interface LandingPageProps {
title: React.ReactNode;
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 */}
{/* Phone showcase */}
{/* Content section */}
);
}
function Nav() {
return (
);
}
function Hero({ title, subtitle }: { title: React.ReactNode; subtitle: string }) {
return (
);
}
const CLAUDE_CODE_BADGE_ICON = ;
const CODEX_BADGE_ICON = ;
const OPENCODE_BADGE_ICON = ;
const PI_BADGE_ICON = ;
const CURSOR_BADGE_ICON = ;
const FEATURED_AGENT_COUNT = 5;
const ADDITIONAL_AGENT_COUNT = AGENT_PAGES.length - FEATURED_AGENT_COUNT;
const SOCIAL_PROOF_TWEETS = [
{
name: "Cam",
handle: "@ceeebeeebeee",
date: "Apr 6, 2026",
avatar: "/social-proof/ceeebeeebeee.jpg",
url: "https://x.com/ceeebeeebeee/status/2041008798798864537",
text: "without a doubt the most slept on orchestrator right now. Open source, every OS, and a mobile experience that truly blew me away.",
},
{
name: "Erik Sherman",
handle: "@erikksherman",
date: "Apr 11, 2026",
avatar: "/social-proof/erikksherman.jpg",
url: "https://x.com/erikksherman/status/2043011630590751008",
text: "control agents from anywhere - mac, phone, web. one simple change transformed my health while INCREASING productivity",
},
{
name: "Aman Kumar Jagdev",
handle: "@amankumarjagdev",
date: "Apr 16, 2026",
avatar: "/social-proof/amankumarjagdev.jpg",
url: "https://x.com/amankumarjagdev/status/2044815258414674307",
text: "I have tried 100s of agent orchestrator, cli and gui. the best one i have found. Please give it a try! it's really good",
},
{
name: "RUI",
handle: "@tietougongshiba",
date: "May 3, 2026",
avatar: "/social-proof/tietougongshiba.jpg",
url: "https://x.com/tietougongshiba/status/2050886374941925754",
text: "Being able to check and manage agent progress from my phone while I'm out is so convenient.",
},
{
name: "Jason Torres",
handle: "@jasontorres",
date: "May 11, 2026",
avatar: "/social-proof/jasontorres.jpg",
url: "https://x.com/jasontorres/status/2053875385515790731",
text: "Can interchange between Codex, Claude Code, Opencode, Pi. Stable mobile and desktop apps connected through a secure relay from your VMs.",
},
{
name: "A9",
handle: "@aadtyn",
date: "May 29, 2026",
avatar: "/social-proof/aadtyn.jpg",
url: "https://x.com/aadtyn/status/2060371229773803943",
text: "cross platform agent orchestration with inbuilt relay and tailscale / self host daemon options + the best UI ive seen in this segment",
},
{
name: "boris evstratov",
handle: "@bevstratov",
date: "May 30, 2026",
avatar: "/social-proof/bevstratov.jpg",
url: "https://x.com/bevstratov/status/2060733983042781550",
text: "It’s an incredible piece of software. The last building block I needed to fully work from my phone. everything super smooth.",
},
{
name: "Arnold Gamboa",
handle: "@arnoldgamboa",
date: "May 28, 2026",
avatar: "/social-proof/arnoldgamboa.jpg",
url: "https://x.com/arnoldgamboa/status/2059832028099436921",
text: "Paseo is a really good interface for Pi. It’s not the only thing it does, but that’s my current use case for now.",
},
{
name: "Dong",
handle: "@dongnaebi",
date: "Apr 12, 2026",
avatar: "/social-proof/dongnaebi.jpg",
url: "https://x.com/dongnaebi/status/2043162391941398735",
text: "Paseo is the best software I've used this year. Absolutely amazing!",
},
] as const;
const SOCIAL_PROOF_ROWS = [
{ id: "top", tweets: SOCIAL_PROOF_TWEETS.slice(0, 5), reverse: false },
{ id: "bottom", tweets: SOCIAL_PROOF_TWEETS.slice(5), reverse: true },
] as const;
type SocialProofTweet = (typeof SOCIAL_PROOF_TWEETS)[number];
function AgentBadge({ name, icon }: { name: string; icon: React.ReactNode }) {
const [hovered, setHovered] = React.useState(false);
const handleMouseEnter = React.useCallback(() => setHovered(true), []);
const handleMouseLeave = React.useCallback(() => setHovered(false), []);
return (
{icon}
{hovered && (
{name}
)}
);
}
function FeatureSection({
title,
description,
children,
}: {
title: string;
description: string;
children: React.ReactNode;
}) {
return (
{children}
);
}
function SectionTitle({ title, description }: { title: string; description: string }) {
return (
);
}
function SocialProofWall() {
return (
{SOCIAL_PROOF_ROWS.map((row) => (
))}
);
}
function SocialProofRow({
tweets,
reverse,
}: {
tweets: readonly SocialProofTweet[];
reverse: boolean;
}) {
return (
{tweets.map((tweet) => (
))}
{tweets.map((tweet) => (
))}
);
}
function SocialProofCard({ tweet, inert }: { tweet: SocialProofTweet; inert?: boolean }) {
return (
{tweet.handle}
{tweet.text}
);
}
function MultiProviderSection() {
const providers = [
{ name: "Claude Code", icon: },
{ name: "Codex", icon: },
{ name: "OpenCode", icon: },
{ name: "Pi", icon: },
{ name: "Cursor", icon: },
];
return (
);
}
function SelfHostedDiagram() {
const clients = [
{
name: "Desktop",
icon: (
),
},
{
name: "Web",
icon: (
),
},
{
name: "Mobile",
icon: (
),
},
{
name: "CLI",
icon: (
),
},
];
const hosts = ["MacBook Pro", "Hetzner VM", "Dev server"];
const containerRef = React.useRef(null);
const clientRefs = React.useRef<(HTMLDivElement | null)[]>([]);
const hostRefs = React.useRef<(HTMLDivElement | null)[]>([]);
const centerRef = React.useRef(null);
const setClientRef = React.useCallback(
(index: number) => (el: HTMLDivElement | null) => {
clientRefs.current[index] = el;
},
[],
);
const setHostRef = React.useCallback(
(index: number) => (el: HTMLDivElement | null) => {
hostRefs.current[index] = el;
},
[],
);
const [paths, setPaths] = React.useState<{ left: string[]; right: string[] }>({
left: [],
right: [],
});
React.useEffect(() => {
function computePaths() {
const container = containerRef.current;
const center = centerRef.current;
if (!container || !center) return;
const cRect = container.getBoundingClientRect();
const mRect = center.getBoundingClientRect();
const midL = mRect.left - cRect.left;
const midR = mRect.right - cRect.left;
const midY = mRect.top - cRect.top + mRect.height / 2;
const left = clientRefs.current.map((el) => {
if (!el) return "";
const r = el.getBoundingClientRect();
const x1 = r.right - cRect.left;
const y1 = r.top - cRect.top + r.height / 2;
const cpx = x1 + (midL - x1) * 0.6;
return `M${x1},${y1} C${cpx},${y1} ${midL - (midL - x1) * 0.3},${midY} ${midL},${midY}`;
});
const right = hostRefs.current.map((el) => {
if (!el) return "";
const r = el.getBoundingClientRect();
const x2 = r.left - cRect.left;
const y2 = r.top - cRect.top + r.height / 2;
const cpx = midR + (x2 - midR) * 0.4;
return `M${midR},${midY} C${cpx},${midY} ${x2 - (x2 - midR) * 0.3},${y2} ${x2},${y2}`;
});
setPaths({ left, right });
}
computePaths();
window.addEventListener("resize", computePaths);
return () => window.removeEventListener("resize", computePaths);
}, []);
return (
<>
{/* Mobile: vertical stack */}
{clients.map((c) => (
{c.icon}
{c.name}
))}
E2E Encrypted Relay
or
Direct Connection
{hosts.map((h) => (
{h}
))}
{/* Desktop: horizontal with bezier curves */}
{/* SVG curves */}
{[...paths.left, ...paths.right].map(
(d) =>
d && (
),
)}
{/* Clients */}
{clients.map((c, i) => (
{c.icon}
{c.name}
))}
{/* Spacer */}
{/* Center label */}
E2E Encrypted Relay
or
Direct Connection
{/* Spacer */}
{/* Hosts */}
{hosts.map((h, i) => (
{h}
))}
>
);
}
function SelfHostedSection() {
return (
);
}
const WORKFLOW_STEPS = ["Worktree", "Preview", "Review", "Commit", "PR", "Merge"] as const;
const REVIEW_FILES = [
{ path: "src/auth/session.ts", delta: "+42" },
{ path: "src/auth/middleware.ts", delta: "+18 -9" },
{ path: "tests/auth.test.ts", delta: "+31" },
] as const;
function WorkflowSection() {
return (
);
}
function WorkflowHeader() {
return (
{WORKFLOW_STEPS.map((step) => (
{step}
))}
);
}
function WorkflowPreview() {
return (
);
}
function BrowserChrome() {
return (
web.fix-auth.my-app.localhost
);
}
function PreviewHeader() {
return (
);
}
function PreviewFormCard({
titleWidth,
ctaClassName,
}: {
titleWidth: string;
ctaClassName: string;
}) {
return (
);
}
function WorkflowReviewAndShip() {
return (
);
}
function InlineReviewPanel() {
return (
Inline review
3 files changed
{REVIEW_FILES.map((file) => (
))}
);
}
function ReviewFileRow({ path, delta }: { path: string; delta: string }) {
return (
{path}
{delta.split(" ").map((part) => (
{part}
))}
);
}
function ShipPanel() {
return (
Ready to ship
checks passed
);
}
function SplitPanelsSection() {
return (
);
}
function PanelTile({ label, className }: { label: string; className: string }) {
return (
{label}
);
}
function ServiceProxySection() {
const workspaces = [
{ name: "fix-auth", url: "web.fix-auth.my-app.localhost" },
{ name: "add-search", url: "web.add-search.my-app.localhost" },
{ name: "upgrade-deps", url: "web.upgrade-deps.my-app.localhost" },
];
return (
{/* Project */}
{/* Workspaces indented */}
{workspaces.map((ws) => (
))}
);
}
function ShortcutsSection() {
const shortcuts = [
{ keys: ["⌘", "1-9"], action: "Switch panels" },
{ keys: ["⌘", "D"], action: "Split vertical" },
{ keys: ["⌘", "Shift", "D"], action: "Split horizontal" },
{ keys: ["⌘", "W"], action: "Close panel" },
{ keys: ["⌘", "N"], action: "New agent" },
{ keys: ["⌘", "K"], action: "Command palette" },
];
return (
{shortcuts.map((s) => (
{s.action}
{s.keys.map((k) => (
{k}
))}
))}
);
}
interface VoiceBarProps {
index: number;
barCount: number;
}
function VoiceBar({ index, barCount }: VoiceBarProps) {
const style = React.useMemo(() => {
const center = barCount / 2;
const dist = Math.abs(index - center) / center;
const envelope = 1 - dist * dist;
const minH = 4;
const maxH = 56;
const baseH = minH + (maxH - minH) * envelope;
const jitter = Math.sin(index * 2.3) * 0.3 + Math.cos(index * 1.7) * 0.2;
const h = Math.max(minH, baseH * (0.5 + 0.5 * Math.abs(jitter + Math.sin(index * 0.8))));
return {
height: h,
animationName: "voice-bar",
animationDuration: `${800 + (index % 5) * 200}ms`,
animationTimingFunction: "ease-in-out",
animationIterationCount: "infinite",
animationDirection: "alternate" as const,
animationDelay: `${(index % 7) * 80}ms`,
};
}, [index, barCount]);
return
;
}
const VOICE_BAR_COUNT = 48;
const VOICE_BAR_INDICES = Array.from({ length: VOICE_BAR_COUNT }, (_, i) => i);
function VoiceWaveform() {
return (
{VOICE_BAR_INDICES.map((i) => (
))}
);
}
const USER_WORDS =
"Refactor the auth middleware to use the new session store, then run the test suite".split(" ");
const RESPONSE_WORDS =
"I'll update the auth middleware to use SessionStore instead of the legacy cookie-based approach. Let me refactor the middleware and update the tests.".split(
" ",
);
const DICTATION_LAG = 2;
const RESPONSE_LAG = 3;
const WORD_APPEAR_MS = 150;
const RESPONSE_WORD_MS = 60;
const PHASE_GAP_MS = 800;
const LOOP_PAUSE_MS = 3000;
type VoicePhase =
| "dictation"
| "dictation-flush"
| "pause"
| "response"
| "response-flush"
| "done";
function useVoiceConversation() {
const [phase, setPhase] = React.useState("dictation");
const [wordIndex, setWordIndex] = React.useState(0);
React.useEffect(() => {
if (phase === "dictation") {
if (wordIndex < USER_WORDS.length) {
const t = setTimeout(() => setWordIndex((w) => w + 1), WORD_APPEAR_MS);
return () => clearTimeout(t);
}
setPhase("dictation-flush");
setWordIndex(0);
return;
}
if (phase === "dictation-flush") {
if (wordIndex < DICTATION_LAG) {
const t = setTimeout(() => setWordIndex((w) => w + 1), WORD_APPEAR_MS);
return () => clearTimeout(t);
}
const t = setTimeout(() => {
setPhase("pause");
}, PHASE_GAP_MS);
return () => clearTimeout(t);
}
if (phase === "pause") {
const t = setTimeout(() => {
setPhase("response");
setWordIndex(0);
}, PHASE_GAP_MS);
return () => clearTimeout(t);
}
if (phase === "response") {
if (wordIndex < RESPONSE_WORDS.length) {
const t = setTimeout(() => setWordIndex((w) => w + 1), RESPONSE_WORD_MS);
return () => clearTimeout(t);
}
setPhase("response-flush");
setWordIndex(0);
return;
}
if (phase === "response-flush") {
if (wordIndex < RESPONSE_LAG) {
const t = setTimeout(() => setWordIndex((w) => w + 1), RESPONSE_WORD_MS);
return () => clearTimeout(t);
}
const t = setTimeout(() => {
setPhase("done");
}, LOOP_PAUSE_MS);
return () => clearTimeout(t);
}
if (phase === "done") {
const t = setTimeout(() => {
setPhase("dictation");
setWordIndex(0);
}, 0);
return () => clearTimeout(t);
}
}, [phase, wordIndex]);
// Compute effective word indices for rendering
let dictationWordIndex: number;
if (phase === "dictation") {
dictationWordIndex = wordIndex;
} else if (phase === "dictation-flush") {
dictationWordIndex = USER_WORDS.length + wordIndex;
} else {
dictationWordIndex = USER_WORDS.length + DICTATION_LAG;
}
let responseWordIndex: number;
if (phase === "response") {
responseWordIndex = wordIndex;
} else if (phase === "response-flush") {
responseWordIndex = RESPONSE_WORDS.length + wordIndex;
} else if (phase === "done") {
responseWordIndex = RESPONSE_WORDS.length + RESPONSE_LAG;
} else {
responseWordIndex = 0;
}
const showResponse = phase === "response" || phase === "response-flush" || phase === "done";
return { dictationWordIndex, responseWordIndex, showResponse };
}
function makeWordKey(words: string[], i: number): string {
const word = words[i];
let occurrence = 0;
for (let j = 0; j < i; j++) {
if (words[j] === word) occurrence++;
}
return `${word}#${occurrence}`;
}
function WordSpan({ word, confirmed }: { word: string; confirmed: boolean }) {
return (
{word}{" "}
);
}
function StreamingWords({
words,
wordIndex,
confirmLag = 2,
}: {
words: string[];
wordIndex: number;
confirmLag?: number;
}) {
return (
{/* Invisible full text to reserve height at any viewport width */}
{words.join(" ")}
{/* Visible streaming text overlaid */}
{words.map((word, i) => {
if (i >= wordIndex) return null;
const confirmed = i < wordIndex - confirmLag;
return ;
})}
);
}
function LocalVoiceSection() {
const { dictationWordIndex, responseWordIndex, showResponse } = useVoiceConversation();
return (
{/* Waveform area */}
{/* User dictation */}
{/* Agent response — always rendered to reserve space */}
);
}
function GetStarted() {
return (
);
}
function DownloadButton() {
const release = useRelease();
const detectedPlatform = useDetectedPlatform();
const primary = getDownloadOptions(release).find((o) => o.platform === detectedPlatform)!;
const PrimaryIcon = primary.icon;
return (
Download for {primary.label}
);
}
const SERVER_INSTALL_TRIGGER = (
);
const SERVER_INSTALL_FOOTNOTE = (
<>
Requires Node.js 18+. Run paseo to start the
daemon.
>
);
function ServerInstallButton() {
return (
);
}
function ClaudeCodeIcon(props: React.SVGProps) {
return (
);
}
function CodexIcon(props: React.SVGProps) {
return (
);
}
function OpenCodeIcon(props: React.SVGProps) {
return (
);
}
function CursorIcon(props: React.SVGProps) {
return (
);
}
function PiIcon(props: React.SVGProps) {
return (
);
}
const bashKeywords = new Set([
"while",
"do",
"done",
"if",
"then",
"fi",
"else",
"break",
"true",
"false",
]);
const bashCommands = new Set(["paseo", "echo", "jq"]);
function tokenizeBashComment(code: string, i: number): { node: React.ReactNode; len: number } {
const end = code.indexOf("\n", i);
const comment = end === -1 ? code.slice(i) : code.slice(i, end);
return {
node: {comment} ,
len: comment.length,
};
}
function tokenizeBashDoubleQuoted(code: string, i: number): { node: React.ReactNode; len: number } {
let j = i + 1;
while (j < code.length && code[j] !== '"') {
if (code[j] === "\\") j++;
j++;
}
const str = code.slice(i, j + 1);
return { node: {str} , len: str.length };
}
function tokenizeBashSingleQuoted(code: string, i: number): { node: React.ReactNode; len: number } {
let j = i + 1;
while (j < code.length && code[j] !== "'") j++;
const str = code.slice(i, j + 1);
return { node: {str} , len: str.length };
}
function tokenizeBashDollar(code: string, i: number): { node: React.ReactNode; len: number } {
if (code[i + 1] === "(") {
return { node: $( , len: 2 };
}
let j = i + 1;
while (j < code.length && /\w/.test(code[j])) j++;
return {
node: {code.slice(i, j)} ,
len: j - i,
};
}
function tokenizeBashFlag(code: string, i: number): { node: React.ReactNode; len: number } {
let j = i;
if (code[j + 1] === "-") j++;
j++;
while (j < code.length && /[\w-]/.test(code[j])) j++;
return {
node: {code.slice(i, j)} ,
len: j - i,
};
}
function tokenizeBashWord(code: string, i: number): { node: React.ReactNode; len: number } {
let j = i;
while (j < code.length && /\w/.test(code[j])) j++;
const word = code.slice(i, j);
const len = j - i;
if (bashKeywords.has(word)) {
return { node: {word} , len };
}
if (bashCommands.has(word)) {
return { node: {word} , len };
}
return { node: word, len };
}
function isBashFlagStart(code: string, i: number): boolean {
return (
code[i] === "-" &&
(i === 0 || /\s/.test(code[i - 1])) &&
i + 1 < code.length &&
/[\w-]/.test(code[i + 1])
);
}
function isBashCommentStart(code: string, i: number): boolean {
return code[i] === "#" && (i === 0 || /[\s(]/.test(code[i - 1]));
}
function tokenizeBashChar(code: string, i: number): { node: React.ReactNode; len: number } {
const c = code[i];
if (c === "|" || (c === "&" && code[i + 1] === "&")) {
const op = c === "|" ? "|" : "&&";
return { node: {op} , len: op.length };
}
if (c === "\\") return { node: \ , len: 1 };
if (c === ")") return { node: ) , len: 1 };
return { node: c, len: 1 };
}
function nextBashToken(code: string, i: number): { node: React.ReactNode; len: number } {
if (isBashCommentStart(code, i)) return tokenizeBashComment(code, i);
if (code[i] === '"') return tokenizeBashDoubleQuoted(code, i);
if (code[i] === "'") return tokenizeBashSingleQuoted(code, i);
if (code[i] === "$") return tokenizeBashDollar(code, i);
if (isBashFlagStart(code, i)) return tokenizeBashFlag(code, i);
if (/[a-zA-Z_]/.test(code[i])) return tokenizeBashWord(code, i);
return tokenizeBashChar(code, i);
}
function highlightBash(code: string): React.ReactNode {
const tokens: React.ReactNode[] = [];
let i = 0;
let key = 0;
while (i < code.length) {
const { node, len } = nextBashToken(code, i);
if (React.isValidElement(node)) {
tokens.push(React.cloneElement(node, { key: key++ }));
} else {
tokens.push(node);
}
i += len;
}
return tokens;
}
function CLICodeBlock({ children }: { children: string }) {
const [copied, setCopied] = React.useState(false);
const handleCopy = React.useCallback(() => {
navigator.clipboard.writeText(children);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}, [children]);
return (
{copied ? (
) : (
)}
{highlightBash(children)}
);
}
interface CLIExample {
title: string;
description: string;
code: string;
}
const cliExamples: CLIExample[] = [
{
title: "Run agents",
description:
"Launch agents locally or on any remote host. The --worktree flag spins up an isolated git branch so you can run multiple agents on the same repo without conflicts.",
code: `paseo run "implement user authentication"
paseo run --provider codex --worktree feature-x "implement feature X"
paseo run --host devbox:6767 "run the full test suite"
paseo ls # list running agents
paseo attach abc123 # stream live output
paseo send abc123 "also add tests" # follow-up task`,
},
{
title: "Loops",
description:
"Have one agent do the work, another verify the result, and loop until it passes. Built-in, no shell scripting needed.",
code: `# Worker-verifier loop: fix tests until they pass
paseo loop run "make all tests pass" \\
--verify "verify tests pass and the code is production-ready" \\
--verify-check "npm test" \\
--max-iterations 5
paseo loop ls # list running loops
paseo loop logs abc123 # stream loop output`,
},
{
title: "Schedules",
description:
"Run agents on a cron schedule. Automate recurring tasks like dependency updates, security audits, or report generation.",
code: `# Run a security audit every Monday at 9am
paseo schedule create --cron "0 9 * * 1" \\
"audit the codebase for security issues and open PRs for fixes"
paseo schedule ls # list all schedules
paseo schedule pause abc123 # pause a schedule
paseo schedule delete abc123 # remove a schedule`,
},
];
function PhoneShowcase() {
const containerRef = React.useRef(null);
const textInView = useInView(containerRef, { once: true, margin: "-80px" });
// Scroll-linked animation: track how far through the container the user has scrolled
const { scrollYProgress } = useScroll({
target: containerRef,
offset: ["start end", "center center"],
});
// Responsive slide distance
const [slideDistance, setSlideDistance] = React.useState(260);
React.useEffect(() => {
function update() {
setSlideDistance(window.innerWidth < 768 ? 140 : 260);
}
update();
window.addEventListener("resize", update);
return () => window.removeEventListener("resize", update);
}, []);
// Side phones start at x=0 (behind center) and slide out to final position
const sideOpacity = useTransform(scrollYProgress, [0.2, 0.6], [0, 1]);
const leftX = useTransform(scrollYProgress, [0.2, 0.6], [0, -slideDistance]);
const rightX = useTransform(scrollYProgress, [0.2, 0.6], [0, slideDistance]);
const leftPhoneStyle = React.useMemo(
() => ({ opacity: sideOpacity, x: leftX, rotateY: -15, scale: 0.97 }),
[sideOpacity, leftX],
);
const rightPhoneStyle = React.useMemo(
() => ({ opacity: sideOpacity, x: rightX, rotateY: 15, scale: 0.97 }),
[sideOpacity, rightX],
);
const centerPhoneAnimate = React.useMemo(() => (textInView ? FADE_IN : {}), [textInView]);
const textAnimate = React.useMemo(() => (textInView ? FADE_IN : {}), [textInView]);
return (
{/* Arrow + text */}
When you want to step away from your desk,
you can.
The native mobile app has full feature parity with desktop.
{/* Phone trio — side phones are absolute, start behind center, slide outward with perspective rotation */}
{/* Left phone — rotated to face inward */}
{/* Center phone */}
{/* Right phone — rotated to face inward */}
);
}
function CLITabButton({
title,
index,
active,
onSelect,
}: {
title: string;
index: number;
active: boolean;
onSelect: (i: number) => void;
}) {
const handleClick = React.useCallback(() => onSelect(index), [onSelect, index]);
return (
{title}
);
}
function CLISection() {
const [activeIndex, setActiveIndex] = React.useState(0);
const active = cliExamples[activeIndex];
return (
{cliExamples.map((example, i) => (
))}
{active.code}
Full CLI reference
);
}
function FAQ() {
return (
FAQ
Yes. Paseo is free and open source. You need Claude Code, Codex, Cursor, OpenCode, or Pi
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, Cursor, OpenCode, and Pi. Each agent runs as its own process using its
own CLI or local integration. 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 each provider's local CLI or integration (Claude Code,
Codex, Cursor, OpenCode, Pi) as a subprocess. It doesn't extract tokens or call
inference APIs directly. From the provider's perspective, usage through Paseo is
indistinguishable from running the provider 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 SponsorCTA() {
return (
Paseo is an independent open source project for running coding agents across your own
machines, phone, desktop, and CLI.
It's built around freedom of choice: use the provider you want, run it on your own
infrastructure, and keep your workflow portable.
If you like Paseo, sponsorship is the best way to support continued development.
- Mo
);
}