Reaktor WebView runtime plan
Updated 6 October 2026. Priority targets: macOS, Android, iOS and browser iframes. Windows and Linux remain secondary tracks; their native window checks are deferred.
Reaktor's web runtime hosts local web tools and remote pages inside the existing application. Compose owns the window and workbench. reaktor-web owns browser sessions, content, navigation and the capability-gated JavaScript bridge. Domain state stays in the graph so a renderer can recover without becoming the source of truth.
Desktop uses webview/webview, a Reaktor-owned C++ adapter and C ABI. JNI connects Kotlin/JVM today; the same boundary can integrate with reaktor-ffi later. Android uses Android WebView directly, iOS uses WKWebView directly, and web uses an iframe. Reaktor's implementation languages are Kotlin, TypeScript and C++.
Implementation status
| Area | Implemented |
|---|---|
| Runtime | Graph-owned WebRuntime and WebSession, URL/HTML/bundle content, navigation state, feature reporting, evaluation, focus, history, zoom, profile selection, download notifications and bounded renderer recovery. |
| Local apps | WebApp identity/revision, resource manifests, MIME/CSP, canonical paths, copied asset ownership, size limits and packaged TypeScript bridge assets. Native hosts serve resources directly. |
| Graph bridge | Existing typed Service/OperationDescriptor contracts, schema checks, host authority, request/reply, subscriptions, cancellation, unsubscribe, revocation and navigation epochs. |
| Compose | ReaktorWebView and WebViewState in the existing reaktor-ui module, with JVM, Android, iOS and JS adapters. Headless reaktor-web has no Compose dependency. |
| Platform hosts | macOS C++/JNI, Android, iOS and browser host implementations. Linux C++ compiled in the worker's Docker context. Windows implementation has not been compiled on Windows. |
| Hangar | SystemWebRenderer replaces JCEF through the existing WebEmbedRenderer seam. JCEF dependencies, module flags, Chromium helper packaging and the JCEF-specific JDK selection are removed from the desktop target. |
| Packaging | Pinned upstream archive/checksum/license, native resources in the JVM JAR, ABI version 2, and a successful M1 build of Hangar's app image and DMG. |
Validation is tracked separately from implementation. Android and iOS physical-device probes pass for bundled module imports, typed bridge replies, Unicode and evaluation. Twelve JVM lifecycle/security tests and five TypeScript client tests pass. The four priority target implementations and Compose adapters compile. macOS bundle/bridge/evaluation and two-pane placement, focus, keyboard input, resize, zoom and visibility/modal checks pass. The separate-origin browser probe passes its typed bridge and Unicode input checks. Authenticated Hangar dashboard checks remain separate rollout gates.
Runtime ownership
WebApp describes an application. WebSession activates it within a graph scope. A host attachment places the session in a native view or iframe. These lifetimes are separate: ordinary content changes reuse the host; disposal, graph detach or graph closure releases its sessions and bridge channels.
| Responsibility | Owner |
|---|---|
| App/session/content contracts, policies, bridge and feature reporting | reaktor-web, depending on reaktor-graph-runtime and existing typed services. |
| Native view placement, measured bounds, visibility and Compose disposal | reaktor-ui/web. |
| Browser engine access and platform threading | reaktor-web platform source sets and native/desktop. |
| Generic native loading/transport integration | Future reaktor-ffi reuse; browser-specific C ABI remains in reaktor-web. |
| Tiptap, Monaco, xterm and API-tool assets and domain operations | Consuming modules. The generic runtime does not vendor these applications. |
| Workbench chrome, login notices, zoom controls and external-browser fallback | BestBuds engine and desktop target. Controls use MachineSignal tokens. |
val runtime = graph.Node { WebRuntime(it) }
val bridge = graph.Node {
WebBridge(it, documentService, grants, authorize = ::authorizeDocumentOperation)
}
ReaktorWebView(
runtime = runtime,
content = WebContent.Bundle(documentApp),
state = rememberWebViewState(),
bridge = bridge,
modifier = Modifier.fillMaxSize(),
)
The graph factory matters: constructing a BasicNode alone does not attach it to the graph. Importing or creating the common runtime initializes no browser. A desktop session activates its native library only after an AWT peer exists.
Platform hosts
| Feature | macOS | Android | iOS | Browser iframe |
|---|---|---|---|---|
| URL and HTML | WKWebView | Android WebView | WKWebView | Sandboxed iframe / srcdoc |
| Bundles | In-memory custom scheme | Intercepted HTTPS app origin | In-memory custom scheme | Separately hosted app origin |
| Typed bridge | WK frame provenance | AndroidX origin/main-frame listener | WK frame provenance | Exact engine origin + frame + private MessagePort |
| Evaluation result | Yes | Yes; native provider result semantics | Yes | Unavailable across the iframe boundary |
| Reload / history / zoom | Yes | Yes | Yes | Reload and CSS zoom; history unavailable |
| Storage | Ephemeral/default; named UUID profiles need macOS 14 | Ephemeral/named profiles require provider multi-profile support | Ephemeral/default; named UUID profiles need iOS 17 | Explicit WebProfile.Browser; browser partitioning governs storage/cookies |
| Inspection | Debug WK inspection through Safari | Device developer tools | Debug WK inspection through Safari | Browser developer tools |
| Download handling | Notification, no automatic file write | Notification, no automatic file write | Notification, no automatic file write | Unavailable |
Use session.features, requireFeature and WebApp.requiredFeatures before depending on an engine capability. Unsupported operations throw WebUnavailable. An app requiring a bridge must receive an explicitly bound bridge. openDevTools() cannot promise an in-app inspector where the public platform API only supports external developer tools.
Server/worker targets can own contracts and domain services without an embedded surface. Headless browser automation remains a separate capability. The configured web target is Kotlin/JS; macOS desktop is JVM, while iOS is Kotlin/Native.
Desktop C++ boundary
Upstream is pinned to cbbdee44afff22867de9fd88a9fc8350d9bdd399, with an archive SHA-256 check and MIT license. Versions and checksums live in dependeasy. A small, checked construction hook configures profiles and policy before engine creation; a mismatched upstream hook fails the build.
ABI 2 exposes opaque handles, create/destroy, bounds/visibility/focus, content, evaluation, browser controls, asset registration, feature flags and native callbacks. UTF-8 inputs are borrowed for the call; callbacks own their context until release. JNI converts UTF-16/UTF-8 explicitly so non-BMP characters survive the boundary.
On macOS, upstream installs a container into its supplied window. Reaktor creates an unshown staging window, then reparents the container into the existing AWT content view. The browser-controller handle is the WKWebView; the widget handle is its NSView container. JAWT supplies the public window layer. JNI finds the matching window through the Cocoa view tree and attaches to its content view, matching Compose's window-content coordinates. The session retains that parent until queued native teardown finishes. Pinned macOS backend
Native creation, commands and disposal run on AppKit. Kotlin queues commands on a background thread before AppKit dispatch, so the AWT event thread never waits on AppKit while accessibility may be waiting on AWT. The registry has process lifetime; explicit disposal releases each session on the platform thread.
Windows uses a COM STA, child HWND and WebView2 profile/controller hooks. Linux uses a dedicated GTK loop and an X11 GtkPlug, plus a WebKit extension for engine frame provenance. Native Wayland returns an explicit unsupported result. Windows/Linux real-window behavior and release packaging remain deferred.
Bundle loading
val documentApp = WebApp(
id = "document",
revision = "v1",
assets = documentAssets + WebBridgeAssets,
requiredFeatures = setOf(WebFeature.Bundles, WebFeature.Bridge),
)
Native apps read resource manifests without a localhost HTTP server. Android resolves an internal HTTPS origin such as https://document.reaktor.invalid/v1/index.html. Apple hosts use reaktor-app://document/v1/index.html and map it to the common app identity. reaktor://document/v1/index.html is the logical address, not a claim that every engine has identical URL semantics.
Manifests reject traversal, ambiguous encoded separators and colliding asset paths. Individual assets are limited to 8 MiB and native bundles to 64 MiB. Responses carry MIME, CSP, nosniff and referrer policy. Scripts load from the bundle; network origins must be explicitly declared. Native request interception adds enforcement where the engine exposes it. CSP and operation grants do not constitute complete process-level network isolation.
WK custom schemes do not guarantee HTTPS secure-context or service-worker behavior. Each editor/tool must validate its real module, font, worker, storage and clipboard workload on the engines it supports. A successful HTML probe does not establish Monaco worker compatibility.
Browser deployment
Browser bundles require WebApp.browserUrl on an origin dedicated to that app and revision, separate from the parent application. Serve the same entrypoint, asset manifest and WebBridgeAssets over HTTPS, with the app's CSP and a frame-ancestors policy allowing its host. Select WebProfile.Browser explicitly. Same-origin privileged bundle deployments are rejected.
The bundle iframe enables scripts and same-origin treatment within its separate origin. Remote URLs and inline HTML retain an opaque sandbox and receive no privileged bridge. The SDK reads a fresh connection token and parent origin from the URL fragment; the host accepts only its iframe, the configured engine origin and that token before adopting a private MessagePort. Redirects to another origin cannot acquire the channel. Navigation revokes it; reload creates a new token and epoch.
An opaque sandboxed iframe cannot use an in-memory service-worker asset loader: its active service worker is null. Browser bundles therefore use ordinary website deployment, while native bundles use resource handlers. Service Worker specification
val browserApp = documentApp.copy(
browserUrl = "https://v1.document.webapps.example/index.html",
)
val options = WebViewOptions(profile = WebProfile.Browser)
Bridge authority and protocol
WebBridge adapts existing Service handlers and operation descriptors. It does not introduce another domain RPC registry. Envelopes carry protocol, session, epoch, request ID, operation, contract/version, schema fingerprint, payload, reply correlation and trace ID. Replies serialize the protocol explicitly. The SDK sends envelope fields rather than copying whole operation descriptors.
const result = await window.reaktor.invoke("document.open", { id });
const unsubscribe = await window.reaktor.subscribe("document.changed", refresh);
Authority comes from WebGrant, owned by the host: app/revision, principal, workspace, environment and exact allowed operations. An authorization callback is mandatory. The bridge overwrites request environment and stamps the host grant into request attributes; JavaScript cannot choose its principal or privileges. Changing the grant closes existing connections and cancels their work.
Messages must come from the authorized main document. Native engine frame/origin metadata and browser origin/frame/channel checks precede decoding. Foreign frames, stale epochs, unknown operations and mismatched schemas cannot execute handlers. Navigation and closure revoke subscriptions and pending calls.
Each connection has bounded queues and 32 pending operations, a 64 KiB message limit, JSON depth limit, duplicate-ID protection and 15-second operation deadlines. Cancellation and unsubscribe stop their jobs; rejected/malformed transport closes safely. Renderer recovery reloads at most twice and never replays mutations.
Media, geolocation, popup and file chooser permissions default to denial in native hosts. Native clipboard/file/terminal/deploy access must enter an explicitly authorized host service. A bundled editor does not inherit filesystem, shell, deployment or secret access simply because it runs inside Hangar. Browser sandbox, framing and storage constraints remain visible to callers.
Hangar integration
The desktop target injects SystemWebRenderer into the existing WebEmbedRenderer / WorkbenchEmbedding seam after the workspace graph is ready. MCP-only startup creates no browser. Grafana, Alerts and Radar continue to use their existing pane definitions and external-browser fallback.
The renderer uses a persistent default profile, HTTPS navigation and exact loopback origins for Radar on port 9280. It derives login notices from committed navigation, reapplies zoom to loaded pages, exposes load failures and recreates failed native hosts on retry. Remote dashboards receive no Reaktor bridge.
Native views are rectangular surfaces beside Compose/Skia. Arbitrary Compose clipping, rounded masks and overlapping overlays are not guaranteed. Detach or hide native surfaces while a Compose modal overlaps them; the acceptance fixture exercises this explicitly. Keyboard/IME, accessibility, DPI and multiple-monitor behavior require real-window checks for the supported JDK/engine combination.
Validation and rollout
Builds run on M1 through ~/dev/tools/remote-dev; physical-device and real-window checks run on the UI Mac. Do not start emulators or simulators.
| Check | Evidence / remaining gate |
|---|---|
| Shared lifecycle/security | 12 JVM tests pass: graph disposal, detach/races, typed authority, frames/origins/epochs/schema, duplicate requests, revocation, event unsubscribe, copied assets and bundle-revision navigation. |
| TypeScript | 5 tests pass: envelope shape/correlation, cancellation, subscriptions, message bounds and host revocation. |
| Android | Physical Samsung probe passes bundled ES modules, Unicode typed reply, evaluation, navigation denial and graph disposal. Provider profile support remains feature-probed. |
| iOS | Physical iPhone probe passes bundled ES modules, Unicode typed reply and evaluation. The test-only framework/Swift host stays outside production sources. |
| macOS | Real AWT bundle/typed bridge/evaluation passes. The Compose fixture also passes with Hangar's packaged JVM, production libraries and normal rendering settings, with debugging disabled: two native panes, independent focus/input, Select All, Unicode paste, resize, zoom, hide/show restoration and modal detachment. IME, VoiceOver and multiple-monitor qualification remain release gates. |
| Browser | Hosted iframe probe validates the separate-origin deployment and private bridge. Each production bundle origin must serve correct assets/CSP/framing headers. |
| Hangar | App image and DMG build on M1; selected workbench tests pass. Packaged Hangar renders the Grafana loading page through System WebView. Grafana currently reports its instance is loading; authenticated dashboard navigation and login/restart remain product gates. |
| Windows/Linux | Linux native compile passed. Windows compile and both real-window checks are deferred. |
Measure pane readiness, memory/process count, resize responsiveness, bridge latency and distribution size with reaktor-performance against equivalent JCEF content. No speed or memory improvement is assumed. Add Tiptap, Monaco, xterm and API tools through their consuming modules only after their workload-specific checks pass.