Skip to content

Commit f43879a

Browse files
committed
feat: add post-action UX guidance for install, approval, and empty states
Improve user experience after key actions by guiding users to the next step instead of leaving them without direction. - My Servers empty state: replace plain text with gradient "Discover MCP Servers" button that navigates to the Discover tab - Discover page post-install: success toast now includes "Go to My Servers to enable" action link (servers install disabled by default) - OAuth consent post-approval: new success screen with "Manage Permissions" button guiding users to assign FeatureSets on Clients page - Move activeNav to Zustand store for cross-component navigation - Extend Toast component to support optional action buttons (backward compatible) Signed-off-by: Mohammod Al Amin Ashik <maa.ashik00@gmail.com>
1 parent d43c6fd commit f43879a

14 files changed

Lines changed: 367 additions & 24 deletions

File tree

apps/desktop/src/App.tsx

Lines changed: 11 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ import { ConnectIDEs } from '@/components/ConnectIDEs';
3535
import { useDataSync } from '@/hooks/useDataSync';
3636
import { useAnalytics } from '@/hooks/useAnalytics';
3737
import { initAnalytics, capture, optIn, optOut } from '@/lib/analytics';
38-
import { useAppStore, useActiveSpace, useViewSpace, useTheme, useAnalyticsEnabled } from '@/stores';
38+
import { useAppStore, useActiveSpace, useViewSpace, useTheme, useAnalyticsEnabled, useActiveNav, useNavigateTo } from '@/stores';
3939
import { RegistryPage } from '@/features/registry';
4040
import { FeatureSetsPage } from '@/features/featuresets';
4141
import { ClientsPage } from '@/features/clients';
@@ -80,13 +80,12 @@ function McpMuxGlyph({ className }: { className?: string }) {
8080
);
8181
}
8282

