From 77920b67b7f568dd8db7e769d1e2b7970b7cb26d Mon Sep 17 00:00:00 2001 From: Mohammod Al Amin Ashik Date: Fri, 6 Feb 2026 08:14:53 +0800 Subject: [PATCH 01/20] feat: Add auto-start and system tray functionality (WIP) - Add tauri-plugin-autostart for launch at startup - Implement close-to-tray behavior with window event handler - Add startup settings commands (get/update) - Create Switch UI component - Add Settings UI for auto-launch, start minimized, and close-to-tray - Support --hidden flag for background startup Note: Has compilation error in mcpmux-core to be fixed after merge --- Cargo.lock | 67 +++- apps/desktop/src-tauri/Cargo.toml | 1 + apps/desktop/src-tauri/src/commands/mod.rs | 2 + .../src-tauri/src/commands/settings.rs | 118 ++++++++ apps/desktop/src-tauri/src/lib.rs | 54 ++++ .../src/features/registry/ServerCard.tsx | 44 ++- .../features/registry/ServerDetailModal.tsx | 213 ++++++++++++- .../src/features/settings/SettingsPage.tsx | 285 ++++++++++++++++++ apps/desktop/src/types/registry.ts | 51 ++++ crates/mcpmux-core/src/domain/server.rs | 110 +++++++ packages/ui/src/components/common/Switch.tsx | 46 +++ packages/ui/src/index.ts | 1 + 12 files changed, 975 insertions(+), 17 deletions(-) create mode 100644 apps/desktop/src-tauri/src/commands/settings.rs create mode 100644 apps/desktop/src/features/settings/SettingsPage.tsx create mode 100644 packages/ui/src/components/common/Switch.tsx diff --git a/Cargo.lock b/Cargo.lock index ccdcb8ac..131c4dc0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -260,6 +260,17 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "auto-launch" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f012b8cc0c850f34117ec8252a44418f2e34a2cf501de89e29b241ae5f79471" +dependencies = [ + "dirs 4.0.0", + "thiserror 1.0.69", + "winreg 0.10.1", +] + [[package]] name = "autocfg" version = "1.5.0" @@ -930,6 +941,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "dirs" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3aa72a6f96ea37bbc5aa912f6788242832f75369bdfdadcb0e38423f100059" +dependencies = [ + "dirs-sys 0.3.7", +] + [[package]] name = "dirs" version = "5.0.1" @@ -948,6 +968,17 @@ dependencies = [ "dirs-sys 0.5.0", ] +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users 0.4.6", + "winapi", +] + [[package]] name = "dirs-sys" version = "0.4.1" @@ -1090,7 +1121,7 @@ dependencies = [ "rustc_version", "toml 0.9.11+spec-1.1.0", "vswhom", - "winreg", + "winreg 0.55.0", ] [[package]] @@ -2498,7 +2529,7 @@ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" [[package]] name = "mcpmux" -version = "0.0.3" +version = "0.0.5" dependencies = [ "anyhow", "async-trait", @@ -2517,6 +2548,7 @@ dependencies = [ "serde_json", "tauri", "tauri-build", + "tauri-plugin-autostart", "tauri-plugin-deep-link", "tauri-plugin-opener", "tauri-plugin-single-instance", @@ -2533,7 +2565,7 @@ dependencies = [ [[package]] name = "mcpmux-core" -version = "0.0.3" +version = "0.0.5" dependencies = [ "anyhow", "async-trait", @@ -2555,7 +2587,7 @@ dependencies = [ [[package]] name = "mcpmux-gateway" -version = "0.0.3" +version = "0.0.5" dependencies = [ "anyhow", "async-stream", @@ -2595,7 +2627,7 @@ dependencies = [ [[package]] name = "mcpmux-mcp" -version = "0.0.3" +version = "0.0.5" dependencies = [ "anyhow", "async-trait", @@ -2614,7 +2646,7 @@ dependencies = [ [[package]] name = "mcpmux-storage" -version = "0.0.3" +version = "0.0.5" dependencies = [ "anyhow", "async-trait", @@ -4999,6 +5031,20 @@ dependencies = [ "walkdir", ] +[[package]] +name = "tauri-plugin-autostart" +version = "2.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "459383cebc193cdd03d1ba4acc40f2c408a7abce419d64bdcd2d745bc2886f70" +dependencies = [ + "auto-launch", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.18", +] + [[package]] name = "tauri-plugin-deep-link" version = "2.4.6" @@ -6687,6 +6733,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + [[package]] name = "winreg" version = "0.55.0" diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index 7ad78ec6..835b2f4b 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -20,6 +20,7 @@ tauri-plugin-opener = "2" tauri-plugin-single-instance = "2" tauri-plugin-deep-link = "2" tauri-plugin-updater = "2" +tauri-plugin-autostart = "2" serde.workspace = true serde_json.workspace = true tokio.workspace = true diff --git a/apps/desktop/src-tauri/src/commands/mod.rs b/apps/desktop/src-tauri/src/commands/mod.rs index f5eee08b..9c6f72fb 100644 --- a/apps/desktop/src-tauri/src/commands/mod.rs +++ b/apps/desktop/src-tauri/src/commands/mod.rs @@ -16,6 +16,7 @@ pub mod server; pub mod server_discovery; pub mod server_feature; pub mod server_manager; +pub mod settings; pub mod space; // Re-export commands for convenience @@ -31,4 +32,5 @@ pub use server::*; pub use server_discovery::*; pub use server_feature::*; pub use server_manager::*; +pub use settings::*; pub use space::*; diff --git a/apps/desktop/src-tauri/src/commands/settings.rs b/apps/desktop/src-tauri/src/commands/settings.rs new file mode 100644 index 00000000..3353564c --- /dev/null +++ b/apps/desktop/src-tauri/src/commands/settings.rs @@ -0,0 +1,118 @@ +//! Settings commands for auto-start and system tray behavior + +use serde::{Deserialize, Serialize}; +use tauri::State; +use tauri_plugin_autostart::AutoLaunchManager; +use tracing::{debug, error, info}; + +use crate::state::AppState; + +/// Startup and system tray settings +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct StartupSettings { + /// Whether to launch the app at system startup + pub auto_launch: bool, + /// Whether to start minimized to tray + pub start_minimized: bool, + /// Whether to minimize to tray instead of closing + pub close_to_tray: bool, +} + +impl Default for StartupSettings { + fn default() -> Self { + Self { + auto_launch: false, + start_minimized: false, + close_to_tray: true, // Default to close-to-tray behavior + } + } +} + +/// Get current startup settings +#[tauri::command] +pub async fn get_startup_settings( + app_state: State<'_, AppState>, + manager: State<'_, AutoLaunchManager>, +) -> Result { + debug!("[Settings] Getting startup settings"); + + let settings_repo = &app_state.settings_repository; + + // Get auto-launch status from the OS + let auto_launch = manager + .is_enabled() + .await + .map_err(|e| format!("Failed to check auto-launch status: {}", e))?; + + // Get other settings from database + let start_minimized = settings_repo + .get("startup.start_minimized") + .await + .map_err(|e| format!("Failed to get start_minimized setting: {}", e))? + .map(|v| v == "true") + .unwrap_or(false); + + let close_to_tray = settings_repo + .get("ui.close_to_tray") + .await + .map_err(|e| format!("Failed to get close_to_tray setting: {}", e))? + .map(|v| v == "true") + .unwrap_or(true); // Default to true + + Ok(StartupSettings { + auto_launch, + start_minimized, + close_to_tray, + }) +} + +/// Update startup settings +#[tauri::command] +pub async fn update_startup_settings( + settings: StartupSettings, + app_state: State<'_, AppState>, + manager: State<'_, AutoLaunchManager>, +) -> Result<(), String> { + info!("[Settings] Updating startup settings: {:?}", settings); + + let settings_repo = &app_state.settings_repository; + + // Update auto-launch in OS + if settings.auto_launch { + manager + .enable() + .await + .map_err(|e| format!("Failed to enable auto-launch: {}", e))?; + info!("[Settings] Auto-launch enabled"); + } else { + manager + .disable() + .await + .map_err(|e| format!("Failed to disable auto-launch: {}", e))?; + info!("[Settings] Auto-launch disabled"); + } + + // Update other settings in database + settings_repo + .set( + "startup.start_minimized", + &settings.start_minimized.to_string(), + ) + .await + .map_err(|e| format!("Failed to save start_minimized setting: {}", e))?; + + settings_repo + .set("ui.close_to_tray", &settings.close_to_tray.to_string()) + .await + .map_err(|e| format!("Failed to save close_to_tray setting: {}", e))?; + + info!("[Settings] Startup settings updated successfully"); + Ok(()) +} + +/// Check if app should start hidden (for auto-launch with --hidden flag) +pub fn should_start_hidden() -> bool { + let args: Vec = std::env::args().collect(); + args.contains(&"--hidden".to_string()) +} diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 49d39034..0cbdbcf7 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -189,6 +189,10 @@ pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_deep_link::init()) + .plugin(tauri_plugin_autostart::init( + tauri_plugin_autostart::MacosLauncher::LaunchAgent, + Some(vec!["--hidden"]), // Start minimized to tray + )) .plugin(tauri_plugin_single_instance::init(|app, args, cwd| { // This callback is called when a second instance is launched info!("Second instance detected, focusing existing window"); @@ -517,6 +521,53 @@ pub fn run() { // Setup system tray tray::setup_tray(app.handle())?; + // Setup window close event handler for close-to-tray behavior + if let Some(main_window) = app.get_webview_window("main") { + let app_handle = app.handle().clone(); + let settings_repo = app_state.settings_repository.clone(); + + main_window.on_window_event(move |event| { + if let tauri::WindowEvent::CloseRequested { api, .. } = event { + // Check if close-to-tray is enabled + let app_handle_clone = app_handle.clone(); + let settings_clone = settings_repo.clone(); + + tauri::async_runtime::spawn(async move { + match settings_clone.get("ui.close_to_tray").await { + Ok(Some(value)) if value == "true" => { + // Close to tray - hide window instead of closing + info!("[Window] Close requested, hiding to tray"); + if let Some(window) = app_handle_clone.get_webview_window("main") { + let _ = window.hide(); + } + } + Ok(Some(value)) if value == "false" => { + // Actually close the app + info!("[Window] Close requested, exiting app"); + app_handle_clone.exit(0); + } + _ => { + // Default behavior: close to tray + info!("[Window] Close requested (default), hiding to tray"); + if let Some(window) = app_handle_clone.get_webview_window("main") { + let _ = window.hide(); + } + } + } + }); + + // Always prevent default close to handle it asynchronously + api.prevent_close(); + } + }); + + // Check if app should start hidden (auto-launch with --hidden flag) + if commands::should_start_hidden() { + info!("[Window] Starting hidden (--hidden flag present)"); + let _ = main_window.hide(); + } + } + // Register deep link handler for when app receives URLs #[cfg(desktop)] { @@ -644,6 +695,9 @@ pub fn run() { // App log commands get_logs_path, open_logs_folder, + // Startup settings commands + commands::get_startup_settings, + commands::update_startup_settings, ]) .run(tauri::generate_context!()) .expect("error while running McpMux application"); diff --git a/apps/desktop/src/features/registry/ServerCard.tsx b/apps/desktop/src/features/registry/ServerCard.tsx index 99beff85..9a0a0447 100644 --- a/apps/desktop/src/features/registry/ServerCard.tsx +++ b/apps/desktop/src/features/registry/ServerCard.tsx @@ -50,18 +50,48 @@ export function ServerCard({ }; const getTransportBadge = () => { + // Use hosting_type if available, otherwise infer from transport + const hostingType = server.hosting_type || (server.transport.type === 'stdio' ? 'local' : 'remote'); + const config = { - stdio: { bg: 'bg-purple-500/20', text: 'text-purple-600 dark:text-purple-400', label: 'Local' }, - http: { bg: 'bg-[rgb(var(--primary))]/20', text: 'text-[rgb(var(--primary))]', label: 'HTTP' }, - }[server.transport.type]; + local: { icon: '💻', label: 'Local', bg: 'bg-purple-500/20', text: 'text-purple-600 dark:text-purple-400' }, + remote: { icon: '☁️', label: 'Cloud', bg: 'bg-blue-500/20', text: 'text-blue-600 dark:text-blue-400' }, + hybrid: { icon: '🔄', label: 'Hybrid', bg: 'bg-indigo-500/20', text: 'text-indigo-600 dark:text-indigo-400' }, + }[hostingType]; return ( - {config.label} + {config.icon} {config.label} ); }; + const getBadges = () => { + if (!server.badges || server.badges.length === 0) return null; + + const badgeConfig: Record = { + official: { label: 'Official', bg: 'bg-blue-500/20', text: 'text-blue-600 dark:text-blue-400' }, + verified: { label: '✓ Verified', bg: 'bg-green-500/20', text: 'text-green-600 dark:text-green-400' }, + featured: { label: '⭐ Featured', bg: 'bg-amber-500/20', text: 'text-amber-600 dark:text-amber-400' }, + sponsored: { label: 'Sponsored', bg: 'bg-yellow-500/20', text: 'text-yellow-600 dark:text-yellow-400' }, + popular: { label: '🔥 Popular', bg: 'bg-red-500/20', text: 'text-red-600 dark:text-red-400' }, + }; + + return ( + <> + {server.badges.slice(0, 2).map((badge) => { + const config = badgeConfig[badge]; + if (!config) return null; + return ( + + {config.label} + + ); + })} + + ); + }; + return (
+ {getBadges()} {getTransportBadge()} {getAuthBadge()} + {server.capabilities?.read_only_mode && ( + + 🛡️ Read-Only + + )}
{/* Categories */} diff --git a/apps/desktop/src/features/registry/ServerDetailModal.tsx b/apps/desktop/src/features/registry/ServerDetailModal.tsx index 183517bb..94fc7b13 100644 --- a/apps/desktop/src/features/registry/ServerDetailModal.tsx +++ b/apps/desktop/src/features/registry/ServerDetailModal.tsx @@ -33,7 +33,7 @@ export function ServerDetailModal({
{server.icon || '📦'}
-
+

{server.name}

@@ -42,6 +42,36 @@ export function ServerDetailModal({ ✓ )} + {/* Badges */} + {server.badges && server.badges.length > 0 && ( +
+ {server.badges.includes('official') && ( + + Official + + )} + {server.badges.includes('verified') && ( + + ✓ Verified + + )} + {server.badges.includes('featured') && ( + + ⭐ Featured + + )} + {server.badges.includes('sponsored') && ( + + Sponsored + + )} + {server.badges.includes('popular') && ( + + 🔥 Popular + + )} +
+ )}
{server.publisher?.name && (

@@ -61,6 +91,30 @@ export function ServerDetailModal({ {/* Content */}

+ {/* Sponsored Banner */} + {server.sponsored?.enabled && ( +
+ {server.sponsored.sponsor_logo && ( + Sponsor + )} +
+ Sponsored by + {server.sponsored.sponsor_url ? ( + + {server.sponsored.sponsor_name} + + ) : ( + {server.sponsored.sponsor_name} + )} +
+
+ )} + {/* Description */}

@@ -74,19 +128,26 @@ export function ServerDetailModal({ {/* Transport */}

- Transport + Hosting

- {server.transport.type === 'stdio' - ? '🖥️ Local Process (stdio)' - : '🌐 Remote Server (HTTP)'} + {(server.hosting_type || (server.transport.type === 'stdio' ? 'local' : 'remote')) === 'local' + ? '💻 Local Process' + : (server.hosting_type || 'remote') === 'remote' + ? '☁️ Remote Server' + : '🔄 Hybrid'} + + + ({server.transport.type})
@@ -143,6 +204,144 @@ export function ServerDetailModal({

)} + {/* Capabilities */} + {server.capabilities && ( +
+

+ Capabilities +

+
+ {server.capabilities.tools && ( + + 🛠️ Tools + + )} + {server.capabilities.resources && ( + + 📁 Resources + + )} + {server.capabilities.prompts && ( + + 💬 Prompts + + )} + {server.capabilities.read_only_mode && ( + + 🛡️ Read-Only (Safe) + + )} +
+
+ )} + + {/* Installation Info */} + {server.installation && ( +
+

Installation Info

+
+ {server.installation.difficulty && ( +
+ Difficulty: + + {server.installation.difficulty} + +
+ )} + {server.installation.estimated_time && ( +
+ Time: + {server.installation.estimated_time} +
+ )} + {server.installation.prerequisites && server.installation.prerequisites.length > 0 && ( +
+ Prerequisites: +
    + {server.installation.prerequisites.map((prereq, i) => ( +
  • {prereq}
  • + ))} +
+
+ )} +
+
+ )} + + {/* License */} + {server.license && ( +
+

License

+
+ + {server.license} + + {server.license_url && ( + + View License → + + )} +
+
+ )} + + {/* Screenshots */} + {server.media?.screenshots && server.media.screenshots.length > 0 && ( +
+

Screenshots

+
+ {server.media.screenshots.map((url, i) => ( + {`Screenshot + ))} +
+
+ )} + + {/* Links */} + {(server.media?.demo_video || server.changelog_url) && ( +
+ {server.media?.demo_video && ( + + 🎥 Watch Demo Video → + + )} + {server.changelog_url && ( + + 📝 View Changelog → + + )} +
+ )} + {/* Source */} {server.source.type === 'Registry' && (
diff --git a/apps/desktop/src/features/settings/SettingsPage.tsx b/apps/desktop/src/features/settings/SettingsPage.tsx new file mode 100644 index 00000000..e730ff13 --- /dev/null +++ b/apps/desktop/src/features/settings/SettingsPage.tsx @@ -0,0 +1,285 @@ +import { useState, useEffect } from 'react'; +import { invoke } from '@tauri-apps/api/core'; +import { + Card, + CardHeader, + CardTitle, + CardDescription, + CardContent, + Button, + Switch, +} from '@mcpmux/ui'; +import { + Sun, + Moon, + Monitor, + FileText, + FolderOpen, + Loader2, + Power, + Minimize2, + XCircle, +} from 'lucide-react'; +import { useAppStore, useTheme } from '@/stores'; +import { UpdateChecker } from './UpdateChecker'; + +interface StartupSettings { + autoLaunch: boolean; + startMinimized: boolean; + closeToTray: boolean; +} + +export function SettingsPage() { + const theme = useTheme(); + const setTheme = useAppStore((state) => state.setTheme); + const [logsPath, setLogsPath] = useState(''); + const [openingLogs, setOpeningLogs] = useState(false); + + // Startup settings state + const [startupSettings, setStartupSettings] = useState({ + autoLaunch: false, + startMinimized: false, + closeToTray: true, + }); + const [loadingSettings, setLoadingSettings] = useState(true); + const [savingSettings, setSavingSettings] = useState(false); + + // Load logs path on mount + useEffect(() => { + const loadLogsPath = async () => { + try { + const path = await invoke('get_logs_path'); + setLogsPath(path); + } catch (error) { + console.error('Failed to get logs path:', error); + } + }; + loadLogsPath(); + }, []); + + // Load startup settings on mount + useEffect(() => { + const loadStartupSettings = async () => { + try { + const settings = await invoke('get_startup_settings'); + setStartupSettings(settings); + } catch (error) { + console.error('Failed to load startup settings:', error); + } finally { + setLoadingSettings(false); + } + }; + loadStartupSettings(); + }, []); + + // Save startup settings when they change + const updateStartupSetting = async ( + key: keyof StartupSettings, + value: boolean + ) => { + const newSettings = { ...startupSettings, [key]: value }; + setStartupSettings(newSettings); + + setSavingSettings(true); + try { + await invoke('update_startup_settings', { settings: newSettings }); + console.log('Startup settings saved:', newSettings); + } catch (error) { + console.error('Failed to save startup settings:', error); + // Revert on error + setStartupSettings(startupSettings); + } finally { + setSavingSettings(false); + } + }; + + const handleOpenLogs = async () => { + setOpeningLogs(true); + try { + await invoke('open_logs_folder'); + } catch (error) { + console.error('Failed to open logs folder:', error); + } finally { + setOpeningLogs(false); + } + }; + + return ( +
+
+

Settings

+

Configure McpMux preferences.

+
+ + {/* Updates Section */} + + + {/* Startup & System Tray Section */} + + + + + Startup & System Tray + + + Control how McpMux starts and behaves with the system tray. + + + + {loadingSettings ? ( +
+ +
+ ) : ( +
+
+
+ +
+ +

+ Start McpMux automatically when you log in to your system +

+
+
+ updateStartupSetting('autoLaunch', checked)} + disabled={savingSettings} + data-testid="auto-launch-switch" + /> +
+ +
+
+ +
+ +

+ Launch in background to system tray (requires auto-launch enabled) +

+
+
+ updateStartupSetting('startMinimized', checked)} + disabled={savingSettings || !startupSettings.autoLaunch} + data-testid="start-minimized-switch" + /> +
+ +
+
+ +
+ +

+ Keep running in system tray when window is closed (use "Quit" from tray to exit) +

+
+
+ updateStartupSetting('closeToTray', checked)} + disabled={savingSettings} + data-testid="close-to-tray-switch" + /> +
+ + {savingSettings && ( +
+ + Saving settings... +
+ )} +
+ )} +
+
+ + {/* Appearance Section */} + + + Appearance + Customize the look and feel of McpMux. + + +
+
+ +
+ + + +
+
+
+
+
+ + {/* Logs Section */} + + + + + Logs + + View application logs for debugging and troubleshooting. + + +
+
+ +

+ {logsPath || 'Loading...'} +

+
+
+ +
+

+ Logs are rotated daily. Each file contains detailed debug information including thread IDs and source locations. +

+
+
+
+
+ ); +} diff --git a/apps/desktop/src/types/registry.ts b/apps/desktop/src/types/registry.ts index 3a1e18c1..4302e1b7 100644 --- a/apps/desktop/src/types/registry.ts +++ b/apps/desktop/src/types/registry.ts @@ -60,6 +60,16 @@ export interface ServerDefinition { categories: string[]; publisher: PublisherInfo | null; source: ServerSource; + // Schema v2.1 additions + badges?: Badge[]; + hosting_type?: HostingType; + license?: string; + license_url?: string; + installation?: Installation; + capabilities?: Capabilities; + sponsored?: Sponsored; + media?: Media; + changelog_url?: string; } /** Auth configuration - matches backend snake_case serialization */ @@ -166,4 +176,45 @@ export interface SortRule { /** Home configuration */ export interface HomeConfig { featured_server_ids: string[]; +} + +// ============================================ +// Schema v2.1 Additions +// ============================================ + +/** Visual badge indicators */ +export type Badge = 'official' | 'verified' | 'featured' | 'sponsored' | 'popular'; + +/** Server hosting type */ +export type HostingType = 'local' | 'remote' | 'hybrid'; + +/** Installation metadata */ +export interface Installation { + difficulty?: 'easy' | 'moderate' | 'advanced'; + prerequisites?: string[]; + estimated_time?: string; +} + +/** MCP capabilities with read-only support */ +export interface Capabilities { + tools?: boolean; + resources?: boolean; + prompts?: boolean; + read_only_mode?: boolean; +} + +/** Sponsorship information */ +export interface Sponsored { + enabled?: boolean; + sponsor_name?: string; + sponsor_url?: string; + sponsor_logo?: string; + campaign_id?: string; +} + +/** Rich media content */ +export interface Media { + screenshots?: string[]; + demo_video?: string; + banner?: string; } \ No newline at end of file diff --git a/crates/mcpmux-core/src/domain/server.rs b/crates/mcpmux-core/src/domain/server.rs index d7c34ec2..e6ee5027 100644 --- a/crates/mcpmux-core/src/domain/server.rs +++ b/crates/mcpmux-core/src/domain/server.rs @@ -36,6 +36,35 @@ pub struct ServerDefinition { /// Where this server came from #[serde(default)] pub source: ServerSource, + + /// Visual badges for trust and discovery (v2.1) + #[serde(default)] + pub badges: Vec, + + /// Where the server runs: local, remote, or hybrid (v2.1) + #[serde(default)] + pub hosting_type: HostingType, + + /// SPDX license identifier (v2.1) + pub license: Option, + + /// URL to full license text (v2.1) + pub license_url: Option, + + /// Installation metadata (v2.1) + pub installation: Option, + + /// MCP capabilities (v2.1) + pub capabilities: Option, + + /// Sponsorship information (v2.1) + pub sponsored: Option, + + /// Rich media content (v2.1) + pub media: Option, + + /// Changelog URL (v2.1) + pub changelog_url: Option, // NOTE: Runtime state like 'enabled' is NOT stored here. // It is injected at the application layer by merging with DB state. } @@ -146,3 +175,84 @@ pub struct PublisherInfo { #[serde(default)] pub official: bool, } + +// ============================================ +// Schema v2.1 Additions +// ============================================ + +/// Visual badge indicators for server listings +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Badge { + Official, + Verified, + Featured, + Sponsored, + Popular, +} + +/// Where the server runs +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum HostingType { + Local, + Remote, + Hybrid, +} + +impl Default for HostingType { + fn default() -> Self { + Self::Local + } +} + +/// Installation complexity level +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum InstallDifficulty { + Easy, + Moderate, + Advanced, +} + +/// Installation metadata for user guidance +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Installation { + pub difficulty: Option, + #[serde(default)] + pub prerequisites: Vec, + pub estimated_time: Option, +} + +/// MCP capabilities with read-only mode support +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct Capabilities { + #[serde(default)] + pub tools: bool, + #[serde(default)] + pub resources: bool, + #[serde(default)] + pub prompts: bool, + #[serde(default)] + pub read_only_mode: bool, +} + +/// Sponsorship information for commercial listings +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Sponsored { + #[serde(default)] + pub enabled: bool, + pub sponsor_name: Option, + pub sponsor_url: Option, + pub sponsor_logo: Option, + pub campaign_id: Option, +} + +/// Rich media content for enhanced discovery +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Media { + #[serde(default)] + pub screenshots: Vec, + pub demo_video: Option, + pub banner: Option, +} diff --git a/packages/ui/src/components/common/Switch.tsx b/packages/ui/src/components/common/Switch.tsx new file mode 100644 index 00000000..bb7442fb --- /dev/null +++ b/packages/ui/src/components/common/Switch.tsx @@ -0,0 +1,46 @@ +/** + * Switch/Toggle component + * A simple toggle switch for boolean settings + */ + +import { cn } from '../../lib/cn'; + +interface SwitchProps { + checked: boolean; + onCheckedChange: (checked: boolean) => void; + disabled?: boolean; + className?: string; + 'data-testid'?: string; +} + +export function Switch({ + checked, + onCheckedChange, + disabled = false, + className, + 'data-testid': testId, +}: SwitchProps) { + return ( + + ); +} diff --git a/packages/ui/src/index.ts b/packages/ui/src/index.ts index 2d419480..c881f5ec 100644 --- a/packages/ui/src/index.ts +++ b/packages/ui/src/index.ts @@ -13,6 +13,7 @@ export { StatusBar, StatusBarItem } from './components/layout/StatusBar'; export { Button } from './components/common/Button'; export { Input } from './components/common/Input'; export { Card, CardHeader, CardTitle, CardDescription, CardContent } from './components/common/Card'; +export { Switch } from './components/common/Switch'; // Utilities export { cn } from './lib/cn'; From 7a1ee9a3aaf699ddf4aaf1e5e1088843601678b5 Mon Sep 17 00:00:00 2001 From: Mohammod Al Amin Ashik Date: Fri, 6 Feb 2026 08:19:04 +0800 Subject: [PATCH 02/20] fix: Resolve compilation errors after merge - Fix ServerDefinition initialization with missing v2.1 fields - Add HostingType import to config.rs - Remove incorrect async/await from autostart plugin calls - Remove unused error import --- apps/desktop/src-tauri/src/commands/settings.rs | 5 +---- crates/mcpmux-core/src/domain/config.rs | 13 +++++++++++-- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/apps/desktop/src-tauri/src/commands/settings.rs b/apps/desktop/src-tauri/src/commands/settings.rs index 3353564c..afb59f92 100644 --- a/apps/desktop/src-tauri/src/commands/settings.rs +++ b/apps/desktop/src-tauri/src/commands/settings.rs @@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize}; use tauri::State; use tauri_plugin_autostart::AutoLaunchManager; -use tracing::{debug, error, info}; +use tracing::{debug, info}; use crate::state::AppState; @@ -42,7 +42,6 @@ pub async fn get_startup_settings( // Get auto-launch status from the OS let auto_launch = manager .is_enabled() - .await .map_err(|e| format!("Failed to check auto-launch status: {}", e))?; // Get other settings from database @@ -82,13 +81,11 @@ pub async fn update_startup_settings( if settings.auto_launch { manager .enable() - .await .map_err(|e| format!("Failed to enable auto-launch: {}", e))?; info!("[Settings] Auto-launch enabled"); } else { manager .disable() - .await .map_err(|e| format!("Failed to disable auto-launch: {}", e))?; info!("[Settings] Auto-launch disabled"); } diff --git a/crates/mcpmux-core/src/domain/config.rs b/crates/mcpmux-core/src/domain/config.rs index e25cf1c2..b2789fb7 100644 --- a/crates/mcpmux-core/src/domain/config.rs +++ b/crates/mcpmux-core/src/domain/config.rs @@ -1,6 +1,6 @@ use crate::domain::server::{ - AuthConfig, InputDefinition, PublisherInfo, ServerDefinition, ServerSource, TransportConfig, - TransportMetadata, + AuthConfig, HostingType, InputDefinition, PublisherInfo, ServerDefinition, ServerSource, + TransportConfig, TransportMetadata, }; use lazy_static::lazy_static; use regex::Regex; @@ -125,6 +125,15 @@ impl UserServerEntry { space_id: space_id.to_string(), file_path, }, + badges: vec![], + hosting_type: HostingType::default(), + license: None, + license_url: None, + installation: None, + capabilities: None, + sponsored: None, + media: None, + changelog_url: None, } } From fed9391e45a2559ffd86a0f147bbef26d75a8b5b Mon Sep 17 00:00:00 2001 From: Mohammod Al Amin Ashik Date: Fri, 6 Feb 2026 08:33:25 +0800 Subject: [PATCH 03/20] feat: Simplify system tray and fix startup settings UI - Simplify tray menu to only: Active Space, Open, Quit - Remove unsupported features: export config, refresh servers, create space - Fix Switch component to use correct CSS variables (--primary) - Add comprehensive test coverage for startup settings - Update tray documentation This addresses user feedback about cluttered tray menu and non-working UI elements. --- .../src-tauri/src/services/file_watcher.rs | 1 - apps/desktop/src-tauri/src/tray.rs | 99 ++----------------- packages/ui/src/components/common/Switch.tsx | 4 +- tests/e2e/specs/settings.spec.ts | 85 ++++++++++++++++ 4 files changed, 97 insertions(+), 92 deletions(-) diff --git a/apps/desktop/src-tauri/src/services/file_watcher.rs b/apps/desktop/src-tauri/src/services/file_watcher.rs index 076109a5..e3bfe3a4 100644 --- a/apps/desktop/src-tauri/src/services/file_watcher.rs +++ b/apps/desktop/src-tauri/src/services/file_watcher.rs @@ -223,7 +223,6 @@ impl SpaceFileWatcherBuilder { #[cfg(test)] mod tests { - use super::*; #[test] fn test_builder_default_space_id() { diff --git a/apps/desktop/src-tauri/src/tray.rs b/apps/desktop/src-tauri/src/tray.rs index 33401b10..7e035aae 100644 --- a/apps/desktop/src-tauri/src/tray.rs +++ b/apps/desktop/src-tauri/src/tray.rs @@ -2,9 +2,8 @@ //! //! Provides a system tray icon with quick access to: //! - Space switching -//! - Config export -//! - Server status //! - Open main window +//! - Quit application use tauri::{ menu::{Menu, MenuBuilder, MenuItemBuilder, PredefinedMenuItem, SubmenuBuilder}, @@ -65,36 +64,18 @@ pub fn setup_tray(app: &AppHandle) -> tauri::Result<()> { /// Build the tray menu fn build_tray_menu(app: &AppHandle) -> tauri::Result> { - // Space submenu + // Space submenu (will be populated dynamically) let space_submenu = SubmenuBuilder::new(app, "Active Space") .text("space_default", "🌐 Default") - .separator() - .text("create_space", "➕ Create Space...") - .build()?; - - // Export submenu - let export_submenu = SubmenuBuilder::new(app, "📋 Export Config") - .text("export_cursor", "Cursor") - .text("export_vscode", "VS Code") - .text("export_claude", "Claude Desktop") .build()?; - // Build main menu + // Build simplified main menu let menu = MenuBuilder::new(app) - .item( - &MenuItemBuilder::with_id("status", "McpMux 🟢") - .enabled(false) - .build(app)?, - ) - .separator() .item(&space_submenu) .separator() - .text("refresh", "🔄 Refresh All Servers") - .item(&export_submenu) + .text("open", "Open McpMux") .separator() - .text("open", "⚙️ Open McpMux") - .item(&PredefinedMenuItem::separator(app)?) - .text("quit", "❌ Quit") + .text("quit", "Quit") .build()?; Ok(menu) @@ -110,25 +91,6 @@ fn handle_menu_event(app: &AppHandle, event_id: &str) { let space_id = id.strip_prefix("space_").unwrap_or("default"); handle_switch_space(app, space_id); } - "create_space" => { - open_main_window_at(app, "/spaces/new"); - } - - // Export actions - "export_cursor" => { - handle_export(app, "cursor"); - } - "export_vscode" => { - handle_export(app, "vscode"); - } - "export_claude" => { - handle_export(app, "claude"); - } - - // General actions - "refresh" => { - handle_refresh_servers(app); - } "open" => { if let Some(window) = app.get_webview_window("main") { let _ = window.show(); @@ -149,34 +111,12 @@ fn handle_menu_event(app: &AppHandle, event_id: &str) { fn handle_switch_space(app: &AppHandle, space_id: &str) { info!("Switching to space: {}", space_id); - // Emit event to frontend - let _ = app.emit("tray:switch-space", space_id); -} - -/// Open main window at a specific route -fn open_main_window_at(app: &AppHandle, route: &str) { + // Show window and emit event to frontend if let Some(window) = app.get_webview_window("main") { let _ = window.show(); let _ = window.set_focus(); - // Emit navigation event - let _ = app.emit("tray:navigate", route); } -} - -/// Handle export request -fn handle_export(app: &AppHandle, client_type: &str) { - info!("Export config requested for: {}", client_type); - - // Emit event to frontend to handle export - let _ = app.emit("tray:export-config", client_type); -} - -/// Handle refresh all servers -fn handle_refresh_servers(app: &AppHandle) { - info!("Refresh all servers requested"); - - // Emit event to frontend - let _ = app.emit("tray:refresh-servers", ()); + let _ = app.emit("tray:switch-space", space_id); } /// Update tray menu with current spaces @@ -205,34 +145,15 @@ pub async fn update_tray_spaces( space_menu = space_menu.text(id, label); } - space_menu = space_menu - .separator() - .text("create_space", "➕ Create Space..."); - let space_submenu = space_menu.build()?; - // Rebuild full menu - let export_submenu = SubmenuBuilder::new(app, "📋 Export Config") - .text("export_cursor", "Cursor") - .text("export_vscode", "VS Code") - .text("export_claude", "Claude Desktop") - .build()?; - + // Rebuild simplified menu let menu = MenuBuilder::new(app) - .item( - &MenuItemBuilder::with_id("status", "McpMux 🟢") - .enabled(false) - .build(app)?, - ) - .separator() .item(&space_submenu) .separator() - .text("refresh", "🔄 Refresh All Servers") - .item(&export_submenu) + .text("open", "Open McpMux") .separator() - .text("open", "⚙️ Open McpMux") - .item(&PredefinedMenuItem::separator(app)?) - .text("quit", "❌ Quit") + .text("quit", "Quit") .build()?; tray.set_menu(Some(menu))?; diff --git a/packages/ui/src/components/common/Switch.tsx b/packages/ui/src/components/common/Switch.tsx index bb7442fb..e163cf90 100644 --- a/packages/ui/src/components/common/Switch.tsx +++ b/packages/ui/src/components/common/Switch.tsx @@ -29,8 +29,8 @@ export function Switch({ onClick={() => !disabled && onCheckedChange(!checked)} data-testid={testId} className={cn( - 'relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50', - checked ? 'bg-primary-500' : 'bg-surface-secondary', + 'relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-[rgb(var(--primary))] focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50', + checked ? 'bg-[rgb(var(--primary))]' : 'bg-surface-secondary', className )} > diff --git a/tests/e2e/specs/settings.spec.ts b/tests/e2e/specs/settings.spec.ts index c4afef82..8246efdf 100644 --- a/tests/e2e/specs/settings.spec.ts +++ b/tests/e2e/specs/settings.spec.ts @@ -208,4 +208,89 @@ test.describe('Settings', () => { await expect(mainContent).toBeVisible(); }); }); + + test.describe('Startup & System Tray Settings', () => { + test('should display startup settings section', async ({ page }) => { + const dashboard = new DashboardPage(page); + await dashboard.navigate(); + + await page.locator('nav button:has-text("Settings")').click(); + + // Check for startup settings card + await expect(page.getByText('Startup & System Tray')).toBeVisible(); + await expect(page.getByText(/Control how McpMux starts/)).toBeVisible(); + }); + + test('should display all three startup toggles', async ({ page }) => { + const dashboard = new DashboardPage(page); + await dashboard.navigate(); + + await page.locator('nav button:has-text("Settings")').click(); + + // Check all three settings exist + await expect(page.getByText('Launch at Startup')).toBeVisible(); + await expect(page.getByText('Start Minimized')).toBeVisible(); + await expect(page.getByText('Close to Tray')).toBeVisible(); + }); + + test('should have functional toggle switches', async ({ page }) => { + const dashboard = new DashboardPage(page); + await dashboard.navigate(); + + await page.locator('nav button:has-text("Settings")').click(); + + // Check switches are interactive + const autoLaunchSwitch = page.getByTestId('auto-launch-switch'); + const startMinimizedSwitch = page.getByTestId('start-minimized-switch'); + const closeToTraySwitch = page.getByTestId('close-to-tray-switch'); + + await expect(autoLaunchSwitch).toBeVisible(); + await expect(startMinimizedSwitch).toBeVisible(); + await expect(closeToTraySwitch).toBeVisible(); + + // All switches should be enabled (except start-minimized might be disabled if auto-launch is off) + await expect(autoLaunchSwitch).toBeEnabled(); + await expect(closeToTraySwitch).toBeEnabled(); + }); + + test('should toggle close to tray setting', async ({ page }) => { + const dashboard = new DashboardPage(page); + await dashboard.navigate(); + + await page.locator('nav button:has-text("Settings")').click(); + + const closeToTraySwitch = page.getByTestId('close-to-tray-switch'); + + // Get initial state + const initialState = await closeToTraySwitch.getAttribute('aria-checked'); + + // Toggle the switch + await closeToTraySwitch.click(); + await page.waitForTimeout(500); // Wait for state to update + + // Verify state changed + const newState = await closeToTraySwitch.getAttribute('aria-checked'); + expect(newState).not.toBe(initialState); + }); + + test('start minimized should be disabled when auto-launch is off', async ({ page }) => { + const dashboard = new DashboardPage(page); + await dashboard.navigate(); + + await page.locator('nav button:has-text("Settings")').click(); + + const autoLaunchSwitch = page.getByTestId('auto-launch-switch'); + const startMinimizedSwitch = page.getByTestId('start-minimized-switch'); + + // Ensure auto-launch is off + const autoLaunchState = await autoLaunchSwitch.getAttribute('aria-checked'); + if (autoLaunchState === 'true') { + await autoLaunchSwitch.click(); + await page.waitForTimeout(500); + } + + // Start minimized should be disabled + await expect(startMinimizedSwitch).toBeDisabled(); + }); + }); }); From b0fa549a67f03ea68bcc0b18fc7f7285d3eda9fd Mon Sep 17 00:00:00 2001 From: Mohammod Al Amin Ashik Date: Fri, 6 Feb 2026 08:40:42 +0800 Subject: [PATCH 04/20] fix: Add extensive logging for startup settings debugging - Add console logging throughout Switch component and settings page - Improve error handling with proper state rollback - Extract handleClick to separate function for better debugging - Add step-by-step logging to trace toggle behavior - Clean up unused imports in tray.rs This will help diagnose why toggles appear non-functional. --- apps/desktop/src-tauri/src/tray.rs | 2 +- .../src/features/settings/SettingsPage.tsx | 30 ++++++++++++++----- packages/ui/src/components/common/Switch.tsx | 9 +++++- 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/apps/desktop/src-tauri/src/tray.rs b/apps/desktop/src-tauri/src/tray.rs index 7e035aae..d9795dee 100644 --- a/apps/desktop/src-tauri/src/tray.rs +++ b/apps/desktop/src-tauri/src/tray.rs @@ -6,7 +6,7 @@ //! - Quit application use tauri::{ - menu::{Menu, MenuBuilder, MenuItemBuilder, PredefinedMenuItem, SubmenuBuilder}, + menu::{Menu, MenuBuilder, SubmenuBuilder}, tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}, AppHandle, Emitter, Manager, Runtime, }; diff --git a/apps/desktop/src/features/settings/SettingsPage.tsx b/apps/desktop/src/features/settings/SettingsPage.tsx index 7bb63bbb..b1190e83 100644 --- a/apps/desktop/src/features/settings/SettingsPage.tsx +++ b/apps/desktop/src/features/settings/SettingsPage.tsx @@ -77,17 +77,24 @@ export function SettingsPage() { key: keyof StartupSettings, value: boolean ) => { + console.log(`[Settings] Updating ${key} to ${value}`); + + // Save old state for rollback + const oldSettings = { ...startupSettings }; const newSettings = { ...startupSettings, [key]: value }; + + // Update UI immediately for better UX setStartupSettings(newSettings); - setSavingSettings(true); + try { + console.log('[Settings] Invoking update_startup_settings:', newSettings); await invoke('update_startup_settings', { settings: newSettings }); - console.log('Startup settings saved:', newSettings); + console.log('[Settings] Successfully saved:', newSettings); } catch (error) { - console.error('Failed to save startup settings:', error); + console.error('[Settings] Failed to save:', error); // Revert on error - setStartupSettings(startupSettings); + setStartupSettings(oldSettings); } finally { setSavingSettings(false); } @@ -144,7 +151,10 @@ export function SettingsPage() {
updateStartupSetting('autoLaunch', checked)} + onCheckedChange={(checked) => { + console.log('Auto-launch toggled:', checked); + updateStartupSetting('autoLaunch', checked); + }} disabled={savingSettings} data-testid="auto-launch-switch" /> @@ -162,7 +172,10 @@ export function SettingsPage() {
updateStartupSetting('startMinimized', checked)} + onCheckedChange={(checked) => { + console.log('Start minimized toggled:', checked); + updateStartupSetting('startMinimized', checked); + }} disabled={savingSettings || !startupSettings.autoLaunch} data-testid="start-minimized-switch" /> @@ -180,7 +193,10 @@ export function SettingsPage() {
updateStartupSetting('closeToTray', checked)} + onCheckedChange={(checked) => { + console.log('Close to tray toggled:', checked); + updateStartupSetting('closeToTray', checked); + }} disabled={savingSettings} data-testid="close-to-tray-switch" /> diff --git a/packages/ui/src/components/common/Switch.tsx b/packages/ui/src/components/common/Switch.tsx index e163cf90..7f8370c9 100644 --- a/packages/ui/src/components/common/Switch.tsx +++ b/packages/ui/src/components/common/Switch.tsx @@ -20,13 +20,20 @@ export function Switch({ className, 'data-testid': testId, }: SwitchProps) { + const handleClick = () => { + if (!disabled) { + console.log('[Switch] Clicked, current:', checked, 'will become:', !checked); + onCheckedChange(!checked); + } + }; + return (
) : (
-
-
- +
+
+

@@ -160,9 +160,9 @@ export function SettingsPage() { />

-
-
- +
+
+

@@ -181,9 +181,9 @@ export function SettingsPage() { />

-
-
- +
+
+

diff --git a/packages/ui/src/components/common/Switch.tsx b/packages/ui/src/components/common/Switch.tsx index 7f8370c9..8abdb0e5 100644 --- a/packages/ui/src/components/common/Switch.tsx +++ b/packages/ui/src/components/common/Switch.tsx @@ -36,7 +36,7 @@ export function Switch({ onClick={handleClick} data-testid={testId} className={cn( - 'relative inline-flex h-6 w-11 shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-[rgb(var(--primary))] focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50', + 'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-[rgb(var(--primary))] focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50', checked ? 'bg-[rgb(var(--primary))]' : 'bg-surface-secondary', className )} From 4f7607514c45c21852f9b7f522e744918f103ed4 Mon Sep 17 00:00:00 2001 From: Mohammod Al Amin Ashik Date: Fri, 6 Feb 2026 08:49:44 +0800 Subject: [PATCH 06/20] fix: Improve switch visibility with solid gray colors - Replace bg-surface-secondary with bg-gray-300 (light) / bg-gray-600 (dark) - Add visible borders (gray-400/gray-500) to unchecked state - Ensures switches are always visible regardless of theme - Checked state remains with primary color and transparent border --- apps/desktop/src-tauri/src/lib.rs | 6 +++--- packages/ui/src/components/common/Switch.tsx | 6 ++++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index 9cf36dc6..b36f0833 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -526,13 +526,13 @@ pub fn run() { if let Some(main_window) = app.get_webview_window("main") { let app_handle = app.handle().clone(); let settings_repo = app_state.settings_repository.clone(); - + main_window.on_window_event(move |event| { if let tauri::WindowEvent::CloseRequested { api, .. } = event { // Check if close-to-tray is enabled let app_handle_clone = app_handle.clone(); let settings_clone = settings_repo.clone(); - + tauri::async_runtime::spawn(async move { match settings_clone.get("ui.close_to_tray").await { Ok(Some(value)) if value == "true" => { @@ -556,7 +556,7 @@ pub fn run() { } } }); - + // Always prevent default close to handle it asynchronously api.prevent_close(); } diff --git a/packages/ui/src/components/common/Switch.tsx b/packages/ui/src/components/common/Switch.tsx index 8abdb0e5..4b8cffe1 100644 --- a/packages/ui/src/components/common/Switch.tsx +++ b/packages/ui/src/components/common/Switch.tsx @@ -36,8 +36,10 @@ export function Switch({ onClick={handleClick} data-testid={testId} className={cn( - 'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-[rgb(var(--primary))] focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50', - checked ? 'bg-[rgb(var(--primary))]' : 'bg-surface-secondary', + 'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-[rgb(var(--primary))] focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50', + checked + ? 'bg-[rgb(var(--primary))] border-transparent' + : 'bg-gray-300 dark:bg-gray-600 border-gray-400 dark:border-gray-500', className )} > From 22e5f36e585ed5cfe6e3765103449cf488b2b396 Mon Sep 17 00:00:00 2001 From: Mohammod Al Amin Ashik Date: Fri, 6 Feb 2026 09:04:27 +0800 Subject: [PATCH 07/20] fix: Explicitly set tray icon to improve visibility - Add image crate dependency for PNG decoding - Load 32x32.png explicitly for system tray instead of using default - Convert PNG to RGBA format required by Tauri - This ensures proper tray icon rendering on Windows Note: For best results across all themes, consider creating a dedicated monochrome tray icon (dark icon on transparent background) --- Cargo.lock | 60 ++++++++++++++++++++++++++++-- apps/desktop/src-tauri/Cargo.toml | 1 + apps/desktop/src-tauri/src/tray.rs | 12 ++++++ 3 files changed, 69 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 131c4dc0..30e6afd3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -438,6 +438,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" +[[package]] +name = "byteorder-lite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495" + [[package]] name = "bytes" version = "1.11.1" @@ -2017,7 +2023,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3e795dff5605e0f04bff85ca41b51a96b83e80b281e96231bcaaf1ac35103371" dependencies = [ "byteorder", - "png", + "png 0.17.16", ] [[package]] @@ -2128,6 +2134,19 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "image" +version = "0.25.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6506c6c10786659413faa717ceebcb8f70731c0a60cbae39795fdf114519c1a" +dependencies = [ + "bytemuck", + "byteorder-lite", + "moxcms", + "num-traits", + "png 0.18.0", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -2536,6 +2555,7 @@ dependencies = [ "chrono", "dirs 5.0.1", "dotenvy", + "image", "keyring", "mcpmux-core", "mcpmux-gateway", @@ -2717,6 +2737,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "moxcms" +version = "0.7.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac9557c559cd6fc9867e122e20d2cbefc9ca29d80d027a8e39310920ed2f0a97" +dependencies = [ + "num-traits", + "pxfm", +] + [[package]] name = "muda" version = "0.17.1" @@ -2732,7 +2762,7 @@ dependencies = [ "objc2-core-foundation", "objc2-foundation", "once_cell", - "png", + "png 0.17.16", "serde", "thiserror 2.0.18", "windows-sys 0.60.2", @@ -3518,6 +3548,19 @@ dependencies = [ "miniz_oxide", ] +[[package]] +name = "png" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97baced388464909d42d89643fe4361939af9b7ce7a31ee32a168f832a70f2a0" +dependencies = [ + "bitflags 2.10.0", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "polling" version = "3.11.0" @@ -3654,6 +3697,15 @@ dependencies = [ "windows 0.62.2", ] +[[package]] +name = "pxfm" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7186d3822593aa4393561d186d1393b3923e9d6163d3fbfd6e825e3e6cf3e6a8" +dependencies = [ + "num-traits", +] + [[package]] name = "quick-xml" version = "0.38.4" @@ -4984,7 +5036,7 @@ dependencies = [ "ico", "json-patch", "plist", - "png", + "png 0.17.16", "proc-macro2", "quote", "semver", @@ -5707,7 +5759,7 @@ dependencies = [ "objc2-core-graphics", "objc2-foundation", "once_cell", - "png", + "png 0.17.16", "serde", "thiserror 2.0.18", "windows-sys 0.60.2", diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index 835b2f4b..efffb75a 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -21,6 +21,7 @@ tauri-plugin-single-instance = "2" tauri-plugin-deep-link = "2" tauri-plugin-updater = "2" tauri-plugin-autostart = "2" +image = { version = "0.25", default-features = false, features = ["png"] } serde.workspace = true serde_json.workspace = true tokio.workspace = true diff --git a/apps/desktop/src-tauri/src/tray.rs b/apps/desktop/src-tauri/src/tray.rs index d9795dee..a6bd4b50 100644 --- a/apps/desktop/src-tauri/src/tray.rs +++ b/apps/desktop/src-tauri/src/tray.rs @@ -6,6 +6,7 @@ //! - Quit application use tauri::{ + image::Image, menu::{Menu, MenuBuilder, SubmenuBuilder}, tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent}, AppHandle, Emitter, Manager, Runtime, @@ -34,8 +35,19 @@ pub fn setup_tray(app: &AppHandle) -> tauri::Result<()> { let menu = build_tray_menu(app)?; + // Load tray icon - decode PNG and convert to RGBA + let icon_bytes = include_bytes!("../icons/32x32.png"); + let img = image::load_from_memory(icon_bytes) + .map_err(|e| { + tauri::Error::InvalidIcon(std::io::Error::new(std::io::ErrorKind::InvalidData, e)) + })? + .to_rgba8(); + let (width, height) = img.dimensions(); + let icon = Image::new_owned(img.into_raw(), width, height); + let _tray = TrayIconBuilder::with_id("mcpmux-tray") .tooltip("McpMux - MCP Server Manager") + .icon(icon) .menu(&menu) .show_menu_on_left_click(false) .on_menu_event(move |app, event| { From a02c6545a76486a901d44a46794aca4d967da42b Mon Sep 17 00:00:00 2001 From: Mohammod Al Amin Ashik Date: Fri, 6 Feb 2026 09:58:03 +0800 Subject: [PATCH 08/20] test: Add comprehensive tests for auto-start and system tray - Add 8 Rust unit tests for StartupSettings struct - Add 12 React unit tests for Switch component - Add 13 E2E tests for startup settings functionality All unit tests passing (Rust 8/8, React 12/12) E2E tests: 21/39 passed (failures expected in web-only mode) --- .../src-tauri/src/commands/settings.rs | 96 ++++++++++ packages/ui/package.json | 3 +- .../ui/src/components/common/Switch.test.tsx | 133 +++++++++++++ packages/ui/src/components/common/Switch.tsx | 1 - packages/ui/vitest.config.ts | 19 ++ tests/e2e/specs/settings.spec.ts | 175 ++++++++++++++++++ 6 files changed, 425 insertions(+), 2 deletions(-) create mode 100644 packages/ui/src/components/common/Switch.test.tsx create mode 100644 packages/ui/vitest.config.ts diff --git a/apps/desktop/src-tauri/src/commands/settings.rs b/apps/desktop/src-tauri/src/commands/settings.rs index 51c28e52..dde5b369 100644 --- a/apps/desktop/src-tauri/src/commands/settings.rs +++ b/apps/desktop/src-tauri/src/commands/settings.rs @@ -119,3 +119,99 @@ pub fn should_start_hidden() -> bool { let args: Vec = std::env::args().collect(); args.contains(&"--hidden".to_string()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_startup_settings_default() { + let settings = StartupSettings::default(); + assert_eq!(settings.auto_launch, false); + assert_eq!(settings.start_minimized, false); + assert_eq!(settings.close_to_tray, true); + } + + #[test] + fn test_startup_settings_serialization() { + let settings = StartupSettings { + auto_launch: true, + start_minimized: false, + close_to_tray: true, + }; + + let json = serde_json::to_string(&settings).unwrap(); + assert!(json.contains("\"autoLaunch\":true")); + assert!(json.contains("\"startMinimized\":false")); + assert!(json.contains("\"closeToTray\":true")); + } + + #[test] + fn test_startup_settings_deserialization() { + let json = r#"{"autoLaunch":true,"startMinimized":true,"closeToTray":false}"#; + let settings: StartupSettings = serde_json::from_str(json).unwrap(); + + assert_eq!(settings.auto_launch, true); + assert_eq!(settings.start_minimized, true); + assert_eq!(settings.close_to_tray, false); + } + + #[test] + fn test_should_start_hidden_without_flag() { + // This test might be tricky as it depends on actual process args + // In a real test environment, we'd mock std::env::args + // For now, we just verify the function exists and can be called + let _result = should_start_hidden(); + // Can't assert the actual value since it depends on how tests are run + } + + #[test] + fn test_startup_settings_clone() { + let settings = StartupSettings { + auto_launch: true, + start_minimized: false, + close_to_tray: true, + }; + + let cloned = settings.clone(); + assert_eq!(settings.auto_launch, cloned.auto_launch); + assert_eq!(settings.start_minimized, cloned.start_minimized); + assert_eq!(settings.close_to_tray, cloned.close_to_tray); + } + + #[test] + fn test_startup_settings_debug() { + let settings = StartupSettings::default(); + let debug_str = format!("{:?}", settings); + assert!(debug_str.contains("StartupSettings")); + assert!(debug_str.contains("auto_launch")); + assert!(debug_str.contains("start_minimized")); + assert!(debug_str.contains("close_to_tray")); + } + + #[test] + fn test_startup_settings_with_all_enabled() { + let settings = StartupSettings { + auto_launch: true, + start_minimized: true, + close_to_tray: true, + }; + + assert!(settings.auto_launch); + assert!(settings.start_minimized); + assert!(settings.close_to_tray); + } + + #[test] + fn test_startup_settings_with_all_disabled() { + let settings = StartupSettings { + auto_launch: false, + start_minimized: false, + close_to_tray: false, + }; + + assert!(!settings.auto_launch); + assert!(!settings.start_minimized); + assert!(!settings.close_to_tray); + } +} diff --git a/packages/ui/package.json b/packages/ui/package.json index ea5476d2..127b23c8 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -18,7 +18,8 @@ "dev": "tsc --watch", "lint": "eslint src", "lint:fix": "eslint src --fix", - "test": "echo \"No tests yet\"" + "test": "vitest run", + "test:watch": "vitest" }, "peerDependencies": { "react": "^19.0.0", diff --git a/packages/ui/src/components/common/Switch.test.tsx b/packages/ui/src/components/common/Switch.test.tsx new file mode 100644 index 00000000..760b057e --- /dev/null +++ b/packages/ui/src/components/common/Switch.test.tsx @@ -0,0 +1,133 @@ +/** + * Tests for Switch component + */ + +import { describe, it, expect, vi } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { Switch } from './Switch'; + +describe('Switch', () => { + it('renders with unchecked state', () => { + const mockHandler = vi.fn(); + render(); + + const button = screen.getByRole('switch'); + expect(button).toBeInTheDocument(); + expect(button).toHaveAttribute('aria-checked', 'false'); + }); + + it('renders with checked state', () => { + const mockHandler = vi.fn(); + render(); + + const button = screen.getByRole('switch'); + expect(button).toHaveAttribute('aria-checked', 'true'); + }); + + it('calls onCheckedChange when clicked', () => { + const mockHandler = vi.fn(); + render(); + + const button = screen.getByRole('switch'); + fireEvent.click(button); + + expect(mockHandler).toHaveBeenCalledWith(true); + expect(mockHandler).toHaveBeenCalledTimes(1); + }); + + it('toggles from checked to unchecked', () => { + const mockHandler = vi.fn(); + render(); + + const button = screen.getByRole('switch'); + fireEvent.click(button); + + expect(mockHandler).toHaveBeenCalledWith(false); + }); + + it('does not call handler when disabled', () => { + const mockHandler = vi.fn(); + render(); + + const button = screen.getByRole('switch'); + fireEvent.click(button); + + expect(mockHandler).not.toHaveBeenCalled(); + }); + + it('applies disabled attribute when disabled', () => { + const mockHandler = vi.fn(); + render(); + + const button = screen.getByRole('switch'); + expect(button).toBeDisabled(); + }); + + it('applies custom className', () => { + const mockHandler = vi.fn(); + render(); + + const button = screen.getByRole('switch'); + expect(button).toHaveClass('custom-class'); + }); + + it('applies data-testid when provided', () => { + const mockHandler = vi.fn(); + render(); + + const button = screen.getByTestId('test-switch'); + expect(button).toBeInTheDocument(); + }); + + it('has correct styles for checked state', () => { + const mockHandler = vi.fn(); + render(); + + const button = screen.getByRole('switch'); + expect(button.className).toMatch(/bg-\[rgb\(var\(--primary\)\)\]/); + }); + + it('has correct styles for unchecked state', () => { + const mockHandler = vi.fn(); + render(); + + const button = screen.getByRole('switch'); + expect(button.className).toMatch(/bg-gray-300/); + }); + + it('has disabled styling when disabled', () => { + const mockHandler = vi.fn(); + render(); + + const button = screen.getByRole('switch'); + expect(button.className).toMatch(/opacity-50/); + }); + + it('can be toggled multiple times', () => { + const mockHandler = vi.fn(); + const { rerender } = render(); + + const button = screen.getByRole('switch'); + + // First click - should call with true + fireEvent.click(button); + expect(mockHandler).toHaveBeenCalledWith(true); + expect(mockHandler).toHaveBeenCalledTimes(1); + + // Simulate parent updating the prop + rerender(); + + // Second click - should call with false + fireEvent.click(button); + expect(mockHandler).toHaveBeenCalledWith(false); + expect(mockHandler).toHaveBeenCalledTimes(2); + + // Simulate parent updating the prop again + rerender(); + + // Third click - should call with true again + fireEvent.click(button); + expect(mockHandler).toHaveBeenCalledWith(true); + expect(mockHandler).toHaveBeenCalledTimes(3); + }); +}); diff --git a/packages/ui/src/components/common/Switch.tsx b/packages/ui/src/components/common/Switch.tsx index 4b8cffe1..7a5195b8 100644 --- a/packages/ui/src/components/common/Switch.tsx +++ b/packages/ui/src/components/common/Switch.tsx @@ -22,7 +22,6 @@ export function Switch({ }: SwitchProps) { const handleClick = () => { if (!disabled) { - console.log('[Switch] Clicked, current:', checked, 'will become:', !checked); onCheckedChange(!checked); } }; diff --git a/packages/ui/vitest.config.ts b/packages/ui/vitest.config.ts new file mode 100644 index 00000000..3302cbce --- /dev/null +++ b/packages/ui/vitest.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from 'vitest/config'; +import react from '@vitejs/plugin-react'; +import path from 'path'; + +export default defineConfig({ + plugins: [react()], + test: { + environment: 'jsdom', + globals: true, + setupFiles: [path.resolve(__dirname, '../../tests/ts/setup.ts')], + include: ['src/**/*.test.{ts,tsx}'], + exclude: ['**/node_modules/**', 'dist/**'], + }, + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, + }, +}); diff --git a/tests/e2e/specs/settings.spec.ts b/tests/e2e/specs/settings.spec.ts index 8246efdf..d32c6a5e 100644 --- a/tests/e2e/specs/settings.spec.ts +++ b/tests/e2e/specs/settings.spec.ts @@ -233,6 +233,18 @@ test.describe('Settings', () => { await expect(page.getByText('Close to Tray')).toBeVisible(); }); + test('should display descriptive text for each setting', async ({ page }) => { + const dashboard = new DashboardPage(page); + await dashboard.navigate(); + + await page.locator('nav button:has-text("Settings")').click(); + + // Check descriptions + await expect(page.getByText(/Start McpMux automatically when you log in/)).toBeVisible(); + await expect(page.getByText(/Launch in background to system tray/)).toBeVisible(); + await expect(page.getByText(/Keep running in system tray when window is closed/)).toBeVisible(); + }); + test('should have functional toggle switches', async ({ page }) => { const dashboard = new DashboardPage(page); await dashboard.navigate(); @@ -253,6 +265,33 @@ test.describe('Settings', () => { await expect(closeToTraySwitch).toBeEnabled(); }); + test('should toggle auto-launch setting', async ({ page }) => { + const dashboard = new DashboardPage(page); + await dashboard.navigate(); + + await page.locator('nav button:has-text("Settings")').click(); + + const autoLaunchSwitch = page.getByTestId('auto-launch-switch'); + + // Get initial state + const initialState = await autoLaunchSwitch.getAttribute('aria-checked'); + + // Toggle the switch + await autoLaunchSwitch.click(); + await page.waitForTimeout(500); // Wait for backend to process + + // Verify state changed + const newState = await autoLaunchSwitch.getAttribute('aria-checked'); + expect(newState).not.toBe(initialState); + + // Toggle back to original state + await autoLaunchSwitch.click(); + await page.waitForTimeout(500); + + const finalState = await autoLaunchSwitch.getAttribute('aria-checked'); + expect(finalState).toBe(initialState); + }); + test('should toggle close to tray setting', async ({ page }) => { const dashboard = new DashboardPage(page); await dashboard.navigate(); @@ -271,6 +310,13 @@ test.describe('Settings', () => { // Verify state changed const newState = await closeToTraySwitch.getAttribute('aria-checked'); expect(newState).not.toBe(initialState); + + // Toggle back + await closeToTraySwitch.click(); + await page.waitForTimeout(500); + + const finalState = await closeToTraySwitch.getAttribute('aria-checked'); + expect(finalState).toBe(initialState); }); test('start minimized should be disabled when auto-launch is off', async ({ page }) => { @@ -291,6 +337,135 @@ test.describe('Settings', () => { // Start minimized should be disabled await expect(startMinimizedSwitch).toBeDisabled(); + await expect(startMinimizedSwitch).toHaveAttribute('aria-checked', 'false'); + }); + + test('start minimized should be enabled when auto-launch is on', async ({ page }) => { + const dashboard = new DashboardPage(page); + await dashboard.navigate(); + + await page.locator('nav button:has-text("Settings")').click(); + + const autoLaunchSwitch = page.getByTestId('auto-launch-switch'); + const startMinimizedSwitch = page.getByTestId('start-minimized-switch'); + + // Ensure auto-launch is on + const autoLaunchState = await autoLaunchSwitch.getAttribute('aria-checked'); + if (autoLaunchState === 'false') { + await autoLaunchSwitch.click(); + await page.waitForTimeout(500); + } + + // Start minimized should be enabled + await expect(startMinimizedSwitch).toBeEnabled(); + }); + + test('should toggle start minimized when enabled', async ({ page }) => { + const dashboard = new DashboardPage(page); + await dashboard.navigate(); + + await page.locator('nav button:has-text("Settings")').click(); + + const autoLaunchSwitch = page.getByTestId('auto-launch-switch'); + const startMinimizedSwitch = page.getByTestId('start-minimized-switch'); + + // Ensure auto-launch is on first + const autoLaunchState = await autoLaunchSwitch.getAttribute('aria-checked'); + if (autoLaunchState === 'false') { + await autoLaunchSwitch.click(); + await page.waitForTimeout(500); + } + + // Get initial state + const initialState = await startMinimizedSwitch.getAttribute('aria-checked'); + + // Toggle the switch + await startMinimizedSwitch.click(); + await page.waitForTimeout(500); + + // Verify state changed + const newState = await startMinimizedSwitch.getAttribute('aria-checked'); + expect(newState).not.toBe(initialState); + + // Toggle back + await startMinimizedSwitch.click(); + await page.waitForTimeout(500); + + const finalState = await startMinimizedSwitch.getAttribute('aria-checked'); + expect(finalState).toBe(initialState); + }); + + test('should persist settings across page reloads', async ({ page }) => { + const dashboard = new DashboardPage(page); + await dashboard.navigate(); + + await page.locator('nav button:has-text("Settings")').click(); + + const closeToTraySwitch = page.getByTestId('close-to-tray-switch'); + + // Get initial state + const initialState = await closeToTraySwitch.getAttribute('aria-checked'); + + // Toggle the switch + await closeToTraySwitch.click(); + await page.waitForTimeout(500); + + // Reload the page + await page.reload(); + await page.waitForLoadState('networkidle'); + + // Navigate to settings again + await page.locator('nav button:has-text("Settings")').click(); + + // Verify state persisted + const persistedState = await closeToTraySwitch.getAttribute('aria-checked'); + expect(persistedState).not.toBe(initialState); + + // Restore original state + await closeToTraySwitch.click(); + await page.waitForTimeout(500); + }); + + test('should show disabled state visually for start minimized', async ({ page }) => { + const dashboard = new DashboardPage(page); + await dashboard.navigate(); + + await page.locator('nav button:has-text("Settings")').click(); + + const autoLaunchSwitch = page.getByTestId('auto-launch-switch'); + const startMinimizedSwitch = page.getByTestId('start-minimized-switch'); + + // Ensure auto-launch is off + const autoLaunchState = await autoLaunchSwitch.getAttribute('aria-checked'); + if (autoLaunchState === 'true') { + await autoLaunchSwitch.click(); + await page.waitForTimeout(500); + } + + // Check that start minimized has disabled styling + await expect(startMinimizedSwitch).toHaveClass(/opacity-50/); + }); + + test('all settings should work independently', async ({ page }) => { + const dashboard = new DashboardPage(page); + await dashboard.navigate(); + + await page.locator('nav button:has-text("Settings")').click(); + + const closeToTraySwitch = page.getByTestId('close-to-tray-switch'); + + // Close to tray should work regardless of auto-launch state + const initialCloseToTray = await closeToTraySwitch.getAttribute('aria-checked'); + + await closeToTraySwitch.click(); + await page.waitForTimeout(500); + + const newCloseToTray = await closeToTraySwitch.getAttribute('aria-checked'); + expect(newCloseToTray).not.toBe(initialCloseToTray); + + // Restore + await closeToTraySwitch.click(); + await page.waitForTimeout(500); }); }); }); From 52ec9de79506a8e86c8cc33332df825d1fc2b170 Mon Sep 17 00:00:00 2001 From: Mohammod Al Amin Ashik Date: Fri, 6 Feb 2026 10:01:29 +0800 Subject: [PATCH 09/20] refactor: Move desktop-specific E2E tests to WebdriverIO - Create settings-desktop.wdio.ts for Tauri-specific tests - Remove desktop tests from settings.spec.ts (Playwright web-only) - Desktop tests require Tauri backend (auto-start, system tray) - Web tests remain for UI/layout verification only This fixes E2E test failures in web mode by properly separating desktop integration tests from web-only UI tests. --- tests/e2e/specs/settings-desktop.wdio.ts | 227 ++++++++++++++++++++ tests/e2e/specs/settings.spec.ts | 260 ----------------------- 2 files changed, 227 insertions(+), 260 deletions(-) create mode 100644 tests/e2e/specs/settings-desktop.wdio.ts diff --git a/tests/e2e/specs/settings-desktop.wdio.ts b/tests/e2e/specs/settings-desktop.wdio.ts new file mode 100644 index 00000000..41b53fc5 --- /dev/null +++ b/tests/e2e/specs/settings-desktop.wdio.ts @@ -0,0 +1,227 @@ +/** + * Desktop-only E2E tests for Settings (requires Tauri backend) + * Run with: pnpm test:e2e --spec tests/e2e/specs/settings-desktop.wdio.ts + */ + +import { expect, browser } from '@wdio/globals'; + +describe('Settings - Desktop Features', () => { + beforeEach(async () => { + // Navigate to settings page + const dashboardBtn = await $('nav button[data-testid="nav-dashboard"]'); + await dashboardBtn.waitForClickable(); + await dashboardBtn.click(); + + const settingsBtn = await $('nav button[data-testid="nav-settings"]'); + await settingsBtn.waitForClickable(); + await settingsBtn.click(); + + // Wait for settings page to load + await browser.pause(500); + }); + + describe('Startup & System Tray Settings', () => { + it('should display startup settings section', async () => { + const heading = await $('h3*=Startup & System Tray'); + await expect(heading).toBeDisplayed(); + + const description = await $('p*=Control how McpMux starts'); + await expect(description).toBeDisplayed(); + }); + + it('should display all three startup toggles', async () => { + const autoLaunchLabel = await $('label*=Launch at Startup'); + await expect(autoLaunchLabel).toBeDisplayed(); + + const startMinimizedLabel = await $('label*=Start Minimized'); + await expect(startMinimizedLabel).toBeDisplayed(); + + const closeToTrayLabel = await $('label*=Close to Tray'); + await expect(closeToTrayLabel).toBeDisplayed(); + }); + + it('should display descriptive text for each setting', async () => { + const autoLaunchDesc = await $('p*=Start McpMux automatically'); + await expect(autoLaunchDesc).toBeDisplayed(); + + const startMinimizedDesc = await $('p*=Launch in background'); + await expect(startMinimizedDesc).toBeDisplayed(); + + const closeToTrayDesc = await $('p*=Keep running in system tray'); + await expect(closeToTrayDesc).toBeDisplayed(); + }); + + it('should have functional toggle switches', async () => { + const autoLaunchSwitch = await $('[data-testid="auto-launch-switch"]'); + await expect(autoLaunchSwitch).toBeDisplayed(); + await expect(autoLaunchSwitch).toBeEnabled(); + + const closeToTraySwitch = await $('[data-testid="close-to-tray-switch"]'); + await expect(closeToTraySwitch).toBeDisplayed(); + await expect(closeToTraySwitch).toBeEnabled(); + + const startMinimizedSwitch = await $('[data-testid="start-minimized-switch"]'); + await expect(startMinimizedSwitch).toBeDisplayed(); + }); + + it('should toggle auto-launch setting', async () => { + const autoLaunchSwitch = await $('[data-testid="auto-launch-switch"]'); + + const initialState = await autoLaunchSwitch.getAttribute('aria-checked'); + + await autoLaunchSwitch.click(); + await browser.pause(500); + + const newState = await autoLaunchSwitch.getAttribute('aria-checked'); + expect(newState).not.toBe(initialState); + + // Toggle back + await autoLaunchSwitch.click(); + await browser.pause(500); + + const finalState = await autoLaunchSwitch.getAttribute('aria-checked'); + expect(finalState).toBe(initialState); + }); + + it('should toggle close to tray setting', async () => { + const closeToTraySwitch = await $('[data-testid="close-to-tray-switch"]'); + + const initialState = await closeToTraySwitch.getAttribute('aria-checked'); + + await closeToTraySwitch.click(); + await browser.pause(500); + + const newState = await closeToTraySwitch.getAttribute('aria-checked'); + expect(newState).not.toBe(initialState); + + // Toggle back + await closeToTraySwitch.click(); + await browser.pause(500); + + const finalState = await closeToTraySwitch.getAttribute('aria-checked'); + expect(finalState).toBe(initialState); + }); + + it('start minimized should be disabled when auto-launch is off', async () => { + const autoLaunchSwitch = await $('[data-testid="auto-launch-switch"]'); + const startMinimizedSwitch = await $('[data-testid="start-minimized-switch"]'); + + // Ensure auto-launch is off + const autoLaunchState = await autoLaunchSwitch.getAttribute('aria-checked'); + if (autoLaunchState === 'true') { + await autoLaunchSwitch.click(); + await browser.pause(500); + } + + // Start minimized should be disabled + const isDisabled = await startMinimizedSwitch.getAttribute('disabled'); + expect(isDisabled).toBe('true'); + + const ariaChecked = await startMinimizedSwitch.getAttribute('aria-checked'); + expect(ariaChecked).toBe('false'); + }); + + it('start minimized should be enabled when auto-launch is on', async () => { + const autoLaunchSwitch = await $('[data-testid="auto-launch-switch"]'); + const startMinimizedSwitch = await $('[data-testid="start-minimized-switch"]'); + + // Ensure auto-launch is on + const autoLaunchState = await autoLaunchSwitch.getAttribute('aria-checked'); + if (autoLaunchState === 'false') { + await autoLaunchSwitch.click(); + await browser.pause(500); + } + + // Start minimized should be enabled + const isDisabled = await startMinimizedSwitch.getAttribute('disabled'); + expect(isDisabled).toBeNull(); + }); + + it('should toggle start minimized when enabled', async () => { + const autoLaunchSwitch = await $('[data-testid="auto-launch-switch"]'); + const startMinimizedSwitch = await $('[data-testid="start-minimized-switch"]'); + + // Ensure auto-launch is on first + const autoLaunchState = await autoLaunchSwitch.getAttribute('aria-checked'); + if (autoLaunchState === 'false') { + await autoLaunchSwitch.click(); + await browser.pause(500); + } + + const initialState = await startMinimizedSwitch.getAttribute('aria-checked'); + + await startMinimizedSwitch.click(); + await browser.pause(500); + + const newState = await startMinimizedSwitch.getAttribute('aria-checked'); + expect(newState).not.toBe(initialState); + + // Toggle back + await startMinimizedSwitch.click(); + await browser.pause(500); + + const finalState = await startMinimizedSwitch.getAttribute('aria-checked'); + expect(finalState).toBe(initialState); + }); + + it('should persist settings across page reloads', async () => { + const closeToTraySwitch = await $('[data-testid="close-to-tray-switch"]'); + + const initialState = await closeToTraySwitch.getAttribute('aria-checked'); + + await closeToTraySwitch.click(); + await browser.pause(500); + + // Reload the page + await browser.refresh(); + await browser.pause(1000); + + // Navigate to settings again + const settingsBtn = await $('nav button[data-testid="nav-settings"]'); + await settingsBtn.waitForClickable(); + await settingsBtn.click(); + await browser.pause(500); + + // Verify state persisted + const persistedState = await closeToTraySwitch.getAttribute('aria-checked'); + expect(persistedState).not.toBe(initialState); + + // Restore original state + await closeToTraySwitch.click(); + await browser.pause(500); + }); + + it('should show disabled state visually for start minimized', async () => { + const autoLaunchSwitch = await $('[data-testid="auto-launch-switch"]'); + const startMinimizedSwitch = await $('[data-testid="start-minimized-switch"]'); + + // Ensure auto-launch is off + const autoLaunchState = await autoLaunchSwitch.getAttribute('aria-checked'); + if (autoLaunchState === 'true') { + await autoLaunchSwitch.click(); + await browser.pause(500); + } + + // Check that start minimized has disabled styling + const className = await startMinimizedSwitch.getAttribute('class'); + expect(className).toContain('opacity-50'); + }); + + it('all settings should work independently', async () => { + const closeToTraySwitch = await $('[data-testid="close-to-tray-switch"]'); + + // Close to tray should work regardless of auto-launch state + const initialCloseToTray = await closeToTraySwitch.getAttribute('aria-checked'); + + await closeToTraySwitch.click(); + await browser.pause(500); + + const newCloseToTray = await closeToTraySwitch.getAttribute('aria-checked'); + expect(newCloseToTray).not.toBe(initialCloseToTray); + + // Restore + await closeToTraySwitch.click(); + await browser.pause(500); + }); + }); +}); diff --git a/tests/e2e/specs/settings.spec.ts b/tests/e2e/specs/settings.spec.ts index d32c6a5e..c4afef82 100644 --- a/tests/e2e/specs/settings.spec.ts +++ b/tests/e2e/specs/settings.spec.ts @@ -208,264 +208,4 @@ test.describe('Settings', () => { await expect(mainContent).toBeVisible(); }); }); - - test.describe('Startup & System Tray Settings', () => { - test('should display startup settings section', async ({ page }) => { - const dashboard = new DashboardPage(page); - await dashboard.navigate(); - - await page.locator('nav button:has-text("Settings")').click(); - - // Check for startup settings card - await expect(page.getByText('Startup & System Tray')).toBeVisible(); - await expect(page.getByText(/Control how McpMux starts/)).toBeVisible(); - }); - - test('should display all three startup toggles', async ({ page }) => { - const dashboard = new DashboardPage(page); - await dashboard.navigate(); - - await page.locator('nav button:has-text("Settings")').click(); - - // Check all three settings exist - await expect(page.getByText('Launch at Startup')).toBeVisible(); - await expect(page.getByText('Start Minimized')).toBeVisible(); - await expect(page.getByText('Close to Tray')).toBeVisible(); - }); - - test('should display descriptive text for each setting', async ({ page }) => { - const dashboard = new DashboardPage(page); - await dashboard.navigate(); - - await page.locator('nav button:has-text("Settings")').click(); - - // Check descriptions - await expect(page.getByText(/Start McpMux automatically when you log in/)).toBeVisible(); - await expect(page.getByText(/Launch in background to system tray/)).toBeVisible(); - await expect(page.getByText(/Keep running in system tray when window is closed/)).toBeVisible(); - }); - - test('should have functional toggle switches', async ({ page }) => { - const dashboard = new DashboardPage(page); - await dashboard.navigate(); - - await page.locator('nav button:has-text("Settings")').click(); - - // Check switches are interactive - const autoLaunchSwitch = page.getByTestId('auto-launch-switch'); - const startMinimizedSwitch = page.getByTestId('start-minimized-switch'); - const closeToTraySwitch = page.getByTestId('close-to-tray-switch'); - - await expect(autoLaunchSwitch).toBeVisible(); - await expect(startMinimizedSwitch).toBeVisible(); - await expect(closeToTraySwitch).toBeVisible(); - - // All switches should be enabled (except start-minimized might be disabled if auto-launch is off) - await expect(autoLaunchSwitch).toBeEnabled(); - await expect(closeToTraySwitch).toBeEnabled(); - }); - - test('should toggle auto-launch setting', async ({ page }) => { - const dashboard = new DashboardPage(page); - await dashboard.navigate(); - - await page.locator('nav button:has-text("Settings")').click(); - - const autoLaunchSwitch = page.getByTestId('auto-launch-switch'); - - // Get initial state - const initialState = await autoLaunchSwitch.getAttribute('aria-checked'); - - // Toggle the switch - await autoLaunchSwitch.click(); - await page.waitForTimeout(500); // Wait for backend to process - - // Verify state changed - const newState = await autoLaunchSwitch.getAttribute('aria-checked'); - expect(newState).not.toBe(initialState); - - // Toggle back to original state - await autoLaunchSwitch.click(); - await page.waitForTimeout(500); - - const finalState = await autoLaunchSwitch.getAttribute('aria-checked'); - expect(finalState).toBe(initialState); - }); - - test('should toggle close to tray setting', async ({ page }) => { - const dashboard = new DashboardPage(page); - await dashboard.navigate(); - - await page.locator('nav button:has-text("Settings")').click(); - - const closeToTraySwitch = page.getByTestId('close-to-tray-switch'); - - // Get initial state - const initialState = await closeToTraySwitch.getAttribute('aria-checked'); - - // Toggle the switch - await closeToTraySwitch.click(); - await page.waitForTimeout(500); // Wait for state to update - - // Verify state changed - const newState = await closeToTraySwitch.getAttribute('aria-checked'); - expect(newState).not.toBe(initialState); - - // Toggle back - await closeToTraySwitch.click(); - await page.waitForTimeout(500); - - const finalState = await closeToTraySwitch.getAttribute('aria-checked'); - expect(finalState).toBe(initialState); - }); - - test('start minimized should be disabled when auto-launch is off', async ({ page }) => { - const dashboard = new DashboardPage(page); - await dashboard.navigate(); - - await page.locator('nav button:has-text("Settings")').click(); - - const autoLaunchSwitch = page.getByTestId('auto-launch-switch'); - const startMinimizedSwitch = page.getByTestId('start-minimized-switch'); - - // Ensure auto-launch is off - const autoLaunchState = await autoLaunchSwitch.getAttribute('aria-checked'); - if (autoLaunchState === 'true') { - await autoLaunchSwitch.click(); - await page.waitForTimeout(500); - } - - // Start minimized should be disabled - await expect(startMinimizedSwitch).toBeDisabled(); - await expect(startMinimizedSwitch).toHaveAttribute('aria-checked', 'false'); - }); - - test('start minimized should be enabled when auto-launch is on', async ({ page }) => { - const dashboard = new DashboardPage(page); - await dashboard.navigate(); - - await page.locator('nav button:has-text("Settings")').click(); - - const autoLaunchSwitch = page.getByTestId('auto-launch-switch'); - const startMinimizedSwitch = page.getByTestId('start-minimized-switch'); - - // Ensure auto-launch is on - const autoLaunchState = await autoLaunchSwitch.getAttribute('aria-checked'); - if (autoLaunchState === 'false') { - await autoLaunchSwitch.click(); - await page.waitForTimeout(500); - } - - // Start minimized should be enabled - await expect(startMinimizedSwitch).toBeEnabled(); - }); - - test('should toggle start minimized when enabled', async ({ page }) => { - const dashboard = new DashboardPage(page); - await dashboard.navigate(); - - await page.locator('nav button:has-text("Settings")').click(); - - const autoLaunchSwitch = page.getByTestId('auto-launch-switch'); - const startMinimizedSwitch = page.getByTestId('start-minimized-switch'); - - // Ensure auto-launch is on first - const autoLaunchState = await autoLaunchSwitch.getAttribute('aria-checked'); - if (autoLaunchState === 'false') { - await autoLaunchSwitch.click(); - await page.waitForTimeout(500); - } - - // Get initial state - const initialState = await startMinimizedSwitch.getAttribute('aria-checked'); - - // Toggle the switch - await startMinimizedSwitch.click(); - await page.waitForTimeout(500); - - // Verify state changed - const newState = await startMinimizedSwitch.getAttribute('aria-checked'); - expect(newState).not.toBe(initialState); - - // Toggle back - await startMinimizedSwitch.click(); - await page.waitForTimeout(500); - - const finalState = await startMinimizedSwitch.getAttribute('aria-checked'); - expect(finalState).toBe(initialState); - }); - - test('should persist settings across page reloads', async ({ page }) => { - const dashboard = new DashboardPage(page); - await dashboard.navigate(); - - await page.locator('nav button:has-text("Settings")').click(); - - const closeToTraySwitch = page.getByTestId('close-to-tray-switch'); - - // Get initial state - const initialState = await closeToTraySwitch.getAttribute('aria-checked'); - - // Toggle the switch - await closeToTraySwitch.click(); - await page.waitForTimeout(500); - - // Reload the page - await page.reload(); - await page.waitForLoadState('networkidle'); - - // Navigate to settings again - await page.locator('nav button:has-text("Settings")').click(); - - // Verify state persisted - const persistedState = await closeToTraySwitch.getAttribute('aria-checked'); - expect(persistedState).not.toBe(initialState); - - // Restore original state - await closeToTraySwitch.click(); - await page.waitForTimeout(500); - }); - - test('should show disabled state visually for start minimized', async ({ page }) => { - const dashboard = new DashboardPage(page); - await dashboard.navigate(); - - await page.locator('nav button:has-text("Settings")').click(); - - const autoLaunchSwitch = page.getByTestId('auto-launch-switch'); - const startMinimizedSwitch = page.getByTestId('start-minimized-switch'); - - // Ensure auto-launch is off - const autoLaunchState = await autoLaunchSwitch.getAttribute('aria-checked'); - if (autoLaunchState === 'true') { - await autoLaunchSwitch.click(); - await page.waitForTimeout(500); - } - - // Check that start minimized has disabled styling - await expect(startMinimizedSwitch).toHaveClass(/opacity-50/); - }); - - test('all settings should work independently', async ({ page }) => { - const dashboard = new DashboardPage(page); - await dashboard.navigate(); - - await page.locator('nav button:has-text("Settings")').click(); - - const closeToTraySwitch = page.getByTestId('close-to-tray-switch'); - - // Close to tray should work regardless of auto-launch state - const initialCloseToTray = await closeToTraySwitch.getAttribute('aria-checked'); - - await closeToTraySwitch.click(); - await page.waitForTimeout(500); - - const newCloseToTray = await closeToTraySwitch.getAttribute('aria-checked'); - expect(newCloseToTray).not.toBe(initialCloseToTray); - - // Restore - await closeToTraySwitch.click(); - await page.waitForTimeout(500); - }); - }); }); From ccfc890f28370017ee0c3d8a2f119898f9504108 Mon Sep 17 00:00:00 2001 From: Mohammod Al Amin Ashik Date: Fri, 6 Feb 2026 11:28:43 +0800 Subject: [PATCH 10/20] ci: Add pre-commit hooks for Rust and TypeScript validation - Create pre-commit hook that validates before each commit - Runs cargo check for Rust files - Runs pnpm typecheck for TypeScript files - Auto-formats Rust code with cargo fmt - Add validate script: pnpm validate - Add prepare script to set hook permissions - Document git hooks setup in .github/GIT_HOOKS.md This prevents CI failures by catching issues locally. --- .github/GIT_HOOKS.md | 83 ++++ apps/desktop/src-tauri/Cargo.toml | 5 +- package.json | 2 + .../ui/src/components/common/Switch.test.tsx | 246 +++++------ packages/ui/src/components/common/Switch.tsx | 4 +- packages/ui/vitest.config.ts | 24 +- tests/e2e/specs/settings-desktop.wdio.ts | 410 +++++++++--------- 7 files changed, 430 insertions(+), 344 deletions(-) create mode 100644 .github/GIT_HOOKS.md diff --git a/.github/GIT_HOOKS.md b/.github/GIT_HOOKS.md new file mode 100644 index 00000000..fe748b0c --- /dev/null +++ b/.github/GIT_HOOKS.md @@ -0,0 +1,83 @@ +# Git Hooks + +This project uses Git hooks to maintain code quality and prevent CI failures. + +## Pre-commit Hook + +The pre-commit hook automatically runs before each commit to validate: + +### ✅ **Rust Validation** +- Formats Rust code with `cargo fmt` +- Runs `cargo check --workspace` to verify compilation +- Automatically adds formatted files to the commit + +### ✅ **TypeScript Validation** +- Runs `pnpm typecheck` to verify type correctness +- Checks all TypeScript files in the workspace + +## Setup + +The hook is automatically set up when you: +1. Clone the repository +2. Run `pnpm install` (triggers `prepare` script) + +### Manual Setup + +If the hook isn't working, run: + +```bash +# Make hook executable (Linux/Mac) +chmod +x .git/hooks/pre-commit + +# Windows (PowerShell) +icacls ".git\hooks\pre-commit" /grant Everyone:RX +``` + +## Testing the Hook + +To test the validation without committing: + +```bash +# Run both checks +pnpm validate + +# Or individually +cargo check --workspace +pnpm typecheck +``` + +## Bypassing the Hook (Not Recommended) + +In rare cases where you need to bypass validation: + +```bash +git commit --no-verify -m "your message" +``` + +**Note**: Only use `--no-verify` when absolutely necessary, as it skips important validations that prevent CI failures. + +## Troubleshooting + +### Hook not running +- Ensure `.git/hooks/pre-commit` exists +- Check it's executable: `ls -la .git/hooks/pre-commit` +- Try manual setup commands above + +### Hook fails on Windows +- The hook uses bash scripting (requires Git Bash or WSL) +- Alternatively, use the PowerShell version: `.git/hooks/pre-commit.ps1` +- Configure Git to use PowerShell hooks: + ```powershell + git config core.hooksPath .git/hooks + ``` + +### Slow validation +- The hook only validates files in your commit (staged changes) +- If you have many Rust crates, consider using `cargo check -p ` +- TypeScript check runs on entire workspace (necessary for type consistency) + +## CI Integration + +These same checks run in CI: +- GitHub Actions runs `cargo check` and `pnpm typecheck` +- Pre-commit hooks help catch issues early before pushing diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index efffb75a..c4696e54 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -40,11 +40,12 @@ url.workspace = true urlencoding = "2.1" open = "5.3" dotenvy.workspace = true -notify = { version = "7", default-features = false, features = ["macos_fsevent"] } +notify = { version = "7", default-features = false, features = [ + "macos_fsevent", +] } notify-debouncer-mini = "0.5" # Internal crates (path-only, no version needed) mcpmux-core.workspace = true mcpmux-gateway.workspace = true mcpmux-storage.workspace = true - diff --git a/package.json b/package.json index 68b76425..99099746 100644 --- a/package.json +++ b/package.json @@ -5,6 +5,7 @@ "type": "module", "scripts": { "setup": "pwsh -ExecutionPolicy Bypass -File scripts/setup-dev.ps1", + "prepare": "node -e \"try{require('fs').chmodSync('.git/hooks/pre-commit',0o755)}catch(e){}\"", "dev": "pnpm --filter @mcpmux/desktop dev", "dev:web": "pnpm --filter @mcpmux/desktop dev:web", "build": "pnpm --filter @mcpmux/desktop build", @@ -30,6 +31,7 @@ "format": "prettier --write . && cargo fmt --all", "format:check": "prettier --check . && cargo fmt --all --check", "typecheck": "pnpm -r typecheck", + "validate": "cargo check --workspace && pnpm typecheck", "clean": "pnpm -r clean && cargo clean" }, "devDependencies": { diff --git a/packages/ui/src/components/common/Switch.test.tsx b/packages/ui/src/components/common/Switch.test.tsx index 760b057e..f7573852 100644 --- a/packages/ui/src/components/common/Switch.test.tsx +++ b/packages/ui/src/components/common/Switch.test.tsx @@ -7,127 +7,127 @@ import { render, screen, fireEvent } from '@testing-library/react'; import { Switch } from './Switch'; describe('Switch', () => { - it('renders with unchecked state', () => { - const mockHandler = vi.fn(); - render(); - - const button = screen.getByRole('switch'); - expect(button).toBeInTheDocument(); - expect(button).toHaveAttribute('aria-checked', 'false'); - }); - - it('renders with checked state', () => { - const mockHandler = vi.fn(); - render(); - - const button = screen.getByRole('switch'); - expect(button).toHaveAttribute('aria-checked', 'true'); - }); - - it('calls onCheckedChange when clicked', () => { - const mockHandler = vi.fn(); - render(); - - const button = screen.getByRole('switch'); - fireEvent.click(button); - - expect(mockHandler).toHaveBeenCalledWith(true); - expect(mockHandler).toHaveBeenCalledTimes(1); - }); - - it('toggles from checked to unchecked', () => { - const mockHandler = vi.fn(); - render(); - - const button = screen.getByRole('switch'); - fireEvent.click(button); - - expect(mockHandler).toHaveBeenCalledWith(false); - }); - - it('does not call handler when disabled', () => { - const mockHandler = vi.fn(); - render(); - - const button = screen.getByRole('switch'); - fireEvent.click(button); - - expect(mockHandler).not.toHaveBeenCalled(); - }); - - it('applies disabled attribute when disabled', () => { - const mockHandler = vi.fn(); - render(); - - const button = screen.getByRole('switch'); - expect(button).toBeDisabled(); - }); - - it('applies custom className', () => { - const mockHandler = vi.fn(); - render(); - - const button = screen.getByRole('switch'); - expect(button).toHaveClass('custom-class'); - }); - - it('applies data-testid when provided', () => { - const mockHandler = vi.fn(); - render(); - - const button = screen.getByTestId('test-switch'); - expect(button).toBeInTheDocument(); - }); - - it('has correct styles for checked state', () => { - const mockHandler = vi.fn(); - render(); - - const button = screen.getByRole('switch'); - expect(button.className).toMatch(/bg-\[rgb\(var\(--primary\)\)\]/); - }); - - it('has correct styles for unchecked state', () => { - const mockHandler = vi.fn(); - render(); - - const button = screen.getByRole('switch'); - expect(button.className).toMatch(/bg-gray-300/); - }); - - it('has disabled styling when disabled', () => { - const mockHandler = vi.fn(); - render(); - - const button = screen.getByRole('switch'); - expect(button.className).toMatch(/opacity-50/); - }); - - it('can be toggled multiple times', () => { - const mockHandler = vi.fn(); - const { rerender } = render(); - - const button = screen.getByRole('switch'); - - // First click - should call with true - fireEvent.click(button); - expect(mockHandler).toHaveBeenCalledWith(true); - expect(mockHandler).toHaveBeenCalledTimes(1); - - // Simulate parent updating the prop - rerender(); - - // Second click - should call with false - fireEvent.click(button); - expect(mockHandler).toHaveBeenCalledWith(false); - expect(mockHandler).toHaveBeenCalledTimes(2); - - // Simulate parent updating the prop again - rerender(); - - // Third click - should call with true again - fireEvent.click(button); - expect(mockHandler).toHaveBeenCalledWith(true); - expect(mockHandler).toHaveBeenCalledTimes(3); - }); + it('renders with unchecked state', () => { + const mockHandler = vi.fn(); + render(); + + const button = screen.getByRole('switch'); + expect(button).toBeInTheDocument(); + expect(button).toHaveAttribute('aria-checked', 'false'); + }); + + it('renders with checked state', () => { + const mockHandler = vi.fn(); + render(); + + const button = screen.getByRole('switch'); + expect(button).toHaveAttribute('aria-checked', 'true'); + }); + + it('calls onCheckedChange when clicked', () => { + const mockHandler = vi.fn(); + render(); + + const button = screen.getByRole('switch'); + fireEvent.click(button); + + expect(mockHandler).toHaveBeenCalledWith(true); + expect(mockHandler).toHaveBeenCalledTimes(1); + }); + + it('toggles from checked to unchecked', () => { + const mockHandler = vi.fn(); + render(); + + const button = screen.getByRole('switch'); + fireEvent.click(button); + + expect(mockHandler).toHaveBeenCalledWith(false); + }); + + it('does not call handler when disabled', () => { + const mockHandler = vi.fn(); + render(); + + const button = screen.getByRole('switch'); + fireEvent.click(button); + + expect(mockHandler).not.toHaveBeenCalled(); + }); + + it('applies disabled attribute when disabled', () => { + const mockHandler = vi.fn(); + render(); + + const button = screen.getByRole('switch'); + expect(button).toBeDisabled(); + }); + + it('applies custom className', () => { + const mockHandler = vi.fn(); + render(); + + const button = screen.getByRole('switch'); + expect(button).toHaveClass('custom-class'); + }); + + it('applies data-testid when provided', () => { + const mockHandler = vi.fn(); + render(); + + const button = screen.getByTestId('test-switch'); + expect(button).toBeInTheDocument(); + }); + + it('has correct styles for checked state', () => { + const mockHandler = vi.fn(); + render(); + + const button = screen.getByRole('switch'); + expect(button.className).toMatch(/bg-\[rgb\(var\(--primary\)\)\]/); + }); + + it('has correct styles for unchecked state', () => { + const mockHandler = vi.fn(); + render(); + + const button = screen.getByRole('switch'); + expect(button.className).toMatch(/bg-gray-300/); + }); + + it('has disabled styling when disabled', () => { + const mockHandler = vi.fn(); + render(); + + const button = screen.getByRole('switch'); + expect(button.className).toMatch(/opacity-50/); + }); + + it('can be toggled multiple times', () => { + const mockHandler = vi.fn(); + const { rerender } = render(); + + const button = screen.getByRole('switch'); + + // First click - should call with true + fireEvent.click(button); + expect(mockHandler).toHaveBeenCalledWith(true); + expect(mockHandler).toHaveBeenCalledTimes(1); + + // Simulate parent updating the prop + rerender(); + + // Second click - should call with false + fireEvent.click(button); + expect(mockHandler).toHaveBeenCalledWith(false); + expect(mockHandler).toHaveBeenCalledTimes(2); + + // Simulate parent updating the prop again + rerender(); + + // Third click - should call with true again + fireEvent.click(button); + expect(mockHandler).toHaveBeenCalledWith(true); + expect(mockHandler).toHaveBeenCalledTimes(3); + }); }); diff --git a/packages/ui/src/components/common/Switch.tsx b/packages/ui/src/components/common/Switch.tsx index 7a5195b8..2ab5db29 100644 --- a/packages/ui/src/components/common/Switch.tsx +++ b/packages/ui/src/components/common/Switch.tsx @@ -36,8 +36,8 @@ export function Switch({ data-testid={testId} className={cn( 'relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-[rgb(var(--primary))] focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50', - checked - ? 'bg-[rgb(var(--primary))] border-transparent' + checked + ? 'bg-[rgb(var(--primary))] border-transparent' : 'bg-gray-300 dark:bg-gray-600 border-gray-400 dark:border-gray-500', className )} diff --git a/packages/ui/vitest.config.ts b/packages/ui/vitest.config.ts index 3302cbce..5213ac65 100644 --- a/packages/ui/vitest.config.ts +++ b/packages/ui/vitest.config.ts @@ -3,17 +3,17 @@ import react from '@vitejs/plugin-react'; import path from 'path'; export default defineConfig({ - plugins: [react()], - test: { - environment: 'jsdom', - globals: true, - setupFiles: [path.resolve(__dirname, '../../tests/ts/setup.ts')], - include: ['src/**/*.test.{ts,tsx}'], - exclude: ['**/node_modules/**', 'dist/**'], - }, - resolve: { - alias: { - '@': path.resolve(__dirname, './src'), + plugins: [react()], + test: { + environment: 'jsdom', + globals: true, + setupFiles: [path.resolve(__dirname, '../../tests/ts/setup.ts')], + include: ['src/**/*.test.{ts,tsx}'], + exclude: ['**/node_modules/**', 'dist/**'], + }, + resolve: { + alias: { + '@': path.resolve(__dirname, './src'), + }, }, - }, }); diff --git a/tests/e2e/specs/settings-desktop.wdio.ts b/tests/e2e/specs/settings-desktop.wdio.ts index 41b53fc5..18f52de4 100644 --- a/tests/e2e/specs/settings-desktop.wdio.ts +++ b/tests/e2e/specs/settings-desktop.wdio.ts @@ -6,222 +6,222 @@ import { expect, browser } from '@wdio/globals'; describe('Settings - Desktop Features', () => { - beforeEach(async () => { - // Navigate to settings page - const dashboardBtn = await $('nav button[data-testid="nav-dashboard"]'); - await dashboardBtn.waitForClickable(); - await dashboardBtn.click(); - - const settingsBtn = await $('nav button[data-testid="nav-settings"]'); - await settingsBtn.waitForClickable(); - await settingsBtn.click(); - - // Wait for settings page to load - await browser.pause(500); - }); - - describe('Startup & System Tray Settings', () => { - it('should display startup settings section', async () => { - const heading = await $('h3*=Startup & System Tray'); - await expect(heading).toBeDisplayed(); - - const description = await $('p*=Control how McpMux starts'); - await expect(description).toBeDisplayed(); - }); + beforeEach(async () => { + // Navigate to settings page + const dashboardBtn = await $('nav button[data-testid="nav-dashboard"]'); + await dashboardBtn.waitForClickable(); + await dashboardBtn.click(); - it('should display all three startup toggles', async () => { - const autoLaunchLabel = await $('label*=Launch at Startup'); - await expect(autoLaunchLabel).toBeDisplayed(); - - const startMinimizedLabel = await $('label*=Start Minimized'); - await expect(startMinimizedLabel).toBeDisplayed(); - - const closeToTrayLabel = await $('label*=Close to Tray'); - await expect(closeToTrayLabel).toBeDisplayed(); - }); + const settingsBtn = await $('nav button[data-testid="nav-settings"]'); + await settingsBtn.waitForClickable(); + await settingsBtn.click(); - it('should display descriptive text for each setting', async () => { - const autoLaunchDesc = await $('p*=Start McpMux automatically'); - await expect(autoLaunchDesc).toBeDisplayed(); - - const startMinimizedDesc = await $('p*=Launch in background'); - await expect(startMinimizedDesc).toBeDisplayed(); - - const closeToTrayDesc = await $('p*=Keep running in system tray'); - await expect(closeToTrayDesc).toBeDisplayed(); + // Wait for settings page to load + await browser.pause(500); }); - it('should have functional toggle switches', async () => { - const autoLaunchSwitch = await $('[data-testid="auto-launch-switch"]'); - await expect(autoLaunchSwitch).toBeDisplayed(); - await expect(autoLaunchSwitch).toBeEnabled(); - - const closeToTraySwitch = await $('[data-testid="close-to-tray-switch"]'); - await expect(closeToTraySwitch).toBeDisplayed(); - await expect(closeToTraySwitch).toBeEnabled(); - - const startMinimizedSwitch = await $('[data-testid="start-minimized-switch"]'); - await expect(startMinimizedSwitch).toBeDisplayed(); - }); + describe('Startup & System Tray Settings', () => { + it('should display startup settings section', async () => { + const heading = await $('h3*=Startup & System Tray'); + await expect(heading).toBeDisplayed(); - it('should toggle auto-launch setting', async () => { - const autoLaunchSwitch = await $('[data-testid="auto-launch-switch"]'); - - const initialState = await autoLaunchSwitch.getAttribute('aria-checked'); - - await autoLaunchSwitch.click(); - await browser.pause(500); - - const newState = await autoLaunchSwitch.getAttribute('aria-checked'); - expect(newState).not.toBe(initialState); - - // Toggle back - await autoLaunchSwitch.click(); - await browser.pause(500); - - const finalState = await autoLaunchSwitch.getAttribute('aria-checked'); - expect(finalState).toBe(initialState); - }); + const description = await $('p*=Control how McpMux starts'); + await expect(description).toBeDisplayed(); + }); - it('should toggle close to tray setting', async () => { - const closeToTraySwitch = await $('[data-testid="close-to-tray-switch"]'); - - const initialState = await closeToTraySwitch.getAttribute('aria-checked'); - - await closeToTraySwitch.click(); - await browser.pause(500); - - const newState = await closeToTraySwitch.getAttribute('aria-checked'); - expect(newState).not.toBe(initialState); - - // Toggle back - await closeToTraySwitch.click(); - await browser.pause(500); - - const finalState = await closeToTraySwitch.getAttribute('aria-checked'); - expect(finalState).toBe(initialState); - }); + it('should display all three startup toggles', async () => { + const autoLaunchLabel = await $('label*=Launch at Startup'); + await expect(autoLaunchLabel).toBeDisplayed(); - it('start minimized should be disabled when auto-launch is off', async () => { - const autoLaunchSwitch = await $('[data-testid="auto-launch-switch"]'); - const startMinimizedSwitch = await $('[data-testid="start-minimized-switch"]'); - - // Ensure auto-launch is off - const autoLaunchState = await autoLaunchSwitch.getAttribute('aria-checked'); - if (autoLaunchState === 'true') { - await autoLaunchSwitch.click(); - await browser.pause(500); - } - - // Start minimized should be disabled - const isDisabled = await startMinimizedSwitch.getAttribute('disabled'); - expect(isDisabled).toBe('true'); - - const ariaChecked = await startMinimizedSwitch.getAttribute('aria-checked'); - expect(ariaChecked).toBe('false'); - }); + const startMinimizedLabel = await $('label*=Start Minimized'); + await expect(startMinimizedLabel).toBeDisplayed(); - it('start minimized should be enabled when auto-launch is on', async () => { - const autoLaunchSwitch = await $('[data-testid="auto-launch-switch"]'); - const startMinimizedSwitch = await $('[data-testid="start-minimized-switch"]'); - - // Ensure auto-launch is on - const autoLaunchState = await autoLaunchSwitch.getAttribute('aria-checked'); - if (autoLaunchState === 'false') { - await autoLaunchSwitch.click(); - await browser.pause(500); - } - - // Start minimized should be enabled - const isDisabled = await startMinimizedSwitch.getAttribute('disabled'); - expect(isDisabled).toBeNull(); - }); + const closeToTrayLabel = await $('label*=Close to Tray'); + await expect(closeToTrayLabel).toBeDisplayed(); + }); - it('should toggle start minimized when enabled', async () => { - const autoLaunchSwitch = await $('[data-testid="auto-launch-switch"]'); - const startMinimizedSwitch = await $('[data-testid="start-minimized-switch"]'); - - // Ensure auto-launch is on first - const autoLaunchState = await autoLaunchSwitch.getAttribute('aria-checked'); - if (autoLaunchState === 'false') { - await autoLaunchSwitch.click(); - await browser.pause(500); - } - - const initialState = await startMinimizedSwitch.getAttribute('aria-checked'); - - await startMinimizedSwitch.click(); - await browser.pause(500); - - const newState = await startMinimizedSwitch.getAttribute('aria-checked'); - expect(newState).not.toBe(initialState); - - // Toggle back - await startMinimizedSwitch.click(); - await browser.pause(500); - - const finalState = await startMinimizedSwitch.getAttribute('aria-checked'); - expect(finalState).toBe(initialState); - }); + it('should display descriptive text for each setting', async () => { + const autoLaunchDesc = await $('p*=Start McpMux automatically'); + await expect(autoLaunchDesc).toBeDisplayed(); - it('should persist settings across page reloads', async () => { - const closeToTraySwitch = await $('[data-testid="close-to-tray-switch"]'); - - const initialState = await closeToTraySwitch.getAttribute('aria-checked'); - - await closeToTraySwitch.click(); - await browser.pause(500); - - // Reload the page - await browser.refresh(); - await browser.pause(1000); - - // Navigate to settings again - const settingsBtn = await $('nav button[data-testid="nav-settings"]'); - await settingsBtn.waitForClickable(); - await settingsBtn.click(); - await browser.pause(500); - - // Verify state persisted - const persistedState = await closeToTraySwitch.getAttribute('aria-checked'); - expect(persistedState).not.toBe(initialState); - - // Restore original state - await closeToTraySwitch.click(); - await browser.pause(500); - }); + const startMinimizedDesc = await $('p*=Launch in background'); + await expect(startMinimizedDesc).toBeDisplayed(); - it('should show disabled state visually for start minimized', async () => { - const autoLaunchSwitch = await $('[data-testid="auto-launch-switch"]'); - const startMinimizedSwitch = await $('[data-testid="start-minimized-switch"]'); - - // Ensure auto-launch is off - const autoLaunchState = await autoLaunchSwitch.getAttribute('aria-checked'); - if (autoLaunchState === 'true') { - await autoLaunchSwitch.click(); - await browser.pause(500); - } - - // Check that start minimized has disabled styling - const className = await startMinimizedSwitch.getAttribute('class'); - expect(className).toContain('opacity-50'); - }); + const closeToTrayDesc = await $('p*=Keep running in system tray'); + await expect(closeToTrayDesc).toBeDisplayed(); + }); + + it('should have functional toggle switches', async () => { + const autoLaunchSwitch = await $('[data-testid="auto-launch-switch"]'); + await expect(autoLaunchSwitch).toBeDisplayed(); + await expect(autoLaunchSwitch).toBeEnabled(); + + const closeToTraySwitch = await $('[data-testid="close-to-tray-switch"]'); + await expect(closeToTraySwitch).toBeDisplayed(); + await expect(closeToTraySwitch).toBeEnabled(); + + const startMinimizedSwitch = await $('[data-testid="start-minimized-switch"]'); + await expect(startMinimizedSwitch).toBeDisplayed(); + }); + + it('should toggle auto-launch setting', async () => { + const autoLaunchSwitch = await $('[data-testid="auto-launch-switch"]'); + + const initialState = await autoLaunchSwitch.getAttribute('aria-checked'); + + await autoLaunchSwitch.click(); + await browser.pause(500); + + const newState = await autoLaunchSwitch.getAttribute('aria-checked'); + expect(newState).not.toBe(initialState); + + // Toggle back + await autoLaunchSwitch.click(); + await browser.pause(500); + + const finalState = await autoLaunchSwitch.getAttribute('aria-checked'); + expect(finalState).toBe(initialState); + }); + + it('should toggle close to tray setting', async () => { + const closeToTraySwitch = await $('[data-testid="close-to-tray-switch"]'); + + const initialState = await closeToTraySwitch.getAttribute('aria-checked'); + + await closeToTraySwitch.click(); + await browser.pause(500); + + const newState = await closeToTraySwitch.getAttribute('aria-checked'); + expect(newState).not.toBe(initialState); + + // Toggle back + await closeToTraySwitch.click(); + await browser.pause(500); + + const finalState = await closeToTraySwitch.getAttribute('aria-checked'); + expect(finalState).toBe(initialState); + }); + + it('start minimized should be disabled when auto-launch is off', async () => { + const autoLaunchSwitch = await $('[data-testid="auto-launch-switch"]'); + const startMinimizedSwitch = await $('[data-testid="start-minimized-switch"]'); + + // Ensure auto-launch is off + const autoLaunchState = await autoLaunchSwitch.getAttribute('aria-checked'); + if (autoLaunchState === 'true') { + await autoLaunchSwitch.click(); + await browser.pause(500); + } + + // Start minimized should be disabled + const isDisabled = await startMinimizedSwitch.getAttribute('disabled'); + expect(isDisabled).toBe('true'); + + const ariaChecked = await startMinimizedSwitch.getAttribute('aria-checked'); + expect(ariaChecked).toBe('false'); + }); + + it('start minimized should be enabled when auto-launch is on', async () => { + const autoLaunchSwitch = await $('[data-testid="auto-launch-switch"]'); + const startMinimizedSwitch = await $('[data-testid="start-minimized-switch"]'); + + // Ensure auto-launch is on + const autoLaunchState = await autoLaunchSwitch.getAttribute('aria-checked'); + if (autoLaunchState === 'false') { + await autoLaunchSwitch.click(); + await browser.pause(500); + } + + // Start minimized should be enabled + const isDisabled = await startMinimizedSwitch.getAttribute('disabled'); + expect(isDisabled).toBeNull(); + }); + + it('should toggle start minimized when enabled', async () => { + const autoLaunchSwitch = await $('[data-testid="auto-launch-switch"]'); + const startMinimizedSwitch = await $('[data-testid="start-minimized-switch"]'); + + // Ensure auto-launch is on first + const autoLaunchState = await autoLaunchSwitch.getAttribute('aria-checked'); + if (autoLaunchState === 'false') { + await autoLaunchSwitch.click(); + await browser.pause(500); + } + + const initialState = await startMinimizedSwitch.getAttribute('aria-checked'); + + await startMinimizedSwitch.click(); + await browser.pause(500); + + const newState = await startMinimizedSwitch.getAttribute('aria-checked'); + expect(newState).not.toBe(initialState); + + // Toggle back + await startMinimizedSwitch.click(); + await browser.pause(500); + + const finalState = await startMinimizedSwitch.getAttribute('aria-checked'); + expect(finalState).toBe(initialState); + }); + + it('should persist settings across page reloads', async () => { + const closeToTraySwitch = await $('[data-testid="close-to-tray-switch"]'); + + const initialState = await closeToTraySwitch.getAttribute('aria-checked'); + + await closeToTraySwitch.click(); + await browser.pause(500); + + // Reload the page + await browser.refresh(); + await browser.pause(1000); + + // Navigate to settings again + const settingsBtn = await $('nav button[data-testid="nav-settings"]'); + await settingsBtn.waitForClickable(); + await settingsBtn.click(); + await browser.pause(500); + + // Verify state persisted + const persistedState = await closeToTraySwitch.getAttribute('aria-checked'); + expect(persistedState).not.toBe(initialState); + + // Restore original state + await closeToTraySwitch.click(); + await browser.pause(500); + }); + + it('should show disabled state visually for start minimized', async () => { + const autoLaunchSwitch = await $('[data-testid="auto-launch-switch"]'); + const startMinimizedSwitch = await $('[data-testid="start-minimized-switch"]'); + + // Ensure auto-launch is off + const autoLaunchState = await autoLaunchSwitch.getAttribute('aria-checked'); + if (autoLaunchState === 'true') { + await autoLaunchSwitch.click(); + await browser.pause(500); + } + + // Check that start minimized has disabled styling + const className = await startMinimizedSwitch.getAttribute('class'); + expect(className).toContain('opacity-50'); + }); + + it('all settings should work independently', async () => { + const closeToTraySwitch = await $('[data-testid="close-to-tray-switch"]'); + + // Close to tray should work regardless of auto-launch state + const initialCloseToTray = await closeToTraySwitch.getAttribute('aria-checked'); + + await closeToTraySwitch.click(); + await browser.pause(500); + + const newCloseToTray = await closeToTraySwitch.getAttribute('aria-checked'); + expect(newCloseToTray).not.toBe(initialCloseToTray); - it('all settings should work independently', async () => { - const closeToTraySwitch = await $('[data-testid="close-to-tray-switch"]'); - - // Close to tray should work regardless of auto-launch state - const initialCloseToTray = await closeToTraySwitch.getAttribute('aria-checked'); - - await closeToTraySwitch.click(); - await browser.pause(500); - - const newCloseToTray = await closeToTraySwitch.getAttribute('aria-checked'); - expect(newCloseToTray).not.toBe(initialCloseToTray); - - // Restore - await closeToTraySwitch.click(); - await browser.pause(500); + // Restore + await closeToTraySwitch.click(); + await browser.pause(500); + }); }); - }); }); From da3d861be51138c799f6ba5ca38705a2966163fc Mon Sep 17 00:00:00 2001 From: Mohammod Al Amin Ashik Date: Fri, 6 Feb 2026 11:56:29 +0800 Subject: [PATCH 11/20] chore: adds githooks --- .github/GIT_HOOKS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/GIT_HOOKS.md b/.github/GIT_HOOKS.md index fe748b0c..9748c3d5 100644 --- a/.github/GIT_HOOKS.md +++ b/.github/GIT_HOOKS.md @@ -11,7 +11,7 @@ The pre-commit hook automatically runs before each commit to validate: - Runs `cargo check --workspace` to verify compilation - Automatically adds formatted files to the commit -### ✅ **TypeScript Validation** +### ✅ **TypeScript Validation** - Runs `pnpm typecheck` to verify type correctness - Checks all TypeScript files in the workspace From 930d08df9417b2c6be62729aa8418255c70b1d08 Mon Sep 17 00:00:00 2001 From: Mohammod Al Amin Ashik Date: Fri, 6 Feb 2026 12:06:11 +0800 Subject: [PATCH 12/20] ci: Add Clippy to pre-commit hooks and fix linting issues - Add cargo clippy to pre-commit hook and validate script - Fix derivable_impls warning in HostingType enum - Update documentation to reflect clippy check - Update both bash and PowerShell hook versions This ensures clippy warnings are caught before commit, matching CI requirements. --- .github/GIT_HOOKS.md | 2 ++ crates/mcpmux-core/src/domain/server.rs | 9 ++------- package.json | 2 +- 3 files changed, 5 insertions(+), 8 deletions(-) diff --git a/.github/GIT_HOOKS.md b/.github/GIT_HOOKS.md index 9748c3d5..0f435d03 100644 --- a/.github/GIT_HOOKS.md +++ b/.github/GIT_HOOKS.md @@ -8,6 +8,7 @@ The pre-commit hook automatically runs before each commit to validate: ### ✅ **Rust Validation** - Formats Rust code with `cargo fmt` +- Runs `cargo clippy --workspace -- -D warnings` to catch linting issues - Runs `cargo check --workspace` to verify compilation - Automatically adds formatted files to the commit @@ -42,6 +43,7 @@ To test the validation without committing: pnpm validate # Or individually +cargo clippy --workspace -- -D warnings cargo check --workspace pnpm typecheck ``` diff --git a/crates/mcpmux-core/src/domain/server.rs b/crates/mcpmux-core/src/domain/server.rs index e6ee5027..379e9063 100644 --- a/crates/mcpmux-core/src/domain/server.rs +++ b/crates/mcpmux-core/src/domain/server.rs @@ -192,20 +192,15 @@ pub enum Badge { } /// Where the server runs -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] #[serde(rename_all = "lowercase")] pub enum HostingType { + #[default] Local, Remote, Hybrid, } -impl Default for HostingType { - fn default() -> Self { - Self::Local - } -} - /// Installation complexity level #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] diff --git a/package.json b/package.json index 99099746..a327cf79 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "format": "prettier --write . && cargo fmt --all", "format:check": "prettier --check . && cargo fmt --all --check", "typecheck": "pnpm -r typecheck", - "validate": "cargo check --workspace && pnpm typecheck", + "validate": "cargo fmt --all && cargo clippy --workspace -- -D warnings && cargo check --workspace && pnpm typecheck", "clean": "pnpm -r clean && cargo clean" }, "devDependencies": { From c8c815cbf34ef37d9f058e0c8dcf5e2220209935 Mon Sep 17 00:00:00 2001 From: Mohammod Al Amin Ashik Date: Fri, 6 Feb 2026 12:16:22 +0800 Subject: [PATCH 13/20] ci: Add pnpm lint to pre-commit hooks - Add lint check to both bash and PowerShell hooks - Update validate script to include lint - Update documentation to reflect lint check Now pre-commit validation fully matches CI requirements: cargo fmt, clippy, check, lint, typecheck --- .github/GIT_HOOKS.md | 2 ++ package.json | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/GIT_HOOKS.md b/.github/GIT_HOOKS.md index 0f435d03..43e986a6 100644 --- a/.github/GIT_HOOKS.md +++ b/.github/GIT_HOOKS.md @@ -13,6 +13,7 @@ The pre-commit hook automatically runs before each commit to validate: - Automatically adds formatted files to the commit ### ✅ **TypeScript Validation** +- Runs `pnpm lint` to check code style and catch common errors - Runs `pnpm typecheck` to verify type correctness - Checks all TypeScript files in the workspace @@ -45,6 +46,7 @@ pnpm validate # Or individually cargo clippy --workspace -- -D warnings cargo check --workspace +pnpm lint pnpm typecheck ``` diff --git a/package.json b/package.json index a327cf79..a8f3e0ef 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,7 @@ "format": "prettier --write . && cargo fmt --all", "format:check": "prettier --check . && cargo fmt --all --check", "typecheck": "pnpm -r typecheck", - "validate": "cargo fmt --all && cargo clippy --workspace -- -D warnings && cargo check --workspace && pnpm typecheck", + "validate": "cargo fmt --all && cargo clippy --workspace -- -D warnings && cargo check --workspace && pnpm lint && pnpm typecheck", "clean": "pnpm -r clean && cargo clean" }, "devDependencies": { From 7c24f611bf0cffded22d3217c0248ce695bcbefd Mon Sep 17 00:00:00 2001 From: Mohammod Al Amin Ashik Date: Fri, 6 Feb 2026 12:22:57 +0800 Subject: [PATCH 14/20] fix: Auto-update system tray menu when spaces change - Remove dead_code attribute from update_tray_spaces function - Add AppHandle parameter to create_space and delete_space commands - Call update_tray_spaces after space creation, deletion, and activation - Add refresh_tray_menu command for manual tray refresh The system tray now automatically reflects: - New spaces when created - Space removal when deleted - Active space indicator when switching spaces Fixes issue where creating a new space didn't appear in system tray. --- apps/desktop/src-tauri/src/commands/space.rs | 26 ++++++++++++++++++++ apps/desktop/src-tauri/src/lib.rs | 1 + apps/desktop/src-tauri/src/tray.rs | 1 - 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src-tauri/src/commands/space.rs b/apps/desktop/src-tauri/src/commands/space.rs index 8b86f485..9b1e7e06 100644 --- a/apps/desktop/src-tauri/src/commands/space.rs +++ b/apps/desktop/src-tauri/src/commands/space.rs @@ -12,6 +12,7 @@ use uuid::Uuid; use crate::commands::gateway::GatewayAppState; use crate::state::AppState; +use crate::tray; /// Space change event payload #[derive(Debug, Clone, Serialize)] @@ -76,6 +77,7 @@ const DEFAULT_SPACE_CONFIG: &str = r#"{ pub async fn create_space( name: String, icon: Option, + app: AppHandle, state: State<'_, AppState>, gateway_state: State<'_, Arc>>, ) -> Result { @@ -109,6 +111,11 @@ pub async fn create_space( }); } + // Update system tray menu + if let Err(e) = tray::update_tray_spaces(&app, &state).await { + warn!("Failed to update tray menu: {}", e); + } + Ok(space) } @@ -116,6 +123,7 @@ pub async fn create_space( #[tauri::command] pub async fn delete_space( id: String, + app: AppHandle, state: State<'_, AppState>, gateway_state: State<'_, Arc>>, ) -> Result<(), String> { @@ -134,6 +142,11 @@ pub async fn delete_space( gw.emit_domain_event(mcpmux_core::DomainEvent::SpaceDeleted { space_id: uuid }); } + // Update system tray menu + if let Err(e) = tray::update_tray_spaces(&app, &state).await { + warn!("Failed to update tray menu: {}", e); + } + Ok(()) } @@ -242,6 +255,11 @@ pub async fn set_active_space( // will be emitted by the gateway when they make their next request // and the SpaceResolver returns the new active space. + // Update system tray menu to reflect new active space + if let Err(e) = tray::update_tray_spaces(&app_handle, &state).await { + warn!("Failed to update tray menu: {}", e); + } + Ok(()) } @@ -372,3 +390,11 @@ pub async fn remove_server_from_config( Ok(false) } + +/// Refresh the system tray menu to reflect current spaces +#[tauri::command] +pub async fn refresh_tray_menu(app: AppHandle, state: State<'_, AppState>) -> Result<(), String> { + tray::update_tray_spaces(&app, &state) + .await + .map_err(|e| format!("Failed to update tray menu: {}", e)) +} diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs index b36f0833..49527560 100644 --- a/apps/desktop/src-tauri/src/lib.rs +++ b/apps/desktop/src-tauri/src/lib.rs @@ -600,6 +600,7 @@ pub fn run() { commands::read_space_config, commands::save_space_config, commands::remove_server_from_config, + commands::refresh_tray_menu, // Server Discovery commands (v2) commands::discover_servers, commands::get_server_definition, diff --git a/apps/desktop/src-tauri/src/tray.rs b/apps/desktop/src-tauri/src/tray.rs index a6bd4b50..6c330568 100644 --- a/apps/desktop/src-tauri/src/tray.rs +++ b/apps/desktop/src-tauri/src/tray.rs @@ -132,7 +132,6 @@ fn handle_switch_space(app: &AppHandle, space_id: &str) { } /// Update tray menu with current spaces -#[allow(dead_code)] pub async fn update_tray_spaces( app: &AppHandle, state: &AppState, From 6e34ae40aeadf036d39971206832449a7ef2e556 Mon Sep 17 00:00:00 2001 From: Mohammod Al Amin Ashik Date: Fri, 6 Feb 2026 12:50:25 +0800 Subject: [PATCH 15/20] docs: Clarify tray update logic and success conditions - Add comments explaining tray updates only happen after successful operations - Add success logging for create_space, delete_space, and set_active_space - Clarify that active space checkmark (\u2713) is updated in tray - Fix ownership issue by cloning space name before move Improves code maintainability and debugging visibility. --- apps/desktop/src-tauri/src/commands/space.rs | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src-tauri/src/commands/space.rs b/apps/desktop/src-tauri/src/commands/space.rs index 9b1e7e06..7f0bbfc7 100644 --- a/apps/desktop/src-tauri/src/commands/space.rs +++ b/apps/desktop/src-tauri/src/commands/space.rs @@ -111,11 +111,14 @@ pub async fn create_space( }); } - // Update system tray menu + // Update system tray menu to show the new space + // Only reached if both space creation and config file writing succeeded if let Err(e) = tray::update_tray_spaces(&app, &state).await { warn!("Failed to update tray menu: {}", e); } + info!("[create_space] Space '{}' created successfully", space.name); + Ok(space) } @@ -142,11 +145,14 @@ pub async fn delete_space( gw.emit_domain_event(mcpmux_core::DomainEvent::SpaceDeleted { space_id: uuid }); } - // Update system tray menu + // Update system tray menu to remove the deleted space + // Only reached if space deletion from DB succeeded if let Err(e) = tray::update_tray_spaces(&app, &state).await { warn!("Failed to update tray menu: {}", e); } + info!("[delete_space] Space '{}' deleted successfully", uuid); + Ok(()) } @@ -238,7 +244,7 @@ pub async fn set_active_space( let event = SpaceChangeEvent { from_space_id: old_space.map(|s| s.id.to_string()), to_space_id: new_space.id.to_string(), - to_space_name: new_space.name, + to_space_name: new_space.name.clone(), clients_needing_confirmation: clients_needing_confirmation.clone(), }; @@ -255,11 +261,14 @@ pub async fn set_active_space( // will be emitted by the gateway when they make their next request // and the SpaceResolver returns the new active space. - // Update system tray menu to reflect new active space + // Update system tray menu to show checkmark (✓) on the newly active space + // Only reached if set_active operation succeeded in DB if let Err(e) = tray::update_tray_spaces(&app_handle, &state).await { warn!("Failed to update tray menu: {}", e); } + info!("[set_active_space] Switched to space '{}'", new_space.name); + Ok(()) } From 0cbaed6d5d3dddb8166c89c6970a490b61ec57ec Mon Sep 17 00:00:00 2001 From: Mohammod Al Amin Ashik Date: Fri, 6 Feb 2026 13:39:06 +0800 Subject: [PATCH 16/20] fix: Skip Tauri-dependent tests in web E2E and add signing password to workflows - Skip update checker tests in web mode (require Tauri invoke API) - Skip logs path test in web mode (requires Tauri invoke API) - Add TAURI_SIGNING_PRIVATE_KEY_PASSWORD to CI, e2e-desktop, and nightly workflows Fixes 3 failing web E2E tests that were trying to call desktop-only Tauri commands. These tests are properly covered in desktop E2E tests (settings-desktop.wdio.ts). --- .github/workflows/ci.yml | 1 + .github/workflows/e2e-desktop.yml | 3 ++ .github/workflows/nightly.yml | 1 + tests/e2e/specs/settings.spec.ts | 52 +++++++++++++++++-------------- 4 files changed, 33 insertions(+), 24 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 259cad93..d0473a1b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -242,6 +242,7 @@ jobs: - run: pnpm build env: TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} # ───────────────────────────────────────────────────────────── # Test Results Report (separate checks per test type and OS) diff --git a/.github/workflows/e2e-desktop.yml b/.github/workflows/e2e-desktop.yml index 74f48525..94942467 100644 --- a/.github/workflows/e2e-desktop.yml +++ b/.github/workflows/e2e-desktop.yml @@ -12,6 +12,8 @@ on: secrets: TAURI_SIGNING_PRIVATE_KEY: required: false + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: + required: false env: PKG_CONFIG_PATH: /usr/lib/x86_64-linux-gnu/pkgconfig:/usr/lib/pkgconfig:/usr/share/pkgconfig @@ -100,6 +102,7 @@ jobs: env: PKG_CONFIG_PATH: /usr/lib/x86_64-linux-gnu/pkgconfig:/usr/lib/pkgconfig:/usr/share/pkgconfig TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} - name: Run desktop E2E tests (Linux) if: matrix.os == 'ubuntu-latest' diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml index 6a053353..65cd3e1a 100644 --- a/.github/workflows/nightly.yml +++ b/.github/workflows/nightly.yml @@ -57,6 +57,7 @@ jobs: - run: pnpm build env: TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} PKG_CONFIG_PATH: /usr/lib/x86_64-linux-gnu/pkgconfig:/usr/lib/pkgconfig:/usr/share/pkgconfig - name: Upload artifacts diff --git a/tests/e2e/specs/settings.spec.ts b/tests/e2e/specs/settings.spec.ts index c4afef82..a2a4e2ba 100644 --- a/tests/e2e/specs/settings.spec.ts +++ b/tests/e2e/specs/settings.spec.ts @@ -5,10 +5,10 @@ test.describe('Settings', () => { test('should display settings heading', async ({ page }) => { const dashboard = new DashboardPage(page); await dashboard.navigate(); - + // Click Settings in sidebar await page.locator('nav button:has-text("Settings")').click(); - + // Check heading await expect(page.getByRole('heading', { name: 'Settings' })).toBeVisible(); }); @@ -16,7 +16,7 @@ test.describe('Settings', () => { test('should display appearance settings', async ({ page }) => { const dashboard = new DashboardPage(page); await dashboard.navigate(); - + await page.locator('nav button:has-text("Settings")').click(); await expect(page.locator('text=Appearance').first()).toBeVisible(); @@ -27,7 +27,7 @@ test.describe('Settings', () => { test('should display logs section', async ({ page }) => { const dashboard = new DashboardPage(page); await dashboard.navigate(); - + await page.locator('nav button:has-text("Settings")').click(); // Use heading role to be more specific @@ -37,13 +37,13 @@ test.describe('Settings', () => { test('should switch between themes', async ({ page }) => { const dashboard = new DashboardPage(page); await dashboard.navigate(); - + await page.locator('nav button:has-text("Settings")').click(); // Switch to light theme await page.getByRole('button', { name: 'Light', exact: true }).click(); await page.waitForTimeout(300); - + // Switch to dark theme await page.getByRole('button', { name: 'Dark', exact: true }).click(); await page.waitForTimeout(300); @@ -54,7 +54,7 @@ test.describe('Settings', () => { test('should display update checker section', async ({ page }) => { const dashboard = new DashboardPage(page); await dashboard.navigate(); - + await page.locator('nav button:has-text("Settings")').click(); // Check for update checker card @@ -66,7 +66,7 @@ test.describe('Settings', () => { test('should display current version', async ({ page }) => { const dashboard = new DashboardPage(page); await dashboard.navigate(); - + await page.locator('nav button:has-text("Settings")').click(); // Check current version is displayed @@ -77,7 +77,7 @@ test.describe('Settings', () => { test('should have check for updates button', async ({ page }) => { const dashboard = new DashboardPage(page); await dashboard.navigate(); - + await page.locator('nav button:has-text("Settings")').click(); const checkButton = page.getByTestId('check-updates-btn'); @@ -86,10 +86,11 @@ test.describe('Settings', () => { await expect(checkButton).toBeEnabled(); }); - test('should show loading state when checking for updates', async ({ page }) => { + // Skip in web mode - requires Tauri API + test.skip('should show loading state when checking for updates', async ({ page }) => { const dashboard = new DashboardPage(page); await dashboard.navigate(); - + await page.locator('nav button:has-text("Settings")').click(); const checkButton = page.getByTestId('check-updates-btn'); @@ -100,10 +101,11 @@ test.describe('Settings', () => { await expect(checkButton).toBeDisabled(); }); - test('should display update status message', async ({ page }) => { + // Skip in web mode - requires Tauri API + test.skip('should display update status message', async ({ page }) => { const dashboard = new DashboardPage(page); await dashboard.navigate(); - + await page.locator('nav button:has-text("Settings")').click(); const checkButton = page.getByTestId('check-updates-btn'); @@ -117,18 +119,19 @@ test.describe('Settings', () => { // Verify one of the expected states is shown const hasMessage = await page.getByTestId('update-message').isVisible().catch(() => false); const hasUpdate = await page.getByTestId('update-available').isVisible().catch(() => false); - + expect(hasMessage || hasUpdate).toBeTruthy(); }); - test('should allow multiple update checks', async ({ page }) => { + // Skip in web mode - requires Tauri API + test.skip('should allow multiple update checks', async ({ page }) => { const dashboard = new DashboardPage(page); await dashboard.navigate(); - + await page.locator('nav button:has-text("Settings")').click(); const checkButton = page.getByTestId('check-updates-btn'); - + // First check await checkButton.click(); await page.waitForSelector('[data-testid="update-message"], [data-testid="update-available"]', { @@ -137,7 +140,7 @@ test.describe('Settings', () => { // Check button should be available again await expect(checkButton).toBeEnabled(); - + // Second check await checkButton.click(); await expect(checkButton).toContainText(/Checking/); @@ -145,10 +148,11 @@ test.describe('Settings', () => { }); test.describe('Logs Section', () => { - test('should display logs path', async ({ page }) => { + // Skip in web mode - requires Tauri API + test.skip('should display logs path', async ({ page }) => { const dashboard = new DashboardPage(page); await dashboard.navigate(); - + await page.locator('nav button:has-text("Settings")').click(); const logsPath = page.getByTestId('logs-path'); @@ -160,7 +164,7 @@ test.describe('Settings', () => { test('should have open logs folder button', async ({ page }) => { const dashboard = new DashboardPage(page); await dashboard.navigate(); - + await page.locator('nav button:has-text("Settings")').click(); const openButton = page.getByTestId('open-logs-btn'); @@ -171,7 +175,7 @@ test.describe('Settings', () => { test('should show description text', async ({ page }) => { const dashboard = new DashboardPage(page); await dashboard.navigate(); - + await page.locator('nav button:has-text("Settings")').click(); await expect(page.getByText(/Logs are rotated daily/i)).toBeVisible(); @@ -182,7 +186,7 @@ test.describe('Settings', () => { test('should display all sections in order', async ({ page }) => { const dashboard = new DashboardPage(page); await dashboard.navigate(); - + await page.locator('nav button:has-text("Settings")').click(); // Verify sections appear in expected order @@ -200,7 +204,7 @@ test.describe('Settings', () => { test('should be scrollable if content overflows', async ({ page }) => { const dashboard = new DashboardPage(page); await dashboard.navigate(); - + await page.locator('nav button:has-text("Settings")').click(); // Content should be within a scrollable container From 754d48dd6a305ae7d0c58572c60952c4831f24fc Mon Sep 17 00:00:00 2001 From: Mohammod Al Amin Ashik Date: Fri, 6 Feb 2026 14:35:00 +0800 Subject: [PATCH 17/20] fix: Resolve Linux AppImage bundling failure in CI Exclude AppImage from build targets to prevent linuxdeploy failures in CI environments. AppImage bundling requires complex FUSE setup that can be unreliable in GitHub Actions runners. Continue providing .deb and .rpm packages which are more commonly used for Linux distribution. Also add libfuse2 dependency for future AppImage support if needed. --- .github/actions/install-linux-deps/action.yml | 4 ++-- apps/desktop/src-tauri/tauri.conf.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/actions/install-linux-deps/action.yml b/.github/actions/install-linux-deps/action.yml index f4e494e8..ae219eac 100644 --- a/.github/actions/install-linux-deps/action.yml +++ b/.github/actions/install-linux-deps/action.yml @@ -16,8 +16,8 @@ runs: - name: Cache apt packages (base) uses: awalsh128/cache-apt-pkgs-action@latest with: - packages: build-essential pkg-config libglib2.0-dev libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libsecret-1-dev - version: 1.0 + packages: build-essential pkg-config libglib2.0-dev libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libsecret-1-dev libfuse2 + version: 1.1 - name: Cache apt packages (E2E) if: ${{ inputs.e2e == 'true' }} diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index b26d7a16..e805cdff 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -44,7 +44,7 @@ "bundle": { "active": true, "createUpdaterArtifacts": true, - "targets": "all", + "targets": ["deb", "rpm", "nsis", "msi", "app", "dmg", "updater"], "icon": [ "icons/32x32.png", "icons/128x128.png", From 913673f743a6c8d3820e084b41ce2dd905c0a741 Mon Sep 17 00:00:00 2001 From: Mohammod Al Amin Ashik Date: Fri, 6 Feb 2026 14:42:51 +0800 Subject: [PATCH 18/20] fix: Exclude AppImage from Linux builds to prevent CI failures Use Tauri CLI --bundles flag to explicitly exclude AppImage from Linux builds. AppImage bundling requires linuxdeploy and FUSE which can be unreliable in CI environments. The .deb and .rpm packages provide better Linux distribution support. Also added libfuse2 dependency for future AppImage support if needed. --- .github/workflows/e2e-desktop.yml | 12 ++++++++++-- apps/desktop/src-tauri/tauri.conf.json | 5 +---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/.github/workflows/e2e-desktop.yml b/.github/workflows/e2e-desktop.yml index 94942467..34b312a0 100644 --- a/.github/workflows/e2e-desktop.yml +++ b/.github/workflows/e2e-desktop.yml @@ -97,13 +97,21 @@ jobs: - run: pnpm install --frozen-lockfile - - name: Build app - run: pnpm build + - name: Build app (Linux) + if: matrix.os == 'ubuntu-latest' + run: pnpm exec tauri build --bundles deb,rpm,updater env: PKG_CONFIG_PATH: /usr/lib/x86_64-linux-gnu/pkgconfig:/usr/lib/pkgconfig:/usr/share/pkgconfig TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + - name: Build app (Windows) + if: matrix.os == 'windows-latest' + run: pnpm build + env: + TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} + TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }} + - name: Run desktop E2E tests (Linux) if: matrix.os == 'ubuntu-latest' run: | diff --git a/apps/desktop/src-tauri/tauri.conf.json b/apps/desktop/src-tauri/tauri.conf.json index e805cdff..a7f42eee 100644 --- a/apps/desktop/src-tauri/tauri.conf.json +++ b/apps/desktop/src-tauri/tauri.conf.json @@ -44,7 +44,7 @@ "bundle": { "active": true, "createUpdaterArtifacts": true, - "targets": ["deb", "rpm", "nsis", "msi", "app", "dmg", "updater"], + "targets": "all", "icon": [ "icons/32x32.png", "icons/128x128.png", @@ -83,9 +83,6 @@ } }, "linux": { - "appimage": { - "bundleMediaFramework": false - }, "deb": { "depends": [ "libsecret-1-0", From 2ea1d705204ae22c10b1646e223a8fa80b7f70c7 Mon Sep 17 00:00:00 2001 From: Mohammod Al Amin Ashik Date: Fri, 6 Feb 2026 14:59:09 +0800 Subject: [PATCH 19/20] fix: Use pnpm --filter to run Tauri CLI in correct package context --- .github/workflows/e2e-desktop.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/e2e-desktop.yml b/.github/workflows/e2e-desktop.yml index 34b312a0..401baaf8 100644 --- a/.github/workflows/e2e-desktop.yml +++ b/.github/workflows/e2e-desktop.yml @@ -99,7 +99,7 @@ jobs: - name: Build app (Linux) if: matrix.os == 'ubuntu-latest' - run: pnpm exec tauri build --bundles deb,rpm,updater + run: pnpm --filter @mcpmux/desktop exec tauri build --bundles deb,rpm,updater env: PKG_CONFIG_PATH: /usr/lib/x86_64-linux-gnu/pkgconfig:/usr/lib/pkgconfig:/usr/share/pkgconfig TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }} From 9ce9cb96b995df53d257a6b57d86a45e3bc4a27c Mon Sep 17 00:00:00 2001 From: Mohammod Al Amin Ashik Date: Fri, 6 Feb 2026 16:10:53 +0800 Subject: [PATCH 20/20] fix: Add Wayland runtime libraries for Linux E2E tests --- .github/actions/install-linux-deps/action.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/install-linux-deps/action.yml b/.github/actions/install-linux-deps/action.yml index ae219eac..10ced8fe 100644 --- a/.github/actions/install-linux-deps/action.yml +++ b/.github/actions/install-linux-deps/action.yml @@ -23,8 +23,8 @@ runs: if: ${{ inputs.e2e == 'true' }} uses: awalsh128/cache-apt-pkgs-action@latest with: - packages: webkit2gtk-driver xvfb gnome-keyring gsettings-desktop-schemas dbus-x11 at-spi2-core libglib2.0-bin - version: 1.2 + packages: webkit2gtk-driver xvfb gnome-keyring gsettings-desktop-schemas dbus-x11 at-spi2-core libglib2.0-bin libwayland-server0 libwayland-client0 + version: 1.3 # Compile gsettings schemas (required after restore from cache) - name: Compile gsettings schemas