Desktop Client

The Diminuendo desktop client (@igentai/dim-desktop) is a native application built on Tauri v2, and it exists for a reason the web client cannot satisfy: the WebSocket connection belongs in a process that does not pause when a tab loses focus, does not contend with JavaScript’s single-threaded event loop for I/O scheduling, and has direct access to the local filesystem for Chronicle workspace synchronization. The Tauri Rust backend holds the WebSocket via tokio-tungstenite, runs a dedicated tokio task for event ingestion, and exposes the connection to the React frontend through IPC — the same React frontend, the same Zustand stores, the same 70+ components. Only the transport layer differs.

Tech Stack

TechnologyVersionRole
Tauriv2 (@tauri-apps/cli ^2.2)Native desktop shell with Rust backend
@tauri-apps/api^2.2Frontend IPC bindings (invoke, listen)
React19Component model and rendering
TypeScript5.xCompile-time type safety
Tailwind CSSv4Utility-first styling
Effect^3.12Typed effects for the Gateway Adapter abstraction
Zustand5Selector-based state management
Diminuendo Rust SDKlocalWebSocket client in the Rust backend process

Architecture

The desktop client inverts the web client’s connection topology. Where the browser holds the WebSocket directly, the desktop client delegates that responsibility to a Rust process that operates outside the webview’s lifecycle. The React frontend communicates with this Rust backend through Tauri’s IPC mechanism: invoke() for commands (frontend-to-backend), listen() for events (backend-to-frontend).
Tauri Window (webview)
  +-- React Components (from @igentai/dim-shared)
  +-- Zustand Stores (from @igentai/dim-shared)
  +-- TauriGatewayAdapter
  |     +-- invoke() --> Rust #[tauri::command] handlers
  |     +-- listen("gateway-event") <-- Rust app_handle.emit()
  |
Tauri Rust Backend (src-tauri/)
  +-- DiminuendoClient (Rust SDK)
  |     +-- WebSocket --> Gateway
  +-- tokio event loop
  |     +-- event receiver --> emit("gateway-event")
  +-- Tauri managed state (Arc<Mutex<DiminuendoClient>>)
The Rust backend spawns a tokio task that reads from the Rust SDK’s mpsc::UnboundedReceiver<ServerEvent> and emits each event to the frontend via Tauri’s event system. The frontend’s TauriGatewayAdapter listens for these events and forwards them into the shared Zustand stores — the same stores that the web client uses, consuming the same GatewayEvent type through the same GatewayAdapter interface.

The TauriGatewayAdapter

The TauriGatewayAdapter implements the same GatewayAdapter interface as the web client’s WebGatewayAdapter, but bridges to the Rust backend using Tauri’s IPC primitives rather than wrapping the TypeScript SDK:
  • Commands are sent via invoke() from @tauri-apps/api/core. Each invoke call maps to a #[tauri::command] function in the Rust backend that calls the corresponding method on the Rust SDK’s DiminuendoClient.
  • Events are received via listen() from @tauri-apps/api/event. The Rust backend’s event handler emits gateway-event Tauri events, which the adapter consumes and forwards to the Effect stream.
connect(url: string, token?: string): Effect.Effect<void, GatewayError> {
  return Effect.tryPromise({
    try: () => invoke("gateway_connect", { url, token }),
    catch: (err) => new GatewayError("CONNECTION_FAILED", String(err)),
  })
}
The Rust backend holds the DiminuendoClient in Tauri’s managed state, typically wrapped in Arc<Mutex<...>>. Each #[tauri::command] handler retrieves the client from state and calls the corresponding SDK method. The event receiver task calls app_handle.emit("gateway-event", &event) to serialize and deliver each event to the webview.

Data Flow

Command Path (Frontend to Gateway)

1

User Action

The user submits a message through the chat input. The form handler calls sendMessage() from the useChat() hook.
2

Adapter Dispatch

useChat() calls adapter.runTurn() on the TauriGatewayAdapter, which calls invoke("run_turn", { sessionId, text }).
3

Tauri IPC

Tauri’s IPC bridge serializes the arguments as JSON and delivers them across the process boundary to the Rust backend.
4

Rust Command

The #[tauri::command] run_turn handler retrieves the DiminuendoClient from Tauri’s managed state and calls client.run_turn().
5

WebSocket

The Rust SDK serializes the message as JSON and sends it over the WebSocket to the gateway. The Rust SDK’s background tokio task handles all WebSocket I/O, decoupling the command handler from network scheduling.

Event Path (Gateway to Frontend)

1

Gateway Event

The gateway publishes an event to the session topic. The event traverses the WebSocket to the Rust backend.
2

Rust SDK Reception

The Rust SDK’s background tokio task receives the event from the WebSocket, deserializes it via serde into a ServerEvent enum variant, and sends it through the mpsc channel.
3

