Compare commits

..
7 Commits
Author SHA1 Message Date
Mohamed Boudra 0820ee14f1 ci: trigger nix build on highlight changes 2026-03-25 23:20:48 +07:00
José Albornoz 49ebc26cdb integrate with npm run build:daemon 2026-03-25 22:52:43 +07:00
José Albornoz 6bad956c8f fix hash update script, and update hash 2026-03-25 22:52:43 +07:00
José Albornoz 233b309fd9 better document npm workaround 2026-03-25 22:52:43 +07:00
José Albornoz ae4123be10 don't block PRs on nix failures 2026-03-25 22:52:43 +07:00
José Albornoz 6b0ff42748 feat: add Nix flake with package and NixOS module
Add a Nix flake that builds the Paseo daemon (server + CLI) and
provides a NixOS module for declarative deployment.

Package (nix/package.nix):
- Builds relay, server, and CLI workspaces
- Skips onnxruntime-node install script (sandbox-incompatible)
- Rebuilds only node-pty for native terminal support
- Source filter excludes app/website/desktop workspaces

NixOS module (nix/module.nix):
- Systemd service with configurable user, port, listen address
- allowedHosts for DNS rebinding protection
- relay.enable to toggle remote access via app.paseo.sh
- inheritUserEnvironment to expose user tools (git, ssh) to agents
- openFirewall and extra environment variables

ci: add Nix hash maintenance scripts and workflows

scripts/fix-lockfile.mjs:
  Adds missing resolved/integrity fields to package-lock.json for
  workspace-local overrides. Idempotent, uses `npm view`.

scripts/update-nix.sh:
  Runs fix-lockfile.mjs, prefetches deps, computes NAR hash, and
  updates npmDepsHash in nix/package.nix. Supports --check for CI.

.github/workflows/nix-build.yml:
  Builds the Nix package on push/PR and verifies the lockfile and
  hash are up to date.

.github/workflows/fix-nix-hash.yml:
  Auto-fixes lockfile signatures and Nix hash on dependabot PRs.

fix: update npmDepsHash after upstream sync

nix: allowlist workspace symlinks instead of blocklist

