返回 DeepSeek-Reasonix
DESKTOP_SHELL_MIGRATION.md
根目录 / docs / DESKTOP_SHELL_MIGRATION.md
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
376 lines MARKDOWN