Why
In a desktop app that uses zero-native to host a sidecar process (Node, Python, etc.) and points the WebView at that sidecar's ephemeral loopback URL, the sidecar can crash mid-session. A robust shell respawns the sidecar, captures the new port, and needs to reload the WebView to the new URL.
Today there is no thread-safe API to trigger that reload. The watcher thread that detects the sidecar exit cannot call into the platform-specific runtime to navigate the WebView, because the AppKit event loop (and the equivalent on other platforms) is single-threaded and has no documented thread-safe inject mechanism.
The downstream consumer (pi-zero — porting a Gmail/email assistant from Electron) currently has a known limitation: after sidecar respawn, the WebView shows the old URL (connection error or stale content) until the user takes a navigation action. This makes restart-once a half-recovery instead of a transparent one.
What
Add a thread-safe API to Runtime:
pub fn reloadWebViewUrl(self: *Runtime, new_url: []const u8) !void;
The function MUST be safe to call from any thread. The implementation should:
- Atomically stash the new URL in a runtime-owned, lock-protected
pending_reload field.
- Wake the platform event loop (e.g.,
[NSApp postEvent:atStart:] on macOS, g_main_context_invoke on GTK, PostMessage on Win32).
- The next event-loop iteration drains
pending_reload and performs the navigation on the platform-correct thread.
Optional refinement: also expose reloadWebView(self: *Runtime) !void (no URL change, just a reload) for the cookie-mid-session-invalidation case.
Use case (concrete)
pi-zero's Phase 1 (https://github.com/dennisonbertram/pi-zero/commit/cdbe6b4) implements a sidecar lifecycle with restart-once policy. The watcher thread in src/main.zig is structured roughly as:
fn watcher(args: *WatcherArgs) void {
while (true) {
// wait for sidecar exit
const exit = waitpid(args.handle.pid, ...);
if (!args.policy.shouldRestart(now())) {
// load connection-lost fallback — currently NOT possible from this thread
// runtime.reloadWebViewUrl("zero://inline/connection-lost.html") would solve this
return;
}
// respawn
const new = sidecar.Spawn.spawn(...) catch ...;
args.policy.recordRestart(now());
// would also call runtime.reloadWebViewUrl(new_url) here
}
}
The pi-zero shell currently mitigates this by leaving the WebView pointing at the old (now-respawned) port — which sometimes works (same port, different process) but generally doesn't because port-binding uses 127.0.0.1:0 ephemeral.
Acceptance criteria
Out of scope
- Reloading multiple windows simultaneously (single primary window only for v1).
- Custom protocol scheme handling beyond
http://, https://, zero:// (already handled).
- Cookie / storage clearing on reload — that's a separate API.
Workaround in the meantime
pi-zero is shipping Phase 1 with this limitation documented. Phase 2's frontend (Next.js) can detect the WebSocket / fetch failure pattern and self-navigate via window.location.replace(<new-url>) — but that requires the frontend to know the new URL, which means the bridge surface needs to expose a getCurrentSidecarUrl() call. That's a fragile workaround compared to a framework-level reload API.
References
Why
In a desktop app that uses zero-native to host a sidecar process (Node, Python, etc.) and points the WebView at that sidecar's ephemeral loopback URL, the sidecar can crash mid-session. A robust shell respawns the sidecar, captures the new port, and needs to reload the WebView to the new URL.
Today there is no thread-safe API to trigger that reload. The watcher thread that detects the sidecar exit cannot call into the platform-specific runtime to navigate the WebView, because the AppKit event loop (and the equivalent on other platforms) is single-threaded and has no documented thread-safe inject mechanism.
The downstream consumer (pi-zero — porting a Gmail/email assistant from Electron) currently has a known limitation: after sidecar respawn, the WebView shows the old URL (connection error or stale content) until the user takes a navigation action. This makes restart-once a half-recovery instead of a transparent one.
What
Add a thread-safe API to
Runtime:The function MUST be safe to call from any thread. The implementation should:
pending_reloadfield.[NSApp postEvent:atStart:]on macOS,g_main_context_invokeon GTK,PostMessageon Win32).pending_reloadand performs the navigation on the platform-correct thread.Optional refinement: also expose
reloadWebView(self: *Runtime) !void(no URL change, just a reload) for the cookie-mid-session-invalidation case.Use case (concrete)
pi-zero's Phase 1 (https://github.com/dennisonbertram/pi-zero/commit/cdbe6b4) implements a sidecar lifecycle with restart-once policy. The watcher thread in
src/main.zigis structured roughly as:The pi-zero shell currently mitigates this by leaving the WebView pointing at the old (now-respawned) port — which sometimes works (same port, different process) but generally doesn't because port-binding uses 127.0.0.1:0 ephemeral.
Acceptance criteria
Runtime.reloadWebViewUrl(new_url)is callable from any thread without panicking or causing UB.WKWebView.load:on the main thread within ~1 frame of the call.webkit_web_view_load_urion the GTK main thread.Out of scope
http://,https://,zero://(already handled).Workaround in the meantime
pi-zero is shipping Phase 1 with this limitation documented. Phase 2's frontend (Next.js) can detect the WebSocket / fetch failure pattern and self-navigate via
window.location.replace(<new-url>)— but that requires the frontend to know the new URL, which means the bridge surface needs to expose agetCurrentSidecarUrl()call. That's a fragile workaround compared to a framework-level reload API.References