mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
Ensure adding/removing extensions refreshes extensions list (#3695)
This commit is contained in:
@@ -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`,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user