# 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 with two threads inside a single Tauri 2.0 process:

**Main Thread** — Runs the WebView window and system tray. This is the native UI layer that users interact with.

**Background Thread** (`"openfang-server"`) — Runs its own tokio runtime with the full OpenFang kernel: axum API server, channel bridges, and background agents.

The WebView communicates with the background server over `http://127.0.0.1:{port}`, where the port is dynamically assigned at startup.

### 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 via the `open` crate |
| **Status: Running** | Disabled (non-interactive) status indicator |
| **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

Three 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");
```

---

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

### CSP

The `tauri.conf.json` sets `"csp": null`, disabling Tauri's built-in Content Security Policy enforcement. This is necessary because the WebView loads content from the localhost server rather than from Tauri's custom protocol. The axum API server provides its own 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 |
| `tauri-plugin-shell` | 2 | Shell/process access from the WebView |
| `tauri-plugin-single-instance` | 2 | Prevents multiple instances (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"
  ]
}
```

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

| Path | Purpose |
| --- | --- |
| `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/` | App icons at multiple resolutions (png, ico, @2x) |
| `src/main.rs` | Binary entry point (calls `lib::run()`) |
| `src/lib.rs` | Tauri app builder, state types, event listener |
| `src/commands.rs` | IPC command handlers (`get_port`, `get_status`, `get_agent_count`) |
| `src/server.rs` | `ServerHandle`, kernel boot, embedded axum server |
| `src/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.
