OpenFang Desktop App
The OpenFang Desktop App is a native desktop wrapper built with Tauri 2.0 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
- Tracing init --
tracing_subscriberis configured withRUST_LOGenv, defaulting toopenfang=info,tauri=info. - Kernel boot --
OpenFangKernel::boot(None)loads the default configuration (fromconfig.tomlor defaults), wrapped inArc.set_self_handle()is called to enable self-referencing kernel operations. - Port binding -- A
std::net::TcpListenerbinds to127.0.0.1:0on the main thread, which lets the OS assign a random free port. This ensures the port number is known before any window is created. - Server thread -- A dedicated OS thread named
"openfang-server"is spawned. It creates its owntokio::runtime::Builder::new_multi_thread()runtime and runs:kernel.start_background_agents()-- heartbeat monitor, autonomous agents, etc.run_embedded_server()-- builds the axum router viaopenfang_api::server::build_router(), converts thestd::net::TcpListenerto atokio::net::TcpListener, and serves with graceful shutdown.
- 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}. - 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:
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-- Atokio::sync::watchchannel. Sendingtruetriggers 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:
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:
#[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:
.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.
// Frontend usage
const port: number = await invoke("get_port");
get_status
Returns a JSON object with runtime status:
{
"status": "running",
"port": 8042,
"agents": 5,
"uptime_secs": 3600
}
agents-- count of registered agents fromkernel.registry.list().uptime_secs-- seconds since the kernel state was initialized (viaInstant::now()at startup).
get_agent_count
Returns the number of registered agents (usize) as a simple integer.
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
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
cd crates/openfang-desktop
cargo tauri build
This produces platform-specific installers:
- Windows:
.msiand.exe(NSIS) installers - macOS:
.dmgand.appbundle - Linux:
.deb,.rpm, and.AppImage
The release binary suppresses the console window on Windows via:
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
Bundle Configuration
From tauri.conf.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:
{
"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.