diff --git a/ui/desktop/src/components/settings/SettingsView.tsx b/ui/desktop/src/components/settings/SettingsView.tsx index b9e88a12d840..004005809b7c 100644 --- a/ui/desktop/src/components/settings/SettingsView.tsx +++ b/ui/desktop/src/components/settings/SettingsView.tsx @@ -10,6 +10,7 @@ import { MainPanelLayout } from '../Layout/MainPanelLayout'; import { Bot, Share2, Monitor, MessageSquare } from 'lucide-react'; import { useState, useEffect } from 'react'; import ChatSettingsSection from './chat/ChatSettingsSection'; +import { CONFIGURATION_ENABLED } from '../../updates'; export type SettingsViewOptions = { deepLinkConfig?: ExtensionConfig; @@ -126,7 +127,7 @@ export default function SettingsView({ className="mt-0 focus-visible:outline-none focus-visible:ring-0" >
- + {CONFIGURATION_ENABLED && }
diff --git a/ui/desktop/src/components/settings/app/AppSettingsSection.tsx b/ui/desktop/src/components/settings/app/AppSettingsSection.tsx index 407caaadacc0..a5ba96fd77ef 100644 --- a/ui/desktop/src/components/settings/app/AppSettingsSection.tsx +++ b/ui/desktop/src/components/settings/app/AppSettingsSection.tsx @@ -475,7 +475,7 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti className="h-8 w-auto" /> - {String(window.appConfig.get('GOOSE_VERSION') || 'Block Internal v2.1.0')} + {String(window.appConfig.get('GOOSE_VERSION') || 'Development')} diff --git a/ui/desktop/src/components/settings/config/ConfigSettings.tsx b/ui/desktop/src/components/settings/config/ConfigSettings.tsx index 554a7069df71..4085f44463d4 100644 --- a/ui/desktop/src/components/settings/config/ConfigSettings.tsx +++ b/ui/desktop/src/components/settings/config/ConfigSettings.tsx @@ -1,22 +1,50 @@ -import { useState, useEffect } from 'react'; +import { useState, useEffect, useMemo } 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 { Save, RotateCcw, FileText, Settings } from 'lucide-react'; import { toastSuccess, toastError } from '../../../toasts'; import { getUiNames, providerPrefixes } from '../../../utils/configUtils'; import type { ConfigData, ConfigValue } from '../../../types/config'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../../ui/card'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from '../../ui/dialog'; export default function ConfigSettings() { const { config, upsert } = useConfig(); const typedConfig = config as ConfigData; const [configValues, setConfigValues] = useState({}); - const [modified, setModified] = useState(false); + const [modifiedKeys, setModifiedKeys] = useState>(new Set()); const [saving, setSaving] = useState(null); + const [isModalOpen, setIsModalOpen] = useState(false); + const [originalKeyOrder, setOriginalKeyOrder] = useState([]); useEffect(() => { setConfigValues(typedConfig); + setModifiedKeys(new Set()); + + // Capture the original key order only on first load or when new keys are added + const currentKeys = Object.keys(typedConfig); + setOriginalKeyOrder((prevOrder) => { + if (prevOrder.length === 0) { + // First load - capture the initial order + return currentKeys; + } else if (currentKeys.length > prevOrder.length) { + // New keys have been added - add them to the end while preserving existing order + const newKeys = currentKeys.filter((key) => !prevOrder.includes(key)); + return [...prevOrder, ...newKeys]; + } + // Don't reorder when keys are just updated/saved - preserve the original order + return prevOrder; + }); }, [typedConfig]); const handleChange = (key: string, value: string) => { @@ -24,7 +52,16 @@ export default function ConfigSettings() { ...prev, [key]: value, })); - setModified(true); + + setModifiedKeys((prev) => { + const newSet = new Set(prev); + if (value !== String(typedConfig[key] || '')) { + newSet.add(key); + } else { + newSet.delete(key); + } + return newSet; + }); }; const handleSave = async (key: string) => { @@ -35,7 +72,13 @@ export default function ConfigSettings() { title: 'Configuration Updated', msg: `Successfully saved "${getUiNames(key)}"`, }); - setModified(false); + + // Remove this key from modified keys since it's now saved + setModifiedKeys((prev) => { + const newSet = new Set(prev); + newSet.delete(key); + return newSet; + }); } catch (error) { console.error('Failed to save config:', error); toastError({ @@ -50,97 +93,132 @@ export default function ConfigSettings() { const handleReset = () => { setConfigValues(typedConfig); - setModified(false); + setModifiedKeys(new Set()); 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 handleModalClose = (open: boolean) => { + if (!open && modifiedKeys.size > 0) { + // Reset any unsaved changes when closing the modal + setConfigValues(typedConfig); + setModifiedKeys(new Set()); + } + setIsModalOpen(open); + }; - const providerSpecificEntries: [string, ConfigValue][] = []; - const generalEntries: [string, ConfigValue][] = []; + const currentProvider = typedConfig.GOOSE_PROVIDER || ''; - Object.entries(configValues).forEach(([key, value]) => { - // skip secrets - if (key === 'extensions' || key.includes('_KEY') || key.includes('_TOKEN')) { - return; - } + const configEntries: [string, ConfigValue][] = useMemo(() => { + const currentProviderPrefixes = providerPrefixes[currentProvider] || []; + const allProviderPrefixes = Object.values(providerPrefixes).flat(); - const providerSpecific = allProviderPrefixes.some((prefix: string) => key.startsWith(prefix)); + return originalKeyOrder + .filter((key) => { + // skip secrets + if (key === 'extensions' || key.includes('_KEY') || key.includes('_TOKEN')) { + return false; + } - if (providerSpecific) { - if (currentProviderPrefixes.some((prefix: string) => key.startsWith(prefix))) { - providerSpecificEntries.push([key, value]); - } - } else { - generalEntries.push([key, value]); - } - }); + // Only show provider-specific entries for the current provider + const providerSpecific = allProviderPrefixes.some((prefix: string) => + key.startsWith(prefix) + ); + if (providerSpecific) { + return currentProviderPrefixes.some((prefix: string) => key.startsWith(prefix)); + } - const configEntries = [...providerSpecificEntries, ...generalEntries]; + return true; + }) + .map((key) => [key, configValues[key]]); + }, [originalKeyOrder, configValues, currentProvider]); return ( -
-
-
+ + + -

