# 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 `libwebkit2gtk-4.0/4.1-dev` + `libgtk-3-dev`.
  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.

## Build

```sh
cd desktop
wails build          # → build/bin/Reasonix(.app/.exe)
```

`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, and mirrors everything to R2.

```sh
git tag desktop-v1.1.0 && git push origin desktop-v1.1.0
```

The app checks `latest.json` on startup (R2 first, GitHub as fallback) and shows
an update banner when a newer version is published; **Settings → Software update**
has a manual check. 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** — Gatekeeper may report the app "is damaged" or is from an
  unidentified developer. Clear the quarantine attribute, then 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` (`<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` |

```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` sets `WebviewGpuPolicy: OnDemand` to avoid blank/flickering
  webviews from forced compositing. 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).
- **Windows / WebView2** — `Theme: SystemDefault` follows the OS light/dark
  setting; the runtime must be installed (bundle it for distribution).
- **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)
```
