From a3ec5b8ca99cb05560ca6aaa27ed8fdee92fe7fd Mon Sep 17 00:00:00 2001 From: Lily Delalande Date: Thu, 20 Mar 2025 12:34:37 -0400 Subject: [PATCH 1/4] pause --- ui/desktop/src/agent/UpdateAgent.tsx | 99 +++++++++++++- .../extensions/ExtensionsSection.tsx | 27 ++-- .../settings_v2/extensions/extensionUtils.ts | 128 ++++++++++++++++++ 3 files changed, 243 insertions(+), 11 deletions(-) create mode 100644 ui/desktop/src/components/settings_v2/extensions/extensionUtils.ts diff --git a/ui/desktop/src/agent/UpdateAgent.tsx b/ui/desktop/src/agent/UpdateAgent.tsx index 09ad1ce1d8eb..1176fff49d9a 100644 --- a/ui/desktop/src/agent/UpdateAgent.tsx +++ b/ui/desktop/src/agent/UpdateAgent.tsx @@ -11,6 +11,10 @@ import { ToastSuccess, } from '../components/settings/models/toasts'; +export interface ExtensionUpdate { + extension: ExtensionConfig + type: 'add' | 'remove' +} // extensionUpdate = an extension was newly added or updated so we should attempt to add it export const useAgent = () => { @@ -39,7 +43,7 @@ export const useAgent = () => { } }; - const updateAgent = async (extensionUpdate?: ExtensionConfig) => { + const updateAgent = async (extensionUpdate?: ExtensionUpdate) => { setIsUpdating(true); try { @@ -62,7 +66,13 @@ export const useAgent = () => { } if (extensionUpdate) { - await addExtensionToAgent(extensionUpdate); + if (extensionUpdate.type == 'remove') { + // If explicitly set to false, remove the extension -- only need name + await removeExtensionFromAgent(extensionUpdate.extension.name); + } else { + // Otherwise, add or update the extension -- need full config + await addExtensionToAgent(extensionUpdate.extension); + } } return true; @@ -184,6 +194,91 @@ export const useAgent = () => { } }; + const removeExtensionFromAgent = async ( + name: string, + silent: boolean = false + ): Promise => { + try { + let toastId; + if (!silent) { + toastId = toast.loading(`Removing ${name} extension...`, { + position: 'top-center', + }); + } + + const response = await fetch(getApiUrl('/extensions/remove'), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Secret-Key': getSecretKey(), + }, + body: JSON.stringify(name), + }); + + // Handle non-OK responses + if (!response.ok) { + const errorMsg = `Server returned ${response.status}: ${response.statusText}`; + console.error(errorMsg); + + if (!silent) { + if (toastId) toast.dismiss(toastId); + toast.error(`Failed to remove ${name} extension: ${errorMsg}`); + } + return response; + } + + // Parse response JSON safely + let data; + try { + const text = await response.text(); + data = text ? JSON.parse(text) : { error: false }; + } catch (error) { + console.warn('Could not parse response as JSON, assuming success', error); + data = { error: false }; + } + + console.log('Response data:', data); + + if (!data.error) { + if (!silent) { + if (toastId) toast.dismiss(toastId); + toast.success(`Successfully disabled ${name} extension`); + } + return response; + } + + const errorMessage = `Error removing ${name} extension${data.message ? `. ${data.message}` : ''}`; + const ErrorMsg = ({ closeToast }: { closeToast?: () => void }) => ( +
+
Error removing {name} extension
+
+ +
+
+ ); + + console.error(errorMessage); + if (toastId) toast.dismiss(toastId); + toast(ErrorMsg, { type: 'error', autoClose: false }); + + return response; + } catch (error) { + console.log('Got some other error'); + const errorMessage = `Failed to remove ${name} extension: ${error instanceof Error ? error.message : 'Unknown error'}`; + console.error(errorMessage); + toast.error(errorMessage, { autoClose: false }); + throw error; + } + }; + return { updateAgent, addExtensionToAgent, diff --git a/ui/desktop/src/components/settings_v2/extensions/ExtensionsSection.tsx b/ui/desktop/src/components/settings_v2/extensions/ExtensionsSection.tsx index 0c2570309a06..ee3c514e581e 100644 --- a/ui/desktop/src/components/settings_v2/extensions/ExtensionsSection.tsx +++ b/ui/desktop/src/components/settings_v2/extensions/ExtensionsSection.tsx @@ -13,16 +13,17 @@ import { } from './utils'; import { useAgent } from '../../../agent/UpdateAgent'; import { activateExtension } from '.'; +import {useExtensionUpdater, handleExtensionError} from './extensionUtils'; export default function ExtensionsSection() { const { toggleExtension, getExtensions, addExtension, removeExtension } = useConfig(); + const performExtensionAction = useExtensionUpdater() const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [extensions, setExtensions] = useState([]); const [selectedExtension, setSelectedExtension] = useState(null); const [isModalOpen, setIsModalOpen] = useState(false); const [isAddModalOpen, setIsAddModalOpen] = useState(false); - const { updateAgent, addExtensionToAgent } = useAgent(); const fetchExtensions = async () => { setLoading(true); @@ -45,11 +46,15 @@ export default function ExtensionsSection() { }, []); const handleExtensionToggle = async (name: string) => { + // TODO: how to handle updating agent based on if we are toggling extension on vs off? try { - await toggleExtension(name); - fetchExtensions(); // Refresh the list after toggling + await performExtensionAction(toggleExtension, [name]) } catch (error) { - console.error('Failed to toggle extension:', error); + // TODO: move to separate function + // TODO: handle error for configuration problems (make sure it's set to not enabled) -- do we handle that configcontext side or via a callback here? + handleExtensionError(error) + } finally { + await fetchExtensions(); // Refresh the list after toggling } }; @@ -65,10 +70,12 @@ export default function ExtensionsSection() { await activateExtension(formData.name, extensionConfig, addExtension); console.log('attempting to add extension'); await updateAgent(extensionConfig); + await performExtensionAction(addExtension, [formData.name, extensionConfig, formData.enabled]) handleModalClose(); - await fetchExtensions(); // Refresh the list after adding } catch (error) { - console.error('Failed to add extension:', error); + handleExtensionError(error) + } finally { + await fetchExtensions(); // Refresh the list after adding } }; @@ -82,15 +89,17 @@ export default function ExtensionsSection() { } catch (error) { console.error('Failed to update extension configuration:', error); } + return handleAddExtension(formData) }; const handleDeleteExtension = async (name: string) => { try { - await removeExtension(name); + await performExtensionAction(removeExtension, [name]) handleModalClose(); - fetchExtensions(); // Refresh the list after deleting } catch (error) { - console.error('Failed to delete extension:', error); + handleExtensionError(error) + } finally { + await fetchExtensions(); // Refresh the list after deleting } }; diff --git a/ui/desktop/src/components/settings_v2/extensions/extensionUtils.ts b/ui/desktop/src/components/settings_v2/extensions/extensionUtils.ts new file mode 100644 index 000000000000..830fbba98729 --- /dev/null +++ b/ui/desktop/src/components/settings_v2/extensions/extensionUtils.ts @@ -0,0 +1,128 @@ +import { useAgent, ExtensionUpdate } from "../../../agent/UpdateAgent"; +import {ExtensionConfig} from "../../../api"; + +type ExtensionUpdateType = 'add' | 'remove' | 'update' | 'toggle'; + +// Define error types for better error handling +interface ConfigError { + type: 'CONFIG_ERROR'; + message: string; + originalError: unknown; +} + +interface AgentUpdateError { + type: 'AGENT_UPDATE_ERROR'; + message: string; + originalError: unknown; +} + +type ExtensionActionError = ConfigError | AgentUpdateError; + + +// TODO: make this a backend endpoint we can call that handles the config update part and the agent update part all in one + +/** + * This is a custom hook that handles updates to extensions that should require updates to both the config + * and the agent. + * + * (1) We update the config + * (2) We update the agent + * + * We throw an error if either (1) or (2) fails + */ +export function useExtensionUpdater() { + const { updateAgent } = useAgent(); + + // Return a strictly typed function that can be called from components + const updateConfigAndAgent = async function( + extensionUpdateType: ExtensionUpdateType, + params: any[], // TODO: some type checking + ): Promise { + let configSuccess = false; + + // First try-catch for the config operation + try { + // Execute the main extension action with appropriate arguments + // Type safety is ensured by the function overloads + await actionFn(...actionParams); + configSuccess = true; + } catch (error) { + console.error('Extension config operation failed:', error); + const configError: ConfigError = { + type: 'CONFIG_ERROR', + message: 'Failed to update extension configuration', + originalError: error + }; + throw configError; + } + + // If config update succeeded, update the agent + if (configSuccess && extensionName) { + try { + // Determine if we're enabling or disabling based on the action function + let isEnabling = false; + + if (actionFn === toggleExtension) { + // For toggle, we need to know the current state to determine the new state + // This might require additional context or a different approach + // For simplicity, we'll assume we're toggling ON here, but you might need to adjust + isEnabling = true; + } else if (actionFn === addExtension) { + // For add operation, the third param is the enabled state + isEnabling = actionParams[2] as boolean; + } else if (actionFn === removeExtension) { + // For remove, we're definitely disabling + isEnabling = false; + } + + // Update the agent with the extension name and whether it's being added or removed from the agent + const extensionUpdate = ExtensionUpdate{extension} + await updateAgent(extensionName); + } catch (error) { + console.error('Agent update operation failed:', error); + const agentError: AgentUpdateError = { + type: 'AGENT_UPDATE_ERROR', + message: 'Failed to update agent with extension changes', + originalError: error + }; + throw agentError; + } + } + }; + + return performExtensionAction; +} + +/** + * Handles extension operation errors with appropriate logging and actions + * @param error The error caught from extension operations + * @param callbacks Optional callback functions for different error types + */ +export function handleExtensionError( + error: any, + callbacks?: { + onConfigError?: (error: ExtensionActionError) => void; + onAgentError?: (error: ExtensionActionError) => void; + onUnknownError?: (error: any) => void; + } +) { + if (error?.type === 'CONFIG_ERROR') { + console.error('Configuration error:', error.message); + callbacks?.onConfigError?.(error as ExtensionActionError); + } else if (error?.type === 'AGENT_ERROR') { + console.error('Agent update error:', error.message); + callbacks?.onAgentError?.(error as ExtensionActionError); + } else { + // Handle unexpected errors + console.error('Unknown error:', error); + callbacks?.onUnknownError?.(error); + } +} + + +// For TypeScript to recognize these functions in comparison checks +// You would need to export these from your config context +// This is just a placeholder for the actual implementation +export const toggleExtension: ToggleExtensionFn = async (name) => { /* implementation */ }; +export const addExtension: AddExtensionFn = async (name, config, enabled) => { /* implementation */ }; +export const removeExtension: RemoveExtensionFn = async (name) => { /* implementation */ }; \ No newline at end of file From 407d57a02ad48f700da3158dca7cdc647868a843 Mon Sep 17 00:00:00 2001 From: Lily Delalande Date: Thu, 20 Mar 2025 21:59:40 -0400 Subject: [PATCH 2/4] hooks --- .../settings_v2/extensions/extensionUtils.ts | 267 +++++++++++------- 1 file changed, 171 insertions(+), 96 deletions(-) diff --git a/ui/desktop/src/components/settings_v2/extensions/extensionUtils.ts b/ui/desktop/src/components/settings_v2/extensions/extensionUtils.ts index 830fbba98729..05eb9d65ed0e 100644 --- a/ui/desktop/src/components/settings_v2/extensions/extensionUtils.ts +++ b/ui/desktop/src/components/settings_v2/extensions/extensionUtils.ts @@ -1,23 +1,46 @@ -import { useAgent, ExtensionUpdate } from "../../../agent/UpdateAgent"; -import {ExtensionConfig} from "../../../api"; +import { ExtensionUpdate, useAgent } from '../../../agent/UpdateAgent'; +import { ExtensionConfig } from '../../../api'; +import { FixedExtensionEntry, useConfig } from '@/src/components/ConfigContext'; -type ExtensionUpdateType = 'add' | 'remove' | 'update' | 'toggle'; +type ExtensionUpdateType = 'add' | 'remove' | 'toggle'; + +interface addExtensionParams { + name: string; + extensionConfig: ExtensionConfig; + enabled: boolean; +} + +interface removeExtensionParams { + name: string; +} + +interface toggleExtensionParams { + name: string; +} + +type ExtensionUpdateParams = addExtensionParams | removeExtensionParams | toggleExtensionParams; // Define error types for better error handling + +interface LookupError { + type: 'LOOKUP_ERROR'; + message: string; + originalError: unknown; +} + interface ConfigError { - type: 'CONFIG_ERROR'; - message: string; - originalError: unknown; + type: 'CONFIG_ERROR'; + message: string; + originalError: unknown; } interface AgentUpdateError { - type: 'AGENT_UPDATE_ERROR'; - message: string; - originalError: unknown; + type: 'AGENT_UPDATE_ERROR'; + message: string; + originalError: unknown; } -type ExtensionActionError = ConfigError | AgentUpdateError; - +type ExtensionUpdateError = LookupError | ConfigError | AgentUpdateError; // TODO: make this a backend endpoint we can call that handles the config update part and the agent update part all in one @@ -31,66 +54,121 @@ type ExtensionActionError = ConfigError | AgentUpdateError; * We throw an error if either (1) or (2) fails */ export function useExtensionUpdater() { - const { updateAgent } = useAgent(); - - // Return a strictly typed function that can be called from components - const updateConfigAndAgent = async function( - extensionUpdateType: ExtensionUpdateType, - params: any[], // TODO: some type checking - ): Promise { - let configSuccess = false; - - // First try-catch for the config operation - try { - // Execute the main extension action with appropriate arguments - // Type safety is ensured by the function overloads - await actionFn(...actionParams); - configSuccess = true; - } catch (error) { - console.error('Extension config operation failed:', error); - const configError: ConfigError = { - type: 'CONFIG_ERROR', - message: 'Failed to update extension configuration', - originalError: error - }; - throw configError; - } - - // If config update succeeded, update the agent - if (configSuccess && extensionName) { - try { - // Determine if we're enabling or disabling based on the action function - let isEnabling = false; - - if (actionFn === toggleExtension) { - // For toggle, we need to know the current state to determine the new state - // This might require additional context or a different approach - // For simplicity, we'll assume we're toggling ON here, but you might need to adjust - isEnabling = true; - } else if (actionFn === addExtension) { - // For add operation, the third param is the enabled state - isEnabling = actionParams[2] as boolean; - } else if (actionFn === removeExtension) { - // For remove, we're definitely disabling - isEnabling = false; - } - - // Update the agent with the extension name and whether it's being added or removed from the agent - const extensionUpdate = ExtensionUpdate{extension} - await updateAgent(extensionName); - } catch (error) { - console.error('Agent update operation failed:', error); - const agentError: AgentUpdateError = { - type: 'AGENT_UPDATE_ERROR', - message: 'Failed to update agent with extension changes', - originalError: error - }; - throw agentError; - } - } - }; - - return performExtensionAction; + const { updateAgent } = useAgent(); + const { addExtension, removeExtension, toggleExtension, getExtensions } = useConfig(); + + async function getExtensionEntryFromName(name: string) { + const extensions = await getExtensions(true); + const filteredExtensions = extensions.filter((extension) => extension.name === name); + if (filteredExtensions.length > 1) { + throw Error(`Multiple extensions with the same name: ${name}`); + } + return filteredExtensions[0]; + } + + // Extract just the ExtensionConfig part (omitting the 'enabled' property) + const extractConfig = (entry: FixedExtensionEntry): ExtensionConfig => { + const { enabled, ...config } = entry; + return config; + }; + + // TODO: variable naming + // Return a strictly typed function that can be called from components + const updateConfigAndAgent = async function ( + extensionUpdateType: ExtensionUpdateType, + params: ExtensionUpdateParams + ): Promise { + let configSuccess = false; + let lookupSuccess = false; + + let extensionUpdate = { extension: null, type: null }; + let args = null; + let extensionToUpdate = null; + let extEntry = null; + + // get relevant config information + try { + switch (extensionUpdateType) { + case 'add': + args = params as addExtensionParams; + extensionUpdate = { extension: args.extensionConfig, type: 'add' }; + break; + case 'remove': + args = params as removeExtensionParams; + extEntry = await getExtensionEntryFromName(args.name); + extensionToUpdate = extractConfig(extEntry); + extensionUpdate = { extension: extractConfig(extEntry), type: 'remove' }; + break; + case 'toggle': + args = params as toggleExtensionParams; + extEntry = await getExtensionEntryFromName(params.name); + extensionUpdate = { extension: extractConfig(extEntry), type: 'add' }; + break; + default: + throw Error( + "trying to perform an operation other other than 'add', 'remove' or 'toggle'" + ); + } + lookupSuccess = true; + } catch (error) { + const lookupError: LookupError = { + type: 'LOOKUP_ERROR', + message: 'Failed to find extension configuration values', + originalError: error, + }; + throw lookupError; + } + + // First try-catch for the config operation + try { + switch (extensionUpdateType) { + case 'add': + await addExtension(args.name, args.extensionConfig, args.enabled); + break; + case 'remove': + await removeExtension(args.name); + break; + case 'toggle': + await toggleExtension(args.name); + break; + default: + break; + } + configSuccess = true; + } catch (error) { + console.error('Extension config operation failed:', error); + const configError: ConfigError = { + type: 'CONFIG_ERROR', + message: 'Failed to update extension configuration', + originalError: error, + }; + throw configError; + } + + // If config update succeeded, update the agent + if (configSuccess && lookupSuccess) { + try { + await updateAgent(extensionUpdate); + } catch (error) { + console.error('Agent update operation failed:', error); + + // update the config with enabled set to false for this extension + // TODO: if we tried to remove ext with enabled = true, we should re-try removing ? + await addExtension(args.name, extensionUpdate.extension, false); + + // TODO: handle retries / config updates with enabled=false with the handleExtensionError callback? + + const agentError: AgentUpdateError = { + type: 'AGENT_UPDATE_ERROR', + message: 'Failed to update agent with extension changes', + originalError: error, + }; + throw agentError; + } + } + }; + + return updateConfigAndAgent; } /** @@ -99,30 +177,27 @@ export function useExtensionUpdater() { * @param callbacks Optional callback functions for different error types */ export function handleExtensionError( - error: any, - callbacks?: { - onConfigError?: (error: ExtensionActionError) => void; - onAgentError?: (error: ExtensionActionError) => void; - onUnknownError?: (error: any) => void; - } + error: any, + callbacks?: { + onLookupError?: (error: ExtensionUpdateError) => void; + onConfigError?: (error: ExtensionUpdateError) => void; + onAgentError?: (error: ExtensionUpdateError) => void; + onUnknownError?: (error: any) => void; + } ) { - if (error?.type === 'CONFIG_ERROR') { - console.error('Configuration error:', error.message); - callbacks?.onConfigError?.(error as ExtensionActionError); - } else if (error?.type === 'AGENT_ERROR') { - console.error('Agent update error:', error.message); - callbacks?.onAgentError?.(error as ExtensionActionError); - } else { - // Handle unexpected errors - console.error('Unknown error:', error); - callbacks?.onUnknownError?.(error); - } + if (error?.type === 'LOOKUP_ERROR') { + console.error('Lookup error:', error.message); + callbacks?.onLookupError?.(error as ExtensionUpdateError); + } + if (error?.type === 'CONFIG_ERROR') { + console.error('Configuration error:', error.message); + callbacks?.onConfigError?.(error as ExtensionUpdateError); + } else if (error?.type === 'AGENT_ERROR') { + console.error('Agent update error:', error.message); + callbacks?.onAgentError?.(error as ExtensionUpdateError); + } else { + // Handle unexpected errors + console.error('Unknown error:', error); + callbacks?.onUnknownError?.(error); + } } - - -// For TypeScript to recognize these functions in comparison checks -// You would need to export these from your config context -// This is just a placeholder for the actual implementation -export const toggleExtension: ToggleExtensionFn = async (name) => { /* implementation */ }; -export const addExtension: AddExtensionFn = async (name, config, enabled) => { /* implementation */ }; -export const removeExtension: RemoveExtensionFn = async (name) => { /* implementation */ }; \ No newline at end of file From f7c5661900b187d030a4ea2857ec1ee11d12bf94 Mon Sep 17 00:00:00 2001 From: Lily Delalande Date: Mon, 24 Mar 2025 19:48:42 -0400 Subject: [PATCH 3/4] add the flows --- .../extensions/ExtensionsSection.tsx | 71 +++--- .../settings_v2/extensions/agent_stuff.tsx | 136 ++++++++++++ .../settings_v2/extensions/extensionUtils.ts | 203 ------------------ .../settings_v2/extensions/index.ts | 6 + .../subcomponents/ExtensionItem.tsx | 4 +- .../subcomponents/ExtensionList.tsx | 2 +- .../settings_v2/extensions/temp.tsx | 140 ++++++++++++ .../settings_v2/extensions/utils.ts | 10 + 8 files changed, 323 insertions(+), 249 deletions(-) create mode 100644 ui/desktop/src/components/settings_v2/extensions/agent_stuff.tsx delete mode 100644 ui/desktop/src/components/settings_v2/extensions/extensionUtils.ts create mode 100644 ui/desktop/src/components/settings_v2/extensions/temp.tsx diff --git a/ui/desktop/src/components/settings_v2/extensions/ExtensionsSection.tsx b/ui/desktop/src/components/settings_v2/extensions/ExtensionsSection.tsx index ee3c514e581e..45bfd358ddab 100644 --- a/ui/desktop/src/components/settings_v2/extensions/ExtensionsSection.tsx +++ b/ui/desktop/src/components/settings_v2/extensions/ExtensionsSection.tsx @@ -9,15 +9,13 @@ import { createExtensionConfig, ExtensionFormData, extensionToFormData, + extractExtensionConfig, getDefaultFormData, } from './utils'; -import { useAgent } from '../../../agent/UpdateAgent'; -import { activateExtension } from '.'; -import {useExtensionUpdater, handleExtensionError} from './extensionUtils'; +import { AddNewExtension, DeleteExtension, ToggleExtension, UpdateExtension } from './temp'; export default function ExtensionsSection() { const { toggleExtension, getExtensions, addExtension, removeExtension } = useConfig(); - const performExtensionAction = useExtensionUpdater() const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [extensions, setExtensions] = useState([]); @@ -45,17 +43,17 @@ export default function ExtensionsSection() { fetchExtensions(); }, []); - const handleExtensionToggle = async (name: string) => { - // TODO: how to handle updating agent based on if we are toggling extension on vs off? - try { - await performExtensionAction(toggleExtension, [name]) - } catch (error) { - // TODO: move to separate function - // TODO: handle error for configuration problems (make sure it's set to not enabled) -- do we handle that configcontext side or via a callback here? - handleExtensionError(error) - } finally { - await fetchExtensions(); // Refresh the list after toggling - } + const handleExtensionToggle = async (extension: FixedExtensionEntry) => { + // If extension is enabled, we are trying to toggle if off, otherwise on + const toggleDirection = extension.enabled ? 'toggleOff' : 'toggleOn'; + const extensionConfig = extractExtensionConfig(extension); + await ToggleExtension({ + toggle: toggleDirection, + extensionConfig: extensionConfig, + addToConfig: addExtension, + removeFromConfig: removeExtension, + }); + await fetchExtensions(); // Refresh the list after toggling }; const handleConfigureClick = (extension: FixedExtensionEntry) => { @@ -65,42 +63,29 @@ export default function ExtensionsSection() { const handleAddExtension = async (formData: ExtensionFormData) => { const extensionConfig = createExtensionConfig(formData); - - try { - await activateExtension(formData.name, extensionConfig, addExtension); - console.log('attempting to add extension'); - await updateAgent(extensionConfig); - await performExtensionAction(addExtension, [formData.name, extensionConfig, formData.enabled]) - handleModalClose(); - } catch (error) { - handleExtensionError(error) - } finally { - await fetchExtensions(); // Refresh the list after adding - } + // TODO: replace activateExtension in index + // TODO: make sure error handling works + await AddNewExtension({ addToConfig: addExtension, extensionConfig: extensionConfig }); + handleModalClose(); + await fetchExtensions(); }; const handleUpdateExtension = async (formData: ExtensionFormData) => { const extensionConfig = createExtensionConfig(formData); - try { - await activateExtension(formData.name, extensionConfig, addExtension); - handleModalClose(); - fetchExtensions(); // Refresh the list after updating - } catch (error) { - console.error('Failed to update extension configuration:', error); - } - return handleAddExtension(formData) + await UpdateExtension({ + enabled: formData.enabled, + extensionConfig: extensionConfig, + addToConfig: addExtension, + }); + handleModalClose(); + await fetchExtensions(); }; const handleDeleteExtension = async (name: string) => { - try { - await performExtensionAction(removeExtension, [name]) - handleModalClose(); - } catch (error) { - handleExtensionError(error) - } finally { - await fetchExtensions(); // Refresh the list after deleting - } + await DeleteExtension({ name, removeFromConfig: removeExtension }); + handleModalClose(); + await fetchExtensions(); }; const handleModalClose = () => { diff --git a/ui/desktop/src/components/settings_v2/extensions/agent_stuff.tsx b/ui/desktop/src/components/settings_v2/extensions/agent_stuff.tsx new file mode 100644 index 000000000000..3998b427519d --- /dev/null +++ b/ui/desktop/src/components/settings_v2/extensions/agent_stuff.tsx @@ -0,0 +1,136 @@ +import { replaceWithShims } from '../../../agent/utils'; +import { ExtensionConfig } from '../../../api'; +import { toast } from 'react-toastify'; +import { getApiUrl, getSecretKey } from '../../../config'; +import React from 'react'; + +// Error message component +const ErrorMsg = ({ + name, + message, + closeToast, +}: { + name: string; + message?: string; + closeToast?: () => void; +}) => ( +
+
+ Error {message?.includes('adding') ? 'adding' : 'removing'} {name} extension +
+
+ +
+
+); + +// Core API call function +async function extensionApiCall( + endpoint: string, + payload: any, + actionType: 'adding' | 'removing', + extensionName: string +): Promise { + let toastId; + const actionVerb = actionType === 'adding' ? 'Adding' : 'Removing'; + const pastVerb = actionType === 'adding' ? 'added' : 'removed'; + + try { + toastId = toast.loading(`${actionVerb} ${extensionName} extension...`, { + position: 'top-center', + }); + + if (actionType === 'adding') { + toast.info( + 'Press the ESC key on your keyboard to continue using goose while extension loads' + ); + } + + const response = await fetch(getApiUrl(endpoint), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Secret-Key': getSecretKey(), + }, + body: JSON.stringify(payload), + }); + + // Handle non-OK responses + if (!response.ok) { + const errorMsg = `Server returned ${response.status}: ${response.statusText}`; + console.error(errorMsg); + + // Special handling for 428 Precondition Required (agent not initialized) + if (response.status === 428 && actionType === 'adding') { + if (toastId) toast.dismiss(toastId); + toast.error('Agent is not initialized. Please initialize the agent first.'); + return response; + } + + if (toastId) toast.dismiss(toastId); + toast.error( + `Failed to ${actionType === 'adding' ? 'add' : 'remove'} ${extensionName} extension: ${errorMsg}` + ); + return response; + } + + // Parse response JSON safely + let data; + try { + const text = await response.text(); + data = text ? JSON.parse(text) : { error: false }; + } catch (error) { + console.warn('Could not parse response as JSON, assuming success', error); + data = { error: false }; + } + + if (!data.error) { + if (toastId) toast.dismiss(toastId); + toast.success( + `Successfully ${actionType === 'adding' ? 'enabled' : 'disabled'} ${extensionName} extension` + ); + return response; + } + + const errorMessage = `Error ${actionType} ${extensionName} extension${data.message ? `. ${data.message}` : ''}`; + console.error(errorMessage); + + if (toastId) toast.dismiss(toastId); + toast(, { + type: 'error', + autoClose: false, + }); + + return response; + } catch (error) { + console.log('Got some other error'); + const errorMessage = `Failed to ${actionType === 'adding' ? 'add' : 'remove'} ${extensionName} extension: ${error instanceof Error ? error.message : 'Unknown error'}`; + console.error(errorMessage); + if (toastId) toast.dismiss(toastId); + toast.error(errorMessage, { autoClose: false }); + throw error; + } +} + +// Public functions +export async function AddToAgent(extension: ExtensionConfig): Promise { + if (extension.type === 'stdio') { + console.log('extension command', extension.cmd); + extension.cmd = await replaceWithShims(extension.cmd); + console.log('next ext command', extension.cmd); + } + + return extensionApiCall('/extensions/add', extension, 'adding', extension.name); +} + +export async function RemoveFromAgent(name: string): Promise { + return extensionApiCall('/extensions/remove', name, 'removing', name); +} diff --git a/ui/desktop/src/components/settings_v2/extensions/extensionUtils.ts b/ui/desktop/src/components/settings_v2/extensions/extensionUtils.ts deleted file mode 100644 index 05eb9d65ed0e..000000000000 --- a/ui/desktop/src/components/settings_v2/extensions/extensionUtils.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { ExtensionUpdate, useAgent } from '../../../agent/UpdateAgent'; -import { ExtensionConfig } from '../../../api'; -import { FixedExtensionEntry, useConfig } from '@/src/components/ConfigContext'; - -type ExtensionUpdateType = 'add' | 'remove' | 'toggle'; - -interface addExtensionParams { - name: string; - extensionConfig: ExtensionConfig; - enabled: boolean; -} - -interface removeExtensionParams { - name: string; -} - -interface toggleExtensionParams { - name: string; -} - -type ExtensionUpdateParams = addExtensionParams | removeExtensionParams | toggleExtensionParams; - -// Define error types for better error handling - -interface LookupError { - type: 'LOOKUP_ERROR'; - message: string; - originalError: unknown; -} - -interface ConfigError { - type: 'CONFIG_ERROR'; - message: string; - originalError: unknown; -} - -interface AgentUpdateError { - type: 'AGENT_UPDATE_ERROR'; - message: string; - originalError: unknown; -} - -type ExtensionUpdateError = LookupError | ConfigError | AgentUpdateError; - -// TODO: make this a backend endpoint we can call that handles the config update part and the agent update part all in one - -/** - * This is a custom hook that handles updates to extensions that should require updates to both the config - * and the agent. - * - * (1) We update the config - * (2) We update the agent - * - * We throw an error if either (1) or (2) fails - */ -export function useExtensionUpdater() { - const { updateAgent } = useAgent(); - const { addExtension, removeExtension, toggleExtension, getExtensions } = useConfig(); - - async function getExtensionEntryFromName(name: string) { - const extensions = await getExtensions(true); - const filteredExtensions = extensions.filter((extension) => extension.name === name); - if (filteredExtensions.length > 1) { - throw Error(`Multiple extensions with the same name: ${name}`); - } - return filteredExtensions[0]; - } - - // Extract just the ExtensionConfig part (omitting the 'enabled' property) - const extractConfig = (entry: FixedExtensionEntry): ExtensionConfig => { - const { enabled, ...config } = entry; - return config; - }; - - // TODO: variable naming - // Return a strictly typed function that can be called from components - const updateConfigAndAgent = async function ( - extensionUpdateType: ExtensionUpdateType, - params: ExtensionUpdateParams - ): Promise { - let configSuccess = false; - let lookupSuccess = false; - - let extensionUpdate = { extension: null, type: null }; - let args = null; - let extensionToUpdate = null; - let extEntry = null; - - // get relevant config information - try { - switch (extensionUpdateType) { - case 'add': - args = params as addExtensionParams; - extensionUpdate = { extension: args.extensionConfig, type: 'add' }; - break; - case 'remove': - args = params as removeExtensionParams; - extEntry = await getExtensionEntryFromName(args.name); - extensionToUpdate = extractConfig(extEntry); - extensionUpdate = { extension: extractConfig(extEntry), type: 'remove' }; - break; - case 'toggle': - args = params as toggleExtensionParams; - extEntry = await getExtensionEntryFromName(params.name); - extensionUpdate = { extension: extractConfig(extEntry), type: 'add' }; - break; - default: - throw Error( - "trying to perform an operation other other than 'add', 'remove' or 'toggle'" - ); - } - lookupSuccess = true; - } catch (error) { - const lookupError: LookupError = { - type: 'LOOKUP_ERROR', - message: 'Failed to find extension configuration values', - originalError: error, - }; - throw lookupError; - } - - // First try-catch for the config operation - try { - switch (extensionUpdateType) { - case 'add': - await addExtension(args.name, args.extensionConfig, args.enabled); - break; - case 'remove': - await removeExtension(args.name); - break; - case 'toggle': - await toggleExtension(args.name); - break; - default: - break; - } - configSuccess = true; - } catch (error) { - console.error('Extension config operation failed:', error); - const configError: ConfigError = { - type: 'CONFIG_ERROR', - message: 'Failed to update extension configuration', - originalError: error, - }; - throw configError; - } - - // If config update succeeded, update the agent - if (configSuccess && lookupSuccess) { - try { - await updateAgent(extensionUpdate); - } catch (error) { - console.error('Agent update operation failed:', error); - - // update the config with enabled set to false for this extension - // TODO: if we tried to remove ext with enabled = true, we should re-try removing ? - await addExtension(args.name, extensionUpdate.extension, false); - - // TODO: handle retries / config updates with enabled=false with the handleExtensionError callback? - - const agentError: AgentUpdateError = { - type: 'AGENT_UPDATE_ERROR', - message: 'Failed to update agent with extension changes', - originalError: error, - }; - throw agentError; - } - } - }; - - return updateConfigAndAgent; -} - -/** - * Handles extension operation errors with appropriate logging and actions - * @param error The error caught from extension operations - * @param callbacks Optional callback functions for different error types - */ -export function handleExtensionError( - error: any, - callbacks?: { - onLookupError?: (error: ExtensionUpdateError) => void; - onConfigError?: (error: ExtensionUpdateError) => void; - onAgentError?: (error: ExtensionUpdateError) => void; - onUnknownError?: (error: any) => void; - } -) { - if (error?.type === 'LOOKUP_ERROR') { - console.error('Lookup error:', error.message); - callbacks?.onLookupError?.(error as ExtensionUpdateError); - } - if (error?.type === 'CONFIG_ERROR') { - console.error('Configuration error:', error.message); - callbacks?.onConfigError?.(error as ExtensionUpdateError); - } else if (error?.type === 'AGENT_ERROR') { - console.error('Agent update error:', error.message); - callbacks?.onAgentError?.(error as ExtensionUpdateError); - } else { - // Handle unexpected errors - console.error('Unknown error:', error); - callbacks?.onUnknownError?.(error); - } -} diff --git a/ui/desktop/src/components/settings_v2/extensions/index.ts b/ui/desktop/src/components/settings_v2/extensions/index.ts index 4bb66c777aca..b0a87d0ee7f4 100644 --- a/ui/desktop/src/components/settings_v2/extensions/index.ts +++ b/ui/desktop/src/components/settings_v2/extensions/index.ts @@ -6,6 +6,7 @@ import { toast } from 'react-toastify'; import { ToastError, ToastLoading, ToastSuccess } from '../../settings/models/toasts'; // Default extension timeout in seconds +// TODO: keep in sync with rust better export const DEFAULT_EXTENSION_TIMEOUT = 300; // Type definition for built-in extensions from JSON @@ -77,6 +78,11 @@ export async function activateExtension( // First add to the config system await addExtensionFn(nameToKey(name), config, true); + if (config.type != 'stdio') { + console.error('only stdio is supported'); + throw Error('Only STDIO extensions are currently supported'); + } + // Then call the API endpoint const response = await fetch(getApiUrl('/extensions/add'), { method: 'POST', diff --git a/ui/desktop/src/components/settings_v2/extensions/subcomponents/ExtensionItem.tsx b/ui/desktop/src/components/settings_v2/extensions/subcomponents/ExtensionItem.tsx index 7d17a01c3d31..b5e286e59fc2 100644 --- a/ui/desktop/src/components/settings_v2/extensions/subcomponents/ExtensionItem.tsx +++ b/ui/desktop/src/components/settings_v2/extensions/subcomponents/ExtensionItem.tsx @@ -7,7 +7,7 @@ import { getSubtitle, getFriendlyTitle } from './ExtensionList'; interface ExtensionItemProps { extension: FixedExtensionEntry; - onToggle: (name: string) => void; + onToggle: (extension: FixedExtensionEntry) => void; onConfigure: (extension: FixedExtensionEntry) => void; } @@ -37,7 +37,7 @@ export default function ExtensionItem({ extension, onToggle, onConfigure }: Exte )} onToggle(extension.name)} + onCheckedChange={() => onToggle(extension)} variant="mono" /> diff --git a/ui/desktop/src/components/settings_v2/extensions/subcomponents/ExtensionList.tsx b/ui/desktop/src/components/settings_v2/extensions/subcomponents/ExtensionList.tsx index 587a8c957568..79cbcfcf0f23 100644 --- a/ui/desktop/src/components/settings_v2/extensions/subcomponents/ExtensionList.tsx +++ b/ui/desktop/src/components/settings_v2/extensions/subcomponents/ExtensionList.tsx @@ -7,7 +7,7 @@ import { combineCmdAndArgs } from '../utils'; interface ExtensionListProps { extensions: FixedExtensionEntry[]; - onToggle: (name: string) => void; + onToggle: (extension: FixedExtensionEntry) => void; onConfigure: (extension: FixedExtensionEntry) => void; } diff --git a/ui/desktop/src/components/settings_v2/extensions/temp.tsx b/ui/desktop/src/components/settings_v2/extensions/temp.tsx new file mode 100644 index 000000000000..d59d8dc271f6 --- /dev/null +++ b/ui/desktop/src/components/settings_v2/extensions/temp.tsx @@ -0,0 +1,140 @@ +import { ExtensionConfig } from '../../../api'; +import { AddToAgent, RemoveFromAgent } from './agent_stuff'; + +interface UpdateExtensionProps { + enabled: boolean; + addToConfig: (name: string, extensionConfig: ExtensionConfig, enabled: boolean) => Promise; + extensionConfig: ExtensionConfig; +} + +// updating -- no change to enabled state +export async function UpdateExtension({ + enabled, + addToConfig, + extensionConfig, +}: UpdateExtensionProps) { + if (enabled) { + try { + // AddToAgent + await AddToAgent(extensionConfig); + } catch (error) { + // i think only error that gets thrown here is when it's not from the response... rest are handled by agent + console.log('error', error); + // failed to add to agent -- show that error to user and do not update the config file + return; + } + + // Then add to config + try { + await addToConfig(extensionConfig.name, extensionConfig, enabled); + } catch (error) { + // config error workflow + console.log('error', error); + } + } else { + try { + await addToConfig(extensionConfig.name, extensionConfig, enabled); + } catch (error) { + // TODO: Add to agent with previous configuration and raise error + // for now just log error + console.log('error', error); + } + } +} + +// Adding a net-new extension (not in config) +interface AddNewExtensionProps { + addToConfig: (name: string, extensionConfig: ExtensionConfig, enabled: boolean) => Promise; + extensionConfig: ExtensionConfig; +} + +export async function AddNewExtension({ addToConfig, extensionConfig }: AddNewExtensionProps) { + try { + // AddToAgent + await AddToAgent(extensionConfig); + } catch (error) { + // add to config with enabled = false + await addToConfig(extensionConfig.name, extensionConfig, false); + // show user the error, return + console.log('error', error); + return; + } + + // Then add to config + try { + await addToConfig(extensionConfig.name, extensionConfig, true); + } catch (error) { + // remove from Agent + await RemoveFromAgent(extensionConfig.name); + // config error workflow + console.log('error', error); + } +} + +// TODO: handle errors in their respective functions +interface ToggleExtensionProps { + toggle: 'toggleOn' | 'toggleOff'; + extensionConfig: ExtensionConfig; + addToConfig: (name: string, extensionConfig: ExtensionConfig, enabled: boolean) => Promise; + removeFromConfig: (name: string) => Promise; +} + +export async function ToggleExtension({ + toggle, + extensionConfig, + addToConfig, +}: ToggleExtensionProps) { + // disabled to enabled + if (toggle == 'toggleOn') { + try { + // add to agent + await AddToAgent(extensionConfig); + } catch (error) { + // do nothing raise error + // show user error + console.log('Error adding extension to agent. Error:', error); + return; + } + + // update the config + try { + await addToConfig(extensionConfig.name, extensionConfig, true); + } catch (error) { + // remove from agent? + await RemoveFromAgent(extensionConfig.name); + } + } else if (toggle == 'toggleOff') { + // enabled to disabled + try { + await RemoveFromAgent(extensionConfig.name); + } catch (error) { + // note there was an error, but remove from config anyway + console.error('Error removing extension from agent', extensionConfig.name, error); + } + // update the config + try { + await addToConfig(extensionConfig.name, extensionConfig, false); + } catch (error) { + // TODO: Add to agent with previous configuration + console.log('Error removing extension from config', extensionConfig.name, 'Error:', error); + } + } +} + +interface DeleteExtensionProps { + name: string; + removeFromConfig: (name: string) => Promise; +} + +export async function DeleteExtension({ name, removeFromConfig }: DeleteExtensionProps) { + // remove from agent + await RemoveFromAgent(name); + + try { + await removeFromConfig(name); + } catch (error) { + console.log('Failed to remove extension from config after removing from agent. Error:', error); + // TODO: tell user to restart goose and try again to remove (will still be present in settings but not on agent until restart) + throw error; + } +} diff --git a/ui/desktop/src/components/settings_v2/extensions/utils.ts b/ui/desktop/src/components/settings_v2/extensions/utils.ts index 000f84f011be..e68afefcabfe 100644 --- a/ui/desktop/src/components/settings_v2/extensions/utils.ts +++ b/ui/desktop/src/components/settings_v2/extensions/utils.ts @@ -95,3 +95,13 @@ export function splitCmdAndArgs(str: string): { cmd: string; args: string[] } { export function combineCmdAndArgs(cmd: string, args: string[]): string { return [cmd, ...args].join(' '); } + +/** + * Extracts the ExtensionConfig from a FixedExtensionEntry object + * @param fixedEntry - The FixedExtensionEntry object + * @returns The ExtensionConfig portion of the object + */ +export function extractExtensionConfig(fixedEntry: FixedExtensionEntry): ExtensionConfig { + const { enabled, ...extensionConfig } = fixedEntry; + return extensionConfig; +} From 773a524ffdf5b340ac842d6261e0c379da365daf Mon Sep 17 00:00:00 2001 From: Lily Delalande Date: Mon, 24 Mar 2025 20:11:27 -0400 Subject: [PATCH 4/4] unused changes --- ui/desktop/src/agent/UpdateAgent.tsx | 99 +--------------------------- 1 file changed, 2 insertions(+), 97 deletions(-) diff --git a/ui/desktop/src/agent/UpdateAgent.tsx b/ui/desktop/src/agent/UpdateAgent.tsx index 1176fff49d9a..09ad1ce1d8eb 100644 --- a/ui/desktop/src/agent/UpdateAgent.tsx +++ b/ui/desktop/src/agent/UpdateAgent.tsx @@ -11,10 +11,6 @@ import { ToastSuccess, } from '../components/settings/models/toasts'; -export interface ExtensionUpdate { - extension: ExtensionConfig - type: 'add' | 'remove' -} // extensionUpdate = an extension was newly added or updated so we should attempt to add it export const useAgent = () => { @@ -43,7 +39,7 @@ export const useAgent = () => { } }; - const updateAgent = async (extensionUpdate?: ExtensionUpdate) => { + const updateAgent = async (extensionUpdate?: ExtensionConfig) => { setIsUpdating(true); try { @@ -66,13 +62,7 @@ export const useAgent = () => { } if (extensionUpdate) { - if (extensionUpdate.type == 'remove') { - // If explicitly set to false, remove the extension -- only need name - await removeExtensionFromAgent(extensionUpdate.extension.name); - } else { - // Otherwise, add or update the extension -- need full config - await addExtensionToAgent(extensionUpdate.extension); - } + await addExtensionToAgent(extensionUpdate); } return true; @@ -194,91 +184,6 @@ export const useAgent = () => { } }; - const removeExtensionFromAgent = async ( - name: string, - silent: boolean = false - ): Promise => { - try { - let toastId; - if (!silent) { - toastId = toast.loading(`Removing ${name} extension...`, { - position: 'top-center', - }); - } - - const response = await fetch(getApiUrl('/extensions/remove'), { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Secret-Key': getSecretKey(), - }, - body: JSON.stringify(name), - }); - - // Handle non-OK responses - if (!response.ok) { - const errorMsg = `Server returned ${response.status}: ${response.statusText}`; - console.error(errorMsg); - - if (!silent) { - if (toastId) toast.dismiss(toastId); - toast.error(`Failed to remove ${name} extension: ${errorMsg}`); - } - return response; - } - - // Parse response JSON safely - let data; - try { - const text = await response.text(); - data = text ? JSON.parse(text) : { error: false }; - } catch (error) { - console.warn('Could not parse response as JSON, assuming success', error); - data = { error: false }; - } - - console.log('Response data:', data); - - if (!data.error) { - if (!silent) { - if (toastId) toast.dismiss(toastId); - toast.success(`Successfully disabled ${name} extension`); - } - return response; - } - - const errorMessage = `Error removing ${name} extension${data.message ? `. ${data.message}` : ''}`; - const ErrorMsg = ({ closeToast }: { closeToast?: () => void }) => ( -
-
Error removing {name} extension
-
- -
-
- ); - - console.error(errorMessage); - if (toastId) toast.dismiss(toastId); - toast(ErrorMsg, { type: 'error', autoClose: false }); - - return response; - } catch (error) { - console.log('Got some other error'); - const errorMessage = `Failed to remove ${name} extension: ${error instanceof Error ? error.message : 'Unknown error'}`; - console.error(errorMessage); - toast.error(errorMessage, { autoClose: false }); - throw error; - } - }; - return { updateAgent, addExtensionToAgent,