Configuration

-
- {modified && ( - - )} -
-
-

- Edit your goose config + Configuration + + + Edit your goose configuration settings {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()}`} - /> - + + + + + + + + + + + + Configuration Editor + + + Edit your goose configuration settings + {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 transition-colors', + modifiedKeys.has(key) && 'border-blue-500 focus:ring-blue-500/20' + )} + placeholder={`Enter ${getUiNames(key)}`} + /> + +
+ )) + )}
- )) - )} -
-
-
+ + + + {modifiedKeys.size > 0 && ( + + )} + + + + + + ); } diff --git a/ui/desktop/src/updates.ts b/ui/desktop/src/updates.ts index 3ec4002a691b..978b7624298d 100644 --- a/ui/desktop/src/updates.ts +++ b/ui/desktop/src/updates.ts @@ -1,3 +1,4 @@ export const UPDATES_ENABLED = true; export const COST_TRACKING_ENABLED = true; export const ANNOUNCEMENTS_ENABLED = false; +export const CONFIGURATION_ENABLED = true; diff --git a/ui/desktop/src/utils/configUtils.ts b/ui/desktop/src/utils/configUtils.ts index ba8c38639e8c..cb10a93dd41e 100644 --- a/ui/desktop/src/utils/configUtils.ts +++ b/ui/desktop/src/utils/configUtils.ts @@ -1,56 +1,56 @@ 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', + GOOSE_PROVIDER: 'Provider', + GOOSE_MODEL: 'Model', + GOOSE_TEMPERATURE: 'Temperature', + GOOSE_MODE: 'Mode', + GOOSE_LEAD_PROVIDER: 'Lead Provider', + GOOSE_LEAD_MODEL: 'Lead Model', + GOOSE_PLANNER_PROVIDER: 'Planner Provider', + GOOSE_PLANNER_MODEL: 'Planner Model', + GOOSE_TOOLSHIM: 'Tool Shim', + GOOSE_TOOLSHIM_OLLAMA_MODEL: 'Tool Shim Ollama Model', + GOOSE_CLI_MIN_PRIORITY: 'CLI Min Priority', + GOOSE_ALLOWLIST: 'Allow List', + GOOSE_RECIPE_GITHUB_REPO: 'Recipe GitHub Repo', // openai - OPENAI_API_KEY: 'OPENAI_API_KEY', - OPENAI_HOST: 'OPENAI_HOST', - OPENAI_BASE_PATH: 'OPENAI_BASE_PATH', + OPENAI_API_KEY: 'OpenAI API Key', + OPENAI_HOST: 'OpenAI Host', + OPENAI_BASE_PATH: 'OpenAI Base Path', // groq - GROQ_API_KEY: 'GROQ_API_KEY', + GROQ_API_KEY: 'Groq API Key', // openrouter - OPENROUTER_API_KEY: 'OPENROUTER_API_KEY', + OPENROUTER_API_KEY: 'OpenRouter API Key', // anthropic - ANTHROPIC_API_KEY: 'ANTHROPIC_API_KEY', - ANTHROPIC_HOST: 'ANTHROPIC_HOST', + ANTHROPIC_API_KEY: 'Anthropic API Key', + ANTHROPIC_HOST: 'Anthropic Host', // google - GOOGLE_API_KEY: 'GOOGLE_API_KEY', + GOOGLE_API_KEY: 'Google API Key', // databricks - DATABRICKS_HOST: 'DATABRICKS_HOST', + DATABRICKS_HOST: 'Databricks Host', // ollama - OLLAMA_HOST: 'OLLAMA_HOST', + 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', + 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', + GCP_PROJECT_ID: 'GCP Project ID', + GCP_LOCATION: 'GCP Location', // snowflake - SNOWFLAKE_HOST: 'SNOWFLAKE_HOST', - SNOWFLAKE_TOKEN: 'SNOWFLAKE_TOKEN', + SNOWFLAKE_HOST: 'Snowflake Host', + SNOWFLAKE_TOKEN: 'Snowflake Token', }; export const providerPrefixes: Record = {