Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions apps/hosted/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@
that drives Studio's agent from a browser. It exposes the same capabilities the
desktop app reaches over IPC, but over HTTP, so the portable `apps/ui` renderer
can talk to it through the **web connector**
(`apps/ui/src/data/core/connectors/web`).
(`apps/ui/src/data/core/connectors/hosted`).

Unlike the desktop app and CLI, this targets a hosted deployment — WordPress.com
/ Telex APIs and a server-side agent sandbox — not a local WordPress install. It
deliberately depends on nothing in `apps/cli`.

```
npm run build:web --workspace=apps/ui # once, or after UI changes
npm run build:hosted --workspace=apps/ui # once, or after UI changes
npm run build --workspace=apps/hosted # build the server bundle
npm run start --workspace=apps/hosted # listens on 127.0.0.1:8088 (STUDIO_WEB_SERVER_PORT)
```
Expand Down
8 changes: 4 additions & 4 deletions apps/hosted/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,14 +328,14 @@ app.use( '/api', api );

// --- Web UI ------------------------------------------------------------------

// Serve the built browser UI (apps/ui `npm run build:web`) so the server is the
// Serve the built browser UI (apps/ui `npm run build:hosted`) so the server is the
// only process needed: API and SPA share one origin. When the build output
// isn't there (API-only usage, or UI served by the Vite dev server on :5300),
// the server still works and the startup message says how to get the UI.
const uiDist =
process.env.STUDIO_WEB_UI_DIST ??
path.resolve( path.dirname( fileURLToPath( import.meta.url ) ), '../../ui/dist-web' );
const uiIndex = path.join( uiDist, 'index.web.html' );
path.resolve( path.dirname( fileURLToPath( import.meta.url ) ), '../../ui/dist-hosted' );
const uiIndex = path.join( uiDist, 'index.hosted.html' );
const hasUi = existsSync( uiIndex );
if ( hasUi ) {
app.use( express.static( uiDist ) );
Expand Down Expand Up @@ -374,7 +374,7 @@ const server = app.listen( port, '127.0.0.1', () => {
if ( ! hasUi ) {
console.log( `No web UI build found at ${ uiDist }.` );
console.log(
`Build it with \`npm run build:web --workspace=apps/ui\`, or run the dev server with \`npm run dev:web --workspace=apps/ui\` and open http://localhost:5300.`
`Build it with \`npm run build:hosted --workspace=apps/ui\`, or run the dev server with \`npm run dev:hosted --workspace=apps/ui\` and open http://localhost:5300.`
);
console.log( '' );
}
Expand Down
4 changes: 2 additions & 2 deletions apps/ui/index.web.html → apps/ui/index.hosted.html
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Studio Web</title>
<title>Studio Hosted</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.web.tsx"></script>
<script type="module" src="/src/main.hosted.tsx"></script>
</body>
</html>
6 changes: 3 additions & 3 deletions apps/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@
"type": "module",
"scripts": {
"dev": "vite",
"dev:web": "STUDIO_TARGET=web vite",
"dev:hosted": "STUDIO_TARGET=hosted vite",
"build": "vite build",
"build:web": "STUDIO_TARGET=web vite build",
"build:hosted": "STUDIO_TARGET=hosted vite build",
"lint": "eslint src",
"typecheck": "tsc -p tsconfig.json --noEmit",
"preview": "vite preview",
"preview:web": "STUDIO_TARGET=web vite preview"
"preview:hosted": "STUDIO_TARGET=hosted vite preview"
},
"dependencies": {
"@base-ui/react": "^1.3.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import type {
} from '../../types';
import type { AgentRunEvent } from '@studio/common/ai/agent-events';

export interface WebConnectorOptions {
// Base URL of the Studio Web backend (`apps/hosted`), e.g. http://localhost:8088.
export interface HostedConnectorOptions {
// Base URL of the Studio hosted backend (`apps/hosted`), e.g. http://localhost:8088.
apiBaseUrl: string;
}

Expand Down Expand Up @@ -56,7 +56,7 @@ type ServerEvent =
* return benign defaults (so mount-time queries don't throw) or throw
* `WebUnsupportedError` for user-triggered actions.
*/
export function createWebConnector( { apiBaseUrl }: WebConnectorOptions ): Connector {
export function createHostedConnector( { apiBaseUrl }: HostedConnectorOptions ): Connector {
// The backend namespaces its API under /api so the SPA's real-path routes
// (also /sessions/:id, /sites/:id) can share the same origin.
const base = `${ apiBaseUrl.replace( /\/$/, '' ) }/api`;
Expand Down
12 changes: 6 additions & 6 deletions apps/ui/src/main.web.tsx → apps/ui/src/main.hosted.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import { StrictMode } from 'react';
import { createRoot } from 'react-dom/client';
import { App } from '@/app';
import { persistPromise } from '@/data/core';
import { createWebConnector } from '@/data/core/connectors/web';
import { createHostedConnector } from '@/data/core/connectors/hosted';
import type { Connector } from '@/data/core';

// Web entry point. Identical to `main.tsx` except it wires the HTTP/SSE web
// Web entry point. Identical to `main.tsx` except it wires the HTTP/SSE hosted
// connector instead of the Electron IPC connector, so the same React app runs
// in a plain browser tab against the Studio Web backend (`apps/hosted`).
// in a plain browser tab against the Studio hosted backend (`apps/hosted`).

async function loadTranslations( connector: Connector ) {
const { locale } = await connector.getUserPreferences();
Expand All @@ -23,22 +23,22 @@ async function loadTranslations( connector: Connector ) {
}

function getDefaultApiBaseUrl(): string {
// Production builds are served by the Studio Web backend itself, so the API is
// Production builds are served by the Studio hosted backend itself, so the API is
// same-origin. The Vite dev server (:5300) is a separate origin and targets
// the backend's default port instead.
return import.meta.env.DEV ? 'http://localhost:8088' : window.location.origin;
}

async function bootstrap() {
const connector = createWebConnector( {
const connector = createHostedConnector( {
apiBaseUrl: import.meta.env.VITE_STUDIO_API_URL ?? getDefaultApiBaseUrl(),
} );

await Promise.all( [ connector.init?.(), loadTranslations( connector ), persistPromise ] );

createRoot( document.getElementById( 'root' )! ).render(
<StrictMode>
{ /* Studio Web stays on the agentic UI; it doesn't use the desk/agentic
{ /* Studio hosted stays on the agentic UI; it doesn't use the desk/agentic
mode switcher. */ }
<App connector={ connector } forcedMode="classic" />
</StrictMode>
Expand Down
24 changes: 12 additions & 12 deletions apps/ui/vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,18 +18,18 @@ const directDeps = Object.entries( pkg.dependencies ?? {} )
.filter( ( [ , version ] ) => ! version.startsWith( 'file:' ) )
.map( ( [ name ] ) => name );

// Web target (`STUDIO_TARGET=web`) builds a standalone browser app wired to the
// HTTP/SSE web connector. It uses a separate entry/output/port so the default
// Hosted target (`STUDIO_TARGET=hosted`) builds a standalone browser app wired to
// the HTTP/SSE hosted connector. It uses a separate entry/output/port so the default
// Electron-renderer build (`dist/`, port 5200) stays byte-for-byte unchanged.
const isWeb = process.env.STUDIO_TARGET === 'web';
const isHosted = process.env.STUDIO_TARGET === 'hosted';

// In dev, Vite serves the root `index.html` (which loads the Electron entry,
// `main.tsx`) for every SPA navigation, regardless of `build` input options.
// Serve `index.web.html` instead for any document navigation (`/`, `/sites`,
// `/sessions/:id`, …) so the web entry + web connector load and client-side
// Serve `index.hosted.html` instead for any document navigation (`/`, `/sites`,
// `/sessions/:id`, …) so the hosted entry + hosted connector load and client-side
// routing/refresh works. Module and asset requests pass through untouched.
const webDevEntryPlugin: Plugin = {
name: 'studio-web-dev-entry',
const hostedDevEntryPlugin: Plugin = {
name: 'studio-hosted-dev-entry',
apply: 'serve',
configureServer( server ) {
server.middlewares.use( ( req, _res, next ) => {
Expand All @@ -41,15 +41,15 @@ const webDevEntryPlugin: Plugin = {
pathname.startsWith( '/node_modules/' ) ||
pathname.includes( '.' );
if ( accept.includes( 'text/html' ) && ! isInternal ) {
req.url = '/index.web.html';
req.url = '/index.hosted.html';
}
next();
} );
},
};

export default defineConfig( {
plugins: [ react(), dsTokenFallbacks(), ...( isWeb ? [ webDevEntryPlugin ] : [] ) ],
plugins: [ react(), dsTokenFallbacks(), ...( isHosted ? [ hostedDevEntryPlugin ] : [] ) ],
css: {
postcss: {
plugins: [ dsTokenFallbacksPostcss ],
Expand All @@ -74,12 +74,12 @@ export default defineConfig( {
include: directDeps,
},
server: {
port: isWeb ? 5300 : 5200,
port: isHosted ? 5300 : 5200,
},
build: {
outDir: isWeb ? 'dist-web' : 'dist',
outDir: isHosted ? 'dist-hosted' : 'dist',
rolldownOptions: {
input: resolve( __dirname, isWeb ? 'index.web.html' : 'index.html' ),
input: resolve( __dirname, isHosted ? 'index.hosted.html' : 'index.html' ),
},
},
} );