ui: turn on extensions at startup (#1861)

This commit is contained in:
Lily Delalande
2025-03-25 22:21:53 -04:00
committed by GitHub
parent f098fed862
commit a8dd39a39b
3 changed files with 204 additions and 110 deletions

View File

@@ -24,12 +24,14 @@ import ProviderSettings from './components/settings_v2/providers/ProviderSetting
import { useChat } from './hooks/useChat';
import 'react-toastify/dist/ReactToastify.css';
import { useConfig } from './components/ConfigContext';
import { FixedExtensionEntry, useConfig } from './components/ConfigContext';
import {
initializeBuiltInExtensions,
syncBuiltInExtensions,
addExtensionFromDeepLink as addExtensionFromDeepLinkV2,
addToAgentOnStartup,
} from './components/settings_v2/extensions';
import { extractExtensionConfig } from './components/settings_v2/extensions/utils';
// Views and their options
export type View =
@@ -74,6 +76,8 @@ export default function App() {
}
// this is all settings v2 stuff
// Modified version of the alpha initialization flow for App.tsx
useEffect(() => {
// Skip if feature flag is not enabled
if (!process.env.ALPHA) {
@@ -82,24 +86,68 @@ export default function App() {
console.log('Alpha flow initializing...');
const setupExtensions = async () => {
// First quickly check if we have model and provider to set chat view
const checkRequiredConfig = async () => {
try {
console.log('Setting up extensions...');
console.log('Reading GOOSE_PROVIDER and GOOSE_MODEL from config...');
const provider = (await read('GOOSE_PROVIDER', false)) as string;
const model = (await read('GOOSE_MODEL', false)) as string;
if (provider && model) {
// We have all needed configuration, set chat view immediately
console.log(`Found provider: ${provider}, model: ${model}, setting chat view`);
setView('chat');
// Initialize the system in background
initializeSystem(provider, model)
.then(() => console.log('System initialization successful'))
.catch((error) => {
console.error('Error initializing system:', error);
setFatalError(`System initialization error: ${error.message || 'Unknown error'}`);
setView('welcome');
});
} else {
// Missing configuration, show onboarding
console.log('Missing configuration, showing onboarding');
if (!provider) console.log('Missing provider');
if (!model) console.log('Missing model');
setView('welcome');
}
} catch (error) {
console.error('Error checking configuration:', error);
setFatalError(`Configuration check error: ${error.message || 'Unknown error'}`);
setView('welcome');
}
};
// Setup extensions in parallel
const setupExtensions = async () => {
// Set the ref immediately to prevent duplicate runs
initAttemptedRef.current = true;
console.log('Set initAttemptedRef to prevent duplicate runs');
let refreshedExtensions: FixedExtensionEntry[] = [];
try {
// Force refresh extensions from the backend to ensure we have the latest
console.log('Getting extensions from backend...');
const refreshedExtensions = await getExtensions(true);
refreshedExtensions = await getExtensions(true);
console.log(`Retrieved ${refreshedExtensions.length} extensions`);
} catch (error) {
console.log('Error getting extensions list');
return; // Exit early if we can't get the extensions list
}
// built-in extensions block -- just adds them to config if missing
try {
console.log('Setting up built-in extensions...');
if (refreshedExtensions.length === 0) {
// If we still have no extensions, this is truly a first-time setup
console.log('First-time setup: Adding all built-in extensions...');
await initializeBuiltInExtensions(addExtension);
console.log('Built-in extensions initialization complete');
// Refresh the extensions list after initialization
refreshedExtensions = await getExtensions(true);
} else {
// Extensions exist, check for any missing built-ins
console.log('Checking for missing built-in extensions...');
@@ -109,69 +157,37 @@ export default function App() {
}
} catch (error) {
console.error('Error setting up extensions:', error);
console.error('Extension setup error details:', {
message: error.message,
stack: error.stack,
name: error.name,
});
// We don't set fatal error here since the app might still work without extensions
}
};
const initializeApp = async () => {
try {
console.log('Initializing alpha app...');
// Check if we have the required configuration
console.log('Reading GOOSE_PROVIDER from config...');
const provider = (await read('GOOSE_PROVIDER', false)) as string;
console.log('Provider from config:', provider);
console.log('Reading GOOSE_MODEL from config...');
const model = (await read('GOOSE_MODEL', false)) as string;
console.log('Model from config:', model);
if (provider && model) {
// We have all needed configuration, initialize the system
console.log(`Initializing system with provider: ${provider}, model: ${model}`);
await initializeSystem(provider, model);
console.log('System initialization successful');
setView('chat');
// now try to add to agent
console.log('Adding enabled extensions to agent...');
for (const extensionEntry of refreshedExtensions) {
if (extensionEntry.enabled) {
console.log(`Adding extension to agent: ${extensionEntry.name}`);
// need to convert to config because that's what the endpoint expects
const extensionConfig = extractExtensionConfig(extensionEntry);
// will handle toasts and also set failures to enabled = false
await addToAgentOnStartup({ addToConfig: addExtension, extensionConfig });
} else {
// Missing configuration, show onboarding
console.log('Missing configuration, showing onboarding');
if (!provider) console.log('Missing provider');
if (!model) console.log('Missing model');
setView('welcome');
console.log(`Skipping disabled extension: ${extensionEntry.name}`);
}
} catch (error) {
console.error('Error initializing app:', error);
console.error('App initialization error details:', {
message: error.message,
stack: error.stack,
name: error.name,
});
setFatalError(`Alpha initialization error: ${error.message || 'Unknown error'}`);
setView('welcome');
}
console.log('Extensions setup complete');
};
// Execute with better promise handling
initializeApp()
.then(() => console.log('Alpha app initialization complete'))
.catch((error) => {
console.error('Unhandled error in initializeApp:', error);
setFatalError(`Unhandled alpha app error: ${error.message || 'Unknown error'}`);
// Execute the two flows in parallel for speed
checkRequiredConfig().catch((error) => {
console.error('Unhandled error in checkRequiredConfig:', error);
setFatalError(`Config check error: ${error.message || 'Unknown error'}`);
});
setupExtensions()
.then(() => console.log('Extensions setup complete'))
.catch((error) => {
setupExtensions().catch((error) => {
console.error('Unhandled error in setupExtensions:', error);
// Not setting fatal error here since extensions are optional
});
}, []); // Empty dependency array since we're using initAttemptedRef
const setView = (view: View, viewOptions: Record<any, any> = {}) => {
console.log(`Setting view to: ${view}`, viewOptions);
setInternalView({ view, viewOptions });

View File

@@ -20,10 +20,6 @@ export default function SettingsView({
setView: (view: View) => void;
viewOptions: SettingsViewOptions;
}) {
const { config } = useConfig();
console.log(config);
return (
<div className="h-screen w-full">
<div className="relative flex items-center h-[36px] w-full bg-bgSubtle"></div>

View File

@@ -78,21 +78,51 @@ export async function activateExtension({
// AddToAgent
await AddToAgent(extensionConfig);
} catch (error) {
console.error('Failed to add extension to agent:', error);
// add to config with enabled = false
await addToConfig(extensionConfig.name, extensionConfig, false);
// show user the error, return
console.log('error', error);
return;
// Rethrow the error to inform the caller
throw error;
}
// Then add to config
try {
await addToConfig(extensionConfig.name, extensionConfig, true);
} catch (error) {
console.error('Failed to add extension to config:', error);
// remove from Agent
try {
await RemoveFromAgent(extensionConfig.name);
// config error workflow
console.log('error', error);
} catch (removeError) {
console.error('Failed to remove extension from agent after config failure:', removeError);
}
// Rethrow the error to inform the caller
throw error;
}
}
interface addToAgentOnStartupProps {
addToConfig: (name: string, extensionConfig: ExtensionConfig, enabled: boolean) => Promise<void>;
extensionConfig: ExtensionConfig;
}
export async function addToAgentOnStartup({
addToConfig,
extensionConfig,
}: addToAgentOnStartupProps): Promise<void> {
try {
// AddToAgent
await AddToAgent(extensionConfig);
} catch (error) {
console.log('got error trying to add to agent in addAgentOnStartUp', error);
// update config with enabled = false
try {
await toggleExtension({ toggle: 'toggleOff', extensionConfig, addToConfig });
} catch (toggleError) {
console.error('Failed to toggle extension off after agent error:', toggleError);
}
// Rethrow the error to inform the caller
throw error;
}
}
@@ -113,26 +143,24 @@ export async function updateExtension({
// 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;
console.error('Failed to add extension to agent during update:', error);
// Failed to add to agent -- show that error to user and do not update the config file
throw error;
}
// Then add to config
try {
await addToConfig(extensionConfig.name, extensionConfig, enabled);
} catch (error) {
// config error workflow
console.log('error', error);
console.error('Failed to update extension in config:', error);
throw 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);
console.error('Failed to update disabled extension in config:', error);
throw error;
}
}
}
@@ -141,7 +169,6 @@ 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({
@@ -155,33 +182,50 @@ export async function toggleExtension({
// 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;
console.error('Error adding extension to agent. Will try to toggle back off. Error:', error);
try {
await toggleExtension({ toggle: 'toggleOff', extensionConfig, addToConfig });
} catch (toggleError) {
console.error('Failed to toggle extension off after agent error:', toggleError);
}
throw error;
}
// update the config
try {
await addToConfig(extensionConfig.name, extensionConfig, true);
} catch (error) {
// remove from agent?
console.error('Failed to update config after enabling extension:', error);
// remove from agent
try {
await RemoveFromAgent(extensionConfig.name);
} catch (removeError) {
console.error('Failed to remove extension from agent after config failure:', removeError);
}
throw error;
}
} else if (toggle == 'toggleOff') {
// enabled to disabled
let agentRemoveError = null;
try {
await RemoveFromAgent(extensionConfig.name);
} catch (error) {
// note there was an error, but remove from config anyway
// note there was an error, but attempt to remove from config anyway
console.error('Error removing extension from agent', extensionConfig.name, error);
agentRemoveError = 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);
console.error('Error removing extension from config', extensionConfig.name, 'Error:', error);
throw error;
}
// If we had an error removing from agent but succeeded updating config, still throw the original error
if (agentRemoveError) {
throw agentRemoveError;
}
}
}
@@ -193,15 +237,29 @@ interface deleteExtensionProps {
export async function deleteExtension({ name, removeFromConfig }: deleteExtensionProps) {
// remove from agent
let agentRemoveError = null;
try {
await RemoveFromAgent(name);
} catch (error) {
console.error('Failed to remove extension from agent during deletion:', error);
agentRemoveError = error;
}
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)
console.error(
'Failed to remove extension from config after removing from agent. Error:',
error
);
// If we also had an agent remove error, log it but throw the config error as it's more critical
throw error;
}
// If we had an error removing from agent but succeeded removing from config, still throw the original error
if (agentRemoveError) {
throw agentRemoveError;
}
}
{
@@ -292,7 +350,12 @@ export async function addExtensionFromDeepLink(
}
// If no env vars are required, proceed with adding the extension
try {
await activateExtension({ extensionConfig: config, addToConfig: addExtensionFn });
} catch (error) {
console.error('Failed to activate extension from deeplink:', error);
throw error;
}
}
{
@@ -340,8 +403,13 @@ export async function syncBuiltInExtensions(
};
// Add the extension with its default enabled state
try {
await addExtensionFn(nameToKey(builtinExt.name), extConfig, builtinExt.enabled);
addedCount++;
} catch (error) {
console.error(`Failed to add built-in extension ${builtinExt.name}:`, error);
// Continue with other extensions even if one fails
}
}
}
@@ -415,7 +483,7 @@ async function extensionApiCall<T>(
msg: 'Agent is not initialized. Please initialize the agent first.',
traceback: errorMsg,
});
return response;
throw new Error('Agent is not initialized. Please initialize the agent first.');
}
const msg = `Failed to ${actionType === 'adding' ? 'add' : 'remove'} ${extensionName} extension: ${errorMsg}`;
@@ -427,7 +495,7 @@ async function extensionApiCall<T>(
msg: msg,
traceback: errorMsg,
});
return response;
throw new Error(msg);
}
// Parse response JSON safely
@@ -435,40 +503,54 @@ async function extensionApiCall<T>(
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);
} catch (parseError) {
console.warn('Could not parse response as JSON, assuming success', parseError);
data = { error: false };
}
if (!data.error) {
if (toastId) toast.dismiss(toastId);
ToastSuccess({ title: extensionName, msg: 'Successfully enabled extension' });
ToastSuccess({ title: extensionName, msg: `Successfully ${pastVerb} extension` });
return response;
} else {
const errorMessage = `Error adding extension -- parsing data`;
const errorMessage = `Error ${actionType} extension -- parsing data: ${data.message || 'Unknown error'}`;
console.error(errorMessage);
if (toastId) toast.dismiss(toastId);
ToastError({
title: extensionName,
msg: errorMessage,
traceback: data.message, // why data.message not data.error?
traceback: data.message || 'Unknown error', // why data.message not data.error?
});
throw new Error(errorMessage);
}
} catch (error) {
//
if (toastId) toast.dismiss(toastId);
console.error(`Error in extensionApiCall for ${extensionName}:`, error);
throw error;
}
}
// Public functions
export async function AddToAgent(extension: ExtensionConfig): Promise<Response> {
try {
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);
return await extensionApiCall('/extensions/add', extension, 'adding', extension.name);
} catch (error) {
console.error(`Failed to add extension ${extension.name} to agent:`, error);
throw error;
}
}
export async function RemoveFromAgent(name: string): Promise<Response> {
return extensionApiCall('/extensions/remove', name, 'removing', name);
try {
return await extensionApiCall('/extensions/remove', name, 'removing', name);
} catch (error) {
console.error(`Failed to remove extension ${name} from agent:`, error);
throw error;
}
}