Window Transport
When to use this mode
Section titled “When to use this mode”window.open transport is a good fit when remote runtime needs its own visible workspace:
- extension behaves like a tool panel or editor;
- remote UI should be isolated from host layout and lifecycle;
- you want explicit postMessage boundaries without iframe embedding.
Core model
Section titled “Core model”Host opens a popup window and both sides exchange messages through postMessage.
@remote-ui/rpc can work here through a custom MessageEndpoint wrapper.
Host App -> popup = window.open(...) -> createEndpoint(fromWindow(popup)) -> call.run(receiver.receive, hostBridge)
Popup Remote App -> createEndpoint(fromOpener(window.opener)) -> expose run/releaseHost side example
Section titled “Host side example”import type { MessageEndpoint } from '@remote-ui/rpc'import type { Channel } from '@omnicajs/vue-remote/host'
import { createEndpoint } from '@remote-ui/rpc'import { createReceiver } from '@omnicajs/vue-remote/host'
type RemoteApi = { run(channel: Channel, bridge: { closeRequested(): void }): Promise<void>; release(): void;}
function fromWindow(target: Window, targetOrigin: string): MessageEndpoint { const listeners = new Set<(event: MessageEvent) => void>()
const onMessage = (event: MessageEvent) => { if (event.origin !== targetOrigin) return if (event.source !== target) return for (const listener of listeners) listener(event) }
window.addEventListener('message', onMessage)
return { postMessage(message: any) { target.postMessage(message, targetOrigin) }, addEventListener(event, listener) { if (event === 'message') listeners.add(listener) }, removeEventListener(event, listener) { if (event === 'message') listeners.delete(listener) }, terminate() { window.removeEventListener('message', onMessage) target.close() }, }}
const popup = window.open('/remote/popup.html', 'remote-runtime', 'width=1200,height=800')if (!popup) throw new Error('Popup blocked')
const receiver = createReceiver()const endpoint = createEndpoint<RemoteApi>(fromWindow(popup, window.location.origin))
await endpoint.call.run(receiver.receive, { closeRequested() { popup.close() },})Remote popup side example
Section titled “Remote popup side example”import type { MessageEndpoint } from '@remote-ui/rpc'import type { Channel } from '@omnicajs/vue-remote/host'
import { createEndpoint, release, retain } from '@remote-ui/rpc'import { createRemoteRoot, createRemoteRenderer, defineRemoteComponent } from '@omnicajs/vue-remote/remote'import { defineComponent, h } from 'vue'
function fromOpener(opener: Window, targetOrigin: string): MessageEndpoint { const listeners = new Set<(event: MessageEvent) => void>()
const onMessage = (event: MessageEvent) => { if (event.origin !== targetOrigin) return if (event.source !== opener) return for (const listener of listeners) listener(event) }
window.addEventListener('message', onMessage)
return { postMessage(message: any) { opener.postMessage(message, targetOrigin) }, addEventListener(event, listener) { if (event === 'message') listeners.add(listener) }, removeEventListener(event, listener) { if (event === 'message') listeners.delete(listener) }, terminate() { window.removeEventListener('message', onMessage) window.close() }, }}
const endpoint = createEndpoint<{ run(channel: Channel, bridge: { closeRequested(): void }): Promise<void>; release(): void;}>(fromOpener(window.opener as Window, window.location.origin))
let onRelease = () => {}
const VInspectorBadge = defineRemoteComponent('VInspectorBadge')
endpoint.expose({ async run(channel, bridge) { retain(channel) retain(bridge)
const root = createRemoteRoot(channel, { components: ['VInspectorBadge'] }) await root.mount()
const app = createRemoteRenderer(root).createApp(defineComponent({ setup() { return () => h(VInspectorBadge, { tone: 'neutral', label: 'Popup runtime connected', onClose: () => bridge.closeRequested(), }) }, }))
app.mount(root)
onRelease = () => { release(channel) release(bridge) app.unmount() } }, release() { onRelease() },})Operational caveats
Section titled “Operational caveats”- Popup blocking: open popup from a direct user gesture.
- Origin checks:
always validate
event.originandevent.source. - Lifecycle sync: handle popup close and host unload symmetrically.
- Security model: this is a transport boundary, not full policy governance.