Desktop app
The Tauri desktop wrapper: auto-pairing, tray icon states, IPC commands, and capabilities.
The Revka desktop app is a native, cross-platform wrapper (macOS, Linux, Windows) built with Tauri v2. It hosts the same web dashboard you’d open in a browser inside a native WebView, but adds three things a browser can’t: it auto-pairs with your locally running gateway so you never paste a token, it lives in the system tray with an icon that reflects agent status in real time, and it keeps a background health poller running so the tray always shows whether the gateway is up.
Use the desktop app when you want the dashboard always-on in your menu bar / tray instead of a browser tab. It is a thin client: it does no agent work itself — it talks to the gateway over HTTP and WebSocket on 127.0.0.1. The gateway must be running first (see Run the dashboard and revka gateway, daemon & service).
Launch or install the app
Section titled “Launch or install the app”The CLI provides revka desktop to find and launch the companion app, or to open the download page.
revka desktop # find and launch the installed desktop binaryrevka desktop --install # open the download page (https://www.kumiho.io/download)| Flag | Meaning |
|---|---|
| (none) | Search for and launch the installed desktop binary. Exits with code 1 if not found. |
--install | Open the download page; on macOS/Linux also launches the browser to https://www.kumiho.io/download. |
Without --install, revka desktop searches these locations in order: /Applications/Revka.app (macOS), the same directory as the CLI binary, ~/.cargo/bin/, ~/.local/bin/, then your PATH. The launched app connects to the local gateway at http://127.0.0.1:42617/_app/.
See revka install, update, migrate… for the full lifecycle command reference.
Build from source
Section titled “Build from source”The Tauri app lives in apps/tauri/. Build it with the Tauri CLI:
cargo tauri devIn dev mode the WebView loads http://127.0.0.1:5173 (the Vite dev server).
cargo tauri buildA release build bundles the dashboard served from the gateway at http://127.0.0.1:42617/_app/ and produces installers for all bundle targets.
Key settings from apps/tauri/tauri.conf.json:
| Setting | Value |
|---|---|
productName | Revka |
identifier | ai.kumihoio.desktop |
version | 2026.5.20 (CalVer, tracks the gateway release) |
| Window | 1200×800, resizable, starts hidden (shown after auto-pair) |
| Bundle targets | all — icons: 32x32.png, 128x128.png, icon.icns, icon.ico |
Content Security Policy
Section titled “Content Security Policy”The app’s CSP restricts all network access to localhost. This is the desktop app’s primary network guardrail — the WebView cannot reach any remote origin:
"csp": "default-src 'self' http://127.0.0.1:* ws://127.0.0.1:*; connect-src 'self' http://127.0.0.1:* ws://127.0.0.1:*; script-src 'self' 'unsafe-inline' http://127.0.0.1:*; style-src 'self' 'unsafe-inline' http://127.0.0.1:*; img-src 'self' http://127.0.0.1:* data:"WebSocket connections are likewise limited to ws://127.0.0.1:*, which covers the gateway’s /ws/chat, /ws/canvas/{id}, and /ws/nodes endpoints.
Auto-pair with the gateway
Section titled “Auto-pair with the gateway”When pairing is enabled on the gateway (require_pairing = true), browser clients normally show a one-time pairing-code dialog. The desktop app skips this entirely: on startup it auto-pairs over localhost and injects the resulting token into the WebView, so the React frontend mounts already authenticated.
The flow runs in a background task on app start:
-
Check whether pairing is required. The app calls
GET /healthand reads therequire_pairingboolean from the response body. If pairing is disabled, no token is needed and the flow stops. -
Reuse a valid token if one exists. If the app already holds a token in its state, it validates it with
GET /api/status(Bearer auth). A valid token is reused as-is. -
Request a fresh pairing code. With no valid token, the app POSTs to the admin-only endpoint
POST /admin/paircode/new, which returns{"pairing_code": "<code>"}. This endpoint is reachable only from localhost, which is what makes silent auto-pair safe. -
Exchange the code for a token. The app POSTs to
POST /pairwith headerX-Pairing-Code: <code>and receives{"token": "<bearer>"}. -
Inject the token into the WebView. The token is written to the WebView’s
localStorageunder the keyrevka_token, so the React app finds it and skips the manual pairing dialog.
The relevant requests:
POST /admin/paircode/new HTTP/1.1Host: 127.0.0.1:42617{ "pairing_code": "123456" }POST /pair HTTP/1.1Host: 127.0.0.1:42617X-Pairing-Code: 123456{ "token": "<bearer-token>" }For the underlying pairing model — codes, tokens, and device registration — see Pairing & authentication and Secrets, pairing & device auth.
Background health poller
Section titled “Background health poller”A background async task polls the gateway every 5 seconds (hardcoded POLL_INTERVAL = 5s) and keeps the rest of the app in sync with reality.
On each tick the poller:
- Calls
GET /health— success means connected, any failure means disconnected. - Updates shared state (
connected: bool). - Updates the system tray icon and tooltip to match the current state.
- Emits a Tauri event
revka://status-changedwith aboolpayload.
Frontend code running in the WebView can listen for revka://status-changed to drive its own connection indicator without polling the gateway itself:
import { listen } from "@tauri-apps/api/event";
await listen("revka://status-changed", (event) => { const connected = event.payload; // boolean // update UI…});System tray icon and menu
Section titled “System tray icon and menu”The app installs a persistent tray icon (id main) that reflects connection and agent status in real time. Four embedded 22×22 RGBA PNGs are selected by the health poller via set_icon() / set_tooltip().
Icon states
Section titled “Icon states”| State | Icon asset | Tooltip | When |
|---|---|---|---|
| Disconnected | icons/tray-disconnected.png | Revka — Disconnected | Gateway unreachable (/health fails) |
| Idle | icons/tray-idle.png | Revka — Idle | Connected, no agent running |
| Working | icons/tray-working.png | Revka — Working | Connected, agent running |
| Error | icons/tray-error.png | Revka — Error | Connected, agent reported an error |
The disconnected state takes priority: when the gateway is unreachable the tooltip is always Revka — Disconnected regardless of the last known agent status.
Tray menu
Section titled “Tray menu”Right-clicking the tray icon opens a menu. Left-clicking the icon directly shows and focuses the main window.
| Item | Id | Action |
|---|---|---|
| Show Dashboard | show | Show and focus the main window |
| Agent Chat | chat | Show the window and deep-link to the /agent route (window.location.hash = '/agent') |
| Status: Checking… | status | Informational only — disabled, not clickable |
| Quit Revka | quit | Exit the process (app.exit(0)) |
Tauri commands (IPC)
Section titled “Tauri commands (IPC)”The app exposes six Tauri commands as an IPC bridge from the React frontend to native Rust. The frontend calls them with invoke() and never has to manage auth headers — the bearer token lives in native shared state and is attached to every gateway request automatically. Each command is async and returns Result<serde_json::Value, String> (or Result<bool, String> for get_health).
| Command | Calls | Auth | Notes |
|---|---|---|---|
get_status | GET /api/status | Bearer | Gateway status snapshot |
get_health | GET /health | none | Returns a bool |
list_channels | GET /api/status | Bearer | Channel info is part of the status payload |
initiate_pairing | POST /api/pairing/initiate | Bearer | Start a pairing flow from the UI |
get_devices | GET /api/devices | Bearer | List paired devices |
send_message | POST /webhook with {"message": "<msg>"} | Bearer | Triggers the agent pipeline via the webhook ingress |
Example call from the WebView:
import { invoke } from "@tauri-apps/api/core";
const status = await invoke("get_status");await invoke("send_message", { message: "Summarize today's logs" });These IPC commands are the supported surface for building on top of the desktop app. The endpoints they wrap are documented under the Gateway API — see Status, health, config & tools endpoints and Pairing & authentication.
Capability permissions
Section titled “Capability permissions”Tauri capabilities declare what the WebView is allowed to do. The app ships two capability files in apps/tauri/capabilities/, both scoped to the main window:
{ "identifier": "default", "windows": ["main"], "permissions": [ "core:default", "shell:allow-open", "store:allow-get", "store:allow-set", "store:allow-save", "store:allow-load" ]}Base permissions: core operations, opening URLs in the system browser (shell:allow-open), and reading/writing the persistent store (store:allow-get/set/save/load).
{ "identifier": "desktop", "windows": ["main"], "permissions": [ "core:default", "shell:allow-open", "shell:allow-execute", "store:allow-get", "store:allow-set", "store:allow-save", "store:allow-load" ]}Adds shell:allow-execute on top of the default set, allowing the desktop build to run system commands for advanced features.
Capabilities and the CSP form two complementary layers of the desktop security model: the CSP restricts which network origins the WebView can reach (localhost only), while capabilities restrict which native operations it can invoke. For the broader picture, see the Security model.
Single-instance enforcement
Section titled “Single-instance enforcement”The app uses tauri_plugin_single_instance so only one instance ever runs. Launching the app (or revka desktop) when it is already running in the tray does not start a duplicate — instead the existing instance’s main window is shown and focused. This is automatic and requires no configuration. It’s why re-running revka desktop just brings the already-running window to the front.