Files
openhuman/src-tauri/src/lib.rs
T
b037627d84 Feat/daemon setup (#148)
* feat: add .mcp.json for MCP server configuration

- Introduced `.mcp.json` with server details for managing MCP integrations
- Defines `readme` server with HTTP type and URL endpoint configuration

* fix: Update skills submodule with Telegram error handling improvements

- Fixed setup flow showing false success when TDLib errors occurred
- Improved async error handling in setup steps
- Added client reset mechanisms for error recovery
- Enhanced error messaging for better user experience
- Cleaned up excessive debug logging while maintaining error logs

Resolves issue where Telegram setup showed success modal despite underlying TDLib errors.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Add daemon lifecycle management system to frontend UI

This implementation integrates the Rust daemon's health monitoring and lifecycle management into the React frontend. Key features include real-time health indicators, automatic start/error recovery, and detailed control panels, enhancing both user experience and operational reliability.

* Enhance daemon health monitoring with detailed logging, event tracking, and state management improvements.

* Add extensive logging for Tauri socket lifecycle and event listeners

This update improves troubleshooting and debugging by adding detailed logs throughout Tauri socket initialization, event listener setup, and error handling processes.

* Define global Tauri interface types in TypeScript

* Fix daemon service management and health communication issues

This comprehensive fix resolves two critical daemon-related issues:

1. **Launchctl "Input/output error" fix**:
   - Add intelligent state checking before service operations
   - Only load LaunchAgent if not already loaded
   - Treat "already running" as success, not failure
   - Add helper functions for cross-platform service state detection
   - Implement idempotent service start operations

2. **Daemon health communication fix**:
   - Add ALPHAHUMAN_DAEMON_INTERNAL environment variable to external service plist
   - Force external daemon to use file-based communication (daemon_state.json)
   - Implement file watching bridge in main app to emit Tauri events
   - Ensure frontend receives proper health updates from external daemon

**Key Changes**:
- Enhanced `start()` function with state checking across all platforms
- Added `is_service_loaded_macos()`, `is_service_enabled_linux()`, `is_task_exists_windows()`
- Modified macOS plist to include `ALPHAHUMAN_DAEMON_INTERNAL=false`
- Added `watch_daemon_health_file()` function for external daemon communication
- Updated daemon mode detection logic for cross-platform consistency
- Added comprehensive logging for service management operations

**Expected Results**:
- No more "Input/output error" when clicking daemon start button
- External daemon status shows "connected" instead of "disconnected"
- Idempotent service operations (safe to run multiple times)
- Single daemon process prevents resource conflicts
- Cross-platform daemon service reliability

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* Add `ALPHAHUMAN_DAEMON_INTERNAL` env variable to configuration documentation

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-02-26 19:55:53 +04:00

933 lines
38 KiB
Rust

//! AlphaHuman Desktop Application
//!
//! This is the Rust backend for the cross-platform crypto community platform.
//! It provides:
//! - System tray with background execution
//! - Deep link authentication
//! - Persistent Socket.io connection
//! - Secure session storage
//! - Native notifications
mod ai;
mod auth;
mod commands;
mod models;
mod runtime;
mod services;
pub mod alphahuman;
mod unified_skills;
mod utils;
use ai::*;
use commands::unified_skills::{unified_execute_skill, unified_generate_skill, unified_list_skills};
use commands::*;
use services::socket_service::SOCKET_SERVICE;
use std::path::PathBuf;
use tauri::{AppHandle, Manager, RunEvent, Emitter};
use tokio::{fs, time::{interval, Duration}};
#[cfg(desktop)]
use tauri::{
menu::{Menu, MenuItem},
tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
};
#[cfg(any(windows, target_os = "linux"))]
use tauri_plugin_deep_link::DeepLinkExt;
/// Demo command - can be removed in production
#[tauri::command]
fn greet(name: &str) -> String {
format!("Hello, {}! You've been greeted from Rust!", name)
}
// Macro to define common handlers shared across all platforms
macro_rules! common_handlers {
() => {
// Demo
greet,
// Auth commands
get_auth_state,
get_session_token,
get_current_user,
is_authenticated,
logout,
store_session,
// Socket commands
socket_connect,
socket_disconnect,
get_socket_state,
is_socket_connected,
report_socket_connected,
report_socket_disconnected,
report_socket_error,
update_socket_status,
// AI encryption commands
ai_init_encryption,
ai_encrypt,
ai_decrypt,
// AI memory filesystem commands
ai_memory_init,
ai_memory_upsert_file,
ai_memory_get_file,
ai_memory_upsert_chunk,
ai_memory_delete_chunks_by_path,
ai_memory_fts_search,
ai_memory_get_chunks,
ai_memory_get_all_embeddings,
ai_memory_cache_embedding,
ai_memory_get_cached_embedding,
ai_memory_set_meta,
ai_memory_get_meta,
// AI session commands
ai_sessions_init,
ai_sessions_load_index,
ai_sessions_update_index,
ai_sessions_append_transcript,
ai_sessions_read_transcript,
ai_sessions_delete,
ai_sessions_list,
ai_read_memory_file,
ai_write_memory_file,
ai_list_memory_files,
// Runtime commands
runtime_discover_skills,
runtime_list_skills,
runtime_start_skill,
runtime_stop_skill,
runtime_get_skill_state,
runtime_call_tool,
runtime_all_tools,
runtime_broadcast_event,
// Runtime enable/disable + KV commands
runtime_enable_skill,
runtime_disable_skill,
runtime_is_skill_enabled,
runtime_get_skill_preferences,
runtime_skill_kv_get,
runtime_skill_kv_set,
// Runtime JSON-RPC + data commands
runtime_rpc,
runtime_skill_data_read,
runtime_skill_data_write,
runtime_skill_data_dir,
// Socket.io commands (Rust-native persistent connection)
runtime_socket_connect,
runtime_socket_disconnect,
runtime_socket_state,
runtime_socket_emit,
};
}
// Macro to define desktop-only window handlers
macro_rules! desktop_window_handlers {
() => {
show_window,
hide_window,
toggle_window,
is_window_visible,
minimize_window,
maximize_window,
close_window,
set_window_title,
};
}
// Helper function to show the window (used by tray and macOS reopen)
#[cfg(desktop)]
fn show_main_window(app: &AppHandle) {
if let Some(window) = app.get_webview_window("main") {
let _ = window.show();
let _ = window.unminimize();
let _ = window.set_focus();
}
}
// Helper function to toggle window visibility
#[cfg(desktop)]
fn toggle_main_window_visibility(app: &AppHandle) {
if let Some(window) = app.get_webview_window("main") {
match window.is_visible() {
Ok(true) => {
let _ = window.hide();
}
Ok(false) => {
show_main_window(app);
}
Err(_) => {
// If we can't determine visibility, try to show it
show_main_window(app);
}
}
} else {
eprintln!("Could not find window 'main'");
}
}
fn is_daemon_mode() -> bool {
std::env::args().any(|arg| arg == "daemon" || arg == "--daemon")
}
fn daemon_foreground_requested() -> bool {
matches!(
std::env::var("ALPHAHUMAN_DAEMON_FOREGROUND")
.ok()
.as_deref(),
Some("1") | Some("true") | Some("TRUE") | Some("yes") | Some("YES")
)
}
// Setup system tray with menu
#[cfg(desktop)]
fn setup_tray(app: &AppHandle) -> Result<(), Box<dyn std::error::Error>> {
let show_hide_item =
MenuItem::with_id(app, "show_hide", "Show/Hide Window", true, None::<&str>)?;
let quit_item = MenuItem::with_id(app, "quit", "Quit", true, None::<&str>)?;
let menu = Menu::with_items(app, &[&show_hide_item, &quit_item])?;
let _tray = TrayIconBuilder::with_id("main-tray")
.icon(app.default_window_icon().unwrap().clone())
.menu(&menu)
.tooltip("AlphaHuman")
.on_menu_event(move |app, event| match event.id().as_ref() {
"show_hide" => {
toggle_main_window_visibility(app);
}
"quit" => {
// Cleanup before exit - request frontend to disconnect
let _ = SOCKET_SERVICE.request_disconnect();
app.exit(0);
}
_ => {}
})
.on_tray_icon_event(|tray, event| match event {
TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Up,
..
} => {
let app = tray.app_handle();
toggle_main_window_visibility(app);
}
TrayIconEvent::DoubleClick {
button: MouseButton::Left,
..
} => {
let app = tray.app_handle();
show_main_window(app);
}
_ => {}
})
.build(app)?;
Ok(())
}
/// Watch daemon health file and bridge changes to frontend Tauri events
async fn watch_daemon_health_file(app_handle: AppHandle, data_dir: PathBuf) {
let state_file = data_dir.join("daemon_state.json");
let mut interval = interval(Duration::from_secs(2));
let mut last_modified: Option<std::time::SystemTime> = None;
log::info!("[alphahuman] Watching daemon health file: {}", state_file.display());
loop {
interval.tick().await;
// Check if file exists and was modified
if let Ok(metadata) = fs::metadata(&state_file).await {
if let Ok(modified) = metadata.modified() {
if last_modified.map_or(true, |last| modified > last) {
last_modified = Some(modified);
// Read and parse health data
if let Ok(content) = fs::read_to_string(&state_file).await {
if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(&content) {
log::debug!("[alphahuman] Broadcasting health event from file: {:?}", json_value);
// Emit Tauri event to frontend (same as internal daemon)
if let Err(e) = app_handle.emit("alphahuman:health", &json_value) {
log::error!("[alphahuman] Failed to emit health event from file: {}", e);
} else {
log::debug!("[alphahuman] Health event emitted successfully from file");
}
} else {
log::debug!("[alphahuman] Failed to parse health file as JSON: {}", state_file.display());
}
} else {
log::debug!("[alphahuman] Failed to read health file: {}", state_file.display());
}
}
}
} else {
// File doesn't exist yet - external daemon may not be writing yet
log::debug!("[alphahuman] Health file not found yet: {}", state_file.display());
}
}
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
if let Err(err) = rustls::crypto::ring::default_provider().install_default() {
log::warn!(
"[app] rustls crypto provider not installed (already set?): {:?}",
err
);
} else {
log::info!("[app] rustls crypto provider installed (ring)");
}
let daemon_mode = is_daemon_mode();
// Initialize platform-appropriate logger
#[cfg(target_os = "android")]
{
android_logger::init_once(
android_logger::Config::default()
.with_max_level(log::LevelFilter::Debug)
.with_tag("AlphaHuman"),
);
}
#[cfg(not(target_os = "android"))]
{
use env_logger::fmt::style::{AnsiColor, Style};
use std::io::Write;
let default_filter = std::env::var("RUST_LOG")
.unwrap_or_else(|_| "info,tungstenite=warn,tokio_tungstenite=warn,reqwest=warn,rusqlite=warn,hyper=warn,h2=warn".to_string());
let write_style = std::env::var("RUST_LOG_STYLE")
.map(|v| match v.as_str() {
"never" => env_logger::fmt::WriteStyle::Never,
_ => env_logger::fmt::WriteStyle::Always,
})
.unwrap_or(env_logger::fmt::WriteStyle::Always);
let _ = env_logger::Builder::new()
.parse_filters(&default_filter)
.write_style(write_style)
.format(|buf, record| {
let timestamp = buf.timestamp_millis()
.to_string();
// Strip the date prefix, keep only HH:MM:SS.mmm
let time_only = timestamp.split('T')
.nth(1)
.and_then(|t| t.strip_suffix('Z'))
.unwrap_or(&timestamp);
let level = record.level();
// Level colors
let level_style = match level {
log::Level::Error => Style::new().fg_color(Some(AnsiColor::Red.into())).bold(),
log::Level::Warn => Style::new().fg_color(Some(AnsiColor::Yellow.into())).bold(),
log::Level::Info => Style::new().fg_color(Some(AnsiColor::Green.into())),
log::Level::Debug => Style::new().fg_color(Some(AnsiColor::BrightBlack.into())),
log::Level::Trace => Style::new().fg_color(Some(AnsiColor::BrightBlack.into())),
};
let msg = format!("{}", record.args());
// Extract tag from message (e.g. "[tdlib]", "[socket-mgr]", "[skill:x]")
let (tag, rest) = if msg.starts_with('[') {
if let Some(end) = msg.find(']') {
let tag = &msg[..=end];
let rest = msg[end + 1..].trim_start();
(Some(tag.to_string()), rest.to_string())
} else {
(None, msg)
}
} else {
(None, msg)
};
// Tag-based colors
let tag_style = if let Some(ref t) = tag {
let t_lower = t.to_lowercase();
if t_lower.contains("tdlib") {
Style::new().fg_color(Some(AnsiColor::Magenta.into())).bold()
} else if t_lower.contains("socket") {
Style::new().fg_color(Some(AnsiColor::Blue.into())).bold()
} else if t_lower.contains("runtime") {
Style::new().fg_color(Some(AnsiColor::Cyan.into())).bold()
} else if t_lower.contains("skill") {
Style::new().fg_color(Some(AnsiColor::Green.into())).bold()
} else if t_lower.contains("ping") || t_lower.contains("cron") {
Style::new().fg_color(Some(AnsiColor::Yellow.into())).bold()
} else if t_lower.contains("app") {
Style::new().fg_color(Some(AnsiColor::White.into())).bold()
} else if t_lower.contains("ai") {
Style::new().fg_color(Some(AnsiColor::BrightMagenta.into())).bold()
} else {
Style::new().fg_color(Some(AnsiColor::BrightBlack.into()))
}
} else {
Style::new()
};
let dim = Style::new().fg_color(Some(AnsiColor::BrightBlack.into()));
if let Some(ref t) = tag {
writeln!(
buf,
"{dim}{time_only}{dim:#} {level_style}{level:<5}{level_style:#} {tag_style}{t}{tag_style:#} {rest}"
)
} else {
writeln!(
buf,
"{dim}{time_only}{dim:#} {level_style}{level:<5}{level_style:#} {rest}"
)
}
})
.try_init();
}
let mut builder = tauri::Builder::default()
// Plugins
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_deep_link::init())
.plugin(tauri_plugin_os::init());
// Add desktop-only plugins (autostart, notification)
#[cfg(desktop)]
{
builder = builder
.plugin(tauri_plugin_autostart::init(
tauri_plugin_autostart::MacosLauncher::LaunchAgent,
Some(vec!["--daemon"]),
))
.plugin(tauri_plugin_notification::init());
}
builder
// Setup
.setup(move |app| {
// Initialize socket service with app handle
SOCKET_SERVICE.set_app_handle(app.handle().clone());
// Register deep link handlers (Windows/Linux)
#[cfg(any(windows, target_os = "linux"))]
{
app.deep_link().register_all()?;
}
// Setup system tray (desktop only)
#[cfg(desktop)]
{
if daemon_mode {
setup_tray(app.handle())?;
}
}
// macOS-specific: Handle window close event to minimize to tray
#[cfg(target_os = "macos")]
{
if let Some(window) = app.get_webview_window("main") {
let app_handle = app.handle().clone();
window.on_window_event(move |event| {
if let tauri::WindowEvent::CloseRequested { api, .. } = event {
// Prevent the window from closing, hide it instead
api.prevent_close();
if let Some(win) = app_handle.get_webview_window("main") {
let _ = win.hide();
}
}
});
}
}
// Start the TDLib client early so it's ready before any skill
// starts. The worker loop auto-sends setTdlibParameters when
// TDLib requests them — no JS involvement needed.
#[cfg(not(any(target_os = "android", target_os = "ios")))]
{
let data_dir = app
.path()
.app_data_dir()
.unwrap_or_else(|_| {
dirs::home_dir()
.unwrap_or_else(|| std::path::PathBuf::from("."))
.join(".alphahuman")
});
let tdlib_data_dir = data_dir.join("telegram");
match crate::services::tdlib::TDLIB_MANAGER.create_client(tdlib_data_dir) {
Ok(id) => log::info!("[tdlib] Client created at app startup (ID {})", id),
Err(e) => log::error!("[tdlib] Failed to create client at startup: {}", e),
}
}
// Create the SocketManager (persistent Rust-native Socket.io)
let socket_mgr = std::sync::Arc::new(
runtime::socket_manager::SocketManager::new(),
);
socket_mgr.set_app_handle(app.handle().clone());
// Initialize QuickJS Runtime Engine (desktop only - not available on Android/iOS)
#[cfg(not(any(target_os = "android", target_os = "ios")))]
{
let data_dir = app
.path()
.app_data_dir()
.unwrap_or_else(|_| {
// Fallback for platforms where app_data_dir isn't available
dirs::home_dir()
.unwrap_or_else(|| std::path::PathBuf::from("."))
.join(".alphahuman")
});
let skills_data_dir = data_dir.join("skills");
match runtime::qjs_engine::RuntimeEngine::new(skills_data_dir) {
Ok(engine) => {
engine.set_app_handle(app.handle().clone());
// Set resource directory for bundled skills (production builds)
if let Ok(resource_dir) = app.path().resource_dir() {
engine.set_resource_dir(resource_dir);
}
let engine = std::sync::Arc::new(engine);
// Wire the SkillRegistry into the SocketManager for MCP
socket_mgr.set_registry(engine.registry());
// Wire the SocketManager into the engine for tool:sync
engine.set_socket_manager(socket_mgr.clone());
app.manage(engine.clone());
// Start the cron scheduler
let cron = engine.cron_scheduler();
tauri::async_runtime::spawn(async move {
cron.start();
});
// Start the ping scheduler (health-checks running skills)
let ping = engine.ping_scheduler();
tauri::async_runtime::spawn(async move {
ping.start();
});
// Auto-start skills in background (no delay needed for QuickJS -
// lightweight contexts don't have V8's memory reservation issue)
let engine_clone = engine.clone();
tauri::async_runtime::spawn(async move {
engine_clone.auto_start_skills().await;
});
log::info!("[runtime] QuickJS runtime engine initialized");
}
Err(e) => {
log::error!("[runtime] Failed to initialize QuickJS runtime: {e}");
}
}
}
#[cfg(target_os = "android")]
{
log::info!("[runtime] QuickJS runtime disabled on Android");
}
#[cfg(target_os = "ios")]
{
log::info!("[runtime] QuickJS runtime disabled on iOS");
}
// Start the alphahuman daemon supervisor (desktop only)
#[cfg(not(any(target_os = "android", target_os = "ios")))]
{
let data_dir = app
.path()
.app_data_dir()
.unwrap_or_else(|_| {
dirs::home_dir()
.unwrap_or_else(|| std::path::PathBuf::from("."))
.join(".alphahuman")
});
let daemon_config = alphahuman::config::DaemonConfig::from_app_data_dir(&data_dir);
let cancel = tokio_util::sync::CancellationToken::new();
let daemon_handle = alphahuman::daemon::DaemonHandle {
cancel: cancel.clone(),
};
app.manage(daemon_handle);
// Determine daemon mode: internal supervisor vs external platform service
let use_internal_daemon = daemon_mode
|| daemon_foreground_requested()
|| cfg!(debug_assertions) // Always use internal supervisor in debug builds
|| std::env::var("ALPHAHUMAN_DAEMON_INTERNAL").unwrap_or("false".to_string()) == "true"; // Cross-platform override via env var
if use_internal_daemon {
// Run internal daemon supervisor with health event emission
// This path is taken when:
// - Daemon mode enabled, OR
// - Foreground daemon requested, OR
// - Debug build (for easier development), OR
// - ALPHAHUMAN_DAEMON_INTERNAL=true env var (any platform)
log::info!("[alphahuman] Using internal daemon supervisor (ALPHAHUMAN_DAEMON_INTERNAL=true or debug build)");
let app_handle_for_daemon = app.handle().clone();
tauri::async_runtime::spawn(async move {
log::info!("[alphahuman] Starting daemon supervisor with health monitoring");
if let Err(e) = alphahuman::daemon::run(
daemon_config,
app_handle_for_daemon,
cancel,
)
.await
{
log::error!("[alphahuman] Daemon supervisor error: {e}");
}
});
} else {
// Start external platform-specific service for background daemon
// This path is taken on all platforms when ALPHAHUMAN_DAEMON_INTERNAL=false/unset
// and not in daemon mode, foreground mode, or debug build
log::info!("[alphahuman] Using external daemon service (ALPHAHUMAN_DAEMON_INTERNAL=false/unset)");
// Setup file watching to bridge external daemon health events to frontend
let app_handle_for_watcher = app.handle().clone();
let data_dir_clone = data_dir.clone();
tauri::async_runtime::spawn(async move {
watch_daemon_health_file(app_handle_for_watcher, data_dir_clone).await;
});
// Start the external platform service
tauri::async_runtime::spawn(async move {
match alphahuman::config::Config::load_or_init().await {
Ok(config) => {
match alphahuman::service::install(&config) {
Ok(status) => log::info!("[alphahuman] External daemon service installed: {:?}", status),
Err(e) => log::error!("[alphahuman] Failed to install external daemon service: {e}"),
}
match alphahuman::service::start(&config) {
Ok(status) => log::info!("[alphahuman] External daemon service started: {:?}", status),
Err(e) => log::error!("[alphahuman] Failed to start external daemon service: {e}"),
}
}
Err(e) => {
log::error!(
"[alphahuman] Failed to load config for external service: {e}"
);
}
}
});
}
}
if daemon_mode {
if let Some(window) = app.get_webview_window("main") {
let _ = window.hide();
}
}
// Store SocketManager as Tauri state
app.manage(socket_mgr.clone());
// Auto-connect socket if there's an existing session
let socket_mgr_clone = socket_mgr.clone();
tauri::async_runtime::spawn(async move {
use commands::auth::SESSION_SERVICE;
if let Some(token) = SESSION_SERVICE.get_token() {
let url = utils::config::get_backend_url();
log::info!("[socket-mgr] Auto-connecting with stored session");
if let Err(e) = socket_mgr_clone.connect(&url, &token).await {
log::error!("[socket-mgr] Auto-connect failed: {e}");
}
} else {
log::info!("[socket-mgr] No stored session — waiting for login");
}
});
Ok(())
})
// Register all commands
// Common handlers are defined via macros above, conditionally include desktop window handlers
.invoke_handler({
#[cfg(desktop)]
{
tauri::generate_handler![
// Common handlers (expanded from common_handlers! macro)
greet,
get_auth_state,
get_session_token,
get_current_user,
is_authenticated,
logout,
store_session,
socket_connect,
socket_disconnect,
get_socket_state,
is_socket_connected,
report_socket_connected,
report_socket_disconnected,
report_socket_error,
update_socket_status,
// Desktop-only window handlers (expanded from desktop_window_handlers! macro)
show_window,
hide_window,
toggle_window,
is_window_visible,
minimize_window,
maximize_window,
close_window,
set_window_title,
// AI encryption commands
ai_init_encryption,
ai_encrypt,
ai_decrypt,
// AI memory filesystem commands
ai_memory_init,
ai_memory_upsert_file,
ai_memory_get_file,
ai_memory_upsert_chunk,
ai_memory_delete_chunks_by_path,
ai_memory_fts_search,
ai_memory_get_chunks,
ai_memory_get_all_embeddings,
ai_memory_cache_embedding,
ai_memory_get_cached_embedding,
ai_memory_set_meta,
ai_memory_get_meta,
// AI session commands
ai_sessions_init,
ai_sessions_load_index,
ai_sessions_update_index,
ai_sessions_append_transcript,
ai_sessions_read_transcript,
ai_sessions_delete,
ai_sessions_list,
ai_read_memory_file,
ai_write_memory_file,
ai_list_memory_files,
// Runtime commands
runtime_discover_skills,
runtime_list_skills,
runtime_start_skill,
runtime_stop_skill,
runtime_get_skill_state,
runtime_call_tool,
runtime_all_tools,
runtime_broadcast_event,
// Runtime enable/disable + KV commands
runtime_enable_skill,
runtime_disable_skill,
runtime_is_skill_enabled,
runtime_get_skill_preferences,
runtime_skill_kv_get,
runtime_skill_kv_set,
// Runtime JSON-RPC + data commands
runtime_rpc,
runtime_skill_data_read,
runtime_skill_data_write,
runtime_skill_data_dir,
// Socket.io commands (Rust-native persistent connection)
runtime_socket_connect,
runtime_socket_disconnect,
runtime_socket_state,
runtime_socket_emit,
// TDLib commands (native Telegram library)
tdlib_create_client,
tdlib_send,
tdlib_receive,
tdlib_destroy,
tdlib_is_available,
// Model commands (backend API proxy)
model_summarize,
model_generate,
// Alphahuman commands
alphahuman_health_snapshot,
alphahuman_security_policy_info,
alphahuman_encrypt_secret,
alphahuman_decrypt_secret,
alphahuman_get_config,
alphahuman_update_model_settings,
alphahuman_update_memory_settings,
alphahuman_update_gateway_settings,
alphahuman_update_tunnel_settings,
alphahuman_update_runtime_settings,
alphahuman_update_browser_settings,
alphahuman_get_runtime_flags,
alphahuman_set_browser_allow_all,
alphahuman_agent_chat,
alphahuman_doctor_report,
alphahuman_doctor_models,
alphahuman_list_integrations,
alphahuman_get_integration_info,
alphahuman_models_refresh,
alphahuman_migrate_openclaw,
alphahuman_hardware_discover,
alphahuman_hardware_introspect,
alphahuman_service_install,
alphahuman_service_start,
alphahuman_service_stop,
alphahuman_service_status,
alphahuman_service_uninstall,
// Unified skill registry commands
unified_list_skills,
unified_execute_skill,
unified_generate_skill,
]
}
#[cfg(not(desktop))]
{
tauri::generate_handler![
// Common handlers (expanded from common_handlers! macro)
greet,
get_auth_state,
get_session_token,
get_current_user,
is_authenticated,
logout,
store_session,
socket_connect,
socket_disconnect,
get_socket_state,
is_socket_connected,
report_socket_connected,
report_socket_disconnected,
report_socket_error,
update_socket_status,
// AI encryption commands
ai_init_encryption,
ai_encrypt,
ai_decrypt,
// AI memory filesystem commands
ai_memory_init,
ai_memory_upsert_file,
ai_memory_get_file,
ai_memory_upsert_chunk,
ai_memory_delete_chunks_by_path,
ai_memory_fts_search,
ai_memory_get_chunks,
ai_memory_get_all_embeddings,
ai_memory_cache_embedding,
ai_memory_get_cached_embedding,
ai_memory_set_meta,
ai_memory_get_meta,
// AI session commands
ai_sessions_init,
ai_sessions_load_index,
ai_sessions_update_index,
ai_sessions_append_transcript,
ai_sessions_read_transcript,
ai_sessions_delete,
ai_sessions_list,
ai_read_memory_file,
ai_write_memory_file,
ai_list_memory_files,
// Runtime commands
runtime_discover_skills,
runtime_list_skills,
runtime_start_skill,
runtime_stop_skill,
runtime_get_skill_state,
runtime_call_tool,
runtime_all_tools,
runtime_broadcast_event,
// Runtime enable/disable + KV commands
runtime_enable_skill,
runtime_disable_skill,
runtime_is_skill_enabled,
runtime_get_skill_preferences,
runtime_skill_kv_get,
runtime_skill_kv_set,
// Runtime JSON-RPC + data commands
runtime_rpc,
runtime_skill_data_read,
runtime_skill_data_write,
runtime_skill_data_dir,
// Socket.io commands (Rust-native persistent connection)
runtime_socket_connect,
runtime_socket_disconnect,
runtime_socket_state,
runtime_socket_emit,
// TDLib commands (native Telegram library)
tdlib_create_client,
tdlib_send,
tdlib_receive,
tdlib_destroy,
tdlib_is_available,
// Model commands (backend API proxy)
model_summarize,
model_generate,
// Alphahuman commands
alphahuman_health_snapshot,
alphahuman_security_policy_info,
alphahuman_encrypt_secret,
alphahuman_decrypt_secret,
alphahuman_get_config,
alphahuman_update_model_settings,
alphahuman_update_memory_settings,
alphahuman_update_gateway_settings,
alphahuman_update_tunnel_settings,
alphahuman_update_runtime_settings,
alphahuman_update_browser_settings,
alphahuman_get_runtime_flags,
alphahuman_set_browser_allow_all,
alphahuman_agent_chat,
alphahuman_doctor_report,
alphahuman_doctor_models,
alphahuman_list_integrations,
alphahuman_get_integration_info,
alphahuman_models_refresh,
alphahuman_migrate_openclaw,
alphahuman_hardware_discover,
alphahuman_hardware_introspect,
alphahuman_service_install,
alphahuman_service_start,
alphahuman_service_stop,
alphahuman_service_status,
alphahuman_service_uninstall,
// Unified skill registry commands (mobile stubs)
unified_list_skills,
unified_execute_skill,
unified_generate_skill,
]
}
})
.build({
let mut context = tauri::generate_context!();
if daemon_mode {
context.config_mut().app.windows.clear();
}
context
})
.expect("error while building tauri application")
.run(move |app_handle, event| {
match event {
// Handle macOS Dock icon click (reopen event)
#[cfg(target_os = "macos")]
RunEvent::Reopen { .. } => {
if !daemon_mode {
show_main_window(app_handle);
}
}
// Gracefully shut down TDLib before process exit to prevent
// use-after-free crash in the blocking receive loop.
#[cfg(not(any(target_os = "android", target_os = "ios")))]
RunEvent::Exit => {
log::info!("[app] Exit event received, shutting down");
// Cancel the alphahuman daemon supervisor
if let Some(daemon) = app_handle.try_state::<alphahuman::daemon::DaemonHandle>() {
daemon.cancel.cancel();
log::info!("[alphahuman] Daemon shutdown signalled");
}
use crate::services::tdlib::TDLIB_MANAGER;
// Signal the TDLib worker to stop. The blocking receive() call
// has a 2-second internal timeout, so we must wait long enough
// for any in-flight call to finish before process teardown runs
// C++ destructors on TDLib's internal state.
TDLIB_MANAGER.signal_shutdown();
std::thread::sleep(std::time::Duration::from_millis(2500));
log::info!("[app] TDLib shutdown wait complete");
let _ = app_handle;
}
_ => {
let _ = app_handle;
}
}
});
}