diff --git a/ui/desktop/src/App.tsx b/ui/desktop/src/App.tsx index 1aa92eba..f5a03186 100644 --- a/ui/desktop/src/App.tsx +++ b/ui/desktop/src/App.tsx @@ -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; - // Set the ref immediately to prevent duplicate runs - initAttemptedRef.current = true; - console.log('Set initAttemptedRef to prevent duplicate runs'); + 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; + + 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) => { - console.error('Unhandled error in setupExtensions:', error); - // Not setting fatal error here since extensions are optional - }); + 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 = {}) => { console.log(`Setting view to: ${view}`, viewOptions); setInternalView({ view, viewOptions }); diff --git a/ui/desktop/src/components/settings_v2/SettingsView.tsx b/ui/desktop/src/components/settings_v2/SettingsView.tsx index c0362712..32f33b90 100644 --- a/ui/desktop/src/components/settings_v2/SettingsView.tsx +++ b/ui/desktop/src/components/settings_v2/SettingsView.tsx @@ -20,10 +20,6 @@ export default function SettingsView({ setView: (view: View) => void; viewOptions: SettingsViewOptions; }) { - const { config } = useConfig(); - - console.log(config); - return (
diff --git a/ui/desktop/src/components/settings_v2/extensions/index.ts b/ui/desktop/src/components/settings_v2/extensions/index.ts index ff33ffa6..7da160f2 100644 --- a/ui/desktop/src/components/settings_v2/extensions/index.ts +++ b/ui/desktop/src/components/settings_v2/extensions/index.ts @@ -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 - await RemoveFromAgent(extensionConfig.name); - // config error workflow - console.log('error', error); + try { + await RemoveFromAgent(extensionConfig.name); + } 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; + extensionConfig: ExtensionConfig; +} + +export async function addToAgentOnStartup({ + addToConfig, + extensionConfig, +}: addToAgentOnStartupProps): Promise { + 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; - removeFromConfig: (name: string) => Promise; } 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? - await RemoveFromAgent(extensionConfig.name); + 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 - await RemoveFromAgent(name); + 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 - await activateExtension({ extensionConfig: config, addToConfig: addExtensionFn }); + 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 - await addExtensionFn(nameToKey(builtinExt.name), extConfig, builtinExt.enabled); - addedCount++; + 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( 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( msg: msg, traceback: errorMsg, }); - return response; + throw new Error(msg); } // Parse response JSON safely @@ -435,40 +503,54 @@ async function extensionApiCall( 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 { - if (extension.type === 'stdio') { - console.log('extension command', extension.cmd); - extension.cmd = await replaceWithShims(extension.cmd); - console.log('next ext command', extension.cmd); - } + 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 { - 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; + } }