diff --git a/Cargo.lock b/Cargo.lock
index 10c865d9..04a26590 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2611,7 +2611,7 @@ checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
[[package]]
name = "mcpmux"
-version = "0.2.0"
+version = "0.2.2"
dependencies = [
"anyhow",
"async-trait",
@@ -2635,6 +2635,7 @@ dependencies = [
"tauri-plugin-deep-link",
"tauri-plugin-dialog",
"tauri-plugin-opener",
+ "tauri-plugin-process",
"tauri-plugin-single-instance",
"tauri-plugin-updater",
"tokio",
@@ -2649,7 +2650,7 @@ dependencies = [
[[package]]
name = "mcpmux-core"
-version = "0.2.0"
+version = "0.2.2"
dependencies = [
"anyhow",
"async-trait",
@@ -2674,7 +2675,7 @@ dependencies = [
[[package]]
name = "mcpmux-gateway"
-version = "0.2.0"
+version = "0.2.2"
dependencies = [
"anyhow",
"async-stream",
@@ -2714,7 +2715,7 @@ dependencies = [
[[package]]
name = "mcpmux-mcp"
-version = "0.2.0"
+version = "0.2.2"
dependencies = [
"anyhow",
"async-trait",
@@ -2733,7 +2734,7 @@ dependencies = [
[[package]]
name = "mcpmux-storage"
-version = "0.2.0"
+version = "0.2.2"
dependencies = [
"anyhow",
"async-trait",
@@ -5307,6 +5308,16 @@ dependencies = [
"zbus",
]
+[[package]]
+name = "tauri-plugin-process"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d55511a7bf6cd70c8767b02c97bf8134fa434daf3926cfc1be0a0f94132d165a"
+dependencies = [
+ "tauri",
+ "tauri-plugin",
+]
+
[[package]]
name = "tauri-plugin-single-instance"
version = "2.3.7"
diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml
index d1834969..3d86555c 100644
--- a/apps/desktop/src-tauri/Cargo.toml
+++ b/apps/desktop/src-tauri/Cargo.toml
@@ -22,6 +22,7 @@ tauri-plugin-deep-link = "2"
tauri-plugin-updater = "2"
tauri-plugin-autostart = "2"
tauri-plugin-dialog = "2"
+tauri-plugin-process = "2"
image = { version = "0.25", default-features = false, features = ["png"] }
serde.workspace = true
serde_json.workspace = true
diff --git a/apps/desktop/src-tauri/capabilities/default.json b/apps/desktop/src-tauri/capabilities/default.json
index 54a377ca..d5621f46 100644
--- a/apps/desktop/src-tauri/capabilities/default.json
+++ b/apps/desktop/src-tauri/capabilities/default.json
@@ -17,6 +17,7 @@
"opener:default",
"deep-link:default",
"updater:default",
+ "process:allow-restart",
"dialog:default"
]
}
diff --git a/apps/desktop/src-tauri/src/lib.rs b/apps/desktop/src-tauri/src/lib.rs
index 008ee446..ad5b178b 100644
--- a/apps/desktop/src-tauri/src/lib.rs
+++ b/apps/desktop/src-tauri/src/lib.rs
@@ -122,12 +122,48 @@ fn init_tracing() -> tracing_appender::non_blocking::WorkerGuard {
guard
}
-/// Get app version
+/// Get app version (compiled into the binary)
#[tauri::command]
fn get_version() -> String {
env!("CARGO_PKG_VERSION").to_string()
}
+/// Get the on-disk bundle version (macOS only).
+///
+/// After a Homebrew Cask upgrade, the `.app` bundle on disk has the new version
+/// but the running process still has the old compiled-in version. Comparing
+/// `get_version()` with `get_bundle_version()` lets the frontend detect this
+/// mismatch and prompt the user to restart.
+#[tauri::command]
+fn get_bundle_version() -> Option {
+ #[cfg(target_os = "macos")]
+ {
+ // Read CFBundleShortVersionString from the running app's Info.plist
+ let exe = std::env::current_exe().ok()?;
+ // exe is typically: Foo.app/Contents/MacOS/Foo
+ let contents_dir = exe.parent()?.parent()?;
+ let plist_path = contents_dir.join("Info.plist");
+ let plist = std::fs::read_to_string(&plist_path).ok()?;
+
+ // Simple extraction — avoids adding a plist parsing dependency.
+ // Looks for CFBundleShortVersionString\nX.Y.Z
+ let key = "CFBundleShortVersionString";
+ let key_pos = plist.find(key)?;
+ let after_key = &plist[key_pos + key.len()..];
+ let string_start = after_key.find("")? + "".len();
+ let string_end = after_key[string_start..].find("")?;
+ Some(
+ after_key[string_start..string_start + string_end]
+ .trim()
+ .to_string(),
+ )
+ }
+ #[cfg(not(target_os = "macos"))]
+ {
+ None
+ }
+}
+
/// Get the path to the logs directory
#[tauri::command]
fn get_logs_path() -> String {
@@ -195,6 +231,7 @@ pub fn run() {
Some(vec!["--hidden"]), // Start minimized to tray
))
.plugin(tauri_plugin_updater::Builder::new().build())
+ .plugin(tauri_plugin_process::init())
.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");
@@ -691,6 +728,7 @@ pub fn run() {
})
.invoke_handler(tauri::generate_handler![
get_version,
+ get_bundle_version,
// Space commands
commands::list_spaces,
commands::get_space,
diff --git a/apps/desktop/src/features/settings/UpdateChecker.tsx b/apps/desktop/src/features/settings/UpdateChecker.tsx
index 6b9f2b94..dbf068f9 100644
--- a/apps/desktop/src/features/settings/UpdateChecker.tsx
+++ b/apps/desktop/src/features/settings/UpdateChecker.tsx
@@ -1,4 +1,4 @@
-import { useState } from 'react';
+import { useState, useEffect } from 'react';
import { check, Update } from '@tauri-apps/plugin-updater';
import { relaunch } from '@tauri-apps/plugin-process';
import {
@@ -9,7 +9,7 @@ import {
CardDescription,
CardContent,
} from '@mcpmux/ui';
-import { Download, Loader2, CheckCircle, AlertCircle, RefreshCw } from 'lucide-react';
+import { Download, Loader2, CheckCircle, AlertCircle, RefreshCw, RotateCcw } from 'lucide-react';
import { invoke } from '@tauri-apps/api/core';
interface DownloadEvent {
@@ -27,6 +27,7 @@ export function UpdateChecker() {
const [downloadProgress, setDownloadProgress] = useState({ downloaded: 0, total: 0 });
const [message, setMessage] = useState<{ type: 'success' | 'error'; text: string } | null>(null);
const [currentVersion, setCurrentVersion] = useState('');
+ const [bundleVersionMismatch, setBundleVersionMismatch] = useState(null);
// Load current version on mount
useState(() => {
@@ -35,6 +36,23 @@ export function UpdateChecker() {
.catch((err) => console.error('Failed to get version:', err));
});
+ // Check if the on-disk bundle version differs from the running version (Homebrew Cask upgrades)
+ useEffect(() => {
+ if (!currentVersion) return;
+ invoke('get_bundle_version')
+ .then((bundleVersion) => {
+ if (bundleVersion && bundleVersion !== currentVersion) {
+ console.log(
+ `[Updater] Bundle version mismatch: running=${currentVersion}, on-disk=${bundleVersion}`
+ );
+ setBundleVersionMismatch(bundleVersion);
+ }
+ })
+ .catch(() => {
+ // Expected to return null on non-macOS platforms
+ });
+ }, [currentVersion]);
+
const checkForUpdates = async () => {
setChecking(true);
setMessage(null);
@@ -149,8 +167,32 @@ export function UpdateChecker() {
+ {/* Bundle version mismatch (e.g., after brew upgrade) */}
+ {bundleVersionMismatch && (
+
+
+
Restart Required
+
+ Version v{bundleVersionMismatch} has been installed on disk, but you are still
+ running v{currentVersion}. Restart to apply the update.
+
+
+
+
+ )}
+
{/* Check Button */}
- {!updateInfo && (
+ {!updateInfo && !bundleVersionMismatch && (