import React, { useState, useEffect } from 'react'; import { ScrollArea } from '../ui/scroll-area'; import { toast } from 'react-toastify'; import { Settings as SettingsType } from './types'; import { FullExtensionConfig, addExtension, removeExtension, BUILT_IN_EXTENSIONS, } from '../../extensions'; import { ConfigureExtensionModal } from './extensions/ConfigureExtensionModal'; import { ManualExtensionModal } from './extensions/ManualExtensionModal'; import { ConfigureBuiltInExtensionModal } from './extensions/ConfigureBuiltInExtensionModal'; import BackButton from '../ui/BackButton'; import { RecentModelsRadio } from './models/RecentModels'; import { ExtensionItem } from './extensions/ExtensionItem'; import type { View } from '../../App'; import ModeSelection from './basic/ModeSelection'; import { getApiUrl, getSecretKey } from '../../config'; const EXTENSIONS_DESCRIPTION = 'The Model Context Protocol (MCP) is a system that allows AI models to securely connect with local or remote resources using standard server setups. It works like a client-server setup and expands AI capabilities using three main components: Prompts, Resources, and Tools.'; const EXTENSIONS_SITE_LINK = 'https://block.github.io/goose/v1/extensions/'; const DEFAULT_SETTINGS: SettingsType = { models: [ { id: 'gpt4', name: 'GPT 4.0', description: 'Standard config', enabled: false, }, { id: 'gpt4lite', name: 'GPT 4.0 lite', description: 'Standard config', enabled: false, }, { id: 'claude', name: 'Claude', description: 'Standard config', enabled: true, }, ], // @ts-expect-error "we actually do always have all the properties required for builtins, but tsc cannot tell for some reason" extensions: BUILT_IN_EXTENSIONS, }; export type SettingsViewOptions = { extensionId: string; showEnvVars: boolean; }; export default function SettingsView({ onClose, setView, viewOptions, }: { onClose: () => void; setView: (view: View) => void; viewOptions: SettingsViewOptions; }) { const [mode, setMode] = useState('approve'); const handleModeChange = async (newMode: string) => { const storeResponse = await fetch(getApiUrl('/configs/store'), { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Secret-Key': getSecretKey(), }, body: JSON.stringify({ key: 'GOOSE_MODE', value: newMode, isSecret: false, }), }); if (!storeResponse.ok) { const errorText = await storeResponse.text(); console.error('Store response error:', errorText); throw new Error(`Failed to store new goose mode: ${newMode}`); } setMode(newMode); }; useEffect(() => { const fetchCurrentMode = async () => { try { const response = await fetch(getApiUrl('/configs/get?key=GOOSE_MODE'), { method: 'GET', headers: { 'Content-Type': 'application/json', 'X-Secret-Key': getSecretKey(), }, }); if (response.ok) { const { value } = await response.json(); setMode(value); } } catch (error) { console.error('Error fetching current mode:', error); } }; fetchCurrentMode(); }, []); const [settings, setSettings] = React.useState(() => { const saved = localStorage.getItem('user_settings'); window.electron.logInfo('Settings: ' + saved); let currentSettings = saved ? JSON.parse(saved) : DEFAULT_SETTINGS; // Ensure built-in extensions are included if not already present BUILT_IN_EXTENSIONS.forEach((builtIn) => { const exists = currentSettings.extensions.some( (ext: FullExtensionConfig) => ext.id === builtIn.id ); if (!exists) { currentSettings.extensions.push(builtIn); } }); return currentSettings; }); const [extensionBeingConfigured, setExtensionBeingConfigured] = useState(null); const [isManualModalOpen, setIsManualModalOpen] = useState(false); // Persist settings changes useEffect(() => { localStorage.setItem('user_settings', JSON.stringify(settings)); }, [settings]); // Listen for settings updates from extension storage useEffect(() => { const handleSettingsUpdate = (_: any) => { const saved = localStorage.getItem('user_settings'); if (saved) { let currentSettings = JSON.parse(saved); setSettings(currentSettings); } }; window.electron.on('settings-updated', handleSettingsUpdate); return () => { window.electron.off('settings-updated', handleSettingsUpdate); }; }, []); // Handle URL parameters for auto-opening extension configuration useEffect(() => { const extensionId = viewOptions.extensionId; const showEnvVars = viewOptions.showEnvVars; if (extensionId && showEnvVars === true) { // Find the extension in settings const extension = settings.extensions.find((ext) => ext.id === extensionId); if (extension) { // Auto-open the configuration modal setExtensionBeingConfigured(extension); // Scroll to extensions section const element = document.getElementById('extensions'); if (element) { element.scrollIntoView({ behavior: 'smooth' }); } } } // We only run this once on load // eslint-disable-next-line react-hooks/exhaustive-deps }, [settings.extensions]); const handleExtensionToggle = async (extensionId: string) => { // Find the extension to get its current state const extension = settings.extensions.find((ext) => ext.id === extensionId); if (!extension) return; const newEnabled = !extension.enabled; const originalSettings = settings; // Optimistically update local component state setSettings((prev) => ({ ...prev, extensions: prev.extensions.map((ext) => ext.id === extensionId ? { ...ext, enabled: newEnabled } : ext ), })); let response: Response; if (newEnabled) { response = await addExtension(extension); } else { response = await removeExtension(extension.name); } if (!response.ok) { setSettings(originalSettings); } }; const handleExtensionRemove = async () => { if (!extensionBeingConfigured) return; const response = await removeExtension(extensionBeingConfigured.name, true); if (response.ok) { toast.success(`Successfully removed ${extensionBeingConfigured.name} extension`); // Remove from localstorage setSettings((prev) => ({ ...prev, extensions: prev.extensions.filter((ext) => ext.id !== extensionBeingConfigured.id), })); setExtensionBeingConfigured(null); } }; const handleExtensionConfigSubmit = () => { setExtensionBeingConfigured(null); }; const isBuiltIn = (extensionId: string) => { return BUILT_IN_EXTENSIONS.some((builtIn) => builtIn.id === extensionId); }; return (
onClose()} />

Settings

{/* Content Area */}

Models

Extensions

Browse

{EXTENSIONS_DESCRIPTION}

{settings.extensions.length === 0 ? (

No Extensions Added

) : (
{settings.extensions.map((ext) => ( setExtensionBeingConfigured(extension)} /> ))}
)}

Others

Others setting like Goose Mode, Tool Output, Experiment and more

{extensionBeingConfigured && isBuiltIn(extensionBeingConfigured.id) ? ( { setExtensionBeingConfigured(null); }} extension={extensionBeingConfigured} onSubmit={handleExtensionConfigSubmit} /> ) : ( { setExtensionBeingConfigured(null); }} extension={extensionBeingConfigured} onSubmit={handleExtensionConfigSubmit} onRemove={handleExtensionRemove} /> )} setIsManualModalOpen(false)} onSubmit={async (extension) => { const response = await addExtension(extension); if (response.ok) { setSettings((prev) => ({ ...prev, extensions: [...prev.extensions, extension], })); setIsManualModalOpen(false); } else { // TODO - Anything for the UI state beyond validation? } }} />
); }