| 1 | # Desktop shell migration: Wails to Electron |
| 2 | |
| 3 | [简体中文](DESKTOP_SHELL_MIGRATION.zh-CN.md) |
| 4 | |
| 5 | This record preserves the Wails-to-Electron decision, migration evidence, and |
| 6 | remaining acceptance items. The current implementation uses Electron, a Go |
| 7 | desktop service, and React; the Wails entry point and build dependencies have |
| 8 | been removed. Migration phases and baseline commands below describe that |
| 9 | transition, not the routine development workflow. For current work, use |
| 10 | [Contributing](../CONTRIBUTING.md), [the host protocol](DESKTOP_HOST_PROTOCOL.md), |
| 11 | and [the generated entry-point inventory](desktop-migration/INVENTORY.md). |
| 12 | Removal does not establish that every platform acceptance item has passed; |
| 13 | the recorded open items remain explicit below. |
| 14 | |
| 15 | ## Decision |
| 16 | |
| 17 | Reasonix Desktop moves from Wails v2 (WebKit on macOS, WebView2 on Windows, |
| 18 | WebKitGTK on Linux) to Electron with a Chromium renderer, because the product |
| 19 | needs a native browser that the user and the agent operate together, and no |
| 20 | system webview offers a second, isolated, scriptable web surface with a stable |
| 21 | engine across all four release targets. The Go desktop layer becomes a |
| 22 | standalone service process joined to the shell by one private JSON-RPC |
| 23 | connection. The development branch replaces Wails directly; no dual-shell |
| 24 | product is maintained, and the branch is not released before every acceptance |
| 25 | gate in this document passes. |
| 26 | |
| 27 | Alternatives considered and rejected: |
| 28 | |
| 29 | - **Keep Wails and embed a browser through CDP to a system Chrome.** Depends on |
| 30 | an external browser install, cannot share a login partition safely, and gives |
| 31 | no control over the surface geometry inside the app window. |
| 32 | - **Wails v3 multi-window.** Still one system engine per platform, no |
| 33 | `WebContentsView` equivalent, and the WebKitGTK/WebView2 rough edges that |
| 34 | motivated the recovery code stay. |
| 35 | - **Rewrite the desktop layer in TypeScript.** Discards the controller, lease, |
| 36 | recovery and remote logic that the CLI, Serve and bot frontends share. |
| 37 | |
| 38 | Consequences accepted: a larger fixed memory and package footprint, measured |
| 39 | across the complete process tree and reported honestly; two runtimes to keep in |
| 40 | one version unit; Chromium sandbox requirements on Linux. |
| 41 | |
| 42 | ## Baseline |
| 43 | |
| 44 | The migration baseline is `main-v2` at `7717f3eeab47f66560ea85cc7dbe27426c3adf47`, |
| 45 | frozen when the branch was cut. The prototype work at `e2298bd78` (isolated |
| 46 | Electron + Go browser experiment and the ACP MCP-interaction forwarding) is |
| 47 | carried on the branch. The fixes between the two commits (session recovery |
| 48 | visible in welcome layouts, global new-session workspace targeting, settings |
| 49 | search/save-bar overlap) are part of the baseline and must survive. |
| 50 | |
| 51 | Wails metrics are captured with `scripts/desktop-shell-metrics.sh` on the same |
| 52 | machine and stored under `docs/desktop-migration/baseline/`. The Electron build |
| 53 | is measured with the same script so the comparison is like for like. |
| 54 | |
| 55 | ## Architecture |
| 56 | |
| 57 | ```text |
| 58 | React UI ──typed IPC via preload──▶ Electron main ──stdio JSON-RPC──▶ Go desktop service |
| 59 | │ │ |
| 60 | ├─ WebContentsView (websites) └─ control.Controller, sessions, |
| 61 | ├─ remote Serve windows tools, leases, recovery, billing |
| 62 | └─ menu, tray, dialogs, clipboard |
| 63 | Remote Reasonix agent ◀── restricted host RPC over the existing SSH channel ──▶ Go desktop service |
| 64 | ``` |
| 65 | |
| 66 | | Layer | Owns | |
| 67 | | --- | --- | |
| 68 | | React UI | rendering, intent, layout, state projection; no Electron or Go globals | |
| 69 | | Electron main | windows, browser views, menu, tray, dialogs, clipboard, notifications, native lifecycle | |
| 70 | | Go desktop service | every desktop business command, controller ownership, approvals, settings, terminals, SSH, extensions, update coordination | |
| 71 | | Go kernel | unchanged agent, provider, tool, persistence, lease, recovery and billing semantics | |
| 72 | | Remote adapter | forwards session-authorised host capabilities; no second browser implementation | |
| 73 | |
| 74 | Contracts (see the protocol document for the wire shapes): |
| 75 | |
| 76 | - `DesktopContract`: the reflected command registry over the Go `App` value, |
| 77 | generated into a TypeScript command table and DTO declarations with a digest |
| 78 | the handshake verifies. |
| 79 | - `DesktopEvent`: one envelope (`seq`, `generation`, `name`, `args`) carrying |
| 80 | the existing event payloads unchanged. |
| 81 | - `NativeHost`: the Go interface that replaced direct shell-toolkit calls; |
| 82 | implemented by the Electron host over `host/*` requests. |
| 83 | - `BrowserExecutor`: the local and remote browser read/act/capture/file |
| 84 | interface (phase D). |
| 85 | - `HostCapabilityRegistry`: host capability discovery, version negotiation |
| 86 | and per-session grants; browser tools register through the existing |
| 87 | capability and tool registry. |
| 88 | - `DesktopLifecycle`: start, ready, hide, restore, quit and update hand-off |
| 89 | states shared by both processes. |
| 90 | |
| 91 | ## Phases and status |
| 92 | |
| 93 | Status values: `implemented` (code on the branch), `locally tested` (tests or |
| 94 | manual checks on the development machine), `externally verified` (CI or |
| 95 | another platform), `blocked` (with the reason). A phase closes only when its |
| 96 | exit condition is met on every release target. |
| 97 | |
| 98 | ### A. Freeze the baseline and inventory every entry point |
| 99 | |
| 100 | - Branch `feature/electron-desktop-shell` from the frozen baseline with the |
| 101 | prototype and ACP work carried over: implemented. |
| 102 | - `tools/desktopinventory` generates the inventory of commands, native calls, |
| 103 | events, frontend bridge uses, CSS markers, persisted files, shell-only Go |
| 104 | files, release artifacts and CI jobs, each with exactly one class; `-check` |
| 105 | fails on drift or an unclassified entry: implemented, locally tested. |
| 106 | - Wails baseline metrics: see `docs/desktop-migration/baseline/`. |
| 107 | - This record, the protocol document and the inventory in English and |
| 108 | Chinese: implemented. |
| 109 | |
| 110 | Exit condition: every existing entry has a destination and an acceptance |
| 111 | case. Met for the inventory; acceptance cases are listed under gates below. |
| 112 | |
| 113 | ### B. Extract the desktop service and the unified bridge |
| 114 | |
| 115 | - `nativeHost` interface with the Wails implementation behind it; Go business |
| 116 | code no longer calls the shell toolkit directly: implemented, locally tested |
| 117 | (`desktop/native_host*.go`, `go test -short .` green). |
| 118 | - `desktop/internal/hostrpc`: reflection registry, contract digest, TypeScript |
| 119 | emitter, strict JSON-RPC server over `rpcwire`, event envelope, reverse host |
| 120 | requests: implemented, locally tested; all 575 commands accepted by the |
| 121 | registry. |
| 122 | - `reasonix-desktop --host-rpc`: one Go service process for all sessions and |
| 123 | tabs; `-emit-contract` writes the generated TypeScript and JSON; the RPC |
| 124 | native host, tray and quit hooks run over the shell connection: |
| 125 | implemented, locally tested. |
| 126 | - One pnpm workspace under `desktop/` for the frontend and the shell: |
| 127 | implemented. |
| 128 | - The root Go module stays static-only; the desktop module keeps its own build. |
| 129 | |
| 130 | Exit condition: the service starts and is tested without Wails; every command |
| 131 | is mapped by the contract; business code has no direct shell calls. |
| 132 | |
| 133 | ### C. Electron hosts the complete existing desktop |
| 134 | |
| 135 | Main window, trusted preload, error recovery page, service supervisor, |
| 136 | `reasonix://app` asset scheme with forwarded authorised media, window state, |
| 137 | theme, title bar drag, shortcuts, file drop, clipboard, dialogs, remote Serve |
| 138 | windows, menu, tray, background close and restore. The transcript kernel, |
| 139 | stable message identity and single scroll writer are untouched. |
| 140 | |
| 141 | Status: the shell (`desktop/electron`), the frontend host adapter |
| 142 | (`src/lib/desktopHost.ts`, boundary gate, one stylesheet with the drag-region |
| 143 | rewrite) and the host-mode routes for tray, remote windows and relaunch are |
| 144 | implemented and locally tested on macOS arm64: `pnpm --dir electron smoke` |
| 145 | boots the real service in a disposable home and passes 12/12 (handshake, |
| 146 | invoke, unknown-command rejection, window bounds, no Node or Wails globals in |
| 147 | the renderer, clean exit of both processes); the full desktop Go lane and the |
| 148 | frontend gates are green. Measured against the Wails baseline with the same |
| 149 | script (`docs/desktop-migration/baseline/README.md`): time to a healthy |
| 150 | frontend is unchanged within noise, process-tree memory is about 280 MiB |
| 151 | higher, and SIGTERM now quits cleanly. Windows and Linux runs of the shell |
| 152 | are external verification items. |
| 153 | |
| 154 | Exit condition: the whole existing desktop flow works in Electron with no |
| 155 | mock fallback, no dead controls and no missing events; rapid session |
| 156 | switching never cross-talks. |
| 157 | |
| 158 | ### D. Production browser with one local/remote executor |
| 159 | |
| 160 | Browser panel in the right workspace (tabs per task, address bar, history, |
| 161 | reload, zoom, load errors, downloads, DevTools) managed by a |
| 162 | `BrowserSurfaceManager`; agent capabilities (structure snapshot, screenshot, |
| 163 | navigate, click, type, keys, scroll, tabs, files) through the existing |
| 164 | capability, approval, cancellation and evidence system; user take-over |
| 165 | revokes pending actions; writes record an operation identity before |
| 166 | execution and report executed / not executed / unknown; remote agents reach |
| 167 | the same executor through the SSH-carried host RPC with generation-bound |
| 168 | grants. |
| 169 | |
| 170 | Status: implemented, locally tested on macOS arm64. The Electron browser |
| 171 | surface (`desktop/electron/src/main/browser/`: WebContentsView surfaces, |
| 172 | snapshot/refs, trusted actions, generation-bound grants with the |
| 173 | stale/taken-over/no-grant error codes, downloads, screenshots) passes 81/81 |
| 174 | unit tests and the shell smoke now opens example.com and verifies the tab |
| 175 | title end to end (15/15). The renderer browser API is fixed at |
| 176 | `window.reasonixDesktop.browser`. The frontend browser panel |
| 177 | (`BrowserPanel`, dock tab, address bar, zoom, DevTools, downloads, |
| 178 | take-over banner, overlay gating) ships as one lazy chunk with the initial |
| 179 | bundle budget ratcheted by measurement (2408.2 → 2408.8 KiB raw, zero |
| 180 | initial-chunk leakage proven by token-level diff). Remote agents reach the |
| 181 | same executor through a 127.0.0.1 loopback broker |
| 182 | (`desktop/browser_broker.go`): per-host generation tokens that die on |
| 183 | reconnect, session-scoped routing with cross-session `no_grant` rejection, |
| 184 | screenshot/download relay over SFTP, and serve capability negotiation so |
| 185 | older remotes keep working; covered by `-race` tests including a real SFTP |
| 186 | round trip. Open items: the remote end-to-end run against a real SSH host, |
| 187 | `browser_upload` reverse staging (the wire passes `files` through; the |
| 188 | broker does not stage remote-to-desktop uploads yet), and the remote |
| 189 | browser acceptance rows in the gate table. |
| 190 | |
| 191 | Exit condition: local and remote agents complete real web tasks through the |
| 192 | same tools with identical take-over, approval, file ownership and recovery |
| 193 | behaviour. |
| 194 | |
| 195 | ### E. Platform features, installation and updates |
| 196 | |
| 197 | Electron menu, tray, notifications, file associations, window restore, |
| 198 | single-instance presentation; unchanged product name, install locations, |
| 199 | shortcuts, uninstall identity, data directory and artifact names; Electron |
| 200 | packaging feeding the existing NSIS, nfpm and signing steps; the Go update |
| 201 | coordinator keeps version resolution, signature checks, layout and recovery |
| 202 | with Electron providing prepare-quit and restart; one version unit for shell, |
| 203 | service, assets and helpers; macOS universal, notarised; Linux Chromium |
| 204 | sandbox without `--no-sandbox`; minisign and digest checks unchanged. |
| 205 | |
| 206 | Status: implemented, locally tested where the development machine allows. |
| 207 | The install layout members, payload schema 2, shell bootstrap and macOS |
| 208 | hand-off below are on the branch with the desktop module suite green and |
| 209 | Windows/Linux cross-builds passing. The release pipeline now packages the |
| 210 | Electron shell end to end: `desktop/packaging/` assembles the `app/` tree |
| 211 | with @electron/packager (+ universal on macOS), `scripts/desktop-build.sh` |
| 212 | runs the contract drift check and drives packaging without `wails build`, |
| 213 | NSIS installs the tree via `File /r`, the deb ships `/usr/lib/reasonix/app` |
| 214 | with a root-owned 4755 `chrome-sandbox`, the SignPath configurations cover |
| 215 | the tree's PE set with two-stage installer signing kept, and the CI/release |
| 216 | workflows run `packaging/smoke.mjs` against the packaged shell (the |
| 217 | `desktop-linux-webkit41` job is removed; the pinned contract tests were |
| 218 | rewritten to the new entry points with negative guards against |
| 219 | `wails build`). Open items: the four-platform install/upgrade matrix, |
| 220 | real-code-signing and notarisation runs, the SignPath preflight |
| 221 | re-attestation (the artifact-configuration fingerprint changed), and |
| 222 | Windows/Linux runner verification. |
| 223 | |
| 224 | Exit condition: all four artifacts install, start and uninstall, and the |
| 225 | Wails→Electron upgrade, Electron→Electron upgrade and failed-install recovery |
| 226 | tests pass. |
| 227 | |
| 228 | Design notes for the versioned install layout (Windows and Linux): the |
| 229 | `installlayout` activator whitelists flat regular files inside |
| 230 | `versions/<v>/`. The Electron payload adds one tree member, `app/`, holding |
| 231 | the Electron bundle; the Windows payload manifest moves to schema 2 and lists |
| 232 | every file under `app/` with its digest so the activator validates the tree |
| 233 | before `current.json` moves. `reasonix-desktop(.exe)` stays the active desktop |
| 234 | executable the thin launcher starts: without `--host-rpc` it bootstraps |
| 235 | `app/Reasonix(.exe)` and exits, and Electron spawns the same binary with |
| 236 | `--host-rpc` as the service. Launcher, `current.json`, single-instance |
| 237 | identity and relaunch logic therefore keep their current shape. On macOS the |
| 238 | bundle's main executable is Electron and the Go service lives in |
| 239 | `Contents/MacOS/`; the `.app` swap path is unchanged. Implementation notes: |
| 240 | `installlayout.Member` names are forward-slash paths under the version |
| 241 | directory, either a whitelisted base name or `app/...` (no `..`, absolute |
| 242 | paths, backslashes or symlinks); manifest readers accept schema 1 (flat list) |
| 243 | and schema 2 (flat list plus `app/`); the migration window's |
| 244 | `REASONIX_DESKTOP_SHELL=wails` in-process fallback left with phase F; under the |
| 245 | shell the |
| 246 | macOS hand-off waits for the Electron process (the service's parent, passed |
| 247 | as `-owner-pid`) and reopens the swapped bundle with `open -n` while the shell |
| 248 | only quits. |
| 249 | |
| 250 | #### First upgrade from Wails |
| 251 | |
| 252 | The first Electron release requires a **manual full-package installation** |
| 253 | when upgrading from v1.38.x. Published clients copy and execute |
| 254 | their existing update helper, which cannot transfer the new `app/` tree. |
| 255 | Release assets therefore carry `install_layout: "electron-v1"`: existing |
| 256 | v1.38.x manifest validation rejects that unknown layout before downloading or |
| 257 | replacing files. The old installation remains usable; its update error view |
| 258 | retains the official download-page link. Quit the old app and install the |
| 259 | complete Windows installer, macOS app, or Linux package from that page. |
| 260 | For a portable archive, extract the complete archive into a new directory; |
| 261 | do not replace only the Go executable. Configuration, sessions and the data |
| 262 | home retain their existing names and formats. macOS also uses this one-time |
| 263 | manual transition because old clients validate the whole platform manifest. |
| 264 | |
| 265 | After that transition, Electron clients accept `electron-v1` and publish the |
| 266 | Go service, CLI and complete shell resources as one version before moving |
| 267 | `current.json`. Linux native packages remain owned by the package manager. |
| 268 | Windows update completion waits for the Electron owner to exit and verifies |
| 269 | the new Go service through a data-home-specific named pipe whose server PID |
| 270 | is provided by Windows; Wails endpoint lookup remains for old running apps. |
| 271 | This boundary must be retained on all mirrors and release manifests; changing |
| 272 | the field back to `versioned-v1` would re-enable unsafe legacy automatic updates. |
| 273 | |
| 274 | ### F. Full-matrix acceptance and removal of the old shell |
| 275 | |
| 276 | CI on the new build, contract generation and native test entry points; Wails |
| 277 | entry, dependencies, generated bindings, WebView2 recovery and shell patches |
| 278 | removed; prototype fault cases promoted into real tests; migration aliases, |
| 279 | duplicate DTOs and temporary adapters deleted. |
| 280 | |
| 281 | Status: the removal is implemented and locally tested. The Wails entry |
| 282 | (`wails.Run`, `native_host_wails.go`, `wails.json`, the generated `wailsjs` |
| 283 | bindings, the in-process remote-window child processes) is gone, and with it |
| 284 | the WebView2/WebKitGTK recovery coordinators, diagnostics observers, native |
| 285 | smoke harnesses (`cmd/transcript-native-smoke`, `cmd/transcript-selection-smoke`), |
| 286 | the vendored go-webview2 fork, the `webkit2_41` build tag and the CI WebKitGTK |
| 287 | toolchain steps. The desktop module's `go list -m all` is Wails-free; the |
| 288 | frontend reaches only `window.reasonixDesktop` (enforced by |
| 289 | `check-desktop-host-boundary.mjs`) and the test seam is an Electron host stub. |
| 290 | `REASONIX_DESKTOP_SHELL=wails` no longer exists: a plain launch without an |
| 291 | installed shell exits with an install hint. The prototype's crash fault cases |
| 292 | (renderer crash before dispatch cancels the act; crash after dispatch settles |
| 293 | executed without replay; recovery keeps the login partition) run as real tests |
| 294 | in `desktop/electron/src/main/browser/`. Kept on purpose: the fyne systray |
| 295 | in-process fallback behind `startNativeShellSupport` (unreachable under the |
| 296 | shell but still the bare-service path), the legacy crash-report decode fields, |
| 297 | the `com.wails.reasonix-desktop` bundle identity, and the update helper's |
| 298 | `wails-app-` single-instance lookup (upgrade-from-Wails detection). Open: the |
| 299 | four-platform acceptance matrix, the interaction p95 comparison against the |
| 300 | Wails baseline, and CI runner verification on Windows/Linux. |
| 301 | |
| 302 | Exit condition: no Wails in the final build graph; no old bridge globals in |
| 303 | business code; every matrix item and gate closed. |
| 304 | |
| 305 | ## Capability matrix |
| 306 | |
| 307 | The generated inventory lists every entry point. This table is the |
| 308 | product-level view the acceptance run follows; each row maps to inventory |
| 309 | classes and to a gate below. |
| 310 | |
| 311 | | Capability | Today (Wails) | Target (Electron) | Class | |
| 312 | | --- | --- | --- | --- | |
| 313 | | Sessions: send, stop, model/effort switch, history, recovery, leases | `App` methods over Wails bindings | same methods over `desktop/invoke` | keep-business | |
| 314 | | Projects, worktrees, file preview, workspace watch | Go + asset middleware | Go + `reasonix://app` forwarding to the resource origin | keep-business | |
| 315 | | Terminal | Go PTY/ConPTY, events | unchanged over `desktop/event` | keep-business | |
| 316 | | Settings, MCP, MCP Apps, skills, plugins | Go | unchanged; MCP Apps keep their loopback origins | keep-business | |
| 317 | | Remote workspaces and remote Serve windows | SSH manager + child Wails process per window | SSH manager unchanged; `BrowserWindow` per host with isolated partition | migrate-host | |
| 318 | | Window geometry, theme, drag regions, shortcuts, zoom | Wails runtime | `host/window.*`, preload window API, `-webkit-app-region` | migrate-host | |
| 319 | | File drop, clipboard, external links, dialogs | Wails runtime | preload native API and `host/dialog.*` | migrate-host | |
| 320 | | Menu, tray, background close, second instance | Wails menu, fyne systray, Wails lock | Electron menu, `Tray`, `requestSingleInstanceLock` keyed by canonical home | migrate-host | |
| 321 | | Updater | Go coordinator + Wails relaunch | Go coordinator + `host/app.relaunch` | migrate-host | |
| 322 | | Renderer recovery (WebView2/WebKitGTK) | Go recovery coordinators | Electron `render-process-gone` handling | delete-shell | |
| 323 | | Native browser for the agent | prototype only | `WebContentsView` panel + `BrowserExecutor` | new | |
| 324 | |
| 325 | ## Data compatibility |
| 326 | |
| 327 | - Session, configuration, project, task, billing and lease formats are |
| 328 | unchanged; the transcript schema is not modified. |
| 329 | - Browser metadata and operation logs are new, versioned files that the old |
| 330 | shell never reads. |
| 331 | - Website logins live in Chromium persistent partitions; cookie values never |
| 332 | enter configuration, logs or model context. |
| 333 | - Restored browser tabs keep safe navigation entries only; no passwords, form |
| 334 | state or replayable submissions are persisted. |
| 335 | - File-backed settings win over old webview-local preferences. The only |
| 336 | allowed resets are renderer-local appearance preferences (font family, text |
| 337 | size, panel widths, typography) that lived in the old webview's storage; |
| 338 | old webview data is left in place and listed in the migration notes. |
| 339 | - Downgrade: stop the Electron build, run the previous Wails build; new |
| 340 | browser state must not break its session and configuration reads. |
| 341 | |
| 342 | ## Acceptance gates |
| 343 | |
| 344 | | Area | Required scenarios | |
| 345 | | --- | --- | |
| 346 | | Contract | Go/TS signature parity, empty arrays, optional fields, error mapping, cancellation, out-of-order replies, protocol mismatch, large resources | |
| 347 | | Sessions and ownership | send, stop, model/effort switch, rapid project and session switching, background reattach, lease conflicts, failed controller replacement keeps the old session | |
| 348 | | Event recovery | renderer reload, event backlog, subscription loss and re-snapshot; no duplicates, no old-generation writes | |
| 349 | | Desktop capabilities | terminal I/O and resize, file drop, media preview, MCP Apps, settings, automation, remote connections and windows | |
| 350 | | Browser | iframes, dynamic DOM, controlled inputs, popups, upload/download, history, temporary partitions, shared vs isolated logins | |
| 351 | | Take-over and unknown writes | take-over before approval, after approval before dispatch, lost receipt after execution, restart after crash, duplicate operation IDs | |
| 352 | | Remote browser | SSH drop, reconnect generation change, stale token, cross-session misrouting, remote upload/download, remote process recovery | |
| 353 | | Native experience | real CJK IME, focus, selection and copy, shortcuts, title bar, split panes, cross-screen DPI, tray restore on macOS, Windows and Linux | |
| 354 | | Install and upgrade | upgrade while the old build runs, coexisting data homes, relative data home, corrupt signature, interrupted install, failed restart and rollback | |
| 355 | | Isolation | websites and iframes have no bridge; forged IPC, expired resource tokens, out-of-bounds file requests and external protocol calls are handled | |
| 356 | |
| 357 | Real-task acceptance: authenticated GitHub PR review draft with sources; |
| 358 | cross-page documentation search saved locally; controlled test-site form |
| 359 | submit, upload and download through approval and take-over; the same tasks |
| 360 | from a remote workspace with the browser local and results owned by the |
| 361 | remote task; interrupted submit with unknown receipt proving no automatic |
| 362 | resubmission after recovery. |
| 363 | |
| 364 | Resource and performance sampling follows `scripts/desktop-shell-metrics.sh` |
| 365 | (full process tree; startup, idle, 1/5 tabs, long session, streaming, one |
| 366 | hour) plus 30 open/close cycles for tabs and sessions proving process, |
| 367 | listener, `WebContents` and session resources are released. Interaction p95 |
| 368 | (session switch, stop feedback, input latency) must stay within |
| 369 | `max(1.2 × baseline, baseline + 50 ms)` of the Wails baseline on the same |
| 370 | machine. Package size, startup and memory deltas are published as measured; |
| 371 | fixed overhead alone is not a failure, a sustained leak is. |
| 372 | |
| 373 | Final evidence is bound to one candidate SHA: root and desktop module tests, |
| 374 | race tests for changed concurrent paths, the complete frontend CI suite, and |
| 375 | native acceptance for the four artifacts. |
| 376 |