From 59f1ce266d39ec06aaad2d8e48514d0e1bf5a968 Mon Sep 17 00:00:00 2001 From: Lily Delalande <119957291+lily-de@users.noreply.github.com> Date: Fri, 14 Mar 2025 13:45:44 -0700 Subject: [PATCH] ui: reorganize extensions settings (#1702) --- .../extensions/ExtensionsSection.tsx | 610 ++++-------------- .../extensions/modal/EnvVarsSection.tsx | 55 ++ .../modal/ExtensionConfigFields.tsx | 60 ++ .../extensions/modal/ExtensionModal.tsx | 119 ++++ .../subcomponents/ExtensionItem.tsx | 39 ++ .../subcomponents/ExtensionList.tsx | 54 ++ .../settings_v2/extensions/utils.tsx | 121 ---- 7 files changed, 447 insertions(+), 611 deletions(-) create mode 100644 ui/desktop/src/components/settings_v2/extensions/modal/EnvVarsSection.tsx create mode 100644 ui/desktop/src/components/settings_v2/extensions/modal/ExtensionConfigFields.tsx create mode 100644 ui/desktop/src/components/settings_v2/extensions/modal/ExtensionModal.tsx create mode 100644 ui/desktop/src/components/settings_v2/extensions/subcomponents/ExtensionItem.tsx create mode 100644 ui/desktop/src/components/settings_v2/extensions/subcomponents/ExtensionList.tsx delete mode 100644 ui/desktop/src/components/settings_v2/extensions/utils.tsx diff --git a/ui/desktop/src/components/settings_v2/extensions/ExtensionsSection.tsx b/ui/desktop/src/components/settings_v2/extensions/ExtensionsSection.tsx index fa7edcc5..45ea2be1 100644 --- a/ui/desktop/src/components/settings_v2/extensions/ExtensionsSection.tsx +++ b/ui/desktop/src/components/settings_v2/extensions/ExtensionsSection.tsx @@ -1,15 +1,11 @@ import React, { useEffect, useState } from 'react'; import { Button } from '../../ui/button'; -import { Switch } from '../../ui/switch'; -import { Plus, X } from 'lucide-react'; -import { Gear } from '../../icons/Gear'; +import { Plus } from 'lucide-react'; import { GPSIcon } from '../../ui/icons'; import { useConfig, FixedExtensionEntry } from '../../ConfigContext'; -import Modal from '../../Modal'; -import { Input } from '../../ui/input'; -import Select from 'react-select'; -import { createDarkSelectStyles, darkSelectTheme } from '../../ui/select-styles'; import { ExtensionConfig } from '../../../api/types.gen'; +import ExtensionList from './subcomponents/ExtensionList'; +import ExtensionModal from './modal/ExtensionModal'; export default function ExtensionsSection() { const { toggleExtension, getExtensions, addExtension } = useConfig(); @@ -19,45 +15,7 @@ export default function ExtensionsSection() { const [selectedExtension, setSelectedExtension] = useState(null); const [isModalOpen, setIsModalOpen] = useState(false); const [isAddModalOpen, setIsAddModalOpen] = useState(false); - const [formData, setFormData] = useState<{ - name: string; - type: 'stdio' | 'sse' | 'builtin'; - cmd?: string; - args?: string[]; - endpoint?: string; - enabled: boolean; - envVars: { key: string; value: string }[]; - }>({ - name: '', - type: 'stdio', - cmd: '', - args: [], - endpoint: '', - enabled: true, - envVars: [], - }); - // Helper function to get a friendly title from extension name - const getFriendlyTitle = (name: string): string => { - return name - .split('-') - .map((word) => word.charAt(0).toUpperCase() + word.slice(1)) - .join(' '); - }; - - // Helper function to get a subtitle based on extension type and configuration - const getSubtitle = (config: ExtensionConfig): string => { - if (config.type === 'builtin') { - return 'Built-in extension'; - } - if (config.type === 'stdio') { - return `STDIO extension${config.cmd ? ` (${config.cmd})` : ''}`; - } - if (config.type === 'sse') { - return `SSE extension${config.uri ? ` (${config.uri})` : ''}`; - } - return `Unknown type of extension`; - }; const fetchExtensions = async () => { setLoading(true); try { @@ -78,31 +36,6 @@ export default function ExtensionsSection() { fetchExtensions(); }, []); - useEffect(() => { - if (selectedExtension) { - // Type guard: Check if 'envs' property exists for this variant - const hasEnvs = selectedExtension.type === 'sse' || selectedExtension.type === 'stdio'; - - const envVars = - hasEnvs && selectedExtension.envs - ? Object.entries(selectedExtension.envs).map(([key, value]) => ({ - key, - value: value as string, - })) - : []; - - setFormData({ - name: selectedExtension.name, - type: selectedExtension.type, - cmd: selectedExtension.type === 'stdio' ? selectedExtension.cmd : undefined, - args: selectedExtension.type === 'stdio' ? selectedExtension.args : [], - endpoint: selectedExtension.type === 'sse' ? selectedExtension.uri : undefined, - enabled: selectedExtension.enabled, - envVars, - }); - } - }, [selectedExtension]); - const handleExtensionToggle = async (name: string) => { try { await toggleExtension(name); @@ -117,41 +50,8 @@ export default function ExtensionsSection() { setIsModalOpen(true); }; - const handleAddExtension = async () => { - const envs = formData.envVars.reduce( - (acc, { key, value }) => { - if (key) { - acc[key] = value; - } - return acc; - }, - {} as Record - ); - - let extensionConfig: ExtensionConfig; - - if (formData.type === 'stdio') { - extensionConfig = { - type: 'stdio', - name: formData.name, - cmd: formData.cmd, - args: formData.args, - ...(Object.keys(envs).length > 0 ? { envs } : {}), - }; - } else if (formData.type === 'sse') { - extensionConfig = { - type: 'sse', - name: formData.name, - uri: formData.endpoint, // Assuming endpoint maps to uri for SSE type - ...(Object.keys(envs).length > 0 ? { envs } : {}), - }; - } else { - // For other types - extensionConfig = { - type: formData.type, - name: formData.name, - }; - } + const handleAddExtension = async (formData: ExtensionFormData) => { + const extensionConfig = createExtensionConfig(formData); try { await addExtension(formData.name, extensionConfig, formData.enabled); @@ -162,85 +62,10 @@ export default function ExtensionsSection() { } }; - const handleModalClose = () => { - setIsModalOpen(false); - setIsAddModalOpen(false); - setSelectedExtension(null); - setFormData({ - name: '', - type: 'stdio', - cmd: '', - args: [], - endpoint: '', - enabled: true, - envVars: [], - }); - }; - - const handleAddEnvVar = () => { - setFormData({ - ...formData, - envVars: [...formData.envVars, { key: '', value: '' }], - }); - }; - - const handleRemoveEnvVar = (index: number) => { - const newEnvVars = [...formData.envVars]; - newEnvVars.splice(index, 1); - setFormData({ - ...formData, - envVars: newEnvVars, - }); - }; - - const handleEnvVarChange = (index: number, field: 'key' | 'value', value: string) => { - const newEnvVars = [...formData.envVars]; - newEnvVars[index][field] = value; - setFormData({ - ...formData, - envVars: newEnvVars, - }); - }; - const handleSaveConfig = async () => { - if (!selectedExtension) return; - - const envs = formData.envVars.reduce( - (acc, { key, value }) => { - if (key) { - acc[key] = value; - } - return acc; - }, - {} as Record - ); - - let extensionConfig: ExtensionConfig; - - if (formData.type === 'stdio') { - extensionConfig = { - type: 'stdio', - name: formData.name, - cmd: formData.cmd, - args: formData.args, - ...(Object.keys(envs).length > 0 ? { envs } : {}), - }; - } else if (formData.type === 'sse') { - extensionConfig = { - type: 'sse', - name: formData.name, - uri: formData.endpoint, // Assuming endpoint maps to uri for SSE type - ...(Object.keys(envs).length > 0 ? { envs } : {}), - }; - } else { - // For other types - extensionConfig = { - type: formData.type, - name: formData.name, - }; - } + const handleUpdateExtension = async (formData: ExtensionFormData) => { + const extensionConfig = createExtensionConfig(formData); try { - // CHANGE: Use addExtension instead of updateExtension await addExtension(formData.name, extensionConfig, formData.enabled); handleModalClose(); fetchExtensions(); // Refresh the list after updating @@ -249,6 +74,12 @@ export default function ExtensionsSection() { } }; + const handleModalClose = () => { + setIsModalOpen(false); + setIsAddModalOpen(false); + setSelectedExtension(null); + }; + return (
@@ -259,55 +90,20 @@ export default function ExtensionsSection() { These extensions use the Model Context Protocol (MCP). They can expand Goose's capabilities using three main components: Prompts, Resources, and Tools.

-
- {extensions.map((extension, index) => ( - -
-
-

- {getFriendlyTitle(extension.name)} -

-

{getSubtitle(extension)}

-
-
- {/* Only show config button for non-builtin extensions */} - {extension.type !== 'builtin' && ( - - )} - handleExtensionToggle(extension.name)} - variant="mono" - /> -
-
- {index < extensions.length - 1 &&
} - - ))} -
+ + +
+ {/* Modal for updating an existing extension */} {isModalOpen && selectedExtension && ( - -
-

Update Extension

- -
-
- - setFormData({ ...formData, name: e.target.value })} - placeholder="Enter extension name..." - /> -
-
- - - setFormData({ ...formData, cmd: e.target.value, endpoint: undefined }) - } - placeholder="Enter command..." - className="w-full" - /> -
-
- - - setFormData({ - ...formData, - args: e.target.value.split(' ').filter((arg) => arg.length > 0), - }) - } - placeholder="Enter arguments..." - className="w-full" - /> -
-
- ) : ( -
- - - setFormData({ - ...formData, - endpoint: e.target.value, - cmd: undefined, - args: [], - }) - } - placeholder="Enter endpoint URL..." - className="w-full" - /> -
- )} -
- -
-
- - -
- -
- {formData.envVars.map((envVar, index) => ( -
- handleEnvVarChange(index, 'key', e.target.value)} - placeholder="Key" - className="flex-1" - /> - handleEnvVarChange(index, 'value', e.target.value)} - placeholder="Value" - className="flex-1" - /> - -
- ))} -
-
- -
- - -
-
- + )} + {/* Modal for adding a new extension */} {isAddModalOpen && ( - -
-

Add New Extension

- -
-
- - setFormData({ ...formData, name: e.target.value })} - placeholder="Enter extension name..." - /> -
-
- - - setFormData({ ...formData, cmd: e.target.value, endpoint: undefined }) - } - placeholder="Enter command..." - className="w-full" - /> -
-
- - - setFormData({ - ...formData, - args: e.target.value.split(' ').filter((arg) => arg.length > 0), - }) - } - placeholder="Enter arguments..." - className="w-full" - /> -
-
- ) : ( -
- - - setFormData({ - ...formData, - endpoint: e.target.value, - cmd: undefined, - args: [], - }) - } - placeholder="Enter endpoint URL..." - className="w-full" - /> -
- )} -
- -
-
- - -
- -
- {formData.envVars.map((envVar, index) => ( -
- handleEnvVarChange(index, 'key', e.target.value)} - placeholder="Key" - className="flex-1" - /> - handleEnvVarChange(index, 'value', e.target.value)} - placeholder="Value" - className="flex-1" - /> - -
- ))} -
-
- -
- - -
-
- + )}
); } + +// Helper functions + +export interface ExtensionFormData { + name: string; + type: 'stdio' | 'sse' | 'builtin'; + cmd?: string; + args?: string[]; + endpoint?: string; + enabled: boolean; + envVars: { key: string; value: string }[]; +} + +function getDefaultFormData(): ExtensionFormData { + return { + name: '', + type: 'stdio', + cmd: '', + args: [], + endpoint: '', + enabled: true, + envVars: [], + }; +} + +function extensionToFormData(extension: FixedExtensionEntry): ExtensionFormData { + // Type guard: Check if 'envs' property exists for this variant + const hasEnvs = extension.type === 'sse' || extension.type === 'stdio'; + + const envVars = + hasEnvs && extension.envs + ? Object.entries(extension.envs).map(([key, value]) => ({ + key, + value: value as string, + })) + : []; + + return { + name: extension.name, + type: extension.type, + cmd: extension.type === 'stdio' ? extension.cmd : undefined, + args: extension.type === 'stdio' ? extension.args : [], + endpoint: extension.type === 'sse' ? extension.uri : undefined, + enabled: extension.enabled, + envVars, + }; +} + +function createExtensionConfig(formData: ExtensionFormData): ExtensionConfig { + const envs = formData.envVars.reduce( + (acc, { key, value }) => { + if (key) { + acc[key] = value; + } + return acc; + }, + {} as Record + ); + + if (formData.type === 'stdio') { + return { + type: 'stdio', + name: formData.name, + cmd: formData.cmd, + args: formData.args, + ...(Object.keys(envs).length > 0 ? { envs } : {}), + }; + } else if (formData.type === 'sse') { + return { + type: 'sse', + name: formData.name, + uri: formData.endpoint, // Assuming endpoint maps to uri for SSE type + ...(Object.keys(envs).length > 0 ? { envs } : {}), + }; + } else { + // For other types + return { + type: formData.type, + name: formData.name, + }; + } +} diff --git a/ui/desktop/src/components/settings_v2/extensions/modal/EnvVarsSection.tsx b/ui/desktop/src/components/settings_v2/extensions/modal/EnvVarsSection.tsx new file mode 100644 index 00000000..7e064b66 --- /dev/null +++ b/ui/desktop/src/components/settings_v2/extensions/modal/EnvVarsSection.tsx @@ -0,0 +1,55 @@ +import React from 'react'; +import { Button } from '../../../ui/button'; +import { X } from 'lucide-react'; +import { Input } from '../../../ui/input'; + +interface EnvVarsSectionProps { + envVars: { key: string; value: string }[]; + onAdd: () => void; + onRemove: (index: number) => void; + onChange: (index: number, field: 'key' | 'value', value: string) => void; +} + +export default function EnvVarsSection({ + envVars, + onAdd, + onRemove, + onChange, +}: EnvVarsSectionProps) { + return ( +
+
+ + +
+ +
+ {envVars.map((envVar, index) => ( +
+ onChange(index, 'key', e.target.value)} + placeholder="Key" + className="flex-1" + /> + onChange(index, 'value', e.target.value)} + placeholder="Value" + className="flex-1" + /> + +
+ ))} +
+
+ ); +} diff --git a/ui/desktop/src/components/settings_v2/extensions/modal/ExtensionConfigFields.tsx b/ui/desktop/src/components/settings_v2/extensions/modal/ExtensionConfigFields.tsx new file mode 100644 index 00000000..2837c1cd --- /dev/null +++ b/ui/desktop/src/components/settings_v2/extensions/modal/ExtensionConfigFields.tsx @@ -0,0 +1,60 @@ +import { Input } from '../../../ui/input'; +import React from 'react'; + +interface ExtensionConfigFieldsProps { + type: 'stdio' | 'sse' | 'builtin'; + cmd: string; + args: string; + endpoint: string; + onChange: (key: string, value: any) => void; +} + +export default function ExtensionConfigFields({ + type, + cmd, + args, + endpoint, + onChange, +}: ExtensionConfigFieldsProps) { + if (type === 'stdio') { + return ( +
+
+ + onChange('cmd', e.target.value)} + placeholder="Enter command..." + className="w-full" + /> +
+
+ + + onChange( + 'args', + e.target.value.split(' ').filter((arg) => arg.length > 0) + ) + } + placeholder="Enter arguments..." + className="w-full" + /> +
+
+ ); + } else { + return ( +
+ + onChange('endpoint', e.target.value)} + placeholder="Enter endpoint URL..." + className="w-full" + /> +
+ ); + } +} diff --git a/ui/desktop/src/components/settings_v2/extensions/modal/ExtensionModal.tsx b/ui/desktop/src/components/settings_v2/extensions/modal/ExtensionModal.tsx new file mode 100644 index 00000000..f3614ec2 --- /dev/null +++ b/ui/desktop/src/components/settings_v2/extensions/modal/ExtensionModal.tsx @@ -0,0 +1,119 @@ +// ExtensionModal.tsx +import React, { useState } from 'react'; +import { Button } from '../../../ui/button'; +import Modal from '../../../Modal'; +import { Input } from '../../../ui/input'; +import Select from 'react-select'; +import { createDarkSelectStyles, darkSelectTheme } from '../../../ui/select-styles'; +import { ExtensionFormData } from '../ExtensionsSection'; +import EnvVarsSection from './EnvVarsSection'; +import ExtensionConfigFields from './ExtensionConfigFields'; + +interface ExtensionModalProps { + title: string; + initialData: ExtensionFormData; + onClose: () => void; + onSubmit: (formData: ExtensionFormData) => void; + submitLabel: string; +} + +export default function ExtensionModal({ + title, + initialData, + onClose, + onSubmit, + submitLabel, +}: ExtensionModalProps) { + const [formData, setFormData] = useState(initialData); + + const handleAddEnvVar = () => { + setFormData({ + ...formData, + envVars: [...formData.envVars, { key: '', value: '' }], + }); + }; + + const handleRemoveEnvVar = (index: number) => { + const newEnvVars = [...formData.envVars]; + newEnvVars.splice(index, 1); + setFormData({ + ...formData, + envVars: newEnvVars, + }); + }; + + const handleEnvVarChange = (index: number, field: 'key' | 'value', value: string) => { + const newEnvVars = [...formData.envVars]; + newEnvVars[index][field] = value; + setFormData({ + ...formData, + envVars: newEnvVars, + }); + }; + + return ( + +
+

{title}

+ +
+
+ + setFormData({ ...formData, name: e.target.value })} + placeholder="Enter extension name..." + /> +
+
+ +