Skip to Content

Desktop app

End users should download a release from GitHub Releases and follow the Quickstart. This page explains how the app is built.

The local build and its per-seam local backends — sandbox, Yjs host, blob store, database, engine — are assembled into a single offline desktop app by a Tauri shell that runs the Next app as a bundled Node sidecar (#418, #404). The shell lives in apps/desktop.

The single build switch

Every local backend is selected together in one place, apps/desktop/desktop.env. Each seam still reads its own env var (the switches are independent by design); this profile is where the desktop distribution sets them as a set, so one repository produces both the hosted deployment (none set) and the desktop app:

SCREENPLAY_DESKTOP=1 # emit `output: standalone` NEXT_PUBLIC_SCREENPLAY_LOCAL=1 # strip the multi-user surface NEXT_PUBLIC_YJS_HOST=local # local y-websocket host NEXT_PUBLIC_BASE_PATH= # served at the origin root (no /app proxy) SANDBOX_BACKEND=local # Branches = per-Branch git worktrees on the host # ("worktree", the old value, still accepted) SCREENPLAY_DB=pglite # embedded Postgres BLOB_STORE=local-fs # thumbnails to local disk AGENT_ENGINE=external # chat via the installed CLI's ACP adapter THUMBNAIL_CAPTURER=tauri-webview # SCREENPLAY_ACP_HARNESS=codex # optional: which installed CLI backs agent # chat; unset → defaults to claude-code

The NEXT_PUBLIC_* half is inlined at next build; the seam selectors are read at runtime and re-injected by the shell when it spawns the sidecar.

Lifecycle

Tauri owns the sidecar end-to-end. All of this runs off the main thread, so the window paints a loading screen (the logo, on the app’s own light or dark background) the moment the app opens:

  1. Port — binds 127.0.0.1:0 for a free port and hands it to the sidecar.
  2. Spawn — extracts the bundled tree (while recovering the login shell’s PATH in parallel) and runs node apps/app/server.js with the desktop profile, the machine-specific data dirs (under the OS app-data directory), and the per-install secrets.
  3. Health gate — polls /api/health and only navigates the webview to the local server once it 200s, so first paint never races the boot. A spinner joins the logo only if the boot runs long; if the sidecar exits or never answers, the loading screen says Screenplay couldn’t start instead of spinning forever.
  4. Shutdown — kills and reaps the sidecar on quit (no orphan).

Secrets

ENCRYPTION_KEY (project-preset / env-var encryption at rest) and TERMINAL_AUTH_SECRET are deployment secrets in the hosted app. A desktop install has no deployment, so the shell mints and persists them on first launch next to the app data — stable across restarts, unique per install.

Thumbnails

The desktop build ships no headless Chromium, so the blob store’s thumbnail capture happens in a webview: the sidecar’s TauriWebviewCapturer POSTs each render URL to the shell’s localhost control server (TAURI_CONTROL_URL), which renders it off-screen and returns a PNG via the platform webview’s snapshot API. The local-fs store writes the WebP and serves it back from the sidecar’s own /blobs route.

The same control server snapshots the canvas window itself when a chat’s agent drives a frame and checks a step. It also plays the agent’s clicks and keys as real input to the canvas window, sets your clipboard aside around a step that copies or pastes, and answers a file picker the agent’s click opens with files from the code of the frame’s chat. These act on your screen and clipboard, so the shell mints a token at launch and hands it only to the sidecar (TAURI_CONTROL_TOKEN); a snapshot or input request without it is refused. The agent’s steps reach the canvas over a WebSocket on the local Yjs server’s port, behind the same per-launch secret and Origin check as the Yjs sync connection.

Building

pnpm --filter desktop build:sidecar # next build → src-tauri/resources/sidecar.tar.gz pnpm --filter desktop dev # run it (tauri dev) pnpm --filter desktop build # package Screenplay.app

The sidecar ships as a tarball, not a directory: the Next-traced node_modules keeps pnpm’s peer-dependency symlinks, which Tauri’s resource copy drops — tar preserves them (and the bundled node exec bit). The build also folds in the pieces standalone tracing misses (.next/static, public, the drizzle/local migrations, and node-pty’s native prebuild). See apps/desktop/README.md for the full rationale.

Prerequisites: the Rust + Tauri toolchain and node on PATH.

Releasing

Signed macOS builds are published as desktop-v<version> GitHub Releases with the .dmg attached twice: as Screenplay_<version>_aarch64.dmg, and as Screenplay.dmg so that releases/latest/download/Screenplay.dmg always fetches the newest build. They’re published by pnpm --filter desktop release on a Mac (see Releasing the desktop app). There’s no auto-update yet.

Last updated on