83-
type NavItem = 'home' | 'registry' | 'servers' | 'spaces' | 'featuresets' | 'clients' | 'settings';
84-
8583
function AppContent() {
8684
// Sync data from backend on mount
8785
useDataSync();
8886

89-
const [activeNav, setActiveNav] = useState<NavItem>('home');
87+
const activeNav = useActiveNav();
88+
const navigateTo = useNavigateTo();
9089
const [availableUpdate, setAvailableUpdate] = useState<{ version: string } | null>(null);
9190

9291
// Auto-check for updates on startup (silent check after 5 seconds)
@@ -199,21 +198,21 @@ function AppContent() {
199198
icon={<Home className="h-4 w-4" />}
200199
label="Dashboard"
201200
active={activeNav === 'home'}
202-
onClick={() => setActiveNav('home')}
201+
onClick={() => navigateTo('home')}
203202
data-testid="nav-dashboard"
204203
/>
205204
<SidebarItem
206205
icon={<Server className="h-4 w-4" />}
207206
label="My Servers"
208207
active={activeNav === 'servers'}
209-
onClick={() => setActiveNav('servers')}
208+
onClick={() => navigateTo('servers')}
210209
data-testid="nav-my-servers"
211210
/>
212211
<SidebarItem
213212
icon={<Server className="h-4 w-4" />}
214213
label="Discover"
215214
active={activeNav === 'registry'}
216-
onClick={() => setActiveNav('registry')}
215+
onClick={() => navigateTo('registry')}
217216
data-testid="nav-discover"
218217
/>
219218
</SidebarSection>
@@ -223,14 +222,14 @@ function AppContent() {
223222
icon={<Globe className="h-4 w-4" />}
224223
label="Spaces"
225224
active={activeNav === 'spaces'}
226-
onClick={() => setActiveNav('spaces')}
225+
onClick={() => navigateTo('spaces')}
227226
data-testid="nav-spaces"
228227
/>
229228
<SidebarItem
230229
icon={<Wrench className="h-4 w-4" />}
231230
label="FeatureSets"
232231
active={activeNav === 'featuresets'}
233-
onClick={() => setActiveNav('featuresets')}
232+
onClick={() => navigateTo('featuresets')}
234233
data-testid="nav-featuresets"
235234
/>
236235
</SidebarSection>
@@ -240,7 +239,7 @@ function AppContent() {
240239
icon={<Monitor className="h-4 w-4" />}
241240
label="Clients"
242241
active={activeNav === 'clients'}
243-
onClick={() => setActiveNav('clients')}
242+
onClick={() => navigateTo('clients')}
244243
data-testid="nav-clients"
245244
/>
246245
</SidebarSection>
@@ -250,7 +249,7 @@ function AppContent() {
250249
icon={<Settings className="h-4 w-4" />}
251250
label="Settings"
252251
active={activeNav === 'settings'}
253-
onClick={() => setActiveNav('settings')}
252+
onClick={() => navigateTo('settings')}
254253
data-testid="nav-settings"
255254
/>
256255
</SidebarSection>
@@ -316,7 +315,7 @@ function AppContent() {
316315
</span>
317316
<button
318317
onClick={() => {
319-
setActiveNav('settings');
318+
navigateTo('settings');
320319
setAvailableUpdate(null);
321320
}}
322321
className="text-blue-500 hover:text-blue-400 font-medium underline underline-offset-2"

apps/desktop/src/components/OAuthConsentModal.tsx

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { listen } from '@tauri-apps/api/event';
1616
import { Check, X, AlertCircle, Loader2, Globe, Lock } from 'lucide-react';
1717
import { Button, Card, CardHeader, CardTitle, CardDescription, CardContent } from '@mcpmux/ui';
1818
import { listSpaces, type Space } from '@/lib/api/spaces';
19+
import { useAppStore } from '@/stores';
1920
import { resolveKnownClientKey } from '@/lib/clientIcons';
2021
import cursorIcon from '@/assets/client-icons/cursor.svg';
2122
import vscodeIcon from '@/assets/client-icons/vscode.png';
@@ -72,7 +73,8 @@ type ModalState =
7273
| { type: 'hidden' }
7374
| { type: 'loading'; requestId: string }
7475
| { type: 'error'; requestId: string; error: ConsentError }
75-
| { type: 'consent'; details: ConsentRequestDetails };
76+
| { type: 'consent'; details: ConsentRequestDetails }
77+
| { type: 'approved'; clientName: string };
7678

7779
/** Open a URL using the backend open command (handles custom protocols like cursor://) */
7880
async function openRedirectUrl(url: string): Promise<void> {
@@ -196,7 +198,7 @@ export function OAuthConsentModal() {
196198
if (response.success && response.redirect_url) {
197199
console.log('[OAuth] Approved, redirecting to:', response.redirect_url);
198200
await openRedirectUrl(response.redirect_url);
199-
setModalState({ type: 'hidden' });
201+
setModalState({ type: 'approved', clientName: clientAlias || details.clientName });
200202
} else {
201203
setProcessError(response.error || 'Failed to approve consent');
202204
}
@@ -289,6 +291,58 @@ export function OAuthConsentModal() {
289291
);
290292
}
291293

294+
// Approved state - show success with next-step guidance
295+
if (modalState.type === 'approved') {
296+
const navigateTo = useAppStore.getState().navigateTo;
297+
return (
298+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm">
299+
<Card className="animate-in fade-in zoom-in mx-4 w-full max-w-md shadow-xl duration-200">
300+
<CardHeader>
301+
<div className="flex items-center gap-3">
302+
<div className="rounded-full bg-green-500/10 p-2">
303+
<Check className="h-6 w-6 text-green-500" />
304+
</div>
305+
<div>
306+
<CardTitle>Client Approved</CardTitle>
307+
<CardDescription>
308+
{modalState.clientName} is now connected
309+
</CardDescription>
310+
</div>
311+
</div>
312+
</CardHeader>
313+
<CardContent className="space-y-4">
314+
<div className="rounded-lg border border-[rgb(var(--border))] bg-[rgb(var(--surface))] p-4 text-sm">
315+
<p className="font-medium mb-1">Next step: Grant permissions</p>
316+
<p className="text-[rgb(var(--muted))]">
317+
Assign FeatureSets to control which tools, prompts, and resources this client can access.
318+
</p>
319+
</div>
320+
<div className="flex gap-3">
321+
<Button
322+
variant="secondary"
323+
className="flex-1"
324+
onClick={handleDismiss}
325+
>
326+
Later
327+
</Button>
328+
<Button
329+
variant="primary"
330+
className="flex-1"
331+
onClick={() => {
332+
navigateTo('clients');
333+
handleDismiss();
334+
}}
335+
data-testid="go-to-clients-btn"
336+
>
337+
Manage Permissions →
338+
</Button>
339+
</div>
340+
</CardContent>
341+
</Card>
342+
</div>
343+
);
344+
}
345+
292346
// Consent state - show approval modal
293347
const { details } = modalState;
294348
const scopes = details.scope?.split(' ').filter(Boolean) || ['mcp'];

apps/desktop/src/features/registry/RegistryPage.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { useToast, ToastContainer } from '@mcpmux/ui';
1010
import { useRegistryStore } from '../../stores/registryStore';
1111
import { ServerCard } from './ServerCard';
1212
import { ServerDetailModal } from './ServerDetailModal';
13-
import { useViewSpace } from '@/stores';
13+
import { useViewSpace, useNavigateTo } from '@/stores';
1414
import { capture } from '@/lib/analytics';
1515

1616
export function RegistryPage() {
@@ -39,6 +39,7 @@ export function RegistryPage() {
3939

4040
const [localSearch, setLocalSearch] = useState('');
4141
const viewSpace = useViewSpace();
42+
const navigateTo = useNavigateTo();
4243
const { toasts, success, error: showToastError, dismiss } = useToast();
4344

4445
const itemsPerPage = uiConfig?.items_per_page ?? 24;
@@ -105,7 +106,13 @@ export function RegistryPage() {
105106
const serverName = server?.name || 'Server';
106107
try {
107108
await installServer(id, viewSpace?.id);
108-
success('Server installed', `"${serverName}" has been installed`);
109+
success('Server installed', `"${serverName}" has been installed`, {
110+
duration: 6000,
111+
action: {
112+
label: 'Go to My Servers to enable →',
113+
onClick: () => navigateTo('servers'),
114+
},
115+
});
109116
} catch {
110117
showToastError('Install failed', `Failed to install "${serverName}"`);
111118
}

apps/desktop/src/features/servers/ServersPage.tsx

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ import type { ServerFeature } from '@/lib/api/serverFeatures';
2626
import { listServerFeaturesByServer } from '@/lib/api/serverFeatures';
2727
import type { ConnectionStatus, ServerStatusResponse } from '@/lib/api/serverManager';
2828
import { getServerStatuses as fetchServerStatuses } from '@/lib/api/serverManager';
29-
import { useViewSpace } from '@/stores';
29+
import { useViewSpace, useNavigateTo } from '@/stores';
3030
import { useServerManager } from '@/hooks/useServerManager';
3131
import { useGatewayEvents, useDomainEvents } from '@/hooks/useDomainEvents';
3232
import type { GatewayChangedPayload, ServerChangedPayload } from '@/hooks/useDomainEvents';
@@ -192,7 +192,8 @@ export function ServersPage() {
192192
const [editConfigSpace, setEditConfigSpace] = useState<{ id: string; name: string } | null>(null);
193193

194194
const viewSpace = useViewSpace();
195-
195+
const navigateTo = useNavigateTo();
196+
196197
// Event-driven server status management
197198
const {
198199
statuses: serverStatuses,
@@ -890,7 +891,13 @@ export function ServersPage() {
890891
<div className="text-center py-12 text-[rgb(var(--muted))]">
891892
<div className="text-5xl mb-4">📦</div>
892893
<p className="text-lg mb-2">No servers installed</p>
893-
<p className="text-sm">Visit Discover to install MCP servers</p>
894+
<button
895+
onClick={() => navigateTo('registry')}
896+
className="mt-3 px-6 py-2.5 rounded-lg text-sm font-semibold text-white bg-gradient-to-r from-primary-500 to-purple-500 hover:from-primary-600 hover:to-purple-600 shadow-md hover:shadow-lg transition-all hover:scale-[1.03]"
897+
data-testid="discover-servers-btn"
898+
>
899+
Discover MCP Servers
900+
</button>
894901
</div>
895902
) : (
896903
<div className="space-y-3">

apps/desktop/src/stores/appStore.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const initialState: AppState = {
77
spaces: [],
88
activeSpaceId: null,
99
viewSpaceId: null,
10+
activeNav: 'home',
1011
sidebarCollapsed: false,
1112
theme: 'system',
1213
analyticsEnabled: true,
@@ -85,6 +86,12 @@ export const useAppStore = create<AppStore>()(
8586
}
8687
}),
8788

89+
// Navigation
90+
navigateTo: (nav) =>
91+
set((state) => {
92+
state.activeNav = nav;
93+
}),
94+
8895
// UI
8996
toggleSidebar: () =>
9097
set((state) => {

apps/desktop/src/stores/selectors.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import { Space } from '@/lib/api/spaces';
55
export const useSpaces = () => useAppStore((state) => state.spaces);
66
export const useActiveSpaceId = () => useAppStore((state) => state.activeSpaceId);
77
export const useViewSpaceId = () => useAppStore((state) => state.viewSpaceId);
8+
export const useActiveNav = () => useAppStore((state) => state.activeNav);
9+
export const useNavigateTo = () => useAppStore((state) => state.navigateTo);
810
export const useTheme = () => useAppStore((state) => state.theme);
911
export const useSidebarCollapsed = () => useAppStore((state) => state.sidebarCollapsed);
1012
export const useAnalyticsEnabled = () => useAppStore((state) => state.analyticsEnabled);

apps/desktop/src/stores/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,16 @@
11
import { Space } from '@/lib/api/spaces';
22

3+
export type NavItem = 'home' | 'registry' | 'servers' | 'spaces' | 'featuresets' | 'clients' | 'settings';
4+
35
export interface AppState {
46
// Spaces
57
spaces: Space[];
68
activeSpaceId: string | null;
79
viewSpaceId: string | null;
810

11+
// Navigation
12+
activeNav: NavItem;
13+
914
// UI state
1015
sidebarCollapsed: boolean;
1116
theme: 'light' | 'dark' | 'system';
@@ -27,6 +32,9 @@ export interface AppActions {
2732
removeSpace: (id: string) => void;
2833
updateSpace: (id: string, updates: Partial<Space>) => void;
2934

35+
// Navigation
36+
navigateTo: (nav: NavItem) => void;
37+
3038
// UI
3139
toggleSidebar: () => void;
3240
setTheme: (theme: 'light' | 'dark' | 'system') => void;

packages/ui/src/components/common/Toast.tsx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,18 @@ import { cn } from '../../lib/cn';
44

55
export type ToastType = 'success' | 'error' | 'warning' | 'info';
66

7+
export interface ToastAction {
8+
label: string;
9+
onClick: () => void;
10+
}
11+
712
export interface ToastProps {
813
id: string;
914
type: ToastType;
1015
title: string;
1116
message?: string;
1217
duration?: number;
18+
action?: ToastAction;
1319
onClose: (id: string) => void;
1420
}
1521

@@ -33,6 +39,7 @@ export function Toast({
3339
title,
3440
message,
3541
duration = 3000,
42+
action,
3643
onClose,
3744
}: ToastProps) {
3845
const Icon = iconMap[type];
@@ -62,6 +69,15 @@ export function Toast({
6269
{message && (
6370
<p className="text-xs text-[rgb(var(--muted))] mt-1">{message}</p>
6471
)}
72+
{action && (
73+
<button
74+
onClick={() => { action.onClick(); onClose(id); }}
75+
className="mt-2 text-xs font-semibold text-[rgb(var(--primary))] hover:underline"
76+
data-testid="toast-action"
77+
>
78+
{action.label}
79+
</button>
80+
)}
6581
</div>
6682
<button
6783
onClick={() => onClose(id)}

packages/ui/src/hooks/useToast.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import { useState, useCallback } from 'react';
2-
import { ToastProps, ToastType } from '../components/common/Toast';
2+
import { ToastProps, ToastType, ToastAction } from '../components/common/Toast';
33

44
export interface ToastOptions {
55
title: string;
66
message?: string;
77
type?: ToastType;
88
duration?: number;
9+
action?: ToastAction;
910
}
1011

1112
export function useToast() {
@@ -19,6 +20,7 @@ export function useToast() {
1920
title: options.title,
2021
message: options.message,
2122
duration: options.duration ?? 3000,
23+
action: options.action,
2224
onClose: (toastId: string) => {
2325
setToasts((prev) => prev.filter((t) => t.id !== toastId));
2426
},
@@ -29,8 +31,9 @@ export function useToast() {
2931
}, []);
3032

3133
const success = useCallback(
32-
(title: string, message?: string, duration?: number) => {
33-
return showToast({ title, message, type: 'success', duration });
34+
(title: string, message?: string, options?: number | { duration?: number; action?: ToastAction }) => {
35+
const opts = typeof options === 'number' ? { duration: options } : options;
36+
return showToast({ title, message, type: 'success', duration: opts?.duration, action: opts?.action });
3437
},
3538
[showToast]
3639
);

packages/ui/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ export { Input } from './components/common/Input';
1515
export { Card, CardHeader, CardTitle, CardDescription, CardContent } from './components/common/Card';
1616
export { Switch } from './components/common/Switch';
1717
export { Toast, ToastContainer } from './components/common/Toast';
18-
export type { ToastProps, ToastType } from './components/common/Toast';
18+
export type { ToastProps, ToastType, ToastAction } from './components/common/Toast';
1919

2020
// Hooks
2121
export { useToast } from './hooks/useToast';

0 commit comments

Comments
 (0)