diff --git a/ui/desktop/src/components/ConfigContext.tsx b/ui/desktop/src/components/ConfigContext.tsx index be4fefc94518..0ef5b2cd77ab 100644 --- a/ui/desktop/src/components/ConfigContext.tsx +++ b/ui/desktop/src/components/ConfigContext.tsx @@ -111,6 +111,23 @@ export const ConfigProvider: React.FC = ({ children }) => { [reloadConfig] ); + const refreshExtensions = useCallback(async () => { + const result = await apiGetExtensions(); + + if (result.response.status === 422) { + throw new MalformedConfigError(); + } + + if (result.error && !result.data) { + console.log(result.error); + return extensionsList; + } + + const extensionResponse: ExtensionResponse = result.data!; + setExtensionsList(extensionResponse.extensions); + return extensionResponse.extensions; + }, [extensionsList]); + const addExtension = useCallback( async (name: string, config: ExtensionConfig, enabled: boolean) => { // remove shims if present @@ -122,39 +139,30 @@ export const ConfigProvider: React.FC = ({ children }) => { body: query, }); await reloadConfig(); + // Refresh extensions list after successful addition + await refreshExtensions(); }, - [reloadConfig] + [reloadConfig, refreshExtensions] ); const removeExtension = useCallback( async (name: string) => { await apiRemoveExtension({ path: { name: name } }); await reloadConfig(); + // Refresh extensions list after successful removal + await refreshExtensions(); }, - [reloadConfig] + [reloadConfig, refreshExtensions] ); const getExtensions = useCallback( async (forceRefresh = false): Promise => { if (forceRefresh || extensionsList.length === 0) { - const result = await apiGetExtensions(); - - if (result.response.status === 422) { - throw new MalformedConfigError(); - } - - if (result.error && !result.data) { - console.log(result.error); - return extensionsList; - } - - const extensionResponse: ExtensionResponse = result.data!; - setExtensionsList(extensionResponse.extensions); - return extensionResponse.extensions; + return await refreshExtensions(); } return extensionsList; }, - [extensionsList] + [extensionsList, refreshExtensions] ); const toggleExtension = useCallback( diff --git a/ui/desktop/src/components/extensions/ExtensionsView.tsx b/ui/desktop/src/components/extensions/ExtensionsView.tsx index b29b5774affa..6f0b7e7fdb84 100644 --- a/ui/desktop/src/components/extensions/ExtensionsView.tsx +++ b/ui/desktop/src/components/extensions/ExtensionsView.tsx @@ -5,7 +5,7 @@ import { MainPanelLayout } from '../Layout/MainPanelLayout'; import { Button } from '../ui/button'; import { Plus } from 'lucide-react'; import { GPSIcon } from '../ui/icons'; -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import ExtensionModal from '../settings/extensions/modal/ExtensionModal'; import { getDefaultFormData, @@ -31,6 +31,13 @@ export default function ExtensionsView({ const [refreshKey, setRefreshKey] = useState(0); const { addExtension } = useConfig(); + // Trigger refresh when deep link config changes (i.e., when a deep link is processed) + useEffect(() => { + if (viewOptions.deepLinkConfig) { + setRefreshKey((prevKey) => prevKey + 1); + } + }, [viewOptions.deepLinkConfig, viewOptions.showEnvVars]); + const handleModalClose = () => { setIsAddModalOpen(false); }; @@ -46,7 +53,7 @@ export default function ExtensionsView({ setRefreshKey((prevKey) => prevKey + 1); } catch (error) { console.error('Failed to activate extension:', error); - // Even if activation fails, we don't reopen the modal + setRefreshKey((prevKey) => prevKey + 1); } }; diff --git a/ui/desktop/src/components/settings/extensions/ExtensionsSection.tsx b/ui/desktop/src/components/settings/extensions/ExtensionsSection.tsx index e8197bb8d86e..3d3f25d6aa41 100644 --- a/ui/desktop/src/components/settings/extensions/ExtensionsSection.tsx +++ b/ui/desktop/src/components/settings/extensions/ExtensionsSection.tsx @@ -45,6 +45,21 @@ export default function ExtensionsSection({ showEnvVars ); + // Update deep link state when props change + useEffect(() => { + setDeepLinkConfigStateVar(deepLinkConfig); + 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); + }; + }, []); + const fetchExtensions = useCallback(async () => { const extensionsList = await getExtensions(true); // Force refresh // Sort extensions by name to maintain consistent order @@ -69,12 +84,6 @@ export default function ExtensionsSection({ enabled: disableConfiguration ? selectedExtensions.includes(ext.name) : ext.enabled, })); - console.log( - 'Setting extensions with selectedExtensions:', - selectedExtensions, - 'Extensions:', - sortedExtensions - ); setExtensions(sortedExtensions); }, [getExtensions, disableConfiguration, selectedExtensions]); @@ -129,27 +138,33 @@ export default function ExtensionsSection({ const extensionConfig = createExtensionConfig(formData); try { await activateExtension({ addToConfig: addExtension, extensionConfig: extensionConfig }); + // Immediately refresh the extensions list after successful activation + await fetchExtensions(); } catch (error) { console.error('Failed to activate extension:', error); - // Even if activation fails, we don't reopen the modal - } finally { - // Add a small delay to ensure backend has updated, then refresh the extensions list - setTimeout(async () => { - await fetchExtensions(); - }, 500); + await fetchExtensions(); } }; const handleUpdateExtension = async (formData: ExtensionFormData) => { + if (!selectedExtension) { + console.error('No selected extension for update'); + return; + } + // Close the modal immediately handleModalClose(); const extensionConfig = createExtensionConfig(formData); + const originalName = selectedExtension.name; + try { await updateExtension({ enabled: formData.enabled, extensionConfig: extensionConfig, addToConfig: addExtension, + removeFromConfig: removeExtension, + originalName: originalName, }); } catch (error) { console.error('Failed to update extension:', error); @@ -182,6 +197,11 @@ export default function ExtensionsSection({ setIsModalOpen(false); setIsAddModalOpen(false); setSelectedExtension(null); + + // Clear any navigation state that might be cached + if (window.history.state?.deepLinkConfig) { + window.history.replaceState({}, '', window.location.hash); + } }; return ( diff --git a/ui/desktop/src/components/settings/extensions/agent-api.ts b/ui/desktop/src/components/settings/extensions/agent-api.ts index 52fd7744722c..0c23e638254c 100644 --- a/ui/desktop/src/components/settings/extensions/agent-api.ts +++ b/ui/desktop/src/components/settings/extensions/agent-api.ts @@ -178,6 +178,6 @@ export async function removeFromAgent( } } -function sanitizeName(name: string) { +export function sanitizeName(name: string) { return name.toLowerCase().replace(/-/g, '').replace(/_/g, '').replace(/\s/g, ''); } diff --git a/ui/desktop/src/components/settings/extensions/extension-manager.ts b/ui/desktop/src/components/settings/extensions/extension-manager.ts index 9c1a3459e692..6550401b5fe2 100644 --- a/ui/desktop/src/components/settings/extensions/extension-manager.ts +++ b/ui/desktop/src/components/settings/extensions/extension-manager.ts @@ -1,6 +1,6 @@ import type { ExtensionConfig } from '../../../api/types.gen'; import { toastService, ToastServiceOptions } from '../../../toasts'; -import { addToAgent, removeFromAgent } from './agent-api'; +import { addToAgent, removeFromAgent, sanitizeName } from './agent-api'; interface ActivateExtensionProps { addToConfig: (name: string, extensionConfig: ExtensionConfig, enabled: boolean) => Promise; @@ -129,46 +129,121 @@ export async function addToAgentOnStartup({ interface UpdateExtensionProps { enabled: boolean; addToConfig: (name: string, extensionConfig: ExtensionConfig, enabled: boolean) => Promise; + removeFromConfig: (name: string) => Promise; extensionConfig: ExtensionConfig; + originalName?: string; } /** - * Updates an extension configuration without changing its enabled state + * Updates an extension configuration, handling name changes */ export async function updateExtension({ enabled, addToConfig, + removeFromConfig, extensionConfig, + originalName, }: UpdateExtensionProps) { - if (enabled) { + // Sanitize the new name to match the behavior when adding extensions + const sanitizedNewName = sanitizeName(extensionConfig.name); + const sanitizedOriginalName = originalName ? sanitizeName(originalName) : undefined; + + // Check if the sanitized name has changed + const nameChanged = sanitizedOriginalName && sanitizedOriginalName !== sanitizedNewName; + + if (nameChanged) { + // Handle name change: remove old extension and add new one + + // First remove the old extension from agent (using original name) try { - // AddToAgent - await addToAgent(extensionConfig); + await removeFromAgent(originalName!, { silent: true }); // Suppress removal toast since we'll show update toast } catch (error) { - console.error('[updateExtension]: Failed to add extension to agent during update:', error); - // Failed to add to agent -- show that error to user and do not update the config file - throw error; + console.error('Failed to remove old extension from agent during rename:', error); + // Continue with the process even if agent removal fails } - // Then add to config + // Remove old extension from config (using original name) try { - await addToConfig(extensionConfig.name, extensionConfig, enabled); + await removeFromConfig(originalName!); // We know originalName is not undefined here because nameChanged is true } catch (error) { - console.error('[updateExtension]: Failed to update extension in config:', error); - throw error; + console.error('Failed to remove old extension from config during rename:', error); + throw error; // This is more critical, so we throw } - } else { + + // Create a copy of the extension config with the sanitized name + const sanitizedExtensionConfig = { + ...extensionConfig, + name: sanitizedNewName, + }; + + // Add new extension with sanitized name + if (enabled) { + try { + // AddToAgent with silent option to avoid duplicate toasts + await addToAgent(sanitizedExtensionConfig, { silent: true }); + } catch (error) { + console.error('[updateExtension]: Failed to add renamed extension to agent:', error); + throw error; + } + } + + // Add to config with sanitized name try { - await addToConfig(extensionConfig.name, extensionConfig, enabled); + await addToConfig(sanitizedNewName, sanitizedExtensionConfig, enabled); } catch (error) { - console.error('[updateExtension]: Failed to update disabled extension in config:', error); + console.error('[updateExtension]: Failed to add renamed extension to config:', error); throw error; } - // show a toast that it was successfully updated + + toastService.configure({ silent: false }); toastService.success({ title: `Update extension`, - msg: `Successfully updated ${extensionConfig.name} extension`, + msg: `Successfully updated ${sanitizedNewName} extension`, }); + } else { + // Create a copy of the extension config with the sanitized name + const sanitizedExtensionConfig = { + ...extensionConfig, + name: sanitizedNewName, + }; + + if (enabled) { + try { + // AddToAgent with silent option to avoid duplicate toasts + await addToAgent(sanitizedExtensionConfig, { silent: true }); + } catch (error) { + console.error('[updateExtension]: Failed to add extension to agent during update:', error); + // Failed to add to agent -- show that error to user and do not update the config file + throw error; + } + + // Then add to config + try { + await addToConfig(sanitizedNewName, sanitizedExtensionConfig, enabled); + } catch (error) { + console.error('[updateExtension]: Failed to update extension in config:', error); + throw error; + } + + // show a toast that it was successfully updated + toastService.success({ + title: `Update extension`, + msg: `Successfully updated ${sanitizedNewName} extension`, + }); + } else { + try { + await addToConfig(sanitizedNewName, sanitizedExtensionConfig, enabled); + } catch (error) { + console.error('[updateExtension]: Failed to update disabled extension in config:', error); + throw error; + } + + // show a toast that it was successfully updated + toastService.success({ + title: `Update extension`, + msg: `Successfully updated ${sanitizedNewName} extension`, + }); + } } }