Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion apps/desktop/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ serde_json.workspace = true
[dependencies]
tauri = { version = "2", features = ["tray-icon"] }
tauri-plugin-opener = "2"
tauri-plugin-single-instance = "2"
tauri-plugin-single-instance = { version = "2", features = ["deep-link"] }
tauri-plugin-deep-link = "2"
tauri-plugin-updater = "2"
tauri-plugin-autostart = "2"
Expand Down
16 changes: 11 additions & 5 deletions apps/desktop/src-tauri/src/commands/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@ pub struct StartupSettings {
impl Default for StartupSettings {
fn default() -> Self {
Self {
auto_launch: false,
start_minimized: false,
auto_launch: true,
start_minimized: true,
close_to_tray: true, // Default to close-to-tray behavior
}
}
Expand All @@ -50,7 +50,7 @@ pub async fn get_startup_settings(
.await
.map_err(|e| format!("Failed to get start_minimized setting: {}", e))?
.map(|v| v == "true")
.unwrap_or(false);
.unwrap_or(true);

let close_to_tray = settings_repo
.get("ui.close_to_tray")
Expand Down Expand Up @@ -96,6 +96,12 @@ pub async fn update_startup_settings(
info!("[Settings] Auto-launch unchanged, skipping OS update");
}

// Mark autostart as explicitly configured so first-launch logic won't re-enable it
settings_repo
.set("startup.autostart_configured", "true")
.await
.map_err(|e| format!("Failed to save autostart_configured flag: {}", e))?;

// Update other settings in database
settings_repo
.set(
Expand Down Expand Up @@ -127,8 +133,8 @@ mod tests {
#[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.auto_launch, true);
assert_eq!(settings.start_minimized, true);
assert_eq!(settings.close_to_tray, true);
}

Expand Down
55 changes: 55 additions & 0 deletions apps/desktop/src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -569,6 +569,61 @@ pub fn run() {
}
}

// Enable auto-start on first launch if not already configured.
// The OS-level autostart is only set if not previously enabled/disabled by the user.
// This ensures fresh installs get autostart without requiring manual Settings toggle.
{
let autostart_manager: tauri::State<'_, tauri_plugin_autostart::AutoLaunchManager> = app.state();
match autostart_manager.is_enabled() {
Ok(false) => {
// Check if user has ever explicitly configured autostart
let app_state: tauri::State<'_, AppState> = app.state();
let was_configured = tauri::async_runtime::block_on(async {
app_state.settings_repository
.get("startup.autostart_configured")
.await
.ok()
.flatten()
.is_some()
});

if !was_configured {
// First launch: enable autostart and mark as configured
if let Err(e) = autostart_manager.enable() {
warn!("[Autostart] Failed to enable on first launch: {}", e);
} else {
info!("[Autostart] Enabled on first launch");
}
tauri::async_runtime::block_on(async {
let _ = app_state.settings_repository
.set("startup.autostart_configured", "true")
.await;
});
}
}
Ok(true) => {
info!("[Autostart] Already enabled");
}
Err(e) => {
warn!("[Autostart] Failed to check status: {}", e);
}
}
}

// Register deep link protocol in OS (Windows registry / Linux xdg-mime)
// NSIS writes to HKCU, MSI writes to HKLM — both register during install.
// This register_all() call is a safety net for dev mode and edge cases
// (e.g. AppImage on Linux, portable installs).
#[cfg(any(windows, target_os = "linux"))]
{
use tauri_plugin_deep_link::DeepLinkExt;
if let Err(e) = app.deep_link().register_all() {
warn!("[DeepLink] Failed to register protocol schemes: {}", e);
} else {
info!("[DeepLink] Protocol schemes registered successfully");
}
}

// Register deep link handler for when app receives URLs
#[cfg(desktop)]
{
Expand Down
14 changes: 12 additions & 2 deletions apps/desktop/src/components/ConfigEditorModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { readSpaceConfig, saveSpaceConfig } from '@/lib/api/spaces';
import { refreshRegistry } from '@/lib/api/registry';
import Editor, { type Monaco } from '@monaco-editor/react';
import type { editor } from 'monaco-editor';
import { useToast, ToastContainer } from '@mcpmux/ui';
import USER_SPACE_CONFIG_SCHEMA from '../../../../schemas/user-space.schema.json';

interface ConfigEditorModalProps {
Expand All @@ -23,6 +24,7 @@ export function ConfigEditorModal({ spaceId, spaceName, onClose, onSaved }: Conf
const [editorReady, setEditorReady] = useState(false);
const editorRef = useRef<editor.IStandaloneCodeEditor | null>(null);
const monacoRef = useRef<Monaco | null>(null);
const { toasts, success, error: showError } = useToast();

// Delay editor mount to avoid glitch during modal open
useEffect(() => {
Expand Down Expand Up @@ -61,6 +63,7 @@ export function ConfigEditorModal({ spaceId, spaceName, onClose, onSaved }: Conf
} catch (e) {
setIsValidJson(false);
setError(`Invalid JSON: ${(e as Error).message}`);
showError('Invalid JSON', (e as Error).message);
return;
}

Expand All @@ -69,10 +72,14 @@ export function ConfigEditorModal({ spaceId, spaceName, onClose, onSaved }: Conf
await saveSpaceConfig(spaceId, content);
// Refresh server discovery to pick up new/changed servers
await refreshRegistry();

success('Configuration saved', 'Space configuration updated successfully');
onSaved();
onClose();
} catch (e) {
setError(String(e));
const errorMsg = e instanceof Error ? e.message : String(e);
setError(errorMsg);
showError('Failed to save configuration', errorMsg);
} finally {
setIsSaving(false);
}
Expand Down Expand Up @@ -150,7 +157,9 @@ export function ConfigEditorModal({ spaceId, spaceName, onClose, onSaved }: Conf
}, [handleFormat, onClose]);

return (
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<>
<ToastContainer toasts={toasts} onClose={(id) => toasts.find(t => t.id === id)?.onClose(id)} />
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 p-4">
<div className="bg-[rgb(var(--surface))] w-full max-w-4xl h-[80vh] rounded-xl shadow-2xl flex flex-col border border-[rgb(var(--border))]">
{/* Header */}
<div className="flex items-center justify-between p-4 border-b border-[rgb(var(--border))]">
Expand Down Expand Up @@ -256,5 +265,6 @@ export function ConfigEditorModal({ spaceId, spaceName, onClose, onSaved }: Conf
)}
</div>
</div>
</>
);
}
21 changes: 18 additions & 3 deletions apps/desktop/src/features/featuresets/FeatureSetsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ import {
CardTitle,
CardContent,
Button,
useToast,
ToastContainer,
} from '@mcpmux/ui';
import type { FeatureSet, CreateFeatureSetInput } from '@/lib/api/featureSets';
import {
Expand Down Expand Up @@ -67,6 +69,7 @@ export function FeatureSetsPage() {
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const [searchQuery, setSearchQuery] = useState('');
const { toasts, success, error: showError } = useToast();

// Create modal state
const [showCreateModal, setShowCreateModal] = useState(false);
Expand Down Expand Up @@ -122,10 +125,14 @@ export function FeatureSetsPage() {
setCreateIcon('');
setShowCreateModal(false);

success('Feature set created', `"${newFs.name}" has been created successfully`);

// Automatically open the new feature set
handleOpenPanel(newFs);
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
const errorMsg = e instanceof Error ? e.message : String(e);
setError(errorMsg);
showError('Failed to create feature set', errorMsg);
} finally {
setIsCreating(false);
}
Expand All @@ -134,13 +141,18 @@ export function FeatureSetsPage() {
const handleDelete = async (id: string) => {
// Confirmation handled by caller if needed, but we do it here too just in case called directly
try {
const deletedSet = featureSets.find(fs => fs.id === id);
await deleteFeatureSet(id);
setFeatureSets((prev) => prev.filter((fs) => fs.id !== id));
if (selectedFeatureSet?.id === id) {
setSelectedFeatureSet(null);
}

success('Feature set deleted', `"${deletedSet?.name || 'Feature set'}" has been deleted`);
} catch (e) {
setError(e instanceof Error ? e.message : String(e));
const errorMsg = e instanceof Error ? e.message : String(e);
setError(errorMsg);
showError('Failed to delete feature set', errorMsg);
}
};

Expand Down Expand Up @@ -176,7 +188,9 @@ export function FeatureSetsPage() {
});

return (
<div className="h-full flex flex-col relative" data-testid="featuresets-page">
<>
<ToastContainer toasts={toasts} onClose={(id) => toasts.find(t => t.id === id)?.onClose(id)} />
<div className="h-full flex flex-col relative" data-testid="featuresets-page">
{/* Header */}
<div className="flex-shrink-0 p-8 border-b border-[rgb(var(--border-subtle))]">
<div className="max-w-[2000px] mx-auto">
Expand Down Expand Up @@ -422,6 +436,7 @@ export function FeatureSetsPage() {
</div>
)}
</div>
</>
);
}

Expand Down
26 changes: 19 additions & 7 deletions apps/desktop/src/features/settings/SettingsPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
CardContent,
Button,
Switch,
useToast,
ToastContainer,
} from '@mcpmux/ui';
import {
Sun,
Expand All @@ -34,6 +36,7 @@ export function SettingsPage() {
const setTheme = useAppStore((state) => state.setTheme);
const [logsPath, setLogsPath] = useState<string>('');
const [openingLogs, setOpeningLogs] = useState(false);
const { toasts, success, error } = useToast();

// Startup settings state
const [startupSettings, setStartupSettings] = useState<StartupSettings>({
Expand Down Expand Up @@ -91,8 +94,14 @@ export function SettingsPage() {
console.log('[Settings] Invoking update_startup_settings:', newSettings);
await invoke('update_startup_settings', { settings: newSettings });
console.log('[Settings] Successfully saved:', newSettings);
} catch (error) {
console.error('[Settings] Failed to save:', error);

// Show success toast
success('Settings saved', 'Your preferences have been updated');
} catch (err) {
console.error('[Settings] Failed to save:', err);
// Show error toast
const errorMessage = err instanceof Error ? err.message : 'Unknown error';
error('Failed to save settings', errorMessage);
// Revert on error
setStartupSettings(oldSettings);
} finally {
Expand All @@ -112,11 +121,13 @@ export function SettingsPage() {
};

return (
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold">Settings</h1>
<p className="text-[rgb(var(--muted))]">Configure McpMux preferences.</p>
</div>
<>
<ToastContainer toasts={toasts} onClose={(id) => toasts.find(t => t.id === id)?.onClose(id)} />
<div className="space-y-6">
<div>
<h1 className="text-2xl font-bold">Settings</h1>
<p className="text-[rgb(var(--muted))]">Configure McpMux preferences.</p>
</div>

{/* Updates Section */}
<UpdateChecker />
Expand Down Expand Up @@ -297,5 +308,6 @@ export function SettingsPage() {
</CardContent>
</Card>
</div>
</>
);
}
11 changes: 11 additions & 0 deletions crates/mcpmux-mcp/src/transports.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ use std::collections::HashMap;
use std::process::Stdio;
use std::sync::Arc;

#[cfg(windows)]
#[allow(unused_imports)] // Trait is used via method call in closure
use std::os::windows::process::CommandExt;

use anyhow::{Context, Result};
use rmcp::{
model::{
Expand Down Expand Up @@ -140,6 +144,13 @@ impl McpSession {
.envs(&env)
.stderr(Stdio::null())
.kill_on_drop(true);

// On Windows, prevent console window from appearing
#[cfg(windows)]
{
const CREATE_NO_WINDOW: u32 = 0x08000000;
cmd.creation_flags(CREATE_NO_WINDOW);
}
})
).context(format!(
"Failed to spawn child process. Command not found: {}. Ensure it's installed and in PATH.",
Expand Down
1 change: 1 addition & 0 deletions packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
},
"dependencies": {
"clsx": "^2.1.1",
"lucide-react": "^0.468.0",
"tailwind-merge": "^3.4.0"
}
}
Loading
Loading