Changed app settings configuration form to match settings panels (#3829)

This commit is contained in:
Zane
2025-08-05 09:14:37 -07:00
committed by GitHub
parent 861bd0a71d
commit c4c6b98d86
5 changed files with 193 additions and 113 deletions
@@ -10,6 +10,7 @@ import { MainPanelLayout } from '../Layout/MainPanelLayout';
import { Bot, Share2, Monitor, MessageSquare } from 'lucide-react'; import { Bot, Share2, Monitor, MessageSquare } from 'lucide-react';
import { useState, useEffect } from 'react'; import { useState, useEffect } from 'react';
import ChatSettingsSection from './chat/ChatSettingsSection'; import ChatSettingsSection from './chat/ChatSettingsSection';
import { CONFIGURATION_ENABLED } from '../../updates';
export type SettingsViewOptions = { export type SettingsViewOptions = {
deepLinkConfig?: ExtensionConfig; deepLinkConfig?: ExtensionConfig;
@@ -126,7 +127,7 @@ export default function SettingsView({
className="mt-0 focus-visible:outline-none focus-visible:ring-0" className="mt-0 focus-visible:outline-none focus-visible:ring-0"
> >
<div className="space-y-8"> <div className="space-y-8">
<ConfigSettings /> {CONFIGURATION_ENABLED && <ConfigSettings />}
<AppSettingsSection scrollToSection={viewOptions.section} /> <AppSettingsSection scrollToSection={viewOptions.section} />
</div> </div>
</TabsContent> </TabsContent>
@@ -475,7 +475,7 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti
className="h-8 w-auto" className="h-8 w-auto"
/> />
<span className="text-2xl font-mono text-black dark:text-white"> <span className="text-2xl font-mono text-black dark:text-white">
{String(window.appConfig.get('GOOSE_VERSION') || 'Block Internal v2.1.0')} {String(window.appConfig.get('GOOSE_VERSION') || 'Development')}
</span> </span>
</div> </div>
</CardContent> </CardContent>
@@ -1,22 +1,50 @@
import { useState, useEffect } from 'react'; import { useState, useEffect, useMemo } from 'react';
import { Input } from '../../ui/input'; import { Input } from '../../ui/input';
import { Button } from '../../ui/button'; import { Button } from '../../ui/button';
import { useConfig } from '../../ConfigContext'; import { useConfig } from '../../ConfigContext';
import { cn } from '../../../utils'; 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 { toastSuccess, toastError } from '../../../toasts';
import { getUiNames, providerPrefixes } from '../../../utils/configUtils'; import { getUiNames, providerPrefixes } from '../../../utils/configUtils';
import type { ConfigData, ConfigValue } from '../../../types/config'; 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() { export default function ConfigSettings() {
const { config, upsert } = useConfig(); const { config, upsert } = useConfig();
const typedConfig = config as ConfigData; const typedConfig = config as ConfigData;
const [configValues, setConfigValues] = useState<ConfigData>({}); const [configValues, setConfigValues] = useState<ConfigData>({});
const [modified, setModified] = useState(false); const [modifiedKeys, setModifiedKeys] = useState<Set<string>>(new Set());
const [saving, setSaving] = useState<string | null>(null); const [saving, setSaving] = useState<string | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [originalKeyOrder, setOriginalKeyOrder] = useState<string[]>([]);
useEffect(() => { useEffect(() => {
setConfigValues(typedConfig); 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]); }, [typedConfig]);
const handleChange = (key: string, value: string) => { const handleChange = (key: string, value: string) => {
@@ -24,7 +52,16 @@ export default function ConfigSettings() {
...prev, ...prev,
[key]: value, [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) => { const handleSave = async (key: string) => {
@@ -35,7 +72,13 @@ export default function ConfigSettings() {
title: 'Configuration Updated', title: 'Configuration Updated',
msg: `Successfully saved "${getUiNames(key)}"`, 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) { } catch (error) {
console.error('Failed to save config:', error); console.error('Failed to save config:', error);
toastError({ toastError({
@@ -50,97 +93,132 @@ export default function ConfigSettings() {
const handleReset = () => { const handleReset = () => {
setConfigValues(typedConfig); setConfigValues(typedConfig);
setModified(false); setModifiedKeys(new Set());
toastSuccess({ toastSuccess({
title: 'Configuration Reset', title: 'Configuration Reset',
msg: 'All changes have been reverted', 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 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][] = []; // Only show provider-specific entries for the current provider
const generalEntries: [string, ConfigValue][] = []; 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]) => { return true;
// skip secrets })
if (key === 'extensions' || key.includes('_KEY') || key.includes('_TOKEN')) { .map((key) => [key, configValues[key]]);
return; }, [originalKeyOrder, configValues, currentProvider]);
}
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 ( return (
<section id="configEditor" className="px-8"> <Card className="rounded-lg">
<div className="flex justify-between items-center mb-2"> <CardHeader className="pb-0">
<div className="flex items-center gap-2"> <CardTitle className="flex items-center gap-2">
<FileText className="text-iconStandard" size={20} /> <FileText className="text-iconStandard" size={20} />
<h2 className="text-xl font-medium text-textStandard">Configuration</h2> Configuration
</div> </CardTitle>
{modified && ( <CardDescription>
<Button onClick={handleReset} variant="ghost" className="text-sm"> Edit your goose configuration settings
<RotateCcw className="h-4 w-4 mr-2" />
Reset
</Button>
)}
</div>
<div className="pb-8">
<p className="text-sm text-textSubtle mb-6">
Edit your goose config
{currentProvider && ` (current settings for ${currentProvider})`} {currentProvider && ` (current settings for ${currentProvider})`}
</p> </CardDescription>
</CardHeader>
<CardContent className="pt-4 px-4">
<Dialog open={isModalOpen} onOpenChange={handleModalClose}>
<DialogTrigger asChild>
<Button className="flex items-center gap-2" variant="secondary" size="sm">
<Settings className="h-4 w-4" />
Edit Configuration
</Button>
</DialogTrigger>
<DialogContent className="max-w-4xl max-h-[80vh]">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<FileText className="text-iconStandard" size={20} />
Configuration Editor
</DialogTitle>
<DialogDescription>
Edit your goose configuration settings
{currentProvider && ` (current settings for ${currentProvider})`}
</DialogDescription>
</DialogHeader>
<div className="space-y-3"> <div className="flex-1 max-h-[60vh] overflow-auto pr-4">
{configEntries.length === 0 ? ( <div className="space-y-4">
<p className="text-textSubtle">No configuration settings found.</p> {configEntries.length === 0 ? (
) : ( <p className="text-textSubtle">No configuration settings found.</p>
configEntries.map(([key, _value]) => ( ) : (
<div key={key} className="grid grid-cols-[200px_1fr_auto] gap-3 items-center"> configEntries.map(([key, _value]) => (
<label className="text-sm font-medium text-textStandard" title={key}> <div key={key} className="grid grid-cols-[200px_1fr_auto] gap-3 items-center">
{getUiNames(key)} <label className="text-sm font-medium text-textStandard" title={key}>
</label> {getUiNames(key)}
<Input </label>
value={String(configValues[key] || '')} <Input
onChange={(e) => handleChange(key, e.target.value)} value={String(configValues[key] || '')}
className={cn( onChange={(e) => handleChange(key, e.target.value)}
'text-textStandard border-borderSubtle hover:border-borderStandard', className={cn(
configValues[key] !== typedConfig[key] && 'border-blue-500' 'text-textStandard border-borderSubtle hover:border-borderStandard transition-colors',
)} modifiedKeys.has(key) && 'border-blue-500 focus:ring-blue-500/20'
placeholder={`Enter ${getUiNames(key).toLowerCase()}`} )}
/> placeholder={`Enter ${getUiNames(key)}`}
<Button />
onClick={() => handleSave(key)} <Button
disabled={configValues[key] === typedConfig[key] || saving === key} onClick={() => handleSave(key)}
variant="ghost" disabled={!modifiedKeys.has(key) || saving === key}
size="sm" variant="ghost"
className="min-w-[60px]" size="sm"
> className="min-w-[60px]"
{saving === key ? ( >
<span className="text-xs">Saving...</span> {saving === key ? (
) : ( <span className="text-xs">Saving...</span>
<Save className="h-4 w-4" /> ) : (
)} <Save className="h-4 w-4" />
</Button> )}
</Button>
</div>
))
)}
</div> </div>
)) </div>
)}
</div> <DialogFooter className="gap-2">
</div> {modifiedKeys.size > 0 && (
</section> <Button onClick={handleReset} variant="outline">
<RotateCcw className="h-4 w-4 mr-2" />
Reset Changes
</Button>
)}
<Button onClick={() => setIsModalOpen(false)} variant="default">
Done
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</CardContent>
</Card>
); );
} }
+1
View File
@@ -1,3 +1,4 @@
export const UPDATES_ENABLED = true; export const UPDATES_ENABLED = true;
export const COST_TRACKING_ENABLED = true; export const COST_TRACKING_ENABLED = true;
export const ANNOUNCEMENTS_ENABLED = false; export const ANNOUNCEMENTS_ENABLED = false;
export const CONFIGURATION_ENABLED = true;
+31 -31
View File
@@ -1,56 +1,56 @@
export const configLabels: Record<string, string> = { export const configLabels: Record<string, string> = {
// goose settings // goose settings
GOOSE_PROVIDER: 'GOOSE_PROVIDER', GOOSE_PROVIDER: 'Provider',
GOOSE_MODEL: 'GOOSE_MODEL', GOOSE_MODEL: 'Model',
GOOSE_TEMPERATURE: 'GOOSE_TEMPERATURE', GOOSE_TEMPERATURE: 'Temperature',
GOOSE_MODE: 'GOOSE_MODE', GOOSE_MODE: 'Mode',
GOOSE_LEAD_PROVIDER: 'GOOSE_LEAD_PROVIDER', GOOSE_LEAD_PROVIDER: 'Lead Provider',
GOOSE_LEAD_MODEL: 'GOOSE_LEAD_MODEL', GOOSE_LEAD_MODEL: 'Lead Model',
GOOSE_PLANNER_PROVIDER: 'GOOSE_PLANNER_PROVIDER', GOOSE_PLANNER_PROVIDER: 'Planner Provider',
GOOSE_PLANNER_MODEL: 'GOOSE_PLANNER_MODEL', GOOSE_PLANNER_MODEL: 'Planner Model',
GOOSE_TOOLSHIM: 'GOOSE_TOOLSHIM', GOOSE_TOOLSHIM: 'Tool Shim',
GOOSE_TOOLSHIM_OLLAMA_MODEL: 'GOOSE_TOOLSHIM_OLLAMA_MODEL', GOOSE_TOOLSHIM_OLLAMA_MODEL: 'Tool Shim Ollama Model',
GOOSE_CLI_MIN_PRIORITY: 'GOOSE_CLI_MIN_PRIORITY', GOOSE_CLI_MIN_PRIORITY: 'CLI Min Priority',
GOOSE_ALLOWLIST: 'GOOSE_ALLOWLIST', GOOSE_ALLOWLIST: 'Allow List',
GOOSE_RECIPE_GITHUB_REPO: 'GOOSE_RECIPE_GITHUB_REPO', GOOSE_RECIPE_GITHUB_REPO: 'Recipe GitHub Repo',
// openai // openai
OPENAI_API_KEY: 'OPENAI_API_KEY', OPENAI_API_KEY: 'OpenAI API Key',
OPENAI_HOST: 'OPENAI_HOST', OPENAI_HOST: 'OpenAI Host',
OPENAI_BASE_PATH: 'OPENAI_BASE_PATH', OPENAI_BASE_PATH: 'OpenAI Base Path',
// groq // groq
GROQ_API_KEY: 'GROQ_API_KEY', GROQ_API_KEY: 'Groq API Key',
// openrouter // openrouter
OPENROUTER_API_KEY: 'OPENROUTER_API_KEY', OPENROUTER_API_KEY: 'OpenRouter API Key',
// anthropic // anthropic
ANTHROPIC_API_KEY: 'ANTHROPIC_API_KEY', ANTHROPIC_API_KEY: 'Anthropic API Key',
ANTHROPIC_HOST: 'ANTHROPIC_HOST', ANTHROPIC_HOST: 'Anthropic Host',
// google // google
GOOGLE_API_KEY: 'GOOGLE_API_KEY', GOOGLE_API_KEY: 'Google API Key',
// databricks // databricks
DATABRICKS_HOST: 'DATABRICKS_HOST', DATABRICKS_HOST: 'Databricks Host',
// ollama // ollama
OLLAMA_HOST: 'OLLAMA_HOST', OLLAMA_HOST: 'Ollama Host',
// azure openai // azure openai
AZURE_OPENAI_API_KEY: 'AZURE_OPENAI_API_KEY', AZURE_OPENAI_API_KEY: 'Azure OpenAI API Key',
AZURE_OPENAI_ENDPOINT: 'AZURE_OPENAI_ENDPOINT', AZURE_OPENAI_ENDPOINT: 'Azure OpenAI Endpoint',
AZURE_OPENAI_DEPLOYMENT_NAME: 'AZURE_OPENAI_DEPLOYMENT_NAME', AZURE_OPENAI_DEPLOYMENT_NAME: 'Azure OpenAI Deployment Name',
AZURE_OPENAI_API_VERSION: 'AZURE_OPENAI_API_VERSION', AZURE_OPENAI_API_VERSION: 'Azure OpenAI API Version',
// gcp vertex // gcp vertex
GCP_PROJECT_ID: 'GCP_PROJECT_ID', GCP_PROJECT_ID: 'GCP Project ID',
GCP_LOCATION: 'GCP_LOCATION', GCP_LOCATION: 'GCP Location',
// snowflake // snowflake
SNOWFLAKE_HOST: 'SNOWFLAKE_HOST', SNOWFLAKE_HOST: 'Snowflake Host',
SNOWFLAKE_TOKEN: 'SNOWFLAKE_TOKEN', SNOWFLAKE_TOKEN: 'Snowflake Token',
}; };
export const providerPrefixes: Record<string, string[]> = { export const providerPrefixes: Record<string, string[]> = {