diff --git a/docs/design-docs/studio-apps-and-surfaces.md b/docs/design-docs/studio-apps-and-surfaces.md new file mode 100644 index 0000000000..342b405753 --- /dev/null +++ b/docs/design-docs/studio-apps-and-surfaces.md @@ -0,0 +1,188 @@ +# Studio Apps & Surfaces + +## About this doc + +This document maps the different Studio "surfaces" (desktop, CLI, local web, hosted), the architecture of each, and the shared layers that let them run the same product. It focuses on how the **agentic UI**, the **`@studio/common` backend**, and the **CLI execution engine** are reused across surfaces, and on the convergence between the local web server and the desktop app. + +## Context + +Studio began as a single Electron desktop app. It has since grown a CLI, a cloud-hosted variant, and a local web server, all of which share large amounts of code. Rather than four independent apps, Studio is better understood as **one product rendered through several surfaces**, layered on a common, Electron-free core and a single execution engine (the CLI). + +This doc is the map of that structure. It does not replace the per-feature docs ([cli.md](./cli.md), [sync.md](./sync.md), [custom-domains-and-ssl.md](./custom-domains-and-ssl.md)); it explains how the pieces fit together. + +## The surfaces at a glance + +| Surface | Package(s) | Runtime | UI | Backend | Audience | +| --- | --- | --- | --- | --- | --- | +| **Desktop** | `apps/studio` | Electron (main/preload/renderer) | `apps/ui` (+ legacy renderer) | `@studio/common` in-process + forks the CLI | End users on macOS/Windows/Linux | +| **CLI** | `apps/cli` | Node (via Electron's `ELECTRON_RUN_AS_NODE`, or standalone npm) | none (terminal) | WordPress Playground / PHP-WASM | Power users, scripts, and the other surfaces | +| **Local web** (`studio ui`) | `apps/local` + `apps/ui` | Node (Express + SSE), bundled into the CLI | `apps/ui` (`dist-local`) in the browser | `@studio/common` in-process + forks the CLI | Users who want the agentic UI in a browser, on their own machine | +| **Hosted** | `apps/hosted` + `apps/ui` | Node server in the cloud | `apps/ui` (`dist-hosted`) in the browser | Cloud backend (WordPress.com / Telex sandbox agent) | Cloud product; no local machine | + +Two of these surfaces run **on the user's machine and own real local WordPress sites** (desktop and local web). One is a pure **execution engine** that the others delegate to (CLI). One targets a **remote backend** with a deliberately reduced scope (hosted). + +## The layers + +``` + ┌─────────────────────────────────────────────────────────────┐ + │ apps/ui │ Shared agentic UI + │ React app + Connector abstraction + capabilities │ (one codebase) + └───────────────┬───────────────┬───────────────┬─────────────┘ + ipc │ local │ hosted │ ← 3 connectors (transport) + ┌───────────────┴──┐ ┌──────────┴─────┐ ┌───────┴──────────┐ + │ apps/studio │ │ apps/local │ │ apps/hosted │ Per-surface shells + │ (Electron/IPC) │ │ (Express/SSE) │ │ (cloud server) │ + └───────────────┬──┘ └──────────┬─────┘ └──────────────────┘ + │ │ + ┌───────────────┴───────────────┴─────────────────────────────┐ + │ @studio/common (tools/common) │ Shared, Electron-free + │ ai/sessions/* sites/* lib/* (business logic) │ business logic + └───────────────┬─────────────────────────────────────────────┘ + │ forks the binary (createCliRunner) + ┌───────────────┴─────────────────────────────────────────────┐ + │ apps/cli │ Execution engine + │ WordPress Playground / PHP-WASM, site lifecycle │ (`studio` command) + └─────────────────────────────────────────────────────────────┘ +``` + +Reading the stack bottom-up: + +1. **`apps/cli` — the execution engine.** Everything that actually boots WordPress (Playground / PHP-WASM), creates/starts/stops sites, imports/exports, and runs the agent lives here. It is invoked directly as the `studio` command, and it is also the thing every machine-local surface delegates to. +2. **`@studio/common` (`tools/common`) — shared business logic.** Transport-agnostic, Electron-free TypeScript: session management, site operations, snapshots, sync, the REST proxy, app detection, OAuth URL building, etc. It is the layer that makes the desktop and the local web server run *the same code*. +3. **Per-surface shells.** Thin adapters that expose `@studio/common` over a transport: `apps/studio` over Electron IPC, `apps/local` over HTTP/SSE, `apps/hosted` over its cloud API. +4. **`apps/ui` — the shared agentic UI.** One React application that talks to whichever shell it's running against through a single **Connector** seam. + +## The shared UI layer (`apps/ui`) + +`apps/ui` is a single React app (React 19, TanStack Query/Router, `@wordpress/ui` + `@wordpress/dataviews`, themed via `@wordpress/theme`'s `ThemeProvider`). It is rendered by **three** surfaces — desktop, local, hosted — without forking. + +It reaches its backend exclusively through the **`Connector`** interface (`apps/ui/src/data/core/types.ts`). The UI never imports IPC, `fetch`, or SSE directly; it calls `connector.getSites()`, `connector.createSite()`, `connector.authenticate()`, etc. Each surface provides one implementation: + +| Connector | File | Transport | Used by | +| --- | --- | --- | --- | +| `ipc` | `connectors/ipc/` | `window.ipcApi.*` / `ipcListener` (Electron contextBridge) | Desktop | +| `local` | `connectors/local/` | HTTP + SSE to the local server | `studio ui` | +| `hosted` | `connectors/hosted/` | Cloud API | Hosted | + +The connectors are intentionally **not** unified behind capability negotiation; each is honest to its own backend. Duplication between `local` and `hosted` is accepted so that machine-local features can grow in `local` without risk to the cloud product. The only shared connector helper is `connectors/unsupported-error.ts`. + +### Capabilities + +Where surfaces genuinely differ (native dialogs, OS integration, preview annotation), the UI branches on a small declarative descriptor instead of sniffing the environment: + +```ts +interface ConnectorCapabilities { + nativeFolderPicker: boolean; // native folder dialog vs. editable path field + nativeSaveDialog: boolean; // native Save-As vs. browser download + openInOS: boolean; // open folder/editor/terminal + app detection + annotatePreview: boolean; // inject the annotation inspector into the preview +} +``` + +For example, the create-site form renders an editable path field when `nativeFolderPicker` is false; exports stream to a browser download when `nativeSaveDialog` is false; the preview's **Annotate** control is hidden entirely when `annotatePreview` is false (a cross-origin `