Ensure adding/removing extensions refreshes extensions list (#3695)

This commit is contained in:
Zane
2025-07-31 12:17:09 -07:00
committed by GitHub
parent 9daa464021
commit c9ae9ba1ce
5 changed files with 165 additions and 55 deletions
+25 -17
View File
@@ -102,6 +102,23 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ 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
@@ -113,39 +130,30 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ 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<FixedExtensionEntry[]> => {
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(
@@ -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);
}
};
@@ -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 (
@@ -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, '');
}
@@ -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<void>;
@@ -129,46 +129,121 @@ export async function addToAgentOnStartup({
interface UpdateExtensionProps {
enabled: boolean;
addToConfig: (name: string, extensionConfig: ExtensionConfig, enabled: boolean) => Promise<void>;
removeFromConfig: (name: string) => Promise<void>;
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
console.error('Failed to remove old extension from agent during rename:', error);
// Continue with the process even if agent removal fails
}
// Remove old extension from config (using original name)
try {
await removeFromConfig(originalName!); // We know originalName is not undefined here because nameChanged is true
} catch (error) {
console.error('Failed to remove old extension from config during rename:', error);
throw error; // This is more critical, so we throw
}
// 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(sanitizedNewName, sanitizedExtensionConfig, enabled);
} catch (error) {
console.error('[updateExtension]: Failed to add renamed extension to config:', error);
throw error;
}
// Then add to config
try {
await addToConfig(extensionConfig.name, extensionConfig, enabled);
} catch (error) {
console.error('[updateExtension]: Failed to update extension in config:', error);
throw error;
}
} else {
try {
await addToConfig(extensionConfig.name, extensionConfig, 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.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`,
});
}
}
}