返回 DeepSeek-Reasonix
README.md
根目录 / desktop / electron / README.md
1 # Reasonix Desktop shell (Electron)
2
3 The Electron process that hosts the React UI and supervises the Go desktop
4 service. The wire contract between the two is
5 [`docs/DESKTOP_HOST_PROTOCOL.md`](../../docs/DESKTOP_HOST_PROTOCOL.md); this
6 package implements the shell side of it and nothing else. Business logic stays
7 in Go, the UI stays in `../frontend`.
8
9 ```text
10 renderer (reasonix://app) ──preload (window.reasonixDesktop)──▶ main process ──NDJSON JSON-RPC over stdio──▶ reasonix-desktop --host-rpc
11 ```
12
13 ## Layout
14
15 | Path | Concern |
16 | --- | --- |
17 | `src/main/index.ts` | bootstrap: data home, single instance, privileged scheme, wiring |
18 | `src/main/service.ts` | Go service supervisor: spawn, stderr log, restart budget, shutdown |
19 | `src/main/rpc.ts` | NDJSON JSON-RPC 2.0 client (64 MiB frames, timeouts, reverse requests) |
20 | `src/main/handshake.ts` | `desktop/hello` params, result validation, failure descriptions |
21 | `src/main/window.ts` | main `BrowserWindow`, `host/window.*`, close and crash handling |
22 | `src/main/protocol.ts` | `reasonix://app` file serving and resource-origin forwarding |
23 | `src/main/ipc.ts` | renderer IPC: sender check, contract allowlist, native calls |
24 | `src/main/hostCalls.ts` | `host/*` dispatch table |
25 | `src/main/lifecycle.ts` | quit sequencing (`beforeClose` → `shutdown` → stdin close → exit) |
26 | `src/main/menu.ts`, `tray.ts`, `dialogs.ts`, `remoteWindows.ts` | native surfaces |
27 | `src/main/browser/` | in-app browser: website views, snapshots, actions, downloads, grants |
28 | `src/preload/index.ts` | the single `window.reasonixDesktop` object |
29 | `src/shared/ipc.ts` | channel names and types shared by main and preload |
30
31 ## Browser surface
32
33 ## Hardware acceleration recovery
34
35 The desktop UI exposes **Settings → General → System → Hardware acceleration**.
36 The preference is stored in the Electron shell profile and only takes effect
37 after a full application restart. If rendering fails before Settings can open,
38 fully quit Reasonix and start it once with `REASONIX_DISABLE_GPU=1`; this is a
39 temporary override and does not change the saved preference. The override is
40 supported on Windows, macOS, and Linux.
41
42 The shell can host real websites next to the app UI (contract:
43 [`docs/DESKTOP_BROWSER.md`](../../docs/DESKTOP_BROWSER.md)). Every tab is a
44 sandboxed `WebContentsView` managed by `browser/surfaceManager.ts`; the React
45 panel drives it through `reasonixDesktop.browser.*` (user surface, no grant),
46 and Go drives it through the `host/browser.*` host calls
47 (`browser/hostCalls.ts`), which require a per-task grant that dies with the
48 service generation.
49
50 | Module | Concern |
51 | --- | --- |
52 | `guestView.ts`, `electronGuestViews.ts` | the `WebContentsView` behind injected interfaces; tests use fakes |
53 | `surfaceManager.ts` | tabs, layout/overlay visibility, take-over and crash recovery |
54 | `grants.ts`, `errors.ts` | per-task grants and the `-32010/-32011/-32012` contract codes |
55 | `snapshotScript.ts`, `snapshot.ts`, `pageScripts.ts` | serialised page walkers: aria-style snapshot, ref resolve/locate/select |
56 | `documents.ts`, `refResolver.ts` | document tokens; a navigation or take-over stales every earlier ref |
57 | `actions.ts`, `keys.ts`, `upload.ts` | trusted input dispatch: click, type, press, scroll, select, upload |
58 | `screenshot.ts` | element/full-page captures into the task scratch directory |
59 | `downloads.ts` | `will-download` routing, progress events, per-tab waits |
60 | `guestPreload.ts` | website-view preload; only reports user input for take-over |
61 | `fakeGuestViews.ts` | in-memory views so all of the above runs under plain `node --test` |
62
63 User input in a website view flips the tab to human mode (take-over), bumps
64 its epoch and is reported to Go as `browser.takeover`; `browser.resume` hands
65 it back. Agent-dispatched input is marked so its echo is not a take-over.
66 Downloads land in the task's scratch directory when one is registered by a
67 `browser.act`/`browser.screenshot` call, otherwise in
68 `userData/downloads/<taskId>`; the renderer hears about them through
69 `reasonixDesktop.browser.onDownload`.
70
71 ## Build
72
73 Prerequisites: Node 24+, pnpm 10, Go. Install from the workspace root once:
74
75 ```sh
76 cd desktop
77 pnpm install
78 ```
79
80 `pnpm install` also downloads the Electron binary (`allowBuilds: electron` in
81 `pnpm-workspace.yaml`). If `node_modules/electron/dist` is missing afterwards,
82 run `node node_modules/electron/install.js` inside `desktop/electron`.
83
84 Build the Go service and the UI, then the shell:
85
86 ```sh
87 cd desktop
88 go build -o build/bin/reasonix-desktop-service . # accepts --host-rpc
89 go run . -emit-contract frontend/src/generated # desktopContract.generated.{ts,json}
90 pnpm --filter reasonix-desktop-frontend build # frontend/dist
91 pnpm --filter reasonix-desktop-shell build # electron/dist/{main,preload}.cjs + desktopContract.json
92 ```
93
94 The shell build reads `frontend/src/generated/desktopContract.generated.json`,
95 recomputes its digest the way `hostrpc.Contract.Canonical` defines it
96 (sorted keys, compact, no HTML escaping), checks it against the
97 `DESKTOP_CONTRACT_DIGEST` the generator emitted, and writes the contract plus
98 `digest` to `dist/desktopContract.json`. A missing contract fails the build;
99 set `REASONIX_ELECTRON_ALLOW_MISSING_CONTRACT=1` to build without it (every
100 `desktop/invoke` is then rejected and the hello digest is empty).
101
102 Packaged shells read the full version tag, channel and commit from
103 `resources/build.json` for `desktop/hello`. `app.getVersion()` and
104 `package.json.version` are numeric native metadata and must not identify the
105 RPC build. The packaged startup smoke runs without development overrides and
106 requires the renderer's `Version` command to match that manifest; the service
107 used by CI must also be linked with the same non-development version.
108
109 ## Run
110
111 ```sh
112 cd desktop/electron
113 pnpm start # electron . against ../build/bin/reasonix-desktop-service
114 REASONIX_DESKTOP_SERVICE=/path/to/binary pnpm start
115 ```
116
117 Development against the Vite dev server instead of the packaged UI:
118
119 ```sh
120 cd desktop/frontend && pnpm dev # http://127.0.0.1:5173
121 cd desktop/electron && pnpm dev # REASONIX_DEV=1, loads REASONIX_ELECTRON_DEV_URL
122 ```
123
124 Environment:
125
126 | Variable | Effect |
127 | --- | --- |
128 | `REASONIX_DESKTOP_SERVICE` | path of the Go service binary (packaged default: `resources/service/reasonix-desktop[.exe]`) |
129 | `REASONIX_HOME` | data home, resolved exactly like `internal/config.ReasonixHomeDir` and sent in `hello.instance.home` |
130 | `REASONIX_DEV` | skips the single-instance lock and marks the instance as `dev` |
131 | `REASONIX_ELECTRON_DEV_URL` | loads this URL instead of `reasonix://app/index.html` |
132 | `REASONIX_FRONTEND_DIST` | overrides the directory served under `reasonix://app/` |
133 | `REASONIX_CHANNEL`, `REASONIX_COMMIT` | build identity in `hello.build` (default `dev`) |
134
135 Logs live under `<home>/desktop-shell/logs/`: `shell.log` (main process) and
136 `service.log` (the Go service's stderr), each rotating at 5 MB. In dev both
137 are echoed to the terminal.
138
139 ## Verify
140
141 ```sh
142 pnpm typecheck # main + preload tsconfigs
143 pnpm test # node --test; pure modules only, Electron is injected through interfaces
144 ```
145
146 ## Security boundaries
147
148 - The application window runs with `sandbox: true`, `contextIsolation: true`,
149 `nodeIntegration: false`, no spellcheck, and loads only `reasonix://app`.
150 Every navigation away from the app origin is blocked; popups are denied;
151 `<webview>` is refused.
152 - The preload exposes exactly one object, `window.reasonixDesktop`, shaped as
153 the protocol document's `ReasonixDesktopHost`. IPC replies are envelopes, so
154 a Go error reaches the renderer as `Error(<Go message>)` with no Electron
155 prefix.
156 - `ipcMain` handlers accept calls only from the main window's top frame
157 (`event.sender` and `event.senderFrame` are both checked); any other sender
158 is rejected and logged.
159 - `desktop/invoke` names are validated against the embedded contract before
160 they reach Go; unknown names fail with a `-32601` error.
161 - `reasonix://app` serves files strictly under the frontend dist (no `..`,
162 no absolute escapes, no directory index fallback except `/`). Only the
163 three resource prefixes are forwarded to the loopback origin, and the bearer
164 token is attached in the main process; it never reaches any renderer.
165 - Remote Serve windows use their own `persist:remote-<hostKey>` session, no
166 preload, sandbox on, popups denied, navigation pinned to the page origin.
167 - Website views are sandboxed `WebContentsView`s on the `persist:browser`
168 partition (`temp:<id>` for temporary tabs) with a preload that only reports
169 user input. `host/browser.*` calls need a grant scoped to one task and one
170 service generation; reads and writes refuse a tab in human mode.
171 - `shell.openExternal` from the renderer accepts `http:`, `https:` and
172 `mailto:` only.
173 - The service is restarted automatically at most three times per five
174 minutes after an unexpected exit; afterwards the failure page offers a
175 manual restart, the logs folder, and quit. There is no mock fallback.
176
177 Packaging (`electron-builder`) is intentionally not part of this package yet.
178
178 lines MARKDOWN