👻 refactor: LocalStorage Cleanup and MCP State Optimization (#9528)

* 👻 refactor: MCP Select State with Jotai Atoms

* refactor: Implement timestamp management for ChatArea localStorage entries

* refactor: Integrate MCP Server Manager into BadgeRow context and components to avoid double-calling within BadgeRow

* refactor: add try/catch

* chore: remove comment
This commit is contained in:
Danny Avila 2025-09-09 17:32:10 -04:00 committed by GitHub
parent 519645c0b0
commit 751c2e1d17
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 435 additions and 115 deletions

View file

@ -5,6 +5,7 @@ import { LocalStorageKeys } from 'librechat-data-provider';
import { useAvailablePluginsQuery } from 'librechat-data-provider/react-query';
import type { TStartupConfig, TPlugin, TUser } from 'librechat-data-provider';
import { mapPlugins, selectPlugins, processPlugins } from '~/utils';
import { cleanupTimestampedStorage } from '~/utils/timestamps';
import useSpeechSettingsInit from './useSpeechSettingsInit';
import store from '~/store';
@ -34,6 +35,11 @@ export default function useAppStartup({
useSpeechSettingsInit(!!user);
/** Clean up old localStorage entries on startup */
useEffect(() => {
cleanupTimestampedStorage();
}, []);
/** Set the app title */
useEffect(() => {
const appTitle = startupConfig?.appTitle ?? '';

View file

@ -1,66 +1,50 @@
import { useRef, useCallback, useMemo } from 'react';
import { useCallback, useEffect } from 'react';
import { useAtom } from 'jotai';
import { useRecoilState } from 'recoil';
import { Constants, LocalStorageKeys } from 'librechat-data-provider';
import useLocalStorage from '~/hooks/useLocalStorageAlt';
import { ephemeralAgentByConvoId } from '~/store';
const storageCondition = (value: unknown, rawCurrentValue?: string | null) => {
if (rawCurrentValue) {
try {
const currentValue = rawCurrentValue?.trim() ?? '';
if (currentValue.length > 2) {
return true;
}
} catch (e) {
console.error(e);
}
}
return Array.isArray(value) && value.length > 0;
};
import { ephemeralAgentByConvoId, mcpValuesAtomFamily, mcpPinnedAtom } from '~/store';
import { setTimestamp } from '~/utils/timestamps';
export function useMCPSelect({ conversationId }: { conversationId?: string | null }) {
const key = conversationId ?? Constants.NEW_CONVO;
const [isPinned, setIsPinned] = useAtom(mcpPinnedAtom);
const [mcpValues, setMCPValuesRaw] = useAtom(mcpValuesAtomFamily(key));
const [ephemeralAgent, setEphemeralAgent] = useRecoilState(ephemeralAgentByConvoId(key));
const storageKey = `${LocalStorageKeys.LAST_MCP_}${key}`;
const mcpState = useMemo(() => {
return ephemeralAgent?.mcp ?? [];
}, [ephemeralAgent?.mcp]);
// Sync Jotai state with ephemeral agent state
useEffect(() => {
if (ephemeralAgent?.mcp && ephemeralAgent.mcp.length > 0) {
setMCPValuesRaw(ephemeralAgent.mcp);
}
}, [ephemeralAgent?.mcp, setMCPValuesRaw]);
const setSelectedValues = useCallback(
(values: string[] | null | undefined) => {
if (!values) {
return;
}
if (!Array.isArray(values)) {
return;
}
// Update ephemeral agent when Jotai state changes
useEffect(() => {
if (mcpValues.length > 0 && JSON.stringify(mcpValues) !== JSON.stringify(ephemeralAgent?.mcp)) {
setEphemeralAgent((prev) => ({
...prev,
mcp: values,
mcp: mcpValues,
}));
}
}, [mcpValues, ephemeralAgent?.mcp, setEphemeralAgent]);
useEffect(() => {
const mcpStorageKey = `${LocalStorageKeys.LAST_MCP_}${key}`;
if (mcpValues.length > 0) {
setTimestamp(mcpStorageKey);
}
}, [mcpValues, key]);
/** Stable memoized setter */
const setMCPValues = useCallback(
(value: string[]) => {
if (!Array.isArray(value)) {
return;
}
setMCPValuesRaw(value);
},
[setEphemeralAgent],
);
const [mcpValues, setMCPValuesRaw] = useLocalStorage<string[]>(
storageKey,
mcpState,
setSelectedValues,
storageCondition,
);
const setMCPValuesRawRef = useRef(setMCPValuesRaw);
setMCPValuesRawRef.current = setMCPValuesRaw;
/** Create a stable memoized setter to avoid re-creating it on every render and causing an infinite render loop */
const setMCPValues = useCallback((value: string[]) => {
setMCPValuesRawRef.current(value);
}, []);
const [isPinned, setIsPinned] = useLocalStorage<boolean>(
`${LocalStorageKeys.PIN_MCP_}${key}`,
true,
[setMCPValuesRaw],
);
return {

View file

@ -25,9 +25,8 @@ export function useMCPServerManager({ conversationId }: { conversationId?: strin
const queryClient = useQueryClient();
const { showToast } = useToastContext();
const { mcpToolDetails } = useGetMCPTools();
const mcpSelect = useMCPSelect({ conversationId });
const { data: startupConfig } = useGetStartupConfig();
const { mcpValues, setMCPValues, isPinned, setIsPinned } = mcpSelect;
const { mcpValues, setMCPValues, isPinned, setIsPinned } = useMCPSelect({ conversationId });
const [isConfigModalOpen, setIsConfigModalOpen] = useState(false);
const [selectedToolForConfig, setSelectedToolForConfig] = useState<TPlugin | null>(null);

View file

@ -5,23 +5,10 @@ import { Constants, LocalStorageKeys } from 'librechat-data-provider';
import type { VerifyToolAuthResponse } from 'librechat-data-provider';
import type { UseQueryOptions } from '@tanstack/react-query';
import { useVerifyAgentToolAuth } from '~/data-provider';
import { setTimestamp } from '~/utils/timestamps';
import useLocalStorage from '~/hooks/useLocalStorageAlt';
import { ephemeralAgentByConvoId } from '~/store';
const storageCondition = (value: unknown, rawCurrentValue?: string | null) => {
if (rawCurrentValue) {
try {
const currentValue = rawCurrentValue?.trim() ?? '';
if (currentValue === 'true' && value === false) {
return true;
}
} catch (e) {
console.error(e);
}
}
return value !== undefined && value !== null;
};
type ToolValue = boolean | string;
interface UseToolToggleOptions {
@ -39,7 +26,7 @@ interface UseToolToggleOptions {
export function useToolToggle({
conversationId,
toolKey,
toolKey: _toolKey,
localStorageKey,
isAuthenticated: externalIsAuthenticated,
setIsDialogOpen,
@ -62,13 +49,8 @@ export function useToolToggle({
[externalIsAuthenticated, authConfig, authQuery.data?.authenticated],
);
// Keep localStorage in sync
const [, setLocalStorageValue] = useLocalStorage<ToolValue>(
`${localStorageKey}${key}`,
false,
undefined,
storageCondition,
);
const toolKey = useMemo(() => _toolKey, [_toolKey]);
const storageKey = useMemo(() => `${localStorageKey}${key}`, [localStorageKey, key]);
// The actual current value comes from ephemeralAgent
const toolValue = useMemo(() => {
@ -83,13 +65,14 @@ export function useToolToggle({
return toolValue === true;
}, [toolValue]);
// Sync to localStorage when ephemeralAgent changes
// Sync to localStorage with timestamps when ephemeralAgent changes
useEffect(() => {
const value = ephemeralAgent?.[toolKey];
if (value !== undefined) {
setLocalStorageValue(value);
localStorage.setItem(storageKey, JSON.stringify(value));
setTimestamp(storageKey);
}
}, [ephemeralAgent, toolKey, setLocalStorageValue]);
}, [ephemeralAgent, toolKey, storageKey]);
const [isPinned, setIsPinned] = useLocalStorage<boolean>(`${localStorageKey}pinned`, false);