🪟 fix: Tab Isolation for Agent Favorites + MCP Selections (#11786)
Some checks are pending
Docker Dev Branch Images Build / build (Dockerfile, lc-dev, node) (push) Waiting to run
Docker Dev Branch Images Build / build (Dockerfile.multi, lc-dev-api, api-build) (push) Waiting to run

* 🔧 refactor: Implement tab-isolated storage for favorites and MCP selections

- Replaced `createStorageAtom` with `createTabIsolatedAtom` in favorites store to prevent cross-tab synchronization of favorites.
- Introduced `createTabIsolatedStorage` and `createTabIsolatedAtom` in `jotai-utils` to facilitate tab-specific state management.
- Updated MCP values atom family to utilize tab-isolated storage, ensuring independent MCP server selections across tabs.

* 🔧 fix: Update MCP selection logic to ensure active MCPs are only set when configured servers are available

- Modified the condition in `useMCPSelect` to check for both available MCPs and configured servers before setting MCP values. This change prevents potential issues when no servers are configured, enhancing the reliability of MCP selections.
This commit is contained in:
Danny Avila 2026-02-13 14:54:49 -05:00 committed by GitHub
parent e50f59062f
commit dc489e7b25
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 75 additions and 4 deletions

View file

@ -28,7 +28,7 @@ export function useMCPSelect({
const mcps = ephemeralAgent?.mcp ?? [];
if (mcps.length === 1 && mcps[0] === Constants.mcp_clear) {
setMCPValuesRaw([]);
} else if (mcps.length > 0) {
} else if (mcps.length > 0 && configuredServers.size > 0) {
// Strip out servers that are not available in the startup config
const activeMcps = mcps.filter((mcp) => configuredServers.has(mcp));
setMCPValuesRaw(activeMcps);

View file

@ -1,4 +1,4 @@
import { createStorageAtom } from './jotai-utils';
import { createTabIsolatedAtom } from './jotai-utils';
export type Favorite = {
agentId?: string;
@ -16,4 +16,4 @@ export type FavoritesState = Favorite[];
/**
* This atom stores the user's favorite models/agents
*/
export const favoritesAtom = createStorageAtom<FavoritesState>('favorites', []);
export const favoritesAtom = createTabIsolatedAtom<FavoritesState>('favorites', []);

View file

@ -1,5 +1,6 @@
import { atom } from 'jotai';
import { atomWithStorage } from 'jotai/utils';
import type { SyncStorage } from 'jotai/vanilla/utils/atomWithStorage';
/**
* Create a simple atom with localStorage persistence
@ -42,6 +43,68 @@ export function createStorageAtomWithEffect<T>(
);
}
/**
* Create a SyncStorage adapter that reads/writes to localStorage but does NOT
* subscribe to browser `storage` events. This prevents cross-tab synchronization
* for atoms where each tab should maintain independent state.
*
* Use this for atoms that represent per-tab working state (e.g., favorites toggle,
* MCP server selections) rather than user preferences.
*/
export function createTabIsolatedStorage<Value>(): SyncStorage<Value> {
return {
getItem(key: string, initialValue: Value): Value {
if (typeof window === 'undefined') {
return initialValue;
}
try {
const stored = localStorage.getItem(key);
if (stored === null) {
return initialValue;
}
return JSON.parse(stored) as Value;
} catch {
return initialValue;
}
},
setItem(key: string, newValue: Value): void {
if (typeof window === 'undefined') {
return;
}
try {
localStorage.setItem(key, JSON.stringify(newValue));
} catch {
// quota exceeded or other write error — silently ignore
}
},
removeItem(key: string): void {
if (typeof window === 'undefined') {
return;
}
try {
localStorage.removeItem(key);
} catch {
// silently ignore
}
},
// subscribe intentionally omitted — prevents cross-tab sync via storage events
};
}
/**
* Create an atom with localStorage persistence that does NOT sync across tabs.
* Parallels `createStorageAtom` but uses tab-isolated storage.
*
* @param key - localStorage key
* @param defaultValue - default value if no saved value exists
* @returns Jotai atom with localStorage persistence, isolated per tab
*/
export function createTabIsolatedAtom<T>(key: string, defaultValue: T) {
return atomWithStorage<T>(key, defaultValue, createTabIsolatedStorage<T>(), {
getOnInit: true,
});
}
/**
* Initialize a value from localStorage and optionally apply it
* Useful for applying saved values on app startup (e.g., theme, fontSize)

View file

@ -1,6 +1,14 @@
import { atom } from 'jotai';
import { atomFamily, atomWithStorage } from 'jotai/utils';
import { Constants, LocalStorageKeys } from 'librechat-data-provider';
import { createTabIsolatedStorage } from './jotai-utils';
/**
* Tab-isolated storage for MCP values prevents cross-tab sync so that
* each tab's MCP server selections are independent (especially for new chats
* which all share the same `LAST_MCP_new` localStorage key).
*/
const mcpTabIsolatedStorage = createTabIsolatedStorage<string[]>();
/**
* Creates a storage atom for MCP values per conversation
@ -10,7 +18,7 @@ export const mcpValuesAtomFamily = atomFamily((conversationId: string | null) =>
const key = conversationId ?? Constants.NEW_CONVO;
const storageKey = `${LocalStorageKeys.LAST_MCP_}${key}`;
return atomWithStorage<string[]>(storageKey, [], undefined, { getOnInit: true });
return atomWithStorage<string[]>(storageKey, [], mcpTabIsolatedStorage, { getOnInit: true });
});
/**