Files
openhuman/app/src-tauri/src/lib.rs
T
Steven EnamakelandGitHub c1a3ae1cfe Refactor controller registration into domain schemas and generic registry (#53)
* Refactor core server helpers and enhance REPL dotenv loading

- Removed unused functions for extracting namespaces and filtering documents by namespace from `helpers.rs`, streamlining the codebase.
- Introduced dotenv loading functionality in `repl.rs`, allowing for environment variable management from a specified `.env` file.
- Added utility functions for parsing dotenv values and resolving the dotenv file path, improving configuration handling in the REPL.
- Enhanced logging for dotenv loading to provide better visibility into the process and any issues encountered.

* Add AI RPC module and enhance core server dispatch functionality

- Introduced a new `rpc` module within the `ai` namespace to handle various AI-related commands, including memory file operations and session management.
- Updated the core server's dispatch logic to integrate the new AI RPC functionality, improving modularity and maintainability.
- Removed outdated memory dispatch implementation and streamlined the overall dispatch structure for better performance and clarity.
- Enhanced error handling and logging throughout the new functionalities to improve maintainability and user feedback.

* Refactor project structure and enhance RPC functionality

- Introduced a new `rpc` module to streamline JSON-RPC handling across various domains, improving code organization and maintainability.
- Updated the core server and API modules to utilize the new `rpc` structure, enhancing modularity and reducing code duplication.
- Added new models for authentication and socket management, improving the overall functionality of the API.
- Removed outdated references to the previous `openhuman` RPC structure, ensuring a cleaner and more efficient codebase.

* Update architecture documentation and refactor AI prompt paths

- Updated references in architecture documentation to reflect the new directory structure for AI prompts, changing paths from `src/ai/prompts` to `src/openhuman/agent/prompts`.
- Enhanced clarity in command documentation by aligning AI-related commands with the updated prompt paths.
- Removed obsolete AI module and streamlined memory management references to improve code organization and maintainability.
- Introduced new markdown files for agent prompts, establishing a foundation for OpenHuman's AI capabilities.

* Update AI prompt paths and configuration references

- Changed all references from `src/ai/prompts` to `src/openhuman/agent/prompts` in documentation and code files to reflect the new directory structure.
- Updated Tauri configuration to include the new resource paths for AI prompts, ensuring proper access and functionality.
- Enhanced the AI configuration commands to align with the updated paths, improving clarity and maintainability across the project.

* Enhance core structure and introduce new RPC functionality

- Added a new `core` module to centralize shared schemas and contracts for controllers, improving code organization and maintainability.
- Introduced `jsonrpc` and `cli` modules within the `core` structure to handle JSON-RPC requests and command-line interactions, enhancing modularity.
- Defined a `ControllerSchema` for transport-agnostic function contracts, allowing for consistent handling across RPC and CLI layers.
- Established a new `all` module to manage registered controllers and their schemas, streamlining the invocation process.
- Updated documentation in `CLAUDE.md` to reflect new controller schema contracts and module organization, improving clarity for developers.

* Refactor core server structure and enhance RPC functionality

- Removed the `core_server` module and integrated its functionalities into the `core` module, improving code organization and maintainability.
- Introduced a new `dispatch` module to handle RPC requests, streamlining the invocation process for various commands.
- Updated the CLI to utilize the new core structure, enhancing command handling and modularity.
- Added comprehensive logging for RPC interactions, improving visibility and debugging capabilities.
- Enhanced error handling across the core server, ensuring consistent feedback for users and developers.

* Refactor CLI command handling and enhance JSON-RPC integration

- Consolidated CLI command structure by removing the `CoreCli` and directly implementing command functions for `run`, `call`, and `namespace`.
- Improved argument parsing for server commands, including port specification and help options, enhancing user experience.
- Streamlined the invocation of JSON-RPC methods, ensuring consistent error handling and response formatting across commands.
- Introduced a new `run_namespace_command` function to manage namespace-specific operations, improving modularity and clarity in command execution.

* Refactor SkillsPanel and TauriCommandsPanel for improved integration handling

- Removed unused integration-related functions and state management from SkillsPanel, simplifying the component's logic.
- Updated TauriCommandsPanel to eliminate integration name input and associated commands, streamlining the user interface.
- Introduced a new local AI memory management module to handle session and memory operations, enhancing overall functionality.
- Refactored core RPC client to integrate local AI method dispatching, improving command handling consistency across the application.
- Cleaned up tauriCommands utility functions to align with the new command structure, enhancing maintainability.

* Refactor TauriCommandsPanel to streamline command handling

- Removed the `openhumanModelsRefresh` function and replaced its usage with `openhumanDoctorReport`, simplifying the command logic.
- Eliminated unused model refresh buttons from the UI, enhancing the user interface and reducing clutter.
- Updated related utility functions in `tauriCommands.ts` to reflect the removal of model refresh functionality, improving maintainability.

* Refactor JSON-RPC server integration and remove legacy server module

- Updated the CLI to invoke the JSON-RPC server directly, enhancing command execution flow.
- Introduced a new HTTP router in the `jsonrpc` module, consolidating route handling for health checks and RPC requests.
- Removed the deprecated `server` module, streamlining the codebase and improving maintainability.
- Adjusted tests to reflect the new routing structure, ensuring continued functionality and integration.

* Remove submodule and update CLAUDE.md documentation

- Deleted the `.gitmodules` file and removed the `skills` submodule, simplifying the project structure.
- Updated the description in `CLAUDE.md` to reflect a broader focus on community assistance rather than just crypto, enhancing clarity.
- Added sections on coding philosophy and controller migration checklist to improve developer guidance and maintainability.

* Refactor controller registration and enhance schema management

- Introduced a centralized registry for registered controllers, improving the organization and validation of controller schemas.
- Updated the `all_registered_controllers` and `all_controller_schemas` functions across various modules to streamline controller management.
- Added comprehensive validation for controller registration to ensure consistency and prevent duplicate entries.
- Enhanced the CLI and JSON-RPC integration to utilize the new registry structure, improving command handling and modularity.
- Updated documentation in `CLAUDE.md` to reflect changes in the skills registry and controller management processes, enhancing clarity for contributors.

* Enhance autocomplete, config, and credentials modules with new schemas and controller registrations

- Added support for autocomplete, config, and credentials functionalities by introducing new schemas and registered controllers.
- Updated the `build_registered_controllers` and `build_declared_controller_schemas` functions to include new entries for autocomplete, config, and credentials.
- Implemented new JSON-RPC tests for autocomplete and config methods, ensuring proper validation and error handling.
- Refactored CLI tests to include checks for new commands related to autocomplete and configuration management, improving test coverage and reliability.
- Introduced new modules for schemas in autocomplete, config, and credentials, enhancing code organization and maintainability.

* Add local AI and migration modules with schemas and controller registrations

- Introduced local AI and migration functionalities by adding new schemas and registered controllers.
- Updated the `build_registered_controllers` and `build_declared_controller_schemas` functions to include entries for local AI and migration.
- Implemented JSON-RPC tests for local AI and migration methods, ensuring proper validation and error handling.
- Enhanced CLI tests to verify new commands related to local AI and migration, improving test coverage and reliability.
- Organized code by creating dedicated modules for schemas in local AI and migration, enhancing maintainability.

* Refactor controller schemas for config and auth modules

- Updated controller schemas for config and auth functionalities, aligning namespaces and function names for consistency.
- Changed function names in the JSON-RPC tests to reflect the new schema structure, ensuring proper invocation.
- Enhanced CLI tests to verify updated commands related to config and auth, improving test coverage and reliability.
- Organized code by consolidating related functionalities under appropriate namespaces, enhancing maintainability.

* Add agent and screen intelligence modules with schemas and controller registrations

- Introduced agent and screen intelligence functionalities by adding new schemas and registered controllers.
- Updated the `build_registered_controllers` and `build_declared_controller_schemas` functions to include entries for agent and screen intelligence.
- Created dedicated modules for schemas in agent, screen intelligence, skills, tools, tray, and workspace, enhancing code organization and maintainability.
- Implemented initial controller schemas for agent and screen intelligence, providing a foundation for future functionality.
- Enhanced the overall structure of the core module to accommodate new integrations, improving modularity and clarity.

* Enhance autocomplete and namespace descriptions in core modules

- Added a new function `namespace_description` to provide descriptions for various namespaces, improving user guidance in CLI commands.
- Updated the CLI help output to include namespace descriptions, enhancing clarity for users.
- Introduced a new `core` module for autocomplete functionalities, including various operations and structures related to inline autocomplete.
- Refactored the `autocomplete` module to improve organization and maintainability, consolidating related functionalities under appropriate namespaces.
- Implemented initial JSON-RPC operations for autocomplete, ensuring a robust interface for managing autocomplete features.

* Refactor and clean up code across multiple modules

- Removed unnecessary whitespace in `TauriCommandsPanel.tsx`, improving code readability.
- Cleaned up imports and reorganized code structure in `localCoreAiMemory.ts`, enhancing maintainability.
- Streamlined module imports in `mod.rs` files across various directories, ensuring consistency and clarity.
- Deleted the obsolete `rpc.rs` file in the `cron` module, consolidating functionality and reducing clutter.
- Updated function definitions in `ops.rs` to improve formatting and readability, enhancing overall code quality.

* Refactor session management and enhance module organization

- Changed `sessionIndex` from a mutable variable to a constant in `localCoreAiMemory.ts`, improving code clarity and immutability.
- Introduced new `ops.rs` files in the `approval`, `providers`, `skills`, and `quickjs_libs` modules, consolidating related functionalities and enhancing code organization.
- Streamlined module imports in `mod.rs` files across various directories, ensuring consistency and clarity in module structure.
- Removed obsolete code and unnecessary comments, improving overall code readability and maintainability.

* Refactor configuration schema organization and module structure

- Moved the configuration schema definitions from `mod.rs` to a new `types.rs` file, enhancing modularity and clarity.
- Updated module imports across various files to reflect the new structure, ensuring consistency in the codebase.
- Cleaned up obsolete code and comments, improving overall readability and maintainability.

* Remove obsolete configuration schema file and its associated modules

- Deleted the `types.rs` file from the configuration schema, consolidating the codebase and removing unused components.
- This change enhances maintainability by eliminating redundant code and streamlining the overall structure of the configuration management.

* Fix config schema module exports

* Add configuration schema types and enhance module exports

- Introduced a new `types.rs` file to define the top-level configuration structure for `config.toml`, improving modularity and clarity.
- Updated `mod.rs` to re-export all public types and configurations, ensuring a streamlined interface for the configuration schema.
- Enhanced the organization of configuration components, making it easier to manage and extend in the future.

* Update import path for AuthProfile and AuthProfileKind in tests module

- Changed the import statement for `AuthProfile` and `AuthProfileKind` to use the correct path, ensuring proper module resolution and consistency in the codebase.

* Refactor import statements in test files for consistency

- Cleaned up import statements across multiple test files by removing unnecessary components and ensuring uniformity in module imports.
- This change enhances code readability and maintainability by streamlining the import structure.

* Add agent chat and REPL session handling with schemas

- Introduced new schemas for agent chat and REPL session management, enhancing the functionality of the agent module.
- Implemented handlers for chat and REPL session operations, allowing for more interactive and persistent user sessions.
- Updated the memory store to include category handling for memory entries, improving data organization and retrieval.
- Refactored memory query methods to support ranked results and category storage, enhancing the memory management capabilities.
- Improved error handling in memory recall tools to ensure non-empty parameters, increasing robustness and user feedback.

* Refactor JSON-RPC method names for consistency and clarity

- Updated JSON-RPC method names in tests to follow a consistent naming convention, improving readability and maintainability.
- Adjusted parameter names in the JSON payload to align with the updated method names, ensuring proper functionality and clarity in the API interactions.
- Enhanced overall code organization by streamlining method calls in the test suite.
2026-03-29 15:59:34 -07:00

663 lines
23 KiB
Rust

//! OpenHuman Desktop Application
//!
//! This is the Rust backend for the cross-platform crypto community platform.
//! It provides deep link handling, core process RPC relay, window management,
//! and AI configuration helpers.
#[cfg(not(any(target_os = "windows", target_os = "macos", target_os = "linux")))]
compile_error!("src-tauri host is desktop-only. Non-desktop targets are not supported.");
mod commands;
mod core_process;
mod core_rpc;
mod utils;
use aes_gcm::aead::{Aead, KeyInit};
use aes_gcm::{Aes256Gcm, Key, Nonce};
use base64::{engine::general_purpose::STANDARD as B64, Engine as _};
use commands::*;
use rand::TryRngCore;
use serde::Serialize;
use std::collections::HashMap;
use std::path::PathBuf;
use tauri::{AppHandle, Emitter, Manager, RunEvent};
use tokio::{
fs,
time::{interval, Duration},
};
#[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)
}
fn derive_key(password: &str) -> [u8; 32] {
use sha2::{Digest, Sha256};
let mut hasher = Sha256::new();
hasher.update(password.as_bytes());
let hash = hasher.finalize();
let mut key = [0u8; 32];
key.copy_from_slice(&hash[..32]);
key
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct AIPreview {
soul: AIPreviewSoul,
tools: AIPreviewTools,
metadata: AIPreviewMetadata,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct AIPreviewSoul {
raw: String,
name: String,
description: String,
personality_preview: Vec<String>,
safety_rules_preview: Vec<String>,
loaded_at: i64,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct AIPreviewTools {
raw: String,
total_tools: usize,
active_skills: usize,
skills_preview: Vec<String>,
loaded_at: i64,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct AIPreviewMetadata {
loaded_at: i64,
loading_duration: i64,
has_fallbacks: bool,
sources: AIPreviewSources,
errors: Vec<String>,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
struct AIPreviewSources {
soul: String,
tools: String,
}
fn now_ms() -> i64 {
chrono::Utc::now().timestamp_millis()
}
fn extract_section(raw: &str, heading: &str) -> String {
let marker = format!("## {heading}");
let Some(start) = raw.find(&marker) else {
return String::new();
};
let body = &raw[start + marker.len()..];
if let Some(next_idx) = body.find("\n## ") {
body[..next_idx].trim().to_string()
} else {
body.trim().to_string()
}
}
fn parse_soul_preview(raw: String, loaded_at: i64) -> AIPreviewSoul {
let name = raw
.lines()
.find_map(|line| line.strip_prefix("# ").map(|s| s.trim().to_string()))
.unwrap_or_else(|| "OpenHuman".to_string());
let description = raw
.lines()
.map(str::trim)
.find(|line| !line.is_empty() && !line.starts_with('#'))
.unwrap_or("AI assistant")
.to_string();
let personality_preview = extract_section(&raw, "Personality")
.lines()
.filter_map(|line| line.trim().strip_prefix("- **"))
.filter_map(|line| {
let mut parts = line.splitn(2, "**:");
let trait_name = parts.next()?.trim();
let detail = parts.next().unwrap_or("").trim();
Some(format!("{trait_name}: {detail}"))
})
.take(3)
.collect::<Vec<_>>();
let safety_rules_preview = extract_section(&raw, "Safety Rules")
.lines()
.filter_map(|line| {
let trimmed = line.trim();
let dot_idx = trimmed.find('.')?;
let (prefix, rest) = trimmed.split_at(dot_idx);
if prefix.chars().all(|c| c.is_ascii_digit()) {
Some(rest.trim_start_matches('.').trim().to_string())
} else {
None
}
})
.take(3)
.collect::<Vec<_>>();
AIPreviewSoul {
raw,
name,
description,
personality_preview,
safety_rules_preview,
loaded_at,
}
}
fn parse_tools_preview(raw: String, loaded_at: i64) -> AIPreviewTools {
let mut current_skill = "General".to_string();
let mut skill_counts: HashMap<String, usize> = HashMap::new();
let mut total_tools = 0usize;
for line in raw.lines() {
let trimmed = line.trim();
if let Some(title) = trimmed.strip_prefix("### ") {
if let Some(skill_title) = title.strip_suffix(" Tools") {
current_skill = skill_title.trim().to_string();
skill_counts.entry(current_skill.clone()).or_insert(0);
}
continue;
}
if trimmed.starts_with("#### ") {
total_tools += 1;
*skill_counts.entry(current_skill.clone()).or_insert(0) += 1;
}
}
let mut skills = skill_counts.into_iter().collect::<Vec<_>>();
let active_skills = skills.len();
skills.sort_by(|a, b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
let skills_preview = skills
.into_iter()
.take(6)
.map(|(name, count)| format!("{name} ({count})"))
.collect::<Vec<_>>();
AIPreviewTools {
raw,
total_tools,
active_skills,
skills_preview,
loaded_at,
}
}
fn resolve_ai_directory(app: &tauri::AppHandle) -> Option<(PathBuf, &'static str)> {
if let Ok(resource_dir) = app.path().resource_dir() {
if let Some(ai_dir) = utils::dev_paths::bundled_openclaw_prompts_dir(&resource_dir) {
return Some((ai_dir, "bundled"));
}
}
if let Ok(cwd) = std::env::current_dir() {
if let Some(path) = utils::dev_paths::repo_ai_prompts_dir(&cwd) {
return Some((path, "bundled"));
}
let fallback = cwd.join("ai");
if fallback.is_dir() {
return Some((fallback, "bundled"));
}
}
None
}
fn build_ai_preview(app: &tauri::AppHandle) -> AIPreview {
let started = now_ms();
let loaded_at = now_ms();
let mut errors = Vec::new();
let mut soul_raw = String::new();
let mut tools_raw = String::new();
let mut source = "bundled".to_string();
if let Some((ai_dir, resolved_source)) = resolve_ai_directory(app) {
source = resolved_source.to_string();
let soul_path = ai_dir.join("SOUL.md");
let tools_path = ai_dir.join("TOOLS.md");
soul_raw = std::fs::read_to_string(&soul_path).unwrap_or_else(|e| {
errors.push(format!("Failed to read SOUL.md: {e}"));
String::new()
});
tools_raw = std::fs::read_to_string(&tools_path).unwrap_or_else(|e| {
errors.push(format!("Failed to read TOOLS.md: {e}"));
String::new()
});
} else {
errors.push("AI config directory not found".to_string());
}
let soul = parse_soul_preview(soul_raw, loaded_at);
let tools = parse_tools_preview(tools_raw, loaded_at);
let done = now_ms();
AIPreview {
soul,
tools,
metadata: AIPreviewMetadata {
loaded_at: done,
loading_duration: done - started,
has_fallbacks: false,
sources: AIPreviewSources {
soul: source.clone(),
tools: source,
},
errors,
},
}
}
#[tauri::command]
async fn ai_get_config(app: tauri::AppHandle) -> Result<AIPreview, String> {
Ok(build_ai_preview(&app))
}
#[tauri::command]
async fn ai_refresh_config(app: tauri::AppHandle) -> Result<AIPreview, String> {
Ok(build_ai_preview(&app))
}
/// Write AI configuration files to `src/openhuman/agent/prompts` in the repo (dev resolution from cwd).
#[tauri::command]
async fn write_ai_config_file(filename: String, content: String) -> Result<bool, String> {
use std::env;
// Determine runtime working directory
let current_dir =
env::current_dir().map_err(|e| format!("Failed to get current directory: {e}"))?;
// Ensure filename is safe (only allow .md files)
if !filename.ends_with(".md") {
return Err("Only .md files are allowed".to_string());
}
// Prevent path traversal by checking for dangerous characters
if filename.contains("..") || filename.contains("/") || filename.contains("\\") {
return Err("Invalid filename: path traversal not allowed".to_string());
}
let ai_dir = utils::dev_paths::repo_ai_prompts_dir(&current_dir)
.unwrap_or_else(|| {
current_dir
.join("src")
.join("openhuman")
.join("agent")
.join("prompts")
});
let file_path = ai_dir.join(&filename);
// Ensure ai directory exists
std::fs::create_dir_all(&ai_dir).map_err(|e| format!("Failed to create ai directory: {e}"))?;
// Write the file
std::fs::write(&file_path, content)
.map_err(|e| format!("Failed to write file {}: {e}", filename))?;
Ok(true)
}
fn is_daemon_mode() -> bool {
std::env::args().any(|arg| arg == "daemon" || arg == "--daemon")
}
/// 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!(
"[openhuman] 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!(
"[openhuman] Broadcasting health event from file: {:?}",
json_value
);
// Emit Tauri event to frontend (same as internal daemon)
if let Err(e) = app_handle.emit("openhuman:health", &json_value) {
log::error!(
"[openhuman] Failed to emit health event from file: {}",
e
);
} else {
log::debug!(
"[openhuman] Health event emitted successfully from file"
);
}
} else {
log::debug!(
"[openhuman] Failed to parse health file as JSON: {}",
state_file.display()
);
}
} else {
log::debug!(
"[openhuman] Failed to read health file: {}",
state_file.display()
);
}
}
}
} else {
// File doesn't exist yet - external daemon may not be writing yet
log::debug!(
"[openhuman] Health file not found yet: {}",
state_file.display()
);
}
}
}
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 logger
{
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. "[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("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| {
// Register deep link handlers (Windows/Linux)
#[cfg(any(windows, target_os = "linux"))]
{
app.deep_link().register_all()?;
}
// 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();
}
}
});
}
}
// Bridge external daemon health file and ensure core background service.
{
let data_dir = app.path().app_data_dir().unwrap_or_else(|_| {
dirs::home_dir()
.unwrap_or_else(|| std::path::PathBuf::from("."))
.join(".openhuman")
});
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;
});
tauri::async_runtime::spawn(async move {
match commands::core_relay::ensure_service_managed_core_running().await {
Ok(()) => {
log::info!("[openhuman] Core background service ensured via core RPC");
}
Err(e) => {
log::error!(
"[openhuman] Failed to ensure core background service: {e}"
);
}
}
});
}
// Start/ensure standalone core process for business logic RPC.
{
let core_run_mode = core_process::default_core_run_mode(daemon_mode);
let core_bin = if matches!(core_run_mode, core_process::CoreRunMode::ChildProcess) {
core_process::default_core_bin()
} else {
None
};
let core_handle = core_process::CoreProcessHandle::new(
core_process::default_core_port(),
core_bin,
core_run_mode,
);
std::env::set_var("OPENHUMAN_CORE_RPC_URL", core_handle.rpc_url());
app.manage(core_handle.clone());
tauri::async_runtime::spawn(async move {
if let Err(err) = core_handle.ensure_running().await {
log::error!("[core] failed to start core process: {err}");
} else {
log::info!("[core] core process ready");
}
});
}
if daemon_mode {
if let Some(window) = app.get_webview_window("main") {
let _ = window.hide();
}
}
Ok(())
})
// Register all commands (desktop build lists handlers explicitly below).
.invoke_handler({
#[cfg(desktop)]
{
tauri::generate_handler![
greet,
// AI config file writing
write_ai_config_file,
ai_get_config,
ai_refresh_config,
core_rpc_relay,
show_window,
hide_window,
toggle_window,
is_window_visible,
minimize_window,
maximize_window,
close_window,
set_window_title,
// OpenHuman local host commands (core RPC uses core_rpc_relay)
openhuman_get_daemon_host_config,
openhuman_set_daemon_host_config,
openhuman_service_install,
openhuman_service_start,
openhuman_service_stop,
openhuman_service_status,
openhuman_service_uninstall,
]
}
})
.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 {
if let Some(window) = app_handle.get_webview_window("main") {
let _ = window.show();
let _ = window.unminimize();
let _ = window.set_focus();
}
}
}
// Gracefully shut down background services before process exit.
RunEvent::Exit => {
log::info!("[app] Exit event received, shutting down");
let _ = app_handle;
}
_ => {
let _ = app_handle;
}
}
});
}
pub fn run_core_from_args(args: &[String]) -> anyhow::Result<()> {
let core_bin = crate::core_process::default_core_bin()
.ok_or_else(|| anyhow::anyhow!("openhuman core binary not found"))?;
let status = std::process::Command::new(core_bin)
.args(args)
.status()
.map_err(|e| anyhow::anyhow!("failed to execute core binary: {e}"))?;
if !status.success() {
anyhow::bail!("core binary exited with status {status}");
}
Ok(())
}