lemonade

Lemonade Desktop App (Tauri)

A native desktop GUI for interacting with the Lemonade Server.

Overview

This app provides a native desktop experience for managing models and chatting with LLMs running on lemond. It connects to the server via HTTP API and offers a modern, resizable panel-based interface.

It is built with Tauri v2, which embeds the operating system’s native webview — WebView2 on Windows, WKWebView on macOS, and webkit2gtk on Linux — instead of bundling Chromium. The renderer is a standard React 19 + TypeScript application served by webpack and shared with the browser-only src/web-app/ build.

Key Features:

Deployment Topology

The Tauri desktop app is a thin client for a separately-running lemond server. A single lemond can be driven by multiple clients at once, including clients running on other machines.

Consequences that callers of this code need to know about:

Code Structure

src/app/
├── package.json                   # Webpack + Tauri CLI devDependencies
├── webpack.config.js              # Bundler config (target: web)
├── tsconfig.json                  # TypeScript config
├── assets/                        # Icons, logos
│
├── src/
│   ├── global.d.ts                # window.api type declaration
│   └── renderer/                  # React UI (TypeScript)
│       ├── index.tsx              # Renderer entry (imports tauriShim first)
│       ├── tauriShim.ts           # Installs window.api → Tauri invoke() bridge
│       ├── App.tsx                # Root component, layout orchestration
│       ├── TitleBar.tsx           # Custom window controls
│       ├── ModelManager.tsx       # Model list and actions
│       ├── ChatWindow.tsx         # LLM chat interface
│       ├── LogsWindow.tsx         # Server log viewer
│       ├── SettingsPanel.tsx      # Inference parameters
│       └── utils/                 # API helpers and config
│
└── src-tauri/                     # Rust host (Tauri backend)
    ├── Cargo.toml                 # Rust dependencies
    ├── tauri.conf.json            # Window config, bundle settings, plugins
    ├── build.rs                   # tauri_build::build()
    ├── capabilities/default.json  # Tauri permissions
    ├── icons/                     # Generated app icons (32x32/128x128/ico/icns)
    └── src/
        ├── main.rs                # Entry point (binary)
        ├── lib.rs                 # Tauri builder, plugin wiring, deep-link routing
        ├── commands.rs            # #[tauri::command] handlers (window, settings, port)
        ├── events.rs              # Tauri event channel name constants
        ├── settings.rs            # app_settings.json read/write + sanitize
        ├── beacon.rs              # UDP beacon listener (single bound socket)
        ├── tray_launcher.rs       # macOS-only tray auto-start helper
        └── webview_shim.rs        # Per-platform webview hooks (mic permission, link interception)

/health, /system-stats, and /system-info are NOT proxied through Rust. The renderer fetches them directly via serverConfig.fetch(...); see StatusBar.tsx and AboutModal.tsx.

Architecture

┌────────────────────────────────────────────────┐
│  Tauri Rust Host (src-tauri/)                  │
│  Window mgmt, IPC commands, background tasks   │
│  UDP beacon listener, settings file I/O        │
├────────────────────────────────────────────────┤
│  tauriShim.ts (installs window.api in webview) │
│  Maps window.api.* → invoke() / listen()       │
├────────────────────────────────────────────────┤
│  React Renderer (TypeScript)                   │
│  Source lives in src/app/src/; the web-app     │
│  build (src/web-app/) reuses it via webpack    │
│  relative entry/template paths — no symlinks.  │
├────────────────────────────────────────────────┤
│  HTTP API → lemond (C++ server)                │
└────────────────────────────────────────────────┘

Prerequisites

Building

cd src/app

# Install webpack + Tauri CLI dependencies
npm ci

# Run in dev mode (opens a window, hot-reloads webpack)
npm run dev

# Production build (single binary, no OS bundles)
npm run build -- --no-bundle

# Production build with platform bundles (macOS .app, Linux .deb/.rpm, Windows MSI/NSIS)
npm run build

The preferred path for shipping is through CMake, which stages the Tauri output alongside the rest of the server:

cmake --build --preset default --target tauri-app      # Linux / macOS
cmake --build --preset windows --target tauri-app      # Windows

Development Scripts

npm run dev                    # Tauri dev mode (window + hot-reload)
npm run build                  # Tauri production build
npm run tauri icon <path>      # Regenerate icons from a source image
npm run build:renderer         # Build just the renderer (webpack, dev mode)
npm run build:renderer:prod    # Build just the renderer (webpack, production)
npm run watch:renderer         # Webpack watch mode for the renderer only

Testing custom Omni Models

The custom Omni Model UI (see Register a custom Omni Model from the desktop app) has both an automated smoke test and a manual checklist.

Automated unit test

A focused Node-based smoke test exercises the custom Omni Model utility layer without starting Tauri or the Lemonade server:

cd src/app
npm run test:custom-collections

It uses the helpers in src/app/src/renderer/utils/customCollections.ts to verify that Omni Models can be saved, edited, imported, exported, and filtered by compatible component role.

Manual desktop smoke test

Use the desktop app to verify the user-facing flow end to end:

  1. Start the Lemonade desktop app.
  2. Download at least one chat-capable LLM in Model Manager.
  3. Optionally download one image model, one edit-capable image model, one vision model, one transcription model, and one speech model.
  4. From the menu bar, choose File > New Omni Model > Manually.
  5. Save an Omni Model with only an LLM and verify it appears as user.<name> in the chat model picker.
  6. Edit the Omni Model to add optional role models and save again.
  7. Select the Omni Model in chat and run prompts that trigger the configured tools, such as image generation, speech synthesis, audio transcription, or image analysis.
  8. Export the Omni Model JSON, delete the Omni Model, import the JSON, and verify it reappears.
  9. Delete one component model and verify the now-stale Omni Model is hidden from the picker until the component is registered again.

Testing the Rust host

Unit tests live alongside the Rust modules and cover settings sanitization, beacon parsing, and deep-link URL parsing:

cargo test --manifest-path src-tauri/Cargo.toml