diff --git a/ui/desktop/src/components/ExtensionInstallModal.test.tsx b/ui/desktop/src/components/ExtensionInstallModal.test.tsx index 057451f56a90..f32e6123ea56 100644 --- a/ui/desktop/src/components/ExtensionInstallModal.test.tsx +++ b/ui/desktop/src/components/ExtensionInstallModal.test.tsx @@ -19,6 +19,13 @@ const mockElectron = { (window as any).electron = mockElectron; +vi.mock('./ConfigContext', () => ({ + useConfig: () => ({ + extensionsList: [], + getExtensions: vi.fn().mockResolvedValue([]), + }), +})); + describe('ExtensionInstallModal', () => { const mockAddExtension = vi.fn(); const mockSetView = vi.fn(); diff --git a/ui/desktop/src/components/ExtensionInstallModal.tsx b/ui/desktop/src/components/ExtensionInstallModal.tsx index 0478730c53c2..3b5153ee1dfc 100644 --- a/ui/desktop/src/components/ExtensionInstallModal.tsx +++ b/ui/desktop/src/components/ExtensionInstallModal.tsx @@ -1,4 +1,4 @@ -import { useState, useCallback, useEffect } from 'react'; +import { useState, useCallback, useEffect, useRef } from 'react'; import { IpcRendererEvent } from 'electron'; import { Dialog, @@ -13,6 +13,8 @@ import { extractExtensionName } from './settings/extensions/utils'; import { addExtensionFromDeepLink } from './settings/extensions/deeplink'; import type { ExtensionConfig } from '../api/types.gen'; import { View, ViewOptions } from '../utils/navigationUtils'; +import { useConfig } from './ConfigContext'; +import { toastService } from '../toasts'; type ModalType = 'blocked' | 'untrusted' | 'trusted'; @@ -66,6 +68,14 @@ function extractRemoteUrl(link: string): string | null { } export function ExtensionInstallModal({ addExtension, setView }: ExtensionInstallModalProps) { + const { getExtensions } = useConfig(); + const getExtensionsRef = useRef(getExtensions); + const processingLinkRef = useRef(null); + + useEffect(() => { + getExtensionsRef.current = getExtensions; + }, [getExtensions]); + const [modalState, setModalState] = useState({ isOpen: false, modalType: 'trusted', @@ -149,12 +159,30 @@ export function ExtensionInstallModal({ addExtension, setView }: ExtensionInstal }; const handleExtensionRequest = useCallback(async (link: string): Promise => { + if (processingLinkRef.current === link) { + console.log(`Skipping duplicate extension request (already processing): ${link}`); + return; + } + processingLinkRef.current = link; + try { console.log(`Processing extension request: ${link}`); const command = extractCommand(link); const remoteUrl = extractRemoteUrl(link); const extName = extractExtensionName(link); + const extensionsList = await getExtensionsRef.current(true); + + if (extensionsList?.find((ext) => ext.name === extName)) { + console.log(`Extension Already Installed: ${extName}`); + + toastService.success({ + title: `Extension '${extName}' Already Installed`, + msg: `'${extName}' extension has already been installed successfully. Start a new chat session to use it.`, + }); + return; + } + console.log('Extension not found, continuing to show modal'); const extensionInfo: ExtensionInfo = { name: extName, @@ -182,6 +210,8 @@ export function ExtensionInstallModal({ addExtension, setView }: ExtensionInstal ...prev, error: error instanceof Error ? error.message : 'Unknown error', })); + } finally { + processingLinkRef.current = null; } }, []); @@ -285,7 +315,7 @@ export function ExtensionInstallModal({ addExtension, setView }: ExtensionInstal {config.title} - + {config.message}