Skip to content
Open
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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@
## 1.75.0 - tbd

- [ai-ide] added an opt-in "Memory" prompt capability that lets agents maintain a wiki-style knowledge base per workspace, stored in the workspace metadata store and exposed to prompts via the new `{{memoryDirectory}}` variable [#17865](https://github.com/eclipse-theia/theia/pull/17865)
- [api-samples] added a split-origin sample: serve the browser SPA from another host, connect with `?backend=`, mint the connection cookie only after `POST /split-origin/session` with a Bearer token, and require that cookie on `/mini-browser` so file preview is not readable without a session
- [core] `Endpoint.backend` optionally points REST and WebSocket URLs at another origin; Socket.IO sends cookies (`withCredentials`) and, when `THEIA_HOSTS` is set, uses that list for CORS with credentials
- [plugin-ext] the frontend plugin worker inherits `Endpoint.backend` from its script URL and loads `hostedPlugin` from that origin
- [filesystem] file upload and download send cookies (`withCredentials` / `credentials: 'include'`) so they work when `Endpoint.backend` is another origin
- [mini-browser] the mini-browser hostname follows `Endpoint` so a remote backend origin reaches file preview
- [core, monaco] fixed Monaco theme CSS, `SelectComponent` dropdown placement, and the OS font class in secondary windows [#17874](https://github.com/eclipse-theia/theia/pull/17874)
- [scm] aligned the history graph with VS Code: ref-role lane and badge colors, a current-commit indicator, and a commit hover rendered from the content the history provider supplies [#17880](https://github.com/eclipse-theia/theia/pull/17880)
- [scm-extra] deprecated `@theia/scm-extra` package [#17882](https://github.com/eclipse-theia/theia/pull/17882)
Expand Down
29 changes: 29 additions & 0 deletions examples/api-samples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,35 @@ The **Telemetry Samples** command category demonstrates reporting events from th

Set `telemetry.telemetryLevel` to `off` to suppress the remote sink, or set `"telemetry.filters": { "sample/console": [] }` to disable it explicitly.

### Split-origin sample

Theia frontend on one origin, Node backend on another (CDN + API).

From the repository root, after `npm run build:browser`:

```sh
npm run start:split
```

Open [http://localhost:8080/](http://localhost:8080/). That page is a stand-in host app at `/`. It embeds Theia from `/theia/`:

```text
http://localhost:8080/theia/?backend=http://localhost:3000&token=S3Cr3t
```

`:3000` is the backend and does not serve the SPA. Open the SPA URL directly if you do not need the host wrapper. Serve the workbench as `/theia/` (trailing slash) so relative assets stay under the prefix.

Preload sets `Endpoint.backend` from `?backend=` (REST and WebSocket) and `POST`s `/split-origin/session` with `Authorization: Bearer` from `?token=` before Socket.IO opens. A real app can assign `Endpoint.backend` at startup and skip the query string. Wrong or missing token: loading indicator, session `POST` returns `401`.

`split-origin/start.cjs` starts the static server and the browser-example backend with:

- `THEIA_SPLIT_ORIGIN=1` — sample backend only: no SPA from Node, CORS for `THEIA_HOSTS`, connection cookie only after a successful session `POST` (WebSocket, file transfer, and mini-browser then require that cookie)
- `THEIA_SPLIT_ORIGIN_TOKEN` — expected Bearer (demo `S3Cr3t`)
- `THEIA_HOSTS=localhost:8080` — allowlist the frontend origin (not authentication)
- `THEIA_WEBVIEW_EXTERNAL_ENDPOINT={{hostname}}` and `THEIA_MINI_BROWSER_HOST_PATTERN={{hostname}}` — keep webview and mini-browser on the backend host so the local demo does not need wildcard DNS

Without `THEIA_SPLIT_ORIGIN`, a normal `npm start` of the browser example is unchanged.

### Sample mock OpenVSX server

These samples contain a mock implementation of an OpenVSX server. This is done
Expand Down
8 changes: 5 additions & 3 deletions examples/api-samples/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"@theia/core": "1.74.0",
"@theia/file-search": "1.74.0",
"@theia/filesystem": "1.74.0",
"@theia/mini-browser": "1.74.0",
"@theia/monaco": "1.74.0",
"@theia/monaco-editor-core": "1.108.201",
"@theia/output": "1.74.0",
Expand All @@ -24,11 +25,14 @@
"@theia/toolbar": "1.74.0",
"@theia/vsx-registry": "1.74.0",
"@theia/workspace": "1.74.0",
"socket.io-client": "^4.8.3",
"zod": "^4.4.3"
},
"theiaExtensions": [
{
"frontend": "lib/browser/api-samples-frontend-module",
"frontendPreload": "lib/browser/api-samples-preload-module",
"secondaryWindow": "lib/browser/api-samples-preload-module",
"backend": "lib/node/api-samples-backend-module"
},
{
Expand All @@ -37,9 +41,6 @@
},
{
"frontendOnly": "lib/browser-only/api-samples-frontend-only-module"
},
{
"frontendPreload": "lib/browser/api-samples-preload-module"
}
],
"keywords": [
Expand All @@ -59,6 +60,7 @@
"src"
],
"scripts": {
"start:split": "node split-origin/start.cjs",
"lint": "theiaext lint",
"build": "theiaext build",
"watch": "theiaext watch",
Expand Down
52 changes: 52 additions & 0 deletions examples/api-samples/split-origin/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Split Frontend and Backend</title>
<style>
* { box-sizing: border-box; }
html, body {
margin: 0;
height: 100%;
font: 14px/1.45 system-ui, sans-serif;
color: #1b2430;
background: #eef1f6;
}
body {
display: flex;
flex-direction: column;
}
header {
flex: none;
margin: 14px 20px 0;
}
header p { margin: 0 0 4px; color: #5c6b7e; }
code { font-size: 12px; background: #fff; padding: 1px 5px; border-radius: 4px; }
main {
flex: 1;
min-height: 0;
margin: 20px;
height: 100%;
border-radius: 10px;
overflow: hidden;
box-shadow: 0 8px 24px rgba(27, 36, 48, 0.16);
}
iframe {
width: 100%;
height: 100%;
border: 0;
display: block;
}
</style>
</head>
<body>
<header>
<p>Host app: <code>__FRONTEND__/</code></p>
<p>Theia static files: <code>__SPA__</code></p>
<p>Iframe loads <code>__SPA__?backend=__BACKEND__&amp;token=__TOKEN__</code> and connects to that backend.</p>
</header>
<main>
<iframe title="Theia" src="__FRONTEND__/theia/?backend=__BACKEND__&amp;token=__TOKEN__"></iframe>
</main>
</body>
</html>
142 changes: 142 additions & 0 deletions examples/api-samples/split-origin/start.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
#!/usr/bin/env node
// *****************************************************************************
// Copyright (C) 2026 Maksim Kachurin and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************

const { spawn } = require('node:child_process');
const { createReadStream, existsSync } = require('node:fs');
const { readFile } = require('node:fs/promises');
const { createServer } = require('node:http');
const { extname, join, normalize, sep } = require('node:path');

const here = __dirname;
const browserDir = join(here, '../../browser');
const frontendDir = join(browserDir, 'lib/frontend');
const port = Number(process.env.THEIA_FRONTEND_PORT ?? 8080);
const backend = process.env.THEIA_BACKEND_URL ?? 'http://localhost:3000';
const token = process.env.THEIA_SPLIT_ORIGIN_TOKEN ?? 'S3Cr3t';
const frontendOrigin = `http://localhost:${port}`;
// Host app at `/`; Theia static files under this prefix (trailing slash in URLs).
const theiaStaticPrefix = '/theia';

const MIME = {
'.css': 'text/css; charset=utf-8',
'.gif': 'image/gif',
'.html': 'text/html; charset=utf-8',
'.ico': 'image/x-icon',
'.js': 'text/javascript; charset=utf-8',
'.json': 'application/json',
'.map': 'application/json',
'.png': 'image/png',
'.svg': 'image/svg+xml',
'.ttf': 'font/ttf',
'.wasm': 'application/wasm',
'.woff': 'font/woff',
'.woff2': 'font/woff2'
};

function resolveUnder(dir, pathname) {
const decoded = decodeURIComponent(pathname.split('?')[0]);
const resolved = normalize(join(dir, decoded));
const root = normalize(dir) + sep;
if (resolved !== normalize(dir) && !resolved.startsWith(root)) {
return undefined;
}
return resolved;
}

if (!existsSync(join(frontendDir, 'index.html'))) {
console.error('Missing examples/browser/lib/frontend. Build first: npm run build:browser');
process.exit(1);
}

const server = createServer((req, res) => {
const url = new URL(req.url ?? '/', frontendOrigin);
const isHost = url.pathname === '/' || url.pathname === '/index.html';
if (isHost && url.searchParams.has('backend')) {
res.writeHead(302, { Location: `${theiaStaticPrefix}/${url.search}` });
res.end();
return;
}
if (isHost) {
readFile(join(here, 'index.html'), 'utf8').then(html => {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(html
.replaceAll('__FRONTEND__', frontendOrigin)
.replaceAll('__SPA__', `${frontendOrigin}${theiaStaticPrefix}/`)
.replaceAll('__BACKEND__', backend)
.replaceAll('__TOKEN__', token));
}).catch(() => {
res.writeHead(500);
res.end();
});
return;
}
if (url.pathname === theiaStaticPrefix) {
res.writeHead(302, { Location: `${theiaStaticPrefix}/${url.search}` });
res.end();
return;
}
if (!url.pathname.startsWith(`${theiaStaticPrefix}/`)) {
res.writeHead(404);
res.end();
return;
}
const rest = url.pathname.slice(theiaStaticPrefix.length);
const pathname = rest === '/' ? '/index.html' : rest;
const filePath = resolveUnder(frontendDir, pathname);
const tryPath = filePath && existsSync(filePath) ? filePath : join(frontendDir, 'index.html');
res.writeHead(200, { 'Content-Type': MIME[extname(tryPath)] ?? 'application/octet-stream' });
const stream = createReadStream(tryPath);
stream.on('error', () => {
if (!res.headersSent) {
res.writeHead(500);
}
res.end();
});
stream.pipe(res);
});

server.listen(port, () => {
console.log(`Host page: ${frontendOrigin}/`);
console.log(`Theia SPA: ${frontendOrigin}${theiaStaticPrefix}/?backend=${backend}&token=${token}`);
const backendProc = spawn('npm', ['run', '-s', 'start'], {
cwd: browserDir,
env: {
...process.env,
THEIA_SPLIT_ORIGIN: '1',
THEIA_SPLIT_ORIGIN_TOKEN: token,
THEIA_HOSTS: `localhost:${port}`,
THEIA_WEBVIEW_EXTERNAL_ENDPOINT: '{{hostname}}',
THEIA_MINI_BROWSER_HOST_PATTERN: '{{hostname}}'
},
stdio: 'inherit'
});
let shuttingDown = false;
const shutdown = () => {
if (shuttingDown) {
return;
}
shuttingDown = true;
backendProc.kill('SIGTERM');
server.close();
};
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
backendProc.on('exit', code => {
server.close();
process.exit(code ?? 0);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,17 @@ import { bindSampleCodeCompletionVariableContribution } from './ai-code-completi
import { bindSamplePreferenceContribution } from './preferences/sample-preferences-contribution';
import { MCPFrontendContribution } from '@theia/ai-mcp-server/lib/browser/mcp-frontend-contribution';
import { SampleFrontendMCPContribution } from './mcp/sample-frontend-mcp-contribution';
import { DefaultSecondaryWindowService } from '@theia/core/lib/browser/window/default-secondary-window-service';
import { FrontendApplicationContribution } from '@theia/core/lib/browser';
import { CommandContribution } from '@theia/core/lib/common';
import { ResolveMcpFrontendContribution } from './mcp/resolve-frontend-mcp-contribution';
import { TelemetrySampleCommandContribution } from './telemetry-sample/telemetry-sample-command-contribution';
import { parseRemoteBackend } from '../common/split-origin/remote-backend';

if (parseRemoteBackend()) {
(DefaultSecondaryWindowService as unknown as { SECONDARY_WINDOW_URL: string }).SECONDARY_WINDOW_URL =
`secondary-window.html${location.search}`;
}

export default new ContainerModule((
bind: interfaces.Bind,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
import { ContainerModule } from '@theia/core/shared/inversify';
import { TextReplacementContribution } from '@theia/core/lib/browser/preload/text-replacement-contribution';
import { TextSampleReplacementContribution } from './preload/text-replacement-sample';
import { installSplitOriginFrontend } from './split-origin/install-split-origin-frontend';

// Must run at module evaluation, before Socket.IO opens
installSplitOriginFrontend();

export default new ContainerModule(bind => {
bind(TextReplacementContribution).to(TextSampleReplacementContribution).inSingletonScope();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// *****************************************************************************
// Copyright (C) 2026 Maksim Kachurin and others.
//
// This program and the accompanying materials are made available under the
// terms of the Eclipse Public License v. 2.0 which is available at
// http://www.eclipse.org/legal/epl-2.0.
//
// This Source Code may also be made available under the following Secondary
// Licenses when the conditions for such availability set forth in the Eclipse
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
// with the GNU Classpath Exception which is available at
// https://www.gnu.org/software/classpath/license.html.
//
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
// *****************************************************************************

import { Endpoint } from '@theia/core/lib/browser/endpoint';
import { WebSocketConnectionSource } from '@theia/core/lib/browser/messaging/ws-connection-source';
import { parseRemoteBackend, RemoteBackend, SPLIT_ORIGIN_SESSION_PATH } from '../../common/split-origin/remote-backend';

/**
* Point {@link Endpoint} at the remote backend and delay Socket.IO until
* `POST /split-origin/session` has set the connection cookie.
*/
export function installSplitOriginFrontend(): void {
const remote = parseRemoteBackend();
if (!remote) {
return;
}
Endpoint.backend = `${remote.origin}${remote.pathname}`;
patchWebSocketConnectionSource(remote);
console.info(`[split-origin] frontend patched to use backend ${remote.origin}`);
}

function patchWebSocketConnectionSource(remote: RemoteBackend): void {
const proto = WebSocketConnectionSource.prototype as unknown as {
openSocket(): void;
};

const originalOpenSocket = proto.openSocket;
proto.openSocket = function (): void {
const start = originalOpenSocket.bind(this);
if (!remote.token) {
console.error('[split-origin] missing ?token=; refusing to open the backend socket');
return;
}
fetch(`${remote.origin}${remote.pathname}${SPLIT_ORIGIN_SESSION_PATH}`, {
method: 'POST',
credentials: 'include',
mode: 'cors',
headers: {
Authorization: `Bearer ${remote.token}`
}
}).then(response => {
if (!response.ok) {
throw new Error(`session auth failed: HTTP ${response.status}`);
}
start();
}).catch(error => {
console.error('[split-origin]', error);
});
};
}
Loading
Loading