From c4c6b98d864cf17af4bd7bdbea0dc4450842771d Mon Sep 17 00:00:00 2001
From: Zane <75694352+zanesq@users.noreply.github.com>
Date: Tue, 5 Aug 2025 09:14:37 -0700
Subject: [PATCH] Changed app settings configuration form to match settings
panels (#3829)
---
.../src/components/settings/SettingsView.tsx | 3 +-
.../settings/app/AppSettingsSection.tsx | 2 +-
.../settings/config/ConfigSettings.tsx | 238 ++++++++++++------
ui/desktop/src/updates.ts | 1 +
ui/desktop/src/utils/configUtils.ts | 62 ++---
5 files changed, 193 insertions(+), 113 deletions(-)
diff --git a/ui/desktop/src/components/settings/SettingsView.tsx b/ui/desktop/src/components/settings/SettingsView.tsx
index b9e88a12..00400580 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 407caaad..a5ba96fd 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 554a7069..4085f444 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 handleModalClose = (open: boolean) => {
+ if (!open && modifiedKeys.size > 0) {
+ // Reset any unsaved changes when closing the modal
+ setConfigValues(typedConfig);
+ setModifiedKeys(new Set());
+ }
+ setIsModalOpen(open);
+ };
+
const currentProvider = typedConfig.GOOSE_PROVIDER || '';
- const currentProviderPrefixes = providerPrefixes[currentProvider] || [];
+ const configEntries: [string, ConfigValue][] = useMemo(() => {
+ const currentProviderPrefixes = providerPrefixes[currentProvider] || [];
+ const allProviderPrefixes = Object.values(providerPrefixes).flat();
- const allProviderPrefixes = Object.values(providerPrefixes).flat();
+ return originalKeyOrder
+ .filter((key) => {
+ // skip secrets
+ if (key === 'extensions' || key.includes('_KEY') || key.includes('_TOKEN')) {
+ return false;
+ }
- const providerSpecificEntries: [string, ConfigValue][] = [];
- const generalEntries: [string, ConfigValue][] = [];
+ // 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));
+ }
- 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 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})`}
-
+
+
+
+
+
+
);
}
diff --git a/ui/desktop/src/updates.ts b/ui/desktop/src/updates.ts
index 3ec4002a..978b7624 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 ba8c3863..cb10a93d 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 = {