mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
chore(lint): escape entities and hoist inline objects in website
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { createContext, useContext, useEffect, useRef, useState } from "react";
|
||||
import { createContext, useContext, useEffect, useMemo, useRef, useState } from "react";
|
||||
|
||||
// --- Vector math utilities ---
|
||||
|
||||
@@ -107,10 +107,21 @@ function ButterflyVisual({
|
||||
direction = "right",
|
||||
heading = 0,
|
||||
}: ButterflyVisualProps) {
|
||||
const wingStyle = {
|
||||
animationDelay: `${delay}s`,
|
||||
animationDuration: `${duration}s`,
|
||||
};
|
||||
const wingStyle = useMemo(
|
||||
() => ({
|
||||
animationDelay: `${delay}s`,
|
||||
animationDuration: `${duration}s`,
|
||||
}),
|
||||
[delay, duration],
|
||||
);
|
||||
|
||||
const bodyStyle = useMemo(
|
||||
() => ({
|
||||
animationDelay: `${delay}s`,
|
||||
animationDuration: `${duration}s`,
|
||||
}),
|
||||
[delay, duration],
|
||||
);
|
||||
|
||||
const scaleX = direction === "left" ? -1 : 1;
|
||||
|
||||
@@ -119,24 +130,28 @@ function ButterflyVisual({
|
||||
// -30° = horizontal, 0° = 30° down (base), +90° = 120° down (nearly straight down)
|
||||
const clampedHeading = Math.max(-30, Math.min(90, heading));
|
||||
|
||||
const svgStyle = useMemo(
|
||||
() => ({
|
||||
filter: "url(#painterly)",
|
||||
transform: `scaleX(${scaleX}) rotateY(25deg) rotate(30deg) rotateZ(${clampedHeading}deg)`,
|
||||
transition: "transform 0.3s ease-out",
|
||||
}),
|
||||
[scaleX, clampedHeading],
|
||||
);
|
||||
|
||||
const farWingStyle = useMemo(
|
||||
() => ({ ...wingStyle, animationDelay: `${delay + 0.08}s` }),
|
||||
[wingStyle, delay],
|
||||
);
|
||||
|
||||
const lowerWingStyle = useMemo(
|
||||
() => ({ ...wingStyle, animationDelay: `${delay + 0.05}s` }),
|
||||
[wingStyle, delay],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className="animate-flutter-body"
|
||||
style={{
|
||||
animationDelay: `${delay}s`,
|
||||
animationDuration: `${duration}s`,
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
viewBox="0 0 50 40"
|
||||
width={size}
|
||||
height={size * 0.8}
|
||||
style={{
|
||||
filter: "url(#painterly)",
|
||||
transform: `scaleX(${scaleX}) rotateY(25deg) rotate(30deg) rotateZ(${clampedHeading}deg)`,
|
||||
transition: "transform 0.3s ease-out",
|
||||
}}
|
||||
>
|
||||
<div className="animate-flutter-body" style={bodyStyle}>
|
||||
<svg viewBox="0 0 50 40" width={size} height={size * 0.8} style={svgStyle}>
|
||||
<defs>
|
||||
<filter id="painterly" x="-20%" y="-20%" width="140%" height="140%">
|
||||
<feTurbulence type="fractalNoise" baseFrequency="0.03" numOctaves="2" result="noise" />
|
||||
@@ -153,7 +168,7 @@ function ButterflyVisual({
|
||||
{/* Far side wings (darker, behind, slightly rotated) */}
|
||||
<g
|
||||
className="animate-flutter-left origin-[38px_20px]"
|
||||
style={{ ...wingStyle, animationDelay: `${delay + 0.08}s` }}
|
||||
style={farWingStyle}
|
||||
transform="translate(6, 0) rotate(8, 38, 20)"
|
||||
>
|
||||
<path d="M38 20 Q20 2 8 10 Q0 18 10 28 Q25 32 38 22" fill={darken(color, 50)} />
|
||||
@@ -165,10 +180,7 @@ function ButterflyVisual({
|
||||
<path d="M38 20 Q20 2 8 10 Q0 18 10 28 Q25 32 38 22" fill={color} />
|
||||
</g>
|
||||
|
||||
<g
|
||||
className="animate-flutter-left origin-[38px_20px]"
|
||||
style={{ ...wingStyle, animationDelay: `${delay + 0.05}s` }}
|
||||
>
|
||||
<g className="animate-flutter-left origin-[38px_20px]" style={lowerWingStyle}>
|
||||
<path d="M38 22 Q25 30 18 38 Q28 42 36 34 Q40 28 38 24" fill={color} />
|
||||
</g>
|
||||
</svg>
|
||||
@@ -358,17 +370,18 @@ export function FloatingButterfly({
|
||||
};
|
||||
}, [mounted]);
|
||||
|
||||
const containerStyle = useMemo(
|
||||
() => ({
|
||||
...style,
|
||||
transform: `translate(${renderState.x}px, ${renderState.y}px)`,
|
||||
}),
|
||||
[style, renderState.x, renderState.y],
|
||||
);
|
||||
|
||||
if (!mounted) return null;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={elementRef}
|
||||
className="absolute pointer-events-none"
|
||||
style={{
|
||||
...style,
|
||||
transform: `translate(${renderState.x}px, ${renderState.y}px)`,
|
||||
}}
|
||||
>
|
||||
<div ref={elementRef} className="absolute pointer-events-none" style={containerStyle}>
|
||||
<ButterflyVisual
|
||||
size={size}
|
||||
color={color}
|
||||
|
||||
@@ -1,5 +1,15 @@
|
||||
import * as React from "react";
|
||||
import { motion, AnimatePresence } from "framer-motion";
|
||||
import { motion, AnimatePresence, type Transition } from "framer-motion";
|
||||
|
||||
const OVERLAY_INITIAL = { opacity: 0 };
|
||||
const OVERLAY_ANIMATE = { opacity: 1 };
|
||||
const OVERLAY_EXIT = { opacity: 0 };
|
||||
const OVERLAY_TRANSITION: Transition = { duration: 0.2 };
|
||||
|
||||
const PANEL_INITIAL = { opacity: 0, scale: 0.95 };
|
||||
const PANEL_ANIMATE = { opacity: 1, scale: 1 };
|
||||
const PANEL_EXIT = { opacity: 0, scale: 0.95 };
|
||||
const PANEL_TRANSITION: Transition = { duration: 0.2, ease: "easeOut" };
|
||||
|
||||
interface CommandDialogProps {
|
||||
trigger: React.ReactNode;
|
||||
@@ -43,18 +53,18 @@ export function CommandDialog({
|
||||
{open && (
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
initial={OVERLAY_INITIAL}
|
||||
animate={OVERLAY_ANIMATE}
|
||||
exit={OVERLAY_EXIT}
|
||||
transition={OVERLAY_TRANSITION}
|
||||
className="fixed inset-0 z-40 bg-black/60 backdrop-blur-sm"
|
||||
onClick={() => setOpen(false)}
|
||||
/>
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
exit={{ opacity: 0, scale: 0.95 }}
|
||||
transition={{ duration: 0.2, ease: "easeOut" }}
|
||||
initial={PANEL_INITIAL}
|
||||
animate={PANEL_ANIMATE}
|
||||
exit={PANEL_EXIT}
|
||||
transition={PANEL_TRANSITION}
|
||||
className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-50 w-full max-w-md rounded-xl border border-white/20 bg-background p-6 space-y-4"
|
||||
>
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -27,6 +27,9 @@ import {
|
||||
} from "lucide-react";
|
||||
import { ClaudeIcon, CodexIcon, SourceControlIcon } from "~/components/mockup";
|
||||
|
||||
const USER_BUBBLE_STYLE = { borderRadius: "16px 2px 16px 16px" };
|
||||
const NEW_BADGE_STYLE = { backgroundColor: "rgba(46, 160, 67, 0.2)" };
|
||||
|
||||
// ── Stagger timing ──────────────────────────────────────
|
||||
|
||||
const D = {
|
||||
@@ -477,7 +480,7 @@ function ChatMessages({ items }: { items: ChatItem[] }) {
|
||||
<div key={i} className="flex justify-end py-1">
|
||||
<div
|
||||
className="bg-mock-surface2 px-2.5 py-1.5 max-w-[85%] leading-none"
|
||||
style={{ borderRadius: "16px 2px 16px 16px" }}
|
||||
style={USER_BUBBLE_STYLE}
|
||||
>
|
||||
<span className="text-[11px] text-mock-fg leading-none">{item.text}</span>
|
||||
</div>
|
||||
@@ -593,7 +596,7 @@ function ExplorerSidebar() {
|
||||
<span className="text-[11px] text-mock-fg-muted truncate min-w-0"> src/pages</span>
|
||||
<span
|
||||
className="text-[10px] text-mock-green-400 px-2 py-[2px] rounded-md flex-shrink-0"
|
||||
style={{ backgroundColor: "rgba(46, 160, 67, 0.2)" }}
|
||||
style={NEW_BADGE_STYLE}
|
||||
>
|
||||
New
|
||||
</span>
|
||||
@@ -658,7 +661,7 @@ function ExplorerSidebar() {
|
||||
{file.isNew && (
|
||||
<span
|
||||
className="text-[10px] text-mock-green-400 px-2 py-[2px] rounded-md flex-shrink-0"
|
||||
style={{ backgroundColor: "rgba(46, 160, 67, 0.2)" }}
|
||||
style={NEW_BADGE_STYLE}
|
||||
>
|
||||
New
|
||||
</span>
|
||||
@@ -683,53 +686,66 @@ const SYNCED_LOADER_DURATION_MS = 950;
|
||||
const DOT_COUNT = 6;
|
||||
const STEP_MS = SYNCED_LOADER_DURATION_MS / DOT_COUNT; // 158.333…ms per step
|
||||
|
||||
interface SyncedLoaderDotProps {
|
||||
dotIndex: number;
|
||||
dotSize: number;
|
||||
gap: number;
|
||||
}
|
||||
|
||||
function SyncedLoaderDot({ dotIndex, dotSize, gap }: SyncedLoaderDotProps) {
|
||||
const col = dotIndex % 2;
|
||||
const row = Math.floor(dotIndex / 2);
|
||||
const sequenceIndex = DOT_SEQUENCE.indexOf(dotIndex as (typeof DOT_SEQUENCE)[number]);
|
||||
const delayMs = -(sequenceIndex * STEP_MS);
|
||||
const style = React.useMemo(
|
||||
() => ({
|
||||
position: "absolute" as const,
|
||||
left: col * (dotSize + gap),
|
||||
top: row * (dotSize + gap),
|
||||
width: dotSize,
|
||||
height: dotSize,
|
||||
borderRadius: "50%",
|
||||
backgroundColor: "#f59e0b",
|
||||
animationName: "synced-snake-dot",
|
||||
animationDuration: `${SYNCED_LOADER_DURATION_MS}ms`,
|
||||
animationTimingFunction: "linear",
|
||||
animationIterationCount: "infinite",
|
||||
animationDelay: `${delayMs}ms`,
|
||||
}),
|
||||
[col, row, dotSize, gap, delayMs],
|
||||
);
|
||||
return <div style={style} />;
|
||||
}
|
||||
|
||||
function SyncedLoader({ size = 11 }: { size?: number }) {
|
||||
const gap = Math.max(1, Math.round(size * 0.12));
|
||||
const dotSize = Math.max(2, Math.floor((size - gap * 2) / 3));
|
||||
const gridW = dotSize * 2 + gap;
|
||||
const gridH = dotSize * 3 + gap * 2;
|
||||
|
||||
const outerStyle = React.useMemo(
|
||||
() => ({
|
||||
width: size,
|
||||
height: size,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexShrink: 0,
|
||||
}),
|
||||
[size],
|
||||
);
|
||||
|
||||
const innerStyle = React.useMemo(
|
||||
() => ({ position: "relative" as const, width: gridW, height: gridH }),
|
||||
[gridW, gridH],
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
flexShrink: 0,
|
||||
}}
|
||||
>
|
||||
<div style={{ position: "relative", width: gridW, height: gridH }}>
|
||||
{Array.from({ length: DOT_COUNT }).map((_, dotIndex) => {
|
||||
const col = dotIndex % 2;
|
||||
const row = Math.floor(dotIndex / 2);
|
||||
// The snake sequence index for this dot: when is it the "head"?
|
||||
const sequenceIndex = DOT_SEQUENCE.indexOf(dotIndex as (typeof DOT_SEQUENCE)[number]);
|
||||
// Negative delay syncs the animation so this dot is the head at t=0 when sequenceIndex=0.
|
||||
// At t=0, headIndex should be 0, meaning sequenceIndex=0 dot is the head.
|
||||
// Each dot is shifted by sequenceIndex steps back in time.
|
||||
const delayMs = -(sequenceIndex * STEP_MS);
|
||||
return (
|
||||
<div
|
||||
key={dotIndex}
|
||||
style={{
|
||||
position: "absolute",
|
||||
left: col * (dotSize + gap),
|
||||
top: row * (dotSize + gap),
|
||||
width: dotSize,
|
||||
height: dotSize,
|
||||
borderRadius: "50%",
|
||||
backgroundColor: "#f59e0b",
|
||||
animationName: "synced-snake-dot",
|
||||
animationDuration: `${SYNCED_LOADER_DURATION_MS}ms`,
|
||||
animationTimingFunction: "linear",
|
||||
animationIterationCount: "infinite",
|
||||
animationDelay: `${delayMs}ms`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
<div style={outerStyle}>
|
||||
<div style={innerStyle}>
|
||||
{Array.from({ length: DOT_COUNT }).map((_, dotIndex) => (
|
||||
<SyncedLoaderDot key={dotIndex} dotIndex={dotIndex} dotSize={dotSize} gap={gap} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -869,15 +885,14 @@ function DesktopMockup() {
|
||||
return () => window.removeEventListener("resize", updateScale);
|
||||
}, []);
|
||||
|
||||
const containerStyle = React.useMemo(() => ({ height: `${1200 * (9 / 16) * scale}px` }), [scale]);
|
||||
const innerStyle = React.useMemo(() => ({ width: 1200, transform: `scale(${scale})` }), [scale]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="w-full overflow-hidden"
|
||||
style={{ height: `${1200 * (9 / 16) * scale}px` }}
|
||||
>
|
||||
<div ref={containerRef} className="w-full overflow-hidden" style={containerStyle}>
|
||||
<div
|
||||
className="mx-auto rounded-xl overflow-hidden border border-mock-border bg-mock-surface0 shadow-[6px_6px_0_rgba(0,0,0,0.4)] origin-top-left"
|
||||
style={{ width: 1200, transform: `scale(${scale})` }}
|
||||
style={innerStyle}
|
||||
>
|
||||
{/* Top-level: left sidebar | center column | explorer sidebar — all full height */}
|
||||
<div className="flex aspect-video">
|
||||
|
||||
@@ -1,5 +1,38 @@
|
||||
import * as React from "react";
|
||||
import { motion, AnimatePresence, useInView, useScroll, useTransform } from "framer-motion";
|
||||
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_SMALL = { opacity: 0, y: 16 };
|
||||
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: Transition = { duration: 0.6, ease: "easeOut" };
|
||||
const EASE_OUT_06_DELAY_015: Transition = { duration: 0.6, delay: 0.15, ease: "easeOut" };
|
||||
const EASE_OUT_06_DELAY_01: Transition = { duration: 0.6, delay: 0.1, ease: "easeOut" };
|
||||
const EASE_OUT_06_DELAY_04: Transition = { duration: 0.6, delay: 0.4, 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 VIEWPORT_40 = { once: true, margin: "-40px" };
|
||||
|
||||
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 {
|
||||
@@ -38,9 +71,9 @@ export function LandingPage({ title, subtitle }: LandingPageProps) {
|
||||
|
||||
{/* Mockup - inside hero so it's above the gradient, positioned to overflow into black section */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 40 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.8, delay: 0.5, ease: "easeOut" }}
|
||||
initial={FADE_IN_UP_40}
|
||||
animate={FADE_IN}
|
||||
transition={EASE_OUT_08_DELAY_05}
|
||||
className="relative px-6 md:px-8 pb-8 md:pb-16"
|
||||
>
|
||||
<div className="max-w-7xl mx-auto">
|
||||
@@ -203,17 +236,17 @@ function Hero({ title, subtitle }: { title: React.ReactNode; subtitle: string })
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<motion.h1
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, ease: "easeOut" }}
|
||||
initial={FADE_IN_UP}
|
||||
animate={FADE_IN}
|
||||
transition={EASE_OUT_06}
|
||||
className="text-3xl md:text-5xl font-medium tracking-tight"
|
||||
>
|
||||
{title}
|
||||
</motion.h1>
|
||||
<motion.p
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.15, ease: "easeOut" }}
|
||||
initial={FADE_IN_UP}
|
||||
animate={FADE_IN}
|
||||
transition={EASE_OUT_06_DELAY_015}
|
||||
className="text-white/70 text-lg leading-relaxed max-w-lg"
|
||||
>
|
||||
{subtitle}
|
||||
@@ -235,10 +268,10 @@ function AgentBadge({ name, icon }: { name: string; icon: React.ReactNode }) {
|
||||
<AnimatePresence>
|
||||
{hovered && (
|
||||
<motion.span
|
||||
initial={{ opacity: 0, y: 4 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
exit={{ opacity: 0, y: 4 }}
|
||||
transition={{ duration: 0.15, ease: "easeOut" }}
|
||||
initial={FADE_IN_UP_4}
|
||||
animate={FADE_IN}
|
||||
exit={FADE_OUT_UP_4}
|
||||
transition={EASE_OUT_015}
|
||||
className="absolute -top-8 left-1/2 -translate-x-1/2 px-2 py-1 rounded bg-white text-black text-xs whitespace-nowrap pointer-events-none"
|
||||
>
|
||||
{name}
|
||||
@@ -260,10 +293,10 @@ function FeatureSection({
|
||||
}) {
|
||||
return (
|
||||
<motion.section
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-60px" }}
|
||||
transition={{ duration: 0.5, ease: "easeOut" }}
|
||||
initial={FADE_IN_UP}
|
||||
whileInView={FADE_IN}
|
||||
viewport={VIEWPORT_60}
|
||||
transition={EASE_OUT_05}
|
||||
className="space-y-8"
|
||||
>
|
||||
<div className="space-y-2">
|
||||
@@ -278,14 +311,14 @@ function FeatureSection({
|
||||
function PrinciplesSection() {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 16 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-40px" }}
|
||||
transition={{ duration: 0.5, ease: "easeOut" }}
|
||||
initial={FADE_IN_UP_SMALL}
|
||||
whileInView={FADE_IN}
|
||||
viewport={VIEWPORT_40}
|
||||
transition={EASE_OUT_05}
|
||||
className="py-32 px-6 md:px-20 max-w-3xl mx-auto text-center"
|
||||
>
|
||||
<p className="text-2xl md:text-4xl font-medium text-white/90">
|
||||
Here's what's under the hood.
|
||||
Here's what's under the hood.
|
||||
</p>
|
||||
</motion.div>
|
||||
);
|
||||
@@ -510,7 +543,7 @@ function SelfHostedDiagram() {
|
||||
{/* SVG curves */}
|
||||
<svg
|
||||
className="absolute inset-0 w-full h-full pointer-events-none"
|
||||
style={{ overflow: "visible" }}
|
||||
style={SVG_OVERFLOW_VISIBLE_STYLE}
|
||||
>
|
||||
{[...paths.left, ...paths.right].map(
|
||||
(d, i) =>
|
||||
@@ -696,38 +729,41 @@ function ShortcutsSection() {
|
||||
);
|
||||
}
|
||||
|
||||
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 <div className="w-[3px] rounded-full bg-white/30" style={style} />;
|
||||
}
|
||||
|
||||
function VoiceWaveform() {
|
||||
const barCount = 48;
|
||||
return (
|
||||
<div className="flex items-center justify-center gap-[3px] h-16">
|
||||
{Array.from({ length: barCount }).map((_, i) => {
|
||||
// Create a natural-looking waveform envelope — louder in center, quieter at edges
|
||||
const center = barCount / 2;
|
||||
const dist = Math.abs(i - center) / center;
|
||||
const envelope = 1 - dist * dist; // quadratic falloff
|
||||
const minH = 4;
|
||||
const maxH = 56;
|
||||
const baseH = minH + (maxH - minH) * envelope;
|
||||
// Vary per-bar so it doesn't look uniform
|
||||
const jitter = Math.sin(i * 2.3) * 0.3 + Math.cos(i * 1.7) * 0.2;
|
||||
const h = Math.max(minH, baseH * (0.5 + 0.5 * Math.abs(jitter + Math.sin(i * 0.8))));
|
||||
|
||||
return (
|
||||
<div
|
||||
key={i}
|
||||
className="w-[3px] rounded-full bg-white/30"
|
||||
style={{
|
||||
height: h,
|
||||
animationName: "voice-bar",
|
||||
animationDuration: `${800 + (i % 5) * 200}ms`,
|
||||
animationTimingFunction: "ease-in-out",
|
||||
animationIterationCount: "infinite",
|
||||
animationDirection: "alternate",
|
||||
animationDelay: `${(i % 7) * 80}ms`,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{Array.from({ length: barCount }).map((_, i) => (
|
||||
<VoiceBar key={i} index={i} barCount={barCount} />
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -925,9 +961,9 @@ function LocalVoiceSection() {
|
||||
function GetStarted() {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
transition={{ duration: 0.6, delay: 0.4, ease: "easeOut" }}
|
||||
initial={FADE_IN_UP}
|
||||
animate={FADE_IN}
|
||||
transition={EASE_OUT_06_DELAY_04}
|
||||
className="pt-10"
|
||||
>
|
||||
<div className="flex flex-row flex-wrap gap-3">
|
||||
@@ -1440,13 +1476,24 @@ function PhoneShowcase() {
|
||||
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 (
|
||||
<div ref={containerRef} className="flex flex-col items-center pt-4 pb-16 gap-20">
|
||||
{/* Arrow + text */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: -10 }}
|
||||
animate={textInView ? { opacity: 1, y: 0 } : {}}
|
||||
transition={{ duration: 0.5 }}
|
||||
initial={FADE_IN_UP_TINY}
|
||||
animate={textAnimate}
|
||||
transition={DURATION_05}
|
||||
className="flex flex-col items-center gap-1.5 px-6"
|
||||
>
|
||||
<svg
|
||||
@@ -1474,13 +1521,10 @@ function PhoneShowcase() {
|
||||
{/* Phone trio — side phones are absolute, start behind center, slide outward with perspective rotation */}
|
||||
<div
|
||||
className="relative flex items-center justify-center overflow-x-clip w-full"
|
||||
style={{ minHeight: 480, perspective: 1200 }}
|
||||
style={PHONE_PERSPECTIVE_STYLE}
|
||||
>
|
||||
{/* Left phone — rotated to face inward */}
|
||||
<motion.div
|
||||
style={{ opacity: sideOpacity, x: leftX, rotateY: -15, scale: 0.97 }}
|
||||
className="w-[160px] md:w-[240px] absolute"
|
||||
>
|
||||
<motion.div style={leftPhoneStyle} className="w-[160px] md:w-[240px] absolute">
|
||||
<img
|
||||
src="/phone-1.png"
|
||||
alt="Paseo sessions list"
|
||||
@@ -1490,9 +1534,9 @@ function PhoneShowcase() {
|
||||
|
||||
{/* Center phone */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 30 }}
|
||||
animate={textInView ? { opacity: 1, y: 0 } : {}}
|
||||
transition={{ duration: 0.6, delay: 0.1, ease: "easeOut" }}
|
||||
initial={FADE_IN_UP_XL}
|
||||
animate={centerPhoneAnimate}
|
||||
transition={EASE_OUT_06_DELAY_01}
|
||||
className="w-[220px] md:w-[240px] relative z-10"
|
||||
>
|
||||
<img
|
||||
@@ -1503,10 +1547,7 @@ function PhoneShowcase() {
|
||||
</motion.div>
|
||||
|
||||
{/* Right phone — rotated to face inward */}
|
||||
<motion.div
|
||||
style={{ opacity: sideOpacity, x: rightX, rotateY: 15, scale: 0.97 }}
|
||||
className="w-[160px] md:w-[240px] absolute"
|
||||
>
|
||||
<motion.div style={rightPhoneStyle} className="w-[160px] md:w-[240px] absolute">
|
||||
<img
|
||||
src="/phone-3.png"
|
||||
alt="Paseo diff view"
|
||||
@@ -1573,10 +1614,10 @@ function CLISection() {
|
||||
function FAQ() {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-60px" }}
|
||||
transition={{ duration: 0.5, ease: "easeOut" }}
|
||||
initial={FADE_IN_UP}
|
||||
whileInView={FADE_IN}
|
||||
viewport={VIEWPORT_60}
|
||||
transition={EASE_OUT_05}
|
||||
className="space-y-6"
|
||||
>
|
||||
<h2 className="text-3xl font-medium">FAQ</h2>
|
||||
@@ -1587,8 +1628,8 @@ function FAQ() {
|
||||
speech providers if you configure them.
|
||||
</FAQItem>
|
||||
<FAQItem question="Does my code leave my machine?">
|
||||
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{" "}
|
||||
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{" "}
|
||||
<a href="/docs/security" className="underline hover:text-white/80">
|
||||
end-to-end encrypted relay
|
||||
</a>
|
||||
@@ -1596,7 +1637,7 @@ function FAQ() {
|
||||
</FAQItem>
|
||||
<FAQItem question="What agents does it support?">
|
||||
Claude Code, Codex, and OpenCode. Each agent runs as its own process using its own CLI.
|
||||
Paseo doesn't modify or wrap their behavior.
|
||||
Paseo doesn't modify or wrap their behavior.
|
||||
</FAQItem>
|
||||
<FAQItem question="Do I need the desktop app?">
|
||||
No. You can run the daemon headless with{" "}
|
||||
@@ -1616,9 +1657,9 @@ function FAQ() {
|
||||
.
|
||||
</FAQItem>
|
||||
<FAQItem question="Can I connect from outside my network?">
|
||||
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{" "}
|
||||
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{" "}
|
||||
<a href="/docs/configuration" className="underline hover:text-white/80">
|
||||
configuration
|
||||
</a>
|
||||
@@ -1626,17 +1667,17 @@ function FAQ() {
|
||||
</FAQItem>
|
||||
<FAQItem question="Do I need git or GitHub?">
|
||||
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.
|
||||
You can run agents anywhere you'd normally work.
|
||||
</FAQItem>
|
||||
<FAQItem question="Can I get banned for using Paseo?">
|
||||
<p>We can't make promises on behalf of providers.</p>
|
||||
<p>We can't make promises on behalf of providers.</p>
|
||||
<p>
|
||||
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.
|
||||
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.
|
||||
</p>
|
||||
<p>I've been using Paseo with all providers for months without issue.</p>
|
||||
<p>I've been using Paseo with all providers for months without issue.</p>
|
||||
</FAQItem>
|
||||
<FAQItem question="How do worktrees work?">
|
||||
When you launch an agent with the worktree option (from the app, desktop, or CLI), Paseo
|
||||
@@ -1655,17 +1696,17 @@ function FAQ() {
|
||||
function SponsorCTA() {
|
||||
return (
|
||||
<motion.div
|
||||
initial={{ opacity: 0, y: 20 }}
|
||||
whileInView={{ opacity: 1, y: 0 }}
|
||||
viewport={{ once: true, margin: "-60px" }}
|
||||
transition={{ duration: 0.5, ease: "easeOut" }}
|
||||
initial={FADE_IN_UP}
|
||||
whileInView={FADE_IN}
|
||||
viewport={VIEWPORT_60}
|
||||
transition={EASE_OUT_05}
|
||||
className="rounded-xl bg-white/5 border border-white/10 p-8 md:p-10 text-left space-y-4 max-w-xl mx-auto"
|
||||
>
|
||||
<div className="text-sm text-muted-foreground leading-relaxed space-y-3">
|
||||
<p>
|
||||
I built Paseo because I wanted better tools for coding agents on my own setup. It's an
|
||||
independent open source project, built around freedom of choice and real workflows. If you
|
||||
like what I'm building, consider becoming a supporter.
|
||||
I built Paseo because I wanted better tools for coding agents on my own setup. It's
|
||||
an independent open source project, built around freedom of choice and real workflows. If
|
||||
you like what I'm building, consider becoming a supporter.
|
||||
</p>
|
||||
<p>- Mo</p>
|
||||
</div>
|
||||
|
||||
@@ -15,6 +15,10 @@ interface StarsContext {
|
||||
const ReleaseCtx = createContext<ReleaseContext>({ version: "" });
|
||||
const StarsCtx = createContext<StarsContext>({ stars: "" });
|
||||
|
||||
const PLAUSIBLE_INIT_SCRIPT = {
|
||||
__html: `window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};plausible.init()`,
|
||||
};
|
||||
|
||||
export function useRelease(): ReleaseContext {
|
||||
return useContext(ReleaseCtx);
|
||||
}
|
||||
@@ -67,11 +71,7 @@ function RootDocument({ children }: Readonly<{ children: ReactNode }>) {
|
||||
<head>
|
||||
<HeadContent />
|
||||
<script async src="https://plausible.io/js/pa-cKNUoWbeH_Iksb2fh82s3.js" />
|
||||
<script
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `window.plausible=window.plausible||function(){(plausible.q=plausible.q||[]).push(arguments)},plausible.init=plausible.init||function(i){plausible.o=i||{}};plausible.init()`,
|
||||
}}
|
||||
/>
|
||||
<script dangerouslySetInnerHTML={PLAUSIBLE_INIT_SCRIPT} />
|
||||
</head>
|
||||
<body className="antialiased bg-background text-foreground">
|
||||
{children}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useMemo } from "react";
|
||||
import { createFileRoute, Link } from "@tanstack/react-router";
|
||||
import { getPosts, formatDate } from "~/posts";
|
||||
import { pageMeta } from "~/meta";
|
||||
@@ -12,6 +13,34 @@ export const Route = createFileRoute("/blog/")({
|
||||
component: BlogIndex,
|
||||
});
|
||||
|
||||
interface PostRowProps {
|
||||
slug: string;
|
||||
title: string;
|
||||
date: string;
|
||||
draft?: boolean;
|
||||
}
|
||||
|
||||
function PostRow({ slug, title, date, draft }: PostRowProps) {
|
||||
const params = useMemo(() => ({ _splat: slug }), [slug]);
|
||||
return (
|
||||
<div className="flex flex-col-reverse items-start md:flex-row md:items-center gap-x-4">
|
||||
<span className="text-lg text-muted-foreground tabular-nums">
|
||||
{formatDate(new Date(date))}
|
||||
</span>
|
||||
<Link
|
||||
to="/blog/$"
|
||||
params={params}
|
||||
className="text-lg text-foreground hover:text-primary transition-colors"
|
||||
>
|
||||
{title}
|
||||
{draft && (
|
||||
<span className="ml-2 text-xs px-2 py-1 bg-primary/20 text-primary rounded">DRAFT</span>
|
||||
)}
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function BlogIndex() {
|
||||
const { drafts } = Route.useSearch();
|
||||
const posts = getPosts(drafts);
|
||||
@@ -25,26 +54,13 @@ function BlogIndex() {
|
||||
)}
|
||||
<div className="space-y-2">
|
||||
{posts.map(({ slug, frontmatter }) => (
|
||||
<div
|
||||
<PostRow
|
||||
key={slug}
|
||||
className="flex flex-col-reverse items-start md:flex-row md:items-center gap-x-4"
|
||||
>
|
||||
<span className="text-lg text-muted-foreground tabular-nums">
|
||||
{formatDate(new Date(frontmatter.date))}
|
||||
</span>
|
||||
<Link
|
||||
to="/blog/$"
|
||||
params={{ _splat: slug }}
|
||||
className="text-lg text-foreground hover:text-primary transition-colors"
|
||||
>
|
||||
{frontmatter.title}
|
||||
{frontmatter.draft && (
|
||||
<span className="ml-2 text-xs px-2 py-1 bg-primary/20 text-primary rounded">
|
||||
DRAFT
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
</div>
|
||||
slug={slug}
|
||||
title={frontmatter.title}
|
||||
date={frontmatter.date}
|
||||
draft={frontmatter.draft}
|
||||
/>
|
||||
))}
|
||||
{posts.length === 0 && <p className="text-muted-foreground">No posts yet.</p>}
|
||||
</div>
|
||||
|
||||
@@ -18,6 +18,10 @@ const navigation = [
|
||||
{ name: "Best practices", href: "/docs/best-practices" },
|
||||
];
|
||||
|
||||
const ACTIVE_OPTIONS_EXACT = { exact: true };
|
||||
const MOBILE_ACTIVE_PROPS = { className: "text-foreground" };
|
||||
const DESKTOP_ACTIVE_PROPS = { className: "bg-muted text-foreground" };
|
||||
|
||||
function DocsLayout() {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
@@ -32,9 +36,9 @@ function DocsLayout() {
|
||||
<Link
|
||||
key={item.href}
|
||||
to={item.href}
|
||||
activeOptions={{ exact: true }}
|
||||
activeOptions={ACTIVE_OPTIONS_EXACT}
|
||||
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
activeProps={{ className: "text-foreground" }}
|
||||
activeProps={MOBILE_ACTIVE_PROPS}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
@@ -54,9 +58,9 @@ function DocsLayout() {
|
||||
<Link
|
||||
key={item.href}
|
||||
to={item.href}
|
||||
activeOptions={{ exact: true }}
|
||||
activeOptions={ACTIVE_OPTIONS_EXACT}
|
||||
className="block px-3 py-2 text-sm rounded-md text-muted-foreground hover:text-foreground hover:bg-muted transition-colors"
|
||||
activeProps={{ className: "bg-muted text-foreground" }}
|
||||
activeProps={DESKTOP_ACTIVE_PROPS}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
|
||||
@@ -17,22 +17,23 @@ function BestPractices() {
|
||||
<div>
|
||||
<h1 className="text-3xl font-medium font-title mb-4">Best Practices</h1>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
What I've learned from using Paseo daily. Not rules, just patterns that have worked for
|
||||
me.
|
||||
What I've learned from using Paseo daily. Not rules, just patterns that have worked
|
||||
for me.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-xl font-medium">Agents replace typing, not thinking</h2>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Your role has changed. You're no longer the one writing code line by line. You're the one
|
||||
making decisions: what to build, how it should work, what the architecture looks like. The
|
||||
agent executes, but you direct.
|
||||
Your role has changed. You're no longer the one writing code line by line.
|
||||
You're the one making decisions: what to build, how it should work, what the
|
||||
architecture looks like. The agent executes, but you direct.
|
||||
</p>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
You can't just say "implement feature X" and walk away. You still have to do the hard
|
||||
part: deciding what to build, how it fits into the system, what trade-offs to make.
|
||||
Thinking is not optional. At least for now, agents replace the typing, not the thinking.
|
||||
You can't just say "implement feature X" and walk away. You still have to
|
||||
do the hard part: deciding what to build, how it fits into the system, what trade-offs to
|
||||
make. Thinking is not optional. At least for now, agents replace the typing, not the
|
||||
thinking.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
@@ -41,16 +42,17 @@ function BestPractices() {
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
The agent needs a way to verify its work. TDD is one implementation of this pattern: get
|
||||
the agent to write a failing test, verify it fails for the right reasons, then tell it to
|
||||
make the test pass. The agent can loop on its own because it knows what "done" means.
|
||||
make the test pass. The agent can loop on its own because it knows what "done"
|
||||
means.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-xl font-medium">Invest in tooling</h2>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
It's not just test runners. For web apps, something like Playwright MCP lets the agent
|
||||
take screenshots and verify UI changes. For a SaaS app I built a CLI that wraps all the
|
||||
business logic so the agent could launch jobs, check statuses, and scrape data without
|
||||
It's not just test runners. For web apps, something like Playwright MCP lets the
|
||||
agent take screenshots and verify UI changes. For a SaaS app I built a CLI that wraps all
|
||||
the business logic so the agent could launch jobs, check statuses, and scrape data without
|
||||
going through the UI.
|
||||
</p>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
@@ -62,22 +64,22 @@ function BestPractices() {
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-xl font-medium">Agents are cheap</h2>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Don't be shy about running multiple agents. Paseo lets you launch agents in isolated
|
||||
Don't be shy about running multiple agents. Paseo lets you launch agents in isolated
|
||||
worktrees. Kick one off with voice while walking, then kick off another. They work
|
||||
independently. You get a notification when they're done.
|
||||
independently. You get a notification when they're done.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-xl font-medium">Use voice extensively</h2>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
It's much more natural to use voice to communicate ideas and pull them out of your brain.
|
||||
The agent will parse and organize your thoughts better than if you try to write the
|
||||
perfect prompt. You don't need to organize anything. Just talk.
|
||||
It's much more natural to use voice to communicate ideas and pull them out of your
|
||||
brain. The agent will parse and organize your thoughts better than if you try to write the
|
||||
perfect prompt. You don't need to organize anything. Just talk.
|
||||
</p>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Current speech-to-text models are really good. They catch accents, acronyms, technical
|
||||
terms. And even when they don't, the LLM will infer what you meant.
|
||||
terms. And even when they don't, the LLM will infer what you meant.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
@@ -85,22 +87,22 @@ function BestPractices() {
|
||||
<h2 className="text-xl font-medium">Understand the type of work</h2>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Sometimes you need to plan: design a spec, verify it, get the agent to follow through.
|
||||
Maybe it takes a couple of agents to work through it. Other times it's conversational:
|
||||
kick off a single agent and start talking, asking questions. Match your approach to the
|
||||
task.
|
||||
Maybe it takes a couple of agents to work through it. Other times it's
|
||||
conversational: kick off a single agent and start talking, asking questions. Match your
|
||||
approach to the task.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-xl font-medium">Iterate and refactor often</h2>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Don't expect perfect. Expect working. Make it work, make it correct, make it beautiful.
|
||||
Each iteration gets you closer. With tests, refactoring is cheap.
|
||||
Don't expect perfect. Expect working. Make it work, make it correct, make it
|
||||
beautiful. Each iteration gets you closer. With tests, refactoring is cheap.
|
||||
</p>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
I don't let myself add too many features before stopping to refactor. Sometimes I kick off
|
||||
an agent and have it trace code paths, explain dependencies, show me how modules connect.
|
||||
I make mental notes during code review and circle back.
|
||||
I don't let myself add too many features before stopping to refactor. Sometimes I
|
||||
kick off an agent and have it trace code paths, explain dependencies, show me how modules
|
||||
connect. I make mental notes during code review and circle back.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
@@ -108,25 +110,25 @@ function BestPractices() {
|
||||
<h2 className="text-xl font-medium">Use agents to check agents</h2>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
If an agent implements something and you ask it to review its own work, it will never find
|
||||
issues. Launch a separate agent with a fresh context to review the first agent's code. It
|
||||
will catch things the first agent missed or glossed over. An agent might say it's done
|
||||
when it's not. Another agent can detect that.
|
||||
issues. Launch a separate agent with a fresh context to review the first agent's
|
||||
code. It will catch things the first agent missed or glossed over. An agent might say
|
||||
it's done when it's not. Another agent can detect that.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-xl font-medium">Learn your agents' quirks</h2>
|
||||
<h2 className="text-xl font-medium">Learn your agents' quirks</h2>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
People argue about which model is better. That's the wrong question. Each model has
|
||||
People argue about which model is better. That's the wrong question. Each model has
|
||||
strengths and weaknesses. Knowing them is more useful than chasing benchmarks. Benchmarks
|
||||
don't mean anything. You need to try the models yourself to form an opinion.
|
||||
don't mean anything. You need to try the models yourself to form an opinion.
|
||||
</p>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
I use Claude Code as my main driver because it's quick and uses tools well. But sometimes
|
||||
it jumps to conclusions and gives up too easily. Codex is frustratingly slow but goes
|
||||
deep, doesn't stop, and is methodical. It's also stubborn and too serious. These aren't
|
||||
good or bad traits, just differences you learn to work around. Use the right model for the
|
||||
job.
|
||||
I use Claude Code as my main driver because it's quick and uses tools well. But
|
||||
sometimes it jumps to conclusions and gives up too easily. Codex is frustratingly slow but
|
||||
goes deep, doesn't stop, and is methodical. It's also stubborn and too serious.
|
||||
These aren't good or bad traits, just differences you learn to work around. Use the
|
||||
right model for the job.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -25,8 +25,9 @@ function CLI() {
|
||||
<div>
|
||||
<h1 className="text-3xl font-medium font-title mb-4">CLI</h1>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
The Paseo CLI lets you manage agents from your terminal. It's the same interface exposed
|
||||
by the daemon's API, so anything you can do in the app you can do from the command line.
|
||||
The Paseo CLI lets you manage agents from your terminal. It's the same interface
|
||||
exposed by the daemon's API, so anything you can do in the app you can do from the
|
||||
command line.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -96,14 +97,14 @@ paseo ls -a -g --json # Full list as JSON`}</pre>
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-xl font-medium">Streaming output</h2>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Use <code className="font-mono">paseo attach</code> to stream an agent's output in
|
||||
Use <code className="font-mono">paseo attach</code> to stream an agent's output in
|
||||
real-time:
|
||||
</p>
|
||||
<Code>
|
||||
<pre className="text-white/80">{`paseo attach abc123 # Attach to agent (Ctrl+C to detach)`}</pre>
|
||||
</Code>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Agent IDs can be shortened — <code className="font-mono">abc</code> works if it's
|
||||
Agent IDs can be shortened — <code className="font-mono">abc</code> works if it's
|
||||
unambiguous.
|
||||
</p>
|
||||
</section>
|
||||
@@ -164,7 +165,7 @@ paseo permit deny <id> --all # Deny all pending`}</pre>
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-xl font-medium">Agent modes</h2>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Change an agent's operational mode (provider-specific):
|
||||
Change an agent's operational mode (provider-specific):
|
||||
</p>
|
||||
<Code>
|
||||
<pre className="text-white/80">{`paseo agent mode <id> --list # Show available modes
|
||||
|
||||
@@ -84,7 +84,7 @@ function GettingStarted() {
|
||||
>
|
||||
GitHub releases page
|
||||
</a>
|
||||
. Open it and you're done.
|
||||
. Open it and you're done.
|
||||
</p>
|
||||
<p className="text-white/60">
|
||||
The desktop app bundles and manages its own daemon automatically, so you do not need a
|
||||
|
||||
@@ -43,12 +43,12 @@ function Providers() {
|
||||
</p>
|
||||
<ul className="text-white/60 space-y-3 list-disc list-inside">
|
||||
<li>
|
||||
<code className="font-mono">claude</code> — Anthropic's Claude Code. Multi-tool
|
||||
<code className="font-mono">claude</code> — Anthropic's Claude Code. Multi-tool
|
||||
assistant with MCP support, streaming, and deep reasoning.
|
||||
</li>
|
||||
<li>
|
||||
<code className="font-mono">codex</code> — OpenAI's Codex workspace agent with sandbox
|
||||
controls and optional network access.
|
||||
<code className="font-mono">codex</code> — OpenAI's Codex workspace agent with
|
||||
sandbox controls and optional network access.
|
||||
</li>
|
||||
<li>
|
||||
<code className="font-mono">opencode</code> — Open-source coding assistant with
|
||||
@@ -90,14 +90,15 @@ function Providers() {
|
||||
Client Protocol over stdio.
|
||||
</li>
|
||||
<li>
|
||||
<strong>Disable</strong> a provider you don't use.
|
||||
<strong>Disable</strong> a provider you don't use.
|
||||
</li>
|
||||
</ul>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Provider IDs must be lowercase alphanumeric with hyphens (
|
||||
<code className="font-mono">/^[a-z][a-z0-9-]*$/</code>). Every custom entry needs{" "}
|
||||
<code className="font-mono">extends</code> (a first-class provider ID or{" "}
|
||||
<code className="font-mono">"acp"</code>) and a <code className="font-mono">label</code>.
|
||||
<code className="font-mono">"acp"</code>) and a{" "}
|
||||
<code className="font-mono">label</code>.
|
||||
</p>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
The examples below are a quick tour. The full, up-to-date reference is on GitHub:{" "}
|
||||
@@ -139,7 +140,7 @@ function Providers() {
|
||||
Z.AI exposes GLM models through an Anthropic-compatible endpoint. Point{" "}
|
||||
<code className="font-mono">ANTHROPIC_BASE_URL</code> at their API and use{" "}
|
||||
<code className="font-mono">ANTHROPIC_AUTH_TOKEN</code> for the key. Third-party endpoints
|
||||
don't support Anthropic's server-side tools, so disable{" "}
|
||||
don't support Anthropic's server-side tools, so disable{" "}
|
||||
<code className="font-mono">WebSearch</code>.
|
||||
</p>
|
||||
<pre className="bg-card border border-border rounded-lg p-4 font-mono text-sm overflow-x-auto text-white/80">
|
||||
@@ -169,8 +170,8 @@ function Providers() {
|
||||
<section className="space-y-4">
|
||||
<h2 className="text-xl font-medium">Alibaba Cloud (Qwen) coding plan</h2>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Alibaba's coding plan routes Claude Code to Qwen models via an Anthropic-compatible API.
|
||||
Subscription keys look like <code className="font-mono">sk-sp-...</code> and must be
|
||||
Alibaba's coding plan routes Claude Code to Qwen models via an Anthropic-compatible
|
||||
API. Subscription keys look like <code className="font-mono">sk-sp-...</code> and must be
|
||||
created in the Singapore region.
|
||||
</p>
|
||||
<pre className="bg-card border border-border rounded-lg p-4 font-mono text-sm overflow-x-auto text-white/80">
|
||||
@@ -253,8 +254,8 @@ function Providers() {
|
||||
>
|
||||
ACP
|
||||
</a>{" "}
|
||||
over stdio can be added with <code className="font-mono">extends: "acp"</code> and a{" "}
|
||||
<code className="font-mono">command</code>. Paseo spawns the process, sends an{" "}
|
||||
over stdio can be added with <code className="font-mono">extends: "acp"</code>{" "}
|
||||
and a <code className="font-mono">command</code>. Paseo spawns the process, sends an{" "}
|
||||
<code className="font-mono">initialize</code> JSON-RPC request, and the agent reports its
|
||||
capabilities, modes, and models at runtime.
|
||||
</p>
|
||||
|
||||
@@ -83,8 +83,8 @@ function Security() {
|
||||
<code className="font-mono">$PASEO_HOME/daemon-keypair.json</code>
|
||||
</li>
|
||||
<li>
|
||||
When you scan the QR code or click the pairing link, your phone receives the daemon's
|
||||
public key
|
||||
When you scan the QR code or click the pairing link, your phone receives the
|
||||
daemon's public key
|
||||
</li>
|
||||
<li>
|
||||
Your phone sends a handshake message with its own public key. The daemon will not accept
|
||||
@@ -100,15 +100,15 @@ function Security() {
|
||||
message contents, forge messages, or derive encryption keys from observing the handshake.
|
||||
</p>
|
||||
|
||||
<h3 className="text-lg font-medium mt-6">Why the relay can't attack you</h3>
|
||||
<h3 className="text-lg font-medium mt-6">Why the relay can't attack you</h3>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
The daemon requires a valid cryptographic handshake before processing any commands. A
|
||||
compromised relay cannot:
|
||||
</p>
|
||||
<ul className="text-white/60 space-y-2 list-disc list-inside">
|
||||
<li>
|
||||
<strong className="text-white/80">Send commands</strong> — Without your phone's private
|
||||
key, it cannot complete the handshake
|
||||
<strong className="text-white/80">Send commands</strong> — Without your phone's
|
||||
private key, it cannot complete the handshake
|
||||
</li>
|
||||
<li>
|
||||
<strong className="text-white/80">Read your traffic</strong> — All messages are
|
||||
@@ -126,9 +126,9 @@ function Security() {
|
||||
|
||||
<h3 className="text-lg font-medium mt-6">Trust model</h3>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
The QR code or pairing link is the trust anchor. It contains the daemon's public key,
|
||||
which is required to establish the encrypted connection. Treat it like a password — don't
|
||||
share it publicly.
|
||||
The QR code or pairing link is the trust anchor. It contains the daemon's public key,
|
||||
which is required to establish the encrypted connection. Treat it like a password —
|
||||
don't share it publicly.
|
||||
</p>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
If you believe a pairing offer has been compromised, restart the daemon to generate a new
|
||||
@@ -226,7 +226,7 @@ function Security() {
|
||||
<code className="font-mono">*.localhost</code>, and all IP addresses
|
||||
</li>
|
||||
<li>
|
||||
<code className="font-mono">['.example.com']</code>: allow{" "}
|
||||
<code className="font-mono">['.example.com']</code>: allow{" "}
|
||||
<code className="font-mono">example.com</code> and any subdomain, plus defaults
|
||||
</li>
|
||||
<li>
|
||||
@@ -244,8 +244,8 @@ function Security() {
|
||||
</p>
|
||||
<ul className="text-white/60 space-y-2 list-disc list-inside">
|
||||
<li>
|
||||
<strong className="text-white/80">Claude Code</strong> — authenticates via Anthropic's
|
||||
OAuth flow, stored in <code className="font-mono">~/.claude/</code>
|
||||
<strong className="text-white/80">Claude Code</strong> — authenticates via
|
||||
Anthropic's OAuth flow, stored in <code className="font-mono">~/.claude/</code>
|
||||
</li>
|
||||
<li>
|
||||
<strong className="text-white/80">Codex</strong> — uses your OpenAI API key or OAuth
|
||||
@@ -267,8 +267,8 @@ function Security() {
|
||||
<h2 className="text-xl font-medium">Recommendations</h2>
|
||||
<ul className="text-white/60 space-y-3 list-disc list-inside">
|
||||
<li>
|
||||
<strong className="text-white/80">Use the relay</strong> for mobile access — it's the
|
||||
simplest option and all traffic is end-to-end encrypted
|
||||
<strong className="text-white/80">Use the relay</strong> for mobile access — it's
|
||||
the simplest option and all traffic is end-to-end encrypted
|
||||
</li>
|
||||
<li>
|
||||
<strong className="text-white/80">Treat the QR code like a password</strong> — anyone
|
||||
|
||||
@@ -28,8 +28,8 @@ function Skills() {
|
||||
Paseo ships orchestration skills that teach coding agents (Claude Code, Codex) how to use
|
||||
the Paseo CLI to spawn, coordinate, and manage other agents. Skills are slash commands
|
||||
your agent can invoke — they provide the prompts, context, and workflows so agents know
|
||||
how to orchestrate without you writing boilerplate. Install them from the desktop app's
|
||||
Integrations settings or via the CLI.
|
||||
how to orchestrate without you writing boilerplate. Install them from the desktop
|
||||
app's Integrations settings or via the CLI.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -60,7 +60,8 @@ function Skills() {
|
||||
command reference so agents know how to run commands.
|
||||
</p>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Not typically invoked directly by users — it's a reference that other skills depend on.
|
||||
Not typically invoked directly by users — it's a reference that other skills depend
|
||||
on.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
@@ -71,8 +72,8 @@ function Skills() {
|
||||
</h2>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Hands off your current task to another agent with full context. The receiving agent gets a
|
||||
comprehensive prompt with: task description, relevant files, what's been tried, decisions
|
||||
made, and acceptance criteria.
|
||||
comprehensive prompt with: task description, relevant files, what's been tried,
|
||||
decisions made, and acceptance criteria.
|
||||
</p>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Default provider is Codex. Can specify Claude (sonnet/opus). Supports{" "}
|
||||
|
||||
@@ -59,7 +59,7 @@ function Worktrees() {
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Drop a <code className="font-mono">paseo.json</code> in your repo root. Paseo reads it
|
||||
from the committed version of the base branch you picked, so uncommitted changes in other
|
||||
branches don't apply.
|
||||
branches don't apply.
|
||||
</p>
|
||||
<Code>
|
||||
<pre className="text-white/80">{`{
|
||||
@@ -113,7 +113,7 @@ function Worktrees() {
|
||||
<code className="font-mono">scripts</code> are named commands you can run inside a
|
||||
worktree on demand. Mark one as a <em>service</em> and Paseo supervises it as a
|
||||
long-running process, assigns it a port, and routes HTTP traffic to it through the
|
||||
daemon's reverse proxy.
|
||||
daemon's reverse proxy.
|
||||
</p>
|
||||
|
||||
<h3 className="text-lg font-medium mt-4">Plain scripts</h3>
|
||||
@@ -167,9 +167,9 @@ http://<script>.<project>.localhost:<daemon-port>`}</pre>
|
||||
|
||||
<h3 className="text-lg font-medium mt-6">Service-to-service</h3>
|
||||
<p className="text-white/60 leading-relaxed">
|
||||
Services launched from the same workspace see each other's ports and proxy URLs. Given{" "}
|
||||
<code className="font-mono">web</code> and <code className="font-mono">api</code> above,
|
||||
each process gets:
|
||||
Services launched from the same workspace see each other's ports and proxy URLs.
|
||||
Given <code className="font-mono">web</code> and <code className="font-mono">api</code>{" "}
|
||||
above, each process gets:
|
||||
</p>
|
||||
<Code>
|
||||
<pre className="text-white/80">{`PASEO_PORT=3000 # this service's port
|
||||
@@ -219,7 +219,7 @@ PASEO_SERVICE_WEB_URL=http://web.my-app.localhost:6767`}</pre>
|
||||
<code className="font-mono">$PASEO_WORKTREE_PATH</code> — the worktree directory
|
||||
</li>
|
||||
<li>
|
||||
<code className="font-mono">$PASEO_BRANCH_NAME</code> — the worktree's branch
|
||||
<code className="font-mono">$PASEO_BRANCH_NAME</code> — the worktree's branch
|
||||
</li>
|
||||
<li>
|
||||
<code className="font-mono">$PASEO_WORKTREE_PORT</code> — legacy per-worktree port
|
||||
@@ -229,10 +229,10 @@ PASEO_SERVICE_WEB_URL=http://web.my-app.localhost:6767`}</pre>
|
||||
<p className="text-white/60 leading-relaxed">Services additionally get:</p>
|
||||
<ul className="text-white/60 space-y-2 list-disc list-inside">
|
||||
<li>
|
||||
<code className="font-mono">$PASEO_PORT</code> — this service's assigned port
|
||||
<code className="font-mono">$PASEO_PORT</code> — this service's assigned port
|
||||
</li>
|
||||
<li>
|
||||
<code className="font-mono">$PASEO_URL</code> — this service's proxy URL
|
||||
<code className="font-mono">$PASEO_URL</code> — this service's proxy URL
|
||||
</li>
|
||||
<li>
|
||||
<code className="font-mono">$PASEO_SERVICE_<NAME>_PORT</code> /{" "}
|
||||
|
||||
@@ -25,7 +25,7 @@ function Privacy() {
|
||||
|
||||
<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't send us any data.</p>
|
||||
<p>Nothing. Paseo runs on your machine and doesn't send us any data.</p>
|
||||
</section>
|
||||
|
||||
<section className="space-y-3">
|
||||
@@ -48,7 +48,8 @@ function Privacy() {
|
||||
<section className="space-y-3">
|
||||
<h2 className="text-xl font-medium text-white">Analytics and tracking</h2>
|
||||
<p>
|
||||
We don't use analytics, tracking pixels, cookies, or ads. The app doesn't phone home.
|
||||
We don't use analytics, tracking pixels, cookies, or ads. The app doesn't
|
||||
phone home.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
@@ -57,7 +58,7 @@ function Privacy() {
|
||||
<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't manage or intercept those API calls.
|
||||
Paseo doesn't manage or intercept those API calls.
|
||||
</p>
|
||||
<p>
|
||||
If you use voice features with cloud providers (OpenAI speech), your voice data is
|
||||
@@ -66,8 +67,8 @@ function Privacy() {
|
||||
</section>
|
||||
|
||||
<section className="space-y-3">
|
||||
<h2 className="text-xl font-medium text-white">We don't sell your data</h2>
|
||||
<p>We don't have your data to sell. Paseo is self-hosted and local-first.</p>
|
||||
<h2 className="text-xl font-medium text-white">We don't sell your data</h2>
|
||||
<p>We don't have your data to sell. Paseo is self-hosted and local-first.</p>
|
||||
</section>
|
||||
|
||||
<section className="space-y-3">
|
||||
|
||||
Reference in New Issue
Block a user