Event Emission

The event receiver task calls app_handle.emit("gateway-event", &event), which serializes the event and delivers it to the frontend webview via Tauri’s event system.
4

Adapter Stream

The TauriGatewayAdapter’s listen("gateway-event") callback receives the event and pushes it into the Effect Stream.Stream.
5

Store Update

The useGatewayConnection hook routes the event to the appropriate Zustand store based on its type field. React components subscribed to the affected store slice re-render with the updated state.

Advantages over the Web Client

Lower Latency

The Rust backend holds the WebSocket connection natively via tokio-tungstenite. There is no browser WebSocket implementation overhead, no JavaScript event loop contention for I/O processing, and no garbage collection pauses during high-throughput event streams. The tokio runtime schedules WebSocket reads on a dedicated async task, decoupled from the webview’s rendering cadence.

Native Window Management

Full control over window chrome, title bar, resizing behavior, and multi-window support via Tauri’s window management API. The application can implement custom title bars, frameless windows, and platform-specific window behaviors that the browser sandbox prohibits.

System Tray

Background presence via system tray icon with notification badges. The application can run minimized to the tray and surface notifications when agent turns complete or questions are requested — the connection remains alive because it lives in the Rust process, not in a tab that the user might close.

Local File Access

Direct filesystem access for Chronicle local-sync integration. The desktop client can synchronize agent workspace files to local directories, enabling users to open and edit files in their preferred IDE while the agent is actively working. This is architecturally impossible in a browser.

Chronicle Local-Sync Integration

The desktop client integrates with Chronicle’s local-sync mode, which replaces FUSE or NFS-based filesystem abstractions with real APFS files synchronized bidirectionally via platform-native FSEvents. Agent workspace files appear as ordinary files in Finder and in editors like VS Code — no kernel extension, no mount point, no virtual filesystem.

Tauri IPC Commands

Four IPC commands provide the local-sync interface from the React frontend:
CommandDescription
start_syncBegin synchronizing a session’s workspace to a local directory
stop_syncStop synchronization for a session
sync_statusQuery the current sync state (active, paused, error)
resolve_conflictResolve a file conflict when both local and remote changes exist
// Start syncing a session workspace to a local directory
await invoke("start_sync", {
  sessionId: "session-123",
  localDir: "/Users/dev/workspaces/session-123",
})

// Check sync status
const status = await invoke("sync_status", { sessionId: "session-123" })
// { state: "active", filesTracked: 42, lastSyncAt: 1709312400000 }

// Stop syncing
await invoke("stop_sync", { sessionId: "session-123" })

Sync Architecture

Local-sync consists of three cooperating components running in the Rust backend:

LocalMaterializer

Writes files from the upstream replication stream to the local filesystem. Implements echo suppression to prevent its own writes from triggering the watcher and creating infinite sync loops.

FsWatcher

Monitors the local directory for changes using notify::RecommendedWatcher (FSEvents on macOS). Changes are debounced via notify-debouncer-full and filtered to exclude editor temp files, .git directories, and other noise.

WriteJournal

Tracks all changes — both local and upstream — in a journal for replication. Provides the mechanism for conflict detection when both the user and the agent modify the same file within the same debounce window.
Echo suppression is the critical invariant: when the LocalMaterializer writes a file, the FsWatcher will detect that write as a local change. Without suppression, this would create an infinite sync loop. The materializer registers each write in a short-lived “echo set,” and the watcher ignores change events for paths currently in that set. Conflict resolution surfaces to the user when both the agent (upstream) and the user (local) modify the same file within the debounce window. The desktop client presents options to keep the local version, accept the upstream version, or merge manually.
Chronicle local-sync uses the notify crate (v7) with RecommendedWatcher for FSEvents on macOS. The local-sync Cargo feature flag must be enabled at build time. This is the default for desktop development builds.

Development

Start the desktop client in development mode:
cd clients
npm install
npm run dev:desktop
This starts the Tauri development window with hot module replacement for the frontend and automatic Rust recompilation for the backend. The Rust backend compiles with the local-sync feature enabled by default in development.
The desktop client’s Vite config is shared with the web client but excludes WebSocket-related code paths. Tauri’s __TAURI__ global is used for platform detection at runtime, selecting the TauriGatewayAdapter over the WebGatewayAdapter.

Build for Production

cd clients
npm run build:desktop
Produces platform-specific installers:
  • macOS: .dmg and .app bundle
  • Windows: .msi installer and .exe
  • Linux: .deb, .rpm, and .AppImage

Prerequisites

  • Rust toolchain (stable)
  • Tauri CLI: cargo install tauri-cli
  • Platform-specific dependencies (see Tauri prerequisites)
  • A running Diminuendo gateway instance for development