# OpenFang Desktop App

The OpenFang Desktop App is a native desktop wrapper built with [Tauri 2.0](https://v2.tauri.app/) that packages the entire OpenFang Agent OS into a single, installable application. Instead of running a CLI daemon and opening a browser, users get a native window with system tray integration, OS notifications, and single-instance enforcement -- all powered by the same kernel and API server that the headless deployment uses.

**Crate:** `openfang-desktop`
**Identifier:** `ai.openfang.desktop`
**Product name:** OpenFang

---

## Architecture

The desktop app follows a straightforward embedded-server pattern:

```
+-------------------------------------------+
|  Tauri 2.0 Process                        |
|                                           |
|  +-----------+    +--------------------+  |
|  |  Main     |    | Background Thread  |  |
|  |  Thread   |    | ("openfang-server")|  |
|  |           |    |                    |  |
|  | WebView   |    | tokio runtime      |  |
|  | Window    |--->| axum API server    |  |
|  | (main)    |    | channel bridges    |  |
|  |           |    | background agents  |  |
|  | System    |    |                    |  |
|  | Tray      |    | OpenFang Kernel    |  |
|  +-----------+    +--------------------+  |
|       |                    |              |
|       |   http://127.0.0.1:{port}        |
|       +------------------------------------
+-------------------------------------------+
```

### Startup Sequence

1. **Tracing init** -- `tracing_subscriber` is configured with `RUST_LOG` env, defaulting to `openfang=info,tauri=info`.
2. **Kernel boot** -- `OpenFangKernel::boot(None)` loads the default configuration (from `config.toml` or defaults), wrapped in `Arc`. `set_self_handle()` is called to enable self-referencing kernel operations.
3. **Port binding** -- A `std::net::TcpListener` binds to `127.0.0.1:0` on the main thread, which lets the OS assign a random free port. This ensures the port number is known before any window is created.
4. **Server thread** -- A dedicated OS thread named `"openfang-server"` is spawned. It creates its own `tokio::runtime::Builder::new_multi_thread()` runtime and runs:
   - `kernel.start_background_agents()` -- heartbeat monitor, autonomous agents, etc.
   - `run_embedded_server()` -- builds the axum router via `openfang_api::server::build_router()`, converts the `std::net::TcpListener` to a `tokio::net::TcpListener`, and serves with graceful shutdown.
5. **Tauri app** -- The Tauri builder is assembled with plugins, managed state, IPC commands, system tray, and a WebView window pointing at `http://127.0.0.1:{port}`.
6. **Event loop** -- Tauri runs its native event loop. On exit, `server_handle.shutdown()` is called to stop the embedded server and kernel.

### ServerHandle

The `ServerHandle` struct (defined in `src/server.rs`) manages the embedded server lifecycle:

```rust
pub struct ServerHandle {
    pub port: u16,
    pub kernel: Arc<OpenFangKernel>,
    shutdown_tx: watch::Sender<bool>,
    server_thread: Option<std::thread::JoinHandle<()>>,
}
```

- **`port`** -- The port the embedded server is listening on.
- **`kernel`** -- Shared reference to the kernel, also used by the Tauri app for IPC commands and notifications.
- **`shutdown_tx`** -- A `tokio::sync::watch` channel. Sending `true` triggers graceful shutdown of the axum server.
- **`server_thread`** -- Join handle for the background thread. `shutdown()` joins it to ensure clean termination.

Calling `shutdown()` sends the shutdown signal, joins the background thread, and calls `kernel.shutdown()`. The `Drop` implementation sends the shutdown signal as a best-effort fallback but does not block on the thread join.

### Graceful Shutdown

The axum server uses `with_graceful_shutdown()` wired to the watch channel:

```rust
let server = axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>())
    .with_graceful_shutdown(async move {
        let _ = shutdown_rx.wait_for(|v| *v).await;
    });
```

After the server shuts down, channel bridges (Telegram, Slack, etc.) are stopped via `bridge.stop().await`.

---

## Features

### System Tray

The system tray (defined in `src/tray.rs`) provides quick access without bringing up the main window:

| Menu Item | Behavior |
|-----------|----------|
| **Show Window** | Calls `show()`, `unminimize()`, and `set_focus()` on the main WebView window |
| **Open in Browser** | Reads the port from managed `PortState` and opens `http://127.0.0.1:{port}` in the default browser |
| **Agents: N running** | Disabled (info only) — shows current agent count |
| **Status: Running (uptime)** | Disabled (info only) — shows uptime in human-readable format |
| **Launch at Login** | Checkbox — toggles OS-level auto-start via `tauri-plugin-autostart` |
| **Check for Updates...** | Checks for updates, downloads, installs, and restarts if available. Shows notifications for progress/success/failure |
| **Open Config Directory** | Opens `~/.openfang/` in the OS file manager |
| **Quit OpenFang** | Logs the quit event and calls `app.exit(0)` |

The tray tooltip reads **"OpenFang Agent OS"**.

**Left-click on tray icon** shows the main window (same as "Show Window" menu item). This is implemented via `on_tray_icon_event` listening for `MouseButton::Left` with `MouseButtonState::Up`.

### Single-Instance Enforcement

On desktop platforms, `tauri-plugin-single-instance` prevents multiple copies of OpenFang from running simultaneously. When a second instance attempts to launch, the existing instance's main window is shown, unminimized, and focused:

```rust
#[cfg(desktop)]
{
    builder = builder.plugin(tauri_plugin_single_instance::init(
        |app, _args, _cwd| {
            if let Some(w) = app.get_webview_window("main") {
                let _ = w.show();
                let _ = w.unminimize();
                let _ = w.set_focus();
            }
        },
    ));
}
```

### Hide-to-Tray on Close

Closing the window does not quit the application. Instead, the window is hidden and the close event is suppressed:

```rust
.on_window_event(|window, event| {
    #[cfg(desktop)]
    if let tauri::WindowEvent::CloseRequested { api, .. } = event {
        let _ = window.hide();
        api.prevent_close();
    }
})
```

To actually quit, use the **"Quit OpenFang"** option in the system tray menu.

### Native OS Notifications

The app subscribes to the kernel's event bus and forwards critical events as native desktop notifications using `tauri-plugin-notification`:

| Event | Notification Title | Body |
|-------|-------------------|------|
| `LifecycleEvent::Crashed` | "Agent Crashed" | `Agent {id} crashed: {error}` |
| `LifecycleEvent::Spawned` | "Agent Started" | `Agent "{name}" is now running` |
| `SystemEvent::HealthCheckFailed` | "Health Check Failed" | `Agent {id} unresponsive for {secs}s` |

All other events are silently skipped. The notification listener runs as an async task spawned via `tauri::async_runtime::spawn` and handles broadcast lag gracefully (logs a warning and continues).

---

## IPC Commands

Eleven Tauri IPC commands are registered, callable from the WebView frontend via `invoke()`:

### `get_port`

Returns the port number (`u16`) the embedded server is listening on.

```typescript
// Frontend usage
const port: number = await invoke("get_port");
```

### `get_status`

Returns a JSON object with runtime status:

```json
{
  "status": "running",
  "port": 8042,
  "agents": 5,
  "uptime_secs": 3600
}
```

- `agents` -- count of registered agents from `kernel.registry.list()`.
- `uptime_secs` -- seconds since the kernel state was initialized (via `Instant::now()` at startup).

### `get_agent_count`

Returns the number of registered agents (`usize`) as a simple integer.

```typescript
const count: number = await invoke("get_agent_count");
```

### `import_agent_toml`

Opens a native file picker for `.toml` files. Validates the selected file as an `AgentManifest`, copies it to `~/.openfang/agents/{name}/agent.toml`, and spawns the agent. Returns the agent name on success.

### `import_skill_file`

Opens a native file picker for skill files (`.md`, `.toml`, `.py`, `.js`, `.wasm`). Copies the file to `~/.openfang/skills/` and triggers a hot-reload of the skill registry.

### `get_autostart` / `set_autostart`

Check or toggle whether OpenFang launches at OS login. Uses `tauri-plugin-autostart` (launchd on macOS, registry on Windows, systemd on Linux).

### `check_for_updates`

Checks for available updates without installing. Returns an `UpdateInfo` object:

```json
{ "available": true, "version": "0.2.0", "body": "Release notes..." }
```

### `install_update`

Downloads and installs the latest update, then restarts the app. The command does not return on success (the app restarts). Returns an error string on failure.

```typescript
await invoke("install_update"); // App restarts if update succeeds
```

### `open_config_dir` / `open_logs_dir`

Opens `~/.openfang/` or `~/.openfang/logs/` in the OS file manager.

---

## Window Configuration

The main window is created programmatically in the `setup` closure (not via `tauri.conf.json`, which declares an empty `windows: []` array):

| Property | Value |
|----------|-------|
| Window label | `"main"` |
| Title | `"OpenFang"` |
| URL | `http://127.0.0.1:{port}` (external) |
| Inner size | 1280 x 800 |
| Minimum inner size | 800 x 600 |
| Position | Centered |

The window uses `WebviewUrl::External(...)` rather than a bundled frontend, because the WebView renders the axum-served UI.

### Auto-Updater

The app checks for updates 10 seconds after startup. If an update is available, it is downloaded, installed, and the app restarts automatically. Users can also trigger a manual check via the system tray.

**Flow:**
1. Startup check (10s delay) → `check_for_update()` → if available → notify user → `download_and_install_update()` → app restarts
2. Tray "Check for Updates" → same flow, with failure notification if install fails

**Configuration** (in `tauri.conf.json`):
- `plugins.updater.pubkey` — Ed25519 public key (must match the signing private key)
- `plugins.updater.endpoints` — URL to `latest.json` (hosted on GitHub Releases)
- `plugins.updater.windows.installMode` — `"passive"` (install without full UI)

**Signing:** Every release bundle is signed with `TAURI_SIGNING_PRIVATE_KEY` (GitHub Secret). The `tauri-action` generates `latest.json` containing download URLs and signatures for each platform.

See [Production Checklist](production-checklist.md) for key generation and setup instructions.

### CSP

The `tauri.conf.json` configures a Content Security Policy that allows connections to the local embedded server:

```
default-src 'self' http://127.0.0.1:* ws://127.0.0.1:*;
img-src 'self' data: http://127.0.0.1:*;
style-src 'self' 'unsafe-inline';
script-src 'self' 'unsafe-inline'
```

This permits the WebView to load content from the localhost API server while blocking external resource loading. The axum API server provides additional security headers middleware.

---

## Building

### Prerequisites

- **Rust** (stable toolchain)
- **Tauri CLI v2**: `cargo install tauri-cli --version "^2"`
- **Platform-specific dependencies**:
  - **Windows**: WebView2 (included in Windows 10/11), Visual Studio Build Tools
  - **macOS**: Xcode Command Line Tools
  - **Linux**: `libwebkit2gtk-4.1-dev`, `libappindicator3-dev`, `librsvg2-dev`, `libssl-dev`, `build-essential`

### Development

```bash
cd crates/openfang-desktop
cargo tauri dev
```

This launches the app with hot-reload support. The console window is visible in debug builds for tracing output.

### Production Build

```bash
cd crates/openfang-desktop
cargo tauri build
```

This produces platform-specific installers:
- **Windows**: `.msi` and `.exe` (NSIS) installers
- **macOS**: `.dmg` and `.app` bundle
- **Linux**: `.deb`, `.rpm`, and `.AppImage`

The release binary suppresses the console window on Windows via:

```rust
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
```

### Bundle Configuration

From `tauri.conf.json`:

```json
{
  "bundle": {
    "active": true,
    "targets": "all",
    "icon": [
      "icons/icon.png",
      "icons/32x32.png",
      "icons/128x128.png",
      "icons/128x128@2x.png"
    ]
  }
}
```

The `"targets": "all"` setting generates every available package format for the current platform. Icons are provided at multiple resolutions, plus an `icon.ico` for Windows.

---

## Plugins

| Plugin | Version | Purpose |
|--------|---------|---------|
| `tauri-plugin-notification` | 2 | Native OS notifications for kernel events and update progress |
| `tauri-plugin-shell` | 2 | Shell/process access from the WebView |
| `tauri-plugin-dialog` | 2 | Native file picker for agent/skill import |
| `tauri-plugin-single-instance` | 2 | Prevents multiple instances (desktop only) |
| `tauri-plugin-autostart` | 2 | Launch at OS login (desktop only) |
| `tauri-plugin-updater` | 2 | Signed auto-updates from GitHub Releases (desktop only) |
| `tauri-plugin-global-shortcut` | 2 | Ctrl+Shift+O/N/C shortcuts (desktop only) |

### Capabilities

The default capability set (defined in `capabilities/default.json`) grants:

```json
{
  "identifier": "default",
  "windows": ["main"],
  "permissions": [
    "core:default",
    "notification:default",
    "shell:default",
    "dialog:default",
    "global-shortcut:allow-register",
    "global-shortcut:allow-unregister",
    "global-shortcut:allow-is-registered",
    "autostart:default",
    "updater:default"
  ]
}
```

Only the `"main"` window receives these permissions.

---

## Mobile Ready

The codebase includes conditional compilation guards for mobile platform support:

- **Entry point**: The `run()` function is annotated with `#[cfg_attr(mobile, tauri::mobile_entry_point)]`, allowing Tauri to use it as the mobile entry point.
- **Desktop-only features**: System tray setup, single-instance enforcement, and hide-to-tray on close are all gated behind `#[cfg(desktop)]` so they compile out on mobile targets.
- **Mobile targets**: iOS and Android builds are structurally supported by the Tauri 2.0 framework, though the kernel and API server would still boot in-process on the device.

---

## File Structure

```
crates/openfang-desktop/
  build.rs                 # tauri_build::build()
  Cargo.toml               # Crate dependencies and metadata
  tauri.conf.json           # Tauri app configuration
  capabilities/
    default.json            # Permission grants for the main window
  gen/
    schemas/                # Auto-generated Tauri schemas
  icons/
    icon.png                # Source icon (327 KB)
    icon.ico                # Windows icon
    32x32.png               # Small icon
    128x128.png             # Standard icon
    128x128@2x.png          # HiDPI icon
  src/
    main.rs                 # Binary entry point (calls lib::run())
    lib.rs                  # Tauri app builder, state types, event listener
    commands.rs             # IPC command handlers (get_port, get_status, get_agent_count)
    server.rs               # ServerHandle, kernel boot, embedded axum server
    tray.rs                 # System tray menu and event handlers
```

---

## Environment Variables

| Variable | Effect |
|----------|--------|
| `RUST_LOG` | Controls tracing verbosity. Defaults to `openfang=info,tauri=info` if unset. |

All other OpenFang environment variables (API keys, configuration) apply as normal since the desktop app boots the same kernel as the headless daemon.
