diff --git a/ui/desktop/src/components/RecipeEditor.tsx b/ui/desktop/src/components/RecipeEditor.tsx index 39418f7b..9f949c94 100644 --- a/ui/desktop/src/components/RecipeEditor.tsx +++ b/ui/desktop/src/components/RecipeEditor.tsx @@ -3,11 +3,10 @@ import { useNavigate } from 'react-router-dom'; import { Recipe, generateDeepLink } from '../recipe'; import { Parameter } from '../recipe/index'; -import { FullExtensionConfig } from '../extensions'; import { Geese } from './icons/Geese'; import Copy from './icons/Copy'; import { Check, Save, Calendar } from 'lucide-react'; -import { useConfig } from './ConfigContext'; +import { ExtensionConfig, useConfig } from './ConfigContext'; import { FixedExtensionEntry } from './ConfigContext'; import RecipeActivityEditor from './RecipeActivityEditor'; import RecipeInfoModal from './RecipeInfoModal'; @@ -186,7 +185,7 @@ export default function RecipeEditor({ config }: RecipeEditorProps) { } return cleanExtension; }) - .filter(Boolean) as FullExtensionConfig[], + .filter(Boolean) as ExtensionConfig[], }; console.log('Final config extensions:', config.extensions); diff --git a/ui/desktop/src/components/ViewRecipeModal.tsx b/ui/desktop/src/components/ViewRecipeModal.tsx index d27d57a3..478eb6a5 100644 --- a/ui/desktop/src/components/ViewRecipeModal.tsx +++ b/ui/desktop/src/components/ViewRecipeModal.tsx @@ -1,11 +1,10 @@ import { useState, useEffect, useCallback } from 'react'; import { Recipe, generateDeepLink } from '../recipe'; import { Parameter } from '../recipe/index'; -import { FullExtensionConfig } from '../extensions'; import { Geese } from './icons/Geese'; import Copy from './icons/Copy'; import { Check, Save, Calendar, X } from 'lucide-react'; -import { useConfig } from './ConfigContext'; +import { ExtensionConfig, useConfig } from './ConfigContext'; import { FixedExtensionEntry } from './ConfigContext'; import RecipeActivityEditor from './RecipeActivityEditor'; import RecipeInfoModal from './RecipeInfoModal'; @@ -171,7 +170,7 @@ export default function ViewRecipeModal({ isOpen, onClose, config }: ViewRecipeM } return cleanExtension; }) - .filter(Boolean) as FullExtensionConfig[], + .filter(Boolean) as ExtensionConfig[], }; return updatedConfig; diff --git a/ui/desktop/src/extensions.tsx b/ui/desktop/src/extensions.tsx deleted file mode 100644 index cfaf5e53..00000000 --- a/ui/desktop/src/extensions.tsx +++ /dev/null @@ -1,232 +0,0 @@ -import { getApiUrl } from './config'; -import { toast } from 'react-toastify'; -import { safeJsonParse } from './utils/jsonUtils'; - -import builtInExtensionsData from './built-in-extensions.json'; -import { toastError, toastLoading, toastSuccess } from './toasts'; - -// Hardcoded default extension timeout in seconds -export const DEFAULT_EXTENSION_TIMEOUT = 300; - -// ExtensionConfig type matching the Rust version -// TODO: refactor this -export type ExtensionConfig = - | { - type: 'sse'; - name: string; - uri: string; - env_keys?: string[]; - timeout?: number; - } - | { - type: 'streamable_http'; - name: string; - uri: string; - env_keys?: string[]; - headers?: Record; - timeout?: number; - } - | { - type: 'stdio'; - name: string; - cmd: string; - args: string[]; - env_keys?: string[]; - timeout?: number; - } - | { - type: 'builtin'; - name: string; - env_keys?: string[]; - timeout?: number; - }; - -// FullExtensionConfig type matching all the fields that come in deep links and are stored in local storage -export type FullExtensionConfig = ExtensionConfig & { - id: string; - description: string; - enabled: boolean; -}; - -export interface ExtensionPayload { - name?: string; - type?: string; - cmd?: string; - args?: string[]; - uri?: string; - env_keys?: string[]; - timeout?: number; -} - -export const BUILT_IN_EXTENSIONS = builtInExtensionsData as FullExtensionConfig[]; - -function sanitizeName(name: string) { - return name.toLowerCase().replace(/-/g, '').replace(/_/g, '').replace(/\s/g, ''); -} - -export async function addExtension( - extension: FullExtensionConfig, - silent: boolean = false -): Promise { - try { - console.log('Adding extension:', extension); - // Create the config based on the extension type - const config = { - type: extension.type, - ...(extension.type === 'stdio' && { - name: sanitizeName(extension.name), - cmd: await replaceWithShims(extension.cmd), - args: extension.args || [], - }), - ...(extension.type === 'sse' && { - name: sanitizeName(extension.name), - uri: extension.uri, - }), - ...(extension.type === 'streamable_http' && { - name: sanitizeName(extension.name), - uri: extension.uri, - }), - ...(extension.type === 'builtin' && { - name: sanitizeName(extension.name), - }), - env_keys: extension.env_keys, - timeout: extension.timeout, - }; - - let toastId; - if (!silent) toastId = toastLoading({ title: extension.name, msg: 'Adding extension...' }); - - const response = await fetch(getApiUrl('/extensions/add'), { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Secret-Key': await window.electron.getSecretKey(), - }, - body: JSON.stringify(config), - }); - - const responseText = await response.text(); - - if (!response.ok) { - const errorMsg = `Server returned ${response.status}: ${response.statusText}. Response: ${responseText}`; - console.error(errorMsg); - if (toastId) toast.dismiss(toastId); - toastError({ - title: extension.name, - msg: 'Failed to add extension', - traceback: errorMsg, - toastOptions: { autoClose: false }, - }); - return response; - } - - // Only try to parse JSON if we got a successful response and have JSON content - let data; - try { - data = JSON.parse(responseText); - } catch (e) { - console.error('Failed to parse response as JSON:', e); - data = { error: true, message: responseText }; - } - - if (!data.error) { - if (!silent) { - if (toastId) toast.dismiss(toastId); - toastSuccess({ title: extension.name, msg: `Successfully enabled extension` }); - } - return response; - } - - var errorMessage = `Error adding extension`; - // Attempt to extract the message from inside StdioProcessError() - // NOTE: this may change if the error response from /extensions/add changes - const regex = /StdioProcessError\("(.*?)"\)/; - const match = data.message.match(regex); - - if (match) { - const extracted = match[1]; - // only display the message if it is less than 100 chars - errorMessage = extracted.length > 100 ? errorMessage : extracted; - } - - if (toastId) toast.dismiss(toastId); - toastError({ - title: extension.name, - msg: errorMessage, - traceback: data.message, - toastOptions: { autoClose: false }, - }); - - return response; - } catch (error) { - const errorMessage = `Failed to add ${extension.name} extension: ${error instanceof Error ? error.message : 'Unknown error'}`; - console.error(errorMessage); - toastError({ - title: extension.name, - msg: 'Failed to add extension', - traceback: error instanceof Error ? error.message : String(error), - toastOptions: { autoClose: false }, - }); - throw error; - } -} - -export async function removeExtension(name: string, silent: boolean = false): Promise { - try { - const response = await fetch(getApiUrl('/extensions/remove'), { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - 'X-Secret-Key': await window.electron.getSecretKey(), - }, - body: JSON.stringify(sanitizeName(name)), - }); - - const data = await safeJsonParse<{ error: boolean; message: string }>(response); - - if (!data.error) { - if (!silent) { - toastSuccess({ title: name, msg: 'Successfully disabled extension' }); - } - return response; - } - - const errorMessage = `Error removing ${name} extension${data.message ? `. ${data.message}` : ''}`; - console.error(errorMessage); - toastError({ - title: name, - msg: 'Error removing extension', - traceback: data.message, - toastOptions: { autoClose: false }, - }); - return response; - } catch (error) { - const errorMessage = `Failed to remove ${name} extension: ${error instanceof Error ? error.message : 'Unknown error'}`; - console.error(errorMessage); - toastError({ - title: name, - msg: 'Error removing extension', - traceback: error instanceof Error ? error.message : String(error), - toastOptions: { autoClose: false }, - }); - throw error; - } -} - -// Update the path to the binary based on the command -export async function replaceWithShims(cmd: string) { - const binaryPathMap: Record = { - goosed: await window.electron.getBinaryPath('goosed'), - jbang: await window.electron.getBinaryPath('jbang'), - npx: await window.electron.getBinaryPath('npx'), - uvx: await window.electron.getBinaryPath('uvx'), - 'npx.cmd': await window.electron.getBinaryPath('npx.cmd'), - }; - - if (binaryPathMap[cmd]) { - console.log('--------> Replacing command with shim ------>', cmd, binaryPathMap[cmd]); - cmd = binaryPathMap[cmd]; - } - - return cmd; -} diff --git a/ui/desktop/src/utils/providerUtils.ts b/ui/desktop/src/utils/providerUtils.ts index fa099f8e..2d0f322a 100644 --- a/ui/desktop/src/utils/providerUtils.ts +++ b/ui/desktop/src/utils/providerUtils.ts @@ -1,5 +1,4 @@ import { getApiUrl } from '../config'; -import { FullExtensionConfig } from '../extensions'; import { initializeAgent } from '../agent'; import { initializeBundledExtensions, @@ -8,16 +7,7 @@ import { } from '../components/settings/extensions'; import { extractExtensionConfig } from '../components/settings/extensions/utils'; import type { ExtensionConfig, FixedExtensionEntry } from '../components/ConfigContext'; -// TODO: remove when removing migration logic -import { toastService } from '../toasts'; -import { - ExtensionQuery, - RecipeParameter, - SubRecipe, - addExtension as apiAddExtension, - updateSessionConfig, - extendPrompt, -} from '../api'; +import { RecipeParameter, SubRecipe, updateSessionConfig, extendPrompt } from '../api'; import { addSubRecipesToAgent } from '../recipe/add_sub_recipe_on_agent'; export interface Provider { @@ -119,81 +109,6 @@ export const updateSystemPromptWithParameters = async ( } }; -/** - * Migrates extensions from localStorage to config.yaml (settings v2) - * This function handles the migration from settings v1 to v2 by: - * 1. Reading extensions from localStorage - * 2. Adding non-builtin extensions to config.yaml - * 3. Marking the migration as complete - * - * NOTE: This logic can be removed eventually when enough versions have passed - * We leave the existing user settings in localStorage, in case users downgrade - * or things need to be reverted. - */ -export const migrateExtensionsToSettingsV3 = async () => { - console.log('need to perform extension migration v3'); - - const userSettingsStr = localStorage.getItem('user_settings'); - let localStorageExtensions: FullExtensionConfig[] = []; - - try { - if (userSettingsStr) { - const userSettings = JSON.parse(userSettingsStr); - localStorageExtensions = userSettings.extensions ?? []; - } - } catch (error) { - console.error('Failed to parse user settings:', error); - } - - if (localStorageExtensions.length === 0) { - localStorage.setItem('configVersion', '3'); - console.log('No extensions to migrate. Config version set to 3.'); - return; - } - - const migrationErrors: { name: string; error: unknown }[] = []; - - // Process extensions in parallel for better performance - const migrationPromises = localStorageExtensions - .filter((extension) => extension.type !== 'builtin') // Skip builtins as before - .map(async (extension) => { - console.log(`Migrating extension ${extension.name} to config.yaml`); - try { - const query: ExtensionQuery = { - name: extension.name, - config: extension, - enabled: extension.enabled, - }; - await apiAddExtension({ - body: query, - throwOnError: true, - }); - } catch (err) { - console.error(`Failed to migrate extension ${extension.name}:`, err); - migrationErrors.push({ - name: extension.name, - error: `failed migration with ${JSON.stringify(err)}`, - }); - } - }); - - await Promise.allSettled(migrationPromises); - - if (migrationErrors.length === 0) { - localStorage.setItem('configVersion', '3'); - console.log('Extension migration complete. Config version set to 3.'); - } else { - const errorSummaryStr = migrationErrors - .map(({ name, error }) => `- ${name}: ${JSON.stringify(error)}`) - .join('\n'); - toastService.error({ - title: 'Config Migration Error', - msg: 'There was a problem updating your config file', - traceback: errorSummaryStr, - }); - } -}; - export const initializeSystem = async ( provider: string, model: string, @@ -254,22 +169,6 @@ export const initializeSystem = async ( return; } - // NOTE: remove when we want to stop migration logic - // Check if we need to migrate extensions from localStorage to config.yaml - const configVersion = localStorage.getItem('configVersion'); - const shouldMigrateExtensions = !configVersion || parseInt(configVersion, 10) < 3; - - if (shouldMigrateExtensions) { - await migrateExtensionsToSettingsV3(); - } - - /* NOTE: - * If we've migrated and this is a version update, refreshedExtensions should be > 0 - * and we'll want to syncBundledExtensions to ensure any new extensions are added. - * Otherwise if the user has never opened goose - refreshedExtensions will be 0 - * and we want to fall into the case to initializeBundledExtensions. - */ - // Initialize or sync built-in extensions into config.yaml let refreshedExtensions = await options.getExtensions(false);