# Reasonix Desktop (Wails shell) A native desktop window around the Reasonix Go kernel. The same transport-agnostic `control.Controller` that backs the chat TUI and the HTTP/SSE server is bound **directly** to a React webview — Go methods in, typed events out, no HTTP hop. ``` ┌─────────────────────────────────────────────────────────────┐ │ webview (React + TS, Vite) │ │ bridge.ts ──calls──▶ window.go.main.App.{Submit,Cancel,…} │ │ bridge.ts ◀─events── window.runtime.EventsOn("agent:event")│ └───────────────▲───────────────────────────┬─────────────────┘ bound methods runtime.EventsEmit ┌───────────────┴───────────────────────────▼─────────────────┐ │ desktop/app.go App (bound) + eventSink (event.Sink) │ │ desktop/main.go Wails options, window, embed frontend/dist │ └───────────────▲───────────────────────────┬─────────────────┘ commands │ │ typed event stream ┌───────────────┴────────────────────────────▼────────────────┐ │ internal/boot.Build → internal/control.Controller (kernel) │ │ (same assembly the CLI uses: providers, tools, gate, …) │ └──────────────────────────────────────────────────────────────┘ ``` ## Why a nested module `desktop/` is its own Go module (`module reasonix/desktop`, `replace reasonix => ../`). That keeps the CGO + WebKit desktop build entirely separate from the CLI's `CGO_ENABLED=0` single-static-binary guarantee: the parent module's `go build / vet / test ./...` skip this directory, while the import path stays under `reasonix/` so it can still import the `reasonix/internal/*` kernel. ## Prerequisites - Go (matches the parent module). - Node + **pnpm** (`npm i -g pnpm`). - Wails CLI: `go install github.com/wailsapp/wails/v2/cmd/wails@latest` - Platform webview libs: macOS ships WebKit; Windows needs the Edge **WebView2** runtime; Linux needs `libgtk-3-dev` plus WebKitGTK. The default build links against **WebKitGTK 4.0**; distros that only ship **4.1** (Fedora 40+, Ubuntu 24.04+, Arch) build with `-tags webkit2_41` — see [Build](#build). Run `wails doctor` to verify. ## Develop ```sh cd desktop wails dev # hot-reloads Go + frontend (Vite dev server) ``` Frontend-only iteration without the Go side: ```sh cd desktop/frontend pnpm install pnpm dev # opens in a plain browser; bridge.ts uses the dev mock ``` In a plain browser the native bindings are absent, so `bridge.ts` falls back to a **mock** that streams a canned turn (text + one `edit_file` tool call) through the exact same event contract — so layout, streaming, markdown, tool cards, and the diff seam can all be built without rebuilding Go. ## Test The desktop package is a nested Go module, so parent `go test ./...` does not run it. Use the full lane before merging desktop changes, and the short lane for fast local feedback: ```sh make desktop-test # cd desktop && go test . make desktop-test-short # skips slow desktop integration/e2e checks ``` To find the next bottleneck, rank individual test cases from the JSON stream: ```sh make desktop-test-times # or: cd desktop && go test -count=1 -json . | python3 ../scripts/desktop-test-times.py ``` ### Frontend UI review checklist For anchored menus, dropdowns, tooltips, and other portaled UI, review both the component code and the CSS positioning contract: - If a component uses `createPortal` plus `getBoundingClientRect()`, it must handle scrollable ancestors, window resize, and `visualViewport` changes. - Add a focused regression test when changing shared positioning primitives such as `AnchoredPopover`, not only the specific menu that exposed the bug. - Exercise at least one scrollable container path, such as Settings content, when manually checking dropdown or popover changes. ## Build ```sh cd desktop wails build # → build/bin/Reasonix(.app/.exe) ``` **Linux on WebKitGTK 4.1 only** (Fedora 40+, Ubuntu 24.04+, Arch — no `webkit2gtk-4.0` package): pass the Wails build tag so cgo links against 4.1. ```sh wails build -tags webkit2_41 wails dev -tags webkit2_41 # same tag for hot-reload ``` Fedora deps: `sudo dnf install webkit2gtk4.1-devel gtk3-devel`. `frontend/dist` is generated by the build (it's git-ignored except for a `.gitkeep` that keeps the Go `//go:embed all:frontend/dist` compilable on a fresh checkout). A bare `go build` without a prior `pnpm build` produces a blank window. ## Releases & auto-update Desktop releases ride their own tag namespace, `desktop-v` (plain `v*` tags are the CLI release). Pushing one triggers `.github/workflows/release-desktop.yml`, which builds on a native runner per platform (Wails can't cross-compile a CGO/WebKit binary), packages each artifact, signs it with minisign, generates a `latest.json` manifest, publishes a GitHub release, marks the desktop release as GitHub's repository-wide `Latest`, mirrors everything to R2, and attaches the current desktop manifest to the matching CLI release for old clients that still ask GitHub's repository-wide `latest` release for it. The Linux artifact links against WebKitGTK 4.1 (`-tags webkit2_41`), so it needs `libwebkit2gtk-4.1-0` at runtime — present by default on Ubuntu 22.04+, Fedora 40+. ```sh git tag desktop-v1.1.0 && git push origin desktop-v1.1.0 ``` The app checks `latest.json` on startup (R2 first, then the `crash.reasonix.io` desktop release gateway) and shows an update banner when a newer version is published; **Settings → Software update** has a manual check. The gateway resolves only the desktop `desktop-v*` release line and never uses GitHub's repository-wide `/releases/latest` shortcut, so updater behavior does not depend on homepage badge semantics. Self-update behavior by platform: - **Linux / Windows** — download, verify the minisign signature, then update in place: Linux replaces the binary and relaunches; Windows runs the per-user NSIS installer (no admin rights needed). - **macOS** — *not* self-updating yet. The build is unsigned/un-notarized, so an in-place swap would be blocked by Gatekeeper; the banner links to the download page for a manual update instead. ### Unsigned builds — first launch There are no Apple/Windows code-signing certificates yet, so a downloaded build trips the OS gatekeepers on first run: - **macOS** — open `Reasonix-darwin-universal.dmg` and drag Reasonix into Applications. Gatekeeper may then report the app "is damaged" or is from an unidentified developer; clear the quarantine attribute and open it: ```sh xattr -dr com.apple.quarantine /Applications/Reasonix.app ``` - **Windows** — SmartScreen shows "Windows protected your PC". Click *More info → Run anyway*. When Developer ID / Authenticode certificates are added, the release workflow's `HAS_APPLE_CERT` gate flips to the signed path and these steps go away. ### Verifying a download Artifacts are signed with minisign (public key ID `AF12CA46F4A9EBB0`). The `.minisig` signature sits next to each artifact in the release; verify with the [minisign](https://jedisct1.github.io/minisign/) CLI: ```sh minisign -Vm Reasonix-darwin-arm64.zip \ -P RWSw66n0RsoSr6Zhh6qt5YO95YkpCayTOCMFVDNUQSjJYwxoYngNVBSq ``` ## Editor seam (Monaco / CodeMirror) Code and diff rendering go through two components with stable prop contracts and a lazy boundary, so a heavy editor stays out of the initial bundle and dropping one in is a one-line change — no consumer touches: | Component | Props | Default impl | Upgrade | |---|---|---|---| | `components/CodeViewer.tsx` | `EditorProps` | `editors/PlainCode.tsx` (`
`) | swap the lazy import for `editors/MonacoCode` or `editors/CodeMirrorCode` |
| `components/DiffView.tsx` | `DiffProps` | `editors/PlainDiff.tsx` (LCS line diff) | swap for `editors/MonacoDiff` or `editors/CodeMirrorMerge` |

```sh
# Monaco
pnpm add @monaco-editor/react monaco-editor
# or CodeMirror 6
pnpm add @uiw/react-codemirror @codemirror/lang-javascript @codemirror/merge
```

Then add `editors/MonacoCode.tsx` (default-export a component taking
`EditorProps`) and point `CodeViewer.tsx`'s `lazy(() => import(...))` at it.
`ToolCard` already routes `edit_file` calls' `old_string`/`new_string` through
`DiffView`, and `Markdown` routes fenced code blocks through `CodeViewer`, so
both seams light up everywhere at once.

Markdown itself is currently minimal (fenced code + plain text). Upgrade path:
`pnpm add react-markdown remark-gfm` and render in `components/Markdown.tsx`,
keeping fenced code delegated to `CodeViewer`.

## Multi-platform adaptation

Wails is the right shell for a Go kernel (no sidecar), but a Go+webview stack uses
the **native** webview per OS, so the rough edges are platform-specific. What's
handled here, and what to reach for if a target misbehaves:

- **Linux / WebKitGTK** is the one real pain point — rendering varies by distro &
  GPU driver. `main.go` keeps `WebviewGpuPolicy: OnDemand` when a DRI render node
  is usable, and falls back to `Never` for xrdp/headless/software-rendered sessions
  that cannot access `/dev/dri`. If artifacts persist, launch with
  `WEBKIT_DISABLE_COMPOSITING_MODE=1`. Test on at least one GTK target before release;
  the CSS deliberately avoids `backdrop-filter`/blur (slow & inconsistent there).
  - **Wayland + NVIDIA**: On KDE Plasma Wayland with NVIDIA GPUs, WebKitGTK can
    crash at startup (`Error 71: Protocol error`) due to an upstream WebKit
    explicit-sync bug (WebKit #280210, #317089, NVIDIA/egl-wayland #179).
    Reasonix automatically sets `__NV_DISABLE_EXPLICIT_SYNC=1` when it detects
    Wayland + NVIDIA GPU. To opt out, set `__NV_DISABLE_EXPLICIT_SYNC=0`.
    Alternative fallbacks: `WEBKIT_DISABLE_DMABUF_RENDERER=1` (poor performance)
    or `GDK_BACKEND=x11` (forces XWayland).
- **Windows / WebView2** — `Theme: SystemDefault` follows the OS light/dark
  setting; the installer embeds the WebView2 bootstrapper. Canary builds disable
  WebView2 GPU acceleration by default to smoke-test blank-window reports; set
  `REASONIX_DESKTOP_DISABLE_WEBVIEW2_GPU=1` or `0` to force the fallback on or
  off.
- **macOS / WebKit** — inset/hidden title bar (`TitleBarHiddenInset`); the CSS
  marks the top bar as an OS drag region (`--wails-draggable: drag`) and leaves
  room for the traffic lights.
- **Theming** — colors are CSS variables gated on `prefers-color-scheme`, which all
  three webviews honor, so the UI follows the OS theme without native glue.
- **Fonts / offline** — system font stack only; no web-font fetches, so first paint
  is instant and identical offline.
- **First paint** — the window background is set to the dark shell color so there's
  no white flash before CSS loads (most visible on WebKitGTK).

## Files

```
desktop/
  main.go            Wails options, window, embed frontend/dist
  app.go             App (bound command surface) + eventSink (event.Sink → webview)
  wire.go            event.Event → JSON wire form (mirrors internal/serve/wire.go)
  wails.json         Wails project config (pnpm install/build/dev)
  frontend/
    src/
      lib/
        types.ts         wire contract (mirrors wire.go)
        bridge.ts        window.go/window.runtime wrapper + browser dev mock
        useController.ts event-stream reducer + command surface (the hook)
      components/
        Transcript, Message, ToolCard, Composer, ApprovalModal, ContextGauge,
        Markdown, CodeViewer, DiffView
        editors/  PlainCode, PlainDiff   ← editor seam impls (swap targets)
