diff --git a/.gitignore b/.gitignore index 6b51867ef..e78486abd 100644 --- a/.gitignore +++ b/.gitignore @@ -21,11 +21,13 @@ src-tauri/resources/lib src-tauri/icons !src-tauri/icons/icon.png src-tauri/gen/apple +src-tauri/gen/android src-tauri/resources/bin # Helper tools .opencode OpenCode.md +Claude.md archive/ .cache/ @@ -60,3 +62,4 @@ src-tauri/resources/ ## test test-data llm-docs +.claude/agents diff --git a/Makefile b/Makefile index e2262a842..890f612c8 100644 --- a/Makefile +++ b/Makefile @@ -41,6 +41,23 @@ else @echo "Not macOS; skipping Rust target installation." endif +# Install required Rust targets for Android builds +install-android-rust-targets: + @echo "Checking and installing Android Rust targets..." + @rustup target list --installed | grep -q "aarch64-linux-android" || rustup target add aarch64-linux-android + @rustup target list --installed | grep -q "armv7-linux-androideabi" || rustup target add armv7-linux-androideabi + @rustup target list --installed | grep -q "i686-linux-android" || rustup target add i686-linux-android + @rustup target list --installed | grep -q "x86_64-linux-android" || rustup target add x86_64-linux-android + @echo "Android Rust targets ready!" + +# Install required Rust targets for iOS builds +install-ios-rust-targets: + @echo "Checking and installing iOS Rust targets..." + @rustup target list --installed | grep -q "aarch64-apple-ios" || rustup target add aarch64-apple-ios + @rustup target list --installed | grep -q "aarch64-apple-ios-sim" || rustup target add aarch64-apple-ios-sim + @rustup target list --installed | grep -q "x86_64-apple-ios" || rustup target add x86_64-apple-ios + @echo "iOS Rust targets ready!" + dev: install-and-build yarn download:bin yarn dev @@ -63,6 +80,35 @@ serve-web-app: build-serve-web-app: build-web-app yarn serve:web-app +# Mobile +dev-android: install-and-build install-android-rust-targets + @echo "Setting up Android development environment..." + @if [ ! -d "src-tauri/gen/android" ]; then \ + echo "Android app not initialized. Initializing..."; \ + yarn tauri android init; \ + fi + @echo "Sourcing Android environment setup..." + @bash autoqa/scripts/setup-android-env.sh echo "Android environment ready" + @echo "Starting Android development server..." + yarn dev:android + +dev-ios: install-and-build install-ios-rust-targets + @echo "Setting up iOS development environment..." +ifeq ($(shell uname -s),Darwin) + @if [ ! -d "src-tauri/gen/ios" ]; then \ + echo "iOS app not initialized. Initializing..."; \ + yarn tauri ios init; \ + fi + @echo "Checking iOS development requirements..." + @xcrun --version > /dev/null 2>&1 || (echo "❌ Xcode command line tools not found. Install with: xcode-select --install" && exit 1) + @xcrun simctl list devices available | grep -q "iPhone\|iPad" || (echo "❌ No iOS simulators found. Install simulators through Xcode." && exit 1) + @echo "Starting iOS development server..." + yarn dev:ios +else + @echo "❌ iOS development is only supported on macOS" + @exit 1 +endif + # Linting lint: install-and-build yarn lint diff --git a/autoqa/scripts/setup-android-env.sh b/autoqa/scripts/setup-android-env.sh new file mode 100755 index 000000000..62adc079f --- /dev/null +++ b/autoqa/scripts/setup-android-env.sh @@ -0,0 +1,80 @@ +#!/bin/bash + +# Android Development Environment Setup for Jan + +# Ensure rustup's Rust toolchain is used instead of Homebrew's +export PATH="$HOME/.cargo/bin:$PATH" + +# Set JAVA_HOME for Android builds +export JAVA_HOME=/opt/homebrew/opt/openjdk@17/libexec/openjdk.jdk/Contents/Home +export PATH="/opt/homebrew/opt/openjdk@17/bin:$PATH" + +export ANDROID_HOME="$HOME/Library/Android/sdk" +export ANDROID_NDK_ROOT="$HOME/Library/Android/sdk/ndk/29.0.14033849" +export NDK_HOME="$HOME/Library/Android/sdk/ndk/29.0.14033849" + +# Add Android tools to PATH +export PATH=$PATH:$ANDROID_HOME/platform-tools:$ANDROID_HOME/tools:$ANDROID_HOME/cmdline-tools/latest/bin:$ANDROID_HOME/emulator:$NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin + +# Set up CC and CXX for Android compilation +export CC_aarch64_linux_android="$NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android21-clang" +export CXX_aarch64_linux_android="$NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android21-clang++" +export AR_aarch64_linux_android="$NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin/llvm-ar" +export RANLIB_aarch64_linux_android="$NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin/llvm-ranlib" + +# Additional environment variables for Rust cross-compilation +export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="$NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android21-clang" + +# Only set global CC and AR for Android builds (when TAURI_ANDROID_BUILD is set) +if [ "$TAURI_ANDROID_BUILD" = "true" ]; then + export CC="$NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android21-clang" + export AR="$NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin/llvm-ar" + echo "Global CC and AR set for Android build" +fi + +# Create symlinks for Android tools if they don't exist +mkdir -p ~/.local/bin +if [ ! -f ~/.local/bin/aarch64-linux-android-ranlib ]; then + ln -sf $NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin/llvm-ranlib ~/.local/bin/aarch64-linux-android-ranlib +fi +if [ ! -f ~/.local/bin/aarch64-linux-android-clang ]; then + ln -sf $NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android21-clang ~/.local/bin/aarch64-linux-android-clang +fi +if [ ! -f ~/.local/bin/aarch64-linux-android-clang++ ]; then + ln -sf $NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin/aarch64-linux-android21-clang++ ~/.local/bin/aarch64-linux-android-clang++ +fi + +# Fix the broken clang symlinks by ensuring base clang is available +if [ ! -f ~/.local/bin/clang ]; then + ln -sf $NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin/clang ~/.local/bin/clang +fi +if [ ! -f ~/.local/bin/clang++ ]; then + ln -sf $NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin/clang++ ~/.local/bin/clang++ +fi + +# Create symlinks for target-specific ar tools +if [ ! -f ~/.local/bin/aarch64-linux-android-ar ]; then + ln -sf $NDK_HOME/toolchains/llvm/prebuilt/darwin-x86_64/bin/llvm-ar ~/.local/bin/aarch64-linux-android-ar +fi +export PATH="$HOME/.local/bin:$PATH" + +echo "Android environment configured:" +echo "ANDROID_HOME: $ANDROID_HOME" +echo "ANDROID_NDK_ROOT: $ANDROID_NDK_ROOT" +echo "PATH includes NDK toolchain: $(echo $PATH | grep -o "ndk.*bin" || echo "NOT FOUND")" + +# Verify required tools +echo -e "\nChecking required tools:" +which adb && echo "✅ adb found" || echo "❌ adb not found" +which emulator && echo "✅ emulator found" || echo "❌ emulator not found" +which $CC_aarch64_linux_android && echo "✅ Android clang found" || echo "❌ Android clang not found" + +# Show available AVDs +echo -e "\nAvailable Android Virtual Devices:" +emulator -list-avds 2>/dev/null || echo "No AVDs found" + +# Execute the provided command +if [ "$1" ]; then + echo -e "\nExecuting: $@" + exec "$@" +fi \ No newline at end of file diff --git a/extensions/yarn.lock b/extensions/yarn.lock index 0751c0069..f4a58c14f 100644 --- a/extensions/yarn.lock +++ b/extensions/yarn.lock @@ -342,41 +342,41 @@ __metadata: "@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fassistant-extension%40workspace%3Aassistant-extension": version: 0.1.10 - resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=f9bdfe&locator=%40janhq%2Fassistant-extension%40workspace%3Aassistant-extension" + resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=f15485&locator=%40janhq%2Fassistant-extension%40workspace%3Aassistant-extension" dependencies: rxjs: "npm:^7.8.1" ulidx: "npm:^2.3.0" - checksum: 10c0/417ea9bd3e5b53264596d2ee816c3e24299f8b721f6ea951d078342555da457ebca4d5b1e116bf187ac77ec0a9e3341211d464f4ffdbd2a3915139523688d41d + checksum: 10c0/257621cb56db31a4dd3a2b509ec4c61217022e74bbd39cf6a1a172073654b9a65eee94ef9c1b4d4f5d2231d159c8818cb02846f3d88fe14f102f43169ad3737c languageName: node linkType: hard "@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fconversational-extension%40workspace%3Aconversational-extension": version: 0.1.10 - resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=f9bdfe&locator=%40janhq%2Fconversational-extension%40workspace%3Aconversational-extension" + resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=f15485&locator=%40janhq%2Fconversational-extension%40workspace%3Aconversational-extension" dependencies: rxjs: "npm:^7.8.1" ulidx: "npm:^2.3.0" - checksum: 10c0/417ea9bd3e5b53264596d2ee816c3e24299f8b721f6ea951d078342555da457ebca4d5b1e116bf187ac77ec0a9e3341211d464f4ffdbd2a3915139523688d41d + checksum: 10c0/257621cb56db31a4dd3a2b509ec4c61217022e74bbd39cf6a1a172073654b9a65eee94ef9c1b4d4f5d2231d159c8818cb02846f3d88fe14f102f43169ad3737c languageName: node linkType: hard "@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fdownload-extension%40workspace%3Adownload-extension": version: 0.1.10 - resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=f9bdfe&locator=%40janhq%2Fdownload-extension%40workspace%3Adownload-extension" + resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=f15485&locator=%40janhq%2Fdownload-extension%40workspace%3Adownload-extension" dependencies: rxjs: "npm:^7.8.1" ulidx: "npm:^2.3.0" - checksum: 10c0/417ea9bd3e5b53264596d2ee816c3e24299f8b721f6ea951d078342555da457ebca4d5b1e116bf187ac77ec0a9e3341211d464f4ffdbd2a3915139523688d41d + checksum: 10c0/257621cb56db31a4dd3a2b509ec4c61217022e74bbd39cf6a1a172073654b9a65eee94ef9c1b4d4f5d2231d159c8818cb02846f3d88fe14f102f43169ad3737c languageName: node linkType: hard "@janhq/core@file:../../core/package.tgz::locator=%40janhq%2Fllamacpp-extension%40workspace%3Allamacpp-extension": version: 0.1.10 - resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=f9bdfe&locator=%40janhq%2Fllamacpp-extension%40workspace%3Allamacpp-extension" + resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=f15485&locator=%40janhq%2Fllamacpp-extension%40workspace%3Allamacpp-extension" dependencies: rxjs: "npm:^7.8.1" ulidx: "npm:^2.3.0" - checksum: 10c0/417ea9bd3e5b53264596d2ee816c3e24299f8b721f6ea951d078342555da457ebca4d5b1e116bf187ac77ec0a9e3341211d464f4ffdbd2a3915139523688d41d + checksum: 10c0/257621cb56db31a4dd3a2b509ec4c61217022e74bbd39cf6a1a172073654b9a65eee94ef9c1b4d4f5d2231d159c8818cb02846f3d88fe14f102f43169ad3737c languageName: node linkType: hard diff --git a/package.json b/package.json index fe5fdff21..cf3767e66 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,8 @@ "scripts": { "lint": "yarn workspace @janhq/web-app lint", "dev": "yarn dev:tauri", + "ios": "yarn tauri ios dev", + "android": "yarn tauri android dev", "build": "yarn build:web && yarn build:tauri", "test": "vitest run", "test:watch": "vitest", @@ -24,7 +26,14 @@ "serve:web-app": "yarn workspace @janhq/web-app serve:web", "build:serve:web-app": "yarn build:web-app && yarn serve:web-app", "dev:tauri": "yarn build:icon && yarn copy:assets:tauri && cross-env IS_CLEAN=true tauri dev", + "dev:ios": "yarn build:extensions-web && yarn copy:assets:mobile && RUSTC_WRAPPER= yarn tauri ios dev --features mobile", + "dev:android": "yarn build:extensions-web && yarn copy:assets:mobile && cross-env IS_CLEAN=true TAURI_ANDROID_BUILD=true yarn tauri android dev --features mobile", + "build:android": "yarn build:icon && yarn copy:assets:mobile && cross-env IS_CLEAN=true TAURI_ANDROID_BUILD=true yarn tauri android build -- --no-default-features --features mobile", + "build:ios": "yarn copy:assets:mobile && yarn tauri ios build -- --no-default-features --features mobile", + "build:ios:device": "yarn build:icon && yarn copy:assets:mobile && yarn tauri ios build -- --no-default-features --features mobile --export-method debugging", "copy:assets:tauri": "cpx \"pre-install/*.tgz\" \"src-tauri/resources/pre-install/\" && cpx \"LICENSE\" \"src-tauri/resources/\"", + "copy:assets:mobile": "cpx \"pre-install/*.tgz\" \"src-tauri/resources/pre-install/\" && cpx \"LICENSE\" \"src-tauri/resources/\"", + "download:lib": "node ./scripts/download-lib.mjs", "download:bin": "node ./scripts/download-bin.mjs", "download:windows-installer": "node ./scripts/download-win-installer-deps.mjs", "build:tauri:win32": "yarn download:bin && yarn download:windows-installer && yarn tauri build", @@ -57,7 +66,9 @@ "hoistingLimits": "workspaces" }, "resolutions": { - "yallist": "4.0.0" + "yallist": "4.0.0", + "@types/react": "19.1.2", + "@types/react-dom": "19.1.2" }, "packageManager": "yarn@4.5.3" } diff --git a/src-tauri/.cargo/config.toml b/src-tauri/.cargo/config.toml index 830adb1f1..9089e8115 100644 --- a/src-tauri/.cargo/config.toml +++ b/src-tauri/.cargo/config.toml @@ -3,3 +3,20 @@ # see https://github.com/tauri-apps/tauri/pull/4383#issuecomment-1212221864 __TAURI_WORKSPACE__ = "true" ENABLE_SYSTEM_TRAY_ICON = "false" + +[target.aarch64-linux-android] +linker = "aarch64-linux-android21-clang" +ar = "llvm-ar" +rustflags = ["-C", "link-arg=-fuse-ld=lld"] + +[target.armv7-linux-androideabi] +linker = "armv7a-linux-androideabi21-clang" +ar = "llvm-ar" + +[target.x86_64-linux-android] +linker = "x86_64-linux-android21-clang" +ar = "llvm-ar" + +[target.i686-linux-android] +linker = "i686-linux-android21-clang" +ar = "llvm-ar" diff --git a/src-tauri/.gitignore b/src-tauri/.gitignore index 40726cbe0..02bc782bf 100644 --- a/src-tauri/.gitignore +++ b/src-tauri/.gitignore @@ -2,6 +2,7 @@ # will have compiled files and executables /target/ /gen/schemas +/gen/android binaries !binaries/download.sh !binaries/download.bat \ No newline at end of file diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index ca75cbd77..da2ca059e 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -85,6 +85,19 @@ dependencies = [ "version_check", ] +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "getrandom 0.3.3", + "once_cell", + "version_check", + "zerocopy", +] + [[package]] name = "aho-corasick" version = "1.1.3" @@ -149,11 +162,11 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" [[package]] name = "ash" -version = "0.38.0+1.3.281" +version = "0.37.3+1.3.251" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f" +checksum = "39e9c3835d686b0a6084ab4234fcd1b07dbf6e4767dce60874b12356a25ecd4a" dependencies = [ - "libloading 0.8.8", + "libloading 0.7.4", ] [[package]] @@ -166,7 +179,7 @@ dependencies = [ "futures-channel", "futures-util", "rand 0.9.2", - "raw-window-handle", + "raw-window-handle 0.6.2", "serde", "serde_repr", "tokio", @@ -510,6 +523,20 @@ name = "bytemuck" version = "1.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c76a5792e44e4abe34d3abf15636779261d45a7450612059293d1d2cfc63422" +dependencies = [ + "bytemuck_derive", +] + +[[package]] +name = "bytemuck_derive" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f154e572231cb6ba2bd1176980827e3d5dc04cc183a75dea38109fbdd672d29" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] [[package]] name = "byteorder" @@ -802,11 +829,22 @@ checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1" dependencies = [ "bitflags 2.9.1", "core-foundation 0.10.1", - "core-graphics-types", + "core-graphics-types 0.2.0", "foreign-types 0.5.0", "libc", ] +[[package]] +name = "core-graphics-types" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "libc", +] + [[package]] name = "core-graphics-types" version = "0.2.0" @@ -845,6 +883,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0f58bbc28f91df819d0aa2a2c00cd19754769c2fad90579b3592b1c9ba7a3115" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.21" @@ -1888,13 +1935,24 @@ dependencies = [ "tracing", ] +[[package]] +name = "half" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +dependencies = [ + "bytemuck", + "cfg-if", + "crunchy", +] + [[package]] name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" dependencies = [ - "ahash", + "ahash 0.7.8", ] [[package]] @@ -2619,6 +2677,15 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" +[[package]] +name = "malloc_buf" +version = "0.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb" +dependencies = [ + "libc", +] + [[package]] name = "markup5ever" version = "0.14.1" @@ -2747,7 +2814,7 @@ dependencies = [ "log", "ndk-sys", "num_enum", - "raw-window-handle", + "raw-window-handle 0.6.2", "thiserror 1.0.69", ] @@ -2869,6 +2936,15 @@ dependencies = [ "libloading 0.8.8", ] +[[package]] +name = "objc" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1" +dependencies = [ + "malloc_buf", +] + [[package]] name = "objc-sys" version = "0.3.5" @@ -3177,6 +3253,15 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" +[[package]] +name = "openssl-src" +version = "300.5.2+3.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d270b79e2926f5150189d475bc7e9d2c69f9c4697b185fa917d5a32b792d21b4" +dependencies = [ + "cc", +] + [[package]] name = "openssl-sys" version = "0.9.109" @@ -3185,6 +3270,7 @@ checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" dependencies = [ "cc", "libc", + "openssl-src", "pkg-config", "vcpkg", ] @@ -3900,6 +3986,12 @@ dependencies = [ "rand_core 0.5.1", ] +[[package]] +name = "raw-window-handle" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2ff9a1f06a88b01621b7ae906ef0211290d1c8a168a15542486a8f61c0833b9" + [[package]] name = "raw-window-handle" version = "0.6.2" @@ -4090,7 +4182,7 @@ dependencies = [ "objc2-app-kit", "objc2-core-foundation", "objc2-foundation 0.3.1", - "raw-window-handle", + "raw-window-handle 0.6.2", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", @@ -4759,7 +4851,7 @@ dependencies = [ "objc2 0.5.2", "objc2-foundation 0.2.2", "objc2-quartz-core 0.2.2", - "raw-window-handle", + "raw-window-handle 0.6.2", "redox_syscall", "wasm-bindgen", "web-sys", @@ -5028,7 +5120,7 @@ dependencies = [ "objc2-foundation 0.3.1", "once_cell", "parking_lot", - "raw-window-handle", + "raw-window-handle 0.6.2", "scopeguard", "tao-macros", "unicode-segmentation", @@ -5103,7 +5195,7 @@ dependencies = [ "objc2-web-kit", "percent-encoding", "plist", - "raw-window-handle", + "raw-window-handle 0.6.2", "reqwest 0.12.22", "serde", "serde_json", @@ -5233,7 +5325,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37e5858cc7b455a73ab4ea2ebc08b5be33682c00ff1bf4cad5537d4fb62499d9" dependencies = [ "log", - "raw-window-handle", + "raw-window-handle 0.6.2", "rfd", "serde", "serde_json", @@ -5280,6 +5372,7 @@ dependencies = [ "sysinfo", "tauri", "tauri-plugin", + "vulkano", ] [[package]] @@ -5489,7 +5582,7 @@ dependencies = [ "objc2 0.6.1", "objc2-ui-kit", "objc2-web-kit", - "raw-window-handle", + "raw-window-handle 0.6.2", "serde", "serde_json", "tauri-utils", @@ -5515,7 +5608,7 @@ dependencies = [ "objc2-foundation 0.3.1", "once_cell", "percent-encoding", - "raw-window-handle", + "raw-window-handle 0.6.2", "softbuffer", "tao", "tauri-runtime", @@ -5639,6 +5732,15 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + [[package]] name = "time" version = "0.3.41" @@ -6154,6 +6256,15 @@ version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +[[package]] +name = "vk-parse" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81086c28be67a8759cd80cbb3c8f7b520e0874605fc5eb74d5a1c9c2d1878e79" +dependencies = [ + "xml-rs", +] + [[package]] name = "vswhom" version = "0.1.0" @@ -6183,6 +6294,48 @@ dependencies = [ "memchr", ] +[[package]] +name = "vulkano" +version = "0.34.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a26f2897a92a30931fceef3d6d1156a1089d9681fb2be73be92bbf24ae2ddf2" +dependencies = [ + "ahash 0.8.12", + "ash", + "bytemuck", + "core-graphics-types 0.1.3", + "crossbeam-queue", + "half", + "heck 0.4.1", + "indexmap 2.10.0", + "libloading 0.8.8", + "objc", + "once_cell", + "parking_lot", + "proc-macro2", + "quote", + "raw-window-handle 0.5.2", + "regex", + "serde", + "serde_json", + "smallvec", + "thread_local", + "vk-parse", + "vulkano-macros", +] + +[[package]] +name = "vulkano-macros" +version = "0.34.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "52be622d364272fd77e298e7f68e8547ae66e7687cb86eb85335412cee7e3965" +dependencies = [ + "proc-macro-crate 1.3.1", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "walkdir" version = "2.5.0" @@ -6517,7 +6670,7 @@ dependencies = [ "objc2-app-kit", "objc2-core-foundation", "objc2-foundation 0.3.1", - "raw-window-handle", + "raw-window-handle 0.6.2", "windows-sys 0.59.0", "windows-version", ] @@ -7089,7 +7242,7 @@ dependencies = [ "objc2-web-kit", "once_cell", "percent-encoding", - "raw-window-handle", + "raw-window-handle 0.6.2", "sha2", "soup3", "tao-macros", @@ -7144,6 +7297,12 @@ dependencies = [ "rustix", ] +[[package]] +name = "xml-rs" +version = "0.8.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fd8403733700263c6eb89f192880191f1b83e332f7a20371ddcf421c4a337c7" + [[package]] name = "yoke" version = "0.8.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 51e84f880..43738b032 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -22,7 +22,19 @@ default = [ "tauri/macos-private-api", "tauri/tray-icon", "tauri/test", - "tauri/custom-protocol" + "tauri/custom-protocol", + "desktop", +] +hardware = ["dep:tauri-plugin-hardware"] +deep-link = ["dep:tauri-plugin-deep-link"] +desktop = [ + "deep-link", + "hardware" +] +mobile = [ + "tauri/protocol-asset", + "tauri/test", + "tauri/wry", ] test-tauri = [ "tauri/wry", @@ -31,6 +43,7 @@ test-tauri = [ "tauri/macos-private-api", "tauri/tray-icon", "tauri/test", + "desktop", ] [build-dependencies] @@ -46,7 +59,7 @@ hyper = { version = "0.14", features = ["server"] } jan-utils = { path = "./utils" } libloading = "0.8.7" log = "0.4" -reqwest = { version = "0.11", features = ["json", "blocking", "stream"] } +reqwest = { version = "0.11", features = ["json", "blocking", "stream", "native-tls-vendored"] } rmcp = { version = "0.6.0", features = [ "client", "transport-sse-client", @@ -60,11 +73,11 @@ serde_json = "1.0" serde_yaml = "0.9.34" tar = "0.4" zip = "0.6" -tauri-plugin-deep-link = { version = "2.3.4" } tauri-plugin-dialog = "2.2.1" -tauri-plugin-hardware = { path = "./plugins/tauri-plugin-hardware" } -tauri-plugin-http = { version = "2", features = ["unsafe-headers"] } +tauri-plugin-deep-link = { version = "2", optional = true } +tauri-plugin-hardware = { path = "./plugins/tauri-plugin-hardware", optional = true } tauri-plugin-llamacpp = { path = "./plugins/tauri-plugin-llamacpp" } +tauri-plugin-http = { version = "2", features = ["unsafe-headers"] } tauri-plugin-log = "2.0.0-rc" tauri-plugin-opener = "2.2.7" tauri-plugin-os = "2.2.1" @@ -94,4 +107,26 @@ windows-sys = { version = "0.60.2", features = ["Win32_Storage_FileSystem"] } [target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] tauri-plugin-updater = "2" once_cell = "1.18" -tauri-plugin-single-instance = { version = "2.3.4", features = ["deep-link"] } +tauri-plugin-single-instance = { version = "2", features = ["deep-link"] } + +[target.'cfg(any(target_os = "android", target_os = "ios"))'.dependencies] +tauri-plugin-dialog = { version = "2.2.1", default-features = false } +tauri-plugin-http = { version = "2", default-features = false } +tauri-plugin-log = { version = "2.0.0-rc", default-features = false } +tauri-plugin-opener = { version = "2.2.7", default-features = false } +tauri-plugin-os = { version = "2.2.1", default-features = false } +tauri-plugin-shell = { version = "2.2.0", default-features = false } +tauri-plugin-store = { version = "2", default-features = false } + +# Release profile optimizations for minimal binary size +[profile.release] +opt-level = "z" # Optimize for size +lto = "fat" # Aggressive Link Time Optimization +strip = "symbols" # Strip debug symbols for smaller binary +codegen-units = 1 # Reduce parallel codegen for better optimization +panic = "abort" # Don't unwind on panic, saves space +overflow-checks = false # Disable overflow checks for size +debug = false # No debug info +debug-assertions = false # No debug assertions +incremental = false # Disable incremental compilation for release +rpath = false # Don't include rpath diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json index e594bf023..5c5e7d48d 100644 --- a/src-tauri/capabilities/default.json +++ b/src-tauri/capabilities/default.json @@ -18,11 +18,10 @@ "os:default", "opener:default", "log:default", - "updater:default", "dialog:default", - "deep-link:default", "core:webview:allow-create-webview-window", "opener:allow-open-url", + "store:default", { "identifier": "http:default", "allow": [ @@ -54,9 +53,6 @@ "url": "http://0.0.0.0:*" } ] - }, - "store:default", - "llamacpp:default", - "hardware:default" + } ] } diff --git a/src-tauri/capabilities/desktop.json b/src-tauri/capabilities/desktop.json new file mode 100644 index 000000000..41be646d3 --- /dev/null +++ b/src-tauri/capabilities/desktop.json @@ -0,0 +1,63 @@ +{ + "$schema": "../gen/schemas/desktop-schema.json", + "identifier": "desktop", + "description": "enables the default permissions for desktop platforms", + "windows": ["main"], + "remote": { + "urls": ["http://*"] + }, + "platforms": ["linux", "macOS", "windows"], + "permissions": [ + "core:default", + "core:webview:allow-set-webview-zoom", + "core:window:allow-start-dragging", + "core:window:allow-set-theme", + "shell:allow-spawn", + "shell:allow-open", + "core:app:allow-set-app-theme", + "core:window:allow-set-focus", + "os:default", + "opener:default", + "log:default", + "dialog:default", + "core:webview:allow-create-webview-window", + "opener:allow-open-url", + "store:default", + "llamacpp:default", + "deep-link:default", + "hardware:default", + + { + "identifier": "http:default", + "allow": [ + { + "url": "https://*:*" + }, + { + "url": "http://*:*" + } + ], + "deny": [] + }, + { + "identifier": "shell:allow-execute", + "allow": [] + }, + { + "identifier": "opener:allow-open-url", + "description": "opens the default permissions for the core module", + "windows": ["*"], + "allow": [ + { + "url": "https://*" + }, + { + "url": "http://127.0.0.1:*" + }, + { + "url": "http://0.0.0.0:*" + } + ] + } + ] +} \ No newline at end of file diff --git a/src-tauri/capabilities/mobile.json b/src-tauri/capabilities/mobile.json new file mode 100644 index 000000000..fdbda476a --- /dev/null +++ b/src-tauri/capabilities/mobile.json @@ -0,0 +1,58 @@ +{ + "$schema": "../gen/schemas/mobile-schema.json", + "identifier": "mobile", + "description": "enables the default permissions for mobile platforms", + "windows": ["main"], + "remote": { + "urls": ["http://*"] + }, + "permissions": [ + "core:default", + "core:webview:allow-set-webview-zoom", + "core:window:allow-start-dragging", + "core:window:allow-set-theme", + "shell:allow-spawn", + "shell:allow-open", + "core:app:allow-set-app-theme", + "core:window:allow-set-focus", + "os:default", + "opener:default", + "log:default", + "dialog:default", + "core:webview:allow-create-webview-window", + "opener:allow-open-url", + "store:default", + { + "identifier": "http:default", + "allow": [ + { + "url": "https://*:*" + }, + { + "url": "http://*:*" + } + ], + "deny": [] + }, + { + "identifier": "shell:allow-execute", + "allow": [] + }, + { + "identifier": "opener:allow-open-url", + "description": "opens the default permissions for the core module", + "windows": ["*"], + "allow": [ + { + "url": "https://*" + }, + { + "url": "http://127.0.0.1:*" + }, + { + "url": "http://0.0.0.0:*" + } + ] + } + ] +} \ No newline at end of file diff --git a/src-tauri/capabilities/system-monitor-window.json b/src-tauri/capabilities/system-monitor-window.json index 740bb82cc..68a75e9fb 100644 --- a/src-tauri/capabilities/system-monitor-window.json +++ b/src-tauri/capabilities/system-monitor-window.json @@ -3,6 +3,7 @@ "identifier": "system-monitor-window", "description": "enables permissions for the system monitor window", "windows": ["system-monitor-window"], + "platforms": ["linux", "macOS", "windows"], "permissions": [ "core:default", "core:window:allow-start-dragging", diff --git a/src-tauri/gen/android/app/src/main/assets/resources/LICENSE b/src-tauri/gen/android/app/src/main/assets/resources/LICENSE new file mode 100644 index 000000000..d614b967f --- /dev/null +++ b/src-tauri/gen/android/app/src/main/assets/resources/LICENSE @@ -0,0 +1,19 @@ +Jan + +Copyright 2025 Menlo Research + +This product includes software developed by Menlo Research (https://menlo.ai). + +Licensed under the Apache License, Version 2.0 (the "License"); +You may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Attribution is requested in user-facing documentation and materials, where appropriate. \ No newline at end of file diff --git a/src-tauri/plugins/tauri-plugin-hardware/Cargo.toml b/src-tauri/plugins/tauri-plugin-hardware/Cargo.toml index 7475fb353..5e6f983fc 100644 --- a/src-tauri/plugins/tauri-plugin-hardware/Cargo.toml +++ b/src-tauri/plugins/tauri-plugin-hardware/Cargo.toml @@ -11,15 +11,19 @@ exclude = ["/examples", "/dist-js", "/guest-js", "/node_modules"] links = "tauri-plugin-hardware" [dependencies] -vulkano = "0.34" libc = "0.2" log = "0.4" -nvml-wrapper = "0.10.0" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" sysinfo = "0.34.2" tauri = { version = "2.5.0", default-features = false, features = ["test"] } +# Desktop-only dependencies +[target.'cfg(not(any(target_os = "android", target_os = "ios")))'.dependencies] +vulkano = "0.34" +ash = "0.37" +nvml-wrapper = "0.10.0" + # Windows-specific dependencies [target.'cfg(windows)'.dependencies] libloading = "0.8" diff --git a/src-tauri/plugins/tauri-plugin-hardware/src/vendor/nvidia.rs b/src-tauri/plugins/tauri-plugin-hardware/src/vendor/nvidia.rs index 006ca66ba..083c0fdae 100644 --- a/src-tauri/plugins/tauri-plugin-hardware/src/vendor/nvidia.rs +++ b/src-tauri/plugins/tauri-plugin-hardware/src/vendor/nvidia.rs @@ -1,7 +1,13 @@ -use crate::types::{GpuInfo, GpuUsage, Vendor}; -use nvml_wrapper::{error::NvmlError, Nvml}; -use std::sync::OnceLock; +use crate::types::{GpuInfo, GpuUsage}; +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use { + crate::types::Vendor, + nvml_wrapper::{error::NvmlError, Nvml}, + std::sync::OnceLock, +}; + +#[cfg(not(any(target_os = "android", target_os = "ios")))] static NVML: OnceLock> = OnceLock::new(); #[derive(Debug, Clone, serde::Serialize)] @@ -10,11 +16,13 @@ pub struct NvidiaInfo { pub compute_capability: String, } +#[cfg(not(any(target_os = "android", target_os = "ios")))] fn get_nvml() -> Option<&'static Nvml> { NVML.get_or_init(|| { + // Try to initialize NVML, with fallback for Linux let result = Nvml::init().or_else(|e| { - // fallback if cfg!(target_os = "linux") { + log::debug!("NVML init failed, trying Linux fallback: {}", e); let lib_path = std::ffi::OsStr::new("libnvidia-ml.so.1"); Nvml::builder().lib_path(lib_path).init() } else { @@ -22,11 +30,13 @@ fn get_nvml() -> Option<&'static Nvml> { } }); - // NvmlError doesn't implement Copy, so we have to store an Option in OnceLock match result { - Ok(nvml) => Some(nvml), + Ok(nvml) => { + log::debug!("NVML initialized successfully"); + Some(nvml) + } Err(e) => { - log::error!("Unable to initialize NVML: {}", e); + log::debug!("Unable to initialize NVML: {}", e); None } } @@ -36,70 +46,111 @@ fn get_nvml() -> Option<&'static Nvml> { impl GpuInfo { pub fn get_usage_nvidia(&self) -> GpuUsage { - let index = match self.nvidia_info { - Some(ref nvidia_info) => nvidia_info.index, - None => { - log::error!("get_usage_nvidia() called on non-NVIDIA GPU"); - return self.get_usage_unsupported(); - } - }; - let closure = || -> Result { - let nvml = get_nvml().ok_or(NvmlError::Unknown)?; - let device = nvml.device_by_index(index)?; - let mem_info = device.memory_info()?; - Ok(GpuUsage { - uuid: self.uuid.clone(), - used_memory: mem_info.used / 1024 / 1024, // bytes to MiB - total_memory: mem_info.total / 1024 / 1024, // bytes to MiB - }) - }; - closure().unwrap_or_else(|e| { - log::error!("Failed to get memory usage for NVIDIA GPU {}: {}", index, e); - self.get_usage_unsupported() + #[cfg(any(target_os = "android", target_os = "ios"))] + { + log::warn!("NVIDIA GPU usage detection is not supported on mobile platforms"); + return self.get_usage_unsupported(); + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + let index = match &self.nvidia_info { + Some(nvidia_info) => nvidia_info.index, + None => { + log::error!("get_usage_nvidia() called on non-NVIDIA GPU"); + return self.get_usage_unsupported(); + } + }; + + self.get_nvidia_memory_usage(index) + .unwrap_or_else(|e| { + log::error!("Failed to get memory usage for NVIDIA GPU {}: {}", index, e); + self.get_usage_unsupported() + }) + } + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + fn get_nvidia_memory_usage(&self, index: u32) -> Result { + let nvml = get_nvml().ok_or(NvmlError::Unknown)?; + let device = nvml.device_by_index(index)?; + let mem_info = device.memory_info()?; + + Ok(GpuUsage { + uuid: self.uuid.clone(), + used_memory: mem_info.used / (1024 * 1024), // bytes to MiB + total_memory: mem_info.total / (1024 * 1024), // bytes to MiB }) } } pub fn get_nvidia_gpus() -> Vec { - let closure = || -> Result, NvmlError> { - let nvml = get_nvml().ok_or(NvmlError::Unknown)?; - let num_gpus = nvml.device_count()?; - let driver_version = nvml.sys_driver_version()?; + #[cfg(any(target_os = "android", target_os = "ios"))] + { + // On mobile platforms, NVIDIA GPU detection is not supported + log::info!("NVIDIA GPU detection is not supported on mobile platforms"); + vec![] + } - let mut gpus = Vec::with_capacity(num_gpus as usize); - for i in 0..num_gpus { - let device = nvml.device_by_index(i)?; - gpus.push(GpuInfo { - name: device.name()?, - total_memory: device.memory_info()?.total / 1024 / 1024, // bytes to MiB - vendor: Vendor::NVIDIA, - uuid: { - let mut uuid = device.uuid()?; - if uuid.starts_with("GPU-") { - uuid = uuid[4..].to_string(); - } - uuid - }, - driver_version: driver_version.clone(), - nvidia_info: Some(NvidiaInfo { - index: i, - compute_capability: { - let cc = device.cuda_compute_capability()?; - format!("{}.{}", cc.major, cc.minor) - }, - }), - vulkan_info: None, - }); - } - - Ok(gpus) - }; - - match closure() { - Ok(gpus) => gpus, - Err(e) => { - log::error!("Failed to get NVIDIA GPUs: {}", e); - vec![] - } + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + get_nvidia_gpus_internal() } } + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +fn get_nvidia_gpus_internal() -> Vec { + let nvml = match get_nvml() { + Some(nvml) => nvml, + None => { + log::debug!("NVML not available"); + return vec![]; + } + }; + + let (num_gpus, driver_version) = match (nvml.device_count(), nvml.sys_driver_version()) { + (Ok(count), Ok(version)) => (count, version), + (Err(e), _) | (_, Err(e)) => { + log::error!("Failed to get NVIDIA system info: {}", e); + return vec![]; + } + }; + + let mut gpus = Vec::with_capacity(num_gpus as usize); + + for i in 0..num_gpus { + match create_gpu_info(nvml, i, &driver_version) { + Ok(gpu_info) => gpus.push(gpu_info), + Err(e) => log::warn!("Failed to get info for NVIDIA GPU {}: {}", i, e), + } + } + + gpus +} + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +fn create_gpu_info(nvml: &Nvml, index: u32, driver_version: &str) -> Result { + let device = nvml.device_by_index(index)?; + let memory_info = device.memory_info()?; + let compute_capability = device.cuda_compute_capability()?; + + let uuid = device.uuid()?; + let clean_uuid = if uuid.starts_with("GPU-") { + uuid[4..].to_string() + } else { + uuid + }; + + Ok(GpuInfo { + name: device.name()?, + total_memory: memory_info.total / (1024 * 1024), // bytes to MiB + vendor: Vendor::NVIDIA, + uuid: clean_uuid, + driver_version: driver_version.to_string(), + nvidia_info: Some(NvidiaInfo { + index, + compute_capability: format!("{}.{}", compute_capability.major, compute_capability.minor), + }), + vulkan_info: None, + }) +} diff --git a/src-tauri/plugins/tauri-plugin-hardware/src/vendor/tests.rs b/src-tauri/plugins/tauri-plugin-hardware/src/vendor/tests.rs index ad48a6fad..d683e4d91 100644 --- a/src-tauri/plugins/tauri-plugin-hardware/src/vendor/tests.rs +++ b/src-tauri/plugins/tauri-plugin-hardware/src/vendor/tests.rs @@ -19,3 +19,115 @@ fn test_get_vulkan_gpus() { println!(" {:?}", gpu.get_usage()); } } + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +#[test] +fn test_get_vulkan_gpus_on_desktop() { + let gpus = vulkan::get_vulkan_gpus(); + + // Test that function returns without panicking on desktop platforms + assert!(gpus.len() >= 0); + + // If GPUs are found, verify they have valid properties + for (i, gpu) in gpus.iter().enumerate() { + println!("Desktop GPU {}:", i); + println!(" Name: {}", gpu.name); + println!(" Vendor: {:?}", gpu.vendor); + println!(" Total Memory: {} MB", gpu.total_memory); + println!(" UUID: {}", gpu.uuid); + println!(" Driver Version: {}", gpu.driver_version); + + // Verify that GPU properties are not empty/default values + assert!(!gpu.name.is_empty(), "GPU name should not be empty"); + assert!(!gpu.uuid.is_empty(), "GPU UUID should not be empty"); + + // Test vulkan-specific info is present + if let Some(vulkan_info) = &gpu.vulkan_info { + println!(" Vulkan API Version: {}", vulkan_info.api_version); + println!(" Device Type: {}", vulkan_info.device_type); + assert!(!vulkan_info.api_version.is_empty(), "Vulkan API version should not be empty"); + assert!(!vulkan_info.device_type.is_empty(), "Device type should not be empty"); + } + } +} + +#[cfg(target_os = "android")] +#[test] +fn test_get_vulkan_gpus_on_android() { + let gpus = vulkan::get_vulkan_gpus(); + + // Test that function returns without panicking on Android + assert!(gpus.len() >= 0); + + // Android-specific validation + for (i, gpu) in gpus.iter().enumerate() { + println!("Android GPU {}:", i); + println!(" Name: {}", gpu.name); + println!(" Vendor: {:?}", gpu.vendor); + println!(" Total Memory: {} MB", gpu.total_memory); + println!(" UUID: {}", gpu.uuid); + println!(" Driver Version: {}", gpu.driver_version); + + // Verify C string parsing works correctly with i8 on Android + assert!(!gpu.name.is_empty(), "GPU name should not be empty on Android"); + assert!(!gpu.uuid.is_empty(), "GPU UUID should not be empty on Android"); + + // Android devices should typically have Adreno, Mali, or PowerVR GPUs + // The name parsing should handle i8 char arrays correctly + assert!( + gpu.name.chars().all(|c| c.is_ascii() || c.is_ascii_control()), + "GPU name should contain valid characters when parsed from i8 array" + ); + + if let Some(vulkan_info) = &gpu.vulkan_info { + println!(" Vulkan API Version: {}", vulkan_info.api_version); + println!(" Device Type: {}", vulkan_info.device_type); + // Verify API version parsing works with Android's i8 char arrays + assert!( + vulkan_info.api_version.matches('.').count() >= 2, + "API version should be in format X.Y.Z" + ); + } + } +} + +#[cfg(target_os = "ios")] +#[test] +fn test_get_vulkan_gpus_on_ios() { + let gpus = vulkan::get_vulkan_gpus(); + + // Note: iOS doesn't support Vulkan natively, so this might return empty + // But the function should still work without crashing + assert!(gpus.len() >= 0); + + // iOS-specific validation (if any Vulkan implementation is available via MoltenVK) + for (i, gpu) in gpus.iter().enumerate() { + println!("iOS GPU {}:", i); + println!(" Name: {}", gpu.name); + println!(" Vendor: {:?}", gpu.vendor); + println!(" Total Memory: {} MB", gpu.total_memory); + println!(" UUID: {}", gpu.uuid); + println!(" Driver Version: {}", gpu.driver_version); + + // Verify C string parsing works correctly with i8 on iOS + assert!(!gpu.name.is_empty(), "GPU name should not be empty on iOS"); + assert!(!gpu.uuid.is_empty(), "GPU UUID should not be empty on iOS"); + + // iOS devices should typically have Apple GPU (if Vulkan is available via MoltenVK) + // The name parsing should handle i8 char arrays correctly + assert!( + gpu.name.chars().all(|c| c.is_ascii() || c.is_ascii_control()), + "GPU name should contain valid characters when parsed from i8 array" + ); + + if let Some(vulkan_info) = &gpu.vulkan_info { + println!(" Vulkan API Version: {}", vulkan_info.api_version); + println!(" Device Type: {}", vulkan_info.device_type); + // Verify API version parsing works with iOS's i8 char arrays + assert!( + vulkan_info.api_version.matches('.').count() >= 2, + "API version should be in format X.Y.Z" + ); + } + } +} diff --git a/src-tauri/plugins/tauri-plugin-hardware/src/vendor/vulkan.rs b/src-tauri/plugins/tauri-plugin-hardware/src/vendor/vulkan.rs index 91af52e5f..372e11037 100644 --- a/src-tauri/plugins/tauri-plugin-hardware/src/vendor/vulkan.rs +++ b/src-tauri/plugins/tauri-plugin-hardware/src/vendor/vulkan.rs @@ -1,8 +1,13 @@ -use crate::types::{GpuInfo, Vendor}; -use vulkano::device::physical::PhysicalDeviceType; -use vulkano::instance::{Instance, InstanceCreateInfo}; -use vulkano::memory::MemoryHeapFlags; -use vulkano::VulkanLibrary; +use crate::types::GpuInfo; + +#[cfg(not(any(target_os = "android", target_os = "ios")))] +use { + crate::types::Vendor, + vulkano::device::physical::PhysicalDeviceType, + vulkano::instance::{Instance, InstanceCreateInfo}, + vulkano::memory::MemoryHeapFlags, + vulkano::VulkanLibrary, +}; #[derive(Debug, Clone, serde::Serialize)] pub struct VulkanInfo { @@ -12,6 +17,7 @@ pub struct VulkanInfo { pub device_id: u32, } +#[cfg(not(any(target_os = "android", target_os = "ios")))] fn parse_uuid(bytes: &[u8; 16]) -> String { format!( "{:02x}{:02x}{:02x}{:02x}-\ @@ -39,15 +45,25 @@ fn parse_uuid(bytes: &[u8; 16]) -> String { } pub fn get_vulkan_gpus() -> Vec { - match get_vulkan_gpus_internal() { - Ok(gpus) => gpus, - Err(e) => { - log::error!("Failed to get Vulkan GPUs: {:?}", e); - vec![] + #[cfg(any(target_os = "android", target_os = "ios"))] + { + log::info!("Vulkan GPU detection is not supported on mobile platforms"); + vec![] + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + match get_vulkan_gpus_internal() { + Ok(gpus) => gpus, + Err(e) => { + log::error!("Failed to get Vulkan GPUs: {:?}", e); + vec![] + } } } } +#[cfg(not(any(target_os = "android", target_os = "ios")))] fn get_vulkan_gpus_internal() -> Result, Box> { let library = VulkanLibrary::new()?; diff --git a/src-tauri/resources/LICENSE b/src-tauri/resources/LICENSE new file mode 100644 index 000000000..d614b967f --- /dev/null +++ b/src-tauri/resources/LICENSE @@ -0,0 +1,19 @@ +Jan + +Copyright 2025 Menlo Research + +This product includes software developed by Menlo Research (https://menlo.ai). + +Licensed under the Apache License, Version 2.0 (the "License"); +You may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. + +Attribution is requested in user-facing documentation and materials, where appropriate. \ No newline at end of file diff --git a/src-tauri/src/core/app/commands.rs b/src-tauri/src/core/app/commands.rs index ba3e493b3..0d9c66c12 100644 --- a/src-tauri/src/core/app/commands.rs +++ b/src-tauri/src/core/app/commands.rs @@ -58,8 +58,8 @@ pub fn get_app_configurations(app_handle: tauri::AppHandle) -> Ap } #[tauri::command] -pub fn update_app_configuration( - app_handle: tauri::AppHandle, +pub fn update_app_configuration( + app_handle: tauri::AppHandle, configuration: AppConfiguration, ) -> Result<(), String> { let configuration_file = get_configuration_file_path(app_handle); @@ -155,13 +155,13 @@ pub fn default_data_folder_path(app_handle: tauri::AppHandle) -> } #[tauri::command] -pub fn get_user_home_path(app: AppHandle) -> String { +pub fn get_user_home_path(app: AppHandle) -> String { return get_app_configurations(app.clone()).data_folder; } #[tauri::command] -pub fn change_app_data_folder( - app_handle: tauri::AppHandle, +pub fn change_app_data_folder( + app_handle: tauri::AppHandle, new_data_folder: String, ) -> Result<(), String> { // Get current data folder path diff --git a/src-tauri/src/core/downloads/commands.rs b/src-tauri/src/core/downloads/commands.rs index f2187046a..6d50ed1a3 100644 --- a/src-tauri/src/core/downloads/commands.rs +++ b/src-tauri/src/core/downloads/commands.rs @@ -3,12 +3,12 @@ use super::models::DownloadItem; use crate::core::app::commands::get_jan_data_folder_path; use crate::core::state::AppState; use std::collections::HashMap; -use tauri::State; +use tauri::{Runtime, State}; use tokio_util::sync::CancellationToken; #[tauri::command] -pub async fn download_files( - app: tauri::AppHandle, +pub async fn download_files( + app: tauri::AppHandle, state: State<'_, AppState>, items: Vec, task_id: &str, diff --git a/src-tauri/src/core/downloads/helpers.rs b/src-tauri/src/core/downloads/helpers.rs index e46d18868..d3d8f6b7c 100644 --- a/src-tauri/src/core/downloads/helpers.rs +++ b/src-tauri/src/core/downloads/helpers.rs @@ -6,7 +6,7 @@ use reqwest::header::{HeaderMap, HeaderName, HeaderValue}; use std::collections::HashMap; use std::path::Path; use std::time::Duration; -use tauri::Emitter; +use tauri::{Emitter, Runtime}; use tokio::fs::File; use tokio::io::AsyncWriteExt; use tokio_util::sync::CancellationToken; @@ -25,7 +25,7 @@ pub fn err_to_string(e: E) -> String { async fn validate_downloaded_file( item: &DownloadItem, save_path: &Path, - app: &tauri::AppHandle, + app: &tauri::AppHandle, cancel_token: &CancellationToken, ) -> Result<(), String> { // Skip validation if no verification data is provided @@ -298,7 +298,7 @@ pub async fn _get_file_size( /// Downloads multiple files in parallel with individual progress tracking pub async fn _download_files_internal( - app: tauri::AppHandle, + app: tauri::AppHandle, items: &[DownloadItem], headers: &HashMap, task_id: &str, @@ -423,7 +423,7 @@ pub async fn _download_files_internal( /// Downloads a single file without blocking other downloads async fn download_single_file( - app: tauri::AppHandle, + app: tauri::AppHandle, item: &DownloadItem, header_map: &HeaderMap, save_path: &std::path::Path, diff --git a/src-tauri/src/core/extensions/commands.rs b/src-tauri/src/core/extensions/commands.rs index 784c71f46..4c5a44a53 100644 --- a/src-tauri/src/core/extensions/commands.rs +++ b/src-tauri/src/core/extensions/commands.rs @@ -1,24 +1,24 @@ use std::fs; use std::path::PathBuf; -use tauri::AppHandle; +use tauri::{AppHandle, Runtime}; use crate::core::app::commands::get_jan_data_folder_path; use crate::core::setup; #[tauri::command] -pub fn get_jan_extensions_path(app_handle: tauri::AppHandle) -> PathBuf { +pub fn get_jan_extensions_path(app_handle: tauri::AppHandle) -> PathBuf { get_jan_data_folder_path(app_handle).join("extensions") } #[tauri::command] -pub fn install_extensions(app: AppHandle) { +pub fn install_extensions(app: AppHandle) { if let Err(err) = setup::install_extensions(app, true) { log::error!("Failed to install extensions: {}", err); } } #[tauri::command] -pub fn get_active_extensions(app: AppHandle) -> Vec { +pub fn get_active_extensions(app: AppHandle) -> Vec { let mut path = get_jan_extensions_path(app); path.push("extensions.json"); log::info!("get jan extensions, path: {:?}", path); diff --git a/src-tauri/src/core/filesystem/commands.rs b/src-tauri/src/core/filesystem/commands.rs index a37cc00ec..fe44052b8 100644 --- a/src-tauri/src/core/filesystem/commands.rs +++ b/src-tauri/src/core/filesystem/commands.rs @@ -140,7 +140,7 @@ pub fn readdir_sync( #[tauri::command] pub fn write_yaml( - app: tauri::AppHandle, + app: tauri::AppHandle, data: serde_json::Value, save_path: &str, ) -> Result<(), String> { @@ -161,7 +161,7 @@ pub fn write_yaml( } #[tauri::command] -pub fn read_yaml(app: tauri::AppHandle, path: &str) -> Result { +pub fn read_yaml(app: tauri::AppHandle, path: &str) -> Result { let jan_data_folder = crate::core::app::commands::get_jan_data_folder_path(app.clone()); let path = jan_utils::normalize_path(&jan_data_folder.join(path)); if !path.starts_with(&jan_data_folder) { @@ -178,7 +178,7 @@ pub fn read_yaml(app: tauri::AppHandle, path: &str) -> Result Result<(), String> { +pub fn decompress(app: tauri::AppHandle, path: &str, output_dir: &str) -> Result<(), String> { let jan_data_folder = crate::core::app::commands::get_jan_data_folder_path(app.clone()); let path_buf = jan_utils::normalize_path(&jan_data_folder.join(path)); diff --git a/src-tauri/src/core/mcp/commands.rs b/src-tauri/src/core/mcp/commands.rs index 3bef12149..a86db598e 100644 --- a/src-tauri/src/core/mcp/commands.rs +++ b/src-tauri/src/core/mcp/commands.rs @@ -80,7 +80,7 @@ pub async fn deactivate_mcp_server(state: State<'_, AppState>, name: String) -> } #[tauri::command] -pub async fn restart_mcp_servers(app: AppHandle, state: State<'_, AppState>) -> Result<(), String> { +pub async fn restart_mcp_servers(app: AppHandle, state: State<'_, AppState>) -> Result<(), String> { let servers = state.mcp_servers.clone(); // Stop the servers stop_mcp_servers(state.mcp_servers.clone()).await?; @@ -119,7 +119,7 @@ pub async fn reset_mcp_restart_count( #[tauri::command] pub async fn get_connected_servers( - _app: AppHandle, + _app: AppHandle, state: State<'_, AppState>, ) -> Result, String> { let servers = state.mcp_servers.clone(); @@ -293,7 +293,7 @@ pub async fn cancel_tool_call( } #[tauri::command] -pub async fn get_mcp_configs(app: AppHandle) -> Result { +pub async fn get_mcp_configs(app: AppHandle) -> Result { let mut path = get_jan_data_folder_path(app); path.push("mcp_config.json"); @@ -308,7 +308,7 @@ pub async fn get_mcp_configs(app: AppHandle) -> Result { } #[tauri::command] -pub async fn save_mcp_configs(app: AppHandle, configs: String) -> Result<(), String> { +pub async fn save_mcp_configs(app: AppHandle, configs: String) -> Result<(), String> { let mut path = get_jan_data_folder_path(app); path.push("mcp_config.json"); log::info!("save mcp configs, path: {:?}", path); diff --git a/src-tauri/src/core/server/commands.rs b/src-tauri/src/core/server/commands.rs index 85450bee5..286d40cc1 100644 --- a/src-tauri/src/core/server/commands.rs +++ b/src-tauri/src/core/server/commands.rs @@ -14,12 +14,12 @@ pub async fn start_server( api_key: String, trusted_hosts: Vec, proxy_timeout: u64, -) -> Result { +) -> Result { let server_handle = state.server_handle.clone(); let plugin_state: State = app_handle.state(); let sessions = plugin_state.llama_server_process.clone(); - proxy::start_server( + let actual_port = proxy::start_server( server_handle, sessions, host, @@ -31,7 +31,7 @@ pub async fn start_server( ) .await .map_err(|e| e.to_string())?; - Ok(true) + Ok(actual_port) } #[tauri::command] diff --git a/src-tauri/src/core/server/proxy.rs b/src-tauri/src/core/server/proxy.rs index 4baf36503..12398ac02 100644 --- a/src-tauri/src/core/server/proxy.rs +++ b/src-tauri/src/core/server/proxy.rs @@ -715,7 +715,7 @@ pub async fn start_server( proxy_api_key: String, trusted_hosts: Vec>, proxy_timeout: u64, -) -> Result> { +) -> Result> { let mut handle_guard = server_handle.lock().await; if handle_guard.is_some() { return Err("Server is already running".into()); @@ -767,7 +767,9 @@ pub async fn start_server( }); *handle_guard = Some(server_task); - Ok(true) + let actual_port = addr.port(); + log::info!("Jan API server started successfully on port {}", actual_port); + Ok(actual_port) } pub async fn stop_server( diff --git a/src-tauri/src/core/setup.rs b/src-tauri/src/core/setup.rs index 68c7c44a1..38eca440e 100644 --- a/src-tauri/src/core/setup.rs +++ b/src-tauri/src/core/setup.rs @@ -6,10 +6,14 @@ use std::{ sync::Arc, }; use tar::Archive; +use tauri::{ + App, Emitter, Manager, Runtime, Wry +}; + +#[cfg(desktop)] use tauri::{ menu::{Menu, MenuItem, PredefinedMenuItem}, tray::{MouseButton, MouseButtonState, TrayIcon, TrayIconBuilder, TrayIconEvent}, - App, Emitter, Manager, Wry, }; use tauri_plugin_store::Store; @@ -19,7 +23,7 @@ use super::{ extensions::commands::get_jan_extensions_path, mcp::helpers::run_mcp_commands, state::AppState, }; -pub fn install_extensions(app: tauri::AppHandle, force: bool) -> Result<(), String> { +pub fn install_extensions(app: tauri::AppHandle, force: bool) -> Result<(), String> { let extensions_path = get_jan_extensions_path(app.clone()); let pre_install_path = app .path() @@ -202,10 +206,10 @@ pub fn extract_extension_manifest( Ok(None) } -pub fn setup_mcp(app: &App) { +pub fn setup_mcp(app: &App) { let state = app.state::(); let servers = state.mcp_servers.clone(); - let app_handle: tauri::AppHandle = app.handle().clone(); + let app_handle = app.handle().clone(); tauri::async_runtime::spawn(async move { if let Err(e) = run_mcp_commands(&app_handle, servers).await { log::error!("Failed to run mcp commands: {}", e); @@ -216,6 +220,7 @@ pub fn setup_mcp(app: &App) { }); } +#[cfg(desktop)] pub fn setup_tray(app: &App) -> tauri::Result { let show_i = MenuItem::with_id(app.handle(), "open", "Open Jan", true, None::<&str>)?; let quit_i = MenuItem::with_id(app.handle(), "quit", "Quit", true, None::<&str>)?; diff --git a/src-tauri/src/core/system/commands.rs b/src-tauri/src/core/system/commands.rs index a8b58d745..f5e9d7618 100644 --- a/src-tauri/src/core/system/commands.rs +++ b/src-tauri/src/core/system/commands.rs @@ -1,6 +1,6 @@ use std::fs; use std::path::PathBuf; -use tauri::{AppHandle, Manager, State}; +use tauri::{AppHandle, Manager, Runtime, State}; use tauri_plugin_llamacpp::cleanup_llama_processes; use crate::core::app::commands::{ @@ -11,13 +11,16 @@ use crate::core::mcp::helpers::clean_up_mcp_servers; use crate::core::state::AppState; #[tauri::command] -pub fn factory_reset(app_handle: tauri::AppHandle, state: State<'_, AppState>) { - // close window - let windows = app_handle.webview_windows(); - for (label, window) in windows.iter() { - window.close().unwrap_or_else(|_| { - log::warn!("Failed to close window: {:?}", label); - }); +pub fn factory_reset(app_handle: tauri::AppHandle, state: State<'_, AppState>) { + // close window (not available on mobile platforms) + #[cfg(not(any(target_os = "ios", target_os = "android")))] + { + let windows = app_handle.webview_windows(); + for (label, window) in windows.iter() { + window.close().unwrap_or_else(|_| { + log::warn!("Failed to close window: {:?}", label); + }); + } } let data_folder = get_jan_data_folder_path(app_handle.clone()); log::info!("Factory reset, removing data folder: {:?}", data_folder); @@ -46,12 +49,12 @@ pub fn factory_reset(app_handle: tauri::AppHandle, state: State<'_, AppState>) { } #[tauri::command] -pub fn relaunch(app: AppHandle) { +pub fn relaunch(app: AppHandle) { app.restart() } #[tauri::command] -pub fn open_app_directory(app: AppHandle) { +pub fn open_app_directory(app: AppHandle) { let app_path = app.path().app_data_dir().unwrap(); if cfg!(target_os = "windows") { std::process::Command::new("explorer") @@ -93,7 +96,7 @@ pub fn open_file_explorer(path: String) { } #[tauri::command] -pub async fn read_logs(app: AppHandle) -> Result { +pub async fn read_logs(app: AppHandle) -> Result { let log_path = get_jan_data_folder_path(app).join("logs").join("app.log"); if log_path.exists() { let content = fs::read_to_string(log_path).map_err(|e| e.to_string())?; diff --git a/src-tauri/src/core/threads/commands.rs b/src-tauri/src/core/threads/commands.rs index 7f16371a7..44ac1964d 100644 --- a/src-tauri/src/core/threads/commands.rs +++ b/src-tauri/src/core/threads/commands.rs @@ -127,7 +127,6 @@ pub async fn create_message( .ok_or("Missing thread_id")?; id.to_string() }; - ensure_thread_dir_exists(app_handle.clone(), &thread_id)?; let path = get_messages_path(app_handle.clone(), &thread_id); if message.get("id").is_none() { @@ -140,6 +139,9 @@ pub async fn create_message( let lock = get_lock_for_thread(&thread_id).await; let _guard = lock.lock().await; + // Ensure directory exists right before file operations to handle race conditions + ensure_thread_dir_exists(app_handle.clone(), &thread_id)?; + let mut file: File = fs::OpenOptions::new() .create(true) .append(true) diff --git a/src-tauri/src/core/threads/helpers.rs b/src-tauri/src/core/threads/helpers.rs index 0edcf41b2..76d2c2e59 100644 --- a/src-tauri/src/core/threads/helpers.rs +++ b/src-tauri/src/core/threads/helpers.rs @@ -3,7 +3,7 @@ use std::io::{BufRead, BufReader, Write}; use tauri::Runtime; // For async file write serialization -use once_cell::sync::Lazy; +use std::sync::OnceLock; use std::collections::HashMap; use std::sync::Arc; use tokio::sync::Mutex; @@ -11,12 +11,12 @@ use tokio::sync::Mutex; use super::utils::{get_messages_path, get_thread_metadata_path}; // Global per-thread locks for message file writes -pub static MESSAGE_LOCKS: Lazy>>>> = - Lazy::new(|| Mutex::new(HashMap::new())); +pub static MESSAGE_LOCKS: OnceLock>>>> = OnceLock::new(); /// Get a lock for a specific thread to ensure thread-safe message file operations pub async fn get_lock_for_thread(thread_id: &str) -> Arc> { - let mut locks = MESSAGE_LOCKS.lock().await; + let locks = MESSAGE_LOCKS.get_or_init(|| Mutex::new(HashMap::new())); + let mut locks = locks.lock().await; let lock = locks .entry(thread_id.to_string()) .or_insert_with(|| Arc::new(Mutex::new(()))) diff --git a/src-tauri/src/core/threads/tests.rs b/src-tauri/src/core/threads/tests.rs index 7a918e46f..8d3524d06 100644 --- a/src-tauri/src/core/threads/tests.rs +++ b/src-tauri/src/core/threads/tests.rs @@ -1,4 +1,3 @@ -use crate::core::app::commands::get_jan_data_folder_path; use super::commands::*; use serde_json::json; @@ -9,11 +8,18 @@ use tauri::test::{mock_app, MockRuntime}; // Helper to create a mock app handle with a temp data dir fn mock_app_with_temp_data_dir() -> (tauri::App, PathBuf) { let app = mock_app(); - let data_dir = get_jan_data_folder_path(app.handle().clone()); + // Create a unique test directory to avoid race conditions between parallel tests + let unique_id = std::thread::current().id(); + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos(); + let data_dir = std::env::current_dir() + .unwrap_or_else(|_| PathBuf::from(".")) + .join(format!("test-data-{:?}-{}", unique_id, timestamp)); println!("Mock app data dir: {}", data_dir.display()); - // Patch get_data_dir to use temp dir (requires get_data_dir to be overridable or injectable) - // For now, we assume get_data_dir uses tauri::api::path::app_data_dir(&app_handle) - // and that we can set the environment variable to redirect it. + // Ensure the unique test directory exists + let _ = fs::create_dir_all(&data_dir); (app, data_dir) } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index a2b263c6a..abd12ddb7 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -13,9 +13,7 @@ use tauri_plugin_llamacpp::cleanup_llama_processes; use tauri_plugin_store::StoreExt; use tokio::sync::Mutex; -use crate::core::setup::setup_tray; - -#[cfg_attr(mobile, tauri::mobile_entry_point)] +#[cfg_attr(all(mobile, any(target_os = "android", target_os = "ios")), tauri::mobile_entry_point)] pub fn run() { let mut builder = tauri::Builder::default(); #[cfg(desktop)] @@ -23,29 +21,29 @@ pub fn run() { builder = builder.plugin(tauri_plugin_single_instance::init(|_app, argv, _cwd| { println!("a new app instance was opened with {argv:?} and the deep link event was already triggered"); // when defining deep link schemes at runtime, you must also check `argv` here - let arg = argv.iter().find(|arg| arg.starts_with("jan://")); - if let Some(deep_link) = arg { - println!("deep link: {deep_link}"); - // handle the deep link, e.g., emit an event to the webview - _app.app_handle().emit("deep-link", deep_link).unwrap(); - if let Some(window) = _app.app_handle().get_webview_window("main") { - let _ = window.set_focus(); - } - } })); } - let app = builder + let mut app_builder = builder .plugin(tauri_plugin_os::init()) - .plugin(tauri_plugin_deep_link::init()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_http::init()) .plugin(tauri_plugin_store::Builder::new().build()) - .plugin(tauri_plugin_updater::Builder::new().build()) .plugin(tauri_plugin_shell::init()) - .plugin(tauri_plugin_llamacpp::init()) - .plugin(tauri_plugin_hardware::init()) + .plugin(tauri_plugin_llamacpp::init()); + + #[cfg(feature = "deep-link")] + { + app_builder = app_builder.plugin(tauri_plugin_deep_link::init()); + } + + #[cfg(not(any(target_os = "android", target_os = "ios")))] + { + app_builder = app_builder.plugin(tauri_plugin_hardware::init()); + } + + let app = app_builder .invoke_handler(tauri::generate_handler![ // FS commands - Deperecate soon core::filesystem::commands::join_path, @@ -121,21 +119,6 @@ pub fn run() { server_handle: Arc::new(Mutex::new(None)), tool_call_cancellations: Arc::new(Mutex::new(HashMap::new())), }) - .on_window_event(|window, event| match event { - tauri::WindowEvent::CloseRequested { api, .. } => { - if option_env!("ENABLE_SYSTEM_TRAY_ICON").unwrap_or("false") == "true" { - #[cfg(target_os = "macos")] - window - .app_handle() - .set_activation_policy(tauri::ActivationPolicy::Accessory) - .unwrap(); - - window.hide().unwrap(); - api.prevent_close(); - } - } - _ => {} - }) .setup(|app| { app.handle().plugin( tauri_plugin_log::Builder::default() @@ -150,8 +133,8 @@ pub fn run() { ]) .build(), )?; - app.handle() - .plugin(tauri_plugin_updater::Builder::new().build())?; + #[cfg(not(any(target_os = "ios", target_os = "android")))] + app.handle().plugin(tauri_plugin_updater::Builder::new().build())?; // Start migration let mut store_path = get_jan_data_folder_path(app.handle().clone()); @@ -185,16 +168,16 @@ pub fn run() { store.set("version", serde_json::json!(app_version)); store.save().expect("Failed to save store"); // Migration completed - + + #[cfg(desktop)] if option_env!("ENABLE_SYSTEM_TRAY_ICON").unwrap_or("false") == "true" { log::info!("Enabling system tray icon"); - let _ = setup_tray(app); + let _ = setup::setup_tray(app); } - #[cfg(any(windows, target_os = "linux"))] + #[cfg(all(feature = "deep-link", any(windows, target_os = "linux")))] { use tauri_plugin_deep_link::DeepLinkExt; - app.deep_link().register_all()?; } setup_mcp(app); @@ -209,12 +192,15 @@ pub fn run() { // This is called when the app is actually exiting (e.g., macOS dock quit) // We can't prevent this, so run cleanup quickly let app_handle = app.clone(); - // Hide window immediately - if let Some(window) = app_handle.get_webview_window("main") { - let _ = window.hide(); - } tokio::task::block_in_place(|| { tauri::async_runtime::block_on(async { + // Hide window immediately (not available on mobile platforms) + if let Some(window) = app_handle.get_webview_window("main") { + #[cfg(not(any(target_os = "ios", target_os = "android")))] + { let _ = window.hide(); } + let _ = window.emit("kill-mcp-servers", ()); + } + // Quick cleanup with shorter timeout let state = app_handle.state::(); let _ = clean_up_mcp_servers(state).await; diff --git a/src-tauri/tauri b/src-tauri/tauri new file mode 100755 index 000000000..f944754d6 --- /dev/null +++ b/src-tauri/tauri @@ -0,0 +1,2 @@ +#!/usr/bin/env node +import('../node_modules/@tauri-apps/cli/tauri.js'); \ No newline at end of file diff --git a/src-tauri/tauri.android.conf.json b/src-tauri/tauri.android.conf.json new file mode 100644 index 000000000..a0b795207 --- /dev/null +++ b/src-tauri/tauri.android.conf.json @@ -0,0 +1,20 @@ +{ + "identifier": "jan.ai.app", + "build": { + "devUrl": null, + "frontendDist": "../web-app/dist" + }, + "app": { + "security": { + "capabilities": ["mobile"] + } + }, + "plugins": {}, + "bundle": { + "resources": ["resources/LICENSE"], + "externalBin": [], + "android": { + "minSdkVersion": 24 + } + } +} \ No newline at end of file diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 10f4325e8..b0df3fc2f 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -40,6 +40,7 @@ } ], "security": { + "capabilities": ["default"], "csp": { "default-src": "'self' customprotocol: asset: http://localhost:* http://127.0.0.1:* ws://localhost:* ws://127.0.0.1:*", "connect-src": "ipc: http://ipc.localhost http://127.0.0.1:* ws://localhost:* ws://127.0.0.1:* https: http:", @@ -72,8 +73,7 @@ "windows": { "installMode": "passive" } - }, - "deep-link": { "schemes": ["jan"] } + } }, "bundle": { "publisher": "Menlo Research Pte. Ltd.", diff --git a/src-tauri/tauri.ios.conf.json b/src-tauri/tauri.ios.conf.json new file mode 100644 index 000000000..546cb4950 --- /dev/null +++ b/src-tauri/tauri.ios.conf.json @@ -0,0 +1,21 @@ +{ + "build": { + "devUrl": null, + "frontendDist": "../web-app/dist" + }, + "identifier": "jan.ai.app", + "app": { + "security": { + "capabilities": ["mobile"] + } + }, + "plugins": {}, + "bundle": { + "active": true, + "iOS": { + "developmentTeam": "" + }, + "resources": ["resources/LICENSE"], + "externalBin": [] + } +} \ No newline at end of file diff --git a/src-tauri/tauri.linux.conf.json b/src-tauri/tauri.linux.conf.json index 6b0684b25..85f39ba50 100644 --- a/src-tauri/tauri.linux.conf.json +++ b/src-tauri/tauri.linux.conf.json @@ -1,8 +1,12 @@ { + "app": { + "security": { + "capabilities": ["desktop", "system-monitor-window"] + } + }, "bundle": { "targets": ["deb", "appimage"], - "resources": ["resources/pre-install/**/*", "resources/LICENSE"], - "externalBin": ["resources/bin/uv"], + "resources": ["resources/LICENSE"], "linux": { "appimage": { "bundleMediaFramework": false, diff --git a/src-tauri/tauri.macos.conf.json b/src-tauri/tauri.macos.conf.json index d7d80f669..2113bd0fa 100644 --- a/src-tauri/tauri.macos.conf.json +++ b/src-tauri/tauri.macos.conf.json @@ -1,7 +1,11 @@ { + "app": { + "security": { + "capabilities": ["desktop", "system-monitor-window"] + } + }, "bundle": { "targets": ["app", "dmg"], - "resources": ["resources/pre-install/**/*", "resources/LICENSE"], - "externalBin": ["resources/bin/bun", "resources/bin/uv"] + "resources": ["resources/LICENSE"] } } diff --git a/src-tauri/tauri.windows.conf.json b/src-tauri/tauri.windows.conf.json index dad811467..91e2eb374 100644 --- a/src-tauri/tauri.windows.conf.json +++ b/src-tauri/tauri.windows.conf.json @@ -1,4 +1,10 @@ { + "app": { + "security": { + "capabilities": ["desktop"] + } + }, + "bundle": { "targets": ["nsis"], "resources": [ diff --git a/web-app/index.html b/web-app/index.html index fc264d096..f59835ecb 100644 --- a/web-app/index.html +++ b/web-app/index.html @@ -1,11 +1,24 @@ - + - - + + - + Jan diff --git a/web-app/src/containers/HeaderPage.tsx b/web-app/src/containers/HeaderPage.tsx index ffa9b0aa2..91dbe4c7a 100644 --- a/web-app/src/containers/HeaderPage.tsx +++ b/web-app/src/containers/HeaderPage.tsx @@ -1,7 +1,8 @@ import { useLeftPanel } from '@/hooks/useLeftPanel' import { cn } from '@/lib/utils' +import { useMobileScreen, useSmallScreen } from '@/hooks/useMediaQuery' import { IconLayoutSidebar, IconMessage, IconMessageFilled } from '@tabler/icons-react' -import { ReactNode } from '@tanstack/react-router' +import { ReactNode } from 'react' import { useRouter } from '@tanstack/react-router' import { route } from '@/constants/routes' import { PlatformFeatures } from '@/lib/platform/const' @@ -13,6 +14,8 @@ type HeaderPageProps = { } const HeaderPage = ({ children }: HeaderPageProps) => { const { open, setLeftPanel } = useLeftPanel() + const isMobile = useMobileScreen() + const isSmallScreen = useSmallScreen() const router = useRouter() const currentPath = router.state.location.pathname @@ -39,16 +42,28 @@ const HeaderPage = ({ children }: HeaderPageProps) => { return (
-
+
{!open && ( )} - {children} +
+ {children} +
{/* Temporary Chat Toggle - Only show on home page if feature is enabled */} {PlatformFeatures[PlatformFeature.TEMPORARY_CHAT] && isHomePage && ( diff --git a/web-app/src/containers/LeftPanel.tsx b/web-app/src/containers/LeftPanel.tsx index f24dcec0d..45532d76c 100644 --- a/web-app/src/containers/LeftPanel.tsx +++ b/web-app/src/containers/LeftPanel.tsx @@ -154,6 +154,7 @@ const LeftPanel = () => { } }, [setLeftPanel, open]) + const currentPath = useRouterState({ select: (state) => state.location.pathname, }) @@ -243,7 +244,7 @@ const LeftPanel = () => { return ( <> {/* Backdrop overlay for small screens */} - {isSmallScreen && open && ( + {isSmallScreen && open && !IS_IOS && !IS_ANDROID && (
{ @@ -266,7 +267,7 @@ const LeftPanel = () => { isResizableContext && 'h-full w-full', // Small screen context: fixed positioning and styling isSmallScreen && - 'fixed h-[calc(100%-16px)] bg-app z-50 rounded-sm border border-left-panel-fg/10 m-2 px-1 w-48', + 'fixed h-full pb-[calc(env(safe-area-inset-bottom)+env(safe-area-inset-top))] bg-main-view z-50 md:border border-left-panel-fg/10 px-1 w-full md:w-48', // Default context: original styling !isResizableContext && !isSmallScreen && diff --git a/web-app/src/containers/SettingsMenu.tsx b/web-app/src/containers/SettingsMenu.tsx index da0e94870..78389233d 100644 --- a/web-app/src/containers/SettingsMenu.tsx +++ b/web-app/src/containers/SettingsMenu.tsx @@ -30,12 +30,15 @@ const SettingsMenu = () => { // On web: exclude llamacpp provider as it's not available const activeProviders = providers.filter((provider) => { if (!provider.active) return false - + // On web version, hide llamacpp provider - if (!PlatformFeatures[PlatformFeature.LOCAL_INFERENCE] && provider.provider === 'llama.cpp') { + if ( + !PlatformFeatures[PlatformFeature.LOCAL_INFERENCE] && + provider.provider === 'llama.cpp' + ) { return false } - + return true }) @@ -92,7 +95,7 @@ const SettingsMenu = () => { title: 'common:keyboardShortcuts', route: route.settings.shortcuts, hasSubMenu: false, - isEnabled: true, + isEnabled: PlatformFeatures[PlatformFeature.SHORTCUT], }, { title: 'common:hardware', @@ -137,7 +140,7 @@ const SettingsMenu = () => { return ( <> - )} -
- +
+ +
+ + {t(menu.title)} + + {menu.hasSubMenu && ( + + )} +
+ - {/* Sub-menu for model providers */} - {menu.hasSubMenu && expandedProviders && ( -
- {activeProviders.map((provider) => { - const isActive = matches.some( - (match) => - match.routeId === '/settings/providers/$providerName' && - 'providerName' in match.params && - match.params.providerName === provider.provider - ) + {/* Sub-menu for model providers */} + {menu.hasSubMenu && expandedProviders && ( +
+ {activeProviders.map((provider) => { + const isActive = matches.some( + (match) => + match.routeId === + '/settings/providers/$providerName' && + 'providerName' in match.params && + match.params.providerName === provider.provider + ) - return ( -
- - )} -
+ ) + })} +
+ )} +
) })}
diff --git a/web-app/src/containers/SetupScreen.tsx b/web-app/src/containers/SetupScreen.tsx index bce474836..dadc16362 100644 --- a/web-app/src/containers/SetupScreen.tsx +++ b/web-app/src/containers/SetupScreen.tsx @@ -6,6 +6,8 @@ import HeaderPage from './HeaderPage' import { isProd } from '@/lib/version' import { useTranslation } from '@/i18n/react-i18next-compat' import { localStorageKey } from '@/constants/localStorage' +import { PlatformFeatures } from '@/lib/platform/const' +import { PlatformFeature } from '@/lib/platform' function SetupScreen() { const { t } = useTranslation() @@ -21,7 +23,7 @@ function SetupScreen() {
-
+

{t('setup:welcome')} @@ -31,22 +33,24 @@ function SetupScreen() {

- -
-

- {t('setup:localModel')} -

-
- - } - >
+ {PlatformFeatures[PlatformFeature.LOCAL_INFERENCE] && ( + +
+

+ {t('setup:localModel')} +

+
+ + } + /> + )} } - > + />
diff --git a/web-app/src/containers/__tests__/ChatInput.test.tsx b/web-app/src/containers/__tests__/ChatInput.test.tsx index 9184e3b98..1d296e15d 100644 --- a/web-app/src/containers/__tests__/ChatInput.test.tsx +++ b/web-app/src/containers/__tests__/ChatInput.test.tsx @@ -9,6 +9,7 @@ import { useAppState } from '@/hooks/useAppState' import { useGeneralSetting } from '@/hooks/useGeneralSetting' import { useModelProvider } from '@/hooks/useModelProvider' import { useChat } from '@/hooks/useChat' +import type { ThreadModel } from '@/types/threads' // Mock dependencies with mutable state let mockPromptState = { @@ -138,18 +139,70 @@ vi.mock('../MovingBorder', () => ({ vi.mock('../DropdownModelProvider', () => ({ __esModule: true, - default: () =>
Model Dropdown
, + default: () =>
Model Dropdown
, +})) + +vi.mock('../loaders/ModelLoader', () => ({ + ModelLoader: () =>
Model Loader
, })) vi.mock('../DropdownToolsAvailable', () => ({ __esModule: true, default: ({ children }: { children: (isOpen: boolean, toolsCount: number) => React.ReactNode }) => { - return
{children(false, 0)}
+ return
{children(false, 0)}
}, })) -vi.mock('../loaders/ModelLoader', () => ({ - ModelLoader: () =>
Loading...
, +vi.mock('@/components/ui/button', () => ({ + Button: ({ children, onClick, disabled, ...props }: any) => ( + + ), +})) + +vi.mock('@/components/ui/tooltip', () => ({ + Tooltip: ({ children }: { children: React.ReactNode }) =>
{children}
, + TooltipContent: ({ children }: { children: React.ReactNode }) =>
{children}
, + TooltipProvider: ({ children }: { children: React.ReactNode }) =>
{children}
, + TooltipTrigger: ({ children }: { children: React.ReactNode }) =>
{children}
, +})) + +vi.mock('react-textarea-autosize', () => ({ + default: ({ value, onChange, onKeyDown, placeholder, disabled, className, minRows, maxRows, onHeightChange, ...props }: any) => ( +