Skip to content

Commit 9731a24

Browse files
its-mashMohammod Al Amin Ashik
authored andcommitted
feat(P2): make id-mapping creation reachable via the create wizard
The Folder/ID toggle now lives in WorkspaceSetupWizard (the actual create path, reached from both "New mapping" and the Home "Set up a folder" card) instead of the inspector's edit-only form, where mode==='create' never rendered so the toggle was dead. ID mode takes a free-text label (step 1) and shows the X-Mcpmux-Workspace value to copy instead of the app-config install panel (step 2), persisting binding_type='id'. The Home folder CTA opens this wizard in folder mode (the default), so it follows the new mapping design. Removes the now-dead toggle from the inspector's BindingForm (edit keeps its id-handling). Signed-off-by: Mohammod Al Amin Ashik <maa.ashik00@gmail.com>
1 parent 1a2e602 commit 9731a24

3 files changed

Lines changed: 167 additions & 97 deletions

File tree

apps/desktop/src/features/home/HomePage.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -163,8 +163,9 @@ function GetStartedStrip() {
163163

164164
/**
165165
* Per-folder setup entry point. The ConnectionCard above connects an app to
166-
* the gateway globally; this routes into the Workspaces walkthrough to map a
167-
* specific project and write its per-folder config.
166+
* the gateway globally; this routes into the Mapping walkthrough (the create
167+
* wizard, in folder mode) to map a specific project and write its per-folder
168+
* config.
168169
*/
169170
function SetUpFolderCard() {
170171
const navigateTo = useNavigateTo();
@@ -179,7 +180,7 @@ function SetUpFolderCard() {
179180
data-testid="home-setup-folder"
180181
className="group flex w-full items-center gap-3 rounded-xl border border-[rgb(var(--border-subtle))] bg-[rgb(var(--card))] p-4 text-left shadow transition-all duration-200 hover:-translate-y-0.5 hover:border-[rgb(var(--border))] hover:shadow-md"
181182
>
182-
<span className="flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg bg-[rgb(var(--primary))]/12 text-[rgb(var(--primary))]">
183+
<span className="bg-[rgb(var(--primary))]/12 flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-lg text-[rgb(var(--primary))]">
183184
<FolderPlus className="h-5 w-5" />
184185
</span>
185186
<span className="min-w-0 flex-1">
@@ -275,7 +276,7 @@ export function HomePage() {
275276
pending-approval nudge. */}
276277
<ConnectionCard />
277278

278-
{/* Per-folder setup — opens the Workspaces walkthrough. */}
279+
{/* Per-folder setup — opens the Mapping walkthrough (folder mode). */}
279280
<SetUpFolderCard />
280281

281282
{/* Stat tiles — each is a shortcut into the page that manages it. */}

apps/desktop/src/features/workspaces/WorkspaceSetupWizard.tsx

Lines changed: 158 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import {
55
ArrowLeft,
66
ArrowRight,
77
Check,
8+
Copy,
89
FolderOpen,
910
FolderSearch,
1011
Layers,
@@ -53,6 +54,11 @@ export function WorkspaceSetupWizard({
5354
onError: (msg: string) => void;
5455
}) {
5556
const [step, setStep] = useState<1 | 2 | 3>(1);
57+
// A mapping is keyed by a folder PATH (the default) or an arbitrary ID/label
58+
// (a client id, machine name, … — for headless/remote clients). `folder`
59+
// holds whichever value the user enters.
60+
const [bindingType, setBindingType] = useState<'path' | 'id'>('path');
61+
const isId = bindingType === 'id';
5662
const [folder, setFolder] = useState('');
5763
const [validating, setValidating] = useState(false);
5864
const [saving, setSaving] = useState(false);
@@ -127,6 +133,7 @@ export function WorkspaceSetupWizard({
127133
workspace_root: folder,
128134
space_id: spaceId,
129135
feature_set_ids: Array.from(fsIds),
136+
binding_type: bindingType,
130137
});
131138
// The parent transitions to the new mapping's inspector (which shows its
132139
// effective features) — don't close here, or that view would be lost.
@@ -136,19 +143,21 @@ export function WorkspaceSetupWizard({
136143
}
137144
};
138145

139-
const TITLES = ['Choose a folder', 'Connect your apps', 'Choose its tools'] as const;
146+
const TITLES = isId
147+
? (['Choose an id', 'How clients connect', 'Choose its tools'] as const)
148+
: (['Choose a folder', 'Connect your apps', 'Choose its tools'] as const);
140149

141150
return (
142151
<div
143-
className="fixed right-0 top-0 bottom-0 z-50 flex w-full min-w-[420px] max-w-[480px] flex-col border-l border-[rgb(var(--border))] bg-[rgb(var(--surface))] shadow-2xl animate-in slide-in-from-right duration-300"
152+
className="animate-in slide-in-from-right fixed bottom-0 right-0 top-0 z-50 flex w-full min-w-[420px] max-w-[480px] flex-col border-l border-[rgb(var(--border))] bg-[rgb(var(--surface))] shadow-2xl duration-300"
144153
data-testid="workspace-setup-wizard"
145154
>
146155
{/* Header + progress */}
147156
<div className="flex-shrink-0 border-b border-[rgb(var(--border))] bg-[rgb(var(--surface-elevated))] p-4">
148157
<div className="flex items-start justify-between">
149158
<div className="min-w-0">
150159
<div className="text-xs font-medium uppercase tracking-wider text-[rgb(var(--muted))]">
151-
Set up a folder · Step {step} of 3
160+
{isId ? 'Set up an ID mapping' : 'Set up a folder'} · Step {step} of 3
152161
</div>
153162
<h2 className="mt-0.5 text-lg font-bold">{TITLES[step - 1]}</h2>
154163
</div>
@@ -175,80 +184,154 @@ export function WorkspaceSetupWizard({
175184
<div className="flex-1 overflow-y-auto p-6">
176185
{step === 1 && (
177186
<div className="space-y-4" data-testid="wizard-step-folder">
178-
<p className="text-sm text-[rgb(var(--muted))]">
179-
Which project folder do you want to map? Pick one, or choose a folder an app already
180-
opened.
181-
</p>
182-
<Button variant="primary" size="sm" onClick={pickFolder} disabled={validating}>
183-
{validating ? (
184-
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
185-
) : (
186-
<FolderOpen className="mr-2 h-4 w-4" />
187-
)}
188-
Choose folder…
189-
</Button>
187+
{/* Folder vs ID — a folder routes editors by the path they open; an
188+
id routes a headless/remote client by an exact label it sends. */}
189+
<div className="flex gap-1 rounded-lg border border-[rgb(var(--border))] bg-[rgb(var(--background))] p-1">
190+
{(['path', 'id'] as const).map((t) => (
191+
<button
192+
key={t}
193+
type="button"
194+
onClick={() => setBindingType(t)}
195+
className={[
196+
'flex-1 rounded-md px-3 py-1.5 text-xs font-medium transition-colors',
197+
bindingType === t
198+
? 'bg-primary-500 text-white'
199+
: 'text-[rgb(var(--muted))] hover:bg-[rgb(var(--surface-hover))]',
200+
].join(' ')}
201+
data-testid={`wizard-type-${t}`}
202+
>
203+
{t === 'path' ? 'Folder' : 'ID / label'}
204+
</button>
205+
))}
206+
</div>
190207

191-
{folder && (
192-
<div
193-
className={`flex items-center gap-2 rounded-lg border px-3 py-2 ${
194-
alreadyMapped
195-
? 'border-amber-300 bg-amber-50 dark:border-amber-800/60 dark:bg-amber-900/20'
196-
: 'border-[rgb(var(--border))] bg-[rgb(var(--background))]'
197-
}`}
198-
>
199-
{alreadyMapped ? (
200-
<AlertCircle className="h-4 w-4 flex-shrink-0 text-amber-600" />
201-
) : (
202-
<Check className="h-4 w-4 flex-shrink-0 text-green-600" />
208+
{isId ? (
209+
<>
210+
<p className="text-sm text-[rgb(var(--muted))]">
211+
Enter an id or label — a client id, machine name, or any string. A headless or
212+
remote client that sends this exact value in the{' '}
213+
<code className="font-mono text-xs">X-Mcpmux-Workspace</code> header gets the
214+
tools you choose next.
215+
</p>
216+
<input
217+
type="text"
218+
value={folder}
219+
onChange={(e) => setFolder(e.target.value)}
220+
placeholder="e.g. a client id or machine name"
221+
className="focus:ring-primary-500 w-full rounded-lg border border-[rgb(var(--border))] bg-[rgb(var(--background))] px-3 py-2 font-mono text-sm focus:outline-none focus:ring-2"
222+
data-testid="wizard-id-input"
223+
/>
224+
{alreadyMapped && (
225+
<p
226+
className="text-xs text-amber-700 dark:text-amber-400"
227+
data-testid="wizard-folder-mapped-error"
228+
>
229+
That id is already mapped — edit it from the Mapping list instead.
230+
</p>
203231
)}
204-
<span className="truncate font-mono text-xs" title={folder}>
205-
{folder}
206-
</span>
207-
</div>
208-
)}
209-
{alreadyMapped && (
210-
<p
211-
className="text-xs text-amber-700 dark:text-amber-400"
212-
data-testid="wizard-folder-mapped-error"
213-
>
214-
This folder is already mapped — edit it from the Workspaces list instead.
215-
</p>
216-
)}
232+
</>
233+
) : (
234+
<>
235+
<p className="text-sm text-[rgb(var(--muted))]">
236+
Which project folder do you want to map? Pick one, or choose a folder an app
237+
already opened.
238+
</p>
239+
<Button variant="primary" size="sm" onClick={pickFolder} disabled={validating}>
240+
{validating ? (
241+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
242+
) : (
243+
<FolderOpen className="mr-2 h-4 w-4" />
244+
)}
245+
Choose folder…
246+
</Button>
217247

218-
{unmappedRoots.length > 0 && (
219-
<div>
220-
<div className="mb-1.5 flex items-center gap-1.5 text-xs font-medium text-[rgb(var(--muted))]">
221-
<FolderSearch className="h-3.5 w-3.5" />
222-
Detected workspaces
223-
</div>
224-
<div className="overflow-hidden rounded-lg border border-[rgb(var(--border))]">
225-
{unmappedRoots.slice(0, 6).map((r, i) => (
226-
<button
227-
key={r}
228-
type="button"
229-
onClick={() => setFolder(r)}
230-
className={`flex w-full items-center gap-2 px-3 py-2 text-left transition-colors hover:bg-[rgb(var(--surface-hover))] ${
231-
i > 0 ? 'border-t border-[rgb(var(--border-subtle))]' : ''
232-
} ${folder === r ? 'bg-primary-500/5' : ''}`}
233-
>
234-
<FolderOpen className="h-4 w-4 flex-shrink-0 text-[rgb(var(--muted))]" />
235-
<span className="truncate font-mono text-xs" title={r}>
236-
{r}
237-
</span>
238-
</button>
239-
))}
240-
</div>
241-
</div>
248+
{folder && (
249+
<div
250+
className={`flex items-center gap-2 rounded-lg border px-3 py-2 ${
251+
alreadyMapped
252+
? 'border-amber-300 bg-amber-50 dark:border-amber-800/60 dark:bg-amber-900/20'
253+
: 'border-[rgb(var(--border))] bg-[rgb(var(--background))]'
254+
}`}
255+
>
256+
{alreadyMapped ? (
257+
<AlertCircle className="h-4 w-4 flex-shrink-0 text-amber-600" />
258+
) : (
259+
<Check className="h-4 w-4 flex-shrink-0 text-green-600" />
260+
)}
261+
<span className="truncate font-mono text-xs" title={folder}>
262+
{folder}
263+
</span>
264+
</div>
265+
)}
266+
{alreadyMapped && (
267+
<p
268+
className="text-xs text-amber-700 dark:text-amber-400"
269+
data-testid="wizard-folder-mapped-error"
270+
>
271+
This folder is already mapped — edit it from the Workspaces list instead.
272+
</p>
273+
)}
274+
275+
{unmappedRoots.length > 0 && (
276+
<div>
277+
<div className="mb-1.5 flex items-center gap-1.5 text-xs font-medium text-[rgb(var(--muted))]">
278+
<FolderSearch className="h-3.5 w-3.5" />
279+
Detected workspaces
280+
</div>
281+
<div className="overflow-hidden rounded-lg border border-[rgb(var(--border))]">
282+
{unmappedRoots.slice(0, 6).map((r, i) => (
283+
<button
284+
key={r}
285+
type="button"
286+
onClick={() => setFolder(r)}
287+
className={`flex w-full items-center gap-2 px-3 py-2 text-left transition-colors hover:bg-[rgb(var(--surface-hover))] ${
288+
i > 0 ? 'border-t border-[rgb(var(--border-subtle))]' : ''
289+
} ${folder === r ? 'bg-primary-500/5' : ''}`}
290+
>
291+
<FolderOpen className="h-4 w-4 flex-shrink-0 text-[rgb(var(--muted))]" />
292+
<span className="truncate font-mono text-xs" title={r}>
293+
{r}
294+
</span>
295+
</button>
296+
))}
297+
</div>
298+
</div>
299+
)}
300+
</>
242301
)}
243302
</div>
244303
)}
245304

246305
{step === 2 && (
247306
<div className="space-y-3" data-testid="wizard-step-apps">
248-
<WorkspaceInstallPanel workspaceRoot={folder} />
249-
<p className="text-center text-xs text-[rgb(var(--muted))]">
250-
Optional — you can connect apps later from this folder&apos;s mapping.
251-
</p>
307+
{isId ? (
308+
<div className="space-y-3">
309+
<p className="text-sm text-[rgb(var(--muted))]">
310+
A headless or remote client routes here by sending this id in the{' '}
311+
<code className="font-mono text-xs">X-Mcpmux-Workspace</code> header. There&apos;s
312+
no folder to auto-write app config for — copy the value into your client.
313+
</p>
314+
<div className="flex items-stretch gap-2">
315+
<code className="flex-1 select-all break-all rounded-lg border border-[rgb(var(--border))] bg-[rgb(var(--background))] px-3 py-2 font-mono text-xs">
316+
{folder || '—'}
317+
</code>
318+
<Button
319+
variant="secondary"
320+
size="sm"
321+
onClick={() => void navigator.clipboard.writeText(folder).catch(() => {})}
322+
>
323+
<Copy className="h-3.5 w-3.5" />
324+
</Button>
325+
</div>
326+
</div>
327+
) : (
328+
<>
329+
<WorkspaceInstallPanel workspaceRoot={folder} />
330+
<p className="text-center text-xs text-[rgb(var(--muted))]">
331+
Optional — you can connect apps later from this folder&apos;s mapping.
332+
</p>
333+
</>
334+
)}
252335
</div>
253336
)}
254337

@@ -299,9 +382,9 @@ export function WorkspaceSetupWizard({
299382
type="checkbox"
300383
checked={fsIds.has(fs.id)}
301384
onChange={() => toggleFs(fs.id)}
302-
className="h-4 w-4 flex-shrink-0 accent-primary-500"
385+
className="accent-primary-500 h-4 w-4 flex-shrink-0"
303386
/>
304-
<Layers className="h-4 w-4 flex-shrink-0 text-primary-500" />
387+
<Layers className="text-primary-500 h-4 w-4 flex-shrink-0" />
305388
<span className="min-w-0 flex-1 truncate text-sm font-medium">{fs.name}</span>
306389
{isStarterFeatureSet(fs) && (
307390
<span className="flex-shrink-0 rounded-full bg-[rgb(var(--surface))] px-1.5 text-[10px] font-semibold uppercase tracking-wider text-[rgb(var(--muted))]">
@@ -354,7 +437,11 @@ export function WorkspaceSetupWizard({
354437
disabled={saving || fsIds.size === 0 || !folder}
355438
data-testid="wizard-finish"
356439
>
357-
{saving ? <Loader2 className="mr-1.5 h-4 w-4 animate-spin" /> : <Wrench className="mr-1.5 h-4 w-4" />}
440+
{saving ? (
441+
<Loader2 className="mr-1.5 h-4 w-4 animate-spin" />
442+
) : (
443+
<Wrench className="mr-1.5 h-4 w-4" />
444+
)}
358445
Finish
359446
</Button>
360447
)}

apps/desktop/src/features/workspaces/WorkspacesPage.tsx

Lines changed: 4 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1610,7 +1610,10 @@ function BindingForm({
16101610
const [fsIds, setFsIds] = useState<string[]>(initial?.feature_set_ids ?? []);
16111611
// A mapping is keyed by a folder path OR an arbitrary id/label. The type is
16121612
// chosen at create time and fixed thereafter (an id never becomes a folder).
1613-
const [bindingType, setBindingType] = useState<'path' | 'id'>(initial?.binding_type ?? 'path');
1613+
// Mapping type is chosen in the create wizard and fixed thereafter; here
1614+
// (edit / create-from-live) we only read it so an id mapping isn't
1615+
// re-validated as a filesystem path.
1616+
const bindingType = initial?.binding_type ?? 'path';
16141617
const isId = bindingType === 'id';
16151618
const [fsSearch, setFsSearch] = useState('');
16161619
const [submitting, setSubmitting] = useState(false);
@@ -1832,27 +1835,6 @@ function BindingForm({
18321835
: 'Pick a folder, then choose the tools it should get. Whenever you open that folder in a connected app — Cursor, VS Code, Claude — McpMux hands it exactly the tools you choose here, and nothing else.'}
18331836
</div>
18341837

1835-
{mode === 'create' && (
1836-
<div className="flex gap-1 rounded-lg border border-[rgb(var(--border))] bg-[rgb(var(--background))] p-1">
1837-
{(['path', 'id'] as const).map((t) => (
1838-
<button
1839-
key={t}
1840-
type="button"
1841-
onClick={() => setBindingType(t)}
1842-
className={[
1843-
'flex-1 rounded-md px-3 py-1.5 text-xs font-medium transition-colors',
1844-
bindingType === t
1845-
? 'bg-primary-500 text-white'
1846-
: 'text-[rgb(var(--muted))] hover:bg-[rgb(var(--surface-hover))]',
1847-
].join(' ')}
1848-
data-testid={`workspace-binding-type-${t}`}
1849-
>
1850-
{t === 'path' ? 'Folder' : 'ID / label'}
1851-
</button>
1852-
))}
1853-
</div>
1854-
)}
1855-
18561838
<FormField label={isId ? 'Mapping ID / label' : 'Workspace folder'}>
18571839
<div className="flex gap-2">
18581840
<input

0 commit comments

Comments
 (0)