diff --git a/ui/desktop/src/components/settings/extensions/ExtensionsSection.tsx b/ui/desktop/src/components/settings/extensions/ExtensionsSection.tsx index 251a96939eaa..609f46e52c3a 100644 --- a/ui/desktop/src/components/settings/extensions/ExtensionsSection.tsx +++ b/ui/desktop/src/components/settings/extensions/ExtensionsSection.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState, useCallback } from 'react'; +import { useEffect, useState, useCallback, useMemo } from 'react'; import { Button } from '../../ui/button'; import { Plus } from 'lucide-react'; import { GPSIcon } from '../../ui/icons'; @@ -33,8 +33,7 @@ export default function ExtensionsSection({ customToggle, selectedExtensions = [], }: ExtensionSectionProps) { - const { getExtensions, addExtension, removeExtension } = useConfig(); - const [extensions, setExtensions] = useState([]); + const { getExtensions, addExtension, removeExtension, extensionsList } = useConfig(); const [selectedExtension, setSelectedExtension] = useState(null); const [isModalOpen, setIsModalOpen] = useState(false); const [isAddModalOpen, setIsAddModalOpen] = useState(false); @@ -51,19 +50,11 @@ export default function ExtensionsSection({ setShowEnvVarsStateVar(showEnvVars); }, [deepLinkConfig, showEnvVars]); - // Reset deep link state when component is re-mounted (via key prop changes) - useEffect(() => { - return () => { - // Cleanup function to reset state when component unmounts - setDeepLinkConfigStateVar(null); - setShowEnvVarsStateVar(null); - }; - }, []); + // Process extensions from context - this automatically updates when extensionsList changes + const extensions = useMemo(() => { + if (extensionsList.length === 0) return []; - const fetchExtensions = useCallback(async () => { - const extensionsList = await getExtensions(true); // Force refresh - // Sort extensions by name to maintain consistent order - const sortedExtensions = [...extensionsList] + return [...extensionsList] .sort((a, b) => { // First sort by builtin if (a.type === 'builtin' && b.type !== 'builtin') return -1; @@ -83,24 +74,15 @@ export default function ExtensionsSection({ // Use selectedExtensions to determine enabled state in recipe editor enabled: disableConfiguration ? selectedExtensions.includes(ext.name) : ext.enabled, })); + }, [extensionsList, disableConfiguration, selectedExtensions]); - setExtensions(sortedExtensions); - }, [getExtensions, disableConfiguration, selectedExtensions]); - - useEffect(() => { - fetchExtensions(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + const fetchExtensions = useCallback(async () => { + await getExtensions(true); // Force refresh - this will update the context + }, [getExtensions]); const handleExtensionToggle = async (extension: FixedExtensionEntry) => { if (customToggle) { await customToggle(extension); - // After custom toggle, update the local state to reflect the change - setExtensions((prevExtensions) => - prevExtensions.map((ext) => - ext.name === extension.name ? { ...ext, enabled: !ext.enabled } : ext - ) - ); return true; } diff --git a/ui/desktop/src/components/settings/extensions/agent-api.ts b/ui/desktop/src/components/settings/extensions/agent-api.ts index 752e86d382fd..a807e140a53b 100644 --- a/ui/desktop/src/components/settings/extensions/agent-api.ts +++ b/ui/desktop/src/components/settings/extensions/agent-api.ts @@ -14,18 +14,20 @@ interface ApiResponse { export async function extensionApiCall( endpoint: string, payload: ExtensionConfig | string, - options: ToastServiceOptions = {} + options: ToastServiceOptions & { isDelete?: boolean } = {} ): Promise { // Configure toast notifications toastService.configure(options); - // Determine if we're activating or removing an extension + // Determine if we're activating, deactivating, or removing an extension const isActivating = endpoint == '/extensions/add'; + const isRemoving = options.isDelete === true; + const action = { - type: isActivating ? 'activating' : 'removing', - verb: isActivating ? 'Activating' : 'Removing', - pastTense: isActivating ? 'activated' : 'removed', - presentTense: isActivating ? 'activate' : 'remove', + type: isActivating ? 'activating' : isRemoving ? 'removing' : 'deactivating', + verb: isActivating ? 'Activating' : isRemoving ? 'Removing' : 'Deactivating', + pastTense: isActivating ? 'activated' : isRemoving ? 'removed' : 'deactivated', + presentTense: isActivating ? 'activate' : isRemoving ? 'remove' : 'deactivate', }; // for adding the payload is an extensionConfig, for removing payload is just the name @@ -71,7 +73,7 @@ export async function extensionApiCall( toastService.dismiss(toastId); toastService.success({ title: extensionName, - msg: `Successfully ${action.pastTense} extension!`, + msg: `Successfully ${action.pastTense} extension`, }); return response; } catch (error) { @@ -114,7 +116,7 @@ function handleErrorResponse( } // General error case - const msg = `Failed to ${action.type === 'activating' ? 'add' : 'remove'} ${extensionName} extension: ${errorMsg}`; + const msg = `Failed to ${action.type === 'activating' ? 'add' : action.type === 'removing' ? 'remove' : 'deactivate'} ${extensionName} extension: ${errorMsg}`; toastService.dismiss(toastId); toastService.error({ title: extensionName, @@ -168,12 +170,13 @@ export async function addToAgent( */ export async function removeFromAgent( name: string, - options: ToastServiceOptions = {} + options: ToastServiceOptions & { isDelete?: boolean } = {} ): Promise { try { return await extensionApiCall('/extensions/remove', sanitizeName(name), options); } catch (error) { - console.error(`Failed to remove extension ${name} from agent:`, error); + const action = options.isDelete ? 'remove' : 'deactivate'; + console.error(`Failed to ${action} extension ${name} from agent:`, error); throw error; } } diff --git a/ui/desktop/src/components/settings/extensions/extension-manager.ts b/ui/desktop/src/components/settings/extensions/extension-manager.ts index 6550401b5fe2..3f97af174f4e 100644 --- a/ui/desktop/src/components/settings/extensions/extension-manager.ts +++ b/ui/desktop/src/components/settings/extensions/extension-manager.ts @@ -336,7 +336,7 @@ export async function deleteExtension({ name, removeFromConfig }: DeleteExtensio // remove from agent let agentRemoveError = null; try { - await removeFromAgent(name); + await removeFromAgent(name, { isDelete: true }); } catch (error) { console.error('Failed to remove extension from agent during deletion:', error); agentRemoveError = error;