diff --git a/ui/desktop/src/components/ProviderGrid.tsx b/ui/desktop/src/components/ProviderGrid.tsx
index 850c09a667fa..1b58662d872b 100644
--- a/ui/desktop/src/components/ProviderGrid.tsx
+++ b/ui/desktop/src/components/ProviderGrid.tsx
@@ -150,7 +150,7 @@ export function ProviderGrid({ onSubmit }: ProviderGridProps) {
ToastError({
title: provider,
msg: `Failed to ${providers.find((p) => p.id === selectedId)?.isConfigured ? 'update' : 'add'} configuration`,
- errorMessage: error.message,
+ traceback: error.message,
});
}
};
diff --git a/ui/desktop/src/components/settings/extensions/ConfigureBuiltInExtensionModal.tsx b/ui/desktop/src/components/settings/extensions/ConfigureBuiltInExtensionModal.tsx
index 7fd5f8bcad43..44d9706cfb2f 100644
--- a/ui/desktop/src/components/settings/extensions/ConfigureBuiltInExtensionModal.tsx
+++ b/ui/desktop/src/components/settings/extensions/ConfigureBuiltInExtensionModal.tsx
@@ -80,7 +80,7 @@ export function ConfigureBuiltInExtensionModal({
ToastError({
title: extension.name,
msg: `Failed to configure the extension`,
- errorMessage: error.message,
+ traceback: error.message,
});
} finally {
setIsSubmitting(false);
diff --git a/ui/desktop/src/components/settings/extensions/ConfigureExtensionModal.tsx b/ui/desktop/src/components/settings/extensions/ConfigureExtensionModal.tsx
index dc51bd9f4493..71d3fa872ff9 100644
--- a/ui/desktop/src/components/settings/extensions/ConfigureExtensionModal.tsx
+++ b/ui/desktop/src/components/settings/extensions/ConfigureExtensionModal.tsx
@@ -82,7 +82,7 @@ export function ConfigureExtensionModal({
ToastError({
title: extension.name,
msg: `Failed to configure extension`,
- errorMessage: error.message,
+ traceback: error.message,
});
} finally {
setIsSubmitting(false);
diff --git a/ui/desktop/src/components/settings/extensions/ManualExtensionModal.tsx b/ui/desktop/src/components/settings/extensions/ManualExtensionModal.tsx
index ca747687645e..0df7cdd5adf6 100644
--- a/ui/desktop/src/components/settings/extensions/ManualExtensionModal.tsx
+++ b/ui/desktop/src/components/settings/extensions/ManualExtensionModal.tsx
@@ -99,7 +99,7 @@ export function ManualExtensionModal({ isOpen, onClose, onSubmit }: ManualExtens
resetForm();
} catch (error) {
console.error('Error configuring extension:', error);
- ToastError({ title: 'Failed to configure extension', errorMessage: error.message });
+ ToastError({ title: 'Failed to configure extension', traceback: error.message });
}
};
diff --git a/ui/desktop/src/components/settings/models/toasts.tsx b/ui/desktop/src/components/settings/models/toasts.tsx
index bbabbaf8ba96..307c6c82ad15 100644
--- a/ui/desktop/src/components/settings/models/toasts.tsx
+++ b/ui/desktop/src/components/settings/models/toasts.tsx
@@ -24,10 +24,10 @@ export function ToastSuccess({ title, msg, toastOptions = {} }: ToastSuccessProp
type ToastErrorProps = {
title?: string;
msg?: string;
- errorMessage?: string;
+ traceback?: string;
toastOptions?: ToastOptions;
};
-export function ToastError({ title, msg, errorMessage, toastOptions }: ToastErrorProps) {
+export function ToastError({ title, msg, traceback, toastOptions }: ToastErrorProps) {
return toast.error(
@@ -35,17 +35,17 @@ export function ToastError({ title, msg, errorMessage, toastOptions }: ToastErro
{msg ?
{msg}
: null}
- {errorMessage ? (
+ {traceback ? (
) : null}
,
- { ...commonToastOptions, autoClose: errorMessage ? false : 5000, ...toastOptions }
+ { ...commonToastOptions, autoClose: traceback ? false : 5000, ...toastOptions }
);
}
diff --git a/ui/desktop/src/components/settings/models/utils.tsx b/ui/desktop/src/components/settings/models/utils.tsx
index 3f0b22590675..5af28772cdc2 100644
--- a/ui/desktop/src/components/settings/models/utils.tsx
+++ b/ui/desktop/src/components/settings/models/utils.tsx
@@ -43,7 +43,7 @@ export function useHandleModelSelection() {
ToastError({
title: model.name,
msg: `Failed to switch to model`,
- errorMessage: error.message,
+ traceback: error.message,
});
}
};
diff --git a/ui/desktop/src/components/settings/providers/ConfigureProvidersGrid.tsx b/ui/desktop/src/components/settings/providers/ConfigureProvidersGrid.tsx
index 0968de2a0b7b..9e190a8920d3 100644
--- a/ui/desktop/src/components/settings/providers/ConfigureProvidersGrid.tsx
+++ b/ui/desktop/src/components/settings/providers/ConfigureProvidersGrid.tsx
@@ -158,7 +158,7 @@ export function ConfigureProvidersGrid() {
ToastError({
title: provider,
msg: `Failed to ${providers.find((p) => p.id === selectedForSetup)?.isConfigured ? 'update' : 'add'} configuration`,
- errorMessage: error.message,
+ traceback: error.message,
});
}
};
@@ -181,7 +181,7 @@ export function ConfigureProvidersGrid() {
// Check if the selected provider is currently active
if (currentModel?.provider === providerToDelete.name) {
const msg = `Cannot delete the configuration because it's the provider of the current model (${currentModel.name}). Please switch to a different model first.`;
- ToastError({ title: providerToDelete.name, msg, errorMessage: msg });
+ ToastError({ title: providerToDelete.name, msg, traceback: msg });
setIsConfirmationOpen(false);
return;
}
@@ -221,7 +221,7 @@ export function ConfigureProvidersGrid() {
ToastError({
title: providerToDelete.name,
msg: 'Failed to delete configuration',
- errorMessage: error.message,
+ traceback: error.message,
});
}
setIsConfirmationOpen(false);
diff --git a/ui/desktop/src/components/settings_v2/extensions/ExtensionsSection.tsx b/ui/desktop/src/components/settings_v2/extensions/ExtensionsSection.tsx
index 0c2570309a06..34c007da0ad7 100644
--- a/ui/desktop/src/components/settings_v2/extensions/ExtensionsSection.tsx
+++ b/ui/desktop/src/components/settings_v2/extensions/ExtensionsSection.tsx
@@ -9,20 +9,20 @@ import {
createExtensionConfig,
ExtensionFormData,
extensionToFormData,
+ extractExtensionConfig,
getDefaultFormData,
} from './utils';
-import { useAgent } from '../../../agent/UpdateAgent';
-import { activateExtension } from '.';
+
+import { activateExtension, deleteExtension, toggleExtension, updateExtension } from './index';
export default function ExtensionsSection() {
- const { toggleExtension, getExtensions, addExtension, removeExtension } = useConfig();
+ const { getExtensions, addExtension, removeExtension } = useConfig();
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);
@@ -44,13 +44,17 @@ export default function ExtensionsSection() {
fetchExtensions();
}, []);
- const handleExtensionToggle = async (name: string) => {
- try {
- await toggleExtension(name);
- fetchExtensions(); // Refresh the list after toggling
- } catch (error) {
- console.error('Failed to toggle extension:', error);
- }
+ 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) => {
@@ -60,38 +64,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);
- handleModalClose();
- await fetchExtensions(); // Refresh the list after adding
- } catch (error) {
- console.error('Failed to add extension:', error);
- }
+ // TODO: replace activateExtension in index
+ // TODO: make sure error handling works
+ await activateExtension({ 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);
- }
+ await updateExtension({
+ enabled: formData.enabled,
+ extensionConfig: extensionConfig,
+ addToConfig: addExtension,
+ });
+ handleModalClose();
+ await fetchExtensions();
};
const handleDeleteExtension = async (name: string) => {
- try {
- await removeExtension(name);
- handleModalClose();
- fetchExtensions(); // Refresh the list after deleting
- } catch (error) {
- console.error('Failed to delete extension:', error);
- }
+ await deleteExtension({ name, removeFromConfig: removeExtension });
+ handleModalClose();
+ await fetchExtensions();
};
const handleModalClose = () => {
diff --git a/ui/desktop/src/components/settings_v2/extensions/index.ts b/ui/desktop/src/components/settings_v2/extensions/index.ts
index 4bb66c777aca..508afaac0ddf 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
@@ -33,7 +34,7 @@ function handleError(message: string, shouldThrow = false): void {
ToastError({
title: 'Error',
msg: message,
- errorMessage: message,
+ traceback: message,
});
console.error(message);
if (shouldThrow) {
@@ -57,6 +58,11 @@ async function replaceWithShims(cmd: string) {
return cmd;
}
+interface activateExtensionProps {
+ addToConfig: (name: string, extensionConfig: ExtensionConfig, enabled: boolean) => Promise;
+ extensionConfig: ExtensionConfig;
+}
+
/**
* Activates an extension by adding it to both the config system and the API.
* @param name The extension name
@@ -64,67 +70,151 @@ async function replaceWithShims(cmd: string) {
* @param addExtensionFn Function to add extension to config
* @returns Promise that resolves when activation is complete
*/
-export async function activateExtension(
- name: string,
- config: ExtensionConfig,
- addExtensionFn: (name: string, config: ExtensionConfig, enabled: boolean) => Promise
-): Promise {
- let toastId;
+export async function activateExtension({
+ addToConfig,
+ extensionConfig,
+}: activateExtensionProps): Promise {
try {
- // Show loading toast
- toastId = ToastLoading({ title: name, msg: 'Adding extension...' });
+ // 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;
+ }
- // First add to the config system
- await addExtensionFn(nameToKey(name), config, true);
+ // 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);
+ }
+}
- // Then call the API endpoint
- const response = await fetch(getApiUrl('/extensions/add'), {
- method: 'POST',
- headers: {
- 'Content-Type': 'application/json',
- 'X-Secret-Key': getSecretKey(),
- },
- body: JSON.stringify({
- type: config.type,
- name: nameToKey(name),
- cmd: await replaceWithShims(config.cmd),
- args: config.args || [],
- env_keys: config.envs ? Object.keys(config.envs) : undefined,
- timeout: config.timeout,
- }),
- });
+interface updateExtensionProps {
+ enabled: boolean;
+ addToConfig: (name: string, extensionConfig: ExtensionConfig, enabled: boolean) => Promise;
+ extensionConfig: ExtensionConfig;
+}
- const data = await response.json();
+// 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;
+ }
- if (!data.error) {
- if (toastId) toast.dismiss(toastId);
- ToastSuccess({ title: name, msg: 'Successfully enabled extension' });
- } else {
- const errorMessage = `Error adding extension`;
- console.error(errorMessage);
- if (toastId) toast.dismiss(toastId);
- ToastError({
- title: name,
- msg: errorMessage,
- errorMessage: data.message,
- });
+ // 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);
+ }
+ }
+}
+
+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) {
- const errorMessage = `Failed to add ${name} extension: ${error instanceof Error ? error.message : 'Unknown error'}`;
- console.error(errorMessage);
- if (toastId) toast.dismiss(toastId);
- ToastError({
- title: name,
- msg: 'Failed to add extension',
- errorMessage: error.message,
- });
+ 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;
}
}
+{
+ /*Deeplinks*/
+}
+
export async function addExtensionFromDeepLink(
url: string,
- addExtensionFn: (name: string, config: ExtensionConfig, enabled: boolean) => Promise,
+ addExtensionFn: (
+ name: string,
+ extensionConfig: ExtensionConfig,
+ enabled: boolean
+ ) => Promise,
setView: (view: string, options: { extensionId: string; showEnvVars: boolean }) => void
) {
const parsedUrl = new URL(url);
@@ -202,7 +292,11 @@ export async function addExtensionFromDeepLink(
}
// If no env vars are required, proceed with adding the extension
- await activateExtension(name, config, addExtensionFn);
+ await activateExtension({ extensionConfig: config, addToConfig: addExtensionFn });
+}
+
+{
+ /*Built ins*/
}
/**
@@ -272,3 +366,109 @@ export async function initializeBuiltInExtensions(
// Call with an empty list to ensure all built-ins are added
await syncBuiltInExtensions([], addExtensionFn);
}
+
+{
+ /* Agent-related helper functions */
+}
+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 {
+ if (actionType === 'adding') {
+ // Show loading toast
+ toastId = ToastLoading({
+ title: extensionName,
+ msg: `${actionVerb} ${extensionName} extension...`,
+ });
+ // FIXME: this also shows when toggling -- should only show when you have modal up (fix: diff message for toggling)
+ 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);
+ ToastError({
+ title: extensionName,
+ msg: 'Agent is not initialized. Please initialize the agent first.',
+ traceback: errorMsg,
+ });
+ return response;
+ }
+
+ const msg = `Failed to ${actionType === 'adding' ? 'add' : 'remove'} ${extensionName} extension: ${errorMsg}`;
+ console.error(msg);
+
+ if (toastId) toast.dismiss(toastId);
+ ToastError({
+ title: extensionName,
+ msg: msg,
+ traceback: 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);
+ ToastSuccess({ title: extensionName, msg: 'Successfully enabled extension' });
+ } else {
+ const errorMessage = `Error adding extension -- parsing data`;
+ console.error(errorMessage);
+ if (toastId) toast.dismiss(toastId);
+ ToastError({
+ title: extensionName,
+ msg: errorMessage,
+ traceback: data.message, // why data.message not data.error?
+ });
+ }
+ } catch (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/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/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;
+}
diff --git a/ui/desktop/src/components/settings_v2/models/AddModelModal.tsx b/ui/desktop/src/components/settings_v2/models/AddModelModal.tsx
index 1f2b420a0a4c..f717f123920d 100644
--- a/ui/desktop/src/components/settings_v2/models/AddModelModal.tsx
+++ b/ui/desktop/src/components/settings_v2/models/AddModelModal.tsx
@@ -51,7 +51,7 @@ export const AddModelModal = ({ onClose }: AddModelModalProps) => {
} catch (e) {
ToastError({
title: 'Failed to add model',
- errorMessage: e.message,
+ traceback: e.message,
});
}
};