diff --git a/ui/desktop/src/components/settings/SettingsView.tsx b/ui/desktop/src/components/settings/SettingsView.tsx index dad9ab65aa09..b9e88a12d840 100644 --- a/ui/desktop/src/components/settings/SettingsView.tsx +++ b/ui/desktop/src/components/settings/SettingsView.tsx @@ -4,6 +4,7 @@ import type { View, ViewOptions } from '../../App'; import ModelsSection from './models/ModelsSection'; import SessionSharingSection from './sessions/SessionSharingSection'; import AppSettingsSection from './app/AppSettingsSection'; +import ConfigSettings from './config/ConfigSettings'; import { ExtensionConfig } from '../../api'; import { MainPanelLayout } from '../Layout/MainPanelLayout'; import { Bot, Share2, Monitor, MessageSquare } from 'lucide-react'; @@ -124,7 +125,10 @@ export default function SettingsView({ value="app" className="mt-0 focus-visible:outline-none focus-visible:ring-0" > - +
+ + +
diff --git a/ui/desktop/src/components/settings/config/ConfigSettings.tsx b/ui/desktop/src/components/settings/config/ConfigSettings.tsx new file mode 100644 index 000000000000..554a7069df71 --- /dev/null +++ b/ui/desktop/src/components/settings/config/ConfigSettings.tsx @@ -0,0 +1,146 @@ +import { useState, useEffect } from 'react'; +import { Input } from '../../ui/input'; +import { Button } from '../../ui/button'; +import { useConfig } from '../../ConfigContext'; +import { cn } from '../../../utils'; +import { Save, RotateCcw, FileText } from 'lucide-react'; +import { toastSuccess, toastError } from '../../../toasts'; +import { getUiNames, providerPrefixes } from '../../../utils/configUtils'; +import type { ConfigData, ConfigValue } from '../../../types/config'; + +export default function ConfigSettings() { + const { config, upsert } = useConfig(); + const typedConfig = config as ConfigData; + const [configValues, setConfigValues] = useState({}); + const [modified, setModified] = useState(false); + const [saving, setSaving] = useState(null); + + useEffect(() => { + setConfigValues(typedConfig); + }, [typedConfig]); + + const handleChange = (key: string, value: string) => { + setConfigValues((prev: ConfigData) => ({ + ...prev, + [key]: value, + })); + setModified(true); + }; + + const handleSave = async (key: string) => { + setSaving(key); + try { + await upsert(key, configValues[key], false); + toastSuccess({ + title: 'Configuration Updated', + msg: `Successfully saved "${getUiNames(key)}"`, + }); + setModified(false); + } catch (error) { + console.error('Failed to save config:', error); + toastError({ + title: 'Save Failed', + msg: `Failed to save "${getUiNames(key)}"`, + traceback: error instanceof Error ? error.message : String(error), + }); + } finally { + setSaving(null); + } + }; + + const handleReset = () => { + setConfigValues(typedConfig); + setModified(false); + toastSuccess({ + title: 'Configuration Reset', + msg: 'All changes have been reverted', + }); + }; + + const currentProvider = typedConfig.GOOSE_PROVIDER || ''; + + const currentProviderPrefixes = providerPrefixes[currentProvider] || []; + + const allProviderPrefixes = Object.values(providerPrefixes).flat(); + + const providerSpecificEntries: [string, ConfigValue][] = []; + const generalEntries: [string, ConfigValue][] = []; + + Object.entries(configValues).forEach(([key, value]) => { + // skip secrets + if (key === 'extensions' || key.includes('_KEY') || key.includes('_TOKEN')) { + return; + } + + const providerSpecific = allProviderPrefixes.some((prefix: string) => key.startsWith(prefix)); + + if (providerSpecific) { + if (currentProviderPrefixes.some((prefix: string) => key.startsWith(prefix))) { + providerSpecificEntries.push([key, value]); + } + } else { + generalEntries.push([key, value]); + } + }); + + const configEntries = [...providerSpecificEntries, ...generalEntries]; + + return ( +
+
+
+ +

Configuration

+
+ {modified && ( + + )} +
+
+

+ Edit your goose config + {currentProvider && ` (current settings for ${currentProvider})`} +

+ +
+ {configEntries.length === 0 ? ( +

No configuration settings found.

+ ) : ( + configEntries.map(([key, _value]) => ( +
+ + handleChange(key, e.target.value)} + className={cn( + 'text-textStandard border-borderSubtle hover:border-borderStandard', + configValues[key] !== typedConfig[key] && 'border-blue-500' + )} + placeholder={`Enter ${getUiNames(key).toLowerCase()}`} + /> + +
+ )) + )} +
+
+
+ ); +} diff --git a/ui/desktop/src/components/settings/providers/ProviderSettingsPage.tsx b/ui/desktop/src/components/settings/providers/ProviderSettingsPage.tsx index 816cb4f7a62c..5b1f7a70dce8 100644 --- a/ui/desktop/src/components/settings/providers/ProviderSettingsPage.tsx +++ b/ui/desktop/src/components/settings/providers/ProviderSettingsPage.tsx @@ -108,7 +108,7 @@ export default function ProviderSettings({ onClose, isOnboarding }: ProviderSett {/* Only show back button if not in onboarding mode */} {!isOnboarding && }

{isOnboarding ? 'Configure your providers' : 'Provider Configuration Settings'} diff --git a/ui/desktop/src/types/config.ts b/ui/desktop/src/types/config.ts new file mode 100644 index 000000000000..78465c923e27 --- /dev/null +++ b/ui/desktop/src/types/config.ts @@ -0,0 +1,2 @@ +export type ConfigValue = string | number | null | undefined; +export type ConfigData = Record; diff --git a/ui/desktop/src/utils/configUtils.ts b/ui/desktop/src/utils/configUtils.ts new file mode 100644 index 000000000000..ba8c38639e8c --- /dev/null +++ b/ui/desktop/src/utils/configUtils.ts @@ -0,0 +1,77 @@ +export const configLabels: Record = { + // goose settings + GOOSE_PROVIDER: 'GOOSE_PROVIDER', + GOOSE_MODEL: 'GOOSE_MODEL', + GOOSE_TEMPERATURE: 'GOOSE_TEMPERATURE', + GOOSE_MODE: 'GOOSE_MODE', + GOOSE_LEAD_PROVIDER: 'GOOSE_LEAD_PROVIDER', + GOOSE_LEAD_MODEL: 'GOOSE_LEAD_MODEL', + GOOSE_PLANNER_PROVIDER: 'GOOSE_PLANNER_PROVIDER', + GOOSE_PLANNER_MODEL: 'GOOSE_PLANNER_MODEL', + GOOSE_TOOLSHIM: 'GOOSE_TOOLSHIM', + GOOSE_TOOLSHIM_OLLAMA_MODEL: 'GOOSE_TOOLSHIM_OLLAMA_MODEL', + GOOSE_CLI_MIN_PRIORITY: 'GOOSE_CLI_MIN_PRIORITY', + GOOSE_ALLOWLIST: 'GOOSE_ALLOWLIST', + GOOSE_RECIPE_GITHUB_REPO: 'GOOSE_RECIPE_GITHUB_REPO', + + // openai + OPENAI_API_KEY: 'OPENAI_API_KEY', + OPENAI_HOST: 'OPENAI_HOST', + OPENAI_BASE_PATH: 'OPENAI_BASE_PATH', + + // groq + GROQ_API_KEY: 'GROQ_API_KEY', + + // openrouter + OPENROUTER_API_KEY: 'OPENROUTER_API_KEY', + + // anthropic + ANTHROPIC_API_KEY: 'ANTHROPIC_API_KEY', + ANTHROPIC_HOST: 'ANTHROPIC_HOST', + + // google + GOOGLE_API_KEY: 'GOOGLE_API_KEY', + + // databricks + DATABRICKS_HOST: 'DATABRICKS_HOST', + + // ollama + OLLAMA_HOST: 'OLLAMA_HOST', + + // azure openai + AZURE_OPENAI_API_KEY: 'AZURE_OPENAI_API_KEY', + AZURE_OPENAI_ENDPOINT: 'AZURE_OPENAI_ENDPOINT', + AZURE_OPENAI_DEPLOYMENT_NAME: 'AZURE_OPENAI_DEPLOYMENT_NAME', + AZURE_OPENAI_API_VERSION: 'AZURE_OPENAI_API_VERSION', + + // gcp vertex + GCP_PROJECT_ID: 'GCP_PROJECT_ID', + GCP_LOCATION: 'GCP_LOCATION', + + // snowflake + SNOWFLAKE_HOST: 'SNOWFLAKE_HOST', + SNOWFLAKE_TOKEN: 'SNOWFLAKE_TOKEN', +}; + +export const providerPrefixes: Record = { + openai: ['OPENAI_'], + anthropic: ['ANTHROPIC_'], + google: ['GOOGLE_'], + groq: ['GROQ_'], + databricks: ['DATABRICKS_'], + openrouter: ['OPENROUTER_'], + ollama: ['OLLAMA_'], + azure_openai: ['AZURE_'], + gcp_vertex_ai: ['GCP_'], + snowflake: ['SNOWFLAKE_'], +}; + +export const getUiNames = (key: string): string => { + if (configLabels[key]) { + return configLabels[key]; + } + return key + .split('_') + .map((word) => word.charAt(0) + word.slice(1).toLowerCase()) + .join(' '); +};