Files
goose/ui/desktop/src/components/settings_v2/extensions/agent-api.ts

175 lines
5.3 KiB
TypeScript

import { ExtensionConfig } from '../../../api/types.gen';
import { getApiUrl, getSecretKey } from '../../../config';
import { toastService, ToastServiceOptions } from '../../../toasts';
import { replaceWithShims } from './utils';
/**
* Makes an API call to the extension endpoints
*/
export async function extensionApiCall(
endpoint: string,
payload: any,
options: ToastServiceOptions = {}
): Promise<Response> {
// Configure toast notifications
toastService.configure(options);
// Determine if we're activating or removing an extension
const isActivating = endpoint == '/extensions/add';
const action = {
type: isActivating ? 'activating' : 'removing',
verb: isActivating ? 'Activating' : 'Removing',
pastTense: isActivating ? 'activated' : 'removed',
presentTense: isActivating ? 'activate' : 'remove',
};
// for adding the payload is an extensionConfig, for removing payload is just the name
const extensionName = isActivating ? payload.name : payload;
let toastId;
// Step 1: Show loading toast (only for activation of stdio)
if (isActivating && (payload as ExtensionConfig) && payload.type == 'stdio') {
toastId = toastService.loading({
title: extensionName,
msg: `${action.verb} ${extensionName} extension...`,
});
}
try {
// Step 2: Make the API call
const response = await fetch(getApiUrl(endpoint), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Secret-Key': getSecretKey(),
},
body: JSON.stringify(payload),
});
// Step 3: Handle non-successful responses
if (!response.ok) {
return handleErrorResponse(response, extensionName, action, toastId);
}
// Step 4: Parse response data
const data = await parseResponseData(response);
// Step 5: Check for errors in the response data
if (data.error) {
const errorMessage = `Error ${action.type} extension: ${data.message || 'Unknown error'}`;
toastService.dismiss(toastId);
toastService.error({
title: extensionName,
msg: errorMessage,
traceback: data.message || 'Unknown error',
});
throw new Error(errorMessage);
}
// Step 6: Success - dismiss loading toast and return
toastService.dismiss(toastId);
toastService.success({
title: extensionName,
msg: `Successfully ${action.pastTense} extension!`,
});
return response;
} catch (error) {
// Final catch-all error handler
toastService.dismiss(toastId);
const msg = error.length < 70 ? error : `Failed to ${action.presentTense} extension`;
toastService.error({
title: extensionName,
msg: msg,
traceback: error,
});
console.error(`Error in extensionApiCall for ${extensionName}:`, error);
throw error;
}
}
// Helper functions to separate concerns
// Handles HTTP error responses
function handleErrorResponse(
response: Response,
extensionName: string,
action: { type: string; verb: string },
toastId: string
): never {
const errorMsg = `Server returned ${response.status}: ${response.statusText}`;
console.error(errorMsg);
// Special case: Agent not initialized (status 428)
if (response.status === 428 && action.type === 'activating') {
toastService.dismiss(toastId);
toastService.error({
title: extensionName,
msg: 'Failed to add extension. Goose Agent was still starting up. Please try again.',
traceback: errorMsg,
});
throw new Error('Agent is not initialized. Please initialize the agent first.');
}
// General error case
const msg = `Failed to ${action.type === 'activating' ? 'add' : 'remove'} ${extensionName} extension: ${errorMsg}`;
toastService.dismiss(toastId);
toastService.error({
title: extensionName,
msg: msg,
traceback: errorMsg,
});
throw new Error(msg);
}
// Safely parses JSON response
async function parseResponseData(response: Response): Promise<any> {
try {
const text = await response.text();
return text ? JSON.parse(text) : { error: false };
} catch (parseError) {
console.warn('Could not parse response as JSON, assuming success', parseError);
return { error: false };
}
}
/**
* Add an extension to the agent
*/
export async function addToAgent(
extension: ExtensionConfig,
options: ToastServiceOptions = {}
): Promise<Response> {
try {
if (extension.type === 'stdio') {
extension.cmd = await replaceWithShims(extension.cmd);
}
return await extensionApiCall('/extensions/add', extension, options);
} catch (error) {
// Check if this is a 428 error and make the message more descriptive
if (error.message && error.message.includes('428')) {
const enhancedError = new Error(
'Failed to add extension. Goose Agent was still starting up. Please try again.'
);
console.error(`Failed to add extension ${extension.name} to agent: ${enhancedError.message}`);
throw enhancedError;
}
throw error;
}
}
/**
* Remove an extension from the agent
*/
export async function removeFromAgent(
name: string,
options: ToastServiceOptions = {}
): Promise<Response> {
try {
return await extensionApiCall('/extensions/remove', name, options);
} catch (error) {
console.error(`Failed to remove extension ${name} from agent:`, error);
throw error;
}
}