Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion ui/desktop/src/components/settings/SettingsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -126,7 +127,7 @@ export default function SettingsView({
className="mt-0 focus-visible:outline-none focus-visible:ring-0"
>
<div className="space-y-8">
<ConfigSettings />
{CONFIGURATION_ENABLED && <ConfigSettings />}
<AppSettingsSection scrollToSection={viewOptions.section} />
</div>
</TabsContent>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -475,7 +475,7 @@ export default function AppSettingsSection({ scrollToSection }: AppSettingsSecti
className="h-8 w-auto"
/>
<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>
</div>
</CardContent>
Expand Down
240 changes: 159 additions & 81 deletions ui/desktop/src/components/settings/config/ConfigSettings.tsx
Original file line number Diff line number Diff line change
@@ -1,30 +1,67 @@
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<ConfigData>({});
const [modified, setModified] = useState(false);
const [modifiedKeys, setModifiedKeys] = useState<Set<string>>(new Set());
const [saving, setSaving] = useState<string | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [originalKeyOrder, setOriginalKeyOrder] = useState<string[]>([]);

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) => {
setConfigValues((prev: ConfigData) => ({
...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) => {
Expand All @@ -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({
Expand All @@ -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 (
<section id="configEditor" className="px-8">
<div className="flex justify-between items-center mb-2">
<div className="flex items-center gap-2">
<Card className="rounded-lg">
<CardHeader className="pb-0">
<CardTitle className="flex items-center gap-2">
<FileText className="text-iconStandard" size={20} />
<h2 className="text-xl font-medium text-textStandard">Configuration</h2>
</div>
{modified && (
<Button onClick={handleReset} variant="ghost" className="text-sm">
<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
Configuration
</CardTitle>
<CardDescription>
Edit your goose configuration settings
{currentProvider && ` (current settings for ${currentProvider})`}
</p>

<div className="space-y-3">
{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">
<label className="text-sm font-medium text-textStandard" title={key}>
{getUiNames(key)}
</label>
<Input
value={String(configValues[key] || '')}
onChange={(e) => 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()}`}
/>
<Button
onClick={() => handleSave(key)}
disabled={configValues[key] === typedConfig[key] || saving === key}
variant="ghost"
size="sm"
className="min-w-[60px]"
>
{saving === key ? (
<span className="text-xs">Saving...</span>
) : (
<Save className="h-4 w-4" />
)}
</Button>
</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="flex-1 max-h-[60vh] overflow-auto pr-4">
<div className="space-y-4">
{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">
<label className="text-sm font-medium text-textStandard" title={key}>
{getUiNames(key)}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is supposed to change

UPPER_CASE_KEY to Upper Case Key, but in your screenshot it does not?

</label>
<Input
value={String(configValues[key] || '')}
onChange={(e) => 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)}`}
/>
<Button
onClick={() => handleSave(key)}
disabled={!modifiedKeys.has(key) || saving === key}
variant="ghost"
size="sm"
className="min-w-[60px]"
>
{saving === key ? (
<span className="text-xs">Saving...</span>
) : (
<Save className="h-4 w-4" />
)}
</Button>
</div>
))
)}
</div>
))
)}
</div>
</div>
</section>
</div>

<DialogFooter className="gap-2">
{modifiedKeys.size > 0 && (
<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 change: 1 addition & 0 deletions ui/desktop/src/updates.ts
Original file line number Diff line number Diff line change
@@ -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;
Loading
Loading