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
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ jobs:
- run: zig build test-gpu-components-smoke
env:
NATIVE_SDK_SMOKE_BUDGET_MS: "1500"
NATIVE_SDK_INPUT_LATENCY_BUDGET_MS: "500"

macos-gpu-perf:
name: macOS GPU Perf
Expand Down
5 changes: 5 additions & 0 deletions packages/core/compile-surface/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,7 @@ export type CmdData =
readonly value: number;
}
| { readonly op: "window_show"; readonly label: string }
| { readonly op: "webview_navigate"; readonly label: string; readonly url: Uint8Array }
| { readonly op: "window_hide"; readonly label: string }
| { readonly op: "dock_presence"; readonly visible: boolean }
| { readonly op: "quit_app" }
Expand Down Expand Up @@ -981,6 +982,10 @@ export const Cmd = {
return { op: "window_show", label };
},

navigateWebView(label: string, url: Uint8Array): CmdData {
return { op: "webview_navigate", label, url };
},

hideWindow(label: string): CmdData {
return { op: "window_hide", label };
},
Expand Down
5 changes: 5 additions & 0 deletions packages/core/sdk/core.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -479,6 +479,10 @@ export type Cmd<M extends Msgish> = {
} | {
readonly op: "window_show";
readonly label: string;
} | {
readonly op: "webview_navigate";
readonly label: string;
readonly url: Uint8Array;
} | {
readonly op: "window_hide";
readonly label: string;
Expand Down Expand Up @@ -615,6 +619,7 @@ export declare const Cmd: {
videoSetMuted(key: string, muted: boolean): Cmd<never>;
videoSetLoop(key: string, loop: boolean): Cmd<never>;
showWindow(label: string): Cmd<never>;
navigateWebView(label: string, url: Uint8Array): Cmd<never>;
hideWindow(label: string): Cmd<never>;
setDockPresence(visible: boolean): Cmd<never>;
launchAtLoginStatus<M extends Msgish>(route: RequestRoute<M>): Cmd<M>;
Expand Down
13 changes: 13 additions & 0 deletions packages/core/sdk/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,9 @@
// "Open" consequence; also restores a
// minimized window. An unknown label is a
// no-op.
// Cmd.navigateWebView(label, url)
// navigate a declared child WebView in the
// main window (fire-and-forget).
// Cmd.hideWindow(label) order a live window out while retaining
// its views; showWindow is the inverse.
// Cmd.setDockPresence(visible) show or remove the app from the macOS
Expand Down Expand Up @@ -1344,6 +1347,7 @@ export type Cmd<M extends Msgish> =
readonly value: number;
}
| { readonly op: "window_show"; readonly label: string }
| { readonly op: "webview_navigate"; readonly label: string; readonly url: Uint8Array }
| { readonly op: "window_hide"; readonly label: string }
| { readonly op: "dock_presence"; readonly visible: boolean }
| { readonly op: "quit_app" }
Expand Down Expand Up @@ -1999,6 +2003,15 @@ export const Cmd = {
return { op: "window_show", label };
},

/// Navigate a declared child WebView in the main window. The runtime
/// validates the label, rejects the main WebView, and applies the same
/// navigation origin policy used by declarative WebView updates. The
/// command is fire-and-forget; invalid or denied requests do not crash
/// the update loop. Passing the current URL again forces a reload.
navigateWebView(label: string, url: Uint8Array): Cmd<never> {
return { op: "webview_navigate", label, url };
},

/// Hide a live window without closing it. Its views and native identity
/// remain intact; `showWindow` brings it back. Unknown labels no-op.
hideWindow(label: string): Cmd<never> {
Expand Down
74 changes: 74 additions & 0 deletions src/runtime/effects.zig
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ const credentials_store = @import("credentials_store.zig");
const file_access = @import("file_access.zig");
const pty_transport = @import("pty.zig");

const effects_log = std.log.scoped(.zero_effects);

/// Maximum in-flight effects (spawn slots / worker threads).
pub const max_effects: usize = 16;
/// Record-store effects have their own capacity: a large batch or a busy
Expand Down Expand Up @@ -378,6 +380,17 @@ pub const WindowActionBinding = struct {
quit_fn: *const fn (context: *anyopaque) bool,
};

/// Type-erased handle for navigating a declared child WebView from a
/// TypeScript core command. The target window is supplied by `UiApp` and is
/// refreshed whenever the canvas window identity changes; the callback uses
/// the runtime's existing `updateView` path so label, target, URL-policy, and
/// platform validation stay centralized.
pub const WebViewActionBinding = struct {
context: *anyopaque,
window_id: platform.WindowId,
navigate_fn: *const fn (context: *anyopaque, window_id: platform.WindowId, label: []const u8, url: []const u8) bool,
};

/// Runtime-owned platform-service entry points used by the intrinsic
/// `native-sdk.*` named commands. Binding the Runtime methods—not the raw
/// PlatformServices table—keeps validation and external-link policy in the
Expand Down Expand Up @@ -470,6 +483,34 @@ pub const WindowActionState = struct {
}
};

/// The WebView-navigation mirror records the last fire-and-forget request so
/// fake execution and session replay remain observable without a platform
/// WebView. The live callback is still invoked in real mode.
pub const WebViewActionState = struct {
navigate_count: u32 = 0,
label_buffer: [platform.max_webview_label_bytes]u8 = @splat(0),
label_len: usize = 0,
url_buffer: [platform.max_webview_url_bytes]u8 = @splat(0),
url_len: usize = 0,

pub fn label(self: *const WebViewActionState) []const u8 {
return self.label_buffer[0..self.label_len];
}

pub fn url(self: *const WebViewActionState) []const u8 {
return self.url_buffer[0..self.url_len];
}

fn record(self: *WebViewActionState, requested_label: []const u8, requested_url: []const u8) void {
const label_len = @min(requested_label.len, self.label_buffer.len);
@memcpy(self.label_buffer[0..label_len], requested_label[0..label_len]);
self.label_len = label_len;
const url_len = @min(requested_url.len, self.url_buffer.len);
@memcpy(self.url_buffer[0..url_len], requested_url[0..url_len]);
self.url_len = url_len;
}
};

/// How a spawn's stdout comes back. `.lines` streams each line as an
/// `on_line` Msg as it arrives (the default; long-running streams).
/// `.collect` accumulates whole stdout — single-line JSON far beyond the
Expand Down Expand Up @@ -4854,6 +4895,10 @@ pub fn Effects(comptime Msg: type) type {
/// by `UiApp` alongside the services — the seam behind
/// app-drawn window controls (loop-thread only).
window_actions: ?WindowActionBinding = null,
/// The runtime's declared child-WebView navigation seam. Unlike
/// window actions, the target window id is refreshed by UiApp when
/// the canvas window identity becomes known.
webview_actions: ?WebViewActionBinding = null,
/// Runtime system services with their validation/policy layer intact.
/// Intrinsic platform commands use this binding; generic host calls
/// continue to use `host_calls` below.
Expand Down Expand Up @@ -4881,6 +4926,8 @@ pub fn Effects(comptime Msg: type) type {
/// Window-action mirror: counts and the last requested label,
/// observable in tests (`windowActionState`).
window_action_state: WindowActionState = .{},
/// WebView-navigation mirror, observable in tests.
webview_action_state: WebViewActionState = .{},
/// The environment spawned children inherit and fetch honors
/// (PATH for `spawnPath`-style lookups, proxy variables).
/// Bound once from the loop thread before the first real
Expand Down Expand Up @@ -6234,6 +6281,13 @@ pub fn Effects(comptime Msg: type) type {
if (self.window_actions == null) self.window_actions = binding;
}

/// Bind the runtime-owned WebView navigation seam. The runtime
/// context and callback are stable, while UiApp may refresh the
/// target canvas window id after the first frame event.
pub fn bindWebViewActions(self: *Self, binding: WebViewActionBinding) void {
self.webview_actions = binding;
}

/// Bind runtime-validated platform services for the SDK-reserved
/// named command family. Loop-thread only; the first bind sticks.
pub fn bindSystemServices(self: *Self, binding: SystemServiceBinding) void {
Expand Down Expand Up @@ -10401,13 +10455,33 @@ pub fn Effects(comptime Msg: type) type {
_ = binding.quit_fn(binding.context);
}

/// Navigate a declared child WebView in the bound canvas window.
/// Fire-and-forget: fake/replay records the request, while real mode
/// invokes the runtime callback. Invalid labels, the main WebView,
/// missing views, and denied origins fail closed and are logged by
/// the callback owner without aborting the update loop.
pub fn navigateWebView(self: *Self, label: []const u8, url: []const u8) void {
self.webview_action_state.navigate_count += 1;
self.webview_action_state.record(label, url);
if (self.executor == .fake) return;
const binding = self.webview_actions orelse return;
if (!binding.navigate_fn(binding.context, binding.window_id, label, url)) {
effects_log.warn("WebView navigation rejected for label '{s}'", .{label});
}
}

/// The window-action mirror, for tests: how many close/minimize/
/// show/quit requests rode the channel and the last label
/// requested.
pub fn windowActionState(self: *const Self) WindowActionState {
return self.window_action_state;
}

/// The WebView-navigation mirror, for tests and replay diagnostics.
pub fn webViewActionState(self: *const Self) WebViewActionState {
return self.webview_action_state;
}

/// Set playback volume, clamped to 0.0—1.0. Remembered across
/// tracks: the next `playAudio` re-applies it.
pub fn setAudioVolume(self: *Self, volume: f32) void {
Expand Down
12 changes: 12 additions & 0 deletions src/runtime/ts_core_host.zig
Original file line number Diff line number Diff line change
Expand Up @@ -297,6 +297,11 @@
//! activate (the tray "Open" consequence of the
//! menu-bar-app loop). No result Msg; the window's own
//! frame event carries the state.
//! webview_navigate -> `fx.navigateWebView(label, url)` — fire-and-
//! forget navigation of a declared child WebView in the
//! main window. The runtime applies the normal WebView
//! label, target, and origin-policy checks; invalid or
//! denied requests do not abort dispatch.
//! window_hide -> `fx.hideWindow(label)` — retain the live window and
//! its views while ordering it out; window_show is the
//! inverse.
Expand Down Expand Up @@ -1243,6 +1248,13 @@ pub fn TsCoreHost(comptime core: type) type {
const label = takeShortBytes(cmd, &at);
fx.showWindow(label);
},
// webview_navigate [op][label_len][label]
// [url_len u32 LE][url]
0x33 => {
const label = takeShortBytes(cmd, &at);
const url = takeLongBytes(cmd, &at);
fx.navigateWebView(label, url);
},
// quit_app [op]
0x11 => fx.quitApp(),
// image_load [op][id f64 LE][event_tag]
Expand Down
66 changes: 63 additions & 3 deletions src/runtime/ts_core_host_tests.zig
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ const std = @import("std");
const effects_mod = @import("effects.zig");
const runtime_clock = @import("clock.zig");
const ts_core_host = @import("ts_core_host.zig");
const platform = @import("../platform/root.zig");

// ------------------------------------------------------ the mini core
//
Expand Down Expand Up @@ -305,9 +306,10 @@ const mini_core = struct {
uget, // 81: fetch "uget" -> ufetched/failed
ufetched: struct { status: u64, body: []const u8 }, // 82: fetch ok
// record with a u64-classed number field
start_capture, // 83: microphone capture key 91 -> capture_evt
stop_capture, // 84: stop capture key 91
capture_evt: struct { // 85: ten-field capture event arm
navigate_webview, // 83: webview_navigate child URL
start_capture, // 84: microphone capture key 91 -> capture_evt
stop_capture, // 85: stop capture key 91
capture_evt: struct { // 86: ten-field capture event arm
key: f64,
state: CaptureState,
source: CaptureSource,
Expand Down Expand Up @@ -687,6 +689,7 @@ const mini_core = struct {
.hide_win => return .{ .model = model, .cmd = cmdWindowHide("player") },
.dock_off => return .{ .model = model, .cmd = cmdDockPresence(false) },
.quit_app => return .{ .model = model, .cmd = cmdQuitApp() },
.navigate_webview => return .{ .model = model, .cmd = cmdWebViewNavigate("preview", "https://status.test/page") },
.open_chan => return .{ .model = model, .cmd = cmdChannelOpen(41, 47) },
.close_chan => return .{ .model = model, .cmd = cmdChannelClose(41) },
.chan_evt => |event| {
Expand Down Expand Up @@ -1186,6 +1189,15 @@ const mini_core = struct {
return out;
}

fn cmdWebViewNavigate(label: []const u8, url: []const u8) []const u8 {
const out = rt.frameAlloc(u8, 2 + label.len + 4 + url.len);
out[0] = 0x33;
out[1] = @intCast(label.len);
@memcpy(out[2..][0..label.len], label);
_ = writeLongBytes(out, 2 + label.len, url);
return out;
}

fn cmdImageLoad(id: f64, event_tag: u8, image_path: []const u8, url: []const u8, cache_path: []const u8, expected: f64) []const u8 {
const out = rt.frameAlloc(u8, 1 + 8 + 1 + 4 + image_path.len + 4 + url.len + 4 + cache_path.len + 8);
out[0] = 0x12;
Expand Down Expand Up @@ -2842,6 +2854,54 @@ test "window verbs bridge to the effects channel's label-addressed verbs" {
try std.testing.expectEqual(boot_pending, fx.pendingHostCount());
}

test "webview navigation decodes onto the effects mirror" {
const fx = freshChannel();
defer fx.deinit();
Host.init(fx);

Host.dispatch(fx, .navigate_webview);
const state = fx.webViewActionState();
try std.testing.expectEqual(@as(u32, 1), state.navigate_count);
try std.testing.expectEqualStrings("preview", state.label());
try std.testing.expectEqualStrings("https://status.test/page", state.url());
}

test "webview navigation invokes its bound runtime seam in real mode" {
const fx = freshChannel();
defer fx.deinit();
fx.executor = .real;

const Stub = struct {
var calls: u32 = 0;
var window_id: platform.WindowId = 0;
var label: []const u8 = "";
var url: []const u8 = "";

fn navigate(context: *anyopaque, target: platform.WindowId, requested_label: []const u8, requested_url: []const u8) bool {
_ = context;
calls += 1;
window_id = target;
label = requested_label;
url = requested_url;
return true;
}
};
Stub.calls = 0;
var context: u8 = 0;
fx.bindWebViewActions(.{
.context = &context,
.window_id = 7,
.navigate_fn = Stub.navigate,
});

Host.init(fx);
Host.dispatch(fx, .navigate_webview);
try std.testing.expectEqual(@as(u32, 1), Stub.calls);
try std.testing.expectEqual(@as(platform.WindowId, 7), Stub.window_id);
try std.testing.expectEqualStrings("preview", Stub.label);
try std.testing.expectEqualStrings("https://status.test/page", Stub.url);
}

test "a channel opens, posts route the five-field arm by name, and close retires the key" {
const fx = freshChannel();
defer fx.deinit();
Expand Down
17 changes: 17 additions & 0 deletions src/runtime/ui_app.zig
Original file line number Diff line number Diff line change
Expand Up @@ -1502,6 +1502,11 @@ pub fn UiAppWithFeatures(comptime ModelT: type, comptime MsgT: type, comptime fe
.dock_presence_fn = effectsSetDockPresence,
.quit_fn = effectsQuitApp,
});
self.effects.bindWebViewActions(.{
.context = runtime,
.window_id = self.canvas_window_id,
.navigate_fn = effectsNavigateWebView,
});
self.effects.bindSystemServices(.{
.context = runtime,
.open_external_url_fn = effectsOpenExternalUrl,
Expand Down Expand Up @@ -6429,6 +6434,18 @@ fn effectsQuitApp(context: *anyopaque) bool {
return true;
}

fn effectsNavigateWebView(context: *anyopaque, window_id: platform.WindowId, label: []const u8, url: []const u8) bool {
const runtime: *Runtime = @ptrCast(@alignCast(context));
_ = runtime.updateView(window_id, label, .{ .url = url }) catch |err| {
ui_app_log.warn(
"WebView navigation for '{s}' rejected: {s} - the view must be a declared child WebView and the URL's origin must be in security.navigation.allowed_origins",
.{ label, @errorName(err) },
);
return false;
};
return true;
}

fn effectsOpenExternalUrl(context: *anyopaque, url: []const u8) anyerror!void {
const runtime: *Runtime = @ptrCast(@alignCast(context));
return runtime.openExternalUrl(url);
Expand Down
5 changes: 5 additions & 0 deletions tools/corewire/emit_facade.zig
Original file line number Diff line number Diff line change
Expand Up @@ -2895,6 +2895,11 @@ const FacadeEmitter = struct {
\\ nscfWU8(sink, 0x10);
\\ nscfWShortText(sink, cmd.label);
\\ return;
\\ case "webview_navigate":
\\ nscfWU8(sink, 0x33);
\\ nscfWShortText(sink, cmd.label);
\\ nscfWBytes(sink, cmd.url);
\\ return;
\\ case "window_hide":
\\ nscfWU8(sink, 0x21);
\\ nscfWShortText(sink, cmd.label);
Expand Down
Loading