> ## Documentation Index
> Fetch the complete documentation index at: https://docs.qomicex.top/llms.txt
> Use this file to discover all available pages before exploring further.

# L4 remote WebView

## L4 remote WebView

L4 is the **highest isolation level** for plugin Rendering (Render): the plugin page runs in its own **Tauri WebviewWindow** (a separate renderer process), isolated from the main interface at the process level. Even if a plugin gets stuck in a busy loop, only its own window freezes; the **main UI is unaffected** (it can still respond, render, and close).

This suits **heavy-UI, unrestrained** plugins (a complex editor, a game UI enhancement, a long-running background page). Inside an iframe (L2) such plugins still share the same WebView with the main interface, so one infinite loop freezes the whole Launcher.

## Manifest declaration

L4 Rendering turns on when **either** condition holds:

```json theme={null}
{
  "render": "webview",
  "layers": ["l4"]
}
```

* `"render": "webview"`, **or**
* the `layers` array contains `"l4"`

Priority (`plugin-loader.tsx` `useWebview`): `render === 'webview' || layers.includes('l4')`. A plugin that declares l4 must still provide `entry.frontend`, or it never activates and renders.

## How the cross-window Bridge (bridge)works

An l4 window is not an ordinary child window opened with `window.open`; it is a **separate WebView window** created through Tauri IPC `WebviewWindow` (`opener` is `null`), so you cannot rely on `window.opener.postMessage`. Cross-window communication uses the **Tauri event system** (`emitTo` / `listen`; `core:event` is allowed by default):

```text theme={null}
┌─────────────── main window (main) ───────────────┐        ┌──────── l4 window (plugin-webview-{id}) ────────┐
│ plugin-loader: createRemoteWebview()          │        │ App lightweight mode -> PluginWebviewPage             │
│   ├─ open WebviewWindow(/plugins/p/:id?…)     │──────▶ │   ├─ iframe (sandbox) loads plugin HTML + bridge script    │
│   └─ initL4Bridge() listens for plugin-l4-api-call   │        │   └─ plugin __plugin_api_call message             │
│        handleApiCall (permission check + run)            │◀───────│        ├─ emitTo('main', plugin-l4-api-call) │
│        └─ emitTo(l4label, plugin-l4-api-response) ───▶ │        └─ postMessage back to the plugin iframe          │
└──────────────────────────────────────────────┘        └──────────────────────────────────────────────┘
```

* **Activation (Activate)**: when the plugin is enabled, `createRemoteWebview` opens the window `plugin-webview-{id}` (1200×800 by default) at the plugin page URL with a `pluginWebview=1` marker.
* **Lightweight startup**: once `App.tsx` sees the marker, it **skips the main Layout / routes / automatic plugin Activation** and renders only `PluginWebviewPage`.
* **Rendering**: `PluginWebviewPage` pulls the plugin frontend assets, injects theme variables and the Bridge script through `buildPluginDoc`, and renders the result into an iframe with `sandbox="allow-scripts"`.
* **API forwarding**: an `__PLUGIN_API__` call inside the iframe emits an `__plugin_api_call` message → the page forwards it as the Tauri event `emitTo('main', 'plugin-l4-api-call', {pluginId, id, method, args})`.
* **Main window execution**: `initL4Bridge` in the main window receives the event → it calls the same `handleApiCall` used for iframes (`METHOD_PERMISSIONS` Permission (permissions)check + `executePluginMethod`) → the response goes back through `emitTo(l4label, 'plugin-l4-api-response')` → `PluginWebviewPage` `listen`s and `postMessage`s it back to the iframe unchanged.
* **Stream abort**: `__plugin_api_abort` → the `plugin-l4-api-abort` event → `abortStream` in the main window.

Message bodies reuse the postMessage protocol from sandbox.ts (`{type,id,method,args}` / `{type,id,result,error}`); **only the transport changes to cross-window events**, so plugins see no difference.

## Non-Tauri fallback

l4 depends on the Tauri WebViewWindow, so it automatically falls back to an iframe (l2) outside Tauri:

* **Plain browser / Vite dev** (no `window.__TAURI_INTERNALS__`): `createRemoteWebview` goes straight to `createSandbox` (iframe Sandbox).
* **Window creation failure** (`tauri://error`): the log reports a fallback to an iframe, and it falls back to `createSandbox`.

Windows are **deduplicated** by the `plugin-webview-{id}` label (an existing one is focused, not rebuilt); when you close a window manually, the instance table is cleaned up so deduplication does not misfire.

## Trade-offs vs. iframe (l2) / WASM (l3)

| Dimension | iframe (l2) | WASM (l3) | Remote WebView (l4) |
| - | - | - | - |
| Execution location | iframe inside the main window (opaque origin) | Backend Wasmtime Sandbox | Separate WebViewWindow / renderer process |
| Isolation strength | DOM/CSS isolation | No DOM, pure computation | **Process-level isolation**; crashes and infinite loops stay contained |
| busy-loop impact | Freezes the main interface | No UI involved | Freezes only its own window |
| API calls | postMessage Bridge | Host API gating | Tauri events → main-window Bridge (same Permission checks) |
| Best for | Regular UI plugins (the default recommendation) | Protocol parsing, agents, complex logic | Heavy-UI / unrestrained plugins that need strong isolation |
| Prerequisites | None | `plugin.wasm` in the package | A Tauri desktop environment (falls back to an iframe outside Tauri) |

* Regular UI plugins → keep the default iframe (l2); do not declare l4 casually.
* Plugins with heavy computation or Rendering that may block the UI → move the heavy work into WASM (l3) first; use l4 only when "the entire page must be isolated in its own process".
* Need access to the main interface DOM → iframes cannot do it, so use `"render": "inline"` (same window as the main interface, weakest isolation; use with care).

## Known boundaries (the code is authoritative)

* Cross-plugin calls through `registerMethod` / `callPlugin`: l4 windows are relayed through `plugin-registry.ts` in the main window; directly using the registry from the l4 side is marked TODO (see ADR-054), so prefer the main-window registry for plugin-to-plugin calls.
* Theme push: l4 windows have no full theme pipeline; the current theme variables (`__qomicex_theme`) are pushed once when the iframe fires `onload`, and new values after a theme switch are not synced automatically (the code is authoritative).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.