diff --git a/.github/workflows/package-and-publish.yml b/.github/workflows/package-and-publish.yml index 4f2aab24f..34079e932 100644 --- a/.github/workflows/package-and-publish.yml +++ b/.github/workflows/package-and-publish.yml @@ -201,11 +201,8 @@ jobs: sudo apt-get update sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf - - name: Install OpenMP (macOS only) - if: matrix.settings.platform == 'macos-latest' - run: | - brew install libomp - echo "OpenMP installed for llama-cpp-sys build" + # OpenMP is now disabled via Cargo.toml (default-features = false for llama-cpp-2) + # This avoids cross-compilation issues where Homebrew ARM64 OpenMP can't be used for x86_64 builds - name: Cache Cargo registry uses: actions/cache@v4 diff --git a/package.json b/package.json index d7c8c75f7..7b75e3993 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "@tauri-apps/api": "^2", "@tauri-apps/plugin-deep-link": "^2", "@tauri-apps/plugin-opener": "^2", + "@tauri-apps/plugin-os": "^2.3.2", "@tauri-apps/plugin-shell": "~2", "@types/react-router-dom": "^5.3.3", "buffer": "^6.0.3", diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 019952508..9db94fefd 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -23,7 +23,6 @@ dependencies = [ "llama-cpp-2", "log", "once_cell", - "openssl", "parking_lot", "rand 0.8.5", "reqwest", @@ -38,6 +37,7 @@ dependencies = [ "tauri-plugin-deep-link", "tauri-plugin-notification", "tauri-plugin-opener", + "tauri-plugin-os", "tdlib-rs", "tokio", "tokio-tungstenite 0.24.0", @@ -1917,6 +1917,16 @@ dependencies = [ "version_check", ] +[[package]] +name = "gethostname" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bd49230192a3797a9a4d6abe9b3eed6f7fa4c8a8a4947977c6f80025f92cbd8" +dependencies = [ + "rustix 1.1.3", + "windows-link 0.2.1", +] + [[package]] name = "getrandom" version = "0.1.16" @@ -3094,6 +3104,18 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nix" +version = "0.30.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" +dependencies = [ + "bitflags 2.10.0", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nodrop" version = "0.1.14" @@ -3268,6 +3290,16 @@ dependencies = [ "objc2-foundation", ] +[[package]] +name = "objc2-core-location" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca347214e24bc973fc025fd0d36ebb179ff30536ed1f80252706db19ee452009" +dependencies = [ + "objc2", + "objc2-foundation", +] + [[package]] name = "objc2-core-text" version = "0.3.2" @@ -3372,8 +3404,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d87d638e33c06f577498cbcc50491496a3ed4246998a7fbba7ccb98b1e7eab22" dependencies = [ "bitflags 2.10.0", + "block2", "objc2", + "objc2-cloud-kit", + "objc2-core-data", "objc2-core-foundation", + "objc2-core-graphics", + "objc2-core-image", + "objc2-core-location", + "objc2-core-text", + "objc2-foundation", + "objc2-quartz-core", + "objc2-user-notifications", +] + +[[package]] +name = "objc2-user-notifications" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9df9128cbbfef73cda168416ccf7f837b62737d748333bfe9ab71c245d76613e" +dependencies = [ + "objc2", "objc2-foundation", ] @@ -3455,15 +3506,6 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" -[[package]] -name = "openssl-src" -version = "300.5.5+3.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f1787d533e03597a7934fd0a765f0d28e94ecc5fb7789f8053b1e699a56f709" -dependencies = [ - "cc", -] - [[package]] name = "openssl-sys" version = "0.9.111" @@ -3472,7 +3514,6 @@ checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321" dependencies = [ "cc", "libc", - "openssl-src", "pkg-config", "vcpkg", ] @@ -3503,6 +3544,22 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "os_info" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e4022a17595a00d6a369236fdae483f0de7f0a339960a53118b818238e132224" +dependencies = [ + "android_system_properties", + "log", + "nix", + "objc2", + "objc2-foundation", + "objc2-ui-kit", + "serde", + "windows-sys 0.61.2", +] + [[package]] name = "outref" version = "0.1.0" @@ -5112,6 +5169,15 @@ dependencies = [ "syn 2.0.114", ] +[[package]] +name = "sys-locale" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" +dependencies = [ + "libc", +] + [[package]] name = "system-configuration" version = "0.6.1" @@ -5416,6 +5482,24 @@ dependencies = [ "zbus", ] +[[package]] +name = "tauri-plugin-os" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8f08346c8deb39e96f86973da0e2d76cbb933d7ac9b750f6dc4daf955a6f997" +dependencies = [ + "gethostname", + "log", + "os_info", + "serde", + "serde_json", + "serialize-to-javascript", + "sys-locale", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", +] + [[package]] name = "tauri-runtime" version = "2.9.2" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 537f613cd..ea0438fca 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -22,6 +22,7 @@ tauri-build = { version = "2", features = [] } tauri = { version = "2", features = ["tray-icon", "macos-private-api"] } tauri-plugin-opener = "2" tauri-plugin-deep-link = "2.0.0" +tauri-plugin-os = "2" # tauri-plugin-autostart moved to desktop-only target section # tauri-plugin-notification moved to desktop-only target section # Serialization @@ -74,12 +75,15 @@ chrono = { version = "0.4", features = ["serde"] } cron = "0.12" # Persistent Socket.io client (Rust-native, survives app backgrounding) -rust_socketio = { version = "0.6", features = ["async"] } +# Note: Using default native-tls on desktop, but we'll handle Android separately futures-util = "0.3" +# Desktop/iOS use native-tls for Socket.io +[target.'cfg(not(target_os = "android"))'.dependencies] +rust_socketio = { version = "0.6", features = ["async"] } + +# Android uses a simpler approach - no persistent Socket.io (use web socket from frontend) [target.'cfg(target_os = "android")'.dependencies] -# Vendor OpenSSL for Android (rust_socketio requires native-tls which needs OpenSSL) -openssl = { version = "0.10", features = ["vendored"] } # Android logging (routes Rust `log` crate to logcat) android_logger = "0.14" @@ -92,16 +96,16 @@ tauri-plugin-notification = "2" deno_core = "0.314" # TDLib Rust bindings (desktop only - downloads prebuilt TDLib library) tdlib-rs = { version = "1.2", features = ["download-tdlib"] } +# Local LLM inference - desktop only (llama.cpp requires native C++ compilation) +# Disable default features (including openmp) to avoid cross-compilation issues +# on macOS where Homebrew installs ARM64-only OpenMP libraries +llama-cpp-2 = { version = "0.1", default-features = false } +encoding_rs = "0.8" [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.build-dependencies] # TDLib build configuration tdlib-rs = { version = "1.2", features = ["download-tdlib"] } -# Local LLM inference - available on desktop and Android (not iOS) -[target.'cfg(not(target_os = "ios"))'.dependencies] -llama-cpp-2 = "0.1" -encoding_rs = "0.8" - [features] # This feature is used for production builds or when a dev server is not specified, DO NOT REMOVE!! custom-protocol = ["tauri/custom-protocol"] diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index bb16970ca..d1253b2a6 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -8,6 +8,7 @@ "core:default", "opener:default", "deep-link:default", + "os:default", "core:tray:default", "autostart:default", "autostart:allow-enable", diff --git a/src-tauri/capabilities/mobile.json b/src-tauri/capabilities/mobile.json index 9c150e1c6..1f75ed4d7 100644 --- a/src-tauri/capabilities/mobile.json +++ b/src-tauri/capabilities/mobile.json @@ -7,6 +7,7 @@ "core:default", "opener:default", "deep-link:default", + "os:default", "shell:default", "shell:allow-spawn", "shell:allow-open" diff --git a/src-tauri/src/commands/model.rs b/src-tauri/src/commands/model.rs index 114928a75..8a1f54216 100644 --- a/src-tauri/src/commands/model.rs +++ b/src-tauri/src/commands/model.rs @@ -51,14 +51,16 @@ fn default_top_p() -> f32 { } /// Check if the local model API is available on this platform. +/// Note: Currently only available on desktop (Windows, macOS, Linux). +/// Android/iOS support requires additional llama.cpp NDK configuration. #[tauri::command] pub fn model_is_available() -> bool { - #[cfg(not(target_os = "ios"))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] { true } - #[cfg(target_os = "ios")] + #[cfg(any(target_os = "android", target_os = "ios"))] { false } @@ -67,7 +69,7 @@ pub fn model_is_available() -> bool { /// Get the current model status. #[tauri::command] pub fn model_get_status() -> ModelStatusResponse { - #[cfg(not(target_os = "ios"))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] { let status = crate::services::llama::LLAMA_MANAGER.get_status(); ModelStatusResponse { @@ -80,14 +82,14 @@ pub fn model_get_status() -> ModelStatusResponse { } } - #[cfg(target_os = "ios")] + #[cfg(any(target_os = "android", target_os = "ios"))] { ModelStatusResponse { available: false, loaded: false, loading: false, download_progress: None, - error: Some("Model not available on iOS".to_string()), + error: Some("Model not available on mobile platforms".to_string()), model_path: None, } } @@ -97,21 +99,21 @@ pub fn model_get_status() -> ModelStatusResponse { /// This is useful for preloading the model. #[tauri::command] pub async fn model_ensure_loaded() -> Result<(), String> { - #[cfg(not(target_os = "ios"))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] { crate::services::llama::LLAMA_MANAGER.ensure_loaded().await } - #[cfg(target_os = "ios")] + #[cfg(any(target_os = "android", target_os = "ios"))] { - Err("Model not available on iOS".to_string()) + Err("Model not available on mobile platforms".to_string()) } } /// Generate text from a prompt. #[tauri::command] pub async fn model_generate(request: GenerateRequest) -> Result { - #[cfg(not(target_os = "ios"))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] { use crate::services::llama::GenerateConfig; @@ -126,17 +128,17 @@ pub async fn model_generate(request: GenerateRequest) -> Result .await } - #[cfg(target_os = "ios")] + #[cfg(any(target_os = "android", target_os = "ios"))] { let _ = request; - Err("Model not available on iOS".to_string()) + Err("Model not available on mobile platforms".to_string()) } } /// Summarize text using a built-in prompt. #[tauri::command] pub async fn model_summarize(text: String, max_tokens: Option) -> Result { - #[cfg(not(target_os = "ios"))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] { let tokens = max_tokens.unwrap_or(500); crate::services::llama::LLAMA_MANAGER @@ -144,24 +146,24 @@ pub async fn model_summarize(text: String, max_tokens: Option) -> Result Result<(), String> { - #[cfg(not(target_os = "ios"))] + #[cfg(not(any(target_os = "android", target_os = "ios")))] { crate::services::llama::LLAMA_MANAGER.unload(); Ok(()) } - #[cfg(target_os = "ios")] + #[cfg(any(target_os = "android", target_os = "ios"))] { - Err("Model not available on iOS".to_string()) + Err("Model not available on mobile platforms".to_string()) } } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 1cc95fbef..4cdcdce4f 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -236,7 +236,8 @@ pub fn run() { let mut builder = tauri::Builder::default() // Plugins .plugin(tauri_plugin_opener::init()) - .plugin(tauri_plugin_deep_link::init()); + .plugin(tauri_plugin_deep_link::init()) + .plugin(tauri_plugin_os::init()); // Add desktop-only plugins (autostart, notification) #[cfg(desktop)] @@ -341,20 +342,7 @@ pub fn run() { #[cfg(target_os = "android")] { - log::info!("[runtime] V8 runtime disabled on Android"); - - // Initialize local model service for Android - let data_dir = app - .path() - .app_data_dir() - .unwrap_or_else(|_| { - dirs::home_dir() - .unwrap_or_else(|| std::path::PathBuf::from(".")) - .join(".alphahuman") - }); - let model_dir = data_dir.join("models"); - services::llama::LLAMA_MANAGER.set_data_dir(model_dir); - log::info!("[runtime] Local model service initialized for Android"); + log::info!("[runtime] V8 runtime and local model disabled on Android"); } #[cfg(target_os = "ios")] diff --git a/src-tauri/src/runtime/socket_manager.rs b/src-tauri/src/runtime/socket_manager.rs index 63213d963..ce6b7d8f9 100644 --- a/src-tauri/src/runtime/socket_manager.rs +++ b/src-tauri/src/runtime/socket_manager.rs @@ -8,20 +8,28 @@ //! - Non-MCP server events forwarded to running skills AND to the frontend //! - Connection state emitted to the frontend via Tauri events //! - Automatic reconnection with exponential backoff +//! +//! Note: On Android, the Rust Socket.io client is not available due to +//! native-tls/OpenSSL build complexity. The frontend uses its own Socket.io +//! connection instead. use std::sync::Arc; -use futures_util::FutureExt; use parking_lot::RwLock; -use rust_socketio::{ - asynchronous::{Client, ClientBuilder}, - Event, Payload, -}; use serde_json::json; use tauri::{AppHandle, Emitter}; use crate::models::socket::{ConnectionStatus, SocketState}; +// rust_socketio only available on non-Android platforms +#[cfg(not(target_os = "android"))] +use futures_util::FutureExt; +#[cfg(not(target_os = "android"))] +use rust_socketio::{ + asynchronous::{Client, ClientBuilder}, + Event, Payload, +}; + // SkillRegistry only available on desktop (V8/deno_core required) #[cfg(not(any(target_os = "android", target_os = "ios")))] use crate::runtime::skill_registry::SkillRegistry; diff --git a/src-tauri/src/services/mod.rs b/src-tauri/src/services/mod.rs index 7ee1b66e9..278131b64 100644 --- a/src-tauri/src/services/mod.rs +++ b/src-tauri/src/services/mod.rs @@ -6,6 +6,6 @@ pub mod tdlib_v8; #[cfg(desktop)] pub mod notification_service; -// Local LLM inference - available on desktop and Android (not iOS) -#[cfg(not(target_os = "ios"))] +// Local LLM inference - desktop only (llama.cpp requires native C++ compilation) +#[cfg(not(any(target_os = "android", target_os = "ios")))] pub mod llama; diff --git a/src/components/ModelDownloadProgress.tsx b/src/components/ModelDownloadProgress.tsx index c74ca1f9a..cea693b92 100644 --- a/src/components/ModelDownloadProgress.tsx +++ b/src/components/ModelDownloadProgress.tsx @@ -1,3 +1,5 @@ +import { useEffect, useState } from 'react'; + import { useModelStatus } from '../hooks/useModelStatus'; interface ModelDownloadProgressProps { @@ -11,6 +13,50 @@ const ModelDownloadProgress = ({ }: ModelDownloadProgressProps) => { const { isAvailable, isLoaded, isLoading, downloadProgress, error, ensureLoaded } = useModelStatus(); + const [isMobile, setIsMobile] = useState(false); + + useEffect(() => { + // Detect mobile platform + const detectMobile = async () => { + try { + const { platform } = await import('@tauri-apps/plugin-os'); + const currentPlatform = await platform(); + setIsMobile(currentPlatform === 'android' || currentPlatform === 'ios'); + } catch { + // If we can't detect platform, assume desktop + setIsMobile(false); + } + }; + detectMobile(); + }, []); + + // Show mobile-only message on mobile platforms + if (isMobile) { + return ( +
+
+
+ + + +
+
+ Local AI Model +

