mirror of
https://github.com/aljazceru/goose.git
synced 2026-01-06 16:04:28 +01:00
ui: remove and update extensions (#1847)
This commit is contained in:
@@ -150,7 +150,7 @@ export function ProviderGrid({ onSubmit }: ProviderGridProps) {
|
|||||||
ToastError({
|
ToastError({
|
||||||
title: provider,
|
title: provider,
|
||||||
msg: `Failed to ${providers.find((p) => p.id === selectedId)?.isConfigured ? 'update' : 'add'} configuration`,
|
msg: `Failed to ${providers.find((p) => p.id === selectedId)?.isConfigured ? 'update' : 'add'} configuration`,
|
||||||
errorMessage: error.message,
|
traceback: error.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export function ConfigureBuiltInExtensionModal({
|
|||||||
ToastError({
|
ToastError({
|
||||||
title: extension.name,
|
title: extension.name,
|
||||||
msg: `Failed to configure the extension`,
|
msg: `Failed to configure the extension`,
|
||||||
errorMessage: error.message,
|
traceback: error.message,
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
|
|||||||
@@ -82,7 +82,7 @@ export function ConfigureExtensionModal({
|
|||||||
ToastError({
|
ToastError({
|
||||||
title: extension.name,
|
title: extension.name,
|
||||||
msg: `Failed to configure extension`,
|
msg: `Failed to configure extension`,
|
||||||
errorMessage: error.message,
|
traceback: error.message,
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
setIsSubmitting(false);
|
setIsSubmitting(false);
|
||||||
|
|||||||
@@ -99,7 +99,7 @@ export function ManualExtensionModal({ isOpen, onClose, onSubmit }: ManualExtens
|
|||||||
resetForm();
|
resetForm();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error configuring extension:', 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 });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -24,10 +24,10 @@ export function ToastSuccess({ title, msg, toastOptions = {} }: ToastSuccessProp
|
|||||||
type ToastErrorProps = {
|
type ToastErrorProps = {
|
||||||
title?: string;
|
title?: string;
|
||||||
msg?: string;
|
msg?: string;
|
||||||
errorMessage?: string;
|
traceback?: string;
|
||||||
toastOptions?: ToastOptions;
|
toastOptions?: ToastOptions;
|
||||||
};
|
};
|
||||||
export function ToastError({ title, msg, errorMessage, toastOptions }: ToastErrorProps) {
|
export function ToastError({ title, msg, traceback, toastOptions }: ToastErrorProps) {
|
||||||
return toast.error(
|
return toast.error(
|
||||||
<div className="flex gap-4">
|
<div className="flex gap-4">
|
||||||
<div className="flex-grow">
|
<div className="flex-grow">
|
||||||
@@ -35,17 +35,17 @@ export function ToastError({ title, msg, errorMessage, toastOptions }: ToastErro
|
|||||||
{msg ? <div>{msg}</div> : null}
|
{msg ? <div>{msg}</div> : null}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-none flex items-center">
|
<div className="flex-none flex items-center">
|
||||||
{errorMessage ? (
|
{traceback ? (
|
||||||
<button
|
<button
|
||||||
className="text-textProminentInverse font-medium"
|
className="text-textProminentInverse font-medium"
|
||||||
onClick={() => navigator.clipboard.writeText(errorMessage)}
|
onClick={() => navigator.clipboard.writeText(traceback)}
|
||||||
>
|
>
|
||||||
Copy error
|
Copy error
|
||||||
</button>
|
</button>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>,
|
</div>,
|
||||||
{ ...commonToastOptions, autoClose: errorMessage ? false : 5000, ...toastOptions }
|
{ ...commonToastOptions, autoClose: traceback ? false : 5000, ...toastOptions }
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export function useHandleModelSelection() {
|
|||||||
ToastError({
|
ToastError({
|
||||||
title: model.name,
|
title: model.name,
|
||||||
msg: `Failed to switch to model`,
|
msg: `Failed to switch to model`,
|
||||||
errorMessage: error.message,
|
traceback: error.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -158,7 +158,7 @@ export function ConfigureProvidersGrid() {
|
|||||||
ToastError({
|
ToastError({
|
||||||
title: provider,
|
title: provider,
|
||||||
msg: `Failed to ${providers.find((p) => p.id === selectedForSetup)?.isConfigured ? 'update' : 'add'} configuration`,
|
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
|
// Check if the selected provider is currently active
|
||||||
if (currentModel?.provider === providerToDelete.name) {
|
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.`;
|
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);
|
setIsConfirmationOpen(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -221,7 +221,7 @@ export function ConfigureProvidersGrid() {
|
|||||||
ToastError({
|
ToastError({
|
||||||
title: providerToDelete.name,
|
title: providerToDelete.name,
|
||||||
msg: 'Failed to delete configuration',
|
msg: 'Failed to delete configuration',
|
||||||
errorMessage: error.message,
|
traceback: error.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
setIsConfirmationOpen(false);
|
setIsConfirmationOpen(false);
|
||||||
|
|||||||
@@ -9,20 +9,20 @@ import {
|
|||||||
createExtensionConfig,
|
createExtensionConfig,
|
||||||
ExtensionFormData,
|
ExtensionFormData,
|
||||||
extensionToFormData,
|
extensionToFormData,
|
||||||
|
extractExtensionConfig,
|
||||||
getDefaultFormData,
|
getDefaultFormData,
|
||||||
} from './utils';
|
} from './utils';
|
||||||
import { useAgent } from '../../../agent/UpdateAgent';
|
|
||||||
import { activateExtension } from '.';
|
import { activateExtension, deleteExtension, toggleExtension, updateExtension } from './index';
|
||||||
|
|
||||||
export default function ExtensionsSection() {
|
export default function ExtensionsSection() {
|
||||||
const { toggleExtension, getExtensions, addExtension, removeExtension } = useConfig();
|
const { getExtensions, addExtension, removeExtension } = useConfig();
|
||||||
const [loading, setLoading] = useState<boolean>(true);
|
const [loading, setLoading] = useState<boolean>(true);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
const [extensions, setExtensions] = useState<FixedExtensionEntry[]>([]);
|
const [extensions, setExtensions] = useState<FixedExtensionEntry[]>([]);
|
||||||
const [selectedExtension, setSelectedExtension] = useState<FixedExtensionEntry | null>(null);
|
const [selectedExtension, setSelectedExtension] = useState<FixedExtensionEntry | null>(null);
|
||||||
const [isModalOpen, setIsModalOpen] = useState(false);
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
||||||
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
|
||||||
const { updateAgent, addExtensionToAgent } = useAgent();
|
|
||||||
|
|
||||||
const fetchExtensions = async () => {
|
const fetchExtensions = async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -44,13 +44,17 @@ export default function ExtensionsSection() {
|
|||||||
fetchExtensions();
|
fetchExtensions();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleExtensionToggle = async (name: string) => {
|
const handleExtensionToggle = async (extension: FixedExtensionEntry) => {
|
||||||
try {
|
// If extension is enabled, we are trying to toggle if off, otherwise on
|
||||||
await toggleExtension(name);
|
const toggleDirection = extension.enabled ? 'toggleOff' : 'toggleOn';
|
||||||
fetchExtensions(); // Refresh the list after toggling
|
const extensionConfig = extractExtensionConfig(extension);
|
||||||
} catch (error) {
|
await toggleExtension({
|
||||||
console.error('Failed to toggle extension:', error);
|
toggle: toggleDirection,
|
||||||
}
|
extensionConfig: extensionConfig,
|
||||||
|
addToConfig: addExtension,
|
||||||
|
removeFromConfig: removeExtension,
|
||||||
|
});
|
||||||
|
await fetchExtensions(); // Refresh the list after toggling
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleConfigureClick = (extension: FixedExtensionEntry) => {
|
const handleConfigureClick = (extension: FixedExtensionEntry) => {
|
||||||
@@ -60,38 +64,29 @@ export default function ExtensionsSection() {
|
|||||||
|
|
||||||
const handleAddExtension = async (formData: ExtensionFormData) => {
|
const handleAddExtension = async (formData: ExtensionFormData) => {
|
||||||
const extensionConfig = createExtensionConfig(formData);
|
const extensionConfig = createExtensionConfig(formData);
|
||||||
|
// TODO: replace activateExtension in index
|
||||||
try {
|
// TODO: make sure error handling works
|
||||||
await activateExtension(formData.name, extensionConfig, addExtension);
|
await activateExtension({ addToConfig: addExtension, extensionConfig: extensionConfig });
|
||||||
console.log('attempting to add extension');
|
handleModalClose();
|
||||||
await updateAgent(extensionConfig);
|
await fetchExtensions();
|
||||||
handleModalClose();
|
|
||||||
await fetchExtensions(); // Refresh the list after adding
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to add extension:', error);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleUpdateExtension = async (formData: ExtensionFormData) => {
|
const handleUpdateExtension = async (formData: ExtensionFormData) => {
|
||||||
const extensionConfig = createExtensionConfig(formData);
|
const extensionConfig = createExtensionConfig(formData);
|
||||||
|
|
||||||
try {
|
await updateExtension({
|
||||||
await activateExtension(formData.name, extensionConfig, addExtension);
|
enabled: formData.enabled,
|
||||||
handleModalClose();
|
extensionConfig: extensionConfig,
|
||||||
fetchExtensions(); // Refresh the list after updating
|
addToConfig: addExtension,
|
||||||
} catch (error) {
|
});
|
||||||
console.error('Failed to update extension configuration:', error);
|
handleModalClose();
|
||||||
}
|
await fetchExtensions();
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleDeleteExtension = async (name: string) => {
|
const handleDeleteExtension = async (name: string) => {
|
||||||
try {
|
await deleteExtension({ name, removeFromConfig: removeExtension });
|
||||||
await removeExtension(name);
|
handleModalClose();
|
||||||
handleModalClose();
|
await fetchExtensions();
|
||||||
fetchExtensions(); // Refresh the list after deleting
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to delete extension:', error);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleModalClose = () => {
|
const handleModalClose = () => {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import { toast } from 'react-toastify';
|
|||||||
import { ToastError, ToastLoading, ToastSuccess } from '../../settings/models/toasts';
|
import { ToastError, ToastLoading, ToastSuccess } from '../../settings/models/toasts';
|
||||||
|
|
||||||
// Default extension timeout in seconds
|
// Default extension timeout in seconds
|
||||||
|
// TODO: keep in sync with rust better
|
||||||
export const DEFAULT_EXTENSION_TIMEOUT = 300;
|
export const DEFAULT_EXTENSION_TIMEOUT = 300;
|
||||||
|
|
||||||
// Type definition for built-in extensions from JSON
|
// Type definition for built-in extensions from JSON
|
||||||
@@ -33,7 +34,7 @@ function handleError(message: string, shouldThrow = false): void {
|
|||||||
ToastError({
|
ToastError({
|
||||||
title: 'Error',
|
title: 'Error',
|
||||||
msg: message,
|
msg: message,
|
||||||
errorMessage: message,
|
traceback: message,
|
||||||
});
|
});
|
||||||
console.error(message);
|
console.error(message);
|
||||||
if (shouldThrow) {
|
if (shouldThrow) {
|
||||||
@@ -57,6 +58,11 @@ async function replaceWithShims(cmd: string) {
|
|||||||
return cmd;
|
return cmd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface activateExtensionProps {
|
||||||
|
addToConfig: (name: string, extensionConfig: ExtensionConfig, enabled: boolean) => Promise<void>;
|
||||||
|
extensionConfig: ExtensionConfig;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Activates an extension by adding it to both the config system and the API.
|
* Activates an extension by adding it to both the config system and the API.
|
||||||
* @param name The extension name
|
* @param name The extension name
|
||||||
@@ -64,67 +70,151 @@ async function replaceWithShims(cmd: string) {
|
|||||||
* @param addExtensionFn Function to add extension to config
|
* @param addExtensionFn Function to add extension to config
|
||||||
* @returns Promise that resolves when activation is complete
|
* @returns Promise that resolves when activation is complete
|
||||||
*/
|
*/
|
||||||
export async function activateExtension(
|
export async function activateExtension({
|
||||||
name: string,
|
addToConfig,
|
||||||
config: ExtensionConfig,
|
extensionConfig,
|
||||||
addExtensionFn: (name: string, config: ExtensionConfig, enabled: boolean) => Promise<void>
|
}: activateExtensionProps): Promise<void> {
|
||||||
): Promise<void> {
|
|
||||||
let toastId;
|
|
||||||
try {
|
try {
|
||||||
// Show loading toast
|
// AddToAgent
|
||||||
toastId = ToastLoading({ title: name, msg: 'Adding extension...' });
|
await AddToAgent(extensionConfig);
|
||||||
|
|
||||||
// First add to the config system
|
|
||||||
await addExtensionFn(nameToKey(name), config, true);
|
|
||||||
|
|
||||||
// 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,
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
const data = await response.json();
|
|
||||||
|
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = `Failed to add ${name} extension: ${error instanceof Error ? error.message : 'Unknown error'}`;
|
// add to config with enabled = false
|
||||||
console.error(errorMessage);
|
await addToConfig(extensionConfig.name, extensionConfig, false);
|
||||||
if (toastId) toast.dismiss(toastId);
|
// show user the error, return
|
||||||
ToastError({
|
console.log('error', error);
|
||||||
title: name,
|
return;
|
||||||
msg: 'Failed to add extension',
|
}
|
||||||
errorMessage: error.message,
|
|
||||||
});
|
// 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface updateExtensionProps {
|
||||||
|
enabled: boolean;
|
||||||
|
addToConfig: (name: string, extensionConfig: ExtensionConfig, enabled: boolean) => Promise<void>;
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface toggleExtensionProps {
|
||||||
|
toggle: 'toggleOn' | 'toggleOff';
|
||||||
|
extensionConfig: ExtensionConfig;
|
||||||
|
addToConfig: (name: string, extensionConfig: ExtensionConfig, enabled: boolean) => Promise<void>;
|
||||||
|
removeFromConfig: (name: string) => Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
/*Deeplinks*/
|
||||||
|
}
|
||||||
|
|
||||||
export async function addExtensionFromDeepLink(
|
export async function addExtensionFromDeepLink(
|
||||||
url: string,
|
url: string,
|
||||||
addExtensionFn: (name: string, config: ExtensionConfig, enabled: boolean) => Promise<void>,
|
addExtensionFn: (
|
||||||
|
name: string,
|
||||||
|
extensionConfig: ExtensionConfig,
|
||||||
|
enabled: boolean
|
||||||
|
) => Promise<void>,
|
||||||
setView: (view: string, options: { extensionId: string; showEnvVars: boolean }) => void
|
setView: (view: string, options: { extensionId: string; showEnvVars: boolean }) => void
|
||||||
) {
|
) {
|
||||||
const parsedUrl = new URL(url);
|
const parsedUrl = new URL(url);
|
||||||
@@ -202,7 +292,11 @@ export async function addExtensionFromDeepLink(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If no env vars are required, proceed with adding the extension
|
// 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
|
// Call with an empty list to ensure all built-ins are added
|
||||||
await syncBuiltInExtensions([], addExtensionFn);
|
await syncBuiltInExtensions([], addExtensionFn);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
/* Agent-related helper functions */
|
||||||
|
}
|
||||||
|
async function extensionApiCall<T>(
|
||||||
|
endpoint: string,
|
||||||
|
payload: any,
|
||||||
|
actionType: 'adding' | 'removing',
|
||||||
|
extensionName: string
|
||||||
|
): Promise<Response> {
|
||||||
|
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<Response> {
|
||||||
|
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<Response> {
|
||||||
|
return extensionApiCall('/extensions/remove', name, 'removing', name);
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { getSubtitle, getFriendlyTitle } from './ExtensionList';
|
|||||||
|
|
||||||
interface ExtensionItemProps {
|
interface ExtensionItemProps {
|
||||||
extension: FixedExtensionEntry;
|
extension: FixedExtensionEntry;
|
||||||
onToggle: (name: string) => void;
|
onToggle: (extension: FixedExtensionEntry) => void;
|
||||||
onConfigure: (extension: FixedExtensionEntry) => void;
|
onConfigure: (extension: FixedExtensionEntry) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,7 +37,7 @@ export default function ExtensionItem({ extension, onToggle, onConfigure }: Exte
|
|||||||
)}
|
)}
|
||||||
<Switch
|
<Switch
|
||||||
checked={extension.enabled}
|
checked={extension.enabled}
|
||||||
onCheckedChange={() => onToggle(extension.name)}
|
onCheckedChange={() => onToggle(extension)}
|
||||||
variant="mono"
|
variant="mono"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { combineCmdAndArgs } from '../utils';
|
|||||||
|
|
||||||
interface ExtensionListProps {
|
interface ExtensionListProps {
|
||||||
extensions: FixedExtensionEntry[];
|
extensions: FixedExtensionEntry[];
|
||||||
onToggle: (name: string) => void;
|
onToggle: (extension: FixedExtensionEntry) => void;
|
||||||
onConfigure: (extension: FixedExtensionEntry) => void;
|
onConfigure: (extension: FixedExtensionEntry) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -95,3 +95,13 @@ export function splitCmdAndArgs(str: string): { cmd: string; args: string[] } {
|
|||||||
export function combineCmdAndArgs(cmd: string, args: string[]): string {
|
export function combineCmdAndArgs(cmd: string, args: string[]): string {
|
||||||
return [cmd, ...args].join(' ');
|
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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ export const AddModelModal = ({ onClose }: AddModelModalProps) => {
|
|||||||
} catch (e) {
|
} catch (e) {
|
||||||
ToastError({
|
ToastError({
|
||||||
title: 'Failed to add model',
|
title: 'Failed to add model',
|
||||||
errorMessage: e.message,
|
traceback: e.message,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user