mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Rework docs navigation and move alternatives to top-level pages (#1487)
* Rework docs navigation and move alternatives to top-level pages - Add nested docs nav with categories, collapsible groups, breadcrumbs, and a right-hand page outline with heading slugs. - Move alternatives out of /docs/alternatives into /alternatives/* marketing pages with 301 redirects. - Add github-slugger for heading IDs and require explicit width on SiteShell so new pages can't accidentally pick the narrow prose layout. - Remove the best-practices doc from public-docs. * Make docs content column prose-width
This commit is contained in:
1
package-lock.json
generated
1
package-lock.json
generated
@@ -39223,6 +39223,7 @@
|
||||
"@tanstack/react-router": "^1.166.4",
|
||||
"@tanstack/react-start": "^1.166.4",
|
||||
"framer-motion": "^12.35.2",
|
||||
"github-slugger": "^2.0.0",
|
||||
"lucide-react": "^1.7.0",
|
||||
"react": "^19.1.4",
|
||||
"react-dom": "^19.1.4",
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"@tanstack/react-router": "^1.166.4",
|
||||
"@tanstack/react-start": "^1.166.4",
|
||||
"framer-motion": "^12.35.2",
|
||||
"github-slugger": "^2.0.0",
|
||||
"lucide-react": "^1.7.0",
|
||||
"react": "^19.1.4",
|
||||
"react-dom": "^19.1.4",
|
||||
|
||||
38
packages/website/src/components/alternative-page.tsx
Normal file
38
packages/website/src/components/alternative-page.tsx
Normal file
@@ -0,0 +1,38 @@
|
||||
import { DocsMarkdown } from "~/components/docs-markdown";
|
||||
import { SiteShell } from "~/components/site-shell";
|
||||
import { getAlternativePage } from "~/data/alternative-pages";
|
||||
import { pageMeta } from "~/meta";
|
||||
|
||||
export function alternativeRouteOptions(slug: string) {
|
||||
const page = getAlternativePage(slug);
|
||||
|
||||
return {
|
||||
head: () =>
|
||||
pageMeta(
|
||||
page?.title ?? "Alternative - Paseo",
|
||||
page?.description ?? "",
|
||||
`/alternatives/${slug}`,
|
||||
),
|
||||
component: function AlternativePageRoute() {
|
||||
return <AlternativePageContent slug={slug} />;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function AlternativePageContent({ slug }: { slug: string }) {
|
||||
const page = getAlternativePage(slug);
|
||||
|
||||
if (!page) {
|
||||
return (
|
||||
<SiteShell width="default">
|
||||
<p className="text-muted-foreground">Page not found.</p>
|
||||
</SiteShell>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<SiteShell width="default">
|
||||
<DocsMarkdown>{page.content}</DocsMarkdown>
|
||||
</SiteShell>
|
||||
);
|
||||
}
|
||||
34
packages/website/src/components/docs-breadcrumbs.tsx
Normal file
34
packages/website/src/components/docs-breadcrumbs.tsx
Normal file
@@ -0,0 +1,34 @@
|
||||
import { Link } from "@tanstack/react-router";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
import { type Doc, type DocsNavNode, getDocBreadcrumbGroups } from "~/docs";
|
||||
|
||||
interface DocsBreadcrumbsProps {
|
||||
doc: Doc;
|
||||
tree: DocsNavNode[];
|
||||
}
|
||||
|
||||
export function DocsBreadcrumbs({ doc, tree }: DocsBreadcrumbsProps) {
|
||||
const groups = getDocBreadcrumbGroups(doc, tree);
|
||||
|
||||
return (
|
||||
<nav aria-label="Breadcrumb" className="not-prose mb-6">
|
||||
<ol className="flex flex-wrap items-center gap-2 text-sm text-muted-foreground">
|
||||
<li>
|
||||
<Link to="/docs" className="hover:text-foreground transition-colors">
|
||||
Docs
|
||||
</Link>
|
||||
</li>
|
||||
{groups.map((group) => (
|
||||
<li key={group.label} className="flex items-center gap-2">
|
||||
<ChevronRight size={14} className="text-border" />
|
||||
<span>{group.label}</span>
|
||||
</li>
|
||||
))}
|
||||
<li className="flex items-center gap-2">
|
||||
<ChevronRight size={14} className="text-border" />
|
||||
<span className="text-foreground">{doc.frontmatter.nav}</span>
|
||||
</li>
|
||||
</ol>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -70,12 +70,14 @@ const docsMarkdownComponents: Components = {
|
||||
|
||||
export function DocsMarkdown({ children }: { children: string }) {
|
||||
return (
|
||||
<ReactMarkdown
|
||||
remarkPlugins={docsRemarkPlugins}
|
||||
rehypePlugins={docsRehypePlugins}
|
||||
components={docsMarkdownComponents}
|
||||
>
|
||||
{children}
|
||||
</ReactMarkdown>
|
||||
<div className="docs-prose">
|
||||
<ReactMarkdown
|
||||
remarkPlugins={docsRemarkPlugins}
|
||||
rehypePlugins={docsRehypePlugins}
|
||||
components={docsMarkdownComponents}
|
||||
>
|
||||
{children}
|
||||
</ReactMarkdown>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
152
packages/website/src/components/docs-nav.tsx
Normal file
152
packages/website/src/components/docs-nav.tsx
Normal file
@@ -0,0 +1,152 @@
|
||||
import { Link, useLocation } from "@tanstack/react-router";
|
||||
import { ChevronDown, ChevronRight } from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { type DocsNavNode } from "~/docs";
|
||||
|
||||
interface DocsNavProps {
|
||||
nodes: DocsNavNode[];
|
||||
mobile?: boolean;
|
||||
onNavigate?: () => void;
|
||||
}
|
||||
|
||||
const ACTIVE_OPTIONS_EXACT = { exact: true };
|
||||
|
||||
function nodeContainsHref(node: DocsNavNode, href: string): boolean {
|
||||
if (node.type === "page") return node.href === href;
|
||||
return node.children.some((child) => nodeContainsHref(child, href));
|
||||
}
|
||||
|
||||
function clsx(...classes: (string | false | null | undefined)[]): string {
|
||||
return classes.filter(Boolean).join(" ");
|
||||
}
|
||||
|
||||
function PageLink({
|
||||
node,
|
||||
mobile,
|
||||
onNavigate,
|
||||
}: {
|
||||
node: Extract<DocsNavNode, { type: "page" }>;
|
||||
mobile?: boolean;
|
||||
onNavigate?: () => void;
|
||||
}) {
|
||||
const location = useLocation();
|
||||
const isActive = location.pathname === node.href;
|
||||
|
||||
return (
|
||||
<Link
|
||||
to={node.href}
|
||||
activeOptions={ACTIVE_OPTIONS_EXACT}
|
||||
onClick={onNavigate}
|
||||
className={clsx(
|
||||
"block px-3 py-2 text-sm rounded-md transition-colors",
|
||||
mobile
|
||||
? "text-muted-foreground hover:text-foreground"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-muted",
|
||||
isActive && (mobile ? "text-foreground" : "bg-muted text-foreground"),
|
||||
)}
|
||||
>
|
||||
{node.label}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
function GroupNode({
|
||||
node,
|
||||
mobile,
|
||||
onNavigate,
|
||||
}: {
|
||||
node: Extract<DocsNavNode, { type: "group" }>;
|
||||
mobile?: boolean;
|
||||
onNavigate?: () => void;
|
||||
}) {
|
||||
const location = useLocation();
|
||||
const currentHref = location.pathname;
|
||||
const containsActive = useMemo(() => nodeContainsHref(node, currentHref), [node, currentHref]);
|
||||
const [isOpen, setIsOpen] = useState(containsActive);
|
||||
const toggle = useCallback(() => setIsOpen((open) => !open), []);
|
||||
|
||||
useEffect(() => {
|
||||
setIsOpen(containsActive);
|
||||
}, [containsActive]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<button
|
||||
type="button"
|
||||
onClick={toggle}
|
||||
className={clsx(
|
||||
"w-full flex items-center justify-between gap-2 px-3 py-2 text-sm rounded-md transition-colors",
|
||||
mobile
|
||||
? "text-muted-foreground hover:text-foreground"
|
||||
: "text-muted-foreground hover:text-foreground hover:bg-muted",
|
||||
containsActive && "text-foreground",
|
||||
)}
|
||||
>
|
||||
<span>{node.label}</span>
|
||||
{isOpen ? <ChevronDown size={14} /> : <ChevronRight size={14} />}
|
||||
</button>
|
||||
{isOpen && (
|
||||
<div className="ml-3 pl-3 border-l border-border space-y-0.5">
|
||||
<NavTree nodes={node.children} mobile={mobile} onNavigate={onNavigate} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CategoryNode({
|
||||
node,
|
||||
mobile,
|
||||
onNavigate,
|
||||
}: {
|
||||
node: Extract<DocsNavNode, { type: "category" }>;
|
||||
mobile?: boolean;
|
||||
onNavigate?: () => void;
|
||||
}) {
|
||||
return (
|
||||
<div className={mobile ? "space-y-1" : "space-y-1 mt-6 first:mt-0"}>
|
||||
<div className="px-3 py-2 text-xs font-medium text-foreground">{node.label}</div>
|
||||
<NavTree nodes={node.children} mobile={mobile} onNavigate={onNavigate} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function NavTree({ nodes, mobile, onNavigate }: DocsNavProps) {
|
||||
return (
|
||||
<div className="space-y-0.5">
|
||||
{nodes.map((node) => {
|
||||
if (node.type === "category") {
|
||||
return (
|
||||
<CategoryNode
|
||||
key={`category-${node.label}`}
|
||||
node={node}
|
||||
mobile={mobile}
|
||||
onNavigate={onNavigate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (node.type === "group") {
|
||||
return (
|
||||
<GroupNode
|
||||
key={`group-${node.segment}`}
|
||||
node={node}
|
||||
mobile={mobile}
|
||||
onNavigate={onNavigate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<PageLink key={`page-${node.href}`} node={node} mobile={mobile} onNavigate={onNavigate} />
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function DocsNav({ nodes, mobile, onNavigate }: DocsNavProps) {
|
||||
return (
|
||||
<div className={mobile ? undefined : "-ml-3"}>
|
||||
<NavTree nodes={nodes} mobile={mobile} onNavigate={onNavigate} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
109
packages/website/src/components/docs-outline.tsx
Normal file
109
packages/website/src/components/docs-outline.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { type DocHeading } from "~/docs";
|
||||
|
||||
interface DocsOutlineProps {
|
||||
headings: DocHeading[];
|
||||
}
|
||||
|
||||
function clsx(...classes: (string | false | null | undefined)[]): string {
|
||||
return classes.filter(Boolean).join(" ");
|
||||
}
|
||||
|
||||
function OutlineLink({
|
||||
heading,
|
||||
active,
|
||||
onActivate,
|
||||
}: {
|
||||
heading: DocHeading;
|
||||
active: boolean;
|
||||
onActivate: (id: string) => void;
|
||||
}) {
|
||||
const handleClick = useCallback(
|
||||
(event: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
event.preventDefault();
|
||||
const element = document.getElementById(heading.id);
|
||||
if (element) {
|
||||
element.scrollIntoView({ behavior: "smooth" });
|
||||
window.history.replaceState(null, "", `#${heading.id}`);
|
||||
onActivate(heading.id);
|
||||
}
|
||||
},
|
||||
[heading.id, onActivate],
|
||||
);
|
||||
|
||||
return (
|
||||
<a
|
||||
href={`#${heading.id}`}
|
||||
onClick={handleClick}
|
||||
className={clsx(
|
||||
"block py-1 transition-colors border-l-2 -ml-px",
|
||||
heading.depth === 2 && "pl-3",
|
||||
heading.depth === 3 && "pl-6",
|
||||
heading.depth >= 4 && "pl-9",
|
||||
active
|
||||
? "text-foreground border-primary"
|
||||
: "text-muted-foreground hover:text-foreground border-transparent",
|
||||
)}
|
||||
>
|
||||
{heading.text}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
|
||||
export function DocsOutline({ headings }: DocsOutlineProps) {
|
||||
const [activeId, setActiveId] = useState<string | null>(() =>
|
||||
headings.length > 0 ? headings[0].id : null,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setActiveId(headings.length > 0 ? headings[0].id : null);
|
||||
}, [headings]);
|
||||
|
||||
useEffect(() => {
|
||||
if (headings.length === 0) return;
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const visible = entries
|
||||
.filter((entry) => entry.isIntersecting)
|
||||
.map((entry) => entry.target.id);
|
||||
if (visible.length > 0) {
|
||||
setActiveId(visible[0]);
|
||||
}
|
||||
},
|
||||
{
|
||||
rootMargin: "-10% 0px -70% 0px",
|
||||
threshold: 0,
|
||||
},
|
||||
);
|
||||
|
||||
for (const heading of headings) {
|
||||
const element = document.getElementById(heading.id);
|
||||
if (element) observer.observe(element);
|
||||
}
|
||||
|
||||
return () => observer.disconnect();
|
||||
}, [headings]);
|
||||
|
||||
if (headings.length === 0) return null;
|
||||
|
||||
return (
|
||||
<nav
|
||||
aria-label="On this page"
|
||||
className="sticky top-8 max-h-[calc(100vh-4rem)] overflow-y-auto py-8 pr-4"
|
||||
>
|
||||
<div className="text-xs font-medium text-muted-foreground mb-3 px-3">On this page</div>
|
||||
<ul className="space-y-1 border-l border-border">
|
||||
{headings.map((heading) => (
|
||||
<li key={heading.id} className="text-sm">
|
||||
<OutlineLink
|
||||
heading={heading}
|
||||
active={activeId === heading.id}
|
||||
onActivate={setActiveId}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getAlternativePages } from "~/data/alternative-pages";
|
||||
import { appStoreUrl, playStoreUrl, webAppUrl } from "~/downloads";
|
||||
|
||||
interface SiteFooterProps {
|
||||
@@ -7,9 +8,10 @@ interface SiteFooterProps {
|
||||
export function SiteFooter({ width = "default" }: SiteFooterProps) {
|
||||
const widthClasses =
|
||||
width === "prose" ? "max-w-prose p-6 md:p-12 md:pt-0" : "max-w-5xl p-6 md:p-20 md:pt-0";
|
||||
const alternatives = getAlternativePages();
|
||||
return (
|
||||
<footer className={`${widthClasses} mx-auto`}>
|
||||
<div className="border-t border-white/10 pt-8 pb-4 grid grid-cols-2 sm:grid-cols-4 gap-8 text-sm">
|
||||
<div className="border-t border-white/10 pt-8 pb-4 grid grid-cols-2 sm:grid-cols-5 gap-8 text-sm">
|
||||
<div className="space-y-3">
|
||||
<p className="text-white/60 font-medium">Product</p>
|
||||
<div className="space-y-2">
|
||||
@@ -80,6 +82,20 @@ export function SiteFooter({ width = "default" }: SiteFooterProps) {
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<p className="text-white/60 font-medium">Alternatives</p>
|
||||
<div className="space-y-2">
|
||||
{alternatives.map((page) => (
|
||||
<a
|
||||
key={page.slug}
|
||||
href={page.href}
|
||||
className="block text-muted-foreground hover:text-foreground transition-colors"
|
||||
>
|
||||
{page.name}
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<p className="text-white/60 font-medium">Community</p>
|
||||
<div className="space-y-2">
|
||||
|
||||
@@ -4,10 +4,10 @@ import { SiteHeader } from "~/components/site-header";
|
||||
|
||||
interface SiteShellProps {
|
||||
children: ReactNode;
|
||||
width?: "default" | "prose";
|
||||
width: "default" | "prose";
|
||||
}
|
||||
|
||||
export function SiteShell({ children, width = "default" }: SiteShellProps) {
|
||||
export function SiteShell({ children, width }: SiteShellProps) {
|
||||
const mainClasses =
|
||||
width === "prose" ? "max-w-prose p-6 md:p-12 mx-auto" : "max-w-5xl p-6 md:p-20 mx-auto";
|
||||
return (
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
title: Open Source Claude Desktop Alternative With Linux, Mobile, and Multi-Provider Support
|
||||
description: Paseo is an open source Claude Desktop alternative for developers who want Linux, self-hosting, native mobile apps, and Claude Code alongside Codex, OpenCode, Copilot, and more.
|
||||
nav: Claude Desktop
|
||||
order: 106
|
||||
order: 55
|
||||
---
|
||||
|
||||
# Paseo vs Claude Desktop
|
||||
@@ -115,4 +115,4 @@ Claude supports voice in Claude's own mobile and app surfaces. Claude Code itsel
|
||||
| MCP server for orchestration | Yes | MCP support inside Claude Code |
|
||||
| Self-hosted daemon | Yes | No |
|
||||
|
||||
See also: [Paseo vs Codex App](/docs/alternatives/codex-app), [Paseo vs OpenCode Desktop](/docs/alternatives/opencode-desktop), [Paseo vs Conductor](/docs/alternatives/conductor).
|
||||
See also: [Paseo vs Codex App](/alternatives/codex-app), [Paseo vs OpenCode Desktop](/alternatives/opencode-desktop), [Paseo vs Conductor](/alternatives/conductor).
|
||||
@@ -2,7 +2,7 @@
|
||||
title: Open Source Codex App Alternative With Linux, Mobile, and Multi-Provider Support
|
||||
description: Paseo is an open source alternative to Codex App for developers who want Linux, native mobile apps, a self-hosted daemon, and Codex alongside Claude Code, OpenCode, Copilot, and more.
|
||||
nav: Codex App
|
||||
order: 105
|
||||
order: 54
|
||||
---
|
||||
|
||||
# Paseo vs Codex App
|
||||
@@ -107,4 +107,4 @@ Paseo supports dictation and realtime voice mode. Speech-to-text and text-to-spe
|
||||
| Voice | Dictation and realtime voice | Dictation |
|
||||
| Self-hosted daemon | Yes | No |
|
||||
|
||||
See also: [Paseo vs Claude Desktop](/docs/alternatives/claude-desktop), [Paseo vs OpenCode Desktop](/docs/alternatives/opencode-desktop), [Supported providers](/docs/supported-providers).
|
||||
See also: [Paseo vs Claude Desktop](/alternatives/claude-desktop), [Paseo vs OpenCode Desktop](/alternatives/opencode-desktop), [Supported providers](/docs/supported-providers).
|
||||
@@ -2,7 +2,7 @@
|
||||
title: Open Source Conductor Alternative With Linux, Windows, and Mobile
|
||||
description: Paseo is open source, runs on macOS, Linux, and Windows, ships native iOS and Android apps, and supports 30+ agents through the in-app catalog plus any ACP or CLI agent. Conductor is macOS only and Claude Code or Codex only.
|
||||
nav: Conductor
|
||||
order: 100
|
||||
order: 50
|
||||
---
|
||||
|
||||
# Paseo vs Conductor
|
||||
@@ -94,4 +94,4 @@ Paseo's speech-to-text and text-to-speech run locally on your device. Nothing le
|
||||
| Local voice (on-device) | Yes | — |
|
||||
| Self-hosted daemon | Yes | — |
|
||||
|
||||
See also: [Paseo vs Superset](/docs/alternatives/superset), [Paseo vs OpenChamber](/docs/alternatives/openchamber), [Paseo vs Happy Coder](/docs/alternatives/happy-coder).
|
||||
See also: [Paseo vs Superset](/alternatives/superset), [Paseo vs OpenChamber](/alternatives/openchamber), [Paseo vs Happy Coder](/alternatives/happy-coder).
|
||||
@@ -2,7 +2,7 @@
|
||||
title: Happy Coder Alternative With a Desktop App and Git Worktrees
|
||||
description: Paseo ships a native desktop app, runs agents in isolated git worktrees, and supports 30+ agents. Happy Coder is mobile and web only, wraps the agent CLI, and supports Claude Code and Codex.
|
||||
nav: Happy Coder
|
||||
order: 104
|
||||
order: 53
|
||||
---
|
||||
|
||||
# Paseo vs Happy Coder
|
||||
@@ -96,4 +96,4 @@ Paseo's speech-to-text and text-to-speech run locally on your device. Nothing le
|
||||
| CLI | Run, `--host`, ls, send, schedule, loop | Launch wrapped session |
|
||||
| Local voice (on-device) | Yes | — |
|
||||
|
||||
See also: [Paseo vs Conductor](/docs/alternatives/conductor), [Paseo vs Superset](/docs/alternatives/superset), [Paseo vs OpenChamber](/docs/alternatives/openchamber).
|
||||
See also: [Paseo vs Conductor](/alternatives/conductor), [Paseo vs Superset](/alternatives/superset), [Paseo vs OpenChamber](/alternatives/openchamber).
|
||||
@@ -2,7 +2,7 @@
|
||||
title: OpenChamber Alternative With Linux, Windows, and Mobile
|
||||
description: Paseo ships native iOS and Android apps, runs on macOS, Linux, and Windows, and supports 30+ agents. OpenChamber is macOS only with a PWA and is built around OpenCode.
|
||||
nav: OpenChamber
|
||||
order: 103
|
||||
order: 52
|
||||
---
|
||||
|
||||
# Paseo vs OpenChamber
|
||||
@@ -90,4 +90,4 @@ Paseo's speech-to-text and text-to-speech run locally on your device. OpenChambe
|
||||
| Local voice (on-device) | Yes | — |
|
||||
| Self-hosted daemon | Yes | — |
|
||||
|
||||
See also: [Paseo vs Conductor](/docs/alternatives/conductor), [Paseo vs Superset](/docs/alternatives/superset), [Paseo vs Happy Coder](/docs/alternatives/happy-coder).
|
||||
See also: [Paseo vs Conductor](/alternatives/conductor), [Paseo vs Superset](/alternatives/superset), [Paseo vs Happy Coder](/alternatives/happy-coder).
|
||||
@@ -2,7 +2,7 @@
|
||||
title: OpenCode Desktop Alternative With Native Mobile and Multi-Provider Orchestration
|
||||
description: Paseo is an OpenCode Desktop alternative for developers who want native mobile apps, a self-hosted daemon, and OpenCode alongside Claude Code, Codex, Copilot, and more.
|
||||
nav: OpenCode Desktop
|
||||
order: 107
|
||||
order: 56
|
||||
---
|
||||
|
||||
# Paseo vs OpenCode Desktop
|
||||
@@ -113,4 +113,4 @@ Paseo supports dictation and realtime voice mode. Speech-to-text and text-to-spe
|
||||
| Local voice | Yes | No |
|
||||
| Self-hosted daemon | Yes | OpenCode server / local runtime |
|
||||
|
||||
See also: [Paseo vs Codex App](/docs/alternatives/codex-app), [Paseo vs Claude Desktop](/docs/alternatives/claude-desktop), [Paseo vs OpenChamber](/docs/alternatives/openchamber).
|
||||
See also: [Paseo vs Codex App](/alternatives/codex-app), [Paseo vs Claude Desktop](/alternatives/claude-desktop), [Paseo vs OpenChamber](/alternatives/openchamber).
|
||||
@@ -2,7 +2,7 @@
|
||||
title: Superset Alternative With Linux, Windows, and Mobile
|
||||
description: Paseo is open source under an OSI license, has no login wall, ships native mobile, and runs on macOS, Linux, and Windows. Superset is source-available, macOS only, and gates the desktop app on a Superset login.
|
||||
nav: Superset
|
||||
order: 101
|
||||
order: 51
|
||||
---
|
||||
|
||||
# Paseo vs Superset
|
||||
@@ -113,4 +113,4 @@ Superset is free for one seat with local workspaces only. Team features and sync
|
||||
| Local voice (on-device) | Yes | — |
|
||||
| Self-hosted daemon | Yes | — |
|
||||
|
||||
See also: [Paseo vs Conductor](/docs/alternatives/conductor), [Paseo vs OpenChamber](/docs/alternatives/openchamber), [Paseo vs Happy Coder](/docs/alternatives/happy-coder).
|
||||
See also: [Paseo vs Conductor](/alternatives/conductor), [Paseo vs OpenChamber](/alternatives/openchamber), [Paseo vs Happy Coder](/alternatives/happy-coder).
|
||||
71
packages/website/src/data/alternative-pages.ts
Normal file
71
packages/website/src/data/alternative-pages.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
export interface AlternativePage {
|
||||
slug: string;
|
||||
name: string;
|
||||
href: string;
|
||||
title: string;
|
||||
description: string;
|
||||
content: string;
|
||||
order: number;
|
||||
}
|
||||
|
||||
function parseFrontmatter(raw: string): { data: Record<string, string>; content: string } {
|
||||
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
||||
if (!match) return { data: {}, content: raw };
|
||||
|
||||
const data: Record<string, string> = {};
|
||||
for (const line of match[1].split("\n")) {
|
||||
const colonIdx = line.indexOf(":");
|
||||
if (colonIdx === -1) continue;
|
||||
const key = line.slice(0, colonIdx).trim();
|
||||
const value = line
|
||||
.slice(colonIdx + 1)
|
||||
.trim()
|
||||
.replace(/^["']|["']$/g, "");
|
||||
data[key] = value;
|
||||
}
|
||||
|
||||
return { data, content: match[2] };
|
||||
}
|
||||
|
||||
const alternativeModules = import.meta.glob("../content/alternatives/*.md", {
|
||||
eager: true,
|
||||
query: "?raw",
|
||||
import: "default",
|
||||
}) as Record<string, string>;
|
||||
|
||||
function loadAlternativePages(): AlternativePage[] {
|
||||
const pages: AlternativePage[] = [];
|
||||
|
||||
for (const [path, raw] of Object.entries(alternativeModules)) {
|
||||
const fileName = path.split("/").pop() ?? "";
|
||||
const slug = fileName.replace(/\.md$/, "");
|
||||
const { data, content } = parseFrontmatter(raw);
|
||||
const order = Number.parseInt(data.order ?? "999", 10);
|
||||
|
||||
pages.push({
|
||||
slug,
|
||||
name: data.nav ?? data.title ?? slug,
|
||||
href: `/alternatives/${slug}`,
|
||||
title: data.title ?? slug,
|
||||
description: data.description ?? "",
|
||||
content,
|
||||
order: Number.isFinite(order) ? order : 999,
|
||||
});
|
||||
}
|
||||
|
||||
pages.sort((a, b) => a.order - b.order);
|
||||
return pages;
|
||||
}
|
||||
|
||||
let cached: AlternativePage[] | undefined;
|
||||
|
||||
export function getAlternativePages(): AlternativePage[] {
|
||||
if (!cached) cached = loadAlternativePages();
|
||||
return cached;
|
||||
}
|
||||
|
||||
export function getAlternativePage(slug: string): AlternativePage | undefined {
|
||||
return getAlternativePages().find((p) => p.slug === slug);
|
||||
}
|
||||
|
||||
export const ALTERNATIVE_PAGE_SLUGS: readonly string[] = getAlternativePages().map((p) => p.slug);
|
||||
@@ -1,8 +1,17 @@
|
||||
import Slugger from "github-slugger";
|
||||
|
||||
interface DocFrontmatter {
|
||||
title: string;
|
||||
description: string;
|
||||
nav: string;
|
||||
order: number;
|
||||
category?: string;
|
||||
}
|
||||
|
||||
export interface DocHeading {
|
||||
depth: number;
|
||||
text: string;
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface Doc {
|
||||
@@ -11,8 +20,31 @@ export interface Doc {
|
||||
sourcePath: string;
|
||||
frontmatter: DocFrontmatter;
|
||||
content: string;
|
||||
headings: DocHeading[];
|
||||
}
|
||||
|
||||
export type DocsNavNode =
|
||||
| {
|
||||
type: "category";
|
||||
label: string;
|
||||
children: DocsNavNode[];
|
||||
order: number;
|
||||
}
|
||||
| {
|
||||
type: "group";
|
||||
segment: string;
|
||||
label: string;
|
||||
children: DocsNavNode[];
|
||||
order: number;
|
||||
}
|
||||
| {
|
||||
type: "page";
|
||||
segment: string;
|
||||
label: string;
|
||||
href: string;
|
||||
order: number;
|
||||
};
|
||||
|
||||
function parseFrontmatter(raw: string): { data: Record<string, string>; content: string } {
|
||||
const match = raw.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
||||
if (!match) return { data: {}, content: raw };
|
||||
@@ -32,6 +64,40 @@ function parseFrontmatter(raw: string): { data: Record<string, string>; content:
|
||||
return { data, content: match[2] };
|
||||
}
|
||||
|
||||
function stripInlineMarkdown(text: string): string {
|
||||
return text
|
||||
.replace(/!?\[([^\]]*)\]\([^)]*\)/g, "$1")
|
||||
.replace(/!?\[([^\]]*)\]\[[^\]]*\]/g, "$1")
|
||||
.replace(/`([^`]+)`/g, "$1")
|
||||
.replace(/(\*\*|__)(.*?)\1/g, "$2")
|
||||
.replace(/(\*|_)(.*?)\1/g, "$2")
|
||||
.replace(/<[^>]+>/g, "")
|
||||
.trim();
|
||||
}
|
||||
|
||||
function parseHeadings(content: string): DocHeading[] {
|
||||
const slugger = new Slugger();
|
||||
const headings: DocHeading[] = [];
|
||||
const regex = /^(#{1,6})\s+(.+)$/gm;
|
||||
let match: RegExpExecArray | null;
|
||||
|
||||
while ((match = regex.exec(content)) !== null) {
|
||||
const depth = match[1].length;
|
||||
if (depth < 2 || depth > 4) continue;
|
||||
|
||||
const text = stripInlineMarkdown(match[2].trim());
|
||||
if (!text) continue;
|
||||
|
||||
headings.push({
|
||||
depth,
|
||||
text,
|
||||
id: slugger.slug(text),
|
||||
});
|
||||
}
|
||||
|
||||
return headings;
|
||||
}
|
||||
|
||||
const docModules = import.meta.glob("../../../public-docs/**/*.md", {
|
||||
eager: true,
|
||||
query: "?raw",
|
||||
@@ -41,7 +107,8 @@ const docModules = import.meta.glob("../../../public-docs/**/*.md", {
|
||||
function pathToSlug(path: string): string {
|
||||
const after = path.split("/public-docs/")[1] ?? path;
|
||||
const noExt = after.replace(/\.md$/, "");
|
||||
return noExt === "index" ? "" : noExt;
|
||||
if (noExt === "index") return "";
|
||||
return noExt.replace(/\/index$/, "");
|
||||
}
|
||||
|
||||
function pathToSourcePath(path: string): string {
|
||||
@@ -66,8 +133,10 @@ function loadDocs(): Doc[] {
|
||||
description: data.description ?? "",
|
||||
nav: data.nav ?? data.title ?? slug,
|
||||
order: Number.isFinite(order) ? order : 999,
|
||||
category: data.category,
|
||||
},
|
||||
content,
|
||||
headings: parseHeadings(content),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -85,3 +154,129 @@ export function getDocs(): Doc[] {
|
||||
export function getDoc(slug: string): Doc | undefined {
|
||||
return getDocs().find((d) => d.slug === slug);
|
||||
}
|
||||
|
||||
function formatLabel(segment: string): string {
|
||||
return segment.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
||||
}
|
||||
|
||||
function insertDocByPath(nodes: DocsNavNode[], doc: Doc): void {
|
||||
const relative = doc.sourcePath.replace(/^public-docs\//, "");
|
||||
const segments = relative.replace(/\.md$/, "").split("/");
|
||||
const fileName = segments.pop() ?? "";
|
||||
const directories = segments;
|
||||
|
||||
let current = nodes;
|
||||
for (const segment of directories) {
|
||||
let group = current.find(
|
||||
(node): node is Extract<DocsNavNode, { type: "group" }> =>
|
||||
node.type === "group" && node.segment === segment,
|
||||
);
|
||||
|
||||
if (!group) {
|
||||
group = {
|
||||
type: "group",
|
||||
segment,
|
||||
label: formatLabel(segment),
|
||||
children: [],
|
||||
order: doc.frontmatter.order,
|
||||
};
|
||||
current.push(group);
|
||||
}
|
||||
|
||||
current = group.children;
|
||||
}
|
||||
|
||||
const pageSegment = fileName === "index" ? (directories.at(-1) ?? "") : fileName;
|
||||
current.push({
|
||||
type: "page",
|
||||
segment: pageSegment,
|
||||
label: doc.frontmatter.nav,
|
||||
href: doc.href,
|
||||
order: doc.frontmatter.order,
|
||||
});
|
||||
}
|
||||
|
||||
function nodeOrder(node: DocsNavNode): number {
|
||||
if (node.type === "page") return node.order;
|
||||
if (node.children.length === 0) return Infinity;
|
||||
let min = Infinity;
|
||||
for (const child of node.children) {
|
||||
const childOrder = nodeOrder(child);
|
||||
if (childOrder < min) min = childOrder;
|
||||
}
|
||||
return min;
|
||||
}
|
||||
|
||||
function sortNodes(nodes: DocsNavNode[]): void {
|
||||
nodes.sort((a, b) => nodeOrder(a) - nodeOrder(b));
|
||||
for (const node of nodes) {
|
||||
if (node.type !== "page") {
|
||||
sortNodes(node.children);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function buildDocsNavTree(docs: Doc[]): DocsNavNode[] {
|
||||
const byCategory = new Map<string, Doc[]>();
|
||||
const uncategorized: Doc[] = [];
|
||||
|
||||
for (const doc of docs) {
|
||||
const category = doc.frontmatter.category?.trim();
|
||||
if (category) {
|
||||
const list = byCategory.get(category) ?? [];
|
||||
list.push(doc);
|
||||
byCategory.set(category, list);
|
||||
} else {
|
||||
uncategorized.push(doc);
|
||||
}
|
||||
}
|
||||
|
||||
const root: DocsNavNode[] = [];
|
||||
|
||||
for (const doc of uncategorized) {
|
||||
insertDocByPath(root, doc);
|
||||
}
|
||||
|
||||
for (const [label, docsInCategory] of byCategory) {
|
||||
const children: DocsNavNode[] = [];
|
||||
for (const doc of docsInCategory) {
|
||||
insertDocByPath(children, doc);
|
||||
}
|
||||
root.push({
|
||||
type: "category",
|
||||
label,
|
||||
children,
|
||||
order: nodeOrder({ type: "category", label, children, order: Infinity }),
|
||||
});
|
||||
}
|
||||
|
||||
sortNodes(root);
|
||||
return root;
|
||||
}
|
||||
|
||||
function findNodePath(
|
||||
nodes: DocsNavNode[],
|
||||
href: string,
|
||||
path: DocsNavNode[] = [],
|
||||
): DocsNavNode[] | null {
|
||||
for (const node of nodes) {
|
||||
if (node.type === "page" && node.href === href) {
|
||||
return [...path, node];
|
||||
}
|
||||
if (node.type !== "page") {
|
||||
const found = findNodePath(node.children, href, [...path, node]);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getDocBreadcrumbGroups(doc: Doc, tree: DocsNavNode[]): { label: string }[] {
|
||||
const path = findNodePath(tree, doc.href);
|
||||
if (!path) return [];
|
||||
|
||||
return path
|
||||
.slice(0, -1)
|
||||
.filter((node): node is Extract<DocsNavNode, { type: "group" }> => node.type === "group")
|
||||
.map((node) => ({ label: node.label }));
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { getAlternativePages } from "~/data/alternative-pages";
|
||||
import { AGENT_PAGES } from "~/data/agent-pages";
|
||||
import { type Doc, getDocs } from "~/docs";
|
||||
|
||||
@@ -27,17 +28,19 @@ function agentLine(agent: (typeof AGENT_PAGES)[number]): string {
|
||||
return `- [${agent.name}](${SITE_URL}/${agent.slug}): ${agent.subtitle}`;
|
||||
}
|
||||
|
||||
function alternativeLine(page: ReturnType<typeof getAlternativePages>[number]): string {
|
||||
const description = page.description.trim();
|
||||
const suffix = description ? `: ${description}` : "";
|
||||
return `- [${page.title}](${SITE_URL}${page.href})${suffix}`;
|
||||
}
|
||||
|
||||
function topLevelDocs(): Doc[] {
|
||||
return getDocs().filter((d) => !d.slug.includes("/"));
|
||||
}
|
||||
|
||||
function alternativeDocs(): Doc[] {
|
||||
return getDocs().filter((d) => d.slug.startsWith("alternatives/"));
|
||||
}
|
||||
|
||||
export function buildLlmsTxt(): string {
|
||||
const docs = topLevelDocs().map(docLine).join("\n");
|
||||
const alternatives = alternativeDocs().map(docLine).join("\n");
|
||||
const alternatives = getAlternativePages().map(alternativeLine).join("\n");
|
||||
const agents = AGENT_PAGES.map(agentLine).join("\n");
|
||||
|
||||
return `${PRODUCT_PREAMBLE}
|
||||
|
||||
@@ -61,6 +61,13 @@ import { Route as DocsIndexRouteImport } from "./routes/docs/index";
|
||||
import { Route as BlogIndexRouteImport } from "./routes/blog/index";
|
||||
import { Route as DocsSplatRouteImport } from "./routes/docs/$";
|
||||
import { Route as BlogSplatRouteImport } from "./routes/blog/$";
|
||||
import { Route as AlternativesSupersetRouteImport } from "./routes/alternatives/superset";
|
||||
import { Route as AlternativesOpencodeDesktopRouteImport } from "./routes/alternatives/opencode-desktop";
|
||||
import { Route as AlternativesOpenchamberRouteImport } from "./routes/alternatives/openchamber";
|
||||
import { Route as AlternativesHappyCoderRouteImport } from "./routes/alternatives/happy-coder";
|
||||
import { Route as AlternativesConductorRouteImport } from "./routes/alternatives/conductor";
|
||||
import { Route as AlternativesCodexAppRouteImport } from "./routes/alternatives/codex-app";
|
||||
import { Route as AlternativesClaudeDesktopRouteImport } from "./routes/alternatives/claude-desktop";
|
||||
|
||||
const VtcodeRoute = VtcodeRouteImport.update({
|
||||
id: "/vtcode",
|
||||
@@ -322,6 +329,43 @@ const BlogSplatRoute = BlogSplatRouteImport.update({
|
||||
path: "/$",
|
||||
getParentRoute: () => BlogRoute,
|
||||
} as any);
|
||||
const AlternativesSupersetRoute = AlternativesSupersetRouteImport.update({
|
||||
id: "/alternatives/superset",
|
||||
path: "/alternatives/superset",
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any);
|
||||
const AlternativesOpencodeDesktopRoute =
|
||||
AlternativesOpencodeDesktopRouteImport.update({
|
||||
id: "/alternatives/opencode-desktop",
|
||||
path: "/alternatives/opencode-desktop",
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any);
|
||||
const AlternativesOpenchamberRoute = AlternativesOpenchamberRouteImport.update({
|
||||
id: "/alternatives/openchamber",
|
||||
path: "/alternatives/openchamber",
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any);
|
||||
const AlternativesHappyCoderRoute = AlternativesHappyCoderRouteImport.update({
|
||||
id: "/alternatives/happy-coder",
|
||||
path: "/alternatives/happy-coder",
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any);
|
||||
const AlternativesConductorRoute = AlternativesConductorRouteImport.update({
|
||||
id: "/alternatives/conductor",
|
||||
path: "/alternatives/conductor",
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any);
|
||||
const AlternativesCodexAppRoute = AlternativesCodexAppRouteImport.update({
|
||||
id: "/alternatives/codex-app",
|
||||
path: "/alternatives/codex-app",
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any);
|
||||
const AlternativesClaudeDesktopRoute =
|
||||
AlternativesClaudeDesktopRouteImport.update({
|
||||
id: "/alternatives/claude-desktop",
|
||||
path: "/alternatives/claude-desktop",
|
||||
getParentRoute: () => rootRouteImport,
|
||||
} as any);
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
"/": typeof IndexRoute;
|
||||
@@ -372,6 +416,13 @@ export interface FileRoutesByFullPath {
|
||||
"/sponsor": typeof SponsorRoute;
|
||||
"/stakpak": typeof StakpakRoute;
|
||||
"/vtcode": typeof VtcodeRoute;
|
||||
"/alternatives/claude-desktop": typeof AlternativesClaudeDesktopRoute;
|
||||
"/alternatives/codex-app": typeof AlternativesCodexAppRoute;
|
||||
"/alternatives/conductor": typeof AlternativesConductorRoute;
|
||||
"/alternatives/happy-coder": typeof AlternativesHappyCoderRoute;
|
||||
"/alternatives/openchamber": typeof AlternativesOpenchamberRoute;
|
||||
"/alternatives/opencode-desktop": typeof AlternativesOpencodeDesktopRoute;
|
||||
"/alternatives/superset": typeof AlternativesSupersetRoute;
|
||||
"/blog/$": typeof BlogSplatRoute;
|
||||
"/docs/$": typeof DocsSplatRoute;
|
||||
"/blog/": typeof BlogIndexRoute;
|
||||
@@ -424,6 +475,13 @@ export interface FileRoutesByTo {
|
||||
"/sponsor": typeof SponsorRoute;
|
||||
"/stakpak": typeof StakpakRoute;
|
||||
"/vtcode": typeof VtcodeRoute;
|
||||
"/alternatives/claude-desktop": typeof AlternativesClaudeDesktopRoute;
|
||||
"/alternatives/codex-app": typeof AlternativesCodexAppRoute;
|
||||
"/alternatives/conductor": typeof AlternativesConductorRoute;
|
||||
"/alternatives/happy-coder": typeof AlternativesHappyCoderRoute;
|
||||
"/alternatives/openchamber": typeof AlternativesOpenchamberRoute;
|
||||
"/alternatives/opencode-desktop": typeof AlternativesOpencodeDesktopRoute;
|
||||
"/alternatives/superset": typeof AlternativesSupersetRoute;
|
||||
"/blog/$": typeof BlogSplatRoute;
|
||||
"/docs/$": typeof DocsSplatRoute;
|
||||
"/blog": typeof BlogIndexRoute;
|
||||
@@ -479,6 +537,13 @@ export interface FileRoutesById {
|
||||
"/sponsor": typeof SponsorRoute;
|
||||
"/stakpak": typeof StakpakRoute;
|
||||
"/vtcode": typeof VtcodeRoute;
|
||||
"/alternatives/claude-desktop": typeof AlternativesClaudeDesktopRoute;
|
||||
"/alternatives/codex-app": typeof AlternativesCodexAppRoute;
|
||||
"/alternatives/conductor": typeof AlternativesConductorRoute;
|
||||
"/alternatives/happy-coder": typeof AlternativesHappyCoderRoute;
|
||||
"/alternatives/openchamber": typeof AlternativesOpenchamberRoute;
|
||||
"/alternatives/opencode-desktop": typeof AlternativesOpencodeDesktopRoute;
|
||||
"/alternatives/superset": typeof AlternativesSupersetRoute;
|
||||
"/blog/$": typeof BlogSplatRoute;
|
||||
"/docs/$": typeof DocsSplatRoute;
|
||||
"/blog/": typeof BlogIndexRoute;
|
||||
@@ -535,6 +600,13 @@ export interface FileRouteTypes {
|
||||
| "/sponsor"
|
||||
| "/stakpak"
|
||||
| "/vtcode"
|
||||
| "/alternatives/claude-desktop"
|
||||
| "/alternatives/codex-app"
|
||||
| "/alternatives/conductor"
|
||||
| "/alternatives/happy-coder"
|
||||
| "/alternatives/openchamber"
|
||||
| "/alternatives/opencode-desktop"
|
||||
| "/alternatives/superset"
|
||||
| "/blog/$"
|
||||
| "/docs/$"
|
||||
| "/blog/"
|
||||
@@ -587,6 +659,13 @@ export interface FileRouteTypes {
|
||||
| "/sponsor"
|
||||
| "/stakpak"
|
||||
| "/vtcode"
|
||||
| "/alternatives/claude-desktop"
|
||||
| "/alternatives/codex-app"
|
||||
| "/alternatives/conductor"
|
||||
| "/alternatives/happy-coder"
|
||||
| "/alternatives/openchamber"
|
||||
| "/alternatives/opencode-desktop"
|
||||
| "/alternatives/superset"
|
||||
| "/blog/$"
|
||||
| "/docs/$"
|
||||
| "/blog"
|
||||
@@ -641,6 +720,13 @@ export interface FileRouteTypes {
|
||||
| "/sponsor"
|
||||
| "/stakpak"
|
||||
| "/vtcode"
|
||||
| "/alternatives/claude-desktop"
|
||||
| "/alternatives/codex-app"
|
||||
| "/alternatives/conductor"
|
||||
| "/alternatives/happy-coder"
|
||||
| "/alternatives/openchamber"
|
||||
| "/alternatives/opencode-desktop"
|
||||
| "/alternatives/superset"
|
||||
| "/blog/$"
|
||||
| "/docs/$"
|
||||
| "/blog/"
|
||||
@@ -696,6 +782,13 @@ export interface RootRouteChildren {
|
||||
SponsorRoute: typeof SponsorRoute;
|
||||
StakpakRoute: typeof StakpakRoute;
|
||||
VtcodeRoute: typeof VtcodeRoute;
|
||||
AlternativesClaudeDesktopRoute: typeof AlternativesClaudeDesktopRoute;
|
||||
AlternativesCodexAppRoute: typeof AlternativesCodexAppRoute;
|
||||
AlternativesConductorRoute: typeof AlternativesConductorRoute;
|
||||
AlternativesHappyCoderRoute: typeof AlternativesHappyCoderRoute;
|
||||
AlternativesOpenchamberRoute: typeof AlternativesOpenchamberRoute;
|
||||
AlternativesOpencodeDesktopRoute: typeof AlternativesOpencodeDesktopRoute;
|
||||
AlternativesSupersetRoute: typeof AlternativesSupersetRoute;
|
||||
}
|
||||
|
||||
declare module "@tanstack/react-router" {
|
||||
@@ -1064,6 +1157,55 @@ declare module "@tanstack/react-router" {
|
||||
preLoaderRoute: typeof BlogSplatRouteImport;
|
||||
parentRoute: typeof BlogRoute;
|
||||
};
|
||||
"/alternatives/superset": {
|
||||
id: "/alternatives/superset";
|
||||
path: "/alternatives/superset";
|
||||
fullPath: "/alternatives/superset";
|
||||
preLoaderRoute: typeof AlternativesSupersetRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/alternatives/opencode-desktop": {
|
||||
id: "/alternatives/opencode-desktop";
|
||||
path: "/alternatives/opencode-desktop";
|
||||
fullPath: "/alternatives/opencode-desktop";
|
||||
preLoaderRoute: typeof AlternativesOpencodeDesktopRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/alternatives/openchamber": {
|
||||
id: "/alternatives/openchamber";
|
||||
path: "/alternatives/openchamber";
|
||||
fullPath: "/alternatives/openchamber";
|
||||
preLoaderRoute: typeof AlternativesOpenchamberRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/alternatives/happy-coder": {
|
||||
id: "/alternatives/happy-coder";
|
||||
path: "/alternatives/happy-coder";
|
||||
fullPath: "/alternatives/happy-coder";
|
||||
preLoaderRoute: typeof AlternativesHappyCoderRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/alternatives/conductor": {
|
||||
id: "/alternatives/conductor";
|
||||
path: "/alternatives/conductor";
|
||||
fullPath: "/alternatives/conductor";
|
||||
preLoaderRoute: typeof AlternativesConductorRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/alternatives/codex-app": {
|
||||
id: "/alternatives/codex-app";
|
||||
path: "/alternatives/codex-app";
|
||||
fullPath: "/alternatives/codex-app";
|
||||
preLoaderRoute: typeof AlternativesCodexAppRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
"/alternatives/claude-desktop": {
|
||||
id: "/alternatives/claude-desktop";
|
||||
path: "/alternatives/claude-desktop";
|
||||
fullPath: "/alternatives/claude-desktop";
|
||||
preLoaderRoute: typeof AlternativesClaudeDesktopRouteImport;
|
||||
parentRoute: typeof rootRouteImport;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1140,6 +1282,13 @@ const rootRouteChildren: RootRouteChildren = {
|
||||
SponsorRoute: SponsorRoute,
|
||||
StakpakRoute: StakpakRoute,
|
||||
VtcodeRoute: VtcodeRoute,
|
||||
AlternativesClaudeDesktopRoute: AlternativesClaudeDesktopRoute,
|
||||
AlternativesCodexAppRoute: AlternativesCodexAppRoute,
|
||||
AlternativesConductorRoute: AlternativesConductorRoute,
|
||||
AlternativesHappyCoderRoute: AlternativesHappyCoderRoute,
|
||||
AlternativesOpenchamberRoute: AlternativesOpenchamberRoute,
|
||||
AlternativesOpencodeDesktopRoute: AlternativesOpencodeDesktopRoute,
|
||||
AlternativesSupersetRoute: AlternativesSupersetRoute,
|
||||
};
|
||||
export const routeTree = rootRouteImport
|
||||
._addFileChildren(rootRouteChildren)
|
||||
|
||||
@@ -0,0 +1,6 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { alternativeRouteOptions } from "~/components/alternative-page";
|
||||
|
||||
export const Route = createFileRoute("/alternatives/claude-desktop")(
|
||||
alternativeRouteOptions("claude-desktop"),
|
||||
);
|
||||
6
packages/website/src/routes/alternatives/codex-app.tsx
Normal file
6
packages/website/src/routes/alternatives/codex-app.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { alternativeRouteOptions } from "~/components/alternative-page";
|
||||
|
||||
export const Route = createFileRoute("/alternatives/codex-app")(
|
||||
alternativeRouteOptions("codex-app"),
|
||||
);
|
||||
6
packages/website/src/routes/alternatives/conductor.tsx
Normal file
6
packages/website/src/routes/alternatives/conductor.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { alternativeRouteOptions } from "~/components/alternative-page";
|
||||
|
||||
export const Route = createFileRoute("/alternatives/conductor")(
|
||||
alternativeRouteOptions("conductor"),
|
||||
);
|
||||
6
packages/website/src/routes/alternatives/happy-coder.tsx
Normal file
6
packages/website/src/routes/alternatives/happy-coder.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { alternativeRouteOptions } from "~/components/alternative-page";
|
||||
|
||||
export const Route = createFileRoute("/alternatives/happy-coder")(
|
||||
alternativeRouteOptions("happy-coder"),
|
||||
);
|
||||
6
packages/website/src/routes/alternatives/openchamber.tsx
Normal file
6
packages/website/src/routes/alternatives/openchamber.tsx
Normal file
@@ -0,0 +1,6 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { alternativeRouteOptions } from "~/components/alternative-page";
|
||||
|
||||
export const Route = createFileRoute("/alternatives/openchamber")(
|
||||
alternativeRouteOptions("openchamber"),
|
||||
);
|
||||
@@ -0,0 +1,6 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { alternativeRouteOptions } from "~/components/alternative-page";
|
||||
|
||||
export const Route = createFileRoute("/alternatives/opencode-desktop")(
|
||||
alternativeRouteOptions("opencode-desktop"),
|
||||
);
|
||||
4
packages/website/src/routes/alternatives/superset.tsx
Normal file
4
packages/website/src/routes/alternatives/superset.tsx
Normal file
@@ -0,0 +1,4 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { alternativeRouteOptions } from "~/components/alternative-page";
|
||||
|
||||
export const Route = createFileRoute("/alternatives/superset")(alternativeRouteOptions("superset"));
|
||||
@@ -7,7 +7,7 @@ export const Route = createFileRoute("/blog")({
|
||||
|
||||
function BlogLayout() {
|
||||
return (
|
||||
<SiteShell>
|
||||
<SiteShell width="default">
|
||||
<Outlet />
|
||||
</SiteShell>
|
||||
);
|
||||
|
||||
@@ -16,7 +16,7 @@ export const Route = createFileRoute("/changelog")({
|
||||
|
||||
function Changelog() {
|
||||
return (
|
||||
<SiteShell>
|
||||
<SiteShell width="default">
|
||||
<article className="changelog-markdown rounded-xl border border-border bg-card/40 p-6 md:p-8">
|
||||
<ReactMarkdown>{changelogMarkdown}</ReactMarkdown>
|
||||
</article>
|
||||
|
||||
@@ -22,7 +22,7 @@ type Status = "idle" | "submitting" | "success" | "error";
|
||||
|
||||
function Cloud() {
|
||||
return (
|
||||
<SiteShell>
|
||||
<SiteShell width="default">
|
||||
<h1 className="text-3xl font-medium mb-3">Paseo Cloud</h1>
|
||||
<p className="text-white/70 leading-relaxed mb-10">
|
||||
For using Paseo across machines, with a team, or inside a company. Looking for design
|
||||
|
||||
@@ -1,55 +1,23 @@
|
||||
import { createFileRoute, Link, Outlet } from "@tanstack/react-router";
|
||||
import { createFileRoute, Link, Outlet, useLocation } from "@tanstack/react-router";
|
||||
import { Menu, X } from "lucide-react";
|
||||
import { useCallback, useState } from "react";
|
||||
import { type Doc, getDocs } from "~/docs";
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { DocsBreadcrumbs } from "~/components/docs-breadcrumbs";
|
||||
import { DocsNav } from "~/components/docs-nav";
|
||||
import { DocsOutline } from "~/components/docs-outline";
|
||||
import { buildDocsNavTree, getDoc, getDocs } from "~/docs";
|
||||
import "~/styles.css";
|
||||
|
||||
export const Route = createFileRoute("/docs")({
|
||||
component: DocsLayout,
|
||||
});
|
||||
|
||||
const ACTIVE_OPTIONS_EXACT = { exact: true };
|
||||
const MOBILE_ACTIVE_PROPS = { className: "text-foreground" };
|
||||
const DESKTOP_ACTIVE_PROPS = { className: "bg-muted text-foreground" };
|
||||
|
||||
interface NavItem {
|
||||
name: string;
|
||||
href: string;
|
||||
}
|
||||
|
||||
interface NavGroup {
|
||||
name: string | null;
|
||||
items: NavItem[];
|
||||
}
|
||||
|
||||
const GROUP_LABELS: Record<string, string> = {
|
||||
alternatives: "Alternatives",
|
||||
};
|
||||
|
||||
function groupKey(doc: Doc): string | null {
|
||||
const idx = doc.slug.indexOf("/");
|
||||
return idx === -1 ? null : doc.slug.slice(0, idx);
|
||||
}
|
||||
|
||||
function buildNavigation(): NavGroup[] {
|
||||
const groups = new Map<string | null, NavItem[]>();
|
||||
for (const doc of getDocs()) {
|
||||
const key = groupKey(doc);
|
||||
const items = groups.get(key) ?? [];
|
||||
items.push({ name: doc.frontmatter.nav, href: doc.href });
|
||||
groups.set(key, items);
|
||||
}
|
||||
const ordered: NavGroup[] = [];
|
||||
if (groups.has(null)) ordered.push({ name: null, items: groups.get(null)! });
|
||||
for (const [key, items] of groups) {
|
||||
if (key === null) continue;
|
||||
ordered.push({ name: GROUP_LABELS[key] ?? key, items });
|
||||
}
|
||||
return ordered;
|
||||
}
|
||||
|
||||
function DocsLayout() {
|
||||
const groups = buildNavigation();
|
||||
const location = useLocation();
|
||||
const tree = useMemo(() => buildDocsNavTree(getDocs()), []);
|
||||
|
||||
const slug = location.pathname === "/docs" ? "" : location.pathname.slice("/docs/".length);
|
||||
const doc = getDoc(slug);
|
||||
|
||||
const [mobileNavOpen, setMobileNavOpen] = useState(false);
|
||||
const toggleMobileNav = useCallback(() => setMobileNavOpen((v) => !v), []);
|
||||
const closeMobileNav = useCallback(() => setMobileNavOpen(false), []);
|
||||
@@ -57,7 +25,7 @@ function DocsLayout() {
|
||||
return (
|
||||
<div className="min-h-screen bg-background">
|
||||
{/* Mobile header */}
|
||||
<header className="md:hidden sticky top-0 z-50 bg-background border-b border-border">
|
||||
<header className="lg:hidden sticky top-0 z-50 bg-background border-b border-border">
|
||||
<div className="flex items-center justify-between p-4">
|
||||
<Link to="/" className="flex items-center gap-3">
|
||||
<img src="/logo.svg" alt="Paseo" className="w-6 h-6" />
|
||||
@@ -74,61 +42,34 @@ function DocsLayout() {
|
||||
</button>
|
||||
</div>
|
||||
{mobileNavOpen && (
|
||||
<nav className="border-t border-border px-4 py-4 space-y-4 max-h-[calc(100dvh-4rem)] overflow-y-auto">
|
||||
{groups.map((group) => (
|
||||
<div key={group.name ?? "root"} className="space-y-1">
|
||||
{group.name && (
|
||||
<div className="text-sm font-medium text-foreground mb-2">{group.name}</div>
|
||||
)}
|
||||
{group.items.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
to={item.href}
|
||||
activeOptions={ACTIVE_OPTIONS_EXACT}
|
||||
onClick={closeMobileNav}
|
||||
className="block py-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
|
||||
activeProps={MOBILE_ACTIVE_PROPS}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
<nav className="border-t border-border px-4 py-4 max-h-[calc(100dvh-4rem)] overflow-y-auto">
|
||||
<DocsNav nodes={tree} mobile onNavigate={closeMobileNav} />
|
||||
</nav>
|
||||
)}
|
||||
</header>
|
||||
|
||||
<div className="flex">
|
||||
<div className="max-w-[90rem] mx-auto flex items-start">
|
||||
{/* Desktop sidebar */}
|
||||
<aside className="hidden md:flex md:flex-col w-56 shrink-0 border-r border-border p-6 sticky top-0 h-screen">
|
||||
<Link to="/" className="flex items-center gap-3 mb-8 shrink-0">
|
||||
<aside className="hidden lg:block sticky top-0 h-screen w-60 shrink-0 border-r border-border p-6 overflow-y-auto">
|
||||
<Link to="/" className="flex items-center gap-3 mb-8">
|
||||
<img src="/logo.svg" alt="Paseo" className="w-6 h-6" />
|
||||
<span className="text-lg font-medium">Paseo</span>
|
||||
</Link>
|
||||
<nav className="flex-1 min-h-0 overflow-y-auto -ml-3 -mr-3 pr-3 space-y-4">
|
||||
{groups.map((group) => (
|
||||
<div key={group.name ?? "root"} className="space-y-1">
|
||||
{group.name && (
|
||||
<div className="px-3 py-2 text-sm font-medium text-foreground">{group.name}</div>
|
||||
)}
|
||||
{group.items.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
to={item.href}
|
||||
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={DESKTOP_ACTIVE_PROPS}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</nav>
|
||||
<DocsNav nodes={tree} />
|
||||
</aside>
|
||||
<main className="flex-1 min-w-0 p-6 md:p-12 max-w-3xl docs-prose">
|
||||
<Outlet />
|
||||
|
||||
{/* Main content */}
|
||||
<main className="flex-1 min-w-0 px-6 md:px-12 py-8 md:py-12">
|
||||
<div className="max-w-prose mx-auto">
|
||||
{doc && <DocsBreadcrumbs doc={doc} tree={tree} />}
|
||||
<Outlet />
|
||||
</div>
|
||||
</main>
|
||||
|
||||
{/* Right outline */}
|
||||
<aside className="hidden xl:block sticky top-0 h-screen w-60 shrink-0 px-2 overflow-y-auto">
|
||||
{doc && <DocsOutline headings={doc.headings} />}
|
||||
</aside>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -33,7 +33,7 @@ function Download() {
|
||||
const urls = downloadUrls(release);
|
||||
|
||||
return (
|
||||
<SiteShell>
|
||||
<SiteShell width="default">
|
||||
<h1 className="text-3xl md:text-4xl font-semibold tracking-tight mb-2">Download</h1>
|
||||
<p className="text-muted-foreground mb-10">v{version}</p>
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ export const Route = createFileRoute("/privacy")({
|
||||
|
||||
function Privacy() {
|
||||
return (
|
||||
<SiteShell>
|
||||
<SiteShell width="default">
|
||||
<h1 className="text-3xl font-medium mb-8">Privacy Policy</h1>
|
||||
|
||||
<div className="space-y-6 text-white/70 leading-relaxed">
|
||||
|
||||
@@ -14,7 +14,7 @@ export const Route = createFileRoute("/sponsor")({
|
||||
|
||||
function Sponsor() {
|
||||
return (
|
||||
<SiteShell>
|
||||
<SiteShell width="default">
|
||||
<h1 className="text-3xl font-medium tracking-tight mb-8">Sponsor</h1>
|
||||
|
||||
{/* Founder note */}
|
||||
|
||||
@@ -33,6 +33,12 @@ export default {
|
||||
return Response.redirect(url.toString(), 301);
|
||||
}
|
||||
|
||||
const altRedirectMatch = url.pathname.match(/^\/docs\/alternatives\/(.+?)\/?$/);
|
||||
if (altRedirectMatch) {
|
||||
url.pathname = `/alternatives/${altRedirectMatch[1]}`;
|
||||
return Response.redirect(url.toString(), 301);
|
||||
}
|
||||
|
||||
if (url.pathname === "/llms.txt") {
|
||||
return markdownResponse(buildLlmsTxt());
|
||||
}
|
||||
|
||||
@@ -22,8 +22,11 @@ function discoverDocsRoutes(): string[] {
|
||||
continue;
|
||||
}
|
||||
if (!entry.name.endsWith(".md")) continue;
|
||||
const rel = path.relative(docsDir, full).replace(/\.md$/, "");
|
||||
if (rel === "index") continue;
|
||||
const rel = path
|
||||
.relative(docsDir, full)
|
||||
.replace(/\.md$/, "")
|
||||
.replace(/\/index$/, "");
|
||||
if (rel === "index" || rel === "") continue;
|
||||
routes.add(`/docs/${rel.split(path.sep).join("/")}`);
|
||||
}
|
||||
};
|
||||
@@ -54,6 +57,17 @@ function discoverAgentRoutes(): string[] {
|
||||
.map((slug) => `/${slug}`);
|
||||
}
|
||||
|
||||
function discoverAlternativeRoutes(): string[] {
|
||||
const alternativesDir = path.join(__dirname, "src/routes/alternatives");
|
||||
if (!fs.existsSync(alternativesDir)) return [];
|
||||
const slugs = fs
|
||||
.readdirSync(alternativesDir, { withFileTypes: true })
|
||||
.filter((entry) => entry.isFile() && entry.name.endsWith(".tsx"))
|
||||
.map((entry) => entry.name.replace(/\.tsx$/, ""))
|
||||
.sort();
|
||||
return ["/alternatives", ...slugs.map((slug) => `/alternatives/${slug}`)];
|
||||
}
|
||||
|
||||
function discoverBlogRoutes(): string[] {
|
||||
const postsDir = path.join(__dirname, "posts");
|
||||
if (!fs.existsSync(postsDir)) return ["/blog"];
|
||||
@@ -73,6 +87,7 @@ const sitemapPages = [
|
||||
"/download",
|
||||
"/privacy",
|
||||
...discoverAgentRoutes(),
|
||||
...discoverAlternativeRoutes(),
|
||||
...discoverDocsRoutes(),
|
||||
...discoverBlogRoutes(),
|
||||
].map((routePath) => ({
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
---
|
||||
title: Best practices
|
||||
description: Tips for getting the most out of Paseo and mobile-first agent workflows.
|
||||
nav: Best practices
|
||||
order: 17
|
||||
---
|
||||
|
||||
# Best practices
|
||||
|
||||
What I've learned from using Paseo daily. Not rules, just patterns that have worked for me.
|
||||
|
||||
## Agents replace typing, not thinking
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## Verification loops
|
||||
|
||||
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.
|
||||
|
||||
## Invest in tooling
|
||||
|
||||
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.
|
||||
|
||||
Code is cheap with coding agents. I would have never written that CLI before because it felt like wasted effort. Now I bootstrap tooling first. It pays off exponentially.
|
||||
|
||||
## Agents are cheap
|
||||
|
||||
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.
|
||||
|
||||
## Use voice extensively
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## Understand the type of work
|
||||
|
||||
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.
|
||||
|
||||
## Iterate and refactor often
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
## Use agents to check agents
|
||||
|
||||
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.
|
||||
|
||||
## Learn your agents' quirks
|
||||
|
||||
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.
|
||||
|
||||
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.
|
||||
@@ -2,7 +2,8 @@
|
||||
title: Claude Code
|
||||
description: How Paseo runs Claude Code and how Anthropic's usage policy applies.
|
||||
nav: Claude Code
|
||||
order: 7
|
||||
order: 23
|
||||
category: Providers
|
||||
---
|
||||
|
||||
# Claude Code
|
||||
@@ -57,4 +58,4 @@ Paseo has first-class terminal support. You can run Claude Code in your terminal
|
||||
- [Anthropic: Use the Claude Agent SDK with your Claude plan](https://support.claude.com/en/articles/15036540-use-the-claude-agent-sdk-with-your-claude-plan)
|
||||
- [Custom providers](/docs/custom-providers), for custom binaries, third-party endpoints, or multiple Claude profiles.
|
||||
- [Supported providers](/docs/supported-providers), for other agents you can run alongside Claude Code.
|
||||
- [Paseo vs Claude Desktop](/docs/alternatives/claude-desktop), for a feature comparison.
|
||||
- [Paseo vs Claude Desktop](/alternatives/claude-desktop), for a feature comparison.
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
title: CLI
|
||||
description: "Paseo CLI reference: manage agents, daemons, permissions, and worktrees from your terminal."
|
||||
nav: CLI
|
||||
order: 8
|
||||
order: 3
|
||||
category: Getting started
|
||||
---
|
||||
|
||||
# CLI
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
title: Configuration
|
||||
description: Configure Paseo via config.json, environment variables, and CLI overrides.
|
||||
nav: Configuration
|
||||
order: 14
|
||||
order: 40
|
||||
category: Configuration
|
||||
---
|
||||
|
||||
# Configuration
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
title: Custom providers
|
||||
description: Configure custom providers, alternative endpoints, profiles, custom binaries, and ACP agents in ~/.paseo/config.json.
|
||||
nav: Custom providers
|
||||
order: 7
|
||||
order: 22
|
||||
category: Providers
|
||||
---
|
||||
|
||||
# Custom providers
|
||||
|
||||
@@ -3,6 +3,7 @@ title: Getting started
|
||||
description: Install Paseo and start running coding agents from anywhere.
|
||||
nav: Getting started
|
||||
order: 1
|
||||
category: Getting started
|
||||
---
|
||||
|
||||
# Getting started
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
title: Paseo MCP
|
||||
description: Paseo MCP tools injected into agents.
|
||||
nav: Paseo MCP
|
||||
order: 9
|
||||
order: 30
|
||||
category: Automation
|
||||
---
|
||||
|
||||
# Paseo MCP
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
title: Metadata generation
|
||||
description: How Paseo uses providers to generate agent titles, branch names, commit messages, and pull request text, and how to configure them.
|
||||
nav: Metadata generation
|
||||
order: 15
|
||||
order: 42
|
||||
category: Configuration
|
||||
---
|
||||
|
||||
# Metadata generation
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
title: Providers
|
||||
description: How Paseo thinks about coding agents, wrapping existing CLIs, native vs ACP support, and where to go next.
|
||||
nav: Providers
|
||||
order: 5
|
||||
order: 20
|
||||
category: Providers
|
||||
---
|
||||
|
||||
# Providers
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
title: Schedules
|
||||
description: Run Paseo agents on intervals or cron.
|
||||
nav: Schedules
|
||||
order: 11
|
||||
order: 31
|
||||
category: Automation
|
||||
---
|
||||
|
||||
# Schedules
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
title: Security
|
||||
description: "Security model for Paseo: architecture overview, connection methods, relay encryption, and best practices."
|
||||
nav: Security
|
||||
order: 16
|
||||
order: 4
|
||||
category: Getting started
|
||||
---
|
||||
|
||||
# Security
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
title: Orchestration skills
|
||||
description: "Paseo orchestration skills: teach coding agents to spawn, coordinate, and manage other agents using slash commands."
|
||||
nav: Skills
|
||||
order: 12
|
||||
order: 32
|
||||
category: Automation
|
||||
---
|
||||
|
||||
# Orchestration skills
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
title: Supported providers
|
||||
description: Every coding agent Paseo can launch, natively supported providers and the ACP catalog.
|
||||
nav: Supported providers
|
||||
order: 6
|
||||
order: 21
|
||||
category: Providers
|
||||
---
|
||||
|
||||
# Supported providers
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
title: Updates
|
||||
description: How Paseo releases work, the difference between stable and beta channels, and how to opt in to earlier updates.
|
||||
nav: Updates
|
||||
order: 18
|
||||
order: 5
|
||||
category: Getting started
|
||||
---
|
||||
|
||||
# Updates
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
title: Voice
|
||||
description: Paseo voice architecture, local-first model execution, and provider configuration.
|
||||
nav: Voice
|
||||
order: 13
|
||||
order: 41
|
||||
category: Configuration
|
||||
---
|
||||
|
||||
# Voice
|
||||
|
||||
@@ -3,6 +3,7 @@ title: Why Paseo?
|
||||
description: What Paseo is, what it isn't, and how it fits into your workflow.
|
||||
nav: Why Paseo?
|
||||
order: 2
|
||||
category: Getting started
|
||||
---
|
||||
|
||||
# Why Paseo?
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
title: Workspaces
|
||||
description: Understand Paseo's project, workspace, and session model before setting up agents or git worktrees.
|
||||
nav: Workspaces
|
||||
order: 3
|
||||
order: 10
|
||||
category: Workspaces
|
||||
---
|
||||
|
||||
# Workspaces
|
||||
|
||||
@@ -2,7 +2,8 @@
|
||||
title: Git worktrees
|
||||
description: Run agents in isolated git worktrees with setup hooks, scripts, and long-running services.
|
||||
nav: Git worktrees
|
||||
order: 4
|
||||
order: 11
|
||||
category: Workspaces
|
||||
---
|
||||
|
||||
# Git worktrees
|
||||
|
||||
Reference in New Issue
Block a user