feat: system tray icon build flag (#6500)

* feat: system tray icon build flag

* fix: missing tray feature for testing

* fix: failed tests

* fix: left click should not show menu

* ci: add system dependencies for appindicator lib on Linux

---------

Co-authored-by: Minh141120 <minh.itptit@gmail.com>
This commit is contained in:
Louis 2025-09-17 21:04:43 +07:00 committed by GitHub
commit 973f77cdc6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 89 additions and 21 deletions

View File

@ -64,7 +64,7 @@ jobs:
- name: Install Tauri dependencies
run: |
sudo apt update
sudo apt install -y libglib2.0-dev libatk1.0-dev libpango1.0-dev libgtk-3-dev libsoup-3.0-dev libwebkit2gtk-4.1-dev librsvg2-dev libfuse2
sudo apt install -y libglib2.0-dev libatk1.0-dev libpango1.0-dev libgtk-3-dev libsoup-3.0-dev libwebkit2gtk-4.1-dev librsvg2-dev libfuse2 libayatana-appindicator3-dev
- name: Update app version
run: |

View File

@ -101,7 +101,7 @@ jobs:
- name: Install Tauri dependencies
run: |
sudo apt update
sudo apt install -y libglib2.0-dev libatk1.0-dev libpango1.0-dev libgtk-3-dev libsoup-3.0-dev libwebkit2gtk-4.1-dev librsvg2-dev libfuse2
sudo apt install -y libglib2.0-dev libatk1.0-dev libpango1.0-dev libgtk-3-dev libsoup-3.0-dev libwebkit2gtk-4.1-dev librsvg2-dev libfuse2 libayatana-appindicator3-dev
- name: Update app version base public_provider
run: |

View File

@ -517,41 +517,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=c5357d&locator=%40janhq%2Fassistant-extension%40workspace%3Aassistant-extension"
resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=fcb200&locator=%40janhq%2Fassistant-extension%40workspace%3Aassistant-extension"
dependencies:
rxjs: "npm:^7.8.1"
ulidx: "npm:^2.3.0"
checksum: 10c0/95e2ec1f1213d604730f5c9c381c80840402b00a9649039d1a9754ee3efa13e224e4ca39ea094aab5751f3f2ace1860c7640769e66b191b8c56998fd5f2ba5b9
checksum: 10c0/603e79794614f861a9cf5693a4bbc480a62242309a6cb94a97c81e31518032c7462b2edad93a4380a18110f817ab15d85dc91a7924fd6e103a3462f6915ee368
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=c5357d&locator=%40janhq%2Fconversational-extension%40workspace%3Aconversational-extension"
resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=fcb200&locator=%40janhq%2Fconversational-extension%40workspace%3Aconversational-extension"
dependencies:
rxjs: "npm:^7.8.1"
ulidx: "npm:^2.3.0"
checksum: 10c0/95e2ec1f1213d604730f5c9c381c80840402b00a9649039d1a9754ee3efa13e224e4ca39ea094aab5751f3f2ace1860c7640769e66b191b8c56998fd5f2ba5b9
checksum: 10c0/603e79794614f861a9cf5693a4bbc480a62242309a6cb94a97c81e31518032c7462b2edad93a4380a18110f817ab15d85dc91a7924fd6e103a3462f6915ee368
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=c5357d&locator=%40janhq%2Fdownload-extension%40workspace%3Adownload-extension"
resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=fcb200&locator=%40janhq%2Fdownload-extension%40workspace%3Adownload-extension"
dependencies:
rxjs: "npm:^7.8.1"
ulidx: "npm:^2.3.0"
checksum: 10c0/95e2ec1f1213d604730f5c9c381c80840402b00a9649039d1a9754ee3efa13e224e4ca39ea094aab5751f3f2ace1860c7640769e66b191b8c56998fd5f2ba5b9
checksum: 10c0/603e79794614f861a9cf5693a4bbc480a62242309a6cb94a97c81e31518032c7462b2edad93a4380a18110f817ab15d85dc91a7924fd6e103a3462f6915ee368
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=c5357d&locator=%40janhq%2Fllamacpp-extension%40workspace%3Allamacpp-extension"
resolution: "@janhq/core@file:../../core/package.tgz#../../core/package.tgz::hash=fcb200&locator=%40janhq%2Fllamacpp-extension%40workspace%3Allamacpp-extension"
dependencies:
rxjs: "npm:^7.8.1"
ulidx: "npm:^2.3.0"
checksum: 10c0/95e2ec1f1213d604730f5c9c381c80840402b00a9649039d1a9754ee3efa13e224e4ca39ea094aab5751f3f2ace1860c7640769e66b191b8c56998fd5f2ba5b9
checksum: 10c0/603e79794614f861a9cf5693a4bbc480a62242309a6cb94a97c81e31518032c7462b2edad93a4380a18110f817ab15d85dc91a7924fd6e103a3462f6915ee368
languageName: node
linkType: hard

View File

@ -1,4 +1,5 @@
[env]
# workaround needed to prevent `STATUS_ENTRYPOINT_NOT_FOUND` error in tests
# see https://github.com/tauri-apps/tauri/pull/4383#issuecomment-1212221864
__TAURI_WORKSPACE__ = "true"
__TAURI_WORKSPACE__ = "true"
ENABLE_SYSTEM_TRAY_ICON = "false"

View File

@ -20,6 +20,7 @@ default = [
"tauri/x11",
"tauri/protocol-asset",
"tauri/macos-private-api",
"tauri/tray-icon",
"tauri/test",
]
test-tauri = [
@ -27,6 +28,7 @@ test-tauri = [
"tauri/x11",
"tauri/protocol-asset",
"tauri/macos-private-api",
"tauri/tray-icon",
"tauri/test",
]

View File

@ -296,7 +296,6 @@ pub async fn cancel_tool_call(
pub async fn get_mcp_configs(app: AppHandle) -> Result<String, String> {
let mut path = get_jan_data_folder_path(app);
path.push("mcp_config.json");
log::info!("read mcp configs, path: {:?}", path);
// Create default empty config if file doesn't exist
if !path.exists() {

View File

@ -5,7 +5,11 @@ use std::{
path::PathBuf,
};
use tar::Archive;
use tauri::{App, Emitter, Manager};
use tauri::{
menu::{Menu, MenuItem, PredefinedMenuItem},
tray::{MouseButton, MouseButtonState, TrayIcon, TrayIconBuilder, TrayIconEvent},
App, Emitter, Manager,
};
use tauri_plugin_store::StoreExt;
// use tokio::sync::Mutex;
// use tokio::time::{sleep, Duration}; // Using tokio::sync::Mutex
@ -82,7 +86,6 @@ pub fn install_extensions(app: tauri::AppHandle, force: bool) -> Result<(), Stri
let path = entry.path();
if path.extension().map_or(false, |ext| ext == "tgz") {
log::info!("Installing extension from {:?}", path);
let tar_gz = File::open(&path).map_err(|e| e.to_string())?;
let gz_decoder = GzDecoder::new(tar_gz);
let mut archive = Archive::new(gz_decoder);
@ -207,3 +210,46 @@ pub fn setup_mcp(app: &App) {
.unwrap();
});
}
pub fn setup_tray(app: &App) -> tauri::Result<TrayIcon> {
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>)?;
let separator_i = PredefinedMenuItem::separator(app.handle())?;
let menu = Menu::with_items(app.handle(), &[&show_i, &separator_i, &quit_i])?;
TrayIconBuilder::with_id("tray")
.icon(app.default_window_icon().unwrap().clone())
.menu(&menu)
.show_menu_on_left_click(false)
.on_tray_icon_event(|tray, event| match event {
TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Up,
..
} => {
// let's show and focus the main window when the tray is clicked
let app = tray.app_handle();
if let Some(window) = app.get_webview_window("main") {
let _ = window.unminimize();
let _ = window.show();
let _ = window.set_focus();
}
}
_ => {
log::debug!("unhandled event {event:?}");
}
})
.on_menu_event(|app, event| match event.id.as_ref() {
"open" => {
let window = app.get_webview_window("main").unwrap();
window.show().unwrap();
window.set_focus().unwrap();
}
"quit" => {
app.exit(0);
}
other => {
println!("menu item {} not handled", other);
}
})
.build(app)
}

View File

@ -8,10 +8,12 @@ use core::{
};
use jan_utils::generate_app_token;
use std::{collections::HashMap, sync::Arc};
use tauri::{Emitter, Manager, RunEvent};
use tauri::{Manager, RunEvent};
use tauri_plugin_llamacpp::cleanup_llama_processes;
use tokio::sync::Mutex;
use crate::core::setup::setup_tray;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
let mut builder = tauri::Builder::default();
@ -108,6 +110,21 @@ 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()
@ -129,6 +146,11 @@ pub fn run() {
log::error!("Failed to install extensions: {}", e);
}
if option_env!("ENABLE_SYSTEM_TRAY_ICON").unwrap_or("false") == "true" {
log::info!("Enabling system tray icon");
let _ = setup_tray(app);
}
#[cfg(any(windows, target_os = "linux"))]
{
use tauri_plugin_deep_link::DeepLinkExt;
@ -146,14 +168,12 @@ 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
if let Some(window) = app_handle.get_webview_window("main") {
let _ = window.hide();
let _ = window.emit("kill-mcp-servers", ());
}
// Quick cleanup with shorter timeout
let state = app_handle.state::<AppState>();
let _ = clean_up_mcp_servers(state).await;