mirror of
https://github.com/block/goose.git
synced 2026-07-17 12:56:20 +02:00
feat: allow users view and edit their non-secret config's (#3005)
This commit is contained in:
@@ -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"
|
||||
>
|
||||
<AppSettingsSection scrollToSection={viewOptions.section} />
|
||||
<div className="space-y-8">
|
||||
<ConfigSettings />
|
||||
<AppSettingsSection scrollToSection={viewOptions.section} />
|
||||
</div>
|
||||
</TabsContent>
|
||||
</ScrollArea>
|
||||
</Tabs>
|
||||
|
||||
@@ -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<ConfigData>({});
|
||||
const [modified, setModified] = useState(false);
|
||||
const [saving, setSaving] = useState<string | null>(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 (
|
||||
<section id="configEditor" className="px-8">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<div 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
|
||||
{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>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -108,7 +108,7 @@ export default function ProviderSettings({ onClose, isOnboarding }: ProviderSett
|
||||
{/* Only show back button if not in onboarding mode */}
|
||||
{!isOnboarding && <BackButton className={'mt-[42px]'} onClick={onClose} />}
|
||||
<h1
|
||||
className="text-3xl font-medium text-textStandard mt-1"
|
||||
className="text-3xl font-medium text-textStandard mt-4"
|
||||
data-testid="provider-selection-heading"
|
||||
>
|
||||
{isOnboarding ? 'Configure your providers' : 'Provider Configuration Settings'}
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export type ConfigValue = string | number | null | undefined;
|
||||
export type ConfigData = Record<string, ConfigValue>;
|
||||
@@ -0,0 +1,77 @@
|
||||
export const configLabels: Record<string, string> = {
|
||||
// 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<string, string[]> = {
|
||||
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(' ');
|
||||
};
|
||||
Reference in New Issue
Block a user