Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
6 changes: 3 additions & 3 deletions crates/goose-server/src/routes/config_management.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ pub struct UpsertConfigQuery {
pub is_secret: bool,
}

#[derive(Deserialize, ToSchema)]
#[derive(Deserialize, Serialize, ToSchema)]
pub struct ConfigKeyQuery {
pub key: String,
pub is_secret: bool,
Expand Down Expand Up @@ -123,9 +123,9 @@ pub async fn remove_config(
}

#[utoipa::path(
get,
post, // Change from get to post
path = "/config/read",
request_body = ConfigKeyQuery,
request_body = ConfigKeyQuery, // Switch back to request_body
responses(
(status = 200, description = "Configuration value retrieved successfully", body = Value),
(status = 404, description = "Configuration key not found")
Expand Down
2 changes: 1 addition & 1 deletion ui/desktop/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@
}
},
"/config/read": {
"get": {
"post": {
"tags": [
"super::routes::config_management"
],
Expand Down
2 changes: 1 addition & 1 deletion ui/desktop/src/api/sdk.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export const providers = <ThrowOnError extends boolean = false>(options?: Option
};

export const readConfig = <ThrowOnError extends boolean = false>(options: Options<ReadConfigData, ThrowOnError>) => {
return (options.client ?? _heyApiClient).get<unknown, unknown, ThrowOnError>({
return (options.client ?? _heyApiClient).post<unknown, unknown, ThrowOnError>({
url: '/config/read',
...options,
headers: {
Expand Down
10 changes: 5 additions & 5 deletions ui/desktop/src/components/ConfigContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type {
ExtensionQuery,
ProviderDetails,
} from '../api/types.gen';
import { getSecretKey } from '../config';

// Initialize client configuration
client.setConfig({
Expand Down Expand Up @@ -71,13 +72,12 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
setConfig(response.data.config || {});
};

const upsert = async (key: string, value: unknown, isSecret?: boolean) => {
const upsert = async (key: string, value: unknown, isSecret: boolean = false) => {
const query: UpsertConfigQuery = {
key,
value,
is_secret: isSecret || null,
key: key,
value: value,
is_secret: isSecret,
};

await upsertConfig({
body: query,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,51 +6,37 @@ import ProviderSetupActions from './subcomponents/ProviderSetupActions';
import ProviderLogo from './subcomponents/ProviderLogo';
import { useProviderModal } from './ProviderModalProvider';
import { SecureStorageNotice } from './subcomponents/SecureStorageNotice';
import DefaultSubmitHandler from './subcomponents/handlers/DefaultSubmitHandler';
import { DefaultSubmitHandler } from './subcomponents/handlers/DefaultSubmitHandler';
import OllamaSubmitHandler from './subcomponents/handlers/OllamaSubmitHandler';
import OllamaForm from './subcomponents/forms/OllamaForm';
import { useConfig } from '../../../ConfigContext';

const customSubmitHandler = {
const customSubmitHandlerMap = {
provider_name: OllamaSubmitHandler, // example
};

const customForms = {
const customFormsMap = {
provider_name: OllamaForm, // example
};

export default function ProviderConfigurationModal() {
const { upsert } = useConfig();
const { isOpen, currentProvider, modalProps, closeModal } = useProviderModal();
const [configValues, setConfigValues] = useState({});

useEffect(() => {
if (currentProvider) {
// Initialize form with default values
const initialValues = {};
// FIXME
// if (currentProvider.parameters) {
// currentProvider.parameters.forEach((param) => {
// initialValues[param.name] = param.default || '';
// });
// }
setConfigValues(initialValues);
} else {
setConfigValues({});
}
}, [currentProvider]);

if (!isOpen || !currentProvider) return null;

const headerText = `Configure ${currentProvider.metadata.display_name}`;
const descriptionText = `Add your API key(s) for this provider to integrate into Goose`;

const SubmitHandler = customSubmitHandler[currentProvider.name] || DefaultSubmitHandler;
const FormComponent = customForms[currentProvider.name] || DefaultProviderSetupForm;
const SubmitHandler = customSubmitHandlerMap[currentProvider.name] || DefaultSubmitHandler;
const FormComponent = customFormsMap[currentProvider.name] || DefaultProviderSetupForm;

const handleSubmitForm = (e) => {
e.preventDefault();
console.log('Form submitted for:', currentProvider.name);

SubmitHandler(configValues);
SubmitHandler(upsert, currentProvider, configValues);

// Close the modal unless the custom handler explicitly returns false
// This gives custom handlers the ability to keep the modal open if needed
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React, { useEffect, useMemo } from 'react';
import React, { useEffect, useMemo, useState } from 'react';
import { Input } from '../../../../../ui/input';
import { useConfig } from '../../../../../ConfigContext'; // Adjust this import path as needed

interface DefaultProviderSetupFormProps {
configValues: Record<string, any>;
Expand All @@ -13,29 +14,59 @@ export default function DefaultProviderSetupForm({
provider,
}: DefaultProviderSetupFormProps) {
const parameters = provider.metadata.config_keys || [];
const [isLoading, setIsLoading] = useState(true);
const { read } = useConfig();

// Initialize default values when the component mounts or provider changes
// Initialize values when the component mounts or provider changes
useEffect(() => {
const defaultValues = {};
parameters.forEach((parameter) => {
if (
parameter.required &&
parameter.default !== undefined &&
parameter.default !== null &&
!configValues[parameter.name]
) {
defaultValues[parameter.name] = parameter.default;
const loadConfigValues = async () => {
setIsLoading(true);
const newValues = { ...configValues };

// Try to load actual values from config for each parameter that is not secret
for (const parameter of parameters) {
if (parameter.required && !parameter.secret) {
try {
// Check if there's a stored value in the config system
const configKey = `${parameter.name}`;
const configResponse = await read(configKey, parameter.secret || false);
console.log('configResponse', configResponse);

if (configResponse) {
// Use the value from the config provider
newValues[parameter.name] = configResponse;
} else if (
parameter.default !== undefined &&
parameter.default !== null &&
!configValues[parameter.name]
) {
// Fall back to default value if no config value exists
newValues[parameter.name] = parameter.default;
}
} catch (error) {
console.error(`Failed to load config for ${parameter.name}:`, error);
// Fall back to default if read operation fails
if (
parameter.default !== undefined &&
parameter.default !== null &&
!configValues[parameter.name]
) {
newValues[parameter.name] = parameter.default;
}
}
}
}
});

// Only update if there are default values to add
if (Object.keys(defaultValues).length > 0) {
// Update state with loaded values
setConfigValues((prev) => ({
...prev,
...defaultValues,
...newValues,
}));
}
}, [provider.name, parameters, setConfigValues, configValues]);
setIsLoading(false);
};

loadConfigValues();
}, [provider.name, parameters, setConfigValues, read]);

// Filter parameters to only show required ones
const requiredParameters = useMemo(() => {
Expand All @@ -53,6 +84,10 @@ export default function DefaultProviderSetupForm({
return parameter.name.toUpperCase();
};

if (isLoading) {
return <div className="text-center py-4">Loading configuration values...</div>;
}

return (
<div className="mt-4 space-y-4">
{requiredParameters.length === 0 ? (
Expand All @@ -62,10 +97,7 @@ export default function DefaultProviderSetupForm({
) : (
requiredParameters.map((parameter) => (
<div key={parameter.name}>
<label className="block text-sm font-medium text-gray-700 mb-1">
{parameter.name}
<span className="text-red-500 ml-1">*</span>
</label>
<label className="block text-sm font-medium text-gray-700 mb-1">{parameter.name}</label>
<Input
type={parameter.secret ? 'password' : 'text'}
value={configValues[parameter.name] || ''}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,114 @@
export default function DefaultSubmitHandler(configValues) {
// Log each field value individually for clarity
console.log('Field values:');
Object.entries(configValues).forEach(([key, value]) => {
console.log(`${key}: ${value}`);
import { useConfig } from '../../../../../ConfigContext';
import React from 'react';

/**
* Custom hook for provider configuration submission
* Returns a submit handler function and submission state
*/
export const useDefaultSubmit = () => {
const { upsert } = useConfig();
const [isSubmitting, setIsSubmitting] = React.useState(false);
const [error, setError] = React.useState(null);
const [isSuccess, setIsSuccess] = React.useState(false);

/**
* Submit handler for provider configuration
* @param {Object} provider - The provider object with metadata
* @param {Object} configValues - The form values to be submitted
* @param {Function} onSuccess - Optional callback for successful submission
*/
const handleSubmit = async (provider, configValues, onSuccess) => {
setIsSubmitting(true);
setError(null);
setIsSuccess(false);

try {
const parameters = provider.metadata.config_keys || [];

// Create an array of promises for all the upsert operations
const upsertPromises = parameters.map((parameter) => {
// Skip parameters that don't have a value and aren't required
if (!configValues[parameter.name] && !parameter.required) {
return Promise.resolve();
}

// For required parameters with no value, use the default if available
const value =
configValues[parameter.name] !== undefined
? configValues[parameter.name]
: parameter.default;

// Skip if there's still no value
if (value === undefined || value === null) {
return Promise.resolve();
}

// Create the provider-specific config key
// Format: provider.{provider_name}.{parameter_name}
const configKey = `provider.${provider.name}.${parameter.name}`;

// Pass the is_secret flag from the parameter definition
return upsert(configKey, value, parameter.secret || false);
});

// Wait for all upsert operations to complete
await Promise.all(upsertPromises);

setIsSuccess(true);

// Call the success callback if provided
if (onSuccess) {
onSuccess();
}
} catch (err) {
console.error('Failed to save provider configuration:', err);
setError('Failed to save configuration. Please try again.');
} finally {
setIsSubmitting(false);
}
};

return {
handleSubmit,
isSubmitting,
error,
isSuccess,
};
};

/**
* Standalone function to submit provider configuration
* Useful for components that don't want to use the hook
*/
export const DefaultSubmitHandler = async (upsertFn, provider, configValues) => {
const parameters = provider.metadata.config_keys || [];

const upsertPromises = parameters.map((parameter) => {
// Skip parameters that don't have a value and aren't required
if (!configValues[parameter.name] && !parameter.required) {
return Promise.resolve();
}

// For required parameters with no value, use the default if available
const value =
configValues[parameter.name] !== undefined ? configValues[parameter.name] : parameter.default;

// Skip if there's still no value
if (value === undefined || value === null) {
return Promise.resolve();
}

// Create the provider-specific config key
const configKey = `${parameter.name}`;

// Explicitly define is_secret as a boolean (true/false) or null
// This is critical for Rust's Option<bool> type
const isSecret = parameter.secret === true;

// Pass the is_secret flag from the parameter definition
return upsertFn(configKey, value, isSecret);
});
}

// Wait for all upsert operations to complete
return Promise.all(upsertPromises);
};