Skip to content

Commit efe7472

Browse files
committed
feat: add welcome guide onboarding flow for first-time users
Implement a 5-step welcome guide modal that appears on first launch to walk users through the key features of McpMux: discovering servers, organizing with spaces, connecting AI clients, and controlling access with FeatureSets. The guide is persisted via localStorage so it only shows once. - Add WelcomeGuide component with step navigation (next/back/skip) - Add hasSeenWelcome flag to Zustand store with persistence - Add 22 unit tests for the WelcomeGuide component - Add 3 unit tests for the new store action - Add Playwright e2e spec (14 tests across 4 describe blocks) - Add WebdriverIO desktop e2e spec (16 tests across 3 describe blocks) - Add WelcomeGuidePage page object for e2e tests https://claude.ai/code/session_01TY5YfV9R31MfrJDtovKqmi
1 parent 04ab100 commit efe7472

11 files changed

Lines changed: 870 additions & 0 deletions

File tree

apps/desktop/src/App.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import {
2828
import { ThemeProvider } from '@/components/ThemeProvider';
2929
import { OAuthConsentModal } from '@/components/OAuthConsentModal';
3030
import { ServerInstallModal } from '@/components/ServerInstallModal';
31+
import { WelcomeGuide } from '@/components/WelcomeGuide';
3132
import { SpaceSwitcher } from '@/components/SpaceSwitcher';
3233
import { useDataSync } from '@/hooks/useDataSync';
3334
import { useAppStore, useActiveSpace, useViewSpace, useTheme } from '@/stores';
@@ -251,6 +252,8 @@ function App() {
251252
return (
252253
<ThemeProvider>
253254
<AppContent />
255+
{/* Welcome guide - shown on first launch */}
256+
<WelcomeGuide />
254257
{/* OAuth consent modal - shown when MCP clients request authorization */}
255258
<OAuthConsentModal />
256259
{/* Server install modal - shown when install deep link is received */}
Lines changed: 239 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,239 @@
1+
import { useState } from 'react';
2+
import {
3+
Globe,
4+
Server,
5+
Monitor,
6+
Wrench,
7+
ChevronRight,
8+
ChevronLeft,
9+
Rocket,
10+
} from 'lucide-react';
11+
import { Button, Card } from '@mcpmux/ui';
12+
import { useAppStore, useHasSeenWelcome } from '@/stores';
13+
14+
interface WelcomeStep {
15+
id: string;
16+
icon: React.ReactNode;
17+
title: string;
18+
description: string;
19+
details: string[];
20+
tip: string | null;
21+
}
22+
23+
const WELCOME_STEPS: WelcomeStep[] = [
24+
{
25+
id: 'welcome',
26+
icon: <Rocket className="h-8 w-8" />,
27+
title: 'Welcome to McpMux',
28+
description:
29+
'Your centralized MCP server manager. Configure your MCP servers once, connect every AI client through a single gateway.',
30+
details: [
31+
'No more duplicating server configs across Cursor, Claude Desktop, VS Code, and others',
32+
'Credentials are encrypted in your OS keychain — not stored in plain-text JSON',
33+
'One local gateway endpoint for all your AI clients',
34+
],
35+
tip: null,
36+
},
37+
{
38+
id: 'discover',
39+
icon: <Globe className="h-8 w-8" />,
40+
title: 'Discover & Install Servers',
41+
description:
42+
'Browse the curated registry and install MCP servers with one click. Search by category, auth type, or name.',
43+
details: [
44+
'Open the Discover page from the sidebar to browse available servers',
45+
'Click Install on any server to add it to your active space',
46+
'Servers requiring API keys or OAuth will prompt you for credentials',
47+
],
48+
tip: 'Servers are cached locally so you can browse even when offline.',
49+
},
50+
{
51+
id: 'spaces',
52+
icon: <Server className="h-8 w-8" />,
53+
title: 'Organize with Spaces',
54+
description:
55+
'Spaces are isolated workspaces with their own servers and credentials. Use them to separate work, personal, and project contexts.',
56+
details: [
57+
'Create spaces from the Spaces page — each gets its own server set',
58+
'Switch the active space from the sidebar dropdown',
59+
'Your AI clients automatically follow the active space',
60+
],
61+
tip: 'A default space is created for you on first launch.',
62+
},
63+
{
64+
id: 'connect',
65+
icon: <Monitor className="h-8 w-8" />,
66+
title: 'Connect Your AI Clients',
67+
description:
68+
'Add one config snippet to each AI client. All installed servers become available through the McpMux gateway.',
69+
details: [
70+
'Copy the gateway config from the Dashboard page',
71+
'Paste it into your client\'s MCP settings (inside mcpServers)',
72+
'Clients like Cursor, Claude Desktop, VS Code, and Windsurf are all supported',
73+
],
74+
tip: 'The gateway runs locally — your data never leaves your machine.',
75+
},
76+
{
77+
id: 'featuresets',
78+
icon: <Wrench className="h-8 w-8" />,
79+
title: 'Control with FeatureSets',
80+
description:
81+
'FeatureSets let you control which tools, prompts, and resources each client can access. Create permission bundles for fine-grained control.',
82+
details: [
83+
'Go to FeatureSets to create bundles like "Read Only" or "Dev Tools"',
84+
'Assign specific server features to each bundle',
85+
'Grant bundles to individual clients from the Clients page',
86+
],
87+
tip: 'A "Default" FeatureSet is auto-created for each space.',
88+
},
89+
];
90+
91+
export function WelcomeGuide() {
92+
const hasSeenWelcome = useHasSeenWelcome();
93+
const setHasSeenWelcome = useAppStore((state) => state.setHasSeenWelcome);
94+
const [currentStep, setCurrentStep] = useState(0);
95+
96+
if (hasSeenWelcome) {
97+
return null;
98+
}
99+
100+
const step = WELCOME_STEPS[currentStep];
101+
const isFirstStep = currentStep === 0;
102+
const isLastStep = currentStep === WELCOME_STEPS.length - 1;
103+
const totalSteps = WELCOME_STEPS.length;
104+
105+
const handleNext = () => {
106+
if (isLastStep) {
107+
setHasSeenWelcome(true);
108+
} else {
109+
setCurrentStep((prev) => prev + 1);
110+
}
111+
};
112+
113+
const handleBack = () => {
114+
setCurrentStep((prev) => prev - 1);
115+
};
116+
117+
const handleSkip = () => {
118+
setHasSeenWelcome(true);
119+
};
120+
121+
return (
122+
<div
123+
className="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm"
124+
data-testid="welcome-guide-overlay"
125+
>
126+
<Card
127+
className="w-full max-w-lg mx-4 animate-fade-in"
128+
data-testid="welcome-guide-card"
129+
>
130+
{/* Step indicator */}
131+
<div className="flex items-center justify-between mb-6" data-testid="welcome-guide-step-indicator">
132+
<div className="flex gap-1.5">
133+
{WELCOME_STEPS.map((_, index) => (
134+
<div
135+
key={index}
136+
className={`h-1.5 rounded-full transition-all duration-300 ${
137+
index === currentStep
138+
? 'w-6 bg-[rgb(var(--primary))]'
139+
: index < currentStep
140+
? 'w-1.5 bg-[rgb(var(--primary))]/60'
141+
: 'w-1.5 bg-[rgb(var(--border))]'
142+
}`}
143+
/>
144+
))}
145+
</div>
146+
<span className="text-xs text-[rgb(var(--muted))]" data-testid="welcome-guide-step-count">
147+
{currentStep + 1} / {totalSteps}
148+
</span>
149+
</div>
150+
151+
{/* Icon */}
152+
<div className="flex items-center justify-center mb-4">
153+
<div
154+
className="h-16 w-16 rounded-2xl bg-[rgb(var(--primary))]/10 flex items-center justify-center text-[rgb(var(--primary))]"
155+
data-testid="welcome-guide-icon"
156+
>
157+
{step.icon}
158+
</div>
159+
</div>
160+
161+
{/* Content */}
162+
<div className="text-center mb-6">
163+
<h2
164+
className="text-xl font-bold mb-2"
165+
data-testid="welcome-guide-title"
166+
>
167+
{step.title}
168+
</h2>
169+
<p
170+
className="text-sm text-[rgb(var(--muted))]"
171+
data-testid="welcome-guide-description"
172+
>
173+
{step.description}
174+
</p>
175+
</div>
176+
177+
{/* Details */}
178+
<ul className="space-y-3 mb-6" data-testid="welcome-guide-details">
179+
{step.details.map((detail, index) => (
180+
<li key={index} className="flex items-start gap-3 text-sm">
181+
<ChevronRight className="h-4 w-4 mt-0.5 text-[rgb(var(--primary))] shrink-0" />
182+
<span>{detail}</span>
183+
</li>
184+
))}
185+
</ul>
186+
187+
{/* Tip */}
188+
{step.tip && (
189+
<div
190+
className="rounded-lg bg-[rgb(var(--surface-dim))] px-4 py-3 text-xs text-[rgb(var(--muted))] mb-6"
191+
data-testid="welcome-guide-tip"
192+
>
193+
<span className="font-medium text-[rgb(var(--foreground))]">Tip: </span>
194+
{step.tip}
195+
</div>
196+
)}
197+
198+
{/* Navigation */}
199+
<div className="flex items-center justify-between">
200+
<div>
201+
{isFirstStep ? (
202+
<Button
203+
variant="ghost"
204+
size="sm"
205+
onClick={handleSkip}
206+
data-testid="welcome-guide-skip-btn"
207+
>
208+
Skip
209+
</Button>
210+
) : (
211+
<Button
212+
variant="ghost"
213+
size="sm"
214+
onClick={handleBack}
215+
data-testid="welcome-guide-back-btn"
216+
>
217+
<ChevronLeft className="h-4 w-4" />
218+
Back
219+
</Button>
220+
)}
221+
</div>
222+
<Button
223+
variant="primary"
224+
size="sm"
225+
onClick={handleNext}
226+
data-testid="welcome-guide-next-btn"
227+
>
228+
{isLastStep ? 'Get Started' : 'Next'}
229+
{!isLastStep && <ChevronRight className="h-4 w-4" />}
230+
</Button>
231+
</div>
232+
</Card>
233+
</div>
234+
);
235+
}
236+
237+
/** Re-export the steps data for testing */
238+
export { WELCOME_STEPS };
239+
export type { WelcomeStep };

apps/desktop/src/stores/appStore.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ const initialState: AppState = {
99
viewSpaceId: null,
1010
sidebarCollapsed: false,
1111
theme: 'system',
12+
hasSeenWelcome: false,
1213
loading: {
1314
spaces: false,
1415
servers: false,
@@ -95,6 +96,11 @@ export const useAppStore = create<AppStore>()(
9596
state.theme = theme;
9697
}),
9798

99+
setHasSeenWelcome: (value) =>
100+
set((state) => {
101+
state.hasSeenWelcome = value;
102+
}),
103+
98104
// Loading
99105
setLoading: (key, value) =>
100106
set((state) => {
@@ -110,6 +116,7 @@ export const useAppStore = create<AppStore>()(
110116
activeSpaceId: state.activeSpaceId,
111117
sidebarCollapsed: state.sidebarCollapsed,
112118
theme: state.theme,
119+
hasSeenWelcome: state.hasSeenWelcome,
113120
}),
114121
}
115122
)

apps/desktop/src/stores/selectors.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export const useActiveSpaceId = () => useAppStore((state) => state.activeSpaceId
77
export const useViewSpaceId = () => useAppStore((state) => state.viewSpaceId);
88
export const useTheme = () => useAppStore((state) => state.theme);
99
export const useSidebarCollapsed = () => useAppStore((state) => state.sidebarCollapsed);
10+
export const useHasSeenWelcome = () => useAppStore((state) => state.hasSeenWelcome);
1011

1112
// Computed selectors
1213
export const useActiveSpace = (): Space | null => {

apps/desktop/src/stores/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export interface AppState {
99
// UI state
1010
sidebarCollapsed: boolean;
1111
theme: 'light' | 'dark' | 'system';
12+
hasSeenWelcome: boolean;
1213

1314
// Loading states
1415
loading: {
@@ -29,6 +30,7 @@ export interface AppActions {
2930
// UI
3031
toggleSidebar: () => void;
3132
setTheme: (theme: 'light' | 'dark' | 'system') => void;
33+
setHasSeenWelcome: (value: boolean) => void;
3234

3335
// Loading
3436
setLoading: (key: keyof AppState['loading'], value: boolean) => void;
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { Page, Locator } from '@playwright/test';
2+
import { BasePage } from './BasePage';
3+
4+
/**
5+
* Welcome Guide overlay page object
6+
*/
7+
export class WelcomeGuidePage extends BasePage {
8+
readonly overlay: Locator;
9+
readonly card: Locator;
10+
readonly title: Locator;
11+
readonly description: Locator;
12+
readonly stepCount: Locator;
13+
readonly stepIndicator: Locator;
14+
readonly details: Locator;
15+
readonly tip: Locator;
16+
readonly icon: Locator;
17+
readonly nextButton: Locator;
18+
readonly backButton: Locator;
19+
readonly skipButton: Locator;
20+
21+
constructor(page: Page) {
22+
super(page);
23+
this.overlay = page.getByTestId('welcome-guide-overlay');
24+
this.card = page.getByTestId('welcome-guide-card');
25+
this.title = page.getByTestId('welcome-guide-title');
26+
this.description = page.getByTestId('welcome-guide-description');
27+
this.stepCount = page.getByTestId('welcome-guide-step-count');
28+
this.stepIndicator = page.getByTestId('welcome-guide-step-indicator');
29+
this.details = page.getByTestId('welcome-guide-details');
30+
this.tip = page.getByTestId('welcome-guide-tip');
31+
this.icon = page.getByTestId('welcome-guide-icon');
32+
this.nextButton = page.getByTestId('welcome-guide-next-btn');
33+
this.backButton = page.getByTestId('welcome-guide-back-btn');
34+
this.skipButton = page.getByTestId('welcome-guide-skip-btn');
35+
}
36+
37+
async navigate() {
38+
await this.goto('/');
39+
await this.waitForLoad();
40+
}
41+
42+
async goToStep(stepNumber: number) {
43+
for (let i = 0; i < stepNumber - 1; i++) {
44+
await this.nextButton.click();
45+
}
46+
}
47+
48+
async dismiss() {
49+
await this.skipButton.click();
50+
}
51+
52+
async completeAllSteps() {
53+
// Navigate through all 5 steps and click Get Started
54+
for (let i = 0; i < 5; i++) {
55+
await this.nextButton.click();
56+
}
57+
}
58+
}

tests/e2e/pages/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,4 @@ export { ServersPage } from './ServersPage';
77
export { RegistryPage } from './RegistryPage';
88
export { FeatureSetsPage } from './FeatureSetsPage';
99
export { ClientsPage } from './ClientsPage';
10+
export { WelcomeGuidePage } from './WelcomeGuidePage';

0 commit comments

Comments
 (0)