This page walks through standing up the OpenSail desktop client (Tauri v2 + PyInstaller-frozen FastAPI sidecar) from a clean checkout.
If you already have the backend and frontend running in cloud mode, the desktop client reuses both — there's no separate React or FastAPI codebase; only the Tauri shell and the PyInstaller packaging are unique to desktop.
| Tool | Why | Install |
|---|---|---|
| Rust (stable) | Tauri host + cargo build | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh then source $HOME/.cargo/env |
cargo-tauri CLI |
cargo tauri dev / cargo tauri build entry points |
cargo install tauri-cli --version '^2.0' --locked |
uv |
Manages the sidecar .venv and installs PyInstaller + orchestrator deps |
brew install uv (macOS) or curl -LsSf https://astral.sh/uv/install.sh | sh |
| Python 3.12 | Sidecar runtime — pinned because every native dep PyInstaller bundles (asyncpg, grpcio, hiredis, PyNaCl) ships wheels for it | uv will fetch this on first uv venv |
| pnpm | React frontend | corepack enable && corepack prepare pnpm@latest --activate (or npm i -g pnpm if corepack is off) |
| Node 20+ | pnpm runtime | nvm install 20 or system package |
Linux-only system deps (Tauri WebKitGTK + tray-icon sysdeps):
sudo apt update
sudo apt install -y \
libwebkit2gtk-4.1-dev libssl-dev libgtk-3-dev \
libayatana-appindicator3-dev librsvg2-dev libsoup-3.0-dev \
pkg-config build-essentialWSL2 works — WebKit renders under a software GL fallback (MESA/EGL warnings on startup are harmless).
desktop/scripts/dev.sh and desktop/scripts/build-all.sh bootstrap a
.venv/ at the repo root via uv on first run and install PyInstaller +
the editable orchestrator deps automatically — there's nothing manual to
do for a fresh checkout. If you want to prep the venv ahead of time
(or run scripts directly against the frozen Python):
uv venv --python 3.12 .venv
uv pip install pyinstaller \
-e packages/tesslate-agent \
-e orchestratorThe .venv/ directory is gitignored. Override the Python pin with
OPENSAIL_PY_VERSION=3.x if you have a specific reason — wheels for the
native deps must exist for that version.
The sidecar is a single --onefile PyInstaller executable that Tauri
spawns as externalBin. dev.sh rebuilds it on demand; if you want to
freeze it manually use the venv Python:
.venv/bin/python desktop/sidecar/build_sidecar.pyOutput lands at
desktop/src-tauri/binaries/tesslate-studio-orchestrator-<target-triple>.
On first build this takes 2–3 minutes; subsequent builds reuse
PyInstaller caches and finish in under a minute.
desktop/scripts/dev.sh rebuilds the sidecar on demand when it's missing
or older than the entrypoint / shared spec.
Fastest path — cargo tauri dev starts the React dev server for you,
spawns the sidecar, and opens the window:
cd desktop/src-tauri
cargo tauri devOr use the repo-level wrapper that handles sidecar freshness and toolchain checks:
./desktop/scripts/dev.shDo not run cargo run directly — it skips the beforeDevCommand
that starts vite, so the window will load http://localhost:5173
against nothing and show a connection-refused error.
If you prefer manual control (useful for frontend-only iteration against a previously-built sidecar):
# terminal 1
pnpm --dir app dev
# terminal 2 — once vite reports "Local: http://localhost:5173"
cd desktop/src-tauri && cargo runRun the frozen binary by itself to confirm the Python side boots independently of Tauri. Useful when debugging migration, seeding, or auth issues:
# clean slate
rm -rf /tmp/tesslate-studio-test
mkdir -p /tmp/tesslate-studio-test
OPENSAIL_HOME=/tmp/tesslate-studio-test \
desktop/src-tauri/binaries/tesslate-studio-orchestrator-x86_64-unknown-linux-gnuExpected output on a healthy boot:
TESSLATE_READY <port> <bearer>on the first stdout line.- Alembic migrations running to head.
- Marketplace seeds landing (≈10 "Created base:" / "Created agent:" lines).
Application startup complete.Uvicorn running on http://127.0.0.1:<port>.
Smoke-test the API with the bearer from the ready line:
# read the bearer out of stdout or grep it from your log
curl -H "Authorization: Bearer $BEARER" "http://127.0.0.1:$PORT/api/desktop/tray-state"A {"runtimes":{...},"running_projects":[],"running_agents":[]} response
confirms the sidecar-bearer loopback auth is wired correctly.
For a local installer that isn't signed — useful for smoke-testing packaging:
./desktop/scripts/build-all.sh --releaseArtifacts land under
desktop/src-tauri/target/release/bundle/{deb,rpm,appimage,dmg,msi}/
depending on your host OS. Each OS's full bundle only runs on that OS
(PyInstaller freezes a host-native Python, and the Tauri installer
formats are OS-specific) — cross-compiling is out of scope for this
repo; CI matrix builds are the intended path.
Signing + notarization (macOS Developer ID, Windows Authenticode, AppImage GPG) is run by the release pipeline, not this script.
Environment variables the desktop sidecar reads at boot:
| Variable | Default | Purpose |
|---|---|---|
OPENSAIL_HOME |
per-OS default (macOS Application Support / Windows AppData / XDG) | Root for projects, SQLite DB, cache, marketplace installs |
DEPLOYMENT_MODE |
desktop (set by entrypoint) |
Orchestrator mode selector |
DATABASE_URL |
sqlite+aiosqlite:///$OPENSAIL_HOME/opensail.db |
Override to point at Postgres for debugging |
REDIS_URL |
"" (empty) |
Set to a real URL to opt back into Redis-backed pubsub + ARQ |
TESSLATE_DESKTOP_BEARER |
minted per launch | Loopback isolation token; Tauri host reads it from the handshake |
TESSLATE_CLOUD_TOKEN |
— | Override cloud bearer without going through the pairing deep-link |
TESSLATE_DESKTOP_HOST |
127.0.0.1 |
Override the bind host (don't) |
TESSLATE_DESKTOP_PORT |
ephemeral | Pin the port (don't — the Tauri host reads whichever the sidecar chose) |
See /orchestrator/app/config.py for the full set and
/orchestrator/app/services/desktop_paths.py for the per-OS $OPENSAIL_HOME
resolution.
"Could not connect to localhost" in the window — you ran cargo run
directly. Use cargo tauri dev or ./desktop/scripts/dev.sh so vite
actually starts.
"cannot open shared object file: libpython3.12.so" — the sidecar
binary was built with --onedir (sibling _internal/ dir). Force a
--onefile rebuild: rm -rf desktop/sidecar/dist desktop/src-tauri/binaries && .venv/bin/python desktop/sidecar/build_sidecar.py.
/tray-state returns 401 — the tray poll is sending the wrong
bearer, or TESSLATE_DESKTOP_BEARER isn't set in the sidecar's
environment. Check the sidecar stdout for the TESSLATE_READY line —
the second field on that line is the bearer both the Tauri host and the
backend compare against.
Alembic fails with "unknown function: now()" — you're running against
a SQLite DB without the connect-time UDF. The orchestrator registers it
in app/database.py automatically when DATABASE_URL starts with
sqlite; if you're hitting this the sidecar is mis-wiring the URL.
PyInstaller warning about app.routers.public — expected. Those
modules import at collect-time and need Settings populated; the
warnings are suppressed but submodule collection skips affected packages.
The orchestrator still runs because we collect_submodules each
top-level package explicitly.
/desktop/CLAUDE.md— desktop package entry point/desktop/src-tauri/CLAUDE.md— Rust host internals/desktop/sidecar/CLAUDE.md— PyInstaller packaging/desktop/scripts/CLAUDE.md— dev + build helpers/docs/desktop/runtimes.md— runtime probe + tray-state contract/docs/desktop/cloud.md— pairing + CloudClient