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.

packages/typescript/src/__tests__/bridge-request-id.test.tsmodified+25-6
Changes to packages/typescript/src/__tests__/bridge-request-id.test.ts
@@ -37,20 +37,21 @@ interface Harness {
3737 * they can simply be passed in as parameters — no DOM, no jsdom, and nothing
3838 * shared between tests.
3939 */
40function loadBridge(): Harness {
40function loadBridge(platform: 'webkit' | 'webview2' = 'webkit'): Harness {
4141 const sent: Envelope[] = []
4242 const errors: unknown[][] = []
4343 const win: any = {
44 webkit: {
45 messageHandlers: {
46 craft: { postMessage: (m: Envelope) => { sent.push(m) } },
47 },
48 },
4944 addEventListener: () => {},
5045 removeEventListener: () => {},
5146 // The script fires `craft:ready` on load.
5247 dispatchEvent: () => true,
5348 }
49 if (platform === 'webkit') {
50 win.webkit = { messageHandlers: { craft: { postMessage: (m: Envelope) => { sent.push(m) } } } }
51 }
52 else {
53 win.chrome = { webview: { postMessage: (m: Envelope) => { sent.push(m) } } }
54 }
5455 const doc: any = { readyState: 'complete', addEventListener: () => {} }
5556 const console_: any = { ...console, error: (...a: unknown[]) => { errors.push(a) }, warn: () => {} }
5657
@@ -84,6 +85,24 @@ function settled(p: Promise<unknown>): Promise<unknown> {
8485}
8586
8687describe('bridge reply correlation', () => {
88 it('uses WebView2 when WebKit is absent and preserves the reply id', async () => {
89 const h = loadBridge('webview2')
90 const pending = h.craft.tags.get('/windows')
91 expect(h.sent).toHaveLength(1)
92 expect(h.sent[0]).toMatchObject({ t: 'tags', a: 'get' })
93 expect(h.sent[0].i).toBeGreaterThan(0)
94 h.reply('get', { tags: ['native'] }, h.sent[0].i!)
95 expect(await pending).toEqual(['native'])
96
97 // A page may expose a partial `webkit` object without Craft's handler.
98 // It must still use the authenticated WebView2 transport.
99 h.win.webkit = { messageHandlers: {} }
100 const next = h.craft.tags.get('/windows-next')
101 expect(h.sent).toHaveLength(2)
102 h.reply('get', { tags: ['again'] }, h.sent[1].i!)
103 expect(await next).toEqual(['again'])
104 })
105
87106 it('gives each caller its own answer when two bridges share an action name', async () => {
88107 // `get` is served by both keychain and tags. Before request ids, both
89108 // callers queued under the string "get" and were matched by arrival order,
packages/zig/src/js/craft-bridge.jsmodified+7-3
Changes to packages/zig/src/js/craft-bridge.js
@@ -8,8 +8,8 @@
88// 1. **Self-contained** — no imports, no transpiler, ES5-friendly
99// (avoid arrow functions, const/let are fine in modern WebKit).
1010// 2. **Idempotent** — may run twice if the page reloads.
11// 3. **Defensive** — `webkit.messageHandlers.craft` may not exist (e.g.
12// when served outside a Craft window). Don't throw at module load.
11// 3. **Defensive** — neither WebKit's `messageHandlers.craft` nor WebView2's
12// `chrome.webview` need exist (e.g. outside Craft). Don't throw at load.
1313//
1414// The native side dispatches messages by `t` (type) and answers async ones
1515// via `window.__craftBridgeResult(action, payload, id)`, where `id` is the
@@ -171,7 +171,11 @@
171171 try {
172172 const msg = { t: t, a: a, d: d || '' }
173173 if (i) msg.i = i
174 window.webkit.messageHandlers.craft.postMessage(msg)
174 const webkit = window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.craft
175 const webview2 = window.chrome && window.chrome.webview
176 if (webkit && typeof webkit.postMessage === 'function') webkit.postMessage(msg)
177 else if (webview2 && typeof webview2.postMessage === 'function') webview2.postMessage(msg)
178 else throw new Error('native bridge unavailable')
175179 return true
176180 }
177181 catch (e) {
packages/zig/test/injected_js_test.zigmodified+19-0
Changes to packages/zig/test/injected_js_test.zig
@@ -222,6 +222,25 @@ const WEBVIEW_HOST =
222222 \\globalThis.setTimeout = function () { return 0 };
223223 \\globalThis.clearTimeout = function () {};
224224;
225
226test "the embedded bridge posts through WebView2 when WebKit is absent" {
227 var fx = try Fixture.init();
228 defer fx.deinit();
229 const ctx = fx.ctx;
230
231 _ = try ctx.evaluate(WEBVIEW_HOST);
232 _ = try ctx.evaluate(
233 \\delete window.webkit;
234 \\window.chrome = { webview: { postMessage: function (m) { posted.push(m) } } };
235 );
236 _ = try ctx.evaluate(BRIDGE);
237 _ = try ctx.evaluate("window.craft.window.show()");
238
239 try testing.expectEqualStrings("1", try fx.text("String(posted.length)"));
240 try testing.expectEqualStrings("window", try fx.text("posted[0].t"));
241 try testing.expectEqualStrings("show", try fx.text("posted[0].a"));
242 try testing.expectEqualStrings("true", try fx.text("posted[0].i > 0"));
243}
225244// The timer stubs are load-bearing rather than cosmetic. `_req` arms a
226245// 30-second reaping timeout inside its Promise executor, so without a
227246// `setTimeout` the executor throws a ReferenceError and every request-shaped