Prevents build failures when upstream adds new workspace packages.
2026-03-25 22:52:43 +07:00
José Albornoz 627dc21736 fix: add missing resolved/integrity fields to package-lock.json
npm omits resolved URLs and integrity hashes for workspace-local
node_modules overrides. This breaks offline installers like Nix's
npm ci. Add the missing fields for 25 workspace-hoisted packages.
2026-03-25 22:52:43 +07:00
38 changed files with 929 additions and 1993 deletions
-14
View File
@@ -1,19 +1,5 @@
# Changelog
## 0.1.35 - 2026-03-26
### Improved
- Faster app startup by redirecting to the welcome screen immediately and showing host connection status inline.
- Codex file deletions now display correctly as removed lines in diffs.
- OpenCode questions are now surfaced in the permission UI.
### Fixed
- Fixed queued prompt dispatch after idle transition.
- Replaced bash-only `mapfile` with a portable `while-read` loop in the chat script.
### Added
- Added support for Nix and NixOS installation.
## 0.1.34 - 2026-03-25
### Added
+9 -61
View File
@@ -4,88 +4,40 @@
<h1 align="center">Paseo</h1>
<p align="center">One interface for all your Claude Code, Codex and OpenCode agents.</p>
<p align="center">One interface for all your coding agents.</p>
<p align="center">
<img src="https://paseo.sh/hero-mockup.png" alt="Paseo app screenshot" width="100%">
</p>
<p align="center">
<img src="https://paseo.sh/mobile-mockup.png" alt="Paseo mobile app" width="100%">
<img src="https://paseo.sh/paseo-mockup.png" alt="Paseo app screenshot" width="100%">
</p>
---
Run agents in parallel on your own machines. Ship from your phone or your desk.
- **Self-hosted:** Agents run on your machine with your full dev environment. Use your tools, your configs, and your skills.
- **Multi-provider:** Claude Code, Codex, and OpenCode through the same interface. Pick the right model for each job.
- **Voice control:** Dictate tasks or talk through problems in voice mode. Hands-free when you need it.
- **Cross-device:** iOS, Android, desktop, web, and CLI. Start work at your desk, check in from your phone, script it from the terminal.
- **Privacy-first:** Paseo doesn't have any telemetry, tracking, or forced log-ins.
- **Self-hosted** — Agents run on your machine with your full dev environment. Use your tools, your configs, and your skills.
- **Multi-provider** — Claude Code, Codex, and OpenCode through the same interface. Pick the right model for each job.
- **Voice control** — Dictate tasks or talk through problems in voice mode. Hands-free when you need it.
- **Cross-device** — iOS, Android, desktop, web, and CLI. Start work at your desk, check in from your phone, script it from the terminal.
## Getting Started
### Desktop app
Download from [paseo.sh/download](https://paseo.sh/download) or the [GitHub releases page](https://github.com/getpaseo/paseo/releases). The app bundles its own daemon, so there's nothing else to install. It can also connect to daemons running on other machines.
Download the desktop app from [paseo.sh](https://paseo.sh) or the [GitHub releases page](https://github.com/getpaseo/paseo/releases) — it bundles the daemon so there's nothing else to install.
### Headless / server mode
Run the daemon on any machine:
To run the daemon on a remote or headless machine:
```bash
npm install -g @getpaseo/cli
paseo
```
Then connect from any client — desktop, web, mobile, or CLI. See [paseo.sh/download](https://paseo.sh/download) for all options.
Then connect from the desktop app, mobile app, or CLI.
For full setup and configuration, see:
- [Docs](https://paseo.sh/docs)
- [Configuration reference](https://paseo.sh/docs/configuration)
## CLI
Everything you can do in the app, you can do from the terminal.
```bash
paseo run --provider claude/opus-4.6 "implement user authentication"
paseo run --provider codex/gpt-5.4 --worktree feature-x "implement feature X"
paseo ls # list running agents
paseo attach abc123 # stream live output
paseo send abc123 "also add tests" # follow-up task
# run on a remote daemon
paseo --host workstation.local:6767 run "run the full test suite"
```
See the [full CLI reference](https://paseo.sh/docs/cli) for more.
## Orchestration skills (Unstable)
Experimental skills that teach agents how to use the Paseo CLI to orchestrate other agents. I am updating these very frequently as I learn new things, expect changes without notice, might be coupled to my own setup, use at your own risk.
```bash
npx skills add getpaseo/paseo
```
Then use them in any agent conversation:
```bash
# Use handoff when you discuss something with an agent but want another one to implement.
# I use this to plan with Claude and then handoff to Codex to implement.
/paseo-handoff hand off the authentication fix to codex 5.4 in a worktree
# Use loops when you have clear acceptance criteria (aka Ralph loops).
/paseo-loop loop a codex agent to fix the backend tests, use sonnet to verify, max 10 iterations
# Orchestrator teaches the agent how to create teams and manage them via a chat room.
# Very opinionated and expects both Codex and Claude to work.
/paseo-orchestrator spin up a team to implement the database refactor, use chat to coordinate. use claude to plan and codex to implement and review
```
## Development
Quick monorepo package map:
@@ -105,12 +57,8 @@ npm run dev
# run individual surfaces
npm run dev:server
npm run dev:app
npm run dev:desktop
npm run dev:website
# build the daemon
npm run build:daemon
# repo-wide checks
npm run typecheck
```
+1 -1
View File
@@ -51,4 +51,4 @@ Paseo wraps agent CLIs (Claude Code, Codex, OpenCode) but does not manage their
## Reporting vulnerabilities
If you discover a security vulnerability, please report it privately by emailing hello@moboudra.com. Do not open a public issue.
If you discover a security vulnerability, please report it privately by emailing mo@faro.so. Do not open a public issue.
+1 -1
View File
@@ -42,7 +42,7 @@ buildNpmPackage rec {
# To update: run `nix build` with lib.fakeHash, copy the `got:` hash.
# CI auto-updates this when package-lock.json changes (see .github/workflows/).
npmDepsHash = "sha256-gOwvUBvem1SxDMypaexz6RaHRm2xFmUT9iwOW2ErEAM=";
npmDepsHash = "sha256-qRknrMt2VBCCRGGedyTrPEBs3u+Z8lmDZuuvX7mE2Cg=";
# Prevent onnxruntime-node's install script from running during automatic
# npm rebuild (it tries to download from api.nuget.org, which fails in the sandbox).
+94 -95
View File
@@ -1,12 +1,12 @@
{
"name": "paseo",
"version": "0.1.35",
"version": "0.1.34",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "paseo",
"version": "0.1.35",
"version": "0.1.34",
"hasInstallScript": true,
"license": "AGPL-3.0-or-later",
"workspaces": [
@@ -34842,16 +34842,16 @@
},
"packages/app": {
"name": "@getpaseo/app",
"version": "0.1.35",
"version": "0.1.34",
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@expo/vector-icons": "^15.0.2",
"@floating-ui/react-native": "^0.10.7",
"@getpaseo/expo-two-way-audio": "0.1.35",
"@getpaseo/highlight": "0.1.35",
"@getpaseo/server": "0.1.35",
"@getpaseo/expo-two-way-audio": "0.1.34",
"@getpaseo/highlight": "0.1.34",
"@getpaseo/server": "0.1.34",
"@gorhom/bottom-sheet": "^5.2.6",
"@gorhom/portal": "^1.0.14",
"@react-native-async-storage/async-storage": "2.2.0",
@@ -34937,14 +34937,14 @@
},
"packages/app/node_modules/expo-clipboard": {
"version": "8.0.7",
"resolved": "https://registry.npmjs.org/expo-clipboard/-/expo-clipboard-8.0.7.tgz",
"integrity": "sha512-zvlfFV+wB2QQrQnHWlo0EKHAkdi2tycLtE+EXFUWTPZYkgu1XcH+aiKfd4ul7Z0SDF+1IuwoiW9AA9eO35aj3Q==",
"license": "MIT",
"peerDependencies": {
"expo": "*",
"react": "*",
"react-native": "*"
}
},
"resolved": "https://registry.npmjs.org/expo-clipboard/-/expo-clipboard-8.0.7.tgz",
"integrity": "sha512-zvlfFV+wB2QQrQnHWlo0EKHAkdi2tycLtE+EXFUWTPZYkgu1XcH+aiKfd4ul7Z0SDF+1IuwoiW9AA9eO35aj3Q=="
},
"packages/app/node_modules/react-native-nitro-modules": {
"version": "0.33.8",
@@ -34958,20 +34958,20 @@
},
"packages/app/node_modules/zod": {
"version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="
},
"packages/cli": {
"name": "@getpaseo/cli",
"version": "0.1.35",
"version": "0.1.34",
"dependencies": {
"@clack/prompts": "^1.0.0",
"@getpaseo/relay": "0.1.35",
"@getpaseo/server": "0.1.35",
"@getpaseo/relay": "0.1.34",
"@getpaseo/server": "0.1.34",
"chalk": "^5.3.0",
"commander": "^12.0.0",
"mime-types": "^2.1.35",
@@ -34991,32 +34991,31 @@
},
"packages/cli/node_modules/chalk": {
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
"license": "MIT",
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
"integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="
},
"packages/cli/node_modules/commander": {
"version": "12.1.0",
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
"integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==",
"license": "MIT",
"engines": {
"node": ">=18"
}
},
"resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz",
"integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="
},
"packages/desktop": {
"name": "@getpaseo/desktop",
"version": "0.1.35",
"license": "AGPL-3.0-or-later",
"version": "0.1.34",
"dependencies": {
"@getpaseo/cli": "0.1.35",
"@getpaseo/server": "0.1.35",
"@getpaseo/cli": "0.1.34",
"@getpaseo/server": "0.1.34",
"electron-updater": "^6.6.2",
"ws": "^8.14.2"
},
@@ -35049,7 +35048,7 @@
},
"packages/expo-two-way-audio": {
"name": "@getpaseo/expo-two-way-audio",
"version": "0.1.35",
"version": "0.1.34",
"license": "MIT",
"devDependencies": {
"@biomejs/biome": "1.9.4",
@@ -35250,7 +35249,7 @@
},
"packages/highlight": {
"name": "@getpaseo/highlight",
"version": "0.1.35",
"version": "0.1.34",
"dependencies": {
"@lezer/common": "^1.5.0",
"@lezer/cpp": "^1.1.5",
@@ -35276,7 +35275,7 @@
},
"packages/relay": {
"name": "@getpaseo/relay",
"version": "0.1.35",
"version": "0.1.34",
"dependencies": {
"base64-js": "^1.5.1",
"tweetnacl": "^1.0.3",
@@ -35292,13 +35291,13 @@
},
"packages/server": {
"name": "@getpaseo/server",
"version": "0.1.35",
"version": "0.1.34",
"dependencies": {
"@ai-sdk/openai": "2.0.52",
"@anthropic-ai/claude-agent-sdk": "^0.2.11",
"@deepgram/sdk": "^3.4.0",
"@getpaseo/highlight": "0.1.35",
"@getpaseo/relay": "0.1.35",
"@getpaseo/highlight": "0.1.34",
"@getpaseo/relay": "0.1.34",
"@modelcontextprotocol/sdk": "^1.20.1",
"@opencode-ai/sdk": "1.2.6",
"@sctg/sentencepiece-js": "^1.1.0",
@@ -35343,8 +35342,6 @@
},
"packages/server/node_modules/@modelcontextprotocol/sdk": {
"version": "1.20.1",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.20.1.tgz",
"integrity": "sha512-j/P+yuxXfgxb+mW7OEoRCM3G47zCTDqUPivJo/VzpjbG8I9csTXtOprCf5FfOfHK4whOJny0aHuBEON+kS7CCA==",
"license": "MIT",
"dependencies": {
"ajv": "^6.12.6",
@@ -35362,12 +35359,12 @@
},
"engines": {
"node": ">=18"
}
},
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.20.1.tgz",
"integrity": "sha512-j/P+yuxXfgxb+mW7OEoRCM3G47zCTDqUPivJo/VzpjbG8I9csTXtOprCf5FfOfHK4whOJny0aHuBEON+kS7CCA=="
},
"packages/server/node_modules/@modelcontextprotocol/sdk/node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
@@ -35378,12 +35375,12 @@
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g=="
},
"packages/server/node_modules/@modelcontextprotocol/sdk/node_modules/express": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
"integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==",
"license": "MIT",
"dependencies": {
"accepts": "^2.0.0",
@@ -35420,7 +35417,9 @@
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz",
"integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="
},
"packages/server/node_modules/@modelcontextprotocol/sdk/node_modules/json-schema-traverse": {
"version": "0.4.1",
@@ -35430,8 +35429,6 @@
},
"packages/server/node_modules/accepts": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
"integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
"license": "MIT",
"dependencies": {
"mime-types": "^3.0.0",
@@ -35439,12 +35436,12 @@
},
"engines": {
"node": ">= 0.6"
}
},
"resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
"integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="
},
"packages/server/node_modules/ajv": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
@@ -35455,24 +35452,24 @@
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="
},
"packages/server/node_modules/ansi-regex": {
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
"license": "MIT",
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
}
},
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="
},
"packages/server/node_modules/body-parser": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
"integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==",
"license": "MIT",
"dependencies": {
"bytes": "^3.1.2",
@@ -35487,33 +35484,33 @@
},
"engines": {
"node": ">=18"
}
},
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz",
"integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="
},
"packages/server/node_modules/content-disposition": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
"integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==",
"license": "MIT",
"dependencies": {
"safe-buffer": "5.2.1"
},
"engines": {
"node": ">= 0.6"
}
},
"resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz",
"integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="
},
"packages/server/node_modules/cookie-signature": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
"integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
"license": "MIT",
"engines": {
"node": ">=6.6.0"
}
},
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
"integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="
},
"packages/server/node_modules/finalhandler": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
"integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==",
"license": "MIT",
"dependencies": {
"debug": "^4.4.0",
@@ -35525,58 +35522,60 @@
},
"engines": {
"node": ">= 0.8"
}
},
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz",
"integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="
},
"packages/server/node_modules/fresh": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
"integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
"integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="
},
"packages/server/node_modules/media-typer": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
"integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
"license": "MIT",
"engines": {
"node": ">= 0.8"
}
},
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
"integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="
},
"packages/server/node_modules/merge-descriptors": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
"integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
"integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="
},
"packages/server/node_modules/mime-types": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
"integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==",
"license": "MIT",
"dependencies": {
"mime-db": "^1.54.0"
},
"engines": {
"node": ">= 0.6"
}
},
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz",
"integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="
},
"packages/server/node_modules/negotiator": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
"integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="
},
"packages/server/node_modules/raw-body": {
"version": "3.0.2",
@@ -35611,8 +35610,6 @@
},
"packages/server/node_modules/send": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
"integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==",
"license": "MIT",
"dependencies": {
"debug": "^4.3.5",
@@ -35629,12 +35626,12 @@
},
"engines": {
"node": ">= 18"
}
},
"resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz",
"integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="
},
"packages/server/node_modules/serve-static": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
"integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==",
"license": "MIT",
"dependencies": {
"encodeurl": "^2.0.0",
@@ -35644,12 +35641,12 @@
},
"engines": {
"node": ">= 18"
}
},
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz",
"integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="
},
"packages/server/node_modules/strip-ansi": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
"license": "MIT",
"dependencies": {
"ansi-regex": "^6.0.1"
@@ -35659,12 +35656,12 @@
},
"funding": {
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
}
},
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="
},
"packages/server/node_modules/type-is": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
"integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
"license": "MIT",
"dependencies": {
"content-type": "^1.0.5",
@@ -35673,20 +35670,22 @@
},
"engines": {
"node": ">= 0.6"
}
},
"resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
"integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="
},
"packages/server/node_modules/zod": {
"version": "3.25.76",
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
"integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="
},
"packages/website": {
"name": "@getpaseo/website",
"version": "0.1.35",
"version": "0.1.34",
"dependencies": {
"@cloudflare/vite-plugin": "^1.20.3",
"@cloudflare/workers-types": "^4.20260114.0",
@@ -35712,13 +35711,13 @@
},
"packages/website/node_modules/@types/node": {
"version": "22.19.6",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.6.tgz",
"integrity": "sha512-qm+G8HuG6hOHQigsi7VGuLjUVu6TtBo/F05zvX04Mw2uCg9Dv0Qxy3Qw7j41SidlTcl5D/5yg0SEZqOB+EqZnQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~6.21.0"
}
},
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.6.tgz",
"integrity": "sha512-qm+G8HuG6hOHQigsi7VGuLjUVu6TtBo/F05zvX04Mw2uCg9Dv0Qxy3Qw7j41SidlTcl5D/5yg0SEZqOB+EqZnQ=="
},
"packages/website/node_modules/react": {
"version": "19.2.4",
+2 -10
View File
@@ -1,6 +1,6 @@
{
"name": "paseo",
"version": "0.1.35",
"version": "0.1.34",
"private": true,
"workspaces": [
"packages/expo-two-way-audio",
@@ -68,7 +68,6 @@
"typescript": "^5.9.3"
},
"description": "Paseo: voice-controlled development environment with OpenAI Realtime API",
"homepage": "https://paseo.sh",
"keywords": [
"openai",
"realtime",
@@ -77,14 +76,7 @@
"development",
"mcp"
],
"author": {
"name": "Mohamed Boudra",
"email": "hello@moboudra.com"
},
"repository": {
"type": "git",
"url": "https://github.com/getpaseo/paseo.git"
},
"author": "moboudra",
"license": "AGPL-3.0-or-later",
"overrides": {
"lightningcss": "1.30.1",
+4 -4
View File
@@ -1,7 +1,7 @@
{
"name": "@getpaseo/app",
"main": "index.ts",
"version": "0.1.35",
"version": "0.1.34",
"private": true,
"scripts": {
"start": "expo start",
@@ -31,9 +31,9 @@
"@dnd-kit/utilities": "^3.2.2",
"@expo/vector-icons": "^15.0.2",
"@floating-ui/react-native": "^0.10.7",
"@getpaseo/expo-two-way-audio": "0.1.35",
"@getpaseo/highlight": "0.1.35",
"@getpaseo/server": "0.1.35",
"@getpaseo/expo-two-way-audio": "0.1.34",
"@getpaseo/highlight": "0.1.34",
"@getpaseo/server": "0.1.34",
"@gorhom/bottom-sheet": "^5.2.6",
"@gorhom/portal": "^1.0.14",
"@react-native-async-storage/async-storage": "2.2.0",
@@ -0,0 +1,36 @@
export const WELCOME_ROUTE = "/welcome";
export function shouldWaitOnStartupRace(input: {
onlineServerId: string | null;
hasTimedOut: boolean;
isDesktopStartupRace: boolean;
daemonCount: number;
pathname: string;
}): boolean {
if (input.onlineServerId) {
return false;
}
if (input.pathname === WELCOME_ROUTE) {
return false;
}
if (input.hasTimedOut) {
return false;
}
return input.isDesktopStartupRace || input.daemonCount > 0;
}
export function shouldRedirectToWelcome(input: {
onlineServerId: string | null;
hasTimedOut: boolean;
pathname: string;
isDesktopStartupRace: boolean;
daemonCount: number;
}): boolean {
if (input.onlineServerId || !input.hasTimedOut) {
return false;
}
if (input.pathname !== "/" && input.pathname !== "") {
return false;
}
return input.isDesktopStartupRace || input.daemonCount > 0;
}
+7 -20
View File
@@ -25,9 +25,6 @@ import {
useHostMutations,
useHostRuntimeClient,
} from "@/runtime/host-runtime";
import { shouldUseDesktopDaemon } from "@/desktop/daemon/desktop-daemon";
import { StartupSplashScreen } from "@/screens/startup-splash-screen";
import { loadSettingsFromStorage } from "@/hooks/use-settings";
import { SessionProvider } from "@/contexts/session-context";
import type { HostProfile } from "@/types/host-connection";
import {
@@ -213,22 +210,14 @@ function HostRuntimeBootstrapProvider({ children }: { children: ReactNode }) {
let cancelled = false;
const store = getHostRuntimeStore();
const init = async () => {
const settings = await loadSettingsFromStorage();
const isDesktopManaged = shouldUseDesktopDaemon() && settings.manageBuiltInDaemon;
await store.loadFromStorage();
if (isDesktopManaged) {
await store.bootstrap({ manageBuiltInDaemon: true });
} else {
void store.bootstrap({ manageBuiltInDaemon: settings.manageBuiltInDaemon });
}
};
void init()
void store
.loadFromStorage()
.then(() => {
if (!cancelled) {
setReady(true);
if (cancelled) {
return;
}
setReady(true);
void store.bootstrap();
})
.catch((error) => {
console.error("[HostRuntime] Failed to initialize store", error);
@@ -426,9 +415,7 @@ function ProvidersWrapper({ children }: { children: ReactNode }) {
}, [isLoading, settings.theme]);
if (isLoading) {
const isDesktopManaged =
!settingsLoading && shouldUseDesktopDaemon() && settings.manageBuiltInDaemon;
return isDesktopManaged ? <StartupSplashScreen /> : <LoadingView />;
return <LoadingView />;
}
return (
+98 -3
View File
@@ -1,18 +1,113 @@
import { useEffect } from "react";
import { useEffect, useSyncExternalStore, useState } from "react";
import { usePathname, useRouter } from "expo-router";
import { useHosts } from "@/runtime/host-runtime";
import { shouldUseDesktopDaemon } from "@/desktop/daemon/desktop-daemon";
import { buildHostRootRoute } from "@/utils/host-routes";
import { StartupSplashScreen } from "@/screens/startup-splash-screen";
import { WelcomeScreen } from "@/components/welcome-screen";
import { getHostRuntimeStore, isHostRuntimeConnected } from "@/runtime/host-runtime";
import {
shouldRedirectToWelcome,
shouldWaitOnStartupRace,
WELCOME_ROUTE,
} from "@/app-support/index-startup";
const WELCOME_ROUTE = "/welcome";
const STARTUP_TIMEOUT_MS = 30_000;
function useAnyHostOnline(serverIds: string[]): string | null {
const runtime = getHostRuntimeStore();
return useSyncExternalStore(
(onStoreChange) => runtime.subscribeAll(onStoreChange),
() => {
let firstOnlineServerId: string | null = null;
let firstOnlineAt: string | null = null;
for (const serverId of serverIds) {
const snapshot = runtime.getSnapshot(serverId);
const lastOnlineAt = snapshot?.lastOnlineAt ?? null;
if (!isHostRuntimeConnected(snapshot) || !lastOnlineAt) {
continue;
}
if (!firstOnlineAt || lastOnlineAt < firstOnlineAt) {
firstOnlineAt = lastOnlineAt;
firstOnlineServerId = serverId;
}
}
return firstOnlineServerId;
},
() => {
let firstOnlineServerId: string | null = null;
let firstOnlineAt: string | null = null;
for (const serverId of serverIds) {
const snapshot = runtime.getSnapshot(serverId);
const lastOnlineAt = snapshot?.lastOnlineAt ?? null;
if (!isHostRuntimeConnected(snapshot) || !lastOnlineAt) {
continue;
}
if (!firstOnlineAt || lastOnlineAt < firstOnlineAt) {
firstOnlineAt = lastOnlineAt;
firstOnlineServerId = serverId;
}
}
return firstOnlineServerId;
},
);
}
export default function Index() {
const router = useRouter();
const pathname = usePathname();
const daemons = useHosts();
const [hasTimedOut, setHasTimedOut] = useState(false);
const isDesktopStartupRace = shouldUseDesktopDaemon();
const onlineServerId = useAnyHostOnline(daemons.map((daemon) => daemon.serverId));
useEffect(() => {
const timer = setTimeout(() => {
setHasTimedOut(true);
}, STARTUP_TIMEOUT_MS);
return () => {
clearTimeout(timer);
};
}, []);
useEffect(() => {
if (!onlineServerId) {
return;
}
if (pathname !== "/" && pathname !== "") {
return;
}
router.replace(buildHostRootRoute(onlineServerId) as any);
}, [onlineServerId, pathname, router]);
useEffect(() => {
if (
!shouldRedirectToWelcome({
onlineServerId,
hasTimedOut,
pathname,
isDesktopStartupRace,
daemonCount: daemons.length,
})
) {
return;
}
router.replace(WELCOME_ROUTE as any);
}, [pathname, router]);
}, [daemons.length, hasTimedOut, isDesktopStartupRace, onlineServerId, pathname, router]);
if (
shouldWaitOnStartupRace({
onlineServerId,
hasTimedOut,
isDesktopStartupRace,
daemonCount: daemons.length,
pathname,
})
) {
return <StartupSplashScreen />;
}
if (!onlineServerId) {
return <WelcomeScreen />;
}
return null;
}
+3 -149
View File
@@ -1,17 +1,11 @@
import { useCallback, useEffect, useState, useSyncExternalStore } from "react";
import { useCallback, useState } from "react";
import { Pressable, Text, View, Platform, ScrollView } from "react-native";
import { useRouter } from "expo-router";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { QrCode, Link2, ClipboardPaste } from "lucide-react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import type { HostProfile } from "@/types/host-connection";
import {
getHostRuntimeStore,
isHostRuntimeConnected,
useHostMutations,
useHostRuntimeSnapshot,
useHosts,
} from "@/runtime/host-runtime";
import { useHostMutations } from "@/runtime/host-runtime";
import { useSessionStore } from "@/stores/session-store";
import { AddHostModal } from "./add-host-modal";
import { PairLinkModal } from "./pair-link-modal";
@@ -85,39 +79,6 @@ const styles = StyleSheet.create((theme) => ({
actionTextPrimary: {
color: theme.colors.accentForeground,
},
hostList: {
width: "100%",
maxWidth: 420,
marginTop: theme.spacing[6],
borderTopWidth: 1,
borderTopColor: theme.colors.border,
paddingTop: theme.spacing[4],
gap: theme.spacing[2],
},
hostRow: {
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[3],
paddingVertical: theme.spacing[2],
},
statusDot: {
width: 8,
height: 8,
borderRadius: 4,
},
hostLabel: {
flex: 1,
color: theme.colors.foreground,
fontSize: theme.fontSize.sm,
},
hostStatus: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.sm,
},
hostStatusError: {
color: theme.colors.destructive,
fontSize: theme.fontSize.sm,
},
versionLabel: {
color: theme.colors.foregroundMuted,
fontSize: theme.fontSize.xs,
@@ -126,89 +87,6 @@ const styles = StyleSheet.create((theme) => ({
},
}));
function useAnyHostOnline(serverIds: string[]): string | null {
const runtime = getHostRuntimeStore();
return useSyncExternalStore(
(onStoreChange) => runtime.subscribeAll(onStoreChange),
() => {
let firstOnlineServerId: string | null = null;
let firstOnlineAt: string | null = null;
for (const serverId of serverIds) {
const snapshot = runtime.getSnapshot(serverId);
const lastOnlineAt = snapshot?.lastOnlineAt ?? null;
if (!isHostRuntimeConnected(snapshot) || !lastOnlineAt) {
continue;
}
if (!firstOnlineAt || lastOnlineAt < firstOnlineAt) {
firstOnlineAt = lastOnlineAt;
firstOnlineServerId = serverId;
}
}
return firstOnlineServerId;
},
() => {
let firstOnlineServerId: string | null = null;
let firstOnlineAt: string | null = null;
for (const serverId of serverIds) {
const snapshot = runtime.getSnapshot(serverId);
const lastOnlineAt = snapshot?.lastOnlineAt ?? null;
if (!isHostRuntimeConnected(snapshot) || !lastOnlineAt) {
continue;
}
if (!firstOnlineAt || lastOnlineAt < firstOnlineAt) {
firstOnlineAt = lastOnlineAt;
firstOnlineServerId = serverId;
}
}
return firstOnlineServerId;
},
);
}
function HostStatusRow({ serverId, label }: { serverId: string; label: string }) {
const { theme } = useUnistyles();
const snapshot = useHostRuntimeSnapshot(serverId);
const status = snapshot?.connectionStatus ?? "connecting";
const lastError = snapshot?.lastError ?? null;
let dotColor: string;
let statusText: string;
let isError = false;
switch (status) {
case "online":
dotColor = theme.colors.success;
statusText = "Online";
break;
case "connecting":
case "idle":
dotColor = theme.colors.foregroundMuted;
statusText = "Connecting…";
break;
case "offline":
dotColor = theme.colors.foregroundMuted;
statusText = "Offline";
break;
case "error":
dotColor = theme.colors.destructive;
statusText = lastError ? lastError.slice(0, 40) : "Connection error";
isError = true;
break;
}
return (
<View style={styles.hostRow}>
<View style={[styles.statusDot, { backgroundColor: dotColor }]} />
<Text style={styles.hostLabel} numberOfLines={1}>
{label}
</Text>
<Text style={isError ? styles.hostStatusError : styles.hostStatus} numberOfLines={1}>
{statusText}
</Text>
</View>
);
}
export interface WelcomeScreenProps {
onHostAdded?: (profile: HostProfile) => void;
}
@@ -227,8 +105,6 @@ export function WelcomeScreen({ onHostAdded }: WelcomeScreenProps) {
hostname: string | null;
} | null>(null);
const [pendingRedirectServerId, setPendingRedirectServerId] = useState<string | null>(null);
const hosts = useHosts();
const anyOnlineServerId = useAnyHostOnline(hosts.map((h) => h.serverId));
const pendingNameHostname = useSessionStore(
useCallback(
(state) => {
@@ -243,16 +119,6 @@ export function WelcomeScreen({ onHostAdded }: WelcomeScreenProps) {
),
);
useEffect(() => {
if (!anyOnlineServerId) {
return;
}
if (pendingNameHost) {
return;
}
router.replace(buildHostRootRoute(anyOnlineServerId) as any);
}, [anyOnlineServerId, pendingNameHost, router]);
const finishOnboarding = useCallback(
(serverId: string) => {
router.replace(buildHostRootRoute(serverId) as any);
@@ -307,8 +173,6 @@ export function WelcomeScreen({ onHostAdded }: WelcomeScreenProps) {
},
];
const showHostList = hosts.length > 0 && !anyOnlineServerId;
return (
<ScrollView
style={{ flex: 1, backgroundColor: theme.colors.surface0 }}
@@ -322,9 +186,7 @@ export function WelcomeScreen({ onHostAdded }: WelcomeScreenProps) {
<View style={styles.content}>
<PaseoLogo size={96} color={theme.colors.foreground} />
<Text style={styles.title}>Welcome to Paseo</Text>
<Text style={styles.subtitle}>
{showHostList ? "Connecting to your hosts…" : "Connect to your host to start"}
</Text>
<Text style={styles.subtitle}>Connect to your host to start</Text>
<View style={styles.actions}>
{actions.map((action) => {
@@ -347,14 +209,6 @@ export function WelcomeScreen({ onHostAdded }: WelcomeScreenProps) {
);
})}
</View>
{showHostList && (
<View style={styles.hostList}>
{hosts.map((host) => (
<HostStatusRow key={host.serverId} serverId={host.serverId} label={host.label} />
))}
</View>
)}
</View>
<Text style={styles.versionLabel}>{appVersionText}</Text>
@@ -49,7 +49,6 @@ import { derivePendingPermissionKey, normalizeAgentSnapshot } from "@/utils/agen
import { resolveProjectPlacement } from "@/utils/project-placement";
import { buildDraftStoreKey } from "@/stores/draft-keys";
import type { AttachmentMetadata } from "@/attachments/types";
import { reconcilePreviousAgentStatuses } from "@/contexts/session-status-tracking";
// Re-export types from session-store and draft-store for backward compatibility
export type { DraftInput } from "@/stores/draft-store";
@@ -296,10 +295,15 @@ function SessionProviderInternal({ children, serverId, client }: SessionProvider
}, []);
useEffect(() => {
previousAgentStatusRef.current = reconcilePreviousAgentStatuses(
previousAgentStatusRef.current,
sessionAgents,
);
if (!sessionAgents) {
previousAgentStatusRef.current.clear();
return;
}
const nextStatuses = new Map<string, AgentLifecycleStatus>();
for (const nextAgent of sessionAgents.values()) {
nextStatuses.set(nextAgent.id, nextAgent.status);
}
previousAgentStatusRef.current = nextStatuses;
}, [sessionAgents]);
const hydrateWorkspaces = useCallback(
@@ -1,72 +0,0 @@
import { describe, expect, it } from "vitest";
import type { Agent } from "@/stores/session-store";
import { reconcilePreviousAgentStatuses } from "./session-status-tracking";
function createAgent(status: Agent["status"]): Agent {
return {
serverId: "server-1",
id: "agent-1",
provider: "codex",
status,
createdAt: new Date(0),
updatedAt: new Date(0),
lastUserMessageAt: null,
lastActivityAt: new Date(0),
capabilities: {
supportsStreaming: true,
supportsSessionPersistence: true,
supportsDynamicModes: true,
supportsMcpServers: true,
supportsReasoningStream: true,
supportsToolInvocations: true,
},
currentModeId: null,
availableModes: [],
pendingPermissions: [],
persistence: null,
title: "Agent",
cwd: "/tmp",
model: null,
labels: {},
projectPlacement: null,
};
}
describe("reconcilePreviousAgentStatuses", () => {
it("preserves previously seen status for existing agents", () => {
const previous = new Map([["agent-1", "running" as const]]);
const sessionAgents = new Map([["agent-1", createAgent("idle")]]);
const result = reconcilePreviousAgentStatuses(previous, sessionAgents);
expect(result).toEqual(new Map([["agent-1", "running"]]));
});
it("seeds newly seen agents from the current snapshot", () => {
const sessionAgents = new Map([["agent-1", createAgent("idle")]]);
const result = reconcilePreviousAgentStatuses(new Map(), sessionAgents);
expect(result).toEqual(new Map([["agent-1", "idle"]]));
});
it("removes agents that are no longer present", () => {
const previous = new Map([
["agent-1", "running" as const],
["agent-2", "idle" as const],
]);
const sessionAgents = new Map([["agent-1", createAgent("idle")]]);
const result = reconcilePreviousAgentStatuses(previous, sessionAgents);
expect(result).toEqual(new Map([["agent-1", "running"]]));
});
it("clears all tracked statuses when the session is unavailable", () => {
const previous = new Map([["agent-1", "running" as const]]);
const result = reconcilePreviousAgentStatuses(previous, undefined);
expect(result).toEqual(new Map());
});
});
@@ -1,29 +0,0 @@
import type { AgentLifecycleStatus } from "@server/shared/agent-lifecycle";
import type { Agent } from "@/stores/session-store";
export function reconcilePreviousAgentStatuses(
previousStatuses: Map<string, AgentLifecycleStatus>,
sessionAgents: Map<string, Agent> | undefined,
): Map<string, AgentLifecycleStatus> {
if (!sessionAgents) {
return new Map();
}
const nextStatuses = new Map(previousStatuses);
const seenAgentIds = new Set<string>();
for (const agent of sessionAgents.values()) {
seenAgentIds.add(agent.id);
if (!nextStatuses.has(agent.id)) {
nextStatuses.set(agent.id, agent.status);
}
}
for (const agentId of nextStatuses.keys()) {
if (!seenAgentIds.has(agentId)) {
nextStatuses.delete(agentId);
}
}
return nextStatuses;
}
+10 -8
View File
@@ -1137,7 +1137,7 @@ export class HostRuntimeStore {
}
}
async bootstrap(options?: { manageBuiltInDaemon?: boolean }): Promise<void> {
async bootstrap(): Promise<void> {
if (this.bootstrapAttempted) {
return;
}
@@ -1153,9 +1153,7 @@ export class HostRuntimeStore {
}
if (shouldUseDesktopDaemon()) {
if (options?.manageBuiltInDaemon ?? true) {
await this.bootstrapDesktop();
}
await this.bootstrapDesktop();
} else {
await this.bootstrapLocalhost();
}
@@ -1165,16 +1163,20 @@ export class HostRuntimeStore {
try {
const daemon = await startDesktopDaemon();
const connection = connectionFromListen(daemon.listen);
if (!connection || !daemon.serverId) {
if (!connection) {
return;
}
const { client, serverId, hostname } = await connectToDaemon(connection, {
timeoutMs: DEFAULT_LOCALHOST_BOOTSTRAP_TIMEOUT_MS,
});
await this.upsertHostConnection({
serverId: daemon.serverId,
label: daemon.hostname ?? undefined,
serverId,
label: hostname ?? daemon.hostname ?? undefined,
connection,
existingClient: client,
});
} catch (error) {
console.warn("[HostRuntime] Failed to bootstrap desktop daemon", error);
console.warn("[HostRuntime] Failed to bootstrap desktop daemon connection", error);
}
}
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@getpaseo/cli",
"version": "0.1.35",
"version": "0.1.34",
"description": "Paseo CLI - control your AI coding agents from the command line",
"type": "module",
"files": [
@@ -24,8 +24,8 @@
},
"dependencies": {
"@clack/prompts": "^1.0.0",
"@getpaseo/relay": "0.1.35",
"@getpaseo/server": "0.1.35",
"@getpaseo/relay": "0.1.34",
"@getpaseo/server": "0.1.34",
"chalk": "^5.3.0",
"commander": "^12.0.0",
"mime-types": "^2.1.35",
-5
View File
@@ -14,7 +14,6 @@ publish:
owner: getpaseo
repo: paseo
mac:
artifactName: "Paseo-${version}-${arch}.${ext}"
category: public.app-category.developer-tools
icon: assets/icon.icns
hardenedRuntime: true
@@ -31,15 +30,11 @@ linux:
category: Development
icon: assets
artifactName: "Paseo-${version}-${arch}.${ext}"
maintainer: "Mohamed Boudra <hello@moboudra.com>"
vendor: "Paseo"
extraResources:
- from: bin/paseo
to: bin/paseo
target:
- AppImage
- deb
- rpm
- tar.gz
win:
icon: assets/icon.ico
+4 -14
View File
@@ -1,6 +1,6 @@
{
"name": "@getpaseo/desktop",
"version": "0.1.35",
"version": "0.1.34",
"private": true,
"description": "Paseo desktop app (Electron wrapper)",
"main": "dist/main.js",
@@ -12,8 +12,8 @@
"typecheck": "tsc --noEmit -p tsconfig.json"
},
"dependencies": {
"@getpaseo/cli": "0.1.35",
"@getpaseo/server": "0.1.35",
"@getpaseo/cli": "0.1.34",
"@getpaseo/server": "0.1.34",
"electron-updater": "^6.6.2",
"ws": "^8.14.2"
},
@@ -25,15 +25,5 @@
"typescript": "5.9.3",
"unzip-crx-3": "^0.2.0",
"wait-on": "8.0.5"
},
"homepage": "https://paseo.sh",
"repository": {
"type": "git",
"url": "https://github.com/getpaseo/paseo.git"
},
"author": {
"name": "Mohamed Boudra",
"email": "hello@moboudra.com"
},
"license": "AGPL-3.0-or-later"
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@getpaseo/expo-two-way-audio",
"version": "0.1.35",
"version": "0.1.34",
"description": "Native module for two way audio streaming",
"main": "build/index.js",
"types": "build/index.d.ts",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@getpaseo/highlight",
"version": "0.1.35",
"version": "0.1.34",
"type": "module",
"publishConfig": {
"access": "public"
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@getpaseo/relay",
"version": "0.1.35",
"version": "0.1.34",
"description": "Paseo relay for bridging daemon and client connections",
"type": "module",
"publishConfig": {
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@getpaseo/server",
"version": "0.1.35",
"version": "0.1.34",
"description": "Paseo backend server",
"type": "module",
"publishConfig": {
@@ -63,8 +63,8 @@
"@ai-sdk/openai": "2.0.52",
"@anthropic-ai/claude-agent-sdk": "^0.2.11",
"@deepgram/sdk": "^3.4.0",
"@getpaseo/highlight": "0.1.35",
"@getpaseo/relay": "0.1.35",
"@getpaseo/highlight": "0.1.34",
"@getpaseo/relay": "0.1.34",
"@modelcontextprotocol/sdk": "^1.20.1",
"@opencode-ai/sdk": "1.2.6",
"@sctg/sentencepiece-js": "^1.1.0",
@@ -579,127 +579,4 @@ describe("codex tool-call mapper", () => {
expect(item).toBeNull();
});
it("maps apply_patch with Delete File directive into edit detail with removed lines", () => {
const patch = [
"*** Begin Patch",
"*** Delete File: /tmp/repo/src/dead-module.ts",
"*** End Patch",
].join("\n");
const item = expectMapped(
mapCodexRolloutToolCall({
callId: "codex-delete-rollout",
name: "apply_patch",
input: patch,
output:
'{"output":"Success. Updated the following files:\\nD /tmp/repo/src/dead-module.ts\\n"}',
cwd: "/tmp/repo",
}),
);
expect(item.status).toBe("completed");
expect(item.detail.type).toBe("edit");
if (item.detail.type === "edit") {
expect(item.detail.filePath).toBe("src/dead-module.ts");
expect(item.detail.unifiedDiff).toContain("/dev/null");
}
});
it("maps multi-file apply_patch with update + delete into edit detail referencing the deleted file", () => {
// Exact data shape from real Codex rollout: update one file, delete another
const patch = [
"*** Begin Patch",
"*** Update File: /tmp/repo/src/app/index.tsx",
"@@",
' import { useEffect } from "react";',
'-import { WELCOME_ROUTE } from "@/app-support/index-startup";',
"+",
'+const WELCOME_ROUTE = "/welcome";',
"*** Delete File: /tmp/repo/src/app-support/index-startup.ts",
"*** End Patch",
].join("\n");
const item = expectMapped(
mapCodexRolloutToolCall({
callId: "codex-delete-multi",
name: "apply_patch",
input: patch,
output: JSON.stringify({
output:
"Success. Updated the following files:\nM /tmp/repo/src/app/index.tsx\nD /tmp/repo/src/app-support/index-startup.ts\n",
metadata: { exit_code: 0, duration_seconds: 0.0 },
}),
cwd: "/tmp/repo",
}),
);
expect(item.detail.type).toBe("edit");
if (item.detail.type === "edit") {
// The unified diff should contain both file sections
const diff = item.detail.unifiedDiff ?? "";
// The update file section should have normal diff lines
expect(diff).toContain("-import");
expect(diff).toContain("+const WELCOME_ROUTE");
// The delete file section should reference /dev/null
expect(diff).toContain("/dev/null");
}
});
it("maps fileChange delete with content as removed lines, not added lines", () => {
const item = mapCodexToolCallFromThreadItem(
{
type: "fileChange",
id: "codex-file-delete-with-content",
status: "completed",
changes: [
{
path: "/tmp/repo/src/dead-module.ts",
kind: "delete",
content: 'export const FOO = "bar";\nexport function hello() {}\n',
},
],
},
{ cwd: "/tmp/repo" },
);
expect(item).toBeTruthy();
expect(item?.detail.type).toBe("edit");
if (item?.detail.type === "edit") {
expect(item.detail.filePath).toBe("src/dead-module.ts");
const diff = item.detail.unifiedDiff ?? "";
// For a deletion, the content should appear as REMOVED lines (-)
// not as ADDED lines (+). This is the core bug.
expect(diff).toContain("/dev/null");
expect(diff).toContain("-export const FOO");
expect(diff).toContain("-export function hello");
// The content must NOT appear as added lines
expect(diff).not.toContain("+export const FOO");
expect(diff).not.toContain("+export function hello");
}
});
it("maps fileChange delete without content to edit detail with /dev/null marker", () => {
const item = mapCodexToolCallFromThreadItem(
{
type: "fileChange",
id: "codex-file-delete-no-content",
status: "completed",
changes: [
{
path: "/tmp/repo/src/dead-module.ts",
kind: "delete",
},
],
},
{ cwd: "/tmp/repo" },
);
expect(item).toBeTruthy();
// A delete without content should still produce a meaningful detail
if (item?.detail.type === "edit") {
expect(item.detail.filePath).toBe("src/dead-module.ts");
const diff = item.detail.unifiedDiff ?? "";
expect(diff).toContain("/dev/null");
}
});
});
@@ -347,7 +347,7 @@ function normalizeDiffHeaderPath(rawPath: string): string {
function codexApplyPatchToUnifiedDiff(text: string): string {
const lines = text.replace(/\r\n/g, "\n").split("\n");
const output: string[] = [];
let sawDiffContent = false;
let sawDiffBody = false;
for (const line of lines) {
const directive = parseCodexApplyPatchDirective(line);
@@ -362,7 +362,6 @@ function codexApplyPatchToUnifiedDiff(text: string): string {
output.push(`diff --git a/${path} b/${path}`);
output.push(`--- ${left}`);
output.push(`+++ ${right}`);
sawDiffContent = true;
}
continue;
}
@@ -379,22 +378,22 @@ function codexApplyPatchToUnifiedDiff(text: string): string {
if (line.startsWith("@@")) {
output.push(line);
sawDiffContent = true;
sawDiffBody = true;
continue;
}
if (line.startsWith("+") || line.startsWith("-") || line.startsWith(" ")) {
output.push(line);
sawDiffContent = true;
sawDiffBody = true;
continue;
}
if (line.startsWith("\\ No newline at end of file")) {
output.push(line);
sawDiffContent = true;
sawDiffBody = true;
continue;
}
}
if (!sawDiffContent) {
if (!sawDiffBody) {
return text;
}
@@ -402,22 +401,6 @@ function codexApplyPatchToUnifiedDiff(text: string): string {
return normalized.length > 0 ? normalized : text;
}
function contentToDeletionDiff(filePath: string, content: string): string {
const lines = content.replace(/\r\n/g, "\n").split("\n");
const output: string[] = [];
output.push(`diff --git a/${filePath} b/${filePath}`);
output.push(`--- a/${filePath}`);
output.push(`+++ /dev/null`);
const nonEmpty = lines.filter((l) => l.length > 0 || lines.indexOf(l) < lines.length - 1);
if (nonEmpty.length > 0) {
output.push(`@@ -1,${nonEmpty.length} +0,0 @@`);
for (const line of nonEmpty) {
output.push(`-${line}`);
}
}
return output.join("\n");
}
function classifyDiffLikeText(
text: string,
): { isDiff: true; text: string } | { isDiff: false; text: string } {
@@ -733,26 +716,6 @@ function parseFileChangeEntries(
.filter((entry): entry is CodexFileChangeEntry => entry !== null);
}
function resolveFileChangeTextFields(
file: CodexFileChangeEntry | undefined,
): { unifiedDiff?: string; newString?: string } {
if (!file) {
return {};
}
const isDelete = file.kind === "delete";
if (isDelete && file.diff) {
const classified = classifyDiffLikeText(file.diff);
if (classified.isDiff) {
return { unifiedDiff: truncateDiffText(classified.text) };
}
return { unifiedDiff: truncateDiffText(contentToDeletionDiff(file.path, file.diff)) };
}
if (isDelete && !file.diff) {
return { unifiedDiff: contentToDeletionDiff(file.path, "") };
}
return asEditTextFields(file.diff);
}
function mapFileChangeItem(
item: z.infer<typeof CodexFileChangeItemSchema>,
options?: CodexMapperOptions,
@@ -776,9 +739,7 @@ function mapFileChangeItem(
files: files.map((file) => ({
path: file.path,
...(file.kind !== undefined ? { kind: file.kind } : {}),
...(file.kind === "delete"
? { patch: resolveFileChangeTextFields(file).unifiedDiff }
: asEditFileOutputFields(file.diff)),
...asEditFileOutputFields(file.diff),
})),
}
: {}),
@@ -788,7 +749,7 @@ function mapFileChangeItem(
const error = item.error ?? null;
const status = resolveStatus(item.status, error, output);
const firstFile = files[0];
const firstTextFields = resolveFileChangeTextFields(firstFile);
const firstTextFields = asEditTextFields(firstFile?.diff);
const hasFirstTextFields = Object.keys(firstTextFields).length > 0;
const input = toNullableObject({
...inputBase,
@@ -597,42 +597,6 @@ function stringifyStructuredAssistantMessage(value: unknown): string | null {
}
}
function readOpenCodeRecord(value: unknown): Record<string, unknown> | null {
return typeof value === "object" && value !== null && !Array.isArray(value)
? (value as Record<string, unknown>)
: null;
}
function readNonEmptyString(value: unknown): string | null {
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
}
function normalizeQuestionOptions(
value: unknown,
): Array<{ label: string; description?: string }> | null {
if (!Array.isArray(value)) {
return null;
}
const options: Array<{ label: string; description?: string }> = [];
for (const item of value) {
if (typeof item === "string" && item.trim().length > 0) {
options.push({ label: item.trim() });
continue;
}
const record = readOpenCodeRecord(item);
const label = readNonEmptyString(record?.label);
if (!label) {
continue;
}
const description = readNonEmptyString(record?.description);
options.push(description ? { label, description } : { label });
}
return options;
}
export function translateOpenCodeEvent(
event: unknown,
state: OpenCodeEventTranslationState,
@@ -821,60 +785,6 @@ export function translateOpenCodeEvent(
break;
}
case "question.asked": {
const sessionId = props.sessionID as string | undefined;
if (sessionId !== state.sessionId) {
break;
}
const requestId = props.id as string | undefined;
const rawQuestions = Array.isArray(props.questions) ? props.questions : [];
if (!requestId || rawQuestions.length === 0) {
break;
}
const questions = rawQuestions.flatMap((item) => {
const questionRecord = readOpenCodeRecord(item);
const question = readNonEmptyString(questionRecord?.question);
const header = readNonEmptyString(questionRecord?.header);
if (!question || !header) {
return [];
}
const options = normalizeQuestionOptions(questionRecord?.options) ?? [];
return [
{
question,
header,
options,
...(questionRecord?.multiple === true ? { multiSelect: true } : {}),
},
];
});
if (questions.length === 0) {
break;
}
events.push({
type: "permission_requested",
provider: "opencode",
request: {
id: requestId,
provider: "opencode",
name: "question",
kind: "question",
title: "Question",
input: { questions },
metadata: {
source: "opencode_question",
...(readOpenCodeRecord(props.tool) ?? {}),
},
},
});
break;
}
case "session.idle": {
const sessionId = props.sessionID as string | undefined;
if (sessionId === state.sessionId) {
@@ -1286,38 +1196,6 @@ class OpenCodeAgentSession implements AgentSession {
throw new Error(`No pending permission request with id '${requestId}'`);
}
if (pending.kind === "question") {
if (response.behavior === "deny") {
await this.client.question.reject({
requestID: requestId,
directory: this.config.cwd,
});
} else {
const answersRecord = readOpenCodeRecord(response.updatedInput?.answers);
const questions = Array.isArray(pending.input?.questions) ? pending.input.questions : [];
const answers = questions.map((item) => {
const header = readNonEmptyString(readOpenCodeRecord(item)?.header);
const rawAnswer = header ? readNonEmptyString(answersRecord?.[header]) : null;
if (!rawAnswer) {
return [];
}
return rawAnswer
.split(",")
.map((entry) => entry.trim())
.filter((entry) => entry.length > 0);
});
await this.client.question.reply({
requestID: requestId,
directory: this.config.cwd,
answers,
});
}
this.pendingPermissions.delete(requestId);
return;
}
const reply = response.behavior === "allow" ? "once" : "reject";
await this.client.permission.reply({
requestID: requestId,
@@ -1,138 +0,0 @@
import { describe, test, expect } from "vitest";
import { existsSync, mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import path from "node:path";
import pino from "pino";
import { OpenCodeAgentClient } from "../agent/providers/opencode-agent.js";
import { createTestPaseoDaemon } from "../test-utils/paseo-daemon.js";
import { DaemonClient } from "../test-utils/daemon-client.js";
import { isProviderAvailable } from "./agent-configs.js";
function tmpCwd(): string {
return mkdtempSync(path.join(tmpdir(), "daemon-real-opencode-"));
}
function pickOpenCodeModel(
models: Array<{ id: string }>,
preferences: string[] = ["gpt-5-nano", "gpt-4.1-nano", "mini", "free"],
): string {
const preferred = models.find((model) => preferences.some((fragment) => model.id.includes(fragment)));
return preferred?.id ?? models[0]!.id;
}
async function createHarness(): Promise<{
client: DaemonClient;
daemon: Awaited<ReturnType<typeof createTestPaseoDaemon>>;
}> {
const logger = pino({ level: "silent" });
const daemon = await createTestPaseoDaemon({
agentClients: { opencode: new OpenCodeAgentClient(logger) },
logger,
});
const client = new DaemonClient({ url: `ws://127.0.0.1:${daemon.port}/ws` });
await client.connect();
await client.fetchAgents({ subscribe: { subscriptionId: "opencode-real" } });
return { client, daemon };
}
describe("daemon E2E (real opencode) - plan mode and clarifying questions", () => {
test.runIf(isProviderAvailable("opencode"))(
"surfaces clarifying questions as pending permissions",
async () => {
const cwd = tmpCwd();
const { client, daemon } = await createHarness();
try {
const modelList = await client.listProviderModels("opencode");
expect(modelList.models.length).toBeGreaterThan(0);
const agent = await client.createAgent({
provider: "opencode",
cwd,
title: "OpenCode question regression",
model: pickOpenCodeModel(modelList.models, ["minimax-m2.5-free", "minimax", "free"]),
modeId: "plan",
});
await client.sendMessage(
agent.id,
[
"Use the question tool/feature to ask me exactly one clarifying question.",
"Ask this exact question: What kind of project should the plan cover?",
"Wait for my answer before doing anything else.",
].join(" "),
);
const snapshotWithQuestion = await client.waitForAgentUpsert(
agent.id,
(snapshot) => (snapshot.pendingPermissions?.[0]?.kind ?? null) === "question",
30_000,
);
expect(snapshotWithQuestion.pendingPermissions?.length).toBeGreaterThan(0);
const permission = snapshotWithQuestion.pendingPermissions?.[0];
expect(permission).toBeTruthy();
expect(permission?.kind).toBe("question");
expect(Array.isArray(permission?.input?.questions)).toBe(true);
const firstQuestion = permission?.input?.questions?.[0] as { header?: string } | undefined;
expect(firstQuestion?.header).toBeTruthy();
} finally {
await client.close().catch(() => undefined);
await daemon.close();
rmSync(cwd, { recursive: true, force: true });
}
},
180_000,
);
test.runIf(isProviderAvailable("opencode"))(
"plan mode stays read-only through the daemon path",
async () => {
const cwd = tmpCwd();
const filePath = path.join(cwd, "plan-mode-output.txt");
const { client, daemon } = await createHarness();
try {
const modelList = await client.listProviderModels("opencode");
expect(modelList.models.length).toBeGreaterThan(0);
const agent = await client.createAgent({
provider: "opencode",
cwd,
title: "OpenCode plan mode regression",
model: pickOpenCodeModel(modelList.models),
modeId: "plan",
});
await client.sendMessage(
agent.id,
"Create a file named plan-mode-output.txt in the current directory containing exactly hello.",
);
const state = await client.waitForFinish(agent.id, 180_000);
expect(state.status).toBe("idle");
expect(existsSync(filePath)).toBe(false);
const timeline = await client.fetchAgentTimeline(agent.id, {
direction: "tail",
limit: 0,
projection: "projected",
});
const assistantText = timeline.entries
.filter((entry) => entry.item.type === "assistant_message")
.map((entry) => entry.item.text)
.join(" ");
expect(assistantText.toLowerCase()).toContain("plan");
} finally {
await client.close().catch(() => undefined);
await daemon.close();
rmSync(cwd, { recursive: true, force: true });
}
},
240_000,
);
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@getpaseo/website",
"version": "0.1.35",
"version": "0.1.34",
"private": true,
"type": "module",
"scripts": {
Binary file not shown.

Before

Width:  |  Height:  |  Size: 862 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 592 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

@@ -1,122 +0,0 @@
import * as React from "react";
import { motion, AnimatePresence } from "framer-motion";
interface CommandDialogProps {
trigger: React.ReactNode;
title: string;
description?: string;
command: string;
footnote?: React.ReactNode;
}
export function CommandDialog({
trigger,
title,
description,
command,
footnote,
}: CommandDialogProps) {
const [open, setOpen] = React.useState(false);
const ref = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
function handleClickOutside(e: MouseEvent) {
if (ref.current && !ref.current.contains(e.target as Node)) {
setOpen(false);
}
}
function handleKeyDown(e: KeyboardEvent) {
if (e.key === "Escape") setOpen(false);
}
document.addEventListener("mousedown", handleClickOutside);
document.addEventListener("keydown", handleKeyDown);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
document.removeEventListener("keydown", handleKeyDown);
};
}, []);
return (
<div className="relative" ref={ref}>
<button onClick={() => setOpen(!open)}>{trigger}</button>
<AnimatePresence>
{open && (
<>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="fixed inset-0 z-40 bg-black/60 backdrop-blur-sm"
onClick={() => setOpen(false)}
/>
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ duration: 0.2, ease: "easeOut" }}
className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-50 w-full max-w-md rounded-xl border border-white/20 bg-background p-6 space-y-4"
>
<div className="space-y-2">
<p className="text-base font-medium text-white">{title}</p>
{description && (
<p className="text-sm text-muted-foreground">{description}</p>
)}
</div>
<CodeBlock>{command}</CodeBlock>
{footnote && (
<p className="text-xs text-white/30">{footnote}</p>
)}
</motion.div>
</>
)}
</AnimatePresence>
</div>
);
}
function CodeBlock({ children }: { children: string }) {
const [copied, setCopied] = React.useState(false);
function handleCopy() {
navigator.clipboard.writeText(children);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
return (
<div className="bg-black/30 backdrop-blur-sm rounded-lg p-3 md:p-4 font-mono text-sm flex items-center justify-between gap-2">
<div>
<span className="text-muted-foreground select-none">$ </span>
<span className="text-foreground">{children}</span>
</div>
<button
onClick={handleCopy}
className="text-muted-foreground hover:text-foreground transition-colors p-1"
title="Copy to clipboard"
>
{copied ? (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
viewBox="0 0 256 256"
>
<path d="M229.66,77.66l-128,128a8,8,0,0,1-11.32,0l-56-56a8,8,0,0,1,11.32-11.32L96,188.69,218.34,66.34a8,8,0,0,1,11.32,11.32Z" />
</svg>
) : (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
viewBox="0 0 256 256"
>
<path d="M216,28H88A20,20,0,0,0,68,48V76H40A20,20,0,0,0,20,96V216a20,20,0,0,0,20,20H168a20,20,0,0,0,20-20V188h28a20,20,0,0,0,20-20V48A20,20,0,0,0,216,28ZM164,212H44V100H164Zm48-48H188V96a20,20,0,0,0-20-20H92V52H212Z" />
</svg>
)}
</button>
</div>
);
}
+347 -105
View File
@@ -1,20 +1,73 @@
import * as React from "react";
import { motion, AnimatePresence } from "framer-motion";
import { CursorFieldProvider } from "~/components/butterfly";
import { CommandDialog } from "~/components/command-dialog";
import {
appStoreUrl,
playStoreUrl,
webAppUrl,
downloadOptions,
useDetectedPlatform,
AppleIcon,
AndroidIcon,
TerminalIcon,
GlobeIcon,
} from "~/downloads";
import websitePackage from "../../package.json";
import "~/styles.css";
const desktopVersion = websitePackage.version;
const releaseBase = `https://github.com/getpaseo/paseo/releases/download/v${desktopVersion}`;
type Platform = "mac-silicon" | "mac-intel" | "windows" | "linux";
interface DownloadOption {
platform: Platform;
label: string;
href: string;
icon: (props: React.SVGProps<SVGSVGElement>) => React.ReactElement;
}
const downloadOptions: DownloadOption[] = [
{
platform: "mac-silicon",
label: "Mac",
href: `${releaseBase}/Paseo-${desktopVersion}-arm64.dmg`,
icon: AppleIcon,
},
{
platform: "mac-intel",
label: "Mac Intel",
href: `${releaseBase}/Paseo-${desktopVersion}.dmg`,
icon: AppleIcon,
},
{
platform: "windows",
label: "Windows",
href: `${releaseBase}/Paseo-Setup-${desktopVersion}.exe`,
icon: WindowsIcon,
},
{
platform: "linux",
label: "Linux",
href: `${releaseBase}/Paseo-${desktopVersion}.AppImage`,
icon: LinuxIcon,
},
];
function useDetectedPlatform(): Platform {
const [platform, setPlatform] = React.useState<Platform>("mac-silicon");
React.useEffect(() => {
const ua = navigator.userAgent.toLowerCase();
if (ua.includes("win")) {
setPlatform("windows");
} else if (ua.includes("linux")) {
setPlatform("linux");
} else if (ua.includes("mac")) {
// Check for Apple Silicon vs Intel
// navigator.platform is deprecated but still the most reliable check
const isARM =
/arm|aarch64/i.test(navigator.platform) ||
// Chrome/Edge on Apple Silicon report x86 platform but have ARM in userAgentData
(navigator as unknown as { userAgentData?: { architecture?: string } }).userAgentData
?.architecture === "arm";
setPlatform(isARM ? "mac-silicon" : "mac-silicon"); // Default to Silicon for modern Macs
}
}, []);
return platform;
}
interface LandingPageProps {
title: React.ReactNode;
subtitle: string;
@@ -43,7 +96,7 @@ export function LandingPage({ title, subtitle }: LandingPageProps) {
>
<div className="max-w-6xl lg:max-w-7xl xl:max-w-[90rem] mx-auto">
<img
src="/hero-mockup.png"
src="/paseo-mockup.png"
alt="Paseo app showing agent management interface"
className="w-full rounded-lg shadow-2xl"
/>
@@ -56,7 +109,6 @@ export function LandingPage({ title, subtitle }: LandingPageProps) {
<main className="p-6 md:p-20 md:pt-8 max-w-5xl mx-auto">
<div className="space-y-24">
<Features />
<MobileSection />
<CLISection />
<FAQ />
<SponsorCTA />
@@ -141,7 +193,7 @@ export function LandingPage({ title, subtitle }: LandingPageProps) {
<p className="text-white/60 font-medium">Download</p>
<div className="space-y-2">
<a
href={appStoreUrl}
href="https://apps.apple.com/app/paseo-pocket-engineer/id6758887924"
target="_blank"
rel="noopener noreferrer"
className="block text-white/40 hover:text-white/60 transition-colors"
@@ -149,7 +201,7 @@ export function LandingPage({ title, subtitle }: LandingPageProps) {
App Store
</a>
<a
href={playStoreUrl}
href="https://play.google.com/store/apps/details?id=sh.paseo"
target="_blank"
rel="noopener noreferrer"
className="block text-white/40 hover:text-white/60 transition-colors"
@@ -165,7 +217,7 @@ export function LandingPage({ title, subtitle }: LandingPageProps) {
Desktop
</a>
<a
href={webAppUrl}
href="https://app.paseo.sh"
target="_blank"
rel="noopener noreferrer"
className="block text-white/40 hover:text-white/60 transition-colors"
@@ -203,12 +255,6 @@ function Nav() {
>
Changelog
</a>
<a
href="/download"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
Download
</a>
<a
href="https://discord.gg/jz8T2uahpH"
target="_blank"
@@ -407,7 +453,7 @@ function Features() {
<span className="text-white/40">{feature.icon}</span>
<p className="font-medium text-lg">{feature.title}</p>
</div>
<p className="text-sm text-muted-foreground leading-relaxed">{feature.description}</p>
<p className="text-sm text-white/60 leading-relaxed">{feature.description}</p>
</motion.div>
))}
</div>
@@ -441,7 +487,7 @@ function GetStarted() {
<div className="flex flex-row flex-wrap gap-3">
<DownloadButton />
<a
href={webAppUrl}
href="https://app.paseo.sh"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center gap-2 rounded-lg border border-white/20 px-4 py-2 text-sm font-medium text-white hover:bg-white/10 transition-colors"
@@ -450,7 +496,7 @@ function GetStarted() {
Web App
</a>
<a
href={appStoreUrl}
href="https://apps.apple.com/app/paseo-pocket-engineer/id6758887924"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center rounded-lg border border-white/20 px-3 py-2 text-white hover:bg-white/10 transition-colors"
@@ -459,7 +505,7 @@ function GetStarted() {
<AppleIcon className="h-5 w-5" />
</a>
<a
href={playStoreUrl}
href="https://play.google.com/store/apps/details?id=sh.paseo"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center rounded-lg border border-white/20 px-3 py-2 text-white hover:bg-white/10 transition-colors"
@@ -469,15 +515,7 @@ function GetStarted() {
</a>
<ServerInstallButton />
</div>
<div className="pt-3">
<a
href="/download"
className="text-xs text-white/40 hover:text-white/70 transition-colors"
>
All download options
</a>
</div>
<div className="flex items-center gap-2 pt-6">
<div className="flex items-center gap-2 pt-8">
<span className="text-xs text-white/40">Supports</span>
<div className="flex items-center gap-1">
<AgentBadge name="Claude Code" icon={<ClaudeCodeIcon className="h-6 w-6" />} />
@@ -491,40 +529,133 @@ function GetStarted() {
function DownloadButton() {
const detectedPlatform = useDetectedPlatform();
const [open, setOpen] = React.useState(false);
const ref = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
function handleClickOutside(e: MouseEvent) {
if (ref.current && !ref.current.contains(e.target as Node)) {
setOpen(false);
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
const primary = downloadOptions.find((o) => o.platform === detectedPlatform)!;
const secondaryOptions = downloadOptions.filter((o) => o.platform !== detectedPlatform);
const PrimaryIcon = primary.icon;
return (
<a
href={primary.href}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 rounded-lg bg-foreground px-4 py-2 text-sm font-medium text-background hover:bg-foreground/90 transition-colors"
>
<PrimaryIcon className="h-4 w-4" />
Download for {primary.label}
</a>
<div className="relative" ref={ref}>
<div className="flex items-stretch">
<a
href={primary.href}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 rounded-l-lg bg-foreground px-4 py-2 text-sm font-medium text-background hover:bg-foreground/90 transition-colors"
>
<PrimaryIcon className="h-4 w-4" />
Download for {primary.label}
</a>
<button
onClick={() => setOpen(!open)}
className="inline-flex items-center justify-center rounded-r-lg border-l border-background/20 bg-foreground px-2 py-2 text-background hover:bg-foreground/90 transition-colors"
aria-label="More download options"
>
<ChevronDownIcon className="h-4 w-4" />
</button>
</div>
<AnimatePresence>
{open && (
<motion.div
initial={{ opacity: 0, y: -4 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -4 }}
transition={{ duration: 0.15, ease: "easeOut" }}
className="absolute left-0 top-full mt-1 z-50 min-w-[200px] rounded-lg border border-white/20 bg-black/90 backdrop-blur-sm py-1"
>
{secondaryOptions.map((option) => {
const Icon = option.icon;
return (
<a
key={option.platform}
href={option.href}
target="_blank"
rel="noopener noreferrer"
onClick={() => setOpen(false)}
className="flex items-center gap-2 px-4 py-2 text-sm text-white/80 hover:bg-white/10 transition-colors"
>
<Icon className="h-4 w-4" />
{option.label}
</a>
);
})}
</motion.div>
)}
</AnimatePresence>
</div>
);
}
function ServerInstallButton() {
const [open, setOpen] = React.useState(false);
const ref = React.useRef<HTMLDivElement>(null);
React.useEffect(() => {
function handleClickOutside(e: MouseEvent) {
if (ref.current && !ref.current.contains(e.target as Node)) {
setOpen(false);
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
return (
<CommandDialog
trigger={
<span className="inline-flex items-center justify-center rounded-lg border border-white/20 px-3 py-2 text-white hover:bg-white/10 transition-colors">
<TerminalIcon className="h-5 w-5" />
</span>
}
title="Run agents on a remote machine"
description="For headless machines you want to connect to from the Paseo apps. The desktop app already includes a built-in daemon."
command="npm install -g @getpaseo/cli && paseo"
footnote={
<>
Requires Node.js 18+. Run <span className="font-mono text-white/40">paseo</span> to
start the daemon.
</>
}
/>
<div className="relative" ref={ref}>
<button
onClick={() => setOpen(!open)}
className="inline-flex items-center justify-center rounded-lg border border-white/20 px-3 py-2 text-white hover:bg-white/10 transition-colors"
aria-label="Install on a server"
>
<TerminalIcon className="h-5 w-5" />
</button>
<AnimatePresence>
{open && (
<>
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.2 }}
className="fixed inset-0 z-40 bg-black/60 backdrop-blur-sm"
onClick={() => setOpen(false)}
/>
<motion.div
initial={{ opacity: 0, scale: 0.95 }}
animate={{ opacity: 1, scale: 1 }}
exit={{ opacity: 0, scale: 0.95 }}
transition={{ duration: 0.2, ease: "easeOut" }}
className="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 z-50 w-full max-w-md rounded-xl border border-white/20 bg-background p-6 space-y-4"
>
<div className="space-y-2">
<p className="text-base font-medium text-white">Run agents on a remote machine</p>
<p className="text-sm text-white/60">
For headless machines you want to connect to from the Paseo apps. The desktop app
already includes a built-in daemon.
</p>
</div>
<CodeBlock>npm install -g @getpaseo/cli && paseo</CodeBlock>
<p className="text-xs text-white/30">
Requires Node.js 18+. Run <span className="font-mono text-white/40">paseo</span> to
start the daemon.
</p>
</motion.div>
</>
)}
</AnimatePresence>
</div>
);
}
@@ -577,6 +708,34 @@ function OpenCodeIcon(props: React.SVGProps<SVGSVGElement>) {
);
}
function AppleIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
{...props}
>
<path d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701" />
</svg>
);
}
function AndroidIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
fill="currentColor"
aria-hidden="true"
{...props}
>
<path d="M380.91,199l42.47-73.57a8.63,8.63,0,0,0-3.12-11.76,8.52,8.52,0,0,0-11.71,3.12l-43,74.52c-32.83-15-69.78-23.35-109.52-23.35s-76.69,8.36-109.52,23.35l-43-74.52a8.6,8.6,0,1,0-14.88,8.64L131,199C57.8,238.64,8.19,312.77,0,399.55H512C503.81,312.77,454.2,238.64,380.91,199ZM138.45,327.65a21.46,21.46,0,1,1,21.46-21.46A21.47,21.47,0,0,1,138.45,327.65Zm235,0A21.46,21.46,0,1,1,395,306.19,21.47,21.47,0,0,1,373.49,327.65Z" />
</svg>
);
}
function AppStoreIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
@@ -603,6 +762,57 @@ function AppStoreIcon(props: React.SVGProps<SVGSVGElement>) {
);
}
function WindowsIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
{...props}
>
<path d="M0,0H11.377V11.372H0ZM12.623,0H24V11.372H12.623ZM0,12.623H11.377V24H0Zm12.623,0H24V24H12.623" />
</svg>
);
}
function LinuxIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
aria-hidden="true"
{...props}
>
<path d="M8.996 4.497c.104-.076.1-.168.186-.158s.022.102-.098.207c-.12.104-.308.243-.46.323-.291.152-.631.336-.993.336s-.647-.167-.853-.33c-.102-.082-.186-.162-.248-.221-.11-.086-.096-.207-.052-.204.075.01.087.109.134.153.064.06.144.137.241.214.195.154.454.304.778.304s.702-.19.932-.32c.13-.073.297-.204.433-.304M7.34 3.781c.055-.02.123-.031.174-.003.011.006.024.021.02.034-.012.038-.074.032-.11.05-.032.017-.057.052-.093.054-.034 0-.086-.012-.09-.046-.007-.044.058-.072.1-.089m.581-.003c.05-.028.119-.018.173.003.041.017.106.045.1.09-.004.033-.057.046-.09.045-.036-.002-.062-.037-.093-.053-.036-.019-.098-.013-.11-.051-.004-.013.008-.028.02-.034" />
<path
fillRule="evenodd"
d="M8.446.019c2.521.003 2.38 2.66 2.364 4.093-.01.939.509 1.574 1.04 2.244.474.56 1.095 1.38 1.45 2.32.29.765.402 1.613.115 2.465a.8.8 0 0 1 .254.152l.001.002c.207.175.271.447.329.698.058.252.112.488.224.615.344.382.494.667.48.922-.015.254-.203.43-.435.57-.465.28-1.164.491-1.586 1.002-.443.527-.99.83-1.505.871a1.25 1.25 0 0 1-1.256-.716v-.001a1 1 0 0 1-.078-.21c-.67.038-1.252-.165-1.718-.128-.687.038-1.116.204-1.506.206-.151.331-.445.547-.808.63-.5.114-1.126 0-1.743-.324-.577-.306-1.31-.278-1.85-.39-.27-.057-.51-.157-.626-.384-.116-.226-.095-.538.07-.988.051-.16.012-.398-.026-.648a2.5 2.5 0 0 1-.037-.369c0-.133.022-.265.087-.386v-.002c.14-.266.368-.377.577-.451s.397-.125.53-.258c.143-.15.27-.374.443-.56q.036-.037.073-.07c-.081-.538.007-1.105.192-1.662.393-1.18 1.223-2.314 1.811-3.014.502-.713.65-1.287.701-2.016.042-.997-.705-3.974 2.112-4.2q.168-.015.321-.013m2.596 10.866-.03.016c-.223.121-.348.337-.427.656-.08.32-.107.733-.13 1.206v.001c-.023.37-.192.824-.31 1.267s-.176.862-.036 1.128v.002c.226.452.608.636 1.051.601s.947-.304 1.36-.795c.474-.576 1.218-.796 1.638-1.05.21-.126.324-.242.333-.4.009-.157-.097-.403-.425-.767-.17-.192-.217-.462-.274-.71-.056-.247-.122-.468-.26-.585l-.001-.001c-.18-.157-.356-.17-.565-.164q-.069.001-.14.005c-.239.275-.805.612-1.197.508-.359-.09-.562-.508-.587-.918m-7.204.03H3.83c-.189.002-.314.09-.44.225-.149.158-.276.382-.445.56v.002h-.002c-.183.184-.414.239-.61.31-.195.069-.353.143-.46.35v.002c-.085.155-.066.378-.029.624.038.245.096.507.018.746v.002l-.001.002c-.157.427-.155.678-.082.822.074.143.235.22.48.272.493.103 1.26.069 1.906.41.583.305 1.168.404 1.598.305.431-.098.712-.369.75-.867v-.002c.029-.292-.195-.673-.485-1.052-.29-.38-.633-.752-.795-1.09v-.002l-.61-1.11c-.21-.286-.43-.462-.68-.5a1 1 0 0 0-.106-.008M9.584 4.85c-.14.2-.386.37-.695.467-.147.048-.302.17-.495.28a1.3 1.3 0 0 1-.74.19.97.97 0 0 1-.582-.227c-.14-.113-.25-.237-.394-.322a3 3 0 0 1-.192-.126c-.063 1.179-.85 2.658-1.226 3.511a5.4 5.4 0 0 0-.43 1.917c-.68-.906-.184-2.066.081-2.568.297-.55.343-.701.27-.649-.266.436-.685 1.13-.848 1.844-.085.372-.1.749.01 1.097.11.349.345.67.766.931.573.351.963.703 1.193 1.015s.302.584.23.777a.4.4 0 0 1-.212.22.7.7 0 0 1-.307.056l.184.235c.094.124.186.249.266.375 1.179.805 2.567.496 3.568-.218.1-.342.197-.664.212-.903.024-.474.05-.896.136-1.245s.244-.634.53-.791a1 1 0 0 1 .138-.061q.005-.045.013-.087c.082-.546.569-.572 1.18-.303.588.266.81.499.71.814h.13c.122-.398-.133-.69-.822-1.025l-.137-.06a2.35 2.35 0 0 0-.012-1.113c-.188-.79-.704-1.49-1.098-1.838-.072-.003-.065.06.081.203.363.333 1.156 1.532.727 2.644a1.2 1.2 0 0 0-.342-.043c-.164-.907-.543-1.66-.735-2.014-.359-.668-.918-2.036-1.158-2.983M7.72 3.503a1 1 0 0 0-.312.053c-.268.093-.447.286-.559.391-.022.021-.05.04-.119.091s-.172.126-.321.238q-.198.151-.13.38c.046.15.192.325.459.476.166.098.28.23.41.334a1 1 0 0 0 .215.133.9.9 0 0 0 .298.066c.282.017.49-.068.673-.173s.34-.233.518-.29c.365-.115.627-.345.709-.564a.37.37 0 0 0-.01-.309c-.048-.096-.148-.187-.318-.257h-.001c-.354-.151-.507-.162-.705-.29-.321-.207-.587-.28-.807-.279m-.89-1.122h-.025a.4.4 0 0 0-.278.135.76.76 0 0 0-.191.334 1.2 1.2 0 0 0-.051.445v.001c.01.162.041.299.102.436.05.116.109.204.183.274l.089-.065.117-.09-.023-.018a.4.4 0 0 1-.11-.161.7.7 0 0 1-.054-.22v-.01a.7.7 0 0 1 .014-.234.4.4 0 0 1 .08-.179q.056-.069.126-.073h.013a.18.18 0 0 1 .123.05c.045.04.08.09.11.162a.7.7 0 0 1 .054.22v.01a.7.7 0 0 1-.002.17 1.1 1.1 0 0 1 .317-.143 1.3 1.3 0 0 0 .002-.194V3.23a1.2 1.2 0 0 0-.102-.437.8.8 0 0 0-.227-.31.4.4 0 0 0-.268-.102m1.95-.155a.63.63 0 0 0-.394.14.9.9 0 0 0-.287.376 1.2 1.2 0 0 0-.1.51v.015q0 .079.01.152c.114.027.278.074.406.138a1 1 0 0 1-.011-.172.8.8 0 0 1 .058-.278.5.5 0 0 1 .139-.2.26.26 0 0 1 .182-.069.26.26 0 0 1 .178.081c.055.054.094.12.124.21.029.086.042.17.04.27l-.002.012a.8.8 0 0 1-.057.277c-.024.059-.089.106-.122.145.046.016.09.03.146.052a5 5 0 0 1 .248.102 1.2 1.2 0 0 0 .244-.763 1.2 1.2 0 0 0-.11-.495.9.9 0 0 0-.294-.37.64.64 0 0 0-.39-.133z"
/>
</svg>
);
}
function TerminalIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
{...props}
>
<polyline points="4 17 10 11 4 5" />
<line x1="12" x2="20" y1="19" y2="19" />
</svg>
);
}
function ChevronDownIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
@@ -621,6 +831,27 @@ function ChevronDownIcon(props: React.SVGProps<SVGSVGElement>) {
);
}
function GlobeIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
{...props}
>
<circle cx="12" cy="12" r="9" />
<path d="M3 12h18" />
<path d="M12 3a15 15 0 0 1 0 18" />
<path d="M12 3a15 15 0 0 0 0 18" />
</svg>
);
}
function Step({ number, children }: { number: number; children: React.ReactNode }) {
return (
<div className="flex gap-4">
@@ -632,6 +863,52 @@ function Step({ number, children }: { number: number; children: React.ReactNode
);
}
function CodeBlock({ children }: { children: React.ReactNode }) {
const [copied, setCopied] = React.useState(false);
const text = typeof children === "string" ? children : "";
function handleCopy() {
navigator.clipboard.writeText(text);
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
return (
<div className="bg-black/30 backdrop-blur-sm rounded-lg p-3 md:p-4 font-mono text-sm flex items-center justify-between gap-2">
<div>
<span className="text-muted-foreground select-none">$ </span>
<span className="text-foreground">{children}</span>
</div>
<button
onClick={handleCopy}
className="text-muted-foreground hover:text-foreground transition-colors p-1"
title="Copy to clipboard"
>
{copied ? (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
viewBox="0 0 256 256"
>
<path d="M229.66,77.66l-128,128a8,8,0,0,1-11.32,0l-56-56a8,8,0,0,1,11.32-11.32L96,188.69,218.34,66.34a8,8,0,0,1,11.32,11.32Z" />
</svg>
) : (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
fill="currentColor"
viewBox="0 0 256 256"
>
<path d="M216,28H88A20,20,0,0,0,68,48V76H40A20,20,0,0,0,20,96V216a20,20,0,0,0,20,20H168a20,20,0,0,0,20-20V188h28a20,20,0,0,0,20-20V48A20,20,0,0,0,216,28ZM164,212H44V100H164Zm48-48H188V96a20,20,0,0,0-20-20H92V52H212Z" />
</svg>
)}
</button>
</div>
);
}
const bashKeywords = new Set([
"while",
@@ -918,28 +1195,6 @@ done`,
},
];
function MobileSection() {
return (
<section className="space-y-6">
<div className="space-y-2">
<h2 className="text-2xl font-medium">Mobile-first</h2>
<p className="text-sm text-muted-foreground max-w-lg">
The mobile app has full feature parity with desktop. Launch agents, review diffs, talk through problems with voice, all from your phone.
</p>
</div>
<div className="-mx-[calc(50vw-50%)] px-6 md:px-8 overflow-x-auto">
<div className="max-w-5xl lg:max-w-6xl mx-auto">
<img
src="/mobile-mockup.png"
alt="Paseo mobile app screens"
className="min-w-[900px] w-full rounded-lg"
/>
</div>
</div>
</section>
);
}
function CLISection() {
const [activeIndex, setActiveIndex] = React.useState(0);
const active = cliExamples[activeIndex];
@@ -954,7 +1209,7 @@ function CLISection() {
>
<div className="space-y-2">
<h2 className="text-2xl font-medium">CLI</h2>
<p className="text-sm text-muted-foreground max-w-lg">
<p className="text-sm text-white/60 max-w-lg">
Everything you can do in the app, you can do from the terminal.
</p>
</div>
@@ -976,7 +1231,7 @@ function CLISection() {
</div>
<div className="space-y-3">
<p className="text-sm text-muted-foreground">{active.description}</p>
<p className="text-sm text-white/50">{active.description}</p>
<CLICodeBlock>{active.code}</CLICodeBlock>
</div>
@@ -1033,7 +1288,7 @@ function FAQ() {
</FAQItem>
<FAQItem question="Do I need the desktop app?">
No. You can run the daemon headless with{" "}
<code className="font-mono text-muted-foreground">npm install -g @getpaseo/cli && paseo</code> and
<code className="font-mono text-white/50">npm install -g @getpaseo/cli && paseo</code> and
use the CLI, web app, or mobile app to connect. The desktop app just bundles the daemon
with a UI.
</FAQItem>
@@ -1090,26 +1345,13 @@ function SponsorCTA() {
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-60px" }}
transition={{ duration: 0.5, ease: "easeOut" }}
className="rounded-xl bg-white/5 border border-white/10 p-8 md:p-10 text-left space-y-4 max-w-xl mx-auto"
className="rounded-xl bg-white/5 border border-white/10 p-8 md:p-10 text-center space-y-4"
>
<p className="text-lg font-medium">Paseo is an independent project</p>
<div className="text-sm text-muted-foreground leading-relaxed space-y-3">
<p>
I believe that open source and freedom of choice always win for developer tools.
</p>
<p>
Paseo has no VC funding and no big team behind it. I built it because the existing tools
weren't good enough for me. No tracking, no telemetry, no forced accounts, no vendor lock-in.
</p>
<p>
The monetization story is still taking shape. The obvious path is optional hosted
infrastructure like cloud sandboxes for teams. But Paseo itself will always be FOSS.
</p>
<p>
If you like Paseo, consider sponsoring development.
</p>
<p>- Mo</p>
</div>
<p className="text-sm text-white/50 max-w-md mx-auto leading-relaxed">
I built Paseo because the existing tools weren't good enough for me. There's no VC or big
team behind this. If it saves you time, sponsoring keeps development going.
</p>
<div className="pt-2">
<a
href="https://github.com/sponsors/boudra"
@@ -1142,7 +1384,7 @@ function FAQItem({ question, children }: { question: string; children: React.Rea
<span className="font-mono text-white/40 hidden group-open:inline">-</span>
{question}
</summary>
<div className="text-sm text-muted-foreground space-y-2 mt-2 ml-4">{children}</div>
<div className="text-sm text-white/60 space-y-2 mt-2 ml-4">{children}</div>
</details>
);
}
-174
View File
@@ -1,174 +0,0 @@
import * as React from "react";
import websitePackage from "../package.json";
export const desktopVersion = websitePackage.version;
export const releaseBase = `https://github.com/getpaseo/paseo/releases/download/v${desktopVersion}`;
export const macAppleSiliconDownloadUrl = `${releaseBase}/Paseo-${desktopVersion}-arm64.dmg`;
export const macIntelDownloadUrl = `${releaseBase}/Paseo-${desktopVersion}-x64.dmg`;
export const linuxAppImageDownloadUrl = `${releaseBase}/Paseo-${desktopVersion}-x86_64.AppImage`;
export const linuxDebDownloadUrl = `${releaseBase}/Paseo-${desktopVersion}-amd64.deb`;
export const linuxRpmDownloadUrl = `${releaseBase}/Paseo-${desktopVersion}-x86_64.rpm`;
export const appStoreUrl = "https://apps.apple.com/app/paseo-pocket-engineer/id6758887924";
export const playStoreUrl = "https://play.google.com/store/apps/details?id=sh.paseo";
export const webAppUrl = "https://app.paseo.sh";
type Platform = "mac-silicon" | "mac-intel" | "windows" | "linux";
export interface DownloadOption {
platform: Platform;
label: string;
href: string;
icon: (props: React.SVGProps<SVGSVGElement>) => React.ReactElement;
}
export const downloadOptions: DownloadOption[] = [
{
platform: "mac-silicon",
label: "Mac",
href: macAppleSiliconDownloadUrl,
icon: AppleIcon,
},
{
platform: "mac-intel",
label: "Mac Intel",
href: macIntelDownloadUrl,
icon: AppleIcon,
},
{
platform: "windows",
label: "Windows",
href: `${releaseBase}/Paseo-Setup-${desktopVersion}.exe`,
icon: WindowsIcon,
},
{
platform: "linux",
label: "Linux",
href: linuxAppImageDownloadUrl,
icon: LinuxIcon,
},
];
export function useDetectedPlatform(): Platform {
const [platform, setPlatform] = React.useState<Platform>("mac-silicon");
React.useEffect(() => {
const ua = navigator.userAgent.toLowerCase();
if (ua.includes("win")) {
setPlatform("windows");
} else if (ua.includes("linux")) {
setPlatform("linux");
} else if (ua.includes("mac")) {
// Check for Apple Silicon vs Intel
// navigator.platform is deprecated but still the most reliable check
const isARM =
/arm|aarch64/i.test(navigator.platform) ||
// Chrome/Edge on Apple Silicon report x86 platform but have ARM in userAgentData
(navigator as unknown as { userAgentData?: { architecture?: string } }).userAgentData
?.architecture === "arm";
setPlatform(isARM ? "mac-silicon" : "mac-silicon"); // Default to Silicon for modern Macs
}
}, []);
return platform;
}
export function AppleIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
{...props}
>
<path d="M12.152 6.896c-.948 0-2.415-1.078-3.96-1.04-2.04.027-3.91 1.183-4.961 3.014-2.117 3.675-.546 9.103 1.519 12.09 1.013 1.454 2.208 3.09 3.792 3.039 1.52-.065 2.09-.987 3.935-.987 1.831 0 2.35.987 3.96.948 1.637-.026 2.676-1.48 3.676-2.948 1.156-1.688 1.636-3.325 1.662-3.415-.039-.013-3.182-1.221-3.22-4.857-.026-3.04 2.48-4.494 2.597-4.559-1.429-2.09-3.623-2.324-4.39-2.376-2-.156-3.675 1.09-4.61 1.09zM15.53 3.83c.843-1.012 1.4-2.427 1.245-3.83-1.207.052-2.662.805-3.532 1.818-.78.896-1.454 2.338-1.273 3.714 1.338.104 2.715-.688 3.559-1.701" />
</svg>
);
}
export function AndroidIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 512 512"
fill="currentColor"
aria-hidden="true"
{...props}
>
<path d="M380.91,199l42.47-73.57a8.63,8.63,0,0,0-3.12-11.76,8.52,8.52,0,0,0-11.71,3.12l-43,74.52c-32.83-15-69.78-23.35-109.52-23.35s-76.69,8.36-109.52,23.35l-43-74.52a8.6,8.6,0,1,0-14.88,8.64L131,199C57.8,238.64,8.19,312.77,0,399.55H512C503.81,312.77,454.2,238.64,380.91,199ZM138.45,327.65a21.46,21.46,0,1,1,21.46-21.46A21.47,21.47,0,0,1,138.45,327.65Zm235,0A21.46,21.46,0,1,1,395,306.19,21.47,21.47,0,0,1,373.49,327.65Z" />
</svg>
);
}
export function WindowsIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="currentColor"
aria-hidden="true"
{...props}
>
<path d="M0,0H11.377V11.372H0ZM12.623,0H24V11.372H12.623ZM0,12.623H11.377V24H0Zm12.623,0H24V24H12.623" />
</svg>
);
}
export function LinuxIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 16 16"
fill="currentColor"
aria-hidden="true"
{...props}
>
<path d="M8.996 4.497c.104-.076.1-.168.186-.158s.022.102-.098.207c-.12.104-.308.243-.46.323-.291.152-.631.336-.993.336s-.647-.167-.853-.33c-.102-.082-.186-.162-.248-.221-.11-.086-.096-.207-.052-.204.075.01.087.109.134.153.064.06.144.137.241.214.195.154.454.304.778.304s.702-.19.932-.32c.13-.073.297-.204.433-.304M7.34 3.781c.055-.02.123-.031.174-.003.011.006.024.021.02.034-.012.038-.074.032-.11.05-.032.017-.057.052-.093.054-.034 0-.086-.012-.09-.046-.007-.044.058-.072.1-.089m.581-.003c.05-.028.119-.018.173.003.041.017.106.045.1.09-.004.033-.057.046-.09.045-.036-.002-.062-.037-.093-.053-.036-.019-.098-.013-.11-.051-.004-.013.008-.028.02-.034" />
<path
fillRule="evenodd"
d="M8.446.019c2.521.003 2.38 2.66 2.364 4.093-.01.939.509 1.574 1.04 2.244.474.56 1.095 1.38 1.45 2.32.29.765.402 1.613.115 2.465a.8.8 0 0 1 .254.152l.001.002c.207.175.271.447.329.698.058.252.112.488.224.615.344.382.494.667.48.922-.015.254-.203.43-.435.57-.465.28-1.164.491-1.586 1.002-.443.527-.99.83-1.505.871a1.25 1.25 0 0 1-1.256-.716v-.001a1 1 0 0 1-.078-.21c-.67.038-1.252-.165-1.718-.128-.687.038-1.116.204-1.506.206-.151.331-.445.547-.808.63-.5.114-1.126 0-1.743-.324-.577-.306-1.31-.278-1.85-.39-.27-.057-.51-.157-.626-.384-.116-.226-.095-.538.07-.988.051-.16.012-.398-.026-.648a2.5 2.5 0 0 1-.037-.369c0-.133.022-.265.087-.386v-.002c.14-.266.368-.377.577-.451s.397-.125.53-.258c.143-.15.27-.374.443-.56q.036-.037.073-.07c-.081-.538.007-1.105.192-1.662.393-1.18 1.223-2.314 1.811-3.014.502-.713.65-1.287.701-2.016.042-.997-.705-3.974 2.112-4.2q.168-.015.321-.013m2.596 10.866-.03.016c-.223.121-.348.337-.427.656-.08.32-.107.733-.13 1.206v.001c-.023.37-.192.824-.31 1.267s-.176.862-.036 1.128v.002c.226.452.608.636 1.051.601s.947-.304 1.36-.795c.474-.576 1.218-.796 1.638-1.05.21-.126.324-.242.333-.4.009-.157-.097-.403-.425-.767-.17-.192-.217-.462-.274-.71-.056-.247-.122-.468-.26-.585l-.001-.001c-.18-.157-.356-.17-.565-.164q-.069.001-.14.005c-.239.275-.805.612-1.197.508-.359-.09-.562-.508-.587-.918m-7.204.03H3.83c-.189.002-.314.09-.44.225-.149.158-.276.382-.445.56v.002h-.002c-.183.184-.414.239-.61.31-.195.069-.353.143-.46.35v.002c-.085.155-.066.378-.029.624.038.245.096.507.018.746v.002l-.001.002c-.157.427-.155.678-.082.822.074.143.235.22.48.272.493.103 1.26.069 1.906.41.583.305 1.168.404 1.598.305.431-.098.712-.369.75-.867v-.002c.029-.292-.195-.673-.485-1.052-.29-.38-.633-.752-.795-1.09v-.002l-.61-1.11c-.21-.286-.43-.462-.68-.5a1 1 0 0 0-.106-.008M9.584 4.85c-.14.2-.386.37-.695.467-.147.048-.302.17-.495.28a1.3 1.3 0 0 1-.74.19.97.97 0 0 1-.582-.227c-.14-.113-.25-.237-.394-.322a3 3 0 0 1-.192-.126c-.063 1.179-.85 2.658-1.226 3.511a5.4 5.4 0 0 0-.43 1.917c-.68-.906-.184-2.066.081-2.568.297-.55.343-.701.27-.649-.266.436-.685 1.13-.848 1.844-.085.372-.1.749.01 1.097.11.349.345.67.766.931.573.351.963.703 1.193 1.015s.302.584.23.777a.4.4 0 0 1-.212.22.7.7 0 0 1-.307.056l.184.235c.094.124.186.249.266.375 1.179.805 2.567.496 3.568-.218.1-.342.197-.664.212-.903.024-.474.05-.896.136-1.245s.244-.634.53-.791a1 1 0 0 1 .138-.061q.005-.045.013-.087c.082-.546.569-.572 1.18-.303.588.266.81.499.71.814h.13c.122-.398-.133-.69-.822-1.025l-.137-.06a2.35 2.35 0 0 0-.012-1.113c-.188-.79-.704-1.49-1.098-1.838-.072-.003-.065.06.081.203.363.333 1.156 1.532.727 2.644a1.2 1.2 0 0 0-.342-.043c-.164-.907-.543-1.66-.735-2.014-.359-.668-.918-2.036-1.158-2.983M7.72 3.503a1 1 0 0 0-.312.053c-.268.093-.447.286-.559.391-.022.021-.05.04-.119.091s-.172.126-.321.238q-.198.151-.13.38c.046.15.192.325.459.476.166.098.28.23.41.334a1 1 0 0 0 .215.133.9.9 0 0 0 .298.066c.282.017.49-.068.673-.173s.34-.233.518-.29c.365-.115.627-.345.709-.564a.37.37 0 0 0-.01-.309c-.048-.096-.148-.187-.318-.257h-.001c-.354-.151-.507-.162-.705-.29-.321-.207-.587-.28-.807-.279m-.89-1.122h-.025a.4.4 0 0 0-.278.135.76.76 0 0 0-.191.334 1.2 1.2 0 0 0-.051.445v.001c.01.162.041.299.102.436.05.116.109.204.183.274l.089-.065.117-.09-.023-.018a.4.4 0 0 1-.11-.161.7.7 0 0 1-.054-.22v-.01a.7.7 0 0 1 .014-.234.4.4 0 0 1 .08-.179q.056-.069.126-.073h.013a.18.18 0 0 1 .123.05c.045.04.08.09.11.162a.7.7 0 0 1 .054.22v.01a.7.7 0 0 1-.002.17 1.1 1.1 0 0 1 .317-.143 1.3 1.3 0 0 0 .002-.194V3.23a1.2 1.2 0 0 0-.102-.437.8.8 0 0 0-.227-.31.4.4 0 0 0-.268-.102m1.95-.155a.63.63 0 0 0-.394.14.9.9 0 0 0-.287.376 1.2 1.2 0 0 0-.1.51v.015q0 .079.01.152c.114.027.278.074.406.138a1 1 0 0 1-.011-.172.8.8 0 0 1 .058-.278.5.5 0 0 1 .139-.2.26.26 0 0 1 .182-.069.26.26 0 0 1 .178.081c.055.054.094.12.124.21.029.086.042.17.04.27l-.002.012a.8.8 0 0 1-.057.277c-.024.059-.089.106-.122.145.046.016.09.03.146.052a5 5 0 0 1 .248.102 1.2 1.2 0 0 0 .244-.763 1.2 1.2 0 0 0-.11-.495.9.9 0 0 0-.294-.37.64.64 0 0 0-.39-.133z"
/>
</svg>
);
}
export function TerminalIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
{...props}
>
<polyline points="4 17 10 11 4 5" />
<line x1="12" x2="20" y1="19" y2="19" />
</svg>
);
}
export function GlobeIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
{...props}
>
<circle cx="12" cy="12" r="9" />
<path d="M3 12h18" />
<path d="M12 3a15 15 0 0 1 0 18" />
<path d="M12 3a15 15 0 0 0 0 18" />
</svg>
);
}
+284 -305
View File
@@ -8,352 +8,332 @@
// You should NOT make any changes in this file as it will be overwritten.
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
import { Route as rootRouteImport } from './routes/__root'
import { Route as PrivacyRouteImport } from './routes/privacy'
import { Route as OpencodeRouteImport } from './routes/opencode'
import { Route as DownloadRouteImport } from './routes/download'
import { Route as DocsRouteImport } from './routes/docs'
import { Route as CodexRouteImport } from './routes/codex'
import { Route as ClaudeCodeRouteImport } from './routes/claude-code'
import { Route as ChangelogRouteImport } from './routes/changelog'
import { Route as IndexRouteImport } from './routes/index'
import { Route as DocsIndexRouteImport } from './routes/docs/index'
import { Route as DocsWorktreesRouteImport } from './routes/docs/worktrees'
import { Route as DocsVoiceRouteImport } from './routes/docs/voice'
import { Route as DocsUpdatesRouteImport } from './routes/docs/updates'
import { Route as DocsSecurityRouteImport } from './routes/docs/security'
import { Route as DocsConfigurationRouteImport } from './routes/docs/configuration'
import { Route as DocsCliRouteImport } from './routes/docs/cli'
import { Route as DocsBestPracticesRouteImport } from './routes/docs/best-practices'
import { Route as rootRouteImport } from "./routes/__root";
import { Route as PrivacyRouteImport } from "./routes/privacy";
import { Route as OpencodeRouteImport } from "./routes/opencode";
import { Route as DocsRouteImport } from "./routes/docs";
import { Route as CodexRouteImport } from "./routes/codex";
import { Route as ClaudeCodeRouteImport } from "./routes/claude-code";
import { Route as ChangelogRouteImport } from "./routes/changelog";
import { Route as IndexRouteImport } from "./routes/index";
import { Route as DocsIndexRouteImport } from "./routes/docs/index";
import { Route as DocsWorktreesRouteImport } from "./routes/docs/worktrees";
import { Route as DocsVoiceRouteImport } from "./routes/docs/voice";
import { Route as DocsUpdatesRouteImport } from "./routes/docs/updates";
import { Route as DocsSecurityRouteImport } from "./routes/docs/security";
import { Route as DocsConfigurationRouteImport } from "./routes/docs/configuration";
import { Route as DocsCliRouteImport } from "./routes/docs/cli";
import { Route as DocsBestPracticesRouteImport } from "./routes/docs/best-practices";
const PrivacyRoute = PrivacyRouteImport.update({
id: '/privacy',
path: '/privacy',
id: "/privacy",
path: "/privacy",
getParentRoute: () => rootRouteImport,
} as any)
} as any);
const OpencodeRoute = OpencodeRouteImport.update({
id: '/opencode',
path: '/opencode',
id: "/opencode",
path: "/opencode",
getParentRoute: () => rootRouteImport,
} as any)
const DownloadRoute = DownloadRouteImport.update({
id: '/download',
path: '/download',
getParentRoute: () => rootRouteImport,
} as any)
} as any);
const DocsRoute = DocsRouteImport.update({
id: '/docs',
path: '/docs',
id: "/docs",
path: "/docs",
getParentRoute: () => rootRouteImport,
} as any)
} as any);
const CodexRoute = CodexRouteImport.update({
id: '/codex',
path: '/codex',
id: "/codex",
path: "/codex",
getParentRoute: () => rootRouteImport,
} as any)
} as any);
const ClaudeCodeRoute = ClaudeCodeRouteImport.update({
id: '/claude-code',
path: '/claude-code',
id: "/claude-code",
path: "/claude-code",
getParentRoute: () => rootRouteImport,
} as any)
} as any);
const ChangelogRoute = ChangelogRouteImport.update({
id: '/changelog',
path: '/changelog',
id: "/changelog",
path: "/changelog",
getParentRoute: () => rootRouteImport,
} as any)
} as any);
const IndexRoute = IndexRouteImport.update({
id: '/',
path: '/',
id: "/",
path: "/",
getParentRoute: () => rootRouteImport,
} as any)
} as any);
const DocsIndexRoute = DocsIndexRouteImport.update({
id: '/',
path: '/',
id: "/",
path: "/",
getParentRoute: () => DocsRoute,
} as any)
} as any);
const DocsWorktreesRoute = DocsWorktreesRouteImport.update({
id: '/worktrees',
path: '/worktrees',
id: "/worktrees",
path: "/worktrees",
getParentRoute: () => DocsRoute,
} as any)
} as any);
const DocsVoiceRoute = DocsVoiceRouteImport.update({
id: '/voice',
path: '/voice',
id: "/voice",
path: "/voice",
getParentRoute: () => DocsRoute,
} as any)
} as any);
const DocsUpdatesRoute = DocsUpdatesRouteImport.update({
id: '/updates',
path: '/updates',
id: "/updates",
path: "/updates",
getParentRoute: () => DocsRoute,
} as any)
} as any);
const DocsSecurityRoute = DocsSecurityRouteImport.update({
id: '/security',
path: '/security',
id: "/security",
path: "/security",
getParentRoute: () => DocsRoute,
} as any)
} as any);
const DocsConfigurationRoute = DocsConfigurationRouteImport.update({
id: '/configuration',
path: '/configuration',
id: "/configuration",
path: "/configuration",
getParentRoute: () => DocsRoute,
} as any)
} as any);
const DocsCliRoute = DocsCliRouteImport.update({
id: '/cli',
path: '/cli',
id: "/cli",
path: "/cli",
getParentRoute: () => DocsRoute,
} as any)
} as any);
const DocsBestPracticesRoute = DocsBestPracticesRouteImport.update({
id: '/best-practices',
path: '/best-practices',
id: "/best-practices",
path: "/best-practices",
getParentRoute: () => DocsRoute,
} as any)
} as any);
export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/changelog': typeof ChangelogRoute
'/claude-code': typeof ClaudeCodeRoute
'/codex': typeof CodexRoute
'/docs': typeof DocsRouteWithChildren
'/download': typeof DownloadRoute
'/opencode': typeof OpencodeRoute
'/privacy': typeof PrivacyRoute
'/docs/best-practices': typeof DocsBestPracticesRoute
'/docs/cli': typeof DocsCliRoute
'/docs/configuration': typeof DocsConfigurationRoute
'/docs/security': typeof DocsSecurityRoute
'/docs/updates': typeof DocsUpdatesRoute
'/docs/voice': typeof DocsVoiceRoute
'/docs/worktrees': typeof DocsWorktreesRoute
'/docs/': typeof DocsIndexRoute
"/": typeof IndexRoute;
"/changelog": typeof ChangelogRoute;
"/claude-code": typeof ClaudeCodeRoute;
"/codex": typeof CodexRoute;
"/docs": typeof DocsRouteWithChildren;
"/opencode": typeof OpencodeRoute;
"/privacy": typeof PrivacyRoute;
"/docs/best-practices": typeof DocsBestPracticesRoute;
"/docs/cli": typeof DocsCliRoute;
"/docs/configuration": typeof DocsConfigurationRoute;
"/docs/security": typeof DocsSecurityRoute;
"/docs/updates": typeof DocsUpdatesRoute;
"/docs/voice": typeof DocsVoiceRoute;
"/docs/worktrees": typeof DocsWorktreesRoute;
"/docs/": typeof DocsIndexRoute;
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/changelog': typeof ChangelogRoute
'/claude-code': typeof ClaudeCodeRoute
'/codex': typeof CodexRoute
'/download': typeof DownloadRoute
'/opencode': typeof OpencodeRoute
'/privacy': typeof PrivacyRoute
'/docs/best-practices': typeof DocsBestPracticesRoute
'/docs/cli': typeof DocsCliRoute
'/docs/configuration': typeof DocsConfigurationRoute
'/docs/security': typeof DocsSecurityRoute
'/docs/updates': typeof DocsUpdatesRoute
'/docs/voice': typeof DocsVoiceRoute
'/docs/worktrees': typeof DocsWorktreesRoute
'/docs': typeof DocsIndexRoute
"/": typeof IndexRoute;
"/changelog": typeof ChangelogRoute;
"/claude-code": typeof ClaudeCodeRoute;
"/codex": typeof CodexRoute;
"/opencode": typeof OpencodeRoute;
"/privacy": typeof PrivacyRoute;
"/docs/best-practices": typeof DocsBestPracticesRoute;
"/docs/cli": typeof DocsCliRoute;
"/docs/configuration": typeof DocsConfigurationRoute;
"/docs/security": typeof DocsSecurityRoute;
"/docs/updates": typeof DocsUpdatesRoute;
"/docs/voice": typeof DocsVoiceRoute;
"/docs/worktrees": typeof DocsWorktreesRoute;
"/docs": typeof DocsIndexRoute;
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/changelog': typeof ChangelogRoute
'/claude-code': typeof ClaudeCodeRoute
'/codex': typeof CodexRoute
'/docs': typeof DocsRouteWithChildren
'/download': typeof DownloadRoute
'/opencode': typeof OpencodeRoute
'/privacy': typeof PrivacyRoute
'/docs/best-practices': typeof DocsBestPracticesRoute
'/docs/cli': typeof DocsCliRoute
'/docs/configuration': typeof DocsConfigurationRoute
'/docs/security': typeof DocsSecurityRoute
'/docs/updates': typeof DocsUpdatesRoute
'/docs/voice': typeof DocsVoiceRoute
'/docs/worktrees': typeof DocsWorktreesRoute
'/docs/': typeof DocsIndexRoute
__root__: typeof rootRouteImport;
"/": typeof IndexRoute;
"/changelog": typeof ChangelogRoute;
"/claude-code": typeof ClaudeCodeRoute;
"/codex": typeof CodexRoute;
"/docs": typeof DocsRouteWithChildren;
"/opencode": typeof OpencodeRoute;
"/privacy": typeof PrivacyRoute;
"/docs/best-practices": typeof DocsBestPracticesRoute;
"/docs/cli": typeof DocsCliRoute;
"/docs/configuration": typeof DocsConfigurationRoute;
"/docs/security": typeof DocsSecurityRoute;
"/docs/updates": typeof DocsUpdatesRoute;
"/docs/voice": typeof DocsVoiceRoute;
"/docs/worktrees": typeof DocsWorktreesRoute;
"/docs/": typeof DocsIndexRoute;
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fileRoutesByFullPath: FileRoutesByFullPath;
fullPaths:
| '/'
| '/changelog'
| '/claude-code'
| '/codex'
| '/docs'
| '/download'
| '/opencode'
| '/privacy'
| '/docs/best-practices'
| '/docs/cli'
| '/docs/configuration'
| '/docs/security'
| '/docs/updates'
| '/docs/voice'
| '/docs/worktrees'
| '/docs/'
fileRoutesByTo: FileRoutesByTo
| "/"
| "/changelog"
| "/claude-code"
| "/codex"
| "/docs"
| "/opencode"
| "/privacy"
| "/docs/best-practices"
| "/docs/cli"
| "/docs/configuration"
| "/docs/security"
| "/docs/updates"
| "/docs/voice"
| "/docs/worktrees"
| "/docs/";
fileRoutesByTo: FileRoutesByTo;
to:
| '/'
| '/changelog'
| '/claude-code'
| '/codex'
| '/download'
| '/opencode'
| '/privacy'
| '/docs/best-practices'
| '/docs/cli'
| '/docs/configuration'
| '/docs/security'
| '/docs/updates'
| '/docs/voice'
| '/docs/worktrees'
| '/docs'
| "/"
| "/changelog"
| "/claude-code"
| "/codex"
| "/opencode"
| "/privacy"
| "/docs/best-practices"
| "/docs/cli"
| "/docs/configuration"
| "/docs/security"
| "/docs/updates"
| "/docs/voice"
| "/docs/worktrees"
| "/docs";
id:
| '__root__'
| '/'
| '/changelog'
| '/claude-code'
| '/codex'
| '/docs'
| '/download'
| '/opencode'
| '/privacy'
| '/docs/best-practices'
| '/docs/cli'
| '/docs/configuration'
| '/docs/security'
| '/docs/updates'
| '/docs/voice'
| '/docs/worktrees'
| '/docs/'
fileRoutesById: FileRoutesById
| "__root__"
| "/"
| "/changelog"
| "/claude-code"
| "/codex"
| "/docs"
| "/opencode"
| "/privacy"
| "/docs/best-practices"
| "/docs/cli"
| "/docs/configuration"
| "/docs/security"
| "/docs/updates"
| "/docs/voice"
| "/docs/worktrees"
| "/docs/";
fileRoutesById: FileRoutesById;
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
ChangelogRoute: typeof ChangelogRoute
ClaudeCodeRoute: typeof ClaudeCodeRoute
CodexRoute: typeof CodexRoute
DocsRoute: typeof DocsRouteWithChildren
DownloadRoute: typeof DownloadRoute
OpencodeRoute: typeof OpencodeRoute
PrivacyRoute: typeof PrivacyRoute
IndexRoute: typeof IndexRoute;
ChangelogRoute: typeof ChangelogRoute;
ClaudeCodeRoute: typeof ClaudeCodeRoute;
CodexRoute: typeof CodexRoute;
DocsRoute: typeof DocsRouteWithChildren;
OpencodeRoute: typeof OpencodeRoute;
PrivacyRoute: typeof PrivacyRoute;
}
declare module '@tanstack/react-router' {
declare module "@tanstack/react-router" {
interface FileRoutesByPath {
'/privacy': {
id: '/privacy'
path: '/privacy'
fullPath: '/privacy'
preLoaderRoute: typeof PrivacyRouteImport
parentRoute: typeof rootRouteImport
}
'/opencode': {
id: '/opencode'
path: '/opencode'
fullPath: '/opencode'
preLoaderRoute: typeof OpencodeRouteImport
parentRoute: typeof rootRouteImport
}
'/download': {
id: '/download'
path: '/download'
fullPath: '/download'
preLoaderRoute: typeof DownloadRouteImport
parentRoute: typeof rootRouteImport
}
'/docs': {
id: '/docs'
path: '/docs'
fullPath: '/docs'
preLoaderRoute: typeof DocsRouteImport
parentRoute: typeof rootRouteImport
}
'/codex': {
id: '/codex'
path: '/codex'
fullPath: '/codex'
preLoaderRoute: typeof CodexRouteImport
parentRoute: typeof rootRouteImport
}
'/claude-code': {
id: '/claude-code'
path: '/claude-code'
fullPath: '/claude-code'
preLoaderRoute: typeof ClaudeCodeRouteImport
parentRoute: typeof rootRouteImport
}
'/changelog': {
id: '/changelog'
path: '/changelog'
fullPath: '/changelog'
preLoaderRoute: typeof ChangelogRouteImport
parentRoute: typeof rootRouteImport
}
'/': {
id: '/'
path: '/'
fullPath: '/'
preLoaderRoute: typeof IndexRouteImport
parentRoute: typeof rootRouteImport
}
'/docs/': {
id: '/docs/'
path: '/'
fullPath: '/docs/'
preLoaderRoute: typeof DocsIndexRouteImport
parentRoute: typeof DocsRoute
}
'/docs/worktrees': {
id: '/docs/worktrees'
path: '/worktrees'
fullPath: '/docs/worktrees'
preLoaderRoute: typeof DocsWorktreesRouteImport
parentRoute: typeof DocsRoute
}
'/docs/voice': {
id: '/docs/voice'
path: '/voice'
fullPath: '/docs/voice'
preLoaderRoute: typeof DocsVoiceRouteImport
parentRoute: typeof DocsRoute
}
'/docs/updates': {
id: '/docs/updates'
path: '/updates'
fullPath: '/docs/updates'
preLoaderRoute: typeof DocsUpdatesRouteImport
parentRoute: typeof DocsRoute
}
'/docs/security': {
id: '/docs/security'
path: '/security'
fullPath: '/docs/security'
preLoaderRoute: typeof DocsSecurityRouteImport
parentRoute: typeof DocsRoute
}
'/docs/configuration': {
id: '/docs/configuration'
path: '/configuration'
fullPath: '/docs/configuration'
preLoaderRoute: typeof DocsConfigurationRouteImport
parentRoute: typeof DocsRoute
}
'/docs/cli': {
id: '/docs/cli'
path: '/cli'
fullPath: '/docs/cli'
preLoaderRoute: typeof DocsCliRouteImport
parentRoute: typeof DocsRoute
}
'/docs/best-practices': {
id: '/docs/best-practices'
path: '/best-practices'
fullPath: '/docs/best-practices'
preLoaderRoute: typeof DocsBestPracticesRouteImport
parentRoute: typeof DocsRoute
}
"/privacy": {
id: "/privacy";
path: "/privacy";
fullPath: "/privacy";
preLoaderRoute: typeof PrivacyRouteImport;
parentRoute: typeof rootRouteImport;
};
"/opencode": {
id: "/opencode";
path: "/opencode";
fullPath: "/opencode";
preLoaderRoute: typeof OpencodeRouteImport;
parentRoute: typeof rootRouteImport;
};
"/docs": {
id: "/docs";
path: "/docs";
fullPath: "/docs";
preLoaderRoute: typeof DocsRouteImport;
parentRoute: typeof rootRouteImport;
};
"/codex": {
id: "/codex";
path: "/codex";
fullPath: "/codex";
preLoaderRoute: typeof CodexRouteImport;
parentRoute: typeof rootRouteImport;
};
"/claude-code": {
id: "/claude-code";
path: "/claude-code";
fullPath: "/claude-code";
preLoaderRoute: typeof ClaudeCodeRouteImport;
parentRoute: typeof rootRouteImport;
};
"/changelog": {
id: "/changelog";
path: "/changelog";
fullPath: "/changelog";
preLoaderRoute: typeof ChangelogRouteImport;
parentRoute: typeof rootRouteImport;
};
"/": {
id: "/";
path: "/";
fullPath: "/";
preLoaderRoute: typeof IndexRouteImport;
parentRoute: typeof rootRouteImport;
};
"/docs/": {
id: "/docs/";
path: "/";
fullPath: "/docs/";
preLoaderRoute: typeof DocsIndexRouteImport;
parentRoute: typeof DocsRoute;
};
"/docs/worktrees": {
id: "/docs/worktrees";
path: "/worktrees";
fullPath: "/docs/worktrees";
preLoaderRoute: typeof DocsWorktreesRouteImport;
parentRoute: typeof DocsRoute;
};
"/docs/voice": {
id: "/docs/voice";
path: "/voice";
fullPath: "/docs/voice";
preLoaderRoute: typeof DocsVoiceRouteImport;
parentRoute: typeof DocsRoute;
};
"/docs/updates": {
id: "/docs/updates";
path: "/updates";
fullPath: "/docs/updates";
preLoaderRoute: typeof DocsUpdatesRouteImport;
parentRoute: typeof DocsRoute;
};
"/docs/security": {
id: "/docs/security";
path: "/security";
fullPath: "/docs/security";
preLoaderRoute: typeof DocsSecurityRouteImport;
parentRoute: typeof DocsRoute;
};
"/docs/configuration": {
id: "/docs/configuration";
path: "/configuration";
fullPath: "/docs/configuration";
preLoaderRoute: typeof DocsConfigurationRouteImport;
parentRoute: typeof DocsRoute;
};
"/docs/cli": {
id: "/docs/cli";
path: "/cli";
fullPath: "/docs/cli";
preLoaderRoute: typeof DocsCliRouteImport;
parentRoute: typeof DocsRoute;
};
"/docs/best-practices": {
id: "/docs/best-practices";
path: "/best-practices";
fullPath: "/docs/best-practices";
preLoaderRoute: typeof DocsBestPracticesRouteImport;
parentRoute: typeof DocsRoute;
};
}
}
interface DocsRouteChildren {
DocsBestPracticesRoute: typeof DocsBestPracticesRoute
DocsCliRoute: typeof DocsCliRoute
DocsConfigurationRoute: typeof DocsConfigurationRoute
DocsSecurityRoute: typeof DocsSecurityRoute
DocsUpdatesRoute: typeof DocsUpdatesRoute
DocsVoiceRoute: typeof DocsVoiceRoute
DocsWorktreesRoute: typeof DocsWorktreesRoute
DocsIndexRoute: typeof DocsIndexRoute
DocsBestPracticesRoute: typeof DocsBestPracticesRoute;
DocsCliRoute: typeof DocsCliRoute;
DocsConfigurationRoute: typeof DocsConfigurationRoute;
DocsSecurityRoute: typeof DocsSecurityRoute;
DocsUpdatesRoute: typeof DocsUpdatesRoute;
DocsVoiceRoute: typeof DocsVoiceRoute;
DocsWorktreesRoute: typeof DocsWorktreesRoute;
DocsIndexRoute: typeof DocsIndexRoute;
}
const DocsRouteChildren: DocsRouteChildren = {
@@ -365,9 +345,9 @@ const DocsRouteChildren: DocsRouteChildren = {
DocsVoiceRoute: DocsVoiceRoute,
DocsWorktreesRoute: DocsWorktreesRoute,
DocsIndexRoute: DocsIndexRoute,
}
};
const DocsRouteWithChildren = DocsRoute._addFileChildren(DocsRouteChildren)
const DocsRouteWithChildren = DocsRoute._addFileChildren(DocsRouteChildren);
const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
@@ -375,19 +355,18 @@ const rootRouteChildren: RootRouteChildren = {
ClaudeCodeRoute: ClaudeCodeRoute,
CodexRoute: CodexRoute,
DocsRoute: DocsRouteWithChildren,
DownloadRoute: DownloadRoute,
OpencodeRoute: OpencodeRoute,
PrivacyRoute: PrivacyRoute,
}
};
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
._addFileTypes<FileRouteTypes>()
._addFileTypes<FileRouteTypes>();
import type { getRouter } from './router.tsx'
import type { createStart } from '@tanstack/react-start'
declare module '@tanstack/react-start' {
import type { getRouter } from "./router.tsx";
import type { createStart } from "@tanstack/react-start";
declare module "@tanstack/react-start" {
interface Register {
ssr: true
router: Awaited<ReturnType<typeof getRouter>>
ssr: true;
router: Awaited<ReturnType<typeof getRouter>>;
}
}
-331
View File
@@ -1,331 +0,0 @@
import { Link, createFileRoute } from "@tanstack/react-router";
import { CommandDialog } from "~/components/command-dialog";
import { pageMeta } from "~/meta";
import {
desktopVersion,
linuxAppImageDownloadUrl,
linuxDebDownloadUrl,
linuxRpmDownloadUrl,
releaseBase,
macAppleSiliconDownloadUrl,
macIntelDownloadUrl,
appStoreUrl,
playStoreUrl,
webAppUrl,
AppleIcon,
AndroidIcon,
WindowsIcon,
LinuxIcon,
TerminalIcon,
GlobeIcon,
} from "~/downloads";
import "~/styles.css";
export const Route = createFileRoute("/download")({
head: () => ({
meta: pageMeta(
"Download - Paseo",
"Download Paseo for macOS, Windows, Linux, iOS, and Android. Your dev environment, in your pocket.",
),
}),
component: Download,
});
function Download() {
return (
<div className="min-h-screen bg-background">
<div className="max-w-3xl mx-auto p-6 md:p-12">
<header className="flex items-center justify-between gap-4 mb-12">
<Link to="/" className="flex items-center gap-3">
<img src="/logo.svg" alt="Paseo" className="w-6 h-6" />
<span className="text-lg font-medium">Paseo</span>
</Link>
<div className="flex items-center gap-4">
<Link
to="/docs"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
Docs
</Link>
<Link
to="/changelog"
className="text-sm text-muted-foreground hover:text-foreground transition-colors"
>
Changelog
</Link>
<a
href="https://discord.gg/jz8T2uahpH"
target="_blank"
rel="noopener noreferrer"
aria-label="Discord"
className="text-muted-foreground hover:text-foreground transition-colors inline-flex items-center"
>
<svg
role="img"
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z" />
</svg>
</a>
<a
href="https://github.com/getpaseo/paseo"
target="_blank"
rel="noopener noreferrer"
aria-label="GitHub"
className="text-muted-foreground hover:text-foreground transition-colors inline-flex items-center"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="18"
height="18"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M12 0C5.37 0 0 5.484 0 12.252c0 5.418 3.438 10.013 8.205 11.637.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.738-4.042-1.61-4.042-1.61-.546-1.403-1.333-1.776-1.333-1.776-1.089-.756.084-.741.084-.741 1.205.087 1.838 1.262 1.838 1.262 1.07 1.87 2.809 1.33 3.495 1.017.108-.79.417-1.33.76-1.636-2.665-.31-5.467-1.35-5.467-6.005 0-1.327.465-2.413 1.235-3.262-.124-.31-.535-1.556.117-3.243 0 0 1.008-.33 3.3 1.248a11.2 11.2 0 0 1 3.003-.404c1.02.005 2.045.138 3.003.404 2.29-1.578 3.297-1.248 3.297-1.248.653 1.687.242 2.933.118 3.243.77.85 1.233 1.935 1.233 3.262 0 4.667-2.807 5.692-5.48 5.995.43.38.823 1.133.823 2.285 0 1.65-.015 2.98-.015 3.386 0 .315.218.694.825.576C20.565 22.26 24 17.667 24 12.252 24 5.484 18.627 0 12 0z" />
</svg>
</a>
</div>
</header>
<h1 className="text-3xl md:text-4xl font-semibold tracking-tight mb-2">Download</h1>
<p className="text-muted-foreground mb-10">
v{desktopVersion}
</p>
{/* Desktop */}
<section className="rounded-xl border border-border bg-card/40 p-6 md:p-8 mb-6">
<div className="flex items-center justify-between mb-8">
<h2 className="text-2xl font-semibold">Desktop</h2>
<MonitorIcon className="h-5 w-5 text-muted-foreground" />
</div>
<div className="divide-y divide-border">
{/* macOS */}
<div className="flex items-center justify-between py-5 first:pt-0 last:pb-0">
<div className="flex items-center gap-3">
<AppleIcon className="h-5 w-5 text-foreground" />
<span className="font-medium">macOS</span>
</div>
<div className="flex flex-wrap items-center gap-2">
<DownloadPill href={macAppleSiliconDownloadUrl} label="Apple Silicon" />
<DownloadPill href={macIntelDownloadUrl} label="Intel" />
<CommandDialog
trigger={
<span className="inline-flex items-center justify-center rounded-full bg-foreground px-4 py-1.5 text-sm font-medium text-background hover:bg-foreground/85 transition-colors">
Homebrew
</span>
}
title="Install via Homebrew"
command="brew install --cask paseo"
/>
</div>
</div>
{/* Windows */}
<div className="flex items-center justify-between py-5 first:pt-0 last:pb-0">
<div className="flex items-center gap-3">
<WindowsIcon className="h-5 w-5 text-foreground" />
<span className="font-medium">Windows</span>
</div>
<div className="flex flex-wrap gap-2">
<DownloadPill
href={`${releaseBase}/Paseo-Setup-${desktopVersion}.exe`}
label="Download"
/>
</div>
</div>
{/* Linux */}
<div className="flex items-center justify-between py-5 first:pt-0 last:pb-0">
<div className="flex items-center gap-3">
<LinuxIcon className="h-5 w-5 text-foreground" />
<span className="font-medium">Linux</span>
</div>
<div className="flex flex-wrap gap-2">
<DownloadPill
href={linuxAppImageDownloadUrl}
label="AppImage"
/>
<DownloadPill href={linuxDebDownloadUrl} label="DEB" />
<DownloadPill href={linuxRpmDownloadUrl} label="RPM" />
</div>
</div>
</div>
</section>
{/* Mobile */}
<section className="rounded-xl border border-border bg-card/40 p-6 md:p-8 mb-6">
<div className="flex items-center justify-between mb-8">
<h2 className="text-2xl font-semibold">Mobile</h2>
<PhoneIcon className="h-5 w-5 text-muted-foreground" />
</div>
<div className="divide-y divide-border">
{/* Android */}
<div className="flex items-center justify-between py-5 first:pt-0 last:pb-0">
<div className="flex items-center gap-3">
<AndroidIcon className="h-5 w-5 text-foreground" />
<span className="font-medium">Android</span>
</div>
<div className="flex flex-wrap gap-2">
<DownloadPill
href={playStoreUrl}
label="Play Store"
external
/>
<DownloadPill
href={`${releaseBase}/paseo-v${desktopVersion}-android.apk`}
label="APK"
/>
</div>
</div>
{/* iOS */}
<div className="flex items-center justify-between py-5 first:pt-0 last:pb-0">
<div className="flex items-center gap-3">
<AppleIcon className="h-5 w-5 text-foreground" />
<span className="font-medium">iOS</span>
</div>
<div className="flex flex-wrap gap-2">
<DownloadPill
href={appStoreUrl}
label="App Store"
external
/>
</div>
</div>
</div>
</section>
{/* Web & CLI */}
<section className="rounded-xl border border-border bg-card/40 p-6 md:p-8">
<div className="flex items-center justify-between mb-8">
<h2 className="text-2xl font-semibold">Web & CLI</h2>
<TerminalIcon className="h-5 w-5 text-muted-foreground" />
</div>
<div className="divide-y divide-border">
<div className="flex items-center justify-between py-5 first:pt-0 last:pb-0">
<div className="flex items-center gap-3">
<GlobeIcon className="h-5 w-5 text-foreground" />
<span className="font-medium">Web App</span>
</div>
<div className="flex flex-wrap gap-2">
<DownloadPill
href={webAppUrl}
label="Open"
external
/>
</div>
</div>
<div className="flex items-center justify-between py-5 first:pt-0 last:pb-0">
<div className="flex items-center gap-3">
<TerminalIcon className="h-5 w-5 text-foreground" />
<span className="font-medium">CLI</span>
</div>
<div className="flex flex-wrap gap-2">
<code className="text-sm text-muted-foreground font-mono bg-muted px-3 py-1.5 rounded-lg">
npm install -g @getpaseo/cli
</code>
</div>
</div>
</div>
</section>
<p className="text-center text-xs text-muted-foreground mt-8">
All releases are available on{" "}
<a
href="https://github.com/getpaseo/paseo/releases"
target="_blank"
rel="noopener noreferrer"
className="underline hover:text-foreground transition-colors"
>
GitHub
</a>
.
</p>
</div>
</div>
);
}
function DownloadPill({
href,
label,
external,
}: {
href: string;
label: string;
external?: boolean;
}) {
return (
<a
href={href}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center rounded-full bg-foreground px-4 py-1.5 text-sm font-medium text-background hover:bg-foreground/85 transition-colors"
>
{label}
{external && (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
className="ml-1.5 h-3 w-3"
aria-hidden="true"
>
<path d="M7 17L17 7" />
<path d="M7 7h10v10" />
</svg>
)}
</a>
);
}
function MonitorIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
{...props}
>
<rect width="20" height="14" x="2" y="3" rx="2" />
<line x1="8" x2="16" y1="21" y2="21" />
<line x1="12" x2="12" y1="17" y2="21" />
</svg>
);
}
function PhoneIcon(props: React.SVGProps<SVGSVGElement>) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
{...props}
>
<rect width="14" height="20" x="5" y="2" rx="2" ry="2" />
<path d="M12 18h.01" />
</svg>
);
}
+1 -1
View File
@@ -79,7 +79,7 @@
--color-background: #0a0a0a;
--color-foreground: #fafafa;
--color-muted: #262626;
--color-muted-foreground: #b6b6b6;
--color-muted-foreground: #a1a1aa;
--color-card: #171717;
--color-border: #27272a;
--color-primary: #3b82f6;
-17
View File
@@ -9,12 +9,6 @@ const rootPackagePath = path.join(rootDir, "package.json");
const rootPackage = JSON.parse(readFileSync(rootPackagePath, "utf8"));
const rootVersion = rootPackage.version;
const workspacePaths = Array.isArray(rootPackage.workspaces) ? rootPackage.workspaces : [];
const sharedMetadata = {
homepage: rootPackage.homepage,
repository: rootPackage.repository,
author: rootPackage.author,
license: rootPackage.license,
};
if (typeof rootVersion !== "string" || rootVersion.length === 0) {
throw new Error('Root package.json must contain a valid "version"');
@@ -43,17 +37,6 @@ for (const workspacePath of workspacePaths) {
changed = true;
}
if (pkg.name === "@getpaseo/desktop") {
for (const [field, value] of Object.entries(sharedMetadata)) {
const currentValue = JSON.stringify(pkg[field]);
const nextValue = JSON.stringify(value);
if (currentValue !== nextValue) {
pkg[field] = value;
changed = true;
}
}
}
for (const section of dependencySections) {
const deps = pkg[section];
if (!deps || typeof deps !== "object") {
+2 -8
View File
@@ -495,10 +495,7 @@ case "$command_name" in
sanitize_room_path "$room"
require_room "$room"
all_files=()
while IFS= read -r _f; do
all_files+=("$_f")
done < <(message_files_sorted "$room")
mapfile -t all_files < <(message_files_sorted "$room")
since_file=""
if [[ -n "$since" ]]; then
@@ -590,10 +587,7 @@ case "$command_name" in
trap 'exit 130' INT TERM
while true; do
all_files=()
while IFS= read -r _f; do
all_files+=("$_f")
done < <(message_files_sorted "$room")
mapfile -t all_files < <(message_files_sorted "$room")
new_files=()
after_baseline=false