From ee8fb7bba0df91b6f539d286eacae966dbef9585 Mon Sep 17 00:00:00 2001 From: Lifei Zhou Date: Wed, 3 Jun 2026 14:13:51 +1000 Subject: [PATCH 1/5] fixed intermittent missing extension override on ui and cleanup --- .../BottomMenuExtensionSelection.tsx | 27 +++---------------- 1 file changed, 4 insertions(+), 23 deletions(-) diff --git a/ui/desktop/src/components/bottom_menu/BottomMenuExtensionSelection.tsx b/ui/desktop/src/components/bottom_menu/BottomMenuExtensionSelection.tsx index 2128b134631c..7e041a3017a5 100644 --- a/ui/desktop/src/components/bottom_menu/BottomMenuExtensionSelection.tsx +++ b/ui/desktop/src/components/bottom_menu/BottomMenuExtensionSelection.tsx @@ -1,4 +1,3 @@ -import { AppEvents } from '../../constants/events'; import { useCallback, useEffect, useMemo, useState, useRef } from 'react'; import { Puzzle } from 'lucide-react'; import { DropdownMenu, DropdownMenuContent, DropdownMenuTrigger } from '../ui/dropdown-menu'; @@ -77,7 +76,6 @@ export const BottomMenuExtensionSelection = ({ sessionId }: BottomMenuExtensionS const [isTransitioning, setIsTransitioning] = useState(false); const [pendingSort, setPendingSort] = useState(false); const [togglingExtension, setTogglingExtension] = useState(null); - const [refreshTrigger, setRefreshTrigger] = useState(0); const [isSessionExtensionsLoaded, setIsSessionExtensionsLoaded] = useState(false); const sortTimeoutRef = useRef | null>(null); const { extensionsList: allExtensions } = useConfig(); @@ -88,18 +86,6 @@ export const BottomMenuExtensionSelection = ({ sessionId }: BottomMenuExtensionS setSessionExtensions([]); }, [sessionId]); - useEffect(() => { - const handleExtensionsLoaded = () => { - setRefreshTrigger((prev) => prev + 1); - }; - - window.addEventListener(AppEvents.SESSION_EXTENSIONS_LOADED, handleExtensionsLoaded); - - return () => { - window.removeEventListener(AppEvents.SESSION_EXTENSIONS_LOADED, handleExtensionsLoaded); - }; - }, []); - useEffect(() => { return () => { if (sortTimeoutRef.current) { @@ -109,12 +95,9 @@ export const BottomMenuExtensionSelection = ({ sessionId }: BottomMenuExtensionS }, []); useEffect(() => { - if (refreshTrigger === 0 && !isOpen) { - return; - } - const fetchExtensions = async () => { if (!sessionId) { + setIsSessionExtensionsLoaded(true); return; } @@ -123,18 +106,16 @@ export const BottomMenuExtensionSelection = ({ sessionId }: BottomMenuExtensionS path: { session_id: sessionId }, }); - if (response.data?.extensions) { - setSessionExtensions(response.data.extensions); - setIsSessionExtensionsLoaded(true); - } + setSessionExtensions(response.data?.extensions ?? []); } catch (error) { console.error('Failed to fetch session extensions:', error); + } finally { setIsSessionExtensionsLoaded(true); } }; fetchExtensions(); - }, [sessionId, isOpen, refreshTrigger]); + }, [sessionId]); const handleToggle = useCallback( async (extensionConfig: FixedExtensionEntry) => { From 891bb9f4286ed174ddea3d022affa425b16055af Mon Sep 17 00:00:00 2001 From: Lifei Zhou Date: Wed, 3 Jun 2026 14:56:03 +1000 Subject: [PATCH 2/5] address comments --- .../BottomMenuExtensionSelection.tsx | 79 ++++++++++++------- 1 file changed, 52 insertions(+), 27 deletions(-) diff --git a/ui/desktop/src/components/bottom_menu/BottomMenuExtensionSelection.tsx b/ui/desktop/src/components/bottom_menu/BottomMenuExtensionSelection.tsx index 7e041a3017a5..3f0bafafe5d0 100644 --- a/ui/desktop/src/components/bottom_menu/BottomMenuExtensionSelection.tsx +++ b/ui/desktop/src/components/bottom_menu/BottomMenuExtensionSelection.tsx @@ -78,10 +78,12 @@ export const BottomMenuExtensionSelection = ({ sessionId }: BottomMenuExtensionS const [togglingExtension, setTogglingExtension] = useState(null); const [isSessionExtensionsLoaded, setIsSessionExtensionsLoaded] = useState(false); const sortTimeoutRef = useRef | null>(null); + const latestSessionIdRef = useRef(sessionId); const { extensionsList: allExtensions } = useConfig(); const isHubView = !sessionId; useEffect(() => { + latestSessionIdRef.current = sessionId; setIsSessionExtensionsLoaded(false); setSessionExtensions([]); }, [sessionId]); @@ -94,28 +96,52 @@ export const BottomMenuExtensionSelection = ({ sessionId }: BottomMenuExtensionS }; }, []); - useEffect(() => { - const fetchExtensions = async () => { - if (!sessionId) { - setIsSessionExtensionsLoaded(true); + const loadSessionExtensions = useCallback( + async (targetSessionId: string, signal?: AbortSignal) => { + const response = await getSessionExtensions({ + path: { session_id: targetSessionId }, + signal, + }); + + if (latestSessionIdRef.current !== targetSessionId) { return; } - try { - const response = await getSessionExtensions({ - path: { session_id: sessionId }, - }); + setSessionExtensions(response.data?.extensions ?? []); + setIsSessionExtensionsLoaded(true); + }, + [] + ); + + useEffect(() => { + if (!sessionId) { + setIsSessionExtensionsLoaded(true); + return; + } - setSessionExtensions(response.data?.extensions ?? []); - } catch (error) { - console.error('Failed to fetch session extensions:', error); - } finally { - setIsSessionExtensionsLoaded(true); + const controller = new AbortController(); + + loadSessionExtensions(sessionId, controller.signal).catch((error) => { + if (controller.signal.aborted || latestSessionIdRef.current !== sessionId) { + return; } + + console.error('Failed to fetch session extensions:', error); + setIsSessionExtensionsLoaded(true); + }); + + return () => { + controller.abort(); }; + }, [sessionId, loadSessionExtensions]); - fetchExtensions(); - }, [sessionId]); + const finishSessionTransition = useCallback((targetSessionId: string) => { + if (latestSessionIdRef.current === targetSessionId) { + setPendingSort(false); + setIsTransitioning(false); + setTogglingExtension(null); + } + }, []); const handleToggle = useCallback( async (extensionConfig: FixedExtensionEntry) => { @@ -177,17 +203,16 @@ export const BottomMenuExtensionSelection = ({ sessionId }: BottomMenuExtensionS clearTimeout(sortTimeoutRef.current); } - sortTimeoutRef.current = setTimeout(async () => { - const response = await getSessionExtensions({ - path: { session_id: sessionId }, - }); - - if (response.data?.extensions) { - setSessionExtensions(response.data.extensions); - } - setPendingSort(false); - setIsTransitioning(false); - setTogglingExtension(null); + sortTimeoutRef.current = setTimeout(() => { + loadSessionExtensions(sessionId) + .catch((error) => { + if (latestSessionIdRef.current === sessionId) { + console.error('Failed to fetch session extensions:', error); + } + }) + .finally(() => { + finishSessionTransition(sessionId); + }); }, 800); } catch { setIsTransitioning(false); @@ -195,7 +220,7 @@ export const BottomMenuExtensionSelection = ({ sessionId }: BottomMenuExtensionS setTogglingExtension(null); } }, - [sessionId, isHubView, togglingExtension, intl] + [sessionId, isHubView, togglingExtension, intl, loadSessionExtensions, finishSessionTransition] ); // Merge all available extensions with session-specific or hub override state From af2a8e50e4e14934ff8c70380876253697e8bd16 Mon Sep 17 00:00:00 2001 From: Lifei Zhou Date: Wed, 3 Jun 2026 15:02:42 +1000 Subject: [PATCH 3/5] fixed lint --- .../components/bottom_menu/BottomMenuExtensionSelection.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/ui/desktop/src/components/bottom_menu/BottomMenuExtensionSelection.tsx b/ui/desktop/src/components/bottom_menu/BottomMenuExtensionSelection.tsx index 3f0bafafe5d0..5c3214770225 100644 --- a/ui/desktop/src/components/bottom_menu/BottomMenuExtensionSelection.tsx +++ b/ui/desktop/src/components/bottom_menu/BottomMenuExtensionSelection.tsx @@ -67,6 +67,8 @@ interface BottomMenuExtensionSelectionProps { sessionId: string | null; } +type GetSessionExtensionsSignal = Parameters[0]['signal']; + export const BottomMenuExtensionSelection = ({ sessionId }: BottomMenuExtensionSelectionProps) => { const intl = useIntl(); const [searchQuery, setSearchQuery] = useState(''); @@ -97,13 +99,13 @@ export const BottomMenuExtensionSelection = ({ sessionId }: BottomMenuExtensionS }, []); const loadSessionExtensions = useCallback( - async (targetSessionId: string, signal?: AbortSignal) => { + async (targetSessionId: string, signal?: GetSessionExtensionsSignal) => { const response = await getSessionExtensions({ path: { session_id: targetSessionId }, signal, }); - if (latestSessionIdRef.current !== targetSessionId) { + if (signal?.aborted || latestSessionIdRef.current !== targetSessionId) { return; } From 76ab6bf597ec375874fdd73f1fc972ac1dac47f9 Mon Sep 17 00:00:00 2001 From: Angie Jones Date: Tue, 2 Jun 2026 22:52:06 -0700 Subject: [PATCH 4/5] fix: gate session extension menu loading Signed-off-by: Angie Jones --- .../BottomMenuExtensionSelection.tsx | 43 ++++++++++++++++--- ui/desktop/src/hooks/useChatStream.ts | 8 +++- 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/ui/desktop/src/components/bottom_menu/BottomMenuExtensionSelection.tsx b/ui/desktop/src/components/bottom_menu/BottomMenuExtensionSelection.tsx index 5c3214770225..3d003c9403a4 100644 --- a/ui/desktop/src/components/bottom_menu/BottomMenuExtensionSelection.tsx +++ b/ui/desktop/src/components/bottom_menu/BottomMenuExtensionSelection.tsx @@ -15,6 +15,7 @@ import { getExtensionOverrides, } from '../../store/extensionOverrides'; import { defineMessages, useIntl } from '../../i18n'; +import { AppEvents } from '../../constants/events'; const i18n = defineMessages({ manageExtensions: { @@ -88,6 +89,14 @@ export const BottomMenuExtensionSelection = ({ sessionId }: BottomMenuExtensionS latestSessionIdRef.current = sessionId; setIsSessionExtensionsLoaded(false); setSessionExtensions([]); + setPendingSort(false); + setIsTransitioning(false); + setTogglingExtension(null); + + if (sortTimeoutRef.current) { + clearTimeout(sortTimeoutRef.current); + sortTimeoutRef.current = null; + } }, [sessionId]); useEffect(() => { @@ -121,19 +130,37 @@ export const BottomMenuExtensionSelection = ({ sessionId }: BottomMenuExtensionS return; } - const controller = new AbortController(); + let controller: AbortController | null = null; + + const loadExtensionsForCurrentSession = (event: Event) => { + const targetSessionId = (event as CustomEvent<{ sessionId?: string }>).detail?.sessionId; - loadSessionExtensions(sessionId, controller.signal).catch((error) => { - if (controller.signal.aborted || latestSessionIdRef.current !== sessionId) { + if (targetSessionId !== sessionId) { return; } - console.error('Failed to fetch session extensions:', error); - setIsSessionExtensionsLoaded(true); - }); + controller?.abort(); + const currentController = new AbortController(); + controller = currentController; + + loadSessionExtensions(targetSessionId, currentController.signal).catch((error) => { + if (currentController.signal.aborted || latestSessionIdRef.current !== targetSessionId) { + return; + } + + console.error('Failed to fetch session extensions:', error); + setIsSessionExtensionsLoaded(true); + }); + }; + + window.addEventListener(AppEvents.SESSION_EXTENSIONS_LOADED, loadExtensionsForCurrentSession); return () => { - controller.abort(); + controller?.abort(); + window.removeEventListener( + AppEvents.SESSION_EXTENSIONS_LOADED, + loadExtensionsForCurrentSession + ); }; }, [sessionId, loadSessionExtensions]); @@ -169,6 +196,7 @@ export const BottomMenuExtensionSelection = ({ sessionId }: BottomMenuExtensionS setPendingSort(false); setIsTransitioning(false); setTogglingExtension(null); + sortTimeoutRef.current = null; }, 800); toastService.success({ @@ -214,6 +242,7 @@ export const BottomMenuExtensionSelection = ({ sessionId }: BottomMenuExtensionS }) .finally(() => { finishSessionTransition(sessionId); + sortTimeoutRef.current = null; }); }, 800); } catch { diff --git a/ui/desktop/src/hooks/useChatStream.ts b/ui/desktop/src/hooks/useChatStream.ts index 507400c236f3..da0ef9706cea 100644 --- a/ui/desktop/src/hooks/useChatStream.ts +++ b/ui/desktop/src/hooks/useChatStream.ts @@ -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?.(); return; } @@ -776,7 +778,9 @@ export function useChatStream({ const extensionResults = resumeData?.extension_results; 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 reattachedToActiveRequest = activeRequestIdRef.current !== null; From 7b644112e0d3edd0fd00c128c687bab0054476d1 Mon Sep 17 00:00:00 2001 From: Angie Jones Date: Tue, 2 Jun 2026 23:09:35 -0700 Subject: [PATCH 5/5] fix: throw on session extension fetch errors Signed-off-by: Angie Jones --- .../src/components/bottom_menu/BottomMenuExtensionSelection.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/desktop/src/components/bottom_menu/BottomMenuExtensionSelection.tsx b/ui/desktop/src/components/bottom_menu/BottomMenuExtensionSelection.tsx index 3d003c9403a4..7ac91a9de0ae 100644 --- a/ui/desktop/src/components/bottom_menu/BottomMenuExtensionSelection.tsx +++ b/ui/desktop/src/components/bottom_menu/BottomMenuExtensionSelection.tsx @@ -112,6 +112,7 @@ export const BottomMenuExtensionSelection = ({ sessionId }: BottomMenuExtensionS const response = await getSessionExtensions({ path: { session_id: targetSessionId }, signal, + throwOnError: true, }); if (signal?.aborted || latestSessionIdRef.current !== targetSessionId) {