返回 DeepSeek-Reasonix
README.md
根目录 / desktop / README.md
1 # Reasonix Desktop (Electron shell)
2
3 ## macOS 1.38.7 update recovery
4
5 Desktop 1.38.7 can report `current executable is not inside a macOS .app bundle`
6 before it installs a newer build. Quit Reasonix, mount the Apple Silicon, Intel,
7 or Universal DMG from the official download page, and replace
8 `/Applications/Reasonix.app`. The application bundle is replaced; settings,
9 sessions, and other user data remain in their existing user-data directories.
10 After this one-time full install, verify automatic update by updating the repaired
11 build to the next candidate. Do not treat the manual replacement itself as an
12 automatic-update pass.
13
14 Model/provider setup: [English guide](../docs/MODEL_SETTINGS.md) · [中文指南](../docs/MODEL_SETTINGS.zh-CN.md).
15
16 A native desktop window around the Reasonix Go kernel. The same
17 transport-agnostic `control.Controller` that backs the chat TUI and the HTTP/SSE
18 server is driven by the Electron shell through the desktop host protocol — the
19 Go binary runs as a supervised service (`reasonix-desktop --host-rpc` over
20 stdio), the shell owns the window, tray, menu and renderer lifecycle. See
21 [the host protocol](../docs/DESKTOP_HOST_PROTOCOL.md) and
22 [the migration record](../docs/DESKTOP_SHELL_MIGRATION.md).
23
24 ```
25 ┌─────────────────────────────────────────────────────────────┐
26 │ Electron shell (desktop/electron) │
27 │ renderer: bridge.ts ──invoke──▶ window.reasonixDesktop │
28 │ bridge.ts ◀─events── host.on("agent:event") │
29 └───────────────▲───────────────────────────┬─────────────────┘
30 desktop/invoke (JSON-RPC over stdio) │ desktop/event
31 ┌───────────────┴───────────────────────────▼─────────────────┐
32 │ desktop/app.go App (bound) + eventSink (event.Sink) │
33 │ desktop/host_rpc.go --host-rpc service, embedded dist │
34 └───────────────▲───────────────────────────┬─────────────────┘
35 commands │ │ typed event stream
36 ┌───────────────┴────────────────────────────▼────────────────┐
37 │ internal/boot.Build → internal/control.Controller (kernel) │
38 │ (same assembly the CLI uses: providers, tools, gate, …) │
39 └──────────────────────────────────────────────────────────────┘
40 ```
41
42 ## Why a nested module
43
44 `desktop/` is its own Go module (`module reasonix/desktop`, `replace reasonix =>
45 ../`). That keeps the CGO desktop build entirely separate from the CLI's
46 `CGO_ENABLED=0` single-static-binary guarantee: the parent module's `go build /
47 vet / test ./...` skip this directory, while the import path stays under
48 `reasonix/` so it can still import the `reasonix/internal/*` kernel.
49
50 ## Prerequisites
51
52 - Go (matches the parent module).
53 - Node 24+ and **pnpm 10** (`npm install -g pnpm@10`); `pnpm --dir desktop
54 install` pulls the Electron toolchain for the shell.
55 - No platform webview dependencies: the shell ships its own Chromium.
56
57 ## Develop
58
59 For browser-only UI development with the built-in mock bridge:
60
61 ```sh
62 cd desktop
63 pnpm install # first run only
64 pnpm dev
65 ```
66
67 For the complete Electron application, including the Go service and Vite dev
68 server, use the single development entry point:
69
70 ```sh
71 cd desktop
72 pnpm install # first run only
73 pnpm dev:desktop
74 ```
75
76 For a production-style renderer build instead of the Vite development server,
77 the equivalent manual sequence remains:
78
79 ```sh
80 cd desktop
81 pnpm install # one workspace: frontend + electron
82 go build -o build/bin/reasonix-desktop-service .
83 pnpm --dir frontend build:electron # rewrites drag regions for Chromium
84 pnpm --dir electron start # launches the shell against the service
85 ```
86
87 Frontend-only iteration without the Go side:
88
89 ```sh
90 cd desktop/frontend
91 pnpm install
92 pnpm dev # opens in a plain browser; bridge.ts uses the dev mock
93 ```
94
95 In a plain browser the native bindings are absent, so `bridge.ts` falls back to a
96 **mock** that streams a canned turn (text + one `edit_file` tool call) through the
97 exact same event contract — so layout, streaming, markdown, tool cards, and the
98 diff seam can all be built without rebuilding Go.
99
100 ```sh
101 pnpm --dir electron smoke # real-service end-to-end check
102 ```
103
104 `go run . -emit-contract frontend/src/generated` regenerates the TypeScript
105 contract; `go test -run HostContract .` fails when it drifts.
106
107 ## Test
108
109 The desktop package is a nested Go module, so parent `go test ./...` does not run
110 it. Use the full lane before merging desktop changes, and the short lane for fast
111 local feedback:
112
113 ```sh
114 make desktop-test # cd desktop && go test .
115 make desktop-test-short # skips slow desktop integration/e2e checks
116 ```
117
118 To find the next bottleneck, rank individual test cases from the JSON stream:
119
120 ```sh
121 make desktop-test-times
122 # or: cd desktop && go test -count=1 -json . | python3 ../scripts/desktop-test-times.py
123 ```
124
125 ### Frontend UI review checklist
126
127 For anchored menus, dropdowns, tooltips, and other portaled UI, review both the
128 component code and the CSS positioning contract:
129
130 - If a component uses `createPortal` plus `getBoundingClientRect()`, it must
131 handle scrollable ancestors, window resize, and `visualViewport` changes.
132 - Add a focused regression test when changing shared positioning primitives such
133 as `AnchoredPopover`, not only the specific menu that exposed the bug.
134 - Exercise at least one scrollable container path, such as Settings content, when
135 manually checking dropdown or popover changes.
136
137 ## Build
138
139 ```sh
140 scripts/desktop-build.sh darwin/arm64 v0.0.0-dev # one platform per run
141 ```
142
143 The script regenerates the host contract (failing on drift), builds the Go
144 service, and packages the Electron shell through `desktop/packaging/package.mjs`.
145 Chromium cannot cross-compile the native targets from one host, so releases run
146 it once per platform on a native runner.
147
148 `frontend/dist` is generated by the build (it's git-ignored except for a
149 `.gitkeep` that keeps the Go `//go:embed all:frontend/dist` compilable on a fresh
150 checkout). A bare `go build` without a prior `pnpm build` produces a service with
151 no frontend assets.
152
153 ## Releases & auto-update
154
155 Desktop releases ride their own tag namespace, `desktop-v<semver>` (plain `v*`
156 tags are the CLI release). Pushing one triggers `.github/workflows/release-desktop.yml`,
157 which builds on a native runner per platform (Electron's Chromium can't
158 cross-compile), packages each artifact, signs it with minisign, generates a
159 `latest.json` manifest, publishes a GitHub release, marks the desktop release as
160 GitHub's repository-wide `Latest`, mirrors everything to R2, and attaches the
161 current desktop manifest to the matching CLI release for old clients that still
162 ask GitHub's repository-wide `latest` release for it.
163 The Linux artifact bundles Electron's Chromium and ships a root-owned
164 `chrome-sandbox` helper in the `.deb`; no system webview is required.
165
166 ```sh
167 git tag desktop-v1.1.0 && git push origin desktop-v1.1.0
168 ```
169
170 The app checks `latest.json` on startup (R2 first, then the
171 `crash.reasonix.io` desktop release gateway) and shows an update banner when a
172 newer version is published; **Settings → Software update** has a manual check.
173 The gateway resolves only the desktop `desktop-v*` release line and never uses
174 GitHub's repository-wide `/releases/latest` shortcut, so updater behavior does
175 not depend on homepage badge semantics. Self-update behavior by platform:
176
177 - **Linux portable (`.tar.gz`)** — download, verify the minisign signature, replace
178 the binaries in the install directory, and relaunch through Guard. No elevation.
179 - **Linux Debian/Ubuntu (`.deb`)** — download the signed `.deb`, request administrator
180 authorization via Polkit (`pkexec`), re-verify and install with `apt-get
181 --only-upgrade`, then relaunch through Guard. The first build that ships the
182 update helper and Polkit policy is a one-time bootstrap: existing `.deb` users
183 should overwrite-install once with
184 `sudo apt install ./Reasonix-linux-amd64.deb` (no uninstall required). After
185 that, in-app authorized updates work. If Polkit/`pkexec` is unavailable, use
186 the same manual command. Failed installs leave the running app intact so you
187 can retry; successful installs are managed by apt/dpkg and are not auto-downgraded.
188 - **Windows** — download, verify the minisign signature, then run the per-user
189 NSIS installer (no admin rights needed).
190 - **macOS** — Developer ID signed and notarized release builds update in place.
191 Local and fork builds use ad-hoc signing and remain manual-only because
192 Gatekeeper cannot authorize their replacement bundle.
193
194 ### Code signing — first launch
195
196 - **Windows** — stable builds carry an Authenticode signature (SignPath, approved
197 per release; `release-desktop.yml` verifies every payload binary through
198 `scripts/verify-windows-authenticode.ps1` and fails the release otherwise). A
199 brand-new version can still show SmartScreen until the signature accumulates
200 reputation: *More info → Run anyway*.
201 - **macOS** — official release builds are signed and notarized. Choose the Apple
202 Silicon or Intel DMG for the smallest download, or the Universal DMG when the
203 CPU architecture is unknown. Local ad-hoc builds may still require clearing the
204 quarantine attribute when Gatekeeper reports the app "is damaged" or is from an
205 unidentified developer:
206 ```sh
207 xattr -dr com.apple.quarantine /Applications/Reasonix.app
208 ```
209 The release workflow's `HAS_APPLE_CERT` gate controls the signed, notarized,
210 self-updating path.
211
212 ### Verifying a download
213
214 Artifacts are signed with minisign (public key ID `AF12CA46F4A9EBB0`). The `.minisig`
215 signature sits next to each artifact in the release; verify with the
216 [minisign](https://jedisct1.github.io/minisign/) CLI:
217
218 ```sh
219 minisign -Vm Reasonix-darwin-arm64.zip \
220 -P RWSw66n0RsoSr6Zhh6qt5YO95YkpCayTOCMFVDNUQSjJYwxoYngNVBSq
221 ```
222
223 ## Editor seams and workspace file previews
224
225 Code and diff rendering go through two components with stable prop contracts and
226 lazy boundaries, so heavier viewers stay out of the initial bundle. `CodeViewer`
227 keeps the compact highlighted viewer for chat, Markdown, and tool output, while
228 workspace file previews opt into the searchable line-number viewer:
229
230 | Component | Props | Default impl | Upgrade |
231 |---|---|---|---|
232 | `components/CodeViewer.tsx` | `EditorProps` | `editors/HljsCode.tsx`; `editors/LineNumberCode.tsx` when `showLineNumbers` is enabled | extend the implementation selection for Monaco or CodeMirror |
233 | `components/DiffView.tsx` | `DiffProps` | `editors/HljsDiff.tsx` (highlighted LCS/unified diff) | swap for `editors/MonacoDiff` or `editors/CodeMirrorMerge` |
234
235 ```sh
236 # Monaco
237 pnpm add @monaco-editor/react monaco-editor
238 # or CodeMirror 6
239 pnpm add @uiw/react-codemirror @codemirror/lang-javascript @codemirror/merge
240 ```
241
242 Then add `editors/MonacoCode.tsx` (default-export a component taking
243 `EditorProps`) and update the implementation selection in `CodeViewer.tsx`.
244 `ToolCard` already routes `edit_file` calls' `old_string`/`new_string` through
245 `DiffView`, and `Markdown` routes fenced code blocks through `CodeViewer`, so
246 both seams light up everywhere at once.
247
248 `WorkspacePanel` passes `showLineNumbers` for text-file previews. The resulting
249 viewer provides a line-number gutter, viewer-scoped Ctrl/Cmd+F search with case
250 and whole-word options, copy support, and virtualized rendering above 100 lines.
251 Search marks are applied only to visible rows so query input does not rebuild the
252 entire highlighted document. Files above 512 KiB or 20,000 lines keep line
253 numbers, search, copy, and virtualization but use escaped plain text instead of
254 syntax highlighting. Workspace files are previewed up to 2 MiB; larger files
255 display the first 2 MiB with a localized truncation notice.
256
257 ## Multi-platform adaptation
258
259 One Chromium runtime (Electron) serves every OS, so the remaining platform work
260 is native-shell behavior, not per-engine rendering quirks:
261
262 - **Linux** — the shell runs with the Chromium sandbox (the `.deb` ships a
263 root-owned 4755 `chrome-sandbox`, never `--no-sandbox`). Close-to-background is
264 enabled only after a DBus health probe confirms a live StatusNotifierWatcher,
265 a registered visual host, and this app's registered StatusNotifierItem. If any
266 of them disappears while the main window is hidden, Reasonix presents the
267 window again until the tray recovers.
268 - **Windows** — the shell follows the OS light/dark setting. Remote Markdown
269 images are fetched by the Go backend with the configured proxy and re-served
270 from the local asset origin, so embedded images never bypass the configured
271 proxy. Image hosts must resolve locally to public addresses; direct,
272 HTTP(S)-proxy, and SOCKS-proxy connections are pinned to those vetted IPs
273 while preserving the original Host and TLS SNI.
274 - **macOS** — inset/hidden title bar; the CSS marks the top bar as an OS drag
275 region (the Electron build rewrites `--reasonix-draggable` to
276 `-webkit-app-region`) and leaves room for the traffic lights.
277 - **Renderer recovery** — the shell reloads a crashed renderer
278 (`render-process-gone`) and reports service state to the UI; a renderer that
279 never reports ready is presented anyway with a diagnostics trail instead of
280 staying hidden.
281 - **Theming** — colors are CSS variables gated on `prefers-color-scheme`, so the
282 UI follows the OS theme without native glue.
283 - **Fonts / offline** — system font stack only; no web-font fetches, so first paint
284 is instant and identical offline.
285 - **First paint** — the window background is set to the dark shell color so there's
286 no white flash before CSS loads.
287
288 ## Files
289
290 ```
291 desktop/
292 main.go service entry: host launch modes, shell bootstrap
293 host_rpc.go --host-rpc service over stdio + -emit-contract
294 app.go App (bound command surface) + eventSink (event.Sink)
295 wire.go event.Event → JSON wire form (mirrors internal/serve/wire.go)
296 electron/ Electron shell (main process, preload, browser surface)
297 packaging/ @electron/packager pipeline + packaged smoke test
298 frontend/
299 src/
300 lib/
301 types.ts wire contract (mirrors wire.go)
302 bridge.ts desktop host bridge + browser dev mock
303 desktopHost.ts the only module touching window.reasonixDesktop
304 useController.ts event-stream reducer + command surface (the hook)
305 components/
306 Transcript, Message, ToolCard, Composer, ApprovalModal, ContextGauge,
307 Markdown, CodeViewer, DiffView
308 editors/ PlainCode, PlainDiff ← editor seam impls (swap targets)
309 ```
310
311 ## Telemetry
312
313 The desktop app sends one anonymous ping per launch to `crash.reasonix.io`:
314 a random anonymous install id (generated locally and not an account id), app
315 version, OS, architecture, Windows build/revision or bounded Linux
316 distribution/kernel/session facts, and the renderer engine tag. When the
317 previous process ended abnormally, the next normal launch may also send a
318 bounded native diagnostic (lifecycle phase, symbolized stack, window failure
319 kind, and coarse device facts). Reports queued by the retired WebView2/WebKitGTK
320 shell still decode and forward unchanged after upgrade.
321 Panic values are removed and paths/secrets are scrubbed before the report is
322 queued. The install id is attached only while sending and is not stored in a
323 pending crash file. It never includes conversations, account data, API keys,
324 file contents, usernames, hostnames, GPU driver details, or full local paths.
325
326 Opt out any time: Settings > Updates > "Anonymous usage ping", or set
327 `telemetry = false` under `[desktop]` in the global config. Dev builds
328 never ping or upload queued native diagnostics. Frontend crash and
329 performance-pressure reports remain separate and are sent only when the user
330 clicks "Send report" on the diagnostic UI.
331
332 Aggregate quality metrics are also enabled by default and can be disabled from
333 Settings > Updates > "Share aggregate quality metrics", or by setting
334 `metrics = false` under `[desktop]`. These metrics are anonymous signal/bucket
335 counts, lifecycle/window failure buckets, and preference buckets; they never
336 include conversations, prompts, keys, paths, base URLs, or file contents.
337
337 lines MARKDOWN