返回 DeepSeek-Reasonix
README.md
根目录 / desktop / README.md
1 # Reasonix Desktop (Wails shell)
2
3 A native desktop window around the Reasonix Go kernel. The same
4 transport-agnostic `control.Controller` that backs the chat TUI and the HTTP/SSE
5 server is bound **directly** to a React webview — Go methods in, typed events
6 out, no HTTP hop.
7
8 ```
9 ┌─────────────────────────────────────────────────────────────┐
10 │ webview (React + TS, Vite) │
11 │ bridge.ts ──calls──▶ window.go.main.App.{Submit,Cancel,…} │
12 │ bridge.ts ◀─events── window.runtime.EventsOn("agent:event")│
13 └───────────────▲───────────────────────────┬─────────────────┘
14 bound methods runtime.EventsEmit
15 ┌───────────────┴───────────────────────────▼─────────────────┐
16 │ desktop/app.go App (bound) + eventSink (event.Sink) │
17 │ desktop/main.go Wails options, window, embed frontend/dist │
18 └───────────────▲───────────────────────────┬─────────────────┘
19 commands │ │ typed event stream
20 ┌───────────────┴────────────────────────────▼────────────────┐
21 │ internal/boot.Build → internal/control.Controller (kernel) │
22 │ (same assembly the CLI uses: providers, tools, gate, …) │
23 └──────────────────────────────────────────────────────────────┘
24 ```
25
26 ## Why a nested module
27
28 `desktop/` is its own Go module (`module reasonix/desktop`, `replace reasonix =>
29 ../`). That keeps the CGO + WebKit desktop build entirely separate from the CLI's
30 `CGO_ENABLED=0` single-static-binary guarantee: the parent module's `go build /
31 vet / test ./...` skip this directory, while the import path stays under
32 `reasonix/` so it can still import the `reasonix/internal/*` kernel.
33
34 ## Prerequisites
35
36 - Go (matches the parent module).
37 - Node + **pnpm** (`npm i -g pnpm`).
38 - Wails CLI: `go install github.com/wailsapp/wails/v2/cmd/wails@latest`
39 - Platform webview libs: macOS ships WebKit; Windows needs the Edge **WebView2**
40 runtime; Linux needs `libgtk-3-dev` plus WebKitGTK. The default build links
41 against **WebKitGTK 4.0**; distros that only ship **4.1** (Fedora 40+, Ubuntu
42 24.04+, Arch) build with `-tags webkit2_41` — see [Build](#build). Run
43 `wails doctor` to verify.
44
45 ## Develop
46
47 ```sh
48 cd desktop
49 wails dev # hot-reloads Go + frontend (Vite dev server)
50 ```
51
52 Frontend-only iteration without the Go side:
53
54 ```sh
55 cd desktop/frontend
56 pnpm install
57 pnpm dev # opens in a plain browser; bridge.ts uses the dev mock
58 ```
59
60 In a plain browser the native bindings are absent, so `bridge.ts` falls back to a
61 **mock** that streams a canned turn (text + one `edit_file` tool call) through the
62 exact same event contract — so layout, streaming, markdown, tool cards, and the
63 diff seam can all be built without rebuilding Go.
64
65 ## Test
66
67 The desktop package is a nested Go module, so parent `go test ./...` does not run
68 it. Use the full lane before merging desktop changes, and the short lane for fast
69 local feedback:
70
71 ```sh
72 make desktop-test # cd desktop && go test .
73 make desktop-test-short # skips slow desktop integration/e2e checks
74 ```
75
76 To find the next bottleneck, rank individual test cases from the JSON stream:
77
78 ```sh
79 make desktop-test-times
80 # or: cd desktop && go test -count=1 -json . | python3 ../scripts/desktop-test-times.py
81 ```
82
83 ### Frontend UI review checklist
84
85 For anchored menus, dropdowns, tooltips, and other portaled UI, review both the
86 component code and the CSS positioning contract:
87
88 - If a component uses `createPortal` plus `getBoundingClientRect()`, it must
89 handle scrollable ancestors, window resize, and `visualViewport` changes.
90 - Add a focused regression test when changing shared positioning primitives such
91 as `AnchoredPopover`, not only the specific menu that exposed the bug.
92 - Exercise at least one scrollable container path, such as Settings content, when
93 manually checking dropdown or popover changes.
94
95 ## Build
96
97 ```sh
98 cd desktop
99 wails build # → build/bin/Reasonix(.app/.exe)
100 ```
101
102 **Linux on WebKitGTK 4.1 only** (Fedora 40+, Ubuntu 24.04+, Arch — no
103 `webkit2gtk-4.0` package): pass the Wails build tag so cgo links against 4.1.
104
105 ```sh
106 wails build -tags webkit2_41
107 wails dev -tags webkit2_41 # same tag for hot-reload
108 ```
109
110 Fedora deps: `sudo dnf install webkit2gtk4.1-devel gtk3-devel`.
111
112 `frontend/dist` is generated by the build (it's git-ignored except for a
113 `.gitkeep` that keeps the Go `//go:embed all:frontend/dist` compilable on a fresh
114 checkout). A bare `go build` without a prior `pnpm build` produces a blank window.
115
116 ## Releases & auto-update
117
118 Desktop releases ride their own tag namespace, `desktop-v<semver>` (plain `v*`
119 tags are the CLI release). Pushing one triggers `.github/workflows/release-desktop.yml`,
120 which builds on a native runner per platform (Wails can't cross-compile a
121 CGO/WebKit binary), packages each artifact, signs it with minisign, generates a
122 `latest.json` manifest, publishes a GitHub release, marks the desktop release as
123 GitHub's repository-wide `Latest`, mirrors everything to R2, and attaches the
124 current desktop manifest to the matching CLI release for old clients that still
125 ask GitHub's repository-wide `latest` release for it.
126 The Linux artifact links against WebKitGTK 4.1 (`-tags webkit2_41`), so it needs
127 `libwebkit2gtk-4.1-0` at runtime — present by default on Ubuntu 22.04+, Fedora 40+.
128
129 ```sh
130 git tag desktop-v1.1.0 && git push origin desktop-v1.1.0
131 ```
132
133 The app checks `latest.json` on startup (R2 first, then the
134 `crash.reasonix.io` desktop release gateway) and shows an update banner when a
135 newer version is published; **Settings → Software update** has a manual check.
136 The gateway resolves only the desktop `desktop-v*` release line and never uses
137 GitHub's repository-wide `/releases/latest` shortcut, so updater behavior does
138 not depend on homepage badge semantics. Self-update behavior by platform:
139
140 - **Linux portable (`.tar.gz`)** — download, verify the minisign signature, replace
141 the binaries in the install directory, and relaunch through Guard. No elevation.
142 - **Linux Debian/Ubuntu (`.deb`)** — download the signed `.deb`, request administrator
143 authorization via Polkit (`pkexec`), re-verify and install with `apt-get
144 --only-upgrade`, then relaunch through Guard. The first build that ships the
145 update helper and Polkit policy is a one-time bootstrap: existing `.deb` users
146 should overwrite-install once with
147 `sudo apt install ./Reasonix-linux-amd64.deb` (no uninstall required). After
148 that, in-app authorized updates work. If Polkit/`pkexec` is unavailable, use
149 the same manual command. Failed installs leave the running app intact so you
150 can retry; successful installs are managed by apt/dpkg and are not auto-downgraded.
151 - **Windows** — download, verify the minisign signature, then run the per-user
152 NSIS installer (no admin rights needed).
153 - **macOS** — *not* self-updating yet. The build is unsigned/un-notarized, so an
154 in-place swap would be blocked by Gatekeeper; the banner links to the download
155 page for a manual update instead.
156
157 ### Code signing — first launch
158
159 - **Windows** — stable builds carry an Authenticode signature (SignPath, approved
160 per release; `release-desktop.yml` verifies every payload binary through
161 `scripts/verify-windows-authenticode.ps1` and fails the release otherwise). A
162 brand-new version can still show SmartScreen until the signature accumulates
163 reputation: *More info → Run anyway*.
164 - **macOS** — still unsigned and un-notarized. Open
165 `Reasonix-darwin-universal.dmg`, drag Reasonix into Applications, then clear the
166 quarantine attribute when Gatekeeper reports the app "is damaged" or is from an
167 unidentified developer:
168 ```sh
169 xattr -dr com.apple.quarantine /Applications/Reasonix.app
170 ```
171 This is also why macOS has no in-place self-update: the swap would be blocked.
172 Adding a Developer ID certificate flips the release workflow's `HAS_APPLE_CERT`
173 gate to the signed path and removes both.
174
175 ### Verifying a download
176
177 Artifacts are signed with minisign (public key ID `AF12CA46F4A9EBB0`). The `.minisig`
178 signature sits next to each artifact in the release; verify with the
179 [minisign](https://jedisct1.github.io/minisign/) CLI:
180
181 ```sh
182 minisign -Vm Reasonix-darwin-arm64.zip \
183 -P RWSw66n0RsoSr6Zhh6qt5YO95YkpCayTOCMFVDNUQSjJYwxoYngNVBSq
184 ```
185
186 ## Editor seams and workspace file previews
187
188 Code and diff rendering go through two components with stable prop contracts and
189 lazy boundaries, so heavier viewers stay out of the initial bundle. `CodeViewer`
190 keeps the compact highlighted viewer for chat, Markdown, and tool output, while
191 workspace file previews opt into the searchable line-number viewer:
192
193 | Component | Props | Default impl | Upgrade |
194 |---|---|---|---|
195 | `components/CodeViewer.tsx` | `EditorProps` | `editors/HljsCode.tsx`; `editors/LineNumberCode.tsx` when `showLineNumbers` is enabled | extend the implementation selection for Monaco or CodeMirror |
196 | `components/DiffView.tsx` | `DiffProps` | `editors/HljsDiff.tsx` (highlighted LCS/unified diff) | swap for `editors/MonacoDiff` or `editors/CodeMirrorMerge` |
197
198 ```sh
199 # Monaco
200 pnpm add @monaco-editor/react monaco-editor
201 # or CodeMirror 6
202 pnpm add @uiw/react-codemirror @codemirror/lang-javascript @codemirror/merge
203 ```
204
205 Then add `editors/MonacoCode.tsx` (default-export a component taking
206 `EditorProps`) and update the implementation selection in `CodeViewer.tsx`.
207 `ToolCard` already routes `edit_file` calls' `old_string`/`new_string` through
208 `DiffView`, and `Markdown` routes fenced code blocks through `CodeViewer`, so
209 both seams light up everywhere at once.
210
211 `WorkspacePanel` passes `showLineNumbers` for text-file previews. The resulting
212 viewer provides a line-number gutter, viewer-scoped Ctrl/Cmd+F search with case
213 and whole-word options, copy support, and virtualized rendering above 100 lines.
214 Search marks are applied only to visible rows so query input does not rebuild the
215 entire highlighted document. Files above 512 KiB or 20,000 lines keep line
216 numbers, search, copy, and virtualization but use escaped plain text instead of
217 syntax highlighting. Workspace files are previewed up to 2 MiB; larger files
218 display the first 2 MiB with a localized truncation notice.
219
220 ## Multi-platform adaptation
221
222 Wails is the right shell for a Go kernel (no sidecar), but a Go+webview stack uses
223 the **native** webview per OS, so the rough edges are platform-specific. What's
224 handled here, and what to reach for if a target misbehaves:
225
226 - **Linux / WebKitGTK** is the one real pain point — rendering varies by distro &
227 GPU driver. `main.go` keeps `WebviewGpuPolicy: OnDemand` when a DRI render node
228 is usable, and falls back to `Never` for xrdp/headless/software-rendered sessions
229 that cannot access `/dev/dri`. If artifacts persist, launch with
230 `WEBKIT_DISABLE_COMPOSITING_MODE=1`. Test on at least one GTK target before release;
231 the CSS deliberately avoids `backdrop-filter`/blur (slow & inconsistent there).
232 - **Wayland + NVIDIA**: On KDE Plasma Wayland with NVIDIA GPUs, WebKitGTK can
233 crash at startup (`Error 71: Protocol error`) due to an upstream WebKit
234 explicit-sync bug (WebKit #280210, #317089, NVIDIA/egl-wayland #179).
235 Reasonix automatically sets `__NV_DISABLE_EXPLICIT_SYNC=1` when it detects
236 Wayland + NVIDIA GPU. To opt out, set `__NV_DISABLE_EXPLICIT_SYNC=0`.
237 Alternative fallbacks: `WEBKIT_DISABLE_DMABUF_RENDERER=1` (poor performance)
238 or `GDK_BACKEND=x11` (forces XWayland).
239 - **Windows / WebView2** — `Theme: SystemDefault` follows the OS light/dark
240 setting; the installer embeds the WebView2 bootstrapper. Canary builds disable
241 WebView2 GPU acceleration by default to smoke-test blank-window reports; set
242 `REASONIX_DESKTOP_DISABLE_WEBVIEW2_GPU=1` or `0` to force the fallback on or
243 off. The WebView2 shell always uses a direct connection for embedded assets
244 and loopback remote-workspace pages; provider and other outbound traffic keeps
245 using Reasonix's own proxy configuration. Remote Markdown images are fetched
246 by the Go backend with the same proxy settings and re-served from the local
247 asset origin, so WebView2 never bypasses the configured proxy for them. Image
248 hosts must resolve locally to public addresses; direct, HTTP(S)-proxy, and
249 SOCKS-proxy connections are pinned to those vetted IPs while preserving the
250 original Host and TLS SNI. If the DOM is still not ready after 15 seconds, the
251 hidden startup window is shown with a native recovery prompt.
252 - **macOS / WebKit** — inset/hidden title bar (`TitleBarHiddenInset`); the CSS
253 marks the top bar as an OS drag region (`--wails-draggable: drag`) and leaves
254 room for the traffic lights.
255 - **Theming** — colors are CSS variables gated on `prefers-color-scheme`, which all
256 three webviews honor, so the UI follows the OS theme without native glue.
257 - **Fonts / offline** — system font stack only; no web-font fetches, so first paint
258 is instant and identical offline.
259 - **First paint** — the window background is set to the dark shell color so there's
260 no white flash before CSS loads (most visible on WebKitGTK).
261
262 ## Files
263
264 ```
265 desktop/
266 main.go Wails options, window, embed frontend/dist
267 app.go App (bound command surface) + eventSink (event.Sink → webview)
268 wire.go event.Event → JSON wire form (mirrors internal/serve/wire.go)
269 wails.json Wails project config (pnpm install/build/dev)
270 frontend/
271 src/
272 lib/
273 types.ts wire contract (mirrors wire.go)
274 bridge.ts window.go/window.runtime wrapper + browser dev mock
275 useController.ts event-stream reducer + command surface (the hook)
276 components/
277 Transcript, Message, ToolCard, Composer, ApprovalModal, ContextGauge,
278 Markdown, CodeViewer, DiffView
279 editors/ PlainCode, PlainDiff ← editor seam impls (swap targets)
280 ```
281
282 ## Telemetry
283
284 The desktop app sends one anonymous ping per launch to `crash.reasonix.io`:
285 a random install id (generated locally, tied to nothing), app version, OS,
286 arch, and OS version. When the previous process ended abnormally, the next
287 normal launch may also send a bounded native diagnostic (lifecycle phase,
288 symbolized stack, WebView2/window failure kind, and coarse device facts).
289 Panic values are removed and paths/secrets are scrubbed before the report is
290 queued. It never includes conversations, API keys, or file contents.
291
292 Opt out any time: Settings > Updates > "Anonymous usage ping", or set
293 `telemetry = false` under `[desktop]` in the global config. Dev builds
294 never ping or upload queued native diagnostics. Frontend crash and
295 performance-pressure reports remain separate and are sent only when the user
296 clicks "Send report" on the diagnostic UI.
297
298 Aggregate quality metrics are also enabled by default and can be disabled from
299 Settings > Updates > "Share aggregate quality metrics", or by setting
300 `metrics = false` under `[desktop]`. These metrics are anonymous signal/bucket
301 counts, lifecycle/window failure buckets, and preference buckets; they never
302 include conversations, prompts, keys, paths, base URLs, or file contents.
303
303 lines MARKDOWN