Skip to content

Commit fa52576

Browse files
its-mashMohammod Al Amin Ashikclaude
authored
fix: ux improvements and fixes (#42)
Co-authored-by: Mohammod Al Amin Ashik <alamin.ashik@sitecore.com> Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
1 parent cb84c51 commit fa52576

37 files changed

Lines changed: 1524 additions & 134 deletions

Cargo.lock

Lines changed: 6 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/desktop/src-tauri/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ serde_json.workspace = true
1717
[dependencies]
1818
tauri = { version = "2", features = ["tray-icon"] }
1919
tauri-plugin-opener = "2"
20-
tauri-plugin-single-instance = "2"
20+
tauri-plugin-single-instance = { version = "2", features = ["deep-link"] }
2121
tauri-plugin-deep-link = "2"
2222
tauri-plugin-updater = "2"
2323
tauri-plugin-autostart = "2"

apps/desktop/src-tauri/src/commands/gateway.rs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -504,9 +504,10 @@ fn create_gateway_dependencies(
504504
builder.build().map_err(|e: String| e)
505505
}
506506

507-
/// Get gateway status
507+
/// Get gateway status, optionally scoped to a specific space
508508
#[tauri::command]
509509
pub async fn get_gateway_status(
510+
space_id: Option<String>,
510511
gateway_state: State<'_, Arc<RwLock<GatewayAppState>>>,
511512
server_manager_state: State<'_, Arc<RwLock<ServerManagerState>>>,
512513
) -> Result<GatewayStatus, String> {
@@ -519,19 +520,24 @@ pub async fn get_gateway_status(
519520
0
520521
};
521522

522-
// Get connected count from ServerManager
523+
// Get connected count from ServerManager, scoped to space if provided
523524
let connected_backends = {
524525
let sm_state = server_manager_state.read().await;
525526
if let Some(ref manager) = sm_state.manager {
526-
manager.connected_count().await
527+
if let Some(ref sid) = space_id {
528+
let uuid = Uuid::parse_str(sid).map_err(|e| e.to_string())?;
529+
manager.connected_count_for_space(&uuid).await
530+
} else {
531+
manager.connected_count().await
532+
}
527533
} else {
528534
0
529535
}
530536
};
531537

532538
info!(
533-
"[Gateway] get_gateway_status: running={}, url={:?}, sessions={}, backends={}",
534-
state.running, state.url, active_sessions, connected_backends
539+
"[Gateway] get_gateway_status: running={}, url={:?}, sessions={}, backends={}, space={:?}",
540+
state.running, state.url, active_sessions, connected_backends, space_id
535541
);
536542

537543
Ok(GatewayStatus {

apps/desktop/src-tauri/src/commands/settings.rs

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ pub struct StartupSettings {
2222
impl Default for StartupSettings {
2323
fn default() -> Self {
2424
Self {
25-
auto_launch: false,
26-
start_minimized: false,
25+
auto_launch: true,
26+
start_minimized: true,
2727
close_to_tray: true, // Default to close-to-tray behavior
2828
}
2929
}
@@ -44,20 +44,22 @@ pub async fn get_startup_settings(
4444
.is_enabled()
4545
.map_err(|e| format!("Failed to check auto-launch status: {}", e))?;
4646

47-
// Get other settings from database
47+
// Get other settings from database; use defaults when key is missing or DB read fails (e.g. no settings yet)
4848
let start_minimized = settings_repo
4949
.get("startup.start_minimized")
5050
.await
51-
.map_err(|e| format!("Failed to get start_minimized setting: {}", e))?
51+
.ok()
52+
.flatten()
5253
.map(|v| v == "true")
53-
.unwrap_or(false);
54+
.unwrap_or(true);
5455

5556
let close_to_tray = settings_repo
5657
.get("ui.close_to_tray")
5758
.await
58-
.map_err(|e| format!("Failed to get close_to_tray setting: {}", e))?
59+
.ok()
60+
.flatten()
5961
.map(|v| v == "true")
60-
.unwrap_or(true); // Default to true
62+
.unwrap_or(true);
6163

6264
Ok(StartupSettings {
6365
auto_launch,
@@ -96,6 +98,12 @@ pub async fn update_startup_settings(
9698
info!("[Settings] Auto-launch unchanged, skipping OS update");
9799
}
98100

101+
// Mark autostart as explicitly configured so first-launch logic won't re-enable it
102+
settings_repo
103+
.set("startup.autostart_configured", "true")
104+
.await
105+
.map_err(|e| format!("Failed to save autostart_configured flag: {}", e))?;
106+
99107
// Update other settings in database
100108
settings_repo
101109
.set(
@@ -127,8 +135,8 @@ mod tests {
127135
#[test]
128136
fn test_startup_settings_default() {
129137
let settings = StartupSettings::default();
130-
assert_eq!(settings.auto_launch, false);
131-
assert_eq!(settings.start_minimized, false);
138+
assert_eq!(settings.auto_launch, true);
139+
assert_eq!(settings.start_minimized, true);
132140
assert_eq!(settings.close_to_tray, true);
133141
}
134142

apps/desktop/src-tauri/src/lib.rs

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,61 @@ pub fn run() {
569569
}
570570
}
571571

572+
// Enable auto-start on first launch if not already configured.
573+
// The OS-level autostart is only set if not previously enabled/disabled by the user.
574+
// This ensures fresh installs get autostart without requiring manual Settings toggle.
575+
{
576+
let autostart_manager: tauri::State<'_, tauri_plugin_autostart::AutoLaunchManager> = app.state();
577+
match autostart_manager.is_enabled() {
578+
Ok(false) => {
579+
// Check if user has ever explicitly configured autostart
580+
let app_state: tauri::State<'_, AppState> = app.state();
581+
let was_configured = tauri::async_runtime::block_on(async {
582+
app_state.settings_repository
583+
.get("startup.autostart_configured")
584+
.await
585+
.ok()
586+
.flatten()
587+
.is_some()
588+
});
589+
590+
if !was_configured {
591+
// First launch: enable autostart and mark as configured
592+
if let Err(e) = autostart_manager.enable() {
593+
warn!("[Autostart] Failed to enable on first launch: {}", e);
594+
} else {
595+
info!("[Autostart] Enabled on first launch");
596+
}
597+
tauri::async_runtime::block_on(async {
598+
let _ = app_state.settings_repository
599+
.set("startup.autostart_configured", "true")
600+
.await;
601+
});
602+
}
603+
}
604+
Ok(true) => {
605+
info!("[Autostart] Already enabled");
606+
}
607+
Err(e) => {
608+
warn!("[Autostart] Failed to check status: {}", e);
609+
}
610+
}
611+
}
612+
613+
// Register deep link protocol in OS (Windows registry / Linux xdg-mime)
614+
// NSIS writes to HKCU, MSI writes to HKLM — both register during install.
615+
// This register_all() call is a safety net for dev mode and edge cases
616+
// (e.g. AppImage on Linux, portable installs).
617+
#[cfg(any(windows, target_os = "linux"))]
618+
{
619+
use tauri_plugin_deep_link::DeepLinkExt;
620+
if let Err(e) = app.deep_link().register_all() {
621+
warn!("[DeepLink] Failed to register protocol schemes: {}", e);
622+
} else {
623+
info!("[DeepLink] Protocol schemes registered successfully");
624+
}
625+
}
626+
572627
// Register deep link handler for when app receives URLs
573628
#[cfg(desktop)]
574629
{

apps/desktop/src/App.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -230,7 +230,7 @@ function DashboardView() {
230230
import('@/lib/api/featureSets').then((m) =>
231231
viewSpace?.id ? m.listFeatureSetsBySpace(viewSpace.id) : m.listFeatureSets()
232232
),
233-
import('@/lib/api/gateway').then((m) => m.getGatewayStatus()),
233+
import('@/lib/api/gateway').then((m) => m.getGatewayStatus(viewSpace?.id)),
234234
import('@/lib/api/registry').then((m) => m.listInstalledServers(viewSpace?.id)),
235235
]);
236236
console.log('[Dashboard] Gateway status received:', gateway);

apps/desktop/src/components/ConfigEditorModal.tsx

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { readSpaceConfig, saveSpaceConfig } from '@/lib/api/spaces';
44
import { refreshRegistry } from '@/lib/api/registry';
55
import Editor, { type Monaco } from '@monaco-editor/react';
66
import type { editor } from 'monaco-editor';
7+
import { useToast, ToastContainer } from '@mcpmux/ui';
78
import USER_SPACE_CONFIG_SCHEMA from '../../../../schemas/user-space.schema.json';
89

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

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

@@ -69,10 +72,14 @@ export function ConfigEditorModal({ spaceId, spaceName, onClose, onSaved }: Conf
6972
await saveSpaceConfig(spaceId, content);
7073
// Refresh server discovery to pick up new/changed servers
7174
await refreshRegistry();
75+
76+
success('Configuration saved', 'Space configuration updated successfully');
7277
onSaved();
7378
onClose();
7479
} catch (e) {
75-
setError(String(e));
80+
const errorMsg = e instanceof Error ? e.message : String(e);
81+
setError(errorMsg);
82+
showError('Failed to save configuration', errorMsg);
7683
} finally {
7784
setIsSaving(false);
7885
}
@@ -150,7 +157,9 @@ export function ConfigEditorModal({ spaceId, spaceName, onClose, onSaved }: Conf
150157
}, [handleFormat, onClose]);
151158

152159
return (
153-
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 p-4">
160+
<>
161+
<ToastContainer toasts={toasts} onClose={(id) => toasts.find(t => t.id === id)?.onClose(id)} />
162+
<div className="fixed inset-0 bg-black/60 backdrop-blur-sm flex items-center justify-center z-50 p-4">
154163
<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))]">
155164
{/* Header */}
156165
<div className="flex items-center justify-between p-4 border-b border-[rgb(var(--border))]">
@@ -256,5 +265,6 @@ export function ConfigEditorModal({ spaceId, spaceName, onClose, onSaved }: Conf
256265
)}
257266
</div>
258267
</div>
268+
</>
259269
);
260270
}

apps/desktop/src/components/ServerLogViewer.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useEffect, useState, useRef } from 'react';
22
import { X, Download, Trash2, RefreshCw } from 'lucide-react';
3+
import { useToast, ToastContainer } from '@mcpmux/ui';
34
import { getServerLogs, clearServerLogs, getServerLogFile, type ServerLogEntry } from '@/lib/api/logs';
45

56
interface ServerLogViewerProps {
@@ -38,6 +39,7 @@ export function ServerLogViewer({ serverId, serverName, onClose }: ServerLogView
3839
const [autoRefresh, setAutoRefresh] = useState(false);
3940
const scrollContainerRef = useRef<HTMLDivElement>(null);
4041
const shouldScrollRef = useRef(true);
42+
const { toasts, success, error: showError, dismiss } = useToast();
4143

4244
const loadLogs = async () => {
4345
try {
@@ -97,19 +99,19 @@ export function ServerLogViewer({ serverId, serverName, onClose }: ServerLogView
9799
try {
98100
await clearServerLogs(serverId);
99101
setLogs([]);
102+
success('Logs cleared', `All logs for "${serverName}" have been cleared`);
100103
} catch (e) {
101-
alert(`Failed to clear logs: ${e}`);
104+
showError('Failed to clear logs', e instanceof Error ? e.message : String(e));
102105
}
103106
};
104107

105108
const handleOpenInEditor = async () => {
106109
try {
107110
const filePath = await getServerLogFile(serverId);
108-
// Copy path to clipboard for user to paste into their editor
109111
await navigator.clipboard.writeText(filePath);
110-
alert(`Log file path copied to clipboard:\n${filePath}\n\nPaste this into your file explorer or text editor.`);
112+
success('Path copied', `Log file path copied to clipboard`);
111113
} catch (e) {
112-
alert(`Failed to get log file path: ${e}`);
114+
showError('Failed to get log file path', e instanceof Error ? e.message : String(e));
113115
}
114116
};
115117

@@ -131,6 +133,7 @@ export function ServerLogViewer({ serverId, serverName, onClose }: ServerLogView
131133

132134
return (
133135
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50">
136+
<ToastContainer toasts={toasts} onClose={dismiss} />
134137
<div className="bg-[rgb(var(--card))] border border-[rgb(var(--border-subtle))] rounded-xl shadow-xl w-[90vw] h-[85vh] flex flex-col">
135138
{/* Header */}
136139
<div className="flex items-center justify-between p-4 border-b border-[rgb(var(--border-subtle))]">

apps/desktop/src/components/SpaceSwitcher.tsx

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
Plus,
66
Loader2,
77
} from 'lucide-react';
8-
import { Button } from '@mcpmux/ui';
8+
import { Button, useToast, ToastContainer } from '@mcpmux/ui';
99
import {
1010
useAppStore,
1111
useActiveSpace,
@@ -25,6 +25,7 @@ export function SpaceSwitcher({ className = '' }: SpaceSwitcherProps) {
2525
const [newName, setNewName] = useState('');
2626
const [showCreateInput, setShowCreateInput] = useState(false);
2727
const dropdownRef = useRef<HTMLDivElement>(null);
28+
const { toasts, success, error: showError, dismiss } = useToast();
2829

2930
const spaces = useSpaces();
3031
const activeSpace = useActiveSpace();
@@ -56,8 +57,10 @@ export function SpaceSwitcher({ className = '' }: SpaceSwitcherProps) {
5657
await setActiveSpaceAPI(spaceId);
5758
setActiveSpaceInStore(spaceId);
5859
setIsOpen(false);
60+
const activatedSpace = spaces.find(s => s.id === spaceId);
61+
success('Space activated', `Switched to "${activatedSpace?.name || 'Space'}"`);
5962
} catch (e) {
60-
console.error('Failed to switch space:', e);
63+
showError('Failed to switch space', e instanceof Error ? e.message : String(e));
6164
}
6265
};
6366

@@ -73,15 +76,17 @@ export function SpaceSwitcher({ className = '' }: SpaceSwitcherProps) {
7376
setNewName('');
7477
setShowCreateInput(false);
7578
setIsOpen(false);
79+
success('Space created', `"${space.name}" has been created and activated`);
7680
} catch (e) {
77-
console.error('Failed to create space:', e);
81+
showError('Failed to create space', e instanceof Error ? e.message : String(e));
7882
} finally {
7983
setIsCreating(false);
8084
}
8185
};
8286

8387
return (
8488
<div ref={dropdownRef} className={`relative ${className}`}>
89+
<ToastContainer toasts={toasts} onClose={dismiss} />
8590
{/* Trigger Button */}
8691
<button
8792
onClick={() => setIsOpen(!isOpen)}

0 commit comments

Comments
 (0)