Fixed intermittent missing extension override on ui and cleanup (#9575)

Signed-off-by: Angie Jones <jones.angie@gmail.com>
Co-authored-by: Angie Jones <jones.angie@gmail.com>
This commit is contained in:
Lifei Zhou
2026-06-04 01:14:30 +10:00
committed by GitHub
parent 30034b9b32
commit f6caaa03b5
2 changed files with 87 additions and 45 deletions
@@ -1,4 +1,3 @@
import { AppEvents } from '../../constants/events';
import { useCallback, useEffect, useMemo, useState, useRef } from 'react'; import { useCallback, useEffect, useMemo, useState, useRef } from 'react';
import { Puzzle } from 'lucide-react'; import { Puzzle } from 'lucide-react';
import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '../ui/dropdown-menu'; import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '../ui/dropdown-menu';
@@ -16,6 +15,7 @@ import {
getExtensionOverrides, getExtensionOverrides,
} from '../../store/extensionOverrides'; } from '../../store/extensionOverrides';
import { defineMessages, useIntl } from '../../i18n'; import { defineMessages, useIntl } from '../../i18n';
import { AppEvents } from '../../constants/events';
const i18n = defineMessages({ const i18n = defineMessages({
manageExtensions: { manageExtensions: {
@@ -68,6 +68,8 @@ interface BottomMenuExtensionSelectionProps {
sessionId: string | null; sessionId: string | null;
} }
type GetSessionExtensionsSignal = Parameters<typeof getSessionExtensions>[0]['signal'];
export const BottomMenuExtensionSelection = ({ sessionId }: BottomMenuExtensionSelectionProps) => { export const BottomMenuExtensionSelection = ({ sessionId }: BottomMenuExtensionSelectionProps) => {
const intl = useIntl(); const intl = useIntl();
const [searchQuery, setSearchQuery] = useState(''); const [searchQuery, setSearchQuery] = useState('');
@@ -77,29 +79,26 @@ export const BottomMenuExtensionSelection = ({ sessionId }: BottomMenuExtensionS
const [isTransitioning, setIsTransitioning] = useState(false); const [isTransitioning, setIsTransitioning] = useState(false);
const [pendingSort, setPendingSort] = useState(false); const [pendingSort, setPendingSort] = useState(false);
const [togglingExtension, setTogglingExtension] = useState<string | null>(null); const [togglingExtension, setTogglingExtension] = useState<string | null>(null);
const [refreshTrigger, setRefreshTrigger] = useState(0);
const [isSessionExtensionsLoaded, setIsSessionExtensionsLoaded] = useState(false); const [isSessionExtensionsLoaded, setIsSessionExtensionsLoaded] = useState(false);
const sortTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null); const sortTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const latestSessionIdRef = useRef(sessionId);
const { extensionsList: allExtensions } = useConfig(); const { extensionsList: allExtensions } = useConfig();
const isHubView = !sessionId; const isHubView = !sessionId;
useEffect(() => { useEffect(() => {
latestSessionIdRef.current = sessionId;
setIsSessionExtensionsLoaded(false); setIsSessionExtensionsLoaded(false);
setSessionExtensions([]); setSessionExtensions([]);
setPendingSort(false);
setIsTransitioning(false);
setTogglingExtension(null);
if (sortTimeoutRef.current) {
clearTimeout(sortTimeoutRef.current);
sortTimeoutRef.current = null;
}
}, [sessionId]); }, [sessionId]);
useEffect(() => {
const handleExtensionsLoaded = () => {
setRefreshTrigger((prev) => prev + 1);
};
window.addEventListener(AppEvents.SESSION_EXTENSIONS_LOADED, handleExtensionsLoaded);
return () => {
window.removeEventListener(AppEvents.SESSION_EXTENSIONS_LOADED, handleExtensionsLoaded);
};
}, []);
useEffect(() => { useEffect(() => {
return () => { return () => {
if (sortTimeoutRef.current) { if (sortTimeoutRef.current) {
@@ -108,33 +107,71 @@ export const BottomMenuExtensionSelection = ({ sessionId }: BottomMenuExtensionS
}; };
}, []); }, []);
useEffect(() => { const loadSessionExtensions = useCallback(
if (refreshTrigger === 0 && !isOpen) { async (targetSessionId: string, signal?: GetSessionExtensionsSignal) => {
return; const response = await getSessionExtensions({
} path: { session_id: targetSessionId },
signal,
throwOnError: true,
});
const fetchExtensions = async () => { if (signal?.aborted || latestSessionIdRef.current !== targetSessionId) {
if (!sessionId) {
return; return;
} }
try { setSessionExtensions(response.data?.extensions ?? []);
const response = await getSessionExtensions({ setIsSessionExtensionsLoaded(true);
path: { session_id: sessionId }, },
}); []
);
if (response.data?.extensions) { useEffect(() => {
setSessionExtensions(response.data.extensions); if (!sessionId) {
setIsSessionExtensionsLoaded(true); setIsSessionExtensionsLoaded(true);
return;
}
let controller: AbortController | null = null;
const loadExtensionsForCurrentSession = (event: Event) => {
const targetSessionId = (event as CustomEvent<{ sessionId?: string }>).detail?.sessionId;
if (targetSessionId !== sessionId) {
return;
}
controller?.abort();
const currentController = new AbortController();
controller = currentController;
loadSessionExtensions(targetSessionId, currentController.signal).catch((error) => {
if (currentController.signal.aborted || latestSessionIdRef.current !== targetSessionId) {
return;
} }
} catch (error) {
console.error('Failed to fetch session extensions:', error); console.error('Failed to fetch session extensions:', error);
setIsSessionExtensionsLoaded(true); setIsSessionExtensionsLoaded(true);
} });
}; };
fetchExtensions(); window.addEventListener(AppEvents.SESSION_EXTENSIONS_LOADED, loadExtensionsForCurrentSession);
}, [sessionId, isOpen, refreshTrigger]);
return () => {
controller?.abort();
window.removeEventListener(
AppEvents.SESSION_EXTENSIONS_LOADED,
loadExtensionsForCurrentSession
);
};
}, [sessionId, loadSessionExtensions]);
const finishSessionTransition = useCallback((targetSessionId: string) => {
if (latestSessionIdRef.current === targetSessionId) {
setPendingSort(false);
setIsTransitioning(false);
setTogglingExtension(null);
}
}, []);
const handleToggle = useCallback( const handleToggle = useCallback(
async (extensionConfig: FixedExtensionEntry) => { async (extensionConfig: FixedExtensionEntry) => {
@@ -160,6 +197,7 @@ export const BottomMenuExtensionSelection = ({ sessionId }: BottomMenuExtensionS
setPendingSort(false); setPendingSort(false);
setIsTransitioning(false); setIsTransitioning(false);
setTogglingExtension(null); setTogglingExtension(null);
sortTimeoutRef.current = null;
}, 800); }, 800);
toastService.success({ toastService.success({
@@ -196,17 +234,17 @@ export const BottomMenuExtensionSelection = ({ sessionId }: BottomMenuExtensionS
clearTimeout(sortTimeoutRef.current); clearTimeout(sortTimeoutRef.current);
} }
sortTimeoutRef.current = setTimeout(async () => { sortTimeoutRef.current = setTimeout(() => {
const response = await getSessionExtensions({ loadSessionExtensions(sessionId)
path: { session_id: sessionId }, .catch((error) => {
}); if (latestSessionIdRef.current === sessionId) {
console.error('Failed to fetch session extensions:', error);
if (response.data?.extensions) { }
setSessionExtensions(response.data.extensions); })
} .finally(() => {
setPendingSort(false); finishSessionTransition(sessionId);
setIsTransitioning(false); sortTimeoutRef.current = null;
setTogglingExtension(null); });
}, 800); }, 800);
} catch { } catch {
setIsTransitioning(false); setIsTransitioning(false);
@@ -214,7 +252,7 @@ export const BottomMenuExtensionSelection = ({ sessionId }: BottomMenuExtensionS
setTogglingExtension(null); setTogglingExtension(null);
} }
}, },
[sessionId, isHubView, togglingExtension, intl] [sessionId, isHubView, togglingExtension, intl, loadSessionExtensions, finishSessionTransition]
); );
// Merge all available extensions with session-specific or hub override state // Merge all available extensions with session-specific or hub override state
+6 -2
View File
@@ -748,7 +748,9 @@ export function useChatStream({
}, },
}, },
}); });
window.dispatchEvent(new CustomEvent(AppEvents.SESSION_EXTENSIONS_LOADED)); window.dispatchEvent(
new CustomEvent(AppEvents.SESSION_EXTENSIONS_LOADED, { detail: { sessionId } })
);
onSessionLoaded?.(); onSessionLoaded?.();
return; return;
} }
@@ -776,7 +778,9 @@ export function useChatStream({
const extensionResults = resumeData?.extension_results; const extensionResults = resumeData?.extension_results;
showExtensionLoadResults(extensionResults); showExtensionLoadResults(extensionResults);
window.dispatchEvent(new CustomEvent(AppEvents.SESSION_EXTENSIONS_LOADED)); window.dispatchEvent(
new CustomEvent(AppEvents.SESSION_EXTENSIONS_LOADED, { detail: { sessionId } })
);
const pendingRequestId = pendingReattachRequestIdRef.current; const pendingRequestId = pendingReattachRequestIdRef.current;
const reattachedToActiveRequest = activeRequestIdRef.current !== null; const reattachedToActiveRequest = activeRequestIdRef.current !== null;