* feat(app): add auto-expand reasoning setting
* chore: add cross-env mise tool
* fix(app): stabilize web export and settings detail placement
* Revert "chore: add cross-env mise tool"
This reverts commit f56a367e0c.
* perf(app): narrow settings reads in agent stream
---------
Co-authored-by: Mohamed Boudra <boudra.moha@gmail.com>
20 KiB
Development
Prerequisites
- Node.js (see
.tool-versionsfor exact version) - npm workspaces (comes with Node)
Running the dev server
npm run dev:server
npm run dev:app
npm run dev:desktop
Root checkout dev is intentionally split across terminals:
npm run dev:serverruns the daemon on127.0.0.1:6768.npm run dev:appruns Expo onhttp://localhost:8081and connects to the dev daemon.npm run dev:desktopruns its own Electron-flavored Expo server on the first free port from8082through8089. It never claims port8081.
npm run dev is only a shorthand for npm run dev:server. Keep 127.0.0.1:6767 for the packaged app and production-style ~/.paseo state.
PASEO_HOME
PASEO_HOME is the directory that holds runtime state (agents, worktrees, workspace config, sockets, daemon log). Resolution rules:
- The server itself (e.g. when launched by the desktop app or
npm run start) defaults to~/.paseo(seepackages/server/src/server/paseo-home.ts). - Repo dev scripts default to
$ROOT/.dev/paseo-home, where$ROOTis the current checkout or worktree root. This keeps all dev state scoped to the checkout instead of the packaged desktop app. npm run cli -- ...runs through the same dev-home wrapper as the dev scripts, so the in-repo CLI automatically targets the current checkout's.dev/paseo-homeand configured dev daemon endpoint.- Paseo-created worktrees seed
$PASEO_WORKTREE_PATH/.dev/paseo-homefrom$PASEO_SOURCE_CHECKOUT_PATH/.dev/paseo-homeby copying durable JSON metadata. Runtime files like pid files, sockets, and logs are not copied. - This repo's worktree setup also best-effort seeds
packages/app/iosand the newest.dev/ios-buildentry from the source checkout so iOS simulator services can reuse native project and Xcode cache state when it is safe enough to do so.
Override knobs:
PASEO_HOME=~/.paseo-blue npm run dev # explicit home
PASEO_DEV_SEED_HOME=/path/to/home npm run dev # seed from a different source home
PASEO_DEV_RESET_HOME=1 npm run dev # clear and reseed the derived worktree home
Daemon endpoints
- Stable daemon launched by the desktop app:
localhost:6767. - Root checkout dev daemon:
localhost:6768. - Root checkout Expo:
http://localhost:8081. - Root checkout desktop dev Expo: first free port from
8082through8089. npm run dev(Windows):localhost:6767for the daemon.
In Paseo-managed worktree services, use the injected service environment rather than hardcoded root checkout ports.
Expo Router
Route ownership, startup restore, and native blank-screen gotchas live in
expo-router.md. Read it before changing packages/app/src/app,
startup routing, remembered workspace restore, or active workspace selection.
iOS simulator preview service
Paseo worktrees expose the native iOS dev app through the ios-simulator service in paseo.json. The service URL serves the simulator preview at /.sim, so the preview link is ${PASEO_URL}/.sim.
The service is designed for concurrent worktrees: it derives a deterministic simulator identity from the worktree path, uses the worktree's assigned PASEO_PORT, pins serve-sim to that simulator UDID, and only tears down that worktree's helper/simulator state. It must not rely on the globally booted simulator or any fixed Metro port.
Worktree setup best-effort seeds the generated iOS project and newest native build cache from the source checkout before the service runs. The service still validates the native project by running Expo prebuild and Xcode; the seed only avoids paying all setup/build cost from a cold worktree every time.
Starting the service must not create, focus, reveal, or leave behind macOS Simulator.app windows. The browser preview is the user-visible simulator surface.
Desktop renderer profiling
npm run dev:desktop starts Electron with Chromium remote debugging enabled on
http://127.0.0.1:9223 so renderer CPU profiles can be captured through CDP.
It launches its own Electron-flavored Expo server and passes that URL to Electron.
Override the CDP port with PASEO_ELECTRON_REMOTE_DEBUGGING_PORT when 9223 is busy.
When running a dedicated Electron QA instance against a non-default Expo port, set
EXPO_DEV_URL explicitly. Desktop main defaults to http://localhost:8081, so
PASEO_PORT=57928 alone starts Metro on 57928 but Electron still loads 8081.
React render profiling
The app has a gated React render profiler in
packages/app/src/utils/render-profiler.tsx. Wrap the component boundary you want
to measure with RenderProfile, then open the app with ?renderProfile=1. When
the query param is absent, RenderProfile returns children directly and records
nothing.
Captured samples are exposed on globalThis.__PASEO_RENDER_PROFILE__. Call
globalThis.__PASEO_RESET_RENDER_PROFILE__?.() after warm-up and before the
interaction you want to measure. If a memo comparator or subscription boundary
needs explanation, call recordRenderProfileReasons(id, reasons) while profiling;
reason counts are exposed on globalThis.__PASEO_RENDER_PROFILE_REASONS__.
Use this workflow for any render investigation:
- Add stable
RenderProfileboundaries around the suspected root and expensive children. Keep IDs specific enough to compare before and after. - Reproduce against real app state, not toy fixtures, whenever practical.
- Record an idle baseline first. If idle is noisy, fix or account for that before optimizing the interaction.
- Warm up the route, reset profiler samples, run the exact interaction, then
compare
actualDuration, render counts, and per-commit samples. - When a memo boundary still renders, record reasons before changing code. Do not guess from object identity alone.
- Keep changes that move the measured profile. Remove probes or memo wrappers that do not move the number.
What this caught during the workspace tab investigation:
- A large apparent workspace cost was real interaction work, not daemon noise; the idle baseline stayed near zero.
- The expensive stream rerender was mostly prop identity churn from pane context callbacks and capability objects, not new stream data.
- Stabilizing provider actions at the pane boundary helped because every mounted panel consumes that context.
- Comparing value-shaped capability flags beat preserving object identity through unrelated stores.
- Some plausible fixes did not pay off: memoizing the tab row and composer draft object barely moved the profile, so they were removed.
Existing scenario script: workspace agent/terminal tab switching. Start Expo on web, keep a daemon available, then run:
PASEO_PROFILE_SERVER_ID=<server-id> \
PASEO_PROFILE_WORKSPACE_ID=<workspace-path> \
PASEO_PROFILE_AGENT_ID=<agent-id> \
npm run profile:workspace-tabs --workspace=@getpaseo/app
This script opens the app with ?renderProfile=1, creates a temporary terminal
tab, switches between a real agent and that terminal, prints aggregated React
Profiler timings, then removes the temporary terminal. It is an example of the
workflow above, not the only way to use the profiler. Useful knobs:
PASEO_PROFILE_APP_URL=http://localhost:19010 # Expo web URL
PASEO_PROFILE_SWITCH_COUNT=1 # number of agent/terminal switch pairs
PASEO_PROFILE_SWITCH_WAIT_MS=250 # delay after each click
PASEO_PROFILE_IDLE_WAIT_MS=3000 # idle baseline before switching
PASEO_PROFILE_DUMP_COMMITS=1 # include per-commit profiler samples
Desktop macOS compositor watchdog
macOS display sleep can leave Chromium's GPU-process display link — the vsync source that drives frame production — stuck on a stale display. The compositor then stops producing frames and the window looks frozen: unresponsive to clicks and keys even though the renderer and every process stay alive. It self-recovers after a few minutes, which is too long for a foreground app.
setupDarwinCompositorWatchdog
(packages/desktop/src/window/compositor-watchdog/index.ts) guards against
this. It polls the renderer for frame production every couple of seconds and,
after a sustained stall while the window is visible and unlocked, restarts the
GPU process so Chromium rebuilds the display link. The probe is skipped while
the screen is locked or the window is hidden or minimized, since a window
legitimately stops producing frames then.
The watchdog deliberately leaves background throttling enabled. Calling
webContents.setBackgroundThrottling(false) would keep the compositor producing
frames non-stop, pinning ProMotion displays at 120Hz forever and draining the
battery while the app is idle — so do not re-add it. The probe's visibility
guards already prevent throttling from causing a false stall.
Daemon logs
Check $PASEO_HOME/daemon.log for daemon logs. The default level is info; set
PASEO_LOG_LEVEL=trace before launching the daemon when you need full provider,
session, and agent-manager traces for stuck-state debugging.
The supervisor rotates daemon.log. Persisted log.file.rotate settings in
$PASEO_HOME/config.json win first. Without persisted config, the optional
PASEO_LOG_ROTATE_SIZE and PASEO_LOG_ROTATE_COUNT env vars override the
defaults. The default rotation is 10m x 3 files everywhere.
Agent Tool Catalog Measurement
Measure the MCP tools/list payload that Paseo injects into agents with:
npm run measure:agent-tools --workspace=@getpaseo/server
The command reports compact JSON bytes, estimated tokens, field totals, largest
tools, and the browser-tools delta. It defaults to the agent-scoped catalog; use
-- --scope=top-level for the unaffiliated /mcp/agents shape and -- --json
for machine-readable output.
paseo.json service scripts
worktree.setup and worktree.teardown accept either a multiline shell script or an array
of commands. Both run sequentially.
Lifecycle commands run in the worktree through a stable script shell: bash
resolved from PATH on macOS/Linux, and PowerShell with -NoProfile on
Windows. They inherit the daemon environment plus Paseo's lifecycle variables;
login and interactive shell startup files are not loaded, and Bash's BASH_ENV
hook is unset. Daemon-run loop verify checks and ACP single-string terminal
commands use the same non-login Bash behavior on macOS/Linux, but preserve their
existing cmd.exe /c string semantics on Windows. Service scripts are separate:
they launch in a terminal and receive the service environment described below.
{
"worktree": {
"setup": "npm ci\ncp \"$PASEO_SOURCE_CHECKOUT_PATH/.env\" .env\nnpm run db:migrate",
"teardown": "npm run db:drop || true"
}
}
Every scripts entry with "type": "service" receives these environment variables:
| Variable | Value |
|---|---|
PASEO_SERVICE_<NAME>_URL |
Proxied URL for a declared peer service. Prefer this for peer discovery; it survives peer restarts. |
PASEO_SERVICE_<NAME>_PORT |
Raw ephemeral port for a declared peer service. Use only as a bypass escape hatch; it can go stale if that peer restarts. |
PASEO_URL |
Self alias for PASEO_SERVICE_<SELF>_URL. |
PASEO_PORT |
Self alias for PASEO_SERVICE_<SELF>_PORT. |
HOST |
Bind host for the service process. |
Service proxy hostnames use the double-dash shape: web--feature-auth--project.localhost or, on the default branch, web--project.localhost. Optional public aliases use the same leftmost label under the configured public base host.
<NAME> is normalized from the script name by uppercasing it, replacing each run of non-A-Z0-9 characters with _, and trimming leading or trailing _. For example, app-server and app.server both normalize to APP_SERVER; that collision fails at spawn time with an actionable error.
PORT is not injected by default. If a framework requires PORT, set it in the command:
{
"scripts": {
"web": {
"type": "service",
"command": "PORT=$PASEO_PORT npm run dev:web"
}
}
}
Bundled daemon web UI
The user-facing guide for this feature (enabling it, reverse proxy, TLS, tunnels, security) lives at public-docs/web-ui.md. This section is the contributor/build reference: how the artifact is produced, bundled, and excluded from desktop packaging.
The daemon can optionally serve the browser web client from the same HTTP server. This is disabled by default.
Enable it for a running daemon with:
paseo daemon start --web-ui
Or set the environment variable:
PASEO_WEB_UI_ENABLED=true paseo daemon start
Or persist it in config.json:
{
"features": {
"webUi": {
"enabled": true
}
}
}
When enabled, opening the daemon HTTP origin (for example http://localhost:6767/) serves the web app. The same HTTP server continues to serve /api/*, /mcp/*, /public/*, the WebSocket upgrade, and service-proxy routes. Static files load without daemon bearer auth; API and WebSocket calls still enforce auth.
The served app auto-bootstraps a connection to the same origin, so opening http://localhost:6767/ directly usually skips the Add Host step.
Build the artifact for packaging or measurement with:
npm run build:daemon-web-ui
This exports the normal browser web app (not the Electron-flavored desktop renderer) and copies it into packages/server/dist/server/web-ui, precompressing .html, .js, .css, and JSON assets as .br and .gz.
Measured bundle size for a standard Expo web export:
- raw: 10.77 MiB
- gzip: 2.55 MiB
- brotli: 1.93 MiB
The desktop-managed daemon disables the bundled web UI by default (PASEO_WEB_UI_ENABLED=false) because the desktop app already ships the renderer as app-dist. Shipping the same assets again inside @getpaseo/server would duplicate the ~10.8 MiB install. Desktop packaging also excludes node_modules/@getpaseo/server/dist/server/web-ui/** from the packaged app.
Built workspace packages
Package imports resolve through package exports to compiled dist/ output, not sibling src/ files. This is true in local dev and in published packages: the app, daemon, CLI, and SDK consumers should all exercise the same runtime paths.
npm run dev:server builds the server-side workspace packages once, then keeps @getpaseo/protocol and @getpaseo/client fresh with TypeScript watch builds while the daemon runs. If you change protocol schemas or client code outside that watch workflow, rebuild the producer before trusting runtime behavior.
Use the named root build targets instead of remembering workspace dependency chains:
npm run build:client # protocol -> client
npm run build:server-deps # highlight -> relay -> protocol -> client
npm run build:server # server-deps -> server -> cli
npm run build:app-deps # highlight -> protocol -> client -> expo-two-way-audio
Use npm run build:server whenever you have changed any daemon/server-facing package and need clean cross-package types or runtime behavior.
The app Metro config disables Watchman and uses Metro's node crawler for exports. Keep that invariant unless you have verified production app exports on machines with and without Watchman installed; distro Watchman builds can differ in capabilities and change Metro's crawl behavior.
For tighter loops, you can rebuild a single workspace:
- Changed
packages/protocol/src/*orpackages/client/src/*:npm run build:client. - Changed
packages/server/src/*,packages/cli/src/*,packages/relay/src/*, orpackages/highlight/src/*:npm run build:server. - Changed app build dependencies:
npm run build:app-deps.
ACP provider catalog versions
The in-app ACP provider catalog pins package-runner entries (npx, npm exec,
and uvx) to exact package versions. Run the drift checker regularly — and
before releases — so catalog installs do not sit on stale agent versions:
npm run acp:version-drift # report stale/non-exact package pins
npm run acp:version-drift:check # same, exits non-zero on drift
npm run acp:version-drift:update # rewrite catalog pins to latest exact versions
The checker updates only package-runner catalog entries. Providers that use a
preinstalled binary such as opencode acp, cursor-agent acp, or goose acp
are reported as skipped because their versions are owned by the user's local
install.
CLI reference
Use npm run cli to run the in-repo CLI from source (npx tsx packages/cli/src/index.ts). The script wraps the CLI with scripts/dev-home.sh, so it automatically uses this checkout's .dev/paseo-home and dev daemon endpoint unless you pass an explicit override. The globally installed paseo binary on macOS is a symlink into the installed Paseo desktop app, not this checkout — use it to drive the desktop's built-in daemon, but use npm run cli when you want to talk to the CLI you are editing.
npm run cli -- ls -a -g # List all agents globally
npm run cli -- ls -a -g --json # Same, as JSON
npm run cli -- inspect <id> # Show detailed agent info
npm run cli -- logs <id> # View agent timeline
npm run cli -- daemon status # Check daemon status
Use --host <host:port> to point the CLI at a different daemon:
npm run cli -- --host localhost:7777 ls -a
Agent state
Agent data lives at:
$PASEO_HOME/agents/{cwd-with-dashes}/{agent-id}.json
Find an agent by ID:
find $PASEO_HOME/agents -name "{agent-id}.json"
Find by content:
rg -l "some title text" $PASEO_HOME/agents/
Provider session files
Get the session ID from the agent JSON (persistence.sessionId), then:
Claude:
~/.claude/projects/{cwd-with-dashes}/{session-id}.jsonl
Codex:
~/.codex/sessions/{YYYY}/{MM}/{DD}/rollout-{timestamp}-{session-id}.jsonl
Testing with Playwright MCP
Point Playwright MCP at the running Expo web target. For root checkout dev, npm run dev:app reserves http://localhost:8081. For Paseo-managed worktree app services, use the service URL or port shown by Paseo for that worktree.
Do NOT use browser history (back/forward). Always navigate by clicking UI elements or using browser_navigate with the full URL — the app uses client-side routing and browser history breaks state.
App web deploys
packages/app exports a single-page Expo web app and deploys the dist/
directory to Cloudflare Pages with npm run deploy:web --workspace=@getpaseo/app.
PWA install metadata lives in packages/app/public/manifest.json and is linked
from packages/app/public/index.html. Keep the install icons in public/ so
Cloudflare serves them from stable root URLs after expo export.
Do not add service-worker caching casually. Paseo is a live control surface for agents, and an aggressive service worker can strand installed users on stale web code. If offline behavior becomes a product requirement, add it deliberately with an update strategy and test the installed-app upgrade path.
Expo troubleshooting
npx expo-doctor
Diagnoses version mismatches and native module issues.
Typecheck
Always run typecheck after changes:
npm run typecheck