Skip to content

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).

The CLI provides revka desktop to find and launch the companion app, or to open the download page.

Terminal window
revka desktop # find and launch the installed desktop binary
revka desktop --install # open the download page (https://www.kumiho.io/download)
FlagMeaning
(none)Search for and launch the installed desktop binary. Exits with code 1 if not found.
--installOpen 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.

The Tauri app lives in apps/tauri/. Build it with the Tauri CLI:

Terminal window
cargo tauri dev

In dev mode the WebView loads http://127.0.0.1:5173 (the Vite dev server).

Key settings from apps/tauri/tauri.conf.json:

SettingValue
productNameRevka
identifierai.kumihoio.desktop
version2026.5.20 (CalVer, tracks the gateway release)
Window1200×800, resizable, starts hidden (shown after auto-pair)
Bundle targetsall — icons: 32x32.png, 128x128.png, icon.icns, icon.ico

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.

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:

  1. Check whether pairing is required. The app calls GET /health and reads the require_pairing boolean from the response body. If pairing is disabled, no token is needed and the flow stops.

  2. 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.

  3. 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.

  4. Exchange the code for a token. The app POSTs to POST /pair with header X-Pairing-Code: <code> and receives {"token": "<bearer>"}.

  5. Inject the token into the WebView. The token is written to the WebView’s localStorage under the key revka_token, so the React app finds it and skips the manual pairing dialog.

The relevant requests:

POST /admin/paircode/new HTTP/1.1
Host: 127.0.0.1:42617
{ "pairing_code": "123456" }
POST /pair HTTP/1.1
Host: 127.0.0.1:42617
X-Pairing-Code: 123456
{ "token": "<bearer-token>" }

For the underlying pairing model — codes, tokens, and device registration — see Pairing & authentication and Secrets, pairing & device auth.

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-changed with a bool payload.

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…
});

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().

StateIcon assetTooltipWhen
Disconnectedicons/tray-disconnected.pngRevka — DisconnectedGateway unreachable (/health fails)
Idleicons/tray-idle.pngRevka — IdleConnected, no agent running
Workingicons/tray-working.pngRevka — WorkingConnected, agent running
Erroricons/tray-error.pngRevka — ErrorConnected, 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.

Right-clicking the tray icon opens a menu. Left-clicking the icon directly shows and focuses the main window.

ItemIdAction
Show DashboardshowShow and focus the main window
Agent ChatchatShow the window and deep-link to the /agent route (window.location.hash = '/agent')
Status: Checking…statusInformational only — disabled, not clickable
Quit RevkaquitExit the process (app.exit(0))

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).

CommandCallsAuthNotes
get_statusGET /api/statusBearerGateway status snapshot
get_healthGET /healthnoneReturns a bool
list_channelsGET /api/statusBearerChannel info is part of the status payload
initiate_pairingPOST /api/pairing/initiateBearerStart a pairing flow from the UI
get_devicesGET /api/devicesBearerList paired devices
send_messagePOST /webhook with {"message": "<msg>"}BearerTriggers 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.

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).

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.

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.