Skip to content

Commit 5629eb6

Browse files
committed
feat(ui): remember the last client selection in the install panel
The "Connect apps to this folder" checklist defaulted to the common three every time. Persist the user's selection to localStorage (matching how the app stores other UI prefs) and restore it across folders and sessions, reconciled against the currently-supported clients so a dropped client can't leave a stale id (falling back to available defaults if pruning empties it). Test: deselect to one client + install, remount, and confirm the remembered selection is restored rather than the default. Claude-Session: https://claude.ai/code/session_01Baan9JmzR43uxxRUh7CAMF Signed-off-by: Mohammod Al Amin Ashik <maa.ashik00@gmail.com>
1 parent fd0f440 commit 5629eb6

2 files changed

Lines changed: 72 additions & 2 deletions

File tree

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

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,35 @@ import {
1212
type WorkspaceInstallResult,
1313
} from '@/lib/api/workspaceInstall';
1414

15-
/** Clients selected by default the most common three. */
15+
/** Clients selected by default the first time, before the user picks. */
1616
const DEFAULT_SELECTED = ['cursor', 'claude-code', 'vscode'];
1717

18+
/** Where the last client selection is remembered across folders/sessions. */
19+
const SELECTION_STORAGE_KEY = 'mcpmux:workspace-install-clients';
20+
21+
/** Read the remembered client selection, or null when none/invalid. */
22+
function loadSavedSelection(): Set<string> | null {
23+
try {
24+
const raw = localStorage.getItem(SELECTION_STORAGE_KEY);
25+
if (!raw) return null;
26+
const arr: unknown = JSON.parse(raw);
27+
if (Array.isArray(arr) && arr.every((x) => typeof x === 'string')) {
28+
return new Set(arr as string[]);
29+
}
30+
} catch {
31+
/* ignore corrupt / unavailable storage */
32+
}
33+
return null;
34+
}
35+
36+
function saveSelection(ids: Set<string>) {
37+
try {
38+
localStorage.setItem(SELECTION_STORAGE_KEY, JSON.stringify(Array.from(ids)));
39+
} catch {
40+
/* ignore */
41+
}
42+
}
43+
1844
/**
1945
* "Connect apps to this folder" — writes (or extends) project-local MCP configs
2046
* inside `workspaceRoot`, injecting `X-Mcpmux-Workspace: <folder path>` so the
@@ -25,7 +51,11 @@ const DEFAULT_SELECTED = ['cursor', 'claude-code', 'vscode'];
2551
*/
2652
export function WorkspaceInstallPanel({ workspaceRoot }: { workspaceRoot: string }) {
2753
const [clients, setClients] = useState<WorkspaceInstallClient[]>([]);
28-
const [selected, setSelected] = useState<Set<string>>(() => new Set(DEFAULT_SELECTED));
54+
// Restore the user's last selection (remembered across folders); fall back to
55+
// the common-three default the first time.
56+
const [selected, setSelected] = useState<Set<string>>(
57+
() => loadSavedSelection() ?? new Set(DEFAULT_SELECTED)
58+
);
2959
const [mcpUrl, setMcpUrl] = useState<string | null>(null);
3060
const [authDisabled, setAuthDisabled] = useState<boolean | null>(null);
3161
const [installing, setInstalling] = useState(false);
@@ -45,6 +75,13 @@ export function WorkspaceInstallPanel({ workspaceRoot }: { workspaceRoot: string
4575
]);
4676
if (cancelled) return;
4777
setClients(list);
78+
// Drop any remembered ids that aren't supported anymore; if that
79+
// leaves nothing, fall back to the defaults that do exist.
80+
setSelected((prev) => {
81+
const known = new Set(list.map((c) => c.id));
82+
const pruned = [...prev].filter((id) => known.has(id));
83+
return new Set(pruned.length ? pruned : DEFAULT_SELECTED.filter((id) => known.has(id)));
84+
});
4885
setAuthDisabled(disabled);
4986
setMcpUrl(status.url ? `${status.url}/mcp` : null);
5087
} catch (e) {
@@ -56,6 +93,11 @@ export function WorkspaceInstallPanel({ workspaceRoot }: { workspaceRoot: string
5693
};
5794
}, []);
5895

96+
// Remember the selection across folders and sessions.
97+
useEffect(() => {
98+
saveSelection(selected);
99+
}, [selected]);
100+
59101
const toggleClient = (id: string) => {
60102
setSelected((prev) => {
61103
const next = new Set(prev);

tests/ts/components/WorkspaceInstallPanel.test.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ const ROOT = process.platform === 'win32' ? 'd:\\proj\\app' : '/proj/app';
5656

5757
describe('WorkspaceInstallPanel', () => {
5858
beforeEach(() => {
59+
localStorage.clear();
5960
listClientsMock.mockReset().mockResolvedValue(CLIENTS);
6061
installMock.mockReset();
6162
snippetMock.mockReset();
@@ -94,6 +95,33 @@ describe('WorkspaceInstallPanel', () => {
9495
expect(await screen.findByTestId('workspace-install-results')).toBeTruthy();
9596
});
9697

98+
it('remembers the previous client selection across renders', async () => {
99+
const user = userEvent.setup();
100+
installMock.mockResolvedValue([]);
101+
102+
// First mount: deselect the defaults down to just opencode, then install
103+
// (which persists the selection).
104+
const first = render(<WorkspaceInstallPanel workspaceRoot={ROOT} />);
105+
await screen.findByTestId('workspace-install-client-cursor');
106+
for (const id of ['cursor', 'claude-code', 'vscode']) {
107+
await user.click(screen.getByTestId(`workspace-install-client-${id}`));
108+
}
109+
await user.click(screen.getByTestId('workspace-install-client-opencode'));
110+
await user.click(screen.getByTestId('workspace-install-button'));
111+
await waitFor(() => expect(installMock).toHaveBeenCalled());
112+
expect(installMock.mock.calls[0][0].clients).toEqual(['opencode']);
113+
first.unmount();
114+
115+
// Second mount: the remembered selection (opencode only) is restored, not
116+
// the big-three default.
117+
installMock.mockClear();
118+
render(<WorkspaceInstallPanel workspaceRoot={ROOT} />);
119+
await screen.findByTestId('workspace-install-button');
120+
await user.click(screen.getByTestId('workspace-install-button'));
121+
await waitFor(() => expect(installMock).toHaveBeenCalled());
122+
expect(installMock.mock.calls[0][0].clients).toEqual(['opencode']);
123+
});
124+
97125
it('shows the auth nudge and disables auth inline', async () => {
98126
const user = userEvent.setup();
99127
getAuthMock.mockResolvedValue(false); // auth currently required

0 commit comments

Comments
 (0)