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.

.github/workflows/ci.ymlmodified+33-2
Changes to .github/workflows/ci.yml
@@ -92,7 +92,38 @@ jobs:
9292
9393 - name: Smoke Linux multi-window bridge
9494 if: runner.os == 'Linux'
95 run: bun scripts/verify-linux-multi-window.ts packages/zig/zig-out/bin/craft
95 run: bun scripts/verify-desktop-multi-window.ts packages/zig/zig-out/bin/craft
96
97 windows-multi-window:
98 runs-on: windows-2025
99 timeout-minutes: 20
100
101 steps:
102 - uses: actions/checkout@v6
103
104 - name: Setup Pantry (provides Bun and Zig)
105 uses: pantry-pm/pantry/packages/action@235036fa0f48bae99b2293df5a3dc35c809b1777 # pinned: last SHA whose bundled typescript resolves on linux-x64
106
107 - name: First-party Zig dependencies
108 uses: ./.github/actions/first-party-zig-deps
109
110 - name: Build Windows CLI
111 run: bun run build:core
112
113 - name: Stage pinned WebView2 loader
114 shell: pwsh
115 run: |
116 $version = '1.0.4191.47'
117 $expected = 'f492bbf547d0da329553b6727435b677579b1e9f91cc9e4a1ad029366d5f23d0'
118 $archive = Join-Path $env:RUNNER_TEMP 'microsoft.web.webview2.zip'
119 $package = Join-Path $env:RUNNER_TEMP 'microsoft.web.webview2'
120 Invoke-WebRequest -Uri "https://api.nuget.org/v3-flatcontainer/microsoft.web.webview2/$version/microsoft.web.webview2.$version.nupkg" -OutFile $archive
121 if ((Get-FileHash $archive -Algorithm SHA256).Hash.ToLowerInvariant() -ne $expected) { throw 'WebView2 NuGet package digest mismatch' }
122 Expand-Archive -LiteralPath $archive -DestinationPath $package
123 Copy-Item (Join-Path $package 'build/native/x64/WebView2Loader.dll') 'packages/zig/zig-out/bin/WebView2Loader.dll'
124
125 - name: Smoke Windows multi-window bridge
126 run: bun scripts/verify-desktop-multi-window.ts packages/zig/zig-out/bin/craft.exe
96127
97128 typescript-sdk:
98129 runs-on: ubuntu-latest
@@ -390,7 +421,7 @@ jobs:
390421 run: bun run verify
391422
392423 publish-commit:
393 needs: [zig-core, typescript-sdk, ios-builder, ios-native-templates, android-builder, create-craft, ts-maps, lint, integration]
424 needs: [zig-core, windows-multi-window, typescript-sdk, ios-builder, ios-native-templates, android-builder, create-craft, ts-maps, lint, integration]
394425 runs-on: ubuntu-latest
395426
396427 steps:
.github/workflows/release.ymlmodified+17-0
Changes to .github/workflows/release.yml
@@ -179,6 +179,23 @@ jobs:
179179 rm -rf zig-out/bin
180180 zig build -Doptimize=ReleaseSafe -Dversion="$VERSION"
181181
182 # The Windows binary dynamically loads WebView2Loader.dll. The runtime
183 # alone does not provide the app-local loader, so stage the x64 DLL beside
184 # craft.exe before Pantry zips zig-out/cross/windows-x64/. Pin and verify
185 # the official NuGet package instead of silently shipping a moving DLL.
186 - name: Stage Windows WebView2 loader
187 if: runner.os == 'Linux'
188 working-directory: packages/zig
189 run: |
190 set -euo pipefail
191 VERSION=1.0.4191.47
192 EXPECTED=f492bbf547d0da329553b6727435b677579b1e9f91cc9e4a1ad029366d5f23d0
193 ARCHIVE="$RUNNER_TEMP/microsoft.web.webview2.$VERSION.nupkg"
194 curl -fsSL --retry 3 "https://api.nuget.org/v3-flatcontainer/microsoft.web.webview2/$VERSION/microsoft.web.webview2.$VERSION.nupkg" -o "$ARCHIVE"
195 echo "$EXPECTED $ARCHIVE" | sha256sum -c -
196 unzip -p "$ARCHIVE" build/native/x64/WebView2Loader.dll > zig-out/cross/windows-x64/WebView2Loader.dll
197 test -s zig-out/cross/windows-x64/WebView2Loader.dll
198
182199 # ── macOS Code Signing & Notarization ──────────────────────────
183200 # The signing credentials live encrypted in `.env.production`, not as
184201 # seven repository secrets.
docs/architecture/multi-window-ownership.mdmodified+4-4
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; cross-build 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,9 +18,9 @@ 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 portable runtime smoke and the still-unspecified
22modal/parent semantics are reviewed. A green cross-build alone does not prove
23that WebView2 opens and tears down a real window on Windows.
21Issue #67 remains open while the Windows runtime smoke and the still-unspecified
22modal/parent semantics are reviewed. The Windows release archive now stages an
23app-local WebView2 loader; users still need the Microsoft WebView2 Runtime.
2424
2525Linux and Windows keep their live native window/webview pairs in
2626`desktop_window_registry.zig`. Page messages are authenticated by the sending
scripts/verify-linux-multi-window.ts scripts/verify-desktop-multi-window.tsrenamed+15-7
Changes to scripts/verify-desktop-multi-window.ts
@@ -1,10 +1,13 @@
1/** Exercise the shipped Linux binary's page-to-native multi-window bridge. */
1/** Exercise the shipped Linux or Windows binary's page-to-native multi-window bridge. */
22import { spawn } from 'node:child_process'
33import { createServer } from 'node:http'
44import type { AddressInfo } from 'node:net'
5import { platform } from 'node:os'
56
67const binary = process.argv[2]
7if (!binary) throw new Error('usage: bun scripts/verify-linux-multi-window.ts <craft-binary>')
8if (!binary) throw new Error('usage: bun scripts/verify-desktop-multi-window.ts <craft-binary>')
9const isWindows = platform() === 'win32'
10const platformName = isWindows ? 'Windows' : 'Linux'
811
912const mainPage = `<!doctype html><script>
1013(async () => {
@@ -130,8 +133,10 @@ await new Promise<void>((resolve, reject) => {
130133 server.listen(0, '127.0.0.1', resolve)
131134})
132135const port = (server.address() as AddressInfo).port
133const child = spawn('timeout', ['110s', 'xvfb-run', '-a', binary, '--url', `http://127.0.0.1:${port}/main`], {
134 detached: true,
136const child = spawn(isWindows ? binary : 'timeout', isWindows
137 ? ['--url', `http://127.0.0.1:${port}/main`]
138 : ['110s', 'xvfb-run', '-a', binary, '--url', `http://127.0.0.1:${port}/main`], {
139 detached: !isWindows,
135140 stdio: ['ignore', 'pipe', 'pipe'],
136141})
137142let output = ''
@@ -140,11 +145,11 @@ for (const stream of [child.stdout, child.stderr]) {
140145}
141146child.once('error', error => rejectDone(error))
142147child.once('exit', (code, signal) => rejectDone(new Error(`Craft exited before smoke completed (${code ?? signal})`)))
143const timer = setTimeout(() => rejectDone(new Error('Linux multi-window smoke timed out')), 100_000)
148const timer = setTimeout(() => rejectDone(new Error(`${platformName} multi-window smoke timed out`)), 100_000)
144149
145150try {
146151 await done
147 console.log(`Linux multi-window smoke passed: ${[...steps].join(', ')}`)
152 console.log(`${platformName} multi-window smoke passed: ${[...steps].join(', ')}`)
148153}
149154catch (error) {
150155 throw new Error(`${String(error)}\nHTTP requests: ${requests.join(', ')}\nCraft output:\n${output}`)
@@ -152,7 +157,10 @@ catch (error) {
152157finally {
153158 clearTimeout(timer)
154159 if (child.pid) {
155 try { process.kill(-child.pid, 'SIGTERM') }
160 try {
161 if (isWindows) child.kill()
162 else process.kill(-child.pid, 'SIGTERM')
163 }
156164 catch { /* The process group may have already exited. */ }
157165 }
158166 await new Promise<void>(resolve => server.close(() => resolve()))