* Surface GitHub auto-merge actions * Namespace GitHub auto-merge RPC * Update workspace git refresh test expectation * Avoid redundant GitHub refresh queue
15 KiB
CLAUDE.md
Paseo is a mobile app for monitoring and controlling your local AI coding agents from anywhere. Your dev environment, in your pocket. Connects directly to your actual development environment — your code stays on your machine.
Supported agents: Claude Code, Codex, and OpenCode.
Repository map
This is an npm workspace monorepo:
packages/server— Daemon: agent lifecycle, WebSocket API, MCP serverpackages/app— Mobile + web client (Expo)packages/cli— Docker-style CLI (paseo run/ls/logs/wait)packages/relay— E2E encrypted relay for remote accesspackages/desktop— Electron desktop wrapperpackages/website— Marketing site (paseo.sh)
Docs
docs/ is the source of truth for system-level and process-level knowledge. "The docs", "check the docs", or "check the X docs" always mean this directory — not the web. Look here before fetching anything online; the docs capture gotchas and conventions you cannot derive from the code or external sources.
At the start of non-trivial work, list docs/ and skim anything relevant to the task. When you learn something meta worth preserving — a gotcha, a convention, a workflow, a piece of system context that will outlive the current task — update an existing doc or propose a new one. Code-level facts belong in inline comments next to the code; system, process, and gotcha-level facts belong in docs/.
| Doc | What's in it |
|---|---|
| docs/product.md | What Paseo is, who it's for, where it's going |
| docs/architecture.md | System design, package layering, WebSocket protocol, agent lifecycle, data flow |
| docs/agent-lifecycle.md | Agent states, parent/child relationships, archive semantics, tabs vs archive, subagents track |
| docs/data-model.md | File-based JSON persistence, Zod schemas, atomic writes, no migrations |
| docs/glossary.md | Authoritative terminology — UI label wins, no synonyms |
| docs/coding-standards.md | Type hygiene, error handling, state design, React patterns, file organization |
| docs/design.md | Theme tokens — colors, fonts, spacing, radii, icons |
| docs/unistyles.md | Unistyles gotchas — useUnistyles() is forbidden, alternatives in order |
| docs/file-icons.md | Material icon theme integration for the file explorer |
| docs/providers.md | Adding a new agent provider end-to-end |
| docs/custom-providers.md | Custom provider config: Z.AI, Alibaba/Qwen, ACP agents, profiles, custom binaries |
| docs/development.md | Dev server, build sync gotchas, CLI reference, agent state, Playwright MCP |
| docs/rpc-namespacing.md | WebSocket RPC naming convention — dotted namespaces and .request/.response pairs |
| docs/testing.md | TDD workflow, determinism, real dependencies over mocks, test organization |
| docs/mobile-testing.md | Maestro and mobile test workflows |
| docs/ad-hoc-daemon-testing.md | Isolated in-process daemon test harness |
| docs/android.md | App variants, local/cloud builds, EAS workflows |
| docs/release.md | Release playbook, draft releases, completion checklist |
| SECURITY.md | Relay threat model, E2E encryption, DNS rebinding, agent auth |
Quick start
npm run dev # Start daemon + Expo in Tmux
npm run cli -- ls -a -g # List all agents
npm run cli -- daemon status # Check daemon status
npm run typecheck # Always run after changes
npm run lint # Always run after changes
npm run format # Auto-format with Biome
npm run format:check # Check formatting without writing
See docs/development.md for full setup, build sync requirements, and debugging.
Critical rules
-
NEVER restart the main Paseo daemon on port 6767 without permission — it manages all running agents. If you're an agent, restarting it kills your own process.
-
NEVER assume a timeout means the service needs restarting — timeouts can be transient.
-
NEVER add auth checks to tests — agent providers handle their own auth.
-
NEVER run the full test suite locally. The test suites are heavy and will freeze the machine, especially if multiple agents run them in parallel. Rules:
- Run only the specific test file you changed:
npx vitest run <file> --bail=1 - Never run
npm run testfor an entire workspace unless explicitly asked. - If you must run a broad suite, pipe output to a file and read it afterward:
npx vitest run <file> --bail=1 > /tmp/test-output.txt 2>&1then read the file. - Never re-run a test suite that another agent already ran and reported green — trust the result.
- For full suite verification, push to CI and check GitHub Actions instead.
- Run only the specific test file you changed:
-
Always run typecheck and lint after every change.
-
Build workspace packages before diagnosing cross-package type errors. This repo consumes generated declarations across workspaces. If typecheck fails in a package that depends on another workspace (especially CLI depending on server/daemon types), rebuild the owning package first so
distdeclarations are current:npm run build:daemon— rebuild highlight, relay, server, and CLI when daemon/server/CLI types may be stale.- Do not patch inferred callback parameters or add local duplicate types just to silence stale declaration errors.
-
Run
npm run formatbefore committing. This repo uses Biome for formatting. Do not manually fix formatting — let the formatter handle it. -
Always use npm scripts for linting and formatting. Do not run tools directly with
npx eslint,npx oxfmt,npx oxlint, or package-local binaries. For targeted checks, pass file paths through the npm script:npm run lint -- packages/app/src/components/message.tsxnpm run format:files -- CLAUDE.md packages/app/src/components/message.tsx
-
The protocol stays backward-compatible. Features don't have to. Two separate contracts:
- Protocol contract (always): schema changes must not break parsing in either direction. An old client must still parse messages from a new daemon; a new daemon must still parse messages from an old client.
- New fields:
.optional()with a sensible default or.transform()fallback. - Never flip optional → required, remove fields, or narrow types (
string→enum,nullable→ non-null). - Removed fields stay accepted (we stop sending them, not stop reading them).
- Test with: "does a 6-month-old client still parse this?" and "does a 6-month-old daemon still send something this client accepts?"
- New fields:
- Feature contract (per-feature): a new feature may require a new daemon capability. The client detects whether the capability is present and either runs the feature or shows "Update the host to use this." That's it.
- No fallback paths. Don't write a degraded version of a new feature that runs on old daemons. Don't fan out across legacy RPCs to simulate a missing capability. The user upgrades or doesn't get the feature.
- No defensive branches scattered through the feature. Capability detection happens in one place; downstream code reads a clean shape.
- Capability flags live in
server_info.features.*with a single// COMPAT(featureName): added in v0.1.X, drop the gate when floor >= v0.1.Xcomment marking the cleanup site. - Existing functionality keeps working across versions — that's the protocol contract doing its job. New-feature degradation is not the goal.
- New RPCs use dotted namespaces with direction suffixes. Follow docs/rpc-namespacing.md:
domain.provider.operation.requestpairs withdomain.provider.operation.response. Existing flat RPC names will migrate over time; don't add new ones.
- Protocol contract (always): schema changes must not break parsing in either direction. An old client must still parse messages from a new daemon; a new daemon must still parse messages from an old client.
-
All back-compat shims are tagged and dated for cleanup. Every shim that exists for old-client/old-daemon support carries a
COMPAT(name)comment with the version it was added in and a target removal date (typically 6 months out). One grep —rg "COMPAT\("— should produce the full list of cleanup work. Don't bury back-compat in untagged??-fallbacks or optional-chain tunnels — that's how it stops being deletable.
Platform gating
The app runs on iOS, Android, web (browser), and web (Electron desktop). Code is cross-platform by default. Gate only when you must. Import gates from @/constants/platform.
The four gates
| Gate | Type | When to use |
|---|---|---|
isWeb |
constant | DOM APIs — document, window, <div>, addEventListener, ResizeObserver. This is the exception, not the default. |
isNative |
constant | Native-only APIs — Haptics, StatusBar.currentHeight, push tokens, camera/scanner, expo-av. |
getIsElectron() |
cached fn | Desktop wrapper features — file dialogs, titlebar drag region, daemon management, app updates, dock badges. |
useIsCompactFormFactor() |
hook | Layout decisions — sidebar overlay vs pinned, modal vs full screen, single-panel vs split. From @/constants/layout. |
Decision matrix
| I need to... | Use |
|---|---|
Access DOM (document, window, <div>, addEventListener) |
if (isWeb) |
| Use a native-only API (Haptics, push tokens, camera) | if (isNative) |
| Use an Electron bridge (file dialog, titlebar, updates) | if (getIsElectron()) |
| Switch layout between phone and tablet/desktop | useIsCompactFormFactor() |
| Show something on hover, always-visible on native | isHovered || isNative || isCompact (hover only works on web) |
| Gate to iOS or Android specifically | Platform.OS === "ios" / Platform.OS === "android" (rare, keep inline) |
Rules
- Default is cross-platform. Don't gate unless you have a specific reason.
- Prefer Metro file extensions over
ifstatements. When a module has fundamentally different implementations per platform, use.web.ts/.native.tsfile extensions instead of runtimeif (isWeb)branches. Metro resolves the correct file at build time — the unused platform code is never bundled. Reserveif (isWeb)for small, inline checks (a single line or a few props). If you find yourself writing a largeif (isWeb) { ... } else { ... }block, split into separate files instead.Import ashooks/ use-audio-recorder.web.ts ← uses Web Audio API use-audio-recorder.native.ts ← uses expo-audio@/hooks/use-audio-recorder— Metro picks the right file automatically. - Use
.electron.ts/.electron.tsxfor Electron-only web modules. Electron is still the Metrowebplatform, but desktop dev/build setsPASEO_WEB_PLATFORM=electron, so Metro first looks for.electron.*files and falls back to normal.web.*files. Use this when the implementation depends on Electron-only behavior such aswebviewTag, desktop preload APIs, or the Electron bridge. Keep plain browser web in.web.*, and keep native fallbacks in the base file or.native.*.Import ascomponents/ browser-pane.electron.tsx ← Electron <webview> implementation browser-pane.web.tsx ← plain web fallback browser-pane.tsx ← native fallback@/components/browser-pane— Electron desktop gets the.electron.tsxfile, browser web gets.web.tsx, and native gets the native/base implementation. - NEVER use raw DOM APIs without
isWebguard. DOM APIs crash native. Casting a RN ref toHTMLElementis a red flag — ensure the block is web-only. - NEVER use
onPointerEnter/onPointerLeave. They don't fire on native iOS. - Hover only works on web. React Native's
onHoverIn/onHoverOutonPressabledoes NOT fire on native iOS/iPad — the underlying W3C pointer events are behind disabled experimental flags. For hover-to-show UI (kebab menus, action buttons), useisHovered || isNative || isCompactso the controls are always visible on native and hover-to-show on web. - Don't use Platform.OS as a proxy for layout capabilities. Use breakpoints for layout decisions, not platform checks.
- Import
isWeb/isNativefrom@/constants/platform. Never writeconst isWeb = Platform.OS === "web"locally.
Debugging
Find the complete daemon logs and traces in the $PASEO_HOME/daemon.log