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-devplus 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. Runwails doctorto verify.
Develop
cd desktop
wails dev # hot-reloads Go + frontend (Vite dev server)
Frontend-only iteration without the Go side:
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:
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:
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
createPortalplusgetBoundingClientRect(), it must handle scrollable ancestors, window resize, andvisualViewportchanges. - 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
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.
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<semver> (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+.
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.dmgand 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: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 CLI:
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 (<pre>) |
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 |
# 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.gokeepsWebviewGpuPolicy: OnDemandwhen a DRI render node is usable, and falls back toNeverfor xrdp/headless/software-rendered sessions that cannot access/dev/dri. If artifacts persist, launch withWEBKIT_DISABLE_COMPOSITING_MODE=1. Test on at least one GTK target before release; the CSS deliberately avoidsbackdrop-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=1when it detects Wayland + NVIDIA GPU. To opt out, set__NV_DISABLE_EXPLICIT_SYNC=0. Alternative fallbacks:WEBKIT_DISABLE_DMABUF_RENDERER=1(poor performance) orGDK_BACKEND=x11(forces XWayland).
- Wayland + NVIDIA: On KDE Plasma Wayland with NVIDIA GPUs, WebKitGTK can
crash at startup (
- Windows / WebView2 —
Theme: SystemDefaultfollows 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; setREASONIX_DESKTOP_DISABLE_WEBVIEW2_GPU=1or0to 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.