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:
@@ -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",
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 (
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
---
|
||||
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: 55
|
||||
---
|
||||
|
||||
# Paseo vs Claude Desktop
|
||||
|
||||
Claude Desktop is Anthropic's desktop app for Claude. It includes Chat, Cowork, and Claude Code in one app. Claude Code runs in the desktop app on macOS and Windows.
|
||||
|
||||
Paseo is an app for orchestrating coding agents, with native clients on desktop, mobile, web, and the CLI. Open source (AGPL-3.0).
|
||||
|
||||

|
||||
|
||||
## When to pick what
|
||||
|
||||
Pick Claude Desktop if you want Anthropic's first-party app for Claude, Claude Cowork, and Claude Code, with Anthropic-managed cloud sessions and the tightest Claude account integration.
|
||||
|
||||
Pick Paseo if you want:
|
||||
|
||||
- Linux alongside macOS and Windows
|
||||
- A native iOS and Android app for the same agent workflow
|
||||
- Claude Code, Codex, OpenCode, Copilot, Pi, and 30+ more agents in one interface
|
||||
- A self-hosted daemon you can run on a laptop, VM, or dev server
|
||||
- A CLI and MCP server for scripting and multi-agent workflows
|
||||
- Open source you can audit and fork
|
||||
|
||||
## Architecture
|
||||
|
||||
Paseo runs a daemon on your machine. Desktop, web, mobile, and CLI clients connect to it over a websocket. The daemon launches Claude Code and other providers as local processes, using your installed CLIs, credentials, MCP servers, skills, and project config.
|
||||
|
||||
Claude Desktop is the host app. The Code tab can run Claude Code locally, connect over SSH, or run remote sessions on Anthropic infrastructure.
|
||||
|
||||
## Providers
|
||||
|
||||
Claude Desktop runs Claude Code.
|
||||
|
||||
Paseo runs Claude Code too, plus Codex, OpenCode, and Pi natively, plus 30+ more agents through the in-app catalog including GitHub Copilot, Cursor, Gemini CLI, and Amp. Paseo speaks the [Agent Client Protocol](https://agentclientprotocol.com), so any ACP agent works. Custom providers run any CLI agent. See [Supported providers](/docs/supported-providers).
|
||||
|
||||
## Desktop platforms
|
||||
|
||||
Claude Desktop is available on macOS and Windows. Anthropic lists Linux as not available.
|
||||
|
||||
Paseo ships on macOS, Linux, and Windows.
|
||||
|
||||
## Mobile
|
||||
|
||||
Paseo ships native iOS and Android apps with the same agent workflow as the desktop app.
|
||||
|
||||
Claude has iOS and Android apps. Claude Code can be controlled from mobile through Remote Control, and Claude Desktop can pair with mobile for some workflows.
|
||||
|
||||
## Panes
|
||||
|
||||
Both tools support visual coding workflows around Claude Code.
|
||||
|
||||
Paseo's app has split panes and tabs (⌘D for vertical, ⌘⇧D for horizontal). Panes include agents, terminals, a diff viewer, and a browser for testing running services.
|
||||
|
||||
Claude Desktop has a graphical Code tab with sessions, integrated terminal, file editor, visual diff review, live app preview, PR monitoring, and scheduled tasks.
|
||||
|
||||
## GitHub
|
||||
|
||||
Paseo's app handles commit, push, opening PRs, watching checks and reviews, and merging.
|
||||
|
||||
Claude Desktop can monitor pull request status and can fix failures or merge when checks pass, depending on the workflow and permissions.
|
||||
|
||||
## CLI and automation
|
||||
|
||||
Claude Code has its own CLI, IDE integrations, web surface, scheduled tasks, and cloud sessions.
|
||||
|
||||
Paseo's CLI controls the same daemon as the app:
|
||||
|
||||
```bash
|
||||
paseo run --provider claude "implement OAuth"
|
||||
paseo run --provider codex --worktree refactor-auth "refactor auth"
|
||||
paseo run --host devbox:6767 "run the test suite"
|
||||
paseo ls
|
||||
paseo send <agent-id> "add tests"
|
||||
paseo schedule create --cron "0 9 * * 1" "audit the codebase"
|
||||
```
|
||||
|
||||
`paseo run --host` connects to a remote daemon. `paseo schedule` runs an agent on a cron. `paseo loop` retries an agent until a verification command passes. The MCP server lets other agents create worktrees, launch agents, open terminals, and send prompts.
|
||||
|
||||
## Worktrees and services
|
||||
|
||||
Both tools support parallel coding sessions, including Git worktrees.
|
||||
|
||||
Paseo also gives each worktree its own dev server URL. Two agents running their dev servers at the same time get `web.fix-auth.my-app.localhost` and `web.add-search.my-app.localhost` instead of port collisions.
|
||||
|
||||
## Voice
|
||||
|
||||
Paseo supports dictation and realtime voice mode. Speech-to-text and text-to-speech can run locally on your device.
|
||||
|
||||
Claude supports voice in Claude's own mobile and app surfaces. Claude Code itself is available in Claude Desktop, terminal, IDE, web, and mobile Remote Control workflows.
|
||||
|
||||
## Comparison
|
||||
|
||||
| | Paseo | Claude Desktop |
|
||||
| ---------------------------- | --------------------------------------------------------------- | --------------------------------- |
|
||||
| License | Open source (AGPL-3.0) | Not published as open source |
|
||||
| Desktop platforms | macOS, Linux, Windows | macOS, Windows |
|
||||
| Native mobile | iOS, Android | iOS, Android Claude apps |
|
||||
| Coding agents | Claude Code, Codex, OpenCode, Pi + 30+ via ACP catalog + custom | Claude Code |
|
||||
| General chat | No | Claude Chat |
|
||||
| Cloud agent | Cloud waitlist | Claude Cowork and remote sessions |
|
||||
| Local execution | Yes | Yes |
|
||||
| SSH remote execution | Via daemon on the remote host | Yes |
|
||||
| Git worktrees | Yes | Yes |
|
||||
| Per-worktree dev server URLs | Yes | No |
|
||||
| Split panes and tabs | Yes | Yes |
|
||||
| In-app terminal | Yes | Yes |
|
||||
| In-app browser / preview | Yes | Yes |
|
||||
| GitHub workflow in app | Commit, push, PR, checks, reviews, merge | PR monitoring and merge workflows |
|
||||
| CLI | Run, `--host`, ls, send, schedule, loop | Claude Code CLI |
|
||||
| MCP server for orchestration | Yes | MCP support inside Claude Code |
|
||||
| Self-hosted daemon | Yes | No |
|
||||
|
||||
See also: [Paseo vs Codex App](/alternatives/codex-app), [Paseo vs OpenCode Desktop](/alternatives/opencode-desktop), [Paseo vs Conductor](/alternatives/conductor).
|
||||
@@ -0,0 +1,110 @@
|
||||
---
|
||||
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: 54
|
||||
---
|
||||
|
||||
# Paseo vs Codex App
|
||||
|
||||
Codex App is OpenAI's desktop app for working with Codex threads in parallel. It runs on macOS and Windows, with local, worktree, and cloud modes.
|
||||
|
||||
Paseo is an app for orchestrating coding agents, with native clients on desktop, mobile, web, and the CLI. Open source (AGPL-3.0).
|
||||
|
||||

|
||||
|
||||
## When to pick what
|
||||
|
||||
Pick Codex App if you want OpenAI's first-party app for Codex, with Codex-specific features like cloud threads, appshots, image generation, and computer use on macOS.
|
||||
|
||||
Pick Paseo if you want:
|
||||
|
||||
- Linux alongside macOS and Windows
|
||||
- A native iOS and Android app
|
||||
- Codex, Claude Code, OpenCode, Copilot, Pi, and 30+ more agents in one interface
|
||||
- A self-hosted daemon you can run on a laptop, VM, or dev server
|
||||
- A CLI and MCP server for scripting and multi-agent workflows
|
||||
- Open source you can audit and fork
|
||||
|
||||
## Architecture
|
||||
|
||||
Paseo runs a daemon on your machine. Desktop, web, mobile, and CLI clients connect to it over a websocket. The daemon launches Codex and other providers as local processes, using your installed CLIs and credentials.
|
||||
|
||||
Codex App is a desktop app for Codex. It can run local and worktree threads on your computer, and cloud threads on OpenAI-managed infrastructure.
|
||||
|
||||
## Providers
|
||||
|
||||
Codex App runs Codex.
|
||||
|
||||
Paseo runs Codex too, plus Claude Code, OpenCode, and Pi natively, plus 30+ more agents through the in-app catalog including GitHub Copilot, Cursor, Gemini CLI, and Amp. Paseo speaks the [Agent Client Protocol](https://agentclientprotocol.com), so any ACP agent works. Custom providers run any CLI agent. See [Supported providers](/docs/supported-providers).
|
||||
|
||||
## Desktop platforms
|
||||
|
||||
Codex App is available on macOS and Windows. OpenAI lists Linux as not available yet.
|
||||
|
||||
Paseo ships on macOS, Linux, and Windows.
|
||||
|
||||
## Mobile
|
||||
|
||||
Paseo ships native iOS and Android apps with the same agent workflow as the desktop app.
|
||||
|
||||
Codex can be controlled remotely through OpenAI's mobile surfaces, including ChatGPT mobile remote connections. Codex App itself is a desktop app.
|
||||
|
||||
## Worktrees and local setup
|
||||
|
||||
Both tools support Git worktrees for parallel work.
|
||||
|
||||
Codex App creates Codex-managed worktrees under `$CODEX_HOME/worktrees` and supports local environment setup scripts and project actions through `.codex` configuration.
|
||||
|
||||
Paseo creates worktrees under `$PASEO_HOME/worktrees`, runs setup and teardown hooks from `paseo.json`, and gives each worktree its own dev server URLs like `web.fix-auth.my-app.localhost` so parallel services don't fight for ports.
|
||||
|
||||
## GitHub and review
|
||||
|
||||
Both tools support reviewing diffs, committing, pushing, and opening pull requests from the app.
|
||||
|
||||
Paseo also surfaces PR checks and reviews in the app, and exposes the same workflow through the CLI and MCP server.
|
||||
|
||||
## CLI and automation
|
||||
|
||||
Codex has its own CLI, IDE extension, web app, automations, and SDK.
|
||||
|
||||
Paseo's CLI controls the same daemon as the app:
|
||||
|
||||
```bash
|
||||
paseo run --provider codex "implement OAuth"
|
||||
paseo run --provider claude --worktree refactor-auth "refactor auth"
|
||||
paseo run --host devbox:6767 "run the test suite"
|
||||
paseo ls
|
||||
paseo send <agent-id> "add tests"
|
||||
paseo schedule create --cron "0 9 * * 1" "audit the codebase"
|
||||
```
|
||||
|
||||
`paseo run --host` connects to a remote daemon. `paseo schedule` runs an agent on a cron. `paseo loop` retries an agent until a verification command passes. The MCP server lets other agents create worktrees, launch agents, open terminals, and send prompts.
|
||||
|
||||
## Voice
|
||||
|
||||
Codex App supports voice dictation.
|
||||
|
||||
Paseo supports dictation and realtime voice mode. Speech-to-text and text-to-speech can run locally on your device.
|
||||
|
||||
## Comparison
|
||||
|
||||
| | Paseo | Codex App |
|
||||
| ---------------------------- | --------------------------------------------------------------- | ---------------------------- |
|
||||
| License | Open source (AGPL-3.0) | Not published as open source |
|
||||
| Desktop platforms | macOS, Linux, Windows | macOS, Windows |
|
||||
| Native mobile | iOS, Android | No |
|
||||
| Providers | Codex, Claude Code, OpenCode, Pi + 30+ via ACP catalog + custom | Codex |
|
||||
| Local execution | Yes | Yes |
|
||||
| Cloud execution | Cloud waitlist | Yes |
|
||||
| Git worktrees | Yes | Yes |
|
||||
| Per-worktree dev server URLs | Yes | No |
|
||||
| In-app terminal | Yes | Yes |
|
||||
| In-app browser | Yes | Yes |
|
||||
| GitHub workflow in app | Commit, push, PR, checks, reviews, merge | Commit, push, PR |
|
||||
| CLI | Run, `--host`, ls, send, schedule, loop | Codex CLI |
|
||||
| MCP server for orchestration | Yes | MCP support inside Codex |
|
||||
| Voice | Dictation and realtime voice | Dictation |
|
||||
| Self-hosted daemon | Yes | No |
|
||||
|
||||
See also: [Paseo vs Claude Desktop](/alternatives/claude-desktop), [Paseo vs OpenCode Desktop](/alternatives/opencode-desktop), [Supported providers](/docs/supported-providers).
|
||||
@@ -0,0 +1,97 @@
|
||||
---
|
||||
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: 50
|
||||
---
|
||||
|
||||
# Paseo vs Conductor
|
||||
|
||||
Conductor is a macOS app for running Claude Code and Codex in parallel git worktrees. Closed source.
|
||||
|
||||
Paseo is an app for orchestrating coding agents, with native clients on desktop, mobile, web, and the CLI. Open source (AGPL-3.0).
|
||||
|
||||

|
||||
|
||||
## Why pick Paseo
|
||||
|
||||
Conductor runs on macOS, with Claude Code and Codex, in parallel git worktrees. Paseo does all of that. Pick Paseo if you want:
|
||||
|
||||
- Linux or Windows alongside macOS
|
||||
- A native iOS and Android app
|
||||
- Many more agents than Claude Code and Codex
|
||||
- A CLI to script agent work and drive remote daemons
|
||||
- A self-hosted daemon you can run on a server, VM, or homelab
|
||||
- Open source you can audit and fork
|
||||
|
||||
## Architecture
|
||||
|
||||
The Paseo daemon runs as its own process. Desktop, web, mobile, and CLI all connect to it over a websocket. Run the daemon on your laptop, on a VM, in Docker, or across a fleet, and connect to any of them from any client.
|
||||
|
||||
Conductor's desktop app is the host. Agents run inside it.
|
||||
|
||||
## Providers
|
||||
|
||||
Paseo runs Claude Code, Codex, OpenCode, and Pi natively, plus 30+ more agents through the in-app catalog including GitHub Copilot, Cursor, Gemini CLI, and Amp. Paseo speaks the [Agent Client Protocol](https://agentclientprotocol.com), so any ACP agent works. Custom providers run any CLI agent. See [Supported providers](/docs/supported-providers).
|
||||
|
||||
Conductor runs Claude Code and Codex.
|
||||
|
||||
Both tools launch the official CLIs as subprocesses with your own credentials. Neither extracts tokens or proxies model calls.
|
||||
|
||||
## Panes
|
||||
|
||||
Paseo's app has split panes and tabs (⌘D for vertical, ⌘⇧D for horizontal). Panes include a terminal alongside your agents, a diff viewer, and a browser for testing running services.
|
||||
|
||||
## GitHub
|
||||
|
||||
Paseo's app handles commit, push, opening PRs, watching checks and reviews, and merging.
|
||||
|
||||
## CLI
|
||||
|
||||
Paseo has a CLI that mirrors the app:
|
||||
|
||||
```bash
|
||||
paseo run --provider codex "implement OAuth"
|
||||
paseo run --host devbox:6767 "run the test suite"
|
||||
paseo ls
|
||||
paseo send <agent-id> "add tests"
|
||||
paseo schedule create --cron "0 9 * * 1" "audit the codebase"
|
||||
```
|
||||
|
||||
`paseo run --host` connects to a remote daemon. `paseo schedule` runs an agent on a cron. `paseo loop` retries an agent until a verification command passes.
|
||||
|
||||
Conductor does not have a CLI.
|
||||
|
||||
## Worktrees and services
|
||||
|
||||
Both tools isolate parallel agents in git worktrees.
|
||||
|
||||
Paseo also gives each worktree its own dev server URL. Two agents running their dev servers at the same time get `web.fix-auth.my-app.localhost` and `web.add-search.my-app.localhost` instead of port collisions.
|
||||
|
||||
## Mobile
|
||||
|
||||
Paseo ships native iOS and Android apps with the same feature set as the desktop app. Conductor has no mobile app.
|
||||
|
||||
## Voice
|
||||
|
||||
Paseo's speech-to-text and text-to-speech run locally on your device. Nothing leaves your network. Conductor does not have voice.
|
||||
|
||||
## Comparison
|
||||
|
||||
| | Paseo | Conductor |
|
||||
| ---------------------------- | --------------------------------------------------------------- | ------------------ |
|
||||
| License | Open source (AGPL-3.0) | Closed source |
|
||||
| Platforms | macOS, Linux, Windows | macOS only |
|
||||
| Native mobile | iOS, Android | — |
|
||||
| Providers | Claude Code, Codex, OpenCode, Pi + 30+ via ACP catalog + custom | Claude Code, Codex |
|
||||
| Git worktrees | Yes | Yes |
|
||||
| Per-worktree dev server URLs | Yes | — |
|
||||
| Split panes and tabs | Yes | — |
|
||||
| In-app terminal | Yes | Yes |
|
||||
| In-app browser | Yes | — |
|
||||
| GitHub workflow in app | Commit, push, PR, checks, reviews, merge | Yes |
|
||||
| CLI | Run, `--host`, ls, send, schedule, loop | — |
|
||||
| Local voice (on-device) | Yes | — |
|
||||
| Self-hosted daemon | Yes | — |
|
||||
|
||||
See also: [Paseo vs Superset](/alternatives/superset), [Paseo vs OpenChamber](/alternatives/openchamber), [Paseo vs Happy Coder](/alternatives/happy-coder).
|
||||
@@ -0,0 +1,99 @@
|
||||
---
|
||||
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: 53
|
||||
---
|
||||
|
||||
# Paseo vs Happy Coder
|
||||
|
||||
Happy Coder is a mobile and web client for Claude Code and Codex. It wraps the agent CLI on your laptop and syncs sessions to phone and browser over an end-to-end encrypted relay. Open source under MIT.
|
||||
|
||||
Paseo is an app for orchestrating coding agents, with native clients on desktop, mobile, web, and the CLI. Open source (AGPL-3.0).
|
||||
|
||||

|
||||
|
||||
## When to pick what
|
||||
|
||||
Pick Happy Coder if you want the most minimal setup. Wrap an existing Claude Code or Codex session on your laptop and check in on it from your phone.
|
||||
|
||||
Pick Paseo if you want:
|
||||
|
||||
- A native desktop app on macOS, Linux, and Windows
|
||||
- Git worktrees for parallel agents
|
||||
- Per-worktree dev server URLs
|
||||
- GitHub PRs, checks, reviews, and merges in the app
|
||||
- Many more agents than Claude Code and Codex
|
||||
- A CLI to script agent work and drive remote daemons
|
||||
|
||||
## Architecture
|
||||
|
||||
Paseo runs the agent inside its own daemon. The daemon owns the agent lifecycle, the worktree, and the dev servers. Clients connect over a websocket and drive the daemon.
|
||||
|
||||
Happy Coder runs the agent inside its existing CLI on your laptop and syncs the session to its mobile and web clients through an end-to-end encrypted relay.
|
||||
|
||||
## Panes
|
||||
|
||||
Paseo's app has split panes and tabs (⌘D for vertical, ⌘⇧D for horizontal). Panes include a terminal alongside your agents, a diff viewer, and a browser for testing running services.
|
||||
|
||||
Happy Coder does not have a desktop app.
|
||||
|
||||
## GitHub
|
||||
|
||||
Paseo's app handles commit, push, opening PRs, watching checks and reviews, and merging.
|
||||
|
||||
## Mobile
|
||||
|
||||
Both tools ship native iOS and Android apps.
|
||||
|
||||
## Providers
|
||||
|
||||
Paseo runs Claude Code, Codex, OpenCode, and Pi natively, plus 30+ more agents through the in-app catalog including GitHub Copilot, Cursor, Gemini CLI, and Amp. Paseo speaks the [Agent Client Protocol](https://agentclientprotocol.com), so any ACP agent works. Custom providers run any CLI agent. See [Supported providers](/docs/supported-providers).
|
||||
|
||||
Happy Coder runs Claude Code and Codex.
|
||||
|
||||
## Worktrees and services
|
||||
|
||||
Paseo runs each agent in its own git worktree. Each worktree gets its own dev server URL like `web.fix-auth.my-app.localhost`, so parallel agents don't fight for the same port.
|
||||
|
||||
Happy Coder runs the agent in the directory you launched the CLI from.
|
||||
|
||||
## CLI
|
||||
|
||||
Paseo has a CLI that mirrors the app:
|
||||
|
||||
```bash
|
||||
paseo run --provider codex "implement OAuth"
|
||||
paseo run --host devbox:6767 "run the test suite"
|
||||
paseo ls
|
||||
paseo send <agent-id> "add tests"
|
||||
paseo schedule create --cron "0 9 * * 1" "audit the codebase"
|
||||
```
|
||||
|
||||
`paseo run --host` connects to a remote daemon. `paseo schedule` runs an agent on a cron. `paseo loop` retries an agent until a verification command passes.
|
||||
|
||||
Happy Coder has a CLI to launch the wrapped session. It does not have schedules or loops.
|
||||
|
||||
## Voice
|
||||
|
||||
Paseo's speech-to-text and text-to-speech run locally on your device. Nothing leaves your network.
|
||||
|
||||
## Comparison
|
||||
|
||||
| | Paseo | Happy Coder |
|
||||
| ---------------------------- | --------------------------------------------------------------- | ---------------------- |
|
||||
| License | Open source (AGPL-3.0) | Open source (MIT) |
|
||||
| Desktop app | macOS, Linux, Windows | — |
|
||||
| Native mobile | iOS, Android | iOS, Android |
|
||||
| Architecture | Daemon owns agent lifecycle | Wraps the agent CLI |
|
||||
| Providers | Claude Code, Codex, OpenCode, Pi + 30+ via ACP catalog + custom | Claude Code, Codex |
|
||||
| Split panes and tabs | Yes | — |
|
||||
| In-app terminal | Yes | — |
|
||||
| In-app browser | Yes | — |
|
||||
| GitHub workflow in app | Commit, push, PR, checks, reviews, merge | — |
|
||||
| Git worktrees | Yes | — |
|
||||
| Per-worktree dev server URLs | Yes | — |
|
||||
| CLI | Run, `--host`, ls, send, schedule, loop | Launch wrapped session |
|
||||
| Local voice (on-device) | Yes | — |
|
||||
|
||||
See also: [Paseo vs Conductor](/alternatives/conductor), [Paseo vs Superset](/alternatives/superset), [Paseo vs OpenChamber](/alternatives/openchamber).
|
||||
@@ -0,0 +1,93 @@
|
||||
---
|
||||
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: 52
|
||||
---
|
||||
|
||||
# Paseo vs OpenChamber
|
||||
|
||||
OpenChamber is a macOS desktop app for OpenCode. Also available as a PWA. Open source under MIT.
|
||||
|
||||
Paseo is an app for orchestrating coding agents, with native clients on desktop, mobile, web, and the CLI. Open source (AGPL-3.0).
|
||||
|
||||

|
||||
|
||||
## Why pick Paseo
|
||||
|
||||
OpenChamber runs on macOS, around OpenCode, with a phone PWA. Paseo runs OpenCode too, on macOS, and adds:
|
||||
|
||||
- Linux and Windows desktop
|
||||
- A native iOS and Android app
|
||||
- Many more agents than OpenCode (Claude Code, Codex, Pi, plus 30+ more via the in-app ACP catalog)
|
||||
- A scriptable CLI to drive agents and connect to remote daemons
|
||||
|
||||
## Mobile
|
||||
|
||||
Paseo ships a native iOS and Android app with the same feature set as the desktop. Install from the App Store or Google Play.
|
||||
|
||||
OpenChamber does not have a native mobile app.
|
||||
|
||||
## Desktop
|
||||
|
||||
Paseo ships on macOS, Linux, and Windows.
|
||||
|
||||
OpenChamber ships on macOS.
|
||||
|
||||
## Providers
|
||||
|
||||
Paseo runs Claude Code, Codex, OpenCode, and Pi natively, plus 30+ more agents through the in-app catalog including GitHub Copilot, Cursor, Gemini CLI, and Amp. Paseo speaks the [Agent Client Protocol](https://agentclientprotocol.com), so any ACP agent works. Custom providers run any CLI agent. See [Supported providers](/docs/supported-providers).
|
||||
|
||||
OpenChamber is built around OpenCode.
|
||||
|
||||
## Panes
|
||||
|
||||
Paseo's app has split panes and tabs (⌘D for vertical, ⌘⇧D for horizontal). Panes include a terminal alongside your agents, a diff viewer, and a browser for testing running services.
|
||||
|
||||
## GitHub
|
||||
|
||||
Paseo's app handles commit, push, opening PRs, watching checks and reviews, and merging.
|
||||
|
||||
## CLI
|
||||
|
||||
Paseo has a CLI that mirrors the app:
|
||||
|
||||
```bash
|
||||
paseo run --provider codex "implement OAuth"
|
||||
paseo run --host devbox:6767 "run the test suite"
|
||||
paseo ls
|
||||
paseo send <agent-id> "add tests"
|
||||
paseo schedule create --cron "0 9 * * 1" "audit the codebase"
|
||||
```
|
||||
|
||||
`paseo run --host` connects to a remote daemon. `paseo schedule` runs an agent on a cron. `paseo loop` retries an agent until a verification command passes.
|
||||
|
||||
OpenChamber does not have a CLI.
|
||||
|
||||
## Worktrees and services
|
||||
|
||||
Paseo runs each agent in its own git worktree. Each worktree gets its own dev server URL like `web.fix-auth.my-app.localhost`, so parallel agents don't fight for ports.
|
||||
|
||||
## Voice
|
||||
|
||||
Paseo's speech-to-text and text-to-speech run locally on your device. OpenChamber does not have voice.
|
||||
|
||||
## Comparison
|
||||
|
||||
| | Paseo | OpenChamber |
|
||||
| ---------------------------- | --------------------------------------------------------------- | ----------------- |
|
||||
| License | Open source (AGPL-3.0) | Open source (MIT) |
|
||||
| Desktop platforms | macOS, Linux, Windows | macOS |
|
||||
| Mobile | Native iOS, Android | PWA |
|
||||
| Providers | Claude Code, Codex, OpenCode, Pi + 30+ via ACP catalog + custom | OpenCode |
|
||||
| Split panes and tabs | Yes | — |
|
||||
| In-app terminal | Yes | — |
|
||||
| In-app browser | Yes | — |
|
||||
| GitHub workflow in app | Commit, push, PR, checks, reviews, merge | Yes |
|
||||
| CLI | Run, `--host`, ls, send, schedule, loop | — |
|
||||
| Git worktrees | Yes | Yes |
|
||||
| Per-worktree dev server URLs | Yes | — |
|
||||
| Local voice (on-device) | Yes | — |
|
||||
| Self-hosted daemon | Yes | — |
|
||||
|
||||
See also: [Paseo vs Conductor](/alternatives/conductor), [Paseo vs Superset](/alternatives/superset), [Paseo vs Happy Coder](/alternatives/happy-coder).
|
||||
@@ -0,0 +1,116 @@
|
||||
---
|
||||
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: 56
|
||||
---
|
||||
|
||||
# Paseo vs OpenCode Desktop
|
||||
|
||||
OpenCode Desktop is the desktop app for OpenCode. It is available in beta for macOS, Windows, and Linux.
|
||||
|
||||
Paseo is an app for orchestrating coding agents, with native clients on desktop, mobile, web, and the CLI. Open source (AGPL-3.0).
|
||||
|
||||

|
||||
|
||||
## When to pick what
|
||||
|
||||
Pick OpenCode Desktop if you want the OpenCode app, the OpenCode terminal workflow, and OpenCode's multi-model provider system.
|
||||
|
||||
Pick Paseo if you want:
|
||||
|
||||
- A native iOS and Android app
|
||||
- OpenCode beside Claude Code, Codex, Copilot, Pi, and 30+ more agents
|
||||
- First-party harnesses for Claude Code and Codex instead of one agent runtime for everything
|
||||
- A self-hosted daemon you can run on a laptop, VM, or dev server
|
||||
- GitHub PRs, checks, reviews, and merges in the app
|
||||
- A CLI and MCP server for scripting and multi-agent workflows
|
||||
|
||||
## Architecture
|
||||
|
||||
Paseo runs a daemon on your machine. Desktop, web, mobile, and CLI clients connect to it over a websocket. The daemon launches OpenCode and other providers as local processes, using your installed CLIs and credentials.
|
||||
|
||||
OpenCode Desktop is the desktop app for OpenCode. OpenCode is available as a terminal interface, desktop app, IDE extension, web surface, and integrations.
|
||||
|
||||
## Providers
|
||||
|
||||
OpenCode is a multi-model coding agent. It can connect to many LLM providers through its own provider system, including OpenCode Zen, local models, and API providers.
|
||||
|
||||
Paseo is multi-provider at the agent harness layer. It runs OpenCode, Claude Code, Codex, and Pi natively, plus 30+ more agents through the in-app catalog including GitHub Copilot, Cursor, Gemini CLI, and Amp. Paseo speaks the [Agent Client Protocol](https://agentclientprotocol.com), so any ACP agent works. Custom providers run any CLI agent. See [Supported providers](/docs/supported-providers).
|
||||
|
||||
## Desktop platforms
|
||||
|
||||
Both tools ship on macOS, Linux, and Windows.
|
||||
|
||||
## Mobile
|
||||
|
||||
Paseo ships native iOS and Android apps with the same agent workflow as the desktop app.
|
||||
|
||||
OpenCode Desktop is a desktop app. OpenCode also has web and share-link workflows, but not a native mobile app.
|
||||
|
||||
## Panes
|
||||
|
||||
Paseo's app has split panes and tabs (⌘D for vertical, ⌘⇧D for horizontal). Panes include agents, terminals, a diff viewer, and a browser for testing running services.
|
||||
|
||||
OpenCode is available in terminal, IDE, and desktop surfaces. Its core workflow centers on OpenCode sessions.
|
||||
|
||||
## GitHub
|
||||
|
||||
Paseo's app handles commit, push, opening PRs, watching checks and reviews, and merging.
|
||||
|
||||
OpenCode has GitHub and GitLab integrations, and OpenCode sessions can make and review code changes through its agent workflow.
|
||||
|
||||
## CLI and automation
|
||||
|
||||
OpenCode has its own terminal interface, CLI, IDE extension, GitHub and GitLab integrations, and share links.
|
||||
|
||||
Paseo's CLI controls the same daemon as the app:
|
||||
|
||||
```bash
|
||||
paseo run --provider opencode "implement OAuth"
|
||||
paseo run --provider claude --worktree refactor-auth "refactor auth"
|
||||
paseo run --host devbox:6767 "run the test suite"
|
||||
paseo ls
|
||||
paseo send <agent-id> "add tests"
|
||||
paseo schedule create --cron "0 9 * * 1" "audit the codebase"
|
||||
```
|
||||
|
||||
`paseo run --host` connects to a remote daemon. `paseo schedule` runs an agent on a cron. `paseo loop` retries an agent until a verification command passes. The MCP server lets other agents create worktrees, launch agents, open terminals, and send prompts.
|
||||
|
||||
## Worktrees and services
|
||||
|
||||
Paseo runs each agent in its own Git worktree. Each worktree gets its own dev server URL like `web.fix-auth.my-app.localhost`, so parallel agents don't fight for ports.
|
||||
|
||||
OpenCode supports multi-session work on the same project. If you want worktree isolation around OpenCode sessions, Paseo can provide that by launching OpenCode inside Paseo workspaces.
|
||||
|
||||
## Privacy and source
|
||||
|
||||
Both tools are open source.
|
||||
|
||||
Paseo is AGPL-3.0 and runs your agents through a daemon you control. OpenCode is open source and says it does not store your code or context data by default. OpenCode share links are public when you create them.
|
||||
|
||||
## Voice
|
||||
|
||||
Paseo supports dictation and realtime voice mode. Speech-to-text and text-to-speech can run locally on your device.
|
||||
|
||||
## Comparison
|
||||
|
||||
| | Paseo | OpenCode Desktop |
|
||||
| ---------------------------- | --------------------------------------------------------------- | ------------------------------- |
|
||||
| License | Open source (AGPL-3.0) | Open source |
|
||||
| Desktop platforms | macOS, Linux, Windows | macOS, Linux, Windows |
|
||||
| Native mobile | iOS, Android | No |
|
||||
| Agent harnesses | Claude Code, Codex, OpenCode, Pi + 30+ via ACP catalog + custom | OpenCode |
|
||||
| Multi-model support | Through supported agent harnesses | Through OpenCode providers |
|
||||
| Git worktrees | Yes | No built-in worktree manager |
|
||||
| Per-worktree dev server URLs | Yes | No |
|
||||
| Split panes and tabs | Yes | Desktop sessions |
|
||||
| In-app terminal | Yes | OpenCode terminal workflow |
|
||||
| In-app browser | Yes | No |
|
||||
| GitHub workflow in app | Commit, push, PR, checks, reviews, merge | GitHub integration |
|
||||
| CLI | Run, `--host`, ls, send, schedule, loop | OpenCode CLI |
|
||||
| MCP server for orchestration | Yes | MCP support inside OpenCode |
|
||||
| Local voice | Yes | No |
|
||||
| Self-hosted daemon | Yes | OpenCode server / local runtime |
|
||||
|
||||
See also: [Paseo vs Codex App](/alternatives/codex-app), [Paseo vs Claude Desktop](/alternatives/claude-desktop), [Paseo vs OpenChamber](/alternatives/openchamber).
|
||||
@@ -0,0 +1,116 @@
|
||||
---
|
||||
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: 51
|
||||
---
|
||||
|
||||
# Paseo vs Superset
|
||||
|
||||
Superset is a macOS desktop app for running CLI coding agents in parallel git worktrees. Source-available under the Elastic License 2.0.
|
||||
|
||||
Paseo is an app for orchestrating coding agents, with native clients on desktop, mobile, web, and the CLI. Open source (AGPL-3.0).
|
||||
|
||||

|
||||
|
||||
## When to pick what
|
||||
|
||||
Pick Superset if you prefer a terminal-first interface where agents live inside terminal panes.
|
||||
|
||||
Pick Paseo if you want:
|
||||
|
||||
- An OSI-approved open source license (AGPL-3.0)
|
||||
- Linux or Windows
|
||||
- A native mobile app
|
||||
- No login wall
|
||||
- A per-agent UI with modes, slash commands, and file pickers
|
||||
- Free without seat limits
|
||||
|
||||
## License
|
||||
|
||||
Paseo is open source under AGPL-3.0. Audit it, fork it, redistribute it.
|
||||
|
||||
Superset is source-available under the Elastic License 2.0. The source is on GitHub, but the license restricts hosting it as a service and limits redistribution.
|
||||
|
||||
## Login
|
||||
|
||||
Superset's desktop app shows a Superset login wall on first launch. A Superset account is required to use it.
|
||||
|
||||
Paseo does not require any login.
|
||||
|
||||
## Architecture
|
||||
|
||||
The Paseo daemon runs as its own process. Desktop, web, mobile, and CLI clients connect to it. Run the daemon on your laptop, on a server, or in Docker, and connect from anywhere.
|
||||
|
||||
Superset's desktop is the host. Agents run inside it.
|
||||
|
||||
## Providers
|
||||
|
||||
Both tools support many agents. Superset is a terminal multiplexer where each agent runs inside a terminal pane. Paseo runs Claude Code, Codex, OpenCode, and Pi natively with a per-agent UI (modes, slash commands, file picker, diff viewer), plus 30+ more agents through the in-app catalog via ACP, plus any custom CLI agent. See [Supported providers](/docs/supported-providers).
|
||||
|
||||
## Panes
|
||||
|
||||
Paseo's app has split panes and tabs. Panes include a diff viewer and a browser for testing running services. Agents render as native UI with modes, slash commands, and file pickers.
|
||||
|
||||
In Superset, each agent runs inside a terminal pane.
|
||||
|
||||
## GitHub
|
||||
|
||||
Paseo's app handles commit, push, opening PRs, watching checks and reviews, and merging.
|
||||
|
||||
## CLI
|
||||
|
||||
Paseo has a CLI that mirrors the app:
|
||||
|
||||
```bash
|
||||
paseo run --provider codex "implement OAuth"
|
||||
paseo run --host devbox:6767 "run the test suite"
|
||||
paseo ls
|
||||
paseo send <agent-id> "add tests"
|
||||
paseo schedule create --cron "0 9 * * 1" "audit the codebase"
|
||||
```
|
||||
|
||||
`paseo run --host` connects to a remote daemon. `paseo schedule` runs an agent on a cron. `paseo loop` retries an agent until a verification command passes.
|
||||
|
||||
Superset is a desktop app and does not have a CLI.
|
||||
|
||||
## Worktrees and services
|
||||
|
||||
Both tools isolate parallel agents in git worktrees.
|
||||
|
||||
Paseo also gives each worktree its own dev server URL like `web.fix-auth.my-app.localhost`, so parallel agents don't fight for ports.
|
||||
|
||||
## Mobile
|
||||
|
||||
Paseo ships native iOS and Android apps with the same feature set as the desktop. Superset does not have a mobile app.
|
||||
|
||||
## Voice
|
||||
|
||||
Paseo's speech-to-text and text-to-speech run locally on your device. Superset does not have voice.
|
||||
|
||||
## Pricing
|
||||
|
||||
Paseo is free with no seat limits.
|
||||
|
||||
Superset is free for one seat with local workspaces only. Team features and sync start at $20 per seat per month.
|
||||
|
||||
## Comparison
|
||||
|
||||
| | Paseo | Superset |
|
||||
| ---------------------------- | ----------------------------------------------------- | -------------------------------------- |
|
||||
| License | Open source (AGPL-3.0) | Source-available (Elastic License 2.0) |
|
||||
| Platforms | macOS, Linux, Windows | macOS only |
|
||||
| Native mobile | iOS, Android | — |
|
||||
| Login required | No | Yes (Superset account) |
|
||||
| Pricing | Free | Free 1 seat, $20/seat/mo Pro |
|
||||
| Per-agent native UI | Yes (modes, slash commands, file picker, diff viewer) | Terminal output |
|
||||
| Split panes and tabs | Yes | Yes (terminals) |
|
||||
| In-app browser | Yes | — |
|
||||
| GitHub workflow in app | Commit, push, PR, checks, reviews, merge | Yes |
|
||||
| Git worktrees | Yes | Yes |
|
||||
| Per-worktree dev server URLs | Yes | — |
|
||||
| CLI | Run, `--host`, ls, send, schedule, loop | — |
|
||||
| Local voice (on-device) | Yes | — |
|
||||
| Self-hosted daemon | Yes | — |
|
||||
|
||||
See also: [Paseo vs Conductor](/alternatives/conductor), [Paseo vs OpenChamber](/alternatives/openchamber), [Paseo vs Happy Coder](/alternatives/happy-coder).
|
||||
@@ -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"),
|
||||
);
|
||||
@@ -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"),
|
||||
);
|
||||
@@ -0,0 +1,6 @@
|
||||
import { createFileRoute } from "@tanstack/react-router";
|
||||
import { alternativeRouteOptions } from "~/components/alternative-page";
|
||||
|
||||
export const Route = createFileRoute("/alternatives/conductor")(
|
||||
alternativeRouteOptions("conductor"),
|
||||
);
|
||||
@@ -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"),
|
||||
);
|
||||
@@ -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"),
|
||||
);
|
||||
@@ -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) => ({
|
||||
|
||||
Reference in New Issue
Block a user