Available on desktop only

+
+
+
+ ); + } // Don't render if not available on this platform if (!isAvailable) { diff --git a/src/components/SkillsGrid.tsx b/src/components/SkillsGrid.tsx index 4a22e11fb..222ce669d 100644 --- a/src/components/SkillsGrid.tsx +++ b/src/components/SkillsGrid.tsx @@ -276,6 +276,7 @@ function SkillActionButton({ export default function SkillsGrid() { const [skillsList, setSkillsList] = useState([]); const [loading, setLoading] = useState(true); + const [isMobile, setIsMobile] = useState(false); const [setupModalOpen, setSetupModalOpen] = useState(false); const [managementModalOpen, setManagementModalOpen] = useState(false); const [activeSkillId, setActiveSkillId] = useState(null); @@ -288,6 +289,19 @@ export default function SkillsGrid() { const skillStates = useAppSelector(state => state.skills.skillStates); useEffect(() => { + // Detect mobile platform + const detectMobile = async () => { + try { + const { platform } = await import('@tauri-apps/plugin-os'); + const currentPlatform = await platform(); + setIsMobile(currentPlatform === 'android' || currentPlatform === 'ios'); + } catch { + // If we can't detect platform, assume desktop + setIsMobile(false); + } + }; + detectMobile(); + // Load skills from the V8 runtime engine. const loadSkills = async () => { try { @@ -355,7 +369,38 @@ export default function SkillsGrid() { }); }, [skillsList, skillsState, skillStates]); - // If loading or no skills, don't render + // Show mobile-only message on mobile platforms + if (!loading && isMobile) { + return ( +
+

+ Skills +

+
+
+ + + +

Skills are available on desktop only

+

+ Use the desktop app to configure and run skills +

+
+
+
+ ); + } + + // If loading or no skills on desktop, don't render if (loading || skillsList.length === 0) { return null; } diff --git a/yarn.lock b/yarn.lock index 3df5a4090..9da88127a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -859,6 +859,13 @@ dependencies: "@tauri-apps/api" "^2.8.0" +"@tauri-apps/plugin-os@^2.3.2": + version "2.3.2" + resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-os/-/plugin-os-2.3.2.tgz#de916a82d8d955bba59a2ffdba7f7aaa29397246" + integrity sha512-n+nXWeuSeF9wcEsSPmRnBEGrRgOy6jjkSU+UVCOV8YUGKb2erhDOxis7IqRXiRVHhY8XMKks00BJ0OAdkpf6+A== + dependencies: + "@tauri-apps/api" "^2.8.0" + "@tauri-apps/plugin-shell@~2": version "2.3.4" resolved "https://registry.yarnpkg.com/@tauri-apps/plugin-shell/-/plugin-shell-2.3.4.tgz#26cbfca659d127da81b0d4583dec9a56d13f5dfe"