```

## Telemetry

The desktop app sends one anonymous ping per launch to `crash.reasonix.io`:
a random install id (generated locally, tied to nothing), app version, OS,
arch, and OS version. It exists solely to count active installs. It never
includes conversations, API keys, file contents, or paths.

Opt out any time: Settings > Updates > "Anonymous usage ping", or set
`telemetry = false` under `[desktop]` in the global config. Dev builds
never ping. Crash and performance-pressure reports are separate and only
ever sent when the user clicks "Send report" on the diagnostic UI.

Aggregate quality metrics are also enabled by default and can be disabled from
Settings > Updates > "Share aggregate quality metrics", or by setting
`metrics = false` under `[desktop]`. These metrics are anonymous signal/bucket
counts and preference buckets; they never include conversations, prompts, keys,
paths, base URLs, or file contents.

When Memory v5 is enabled, the same aggregate metrics pipeline may include only
content-free count/size buckets such as injection on/off, compiled-token bucket,
IR-overhead bucket, memory-reference count, constraint/risk/step counts, and
memory-graph size buckets. It never uploads memory text, tool outputs, prompts,
file paths, IDs, keys, base URLs, or file contents. The Memory v5 runtime itself
is controlled from Settings > General > "Memory v5" and shares the user/global
`agent.memory_compiler.enabled` setting with the CLI/TUI and `reasonix serve`;
CLI users can also run `/memory-v5 off|observe|compact|on|status` in a session
or `reasonix config memory-v5 off|observe|compact|on|status` from a shell.