Building a desktop client for an AI coding agent A developer built a native desktop client for xAI's open-source Rust coding agent grok-build, using Tauri 2 for an ~8 MB binary with a React frontend and Rust runtime that communicates with the CLI over JSON-RPC 2.0. The project, available on GitHub under MIT license, aims to provide a real desktop UX while preserving the CLI's capabilities. Lessons from wrapping grok-build — the architecture, the traps, and why we picked Tauri over Electron. grok-build is xAI's open-source Rust coding agent. It ships as a TUI. We wrote a native desktop client for it — Tauri 2 ~8 MB binary , React frontend, Rust runtime that spawns the CLI as a child process and talks to it over ACP/JSON-RPC 2.0. This post is the architecture deep-dive: how the pieces fit together, what surprised us, and the parts we'd build differently next time. The full source is at github.com/timexingxin/grok-gui https://github.com/timexingxin/grok-gui . MIT-licensed. Demo GIF in the README. grok-build is genuinely good at code work — comparable to Claude Code for my workflow. But it ships as a Rust TUI. After six months of cmd+tab between the terminal and my browser tabs, I wanted a real desktop UX without losing what makes the CLI good. The naive options all had problems: The right answer was staring at me: grok-build already has a JSON-RPC 2.0 over stdio interface called the Agent Client Protocol ACP . That's the protocol I should be a client of. My job is just to write the client. ACP https://agentclientprotocol.com/ is a JSON-RPC 2.0 protocol that coding-agent CLIs expose over their stdin/stdout. The agent emits notifications text deltas, tool calls, plan updates, permission requests, session lifecycle ; the client sends requests user prompts, permission responses, model switches, session loads . If your agent speaks ACP, you can write a client without re-implementing the agent loop. You just connect to the stdio, parse the JSON frames, and render. ┌──────────────────────────────────────────┐ │ Desktop Shell Tauri 2, ~8 MB │ React + Vite + Tailwind └──────────────┬───────────────────────────┘ │ Tauri commands + events typed ▼ ┌──────────────────────────────────────────┐ │ Rust bridge apps/desktop/src-tauri/src/grok runtime.rs │ - spawns grok agent stdio as a child │ │ - JSON-RPC 2.0 over its stdin/stdout │ │ - LANG/LC ALL forwarded for locale │ │ - manages a pool of live runtimes │ └──────────────┬───────────────────────────┘ │ subprocess stdin/stdout ▼ ┌──────────────────────────────────────────┐ │ Grok Build runtime xai-org/grok-build │ upstream, Apache-2.0 │ Agent loop, tools, context, MCP, skills │ └──────────────────────────────────────────┘ The Rust bridge is the interesting part. It manages the lifecycle: spawn the child, do the initialize handshake where the agent tells us its version and supported features , do a session/new or session/load, stream every event into the frontend. The frontend just listens for typed events and renders. I considered both. The decision matrix: | Tauri 2 | Electron | | |---|---|---| Binary size | ~8 MB | ~150 MB | Memory idle | ~80 MB | ~300 MB | Webview | OS-native WebKit / WebView2 | Bundled Chromium | Native feel | Closer real OS widgets | Web app | Backend language | Rust | Node.js | IPC overhead | Function call typed structs | JSON-over-IPC | Tauri won for three reasons: The backend was already going to be Rust. grok-build is Rust, the ACP client had to be Rust to spawn and talk to it properly, and Rust is a good fit for the long-running process-pool pattern. Electron would have meant Rust + Node.js in the same project. 8 MB vs 150 MB matters for distribution. A coding tool installer shouldn't be larger than the Electron runtime itself is on disk. The 150 MB Electron app feels heavy on a MacBook where the rest of the system is small native binaries. OS-native webview feels right. WebKit on macOS renders identically to Safari, which means the React app looks like a Mac app, not a Windows-95 webpage. The title-bar-style="hiddenInset" The tradeoff: Tauri requires the MSVC build tools on Windows. But windows-latest GitHub Actions runners have those preinstalled, so the CI cost is the same. The killer feature for me is having multiple conversations going at once. If I'm asking Grok to refactor one file while waiting on a different task, I don't want to lose the first turn's stream when I switch. Naive implementation: spawn N CLI processes on app startup. Wasteful for users who only have one session going. What I built: a pool of live runtimes , LRU-evicted on idle. // pseudocode struct RuntimePool { runtimes: HashMap