refactor(settings): scope the settings project selector to settings
Picking a project in Settings called setActiveProject, which relocates the chat, the session list, the file tree and the Git surface. Reading another project's MCP servers or agents moved the user's whole app. It had to, because the configuration stores resolved the directory themselves from the active project and held one flat list. Each of them now takes an explicit directory — omitted still means the active project, so every caller outside Settings is unchanged — and keys loaded data by directory next to a flat mirror of the active project. Chat, autocompletes and pickers keep reading that mirror; a load for another directory writes only the map. A failed load restores that directory's previous list. Settings resolves its own directory through useSettingsDirectory, backed by a session-local settingsProjectPath that follows the active project until the user picks something else.
This commit is contained in:
@@ -7,6 +7,7 @@ import { copyTextToClipboard } from '@/lib/clipboard';
|
||||
import { openExternalUrl } from '@/lib/url';
|
||||
import { isVSCodeRuntime } from '@/lib/desktop';
|
||||
import {
|
||||
selectMcpServersForDirectory,
|
||||
useMcpConfigStore,
|
||||
envRecordToArray,
|
||||
type McpDraft,
|
||||
@@ -19,7 +20,7 @@ import {
|
||||
} from './mcpImport';
|
||||
import { useMcpStore } from '@/stores/useMcpStore';
|
||||
import { usePendingOpenCodeRestartStore } from '@/stores/usePendingOpenCodeRestartStore';
|
||||
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
||||
import { useSettingsDirectory } from '@/hooks/useSettingsDirectory';
|
||||
import { runtimeFetch } from '@/lib/runtime-fetch';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { SettingsPageLayout } from '@/components/sections/shared/SettingsPageLayout';
|
||||
@@ -556,7 +557,6 @@ export const McpPage: React.FC = () => {
|
||||
);
|
||||
const {
|
||||
selectedMcpName,
|
||||
mcpServers,
|
||||
mcpDraft,
|
||||
setMcpDraft,
|
||||
setSelectedMcp,
|
||||
@@ -566,7 +566,6 @@ export const McpPage: React.FC = () => {
|
||||
deleteMcp,
|
||||
} = useMcpConfigStore(useShallow((s) => ({
|
||||
selectedMcpName: s.selectedMcpName,
|
||||
mcpServers: s.mcpServers,
|
||||
mcpDraft: s.mcpDraft,
|
||||
setMcpDraft: s.setMcpDraft,
|
||||
setSelectedMcp: s.setSelectedMcp,
|
||||
@@ -576,10 +575,12 @@ export const McpPage: React.FC = () => {
|
||||
deleteMcp: s.deleteMcp,
|
||||
})));
|
||||
|
||||
const currentDirectory = useDirectoryStore((state) => state.currentDirectory);
|
||||
// Settings browses whichever project its own selector points at; the app
|
||||
// stays where it is.
|
||||
const currentDirectory = useSettingsDirectory();
|
||||
const isVSCodeAuthRuntime = React.useMemo(() => isVSCodeRuntime(), []);
|
||||
const mcpStatus = useMcpStore((state) => state.getStatusForDirectory(currentDirectory ?? null));
|
||||
const mcpDiagnostics = useMcpStore((state) => state.getDiagnosticForDirectory(currentDirectory ?? null));
|
||||
const mcpStatus = useMcpStore((state) => state.getStatusForDirectory(currentDirectory));
|
||||
const mcpDiagnostics = useMcpStore((state) => state.getDiagnosticForDirectory(currentDirectory));
|
||||
const refreshStatus = useMcpStore((state) => state.refresh);
|
||||
const connectMcp = useMcpStore((state) => state.connect);
|
||||
const disconnectMcp = useMcpStore((state) => state.disconnect);
|
||||
@@ -588,7 +589,8 @@ export const McpPage: React.FC = () => {
|
||||
const testConnectionMcp = useMcpStore((state) => state.testConnection);
|
||||
const pendingRestartChanges = usePendingOpenCodeRestartStore((state) => state.changes);
|
||||
|
||||
const selectedServer = selectedMcpName ? getMcpByName(selectedMcpName) : null;
|
||||
const mcpServers = useMcpConfigStore((state) => selectMcpServersForDirectory(state, currentDirectory));
|
||||
const selectedServer = selectedMcpName ? getMcpByName(selectedMcpName, currentDirectory) : null;
|
||||
const isNewServer = Boolean(mcpDraft && mcpDraft.name === selectedMcpName && !selectedServer);
|
||||
|
||||
// ── form state ──
|
||||
@@ -908,7 +910,7 @@ export const McpPage: React.FC = () => {
|
||||
};
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const result = isNewServer ? await createMcp(draft) : await updateMcp(name, draft);
|
||||
const result = isNewServer ? await createMcp(draft, currentDirectory) : await updateMcp(name, draft, currentDirectory);
|
||||
if (result.ok) {
|
||||
await clearPendingMcpAuthContext(authStateKey);
|
||||
resetTransientAuthState();
|
||||
@@ -940,7 +942,7 @@ export const McpPage: React.FC = () => {
|
||||
const handleDelete = async () => {
|
||||
if (!selectedMcpName) return;
|
||||
setIsDeleting(true);
|
||||
const result = await deleteMcp(selectedMcpName);
|
||||
const result = await deleteMcp(selectedMcpName, currentDirectory);
|
||||
if (result.ok) {
|
||||
await clearPendingMcpAuthContext(authStateKey);
|
||||
resetTransientAuthState();
|
||||
@@ -967,7 +969,7 @@ export const McpPage: React.FC = () => {
|
||||
} else {
|
||||
await connectMcp(selectedMcpName, currentDirectory);
|
||||
await refreshStatus({ directory: currentDirectory, silent: true });
|
||||
const nextStatus = useMcpStore.getState().getStatusForDirectory(currentDirectory ?? null)[selectedMcpName];
|
||||
const nextStatus = useMcpStore.getState().getStatusForDirectory(currentDirectory)[selectedMcpName];
|
||||
if (nextStatus?.status === 'connected') {
|
||||
toast.success(t('settings.mcp.page.toast.connected'));
|
||||
} else if (nextStatus?.status === 'needs_auth') {
|
||||
@@ -1029,7 +1031,7 @@ export const McpPage: React.FC = () => {
|
||||
const actionKey = runtimeActionKey;
|
||||
let queuedStateKey: string | null = null;
|
||||
try {
|
||||
const currentStatus = useMcpStore.getState().getStatusForDirectory(currentDirectory ?? null)[selectedMcpName]?.status;
|
||||
const currentStatus = useMcpStore.getState().getStatusForDirectory(currentDirectory)[selectedMcpName]?.status;
|
||||
authPollStartsFromNeedsAuthRef.current = currentStatus === 'needs_auth' || currentStatus === 'needs_client_registration';
|
||||
|
||||
// One implementation for every surface that can authorise; the page
|
||||
@@ -1237,7 +1239,7 @@ export const McpPage: React.FC = () => {
|
||||
void (async () => {
|
||||
authPollAttemptsRef.current += 1;
|
||||
await refreshStatus({ directory: currentDirectory, silent: true });
|
||||
const nextStatus = useMcpStore.getState().getStatusForDirectory(currentDirectory ?? null)[selectedMcpName];
|
||||
const nextStatus = useMcpStore.getState().getStatusForDirectory(currentDirectory)[selectedMcpName];
|
||||
|
||||
if (!nextStatus) {
|
||||
return;
|
||||
|
||||
@@ -8,10 +8,10 @@ import {
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '@/components/ui/dialog';
|
||||
import { useMcpConfigStore, type McpDraft, type McpServerConfig } from '@/stores/useMcpConfigStore';
|
||||
import { selectMcpServersForDirectory, useMcpConfigStore, type McpDraft, type McpServerConfig } from '@/stores/useMcpConfigStore';
|
||||
import { useShallow } from 'zustand/react/shallow';
|
||||
import { useMcpStore } from '@/stores/useMcpStore';
|
||||
import { useDirectoryStore } from '@/stores/useDirectoryStore';
|
||||
import { useSettingsDirectory } from '@/hooks/useSettingsDirectory';
|
||||
import { isMobileDeviceViaCSS } from '@/lib/device';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { toast } from '@/components/ui';
|
||||
@@ -65,9 +65,8 @@ export const McpSidebar: React.FC<McpSidebarProps> = ({ onItemSelect }) => {
|
||||
const { t } = useI18n();
|
||||
const bgClass = 'bg-background';
|
||||
|
||||
const { mcpServers, selectedMcpName, setSelectedMcp, setMcpDraft, loadMcpConfigs, deleteMcp } =
|
||||
const { selectedMcpName, setSelectedMcp, setMcpDraft, loadMcpConfigs, deleteMcp } =
|
||||
useMcpConfigStore(useShallow((s) => ({
|
||||
mcpServers: s.mcpServers,
|
||||
selectedMcpName: s.selectedMcpName,
|
||||
setSelectedMcp: s.setSelectedMcp,
|
||||
setMcpDraft: s.setMcpDraft,
|
||||
@@ -75,8 +74,11 @@ export const McpSidebar: React.FC<McpSidebarProps> = ({ onItemSelect }) => {
|
||||
deleteMcp: s.deleteMcp,
|
||||
})));
|
||||
|
||||
const currentDirectory = useDirectoryStore((state) => state.currentDirectory);
|
||||
const mcpStatus = useMcpStore((state) => state.getStatusForDirectory(currentDirectory ?? null));
|
||||
// Settings browses whichever project its own selector points at; the app
|
||||
// stays where it is.
|
||||
const settingsDirectory = useSettingsDirectory();
|
||||
const mcpServers = useMcpConfigStore((state) => selectMcpServersForDirectory(state, settingsDirectory));
|
||||
const mcpStatus = useMcpStore((state) => state.getStatusForDirectory(settingsDirectory));
|
||||
const refreshStatus = useMcpStore((state) => state.refresh);
|
||||
const getErrorForDirectory = useMcpStore((state) => state.getErrorForDirectory);
|
||||
|
||||
@@ -96,8 +98,8 @@ export const McpSidebar: React.FC<McpSidebarProps> = ({ onItemSelect }) => {
|
||||
);
|
||||
|
||||
React.useEffect(() => {
|
||||
void loadMcpConfigs();
|
||||
}, [loadMcpConfigs]);
|
||||
void loadMcpConfigs({ directory: settingsDirectory });
|
||||
}, [loadMcpConfigs, settingsDirectory]);
|
||||
|
||||
const handleRefresh = React.useCallback(() => {
|
||||
if (isRefreshingStatus) return;
|
||||
@@ -106,17 +108,17 @@ export const McpSidebar: React.FC<McpSidebarProps> = ({ onItemSelect }) => {
|
||||
const minSpinPromise = new Promise((resolve) => setTimeout(resolve, 500));
|
||||
|
||||
Promise.all([
|
||||
refreshStatus({ directory: currentDirectory, silent: true }),
|
||||
refreshStatus({ directory: settingsDirectory, silent: true }),
|
||||
minSpinPromise,
|
||||
]).then(() => {
|
||||
const error = getErrorForDirectory(currentDirectory);
|
||||
const error = getErrorForDirectory(settingsDirectory);
|
||||
if (error) {
|
||||
toast.error(error);
|
||||
}
|
||||
}).finally(() => {
|
||||
setIsRefreshingStatus(false);
|
||||
});
|
||||
}, [currentDirectory, getErrorForDirectory, isRefreshingStatus, refreshStatus]);
|
||||
}, [getErrorForDirectory, isRefreshingStatus, refreshStatus, settingsDirectory]);
|
||||
|
||||
const handleCreateNew = () => {
|
||||
const baseName = 'new-mcp-server';
|
||||
@@ -151,7 +153,7 @@ export const McpSidebar: React.FC<McpSidebarProps> = ({ onItemSelect }) => {
|
||||
const handleDelete = async () => {
|
||||
if (!deleteTarget) return;
|
||||
setIsDeleting(true);
|
||||
const result = await deleteMcp(deleteTarget.name);
|
||||
const result = await deleteMcp(deleteTarget.name, settingsDirectory);
|
||||
if (result.ok) {
|
||||
if (result.reloadFailed) {
|
||||
toast.warning(result.message || `MCP server "${deleteTarget.name}" deleted, but OpenCode reload failed`, {
|
||||
|
||||
Reference in New Issue
Block a user