ReviewOS

also looking at this

craft-native/craft

feat: add Linux and Windows named-window routing

#325
Merged glennmichael123 wants to merge codex/multi-window-linux-windows into main
29 files +2250 -200

Review threads live on the whole diff, not on one commit, so none are shown here - a thread's line means something in the branch's final form, and painting it into an intermediate step would put it on code it is not about.

docs/architecture/multi-window-ownership.mdmodified+2-2
Changes to docs/architecture/multi-window-ownership.md
@@ -10,7 +10,7 @@ whether it stores a window-specific target.
1010
1111| Area | macOS | Linux | Windows |
1212| --- | --- | --- | --- |
13| Runtime creation from `createWindow()` | Implemented | Implemented; GUI smoke in CI | Implemented; WebView2 GUI smoke under validation in CI |
13| Runtime creation from `createWindow()` | Implemented | Implemented; GUI smoke in CI | Implemented; WebView2 GUI smoke in CI |
1414| Stable named handles | Implemented | Implemented | Implemented |
1515| Sender-authenticated local actions | Implemented | Implemented through WebKitGTK | Implemented through WebView2 |
1616| Named cross-window actions | Implemented | Implemented for core controls | Implemented for core controls |
@@ -18,7 +18,7 @@ whether it stores a window-specific target.
1818| Permanent destroy and cleanup | Implemented for named windows | Implemented on close/destroy | Implemented on close/destroy |
1919| Modal and parent relationships | Unspecified | Unspecified | Unspecified |
2020
21Issue #67 remains open while the Windows runtime smoke and the still-unspecified
21Issue #67 remains open while the full CI matrix and the still-unspecified
2222modal/parent semantics are reviewed. The Windows release archive now stages an
2323app-local WebView2 loader; users still need the Microsoft WebView2 Runtime.
2424
packages/zig/src/windows.zigmodified+4-8
Changes to packages/zig/src/windows.zig
@@ -682,7 +682,6 @@ const WebMessageReceivedHandler = extern struct {
682682 // Bind its live registry id rather than comparing raw COM interface
683683 // pointers, which need not have the same address for one object.
684684 const entry = desktop_windows.byId(self.window_id) orelse return S_OK;
685 std.debug.print("[WebView2] Page message received for window {d}\n", .{self.window_id});
686685 var message_wide: ?LPWSTR = null;
687686 if (!succeeded(args.lpVtbl.get_WebMessageAsJson(args, &message_wide))) return S_OK;
688687 const wide = message_wide orelse return S_OK;
@@ -1334,21 +1333,20 @@ pub const Window = struct {
13341333 _ = controller.lpVtbl.Release(controller);
13351334 return error.TooManyWindows;
13361335 };
1337 std.debug.print("[WebView2] Native window registered\n", .{});
13381336
13391337 const message_handler = try std.heap.c_allocator.create(WebMessageReceivedHandler);
13401338 message_handler.* = .{ .lpVtbl = &WebMessageReceivedHandler.vtbl_instance, .ref_count = 1, .window_id = window_id };
13411339 var message_token: EventRegistrationToken = .{ .value = 0 };
1342 std.debug.print("[WebView2] Registering page message handler\n", .{});
13431340 const message_hr = webview.lpVtbl.add_WebMessageReceived(webview, @ptrCast(message_handler), &message_token);
1344 std.debug.print("[WebView2] Page message handler returned 0x{x}\n", .{@as(u32, @bitCast(message_hr))});
13451341 _ = WebMessageReceivedHandler.release(message_handler);
1346 if (!succeeded(message_hr)) return error.WebMessageRegistrationFailed;
1342 if (!succeeded(message_hr)) {
1343 std.debug.print("[WebView2] Page message registration failed: 0x{x}\n", .{@as(u32, @bitCast(message_hr))});
1344 return error.WebMessageRegistrationFailed;
1345 }
13471346 if (!desktop_windows.setMessageToken(@intFromPtr(hwnd), message_token.value)) {
13481347 _ = webview.lpVtbl.remove_WebMessageReceived(webview, message_token);
13491348 return error.WebMessageRegistrationFailed;
13501349 }
1351 std.debug.print("[WebView2] Capturing initial window geometry\n", .{});
13521350 observeWindowGeometry(desktop_windows.byId(window_id).?);
13531351
13541352 var window = Window{
@@ -1365,9 +1363,7 @@ pub const Window = struct {
13651363
13661364 // Install before the first Navigate/NavigateToString so every page
13671365 // starts with the same bridge surface as a macOS Craft window.
1368 std.debug.print("[WebView2] Installing document-start bridge\n", .{});
13691366 try window.injectScript(@embedFile("js/craft-bridge.js"));
1370 std.debug.print("[WebView2] Document-start bridge installed\n", .{});
13711367 return window;
13721368 }
13731369
scripts/verify-desktop-multi-window.tsmodified+20-1
Changes to scripts/verify-desktop-multi-window.ts
@@ -55,8 +55,27 @@ const mainPage = `<!doctype html><script>
5555 throw new Error('concurrent child opens returned the wrong handles')
5656 await waitFor('child-queue-a')
5757 await waitFor('child-queue-b')
58 const queueClosed = new Set()
59 const queueClosedDone = new Promise((resolve, reject) => {
60 const timeout = setTimeout(() => reject(new Error('queued child close events missing')), 20000)
61 window.addEventListener('craft:window:close', (event) => {
62 if (event.detail.windowId !== 'queue-a' && event.detail.windowId !== 'queue-b') return
63 queueClosed.add(event.detail.windowId)
64 if (queueClosed.size !== 2) return
65 clearTimeout(timeout)
66 resolve()
67 })
68 })
5869 await window.craft.window._call('close', {}, 'queue-a')
5970 await window.craft.window._call('close', {}, 'queue-b')
71 await queueClosedDone
72 for (const name of ['queue-a', 'queue-b']) {
73 let rejected = false
74 try { await window.craft.window._call('getBounds', {}, name) }
75 catch (_) { rejected = true }
76 if (!rejected) throw new Error('closed queued child still answered a read: ' + name)
77 }
78 await report('queued-closed')
6079
6180 const closed = new Promise((resolve, reject) => {
6281 const timeout = setTimeout(() => reject(new Error('child close event missing')), 20000)
@@ -132,7 +151,7 @@ const server = createServer((request, response) => {
132151 if (step) steps.add(step)
133152 response.end('ok')
134153 if (step === 'done') {
135 const required = ['main', 'child-1', 'closed', 'child-2', 'done']
154 const required = ['main', 'child-1', 'child-queue-a', 'child-queue-b', 'queued-closed', 'closed', 'child-2', 'done']
136155 if (required.every(name => steps.has(name))) resolveDone()
137156 else rejectDone(new Error(`incomplete smoke steps: ${[...steps].join(', ')}`))
138157 }