diff --git a/ui/desktop/openapi.json b/ui/desktop/openapi.json index 011a55f20eac..f0f203b91616 100644 --- a/ui/desktop/openapi.json +++ b/ui/desktop/openapi.json @@ -10,7 +10,7 @@ "license": { "name": "Apache-2.0" }, - "version": "1.0.13" + "version": "1.0.14" }, "paths": { "/config": { diff --git a/ui/desktop/src/components/settings_v2/extensions/ExtensionsSection.tsx b/ui/desktop/src/components/settings_v2/extensions/ExtensionsSection.tsx index ece4fafdc781..fa7edcc537d3 100644 --- a/ui/desktop/src/components/settings_v2/extensions/ExtensionsSection.tsx +++ b/ui/desktop/src/components/settings_v2/extensions/ExtensionsSection.tsx @@ -4,65 +4,29 @@ import { Switch } from '../../ui/switch'; import { Plus, X } from 'lucide-react'; import { Gear } from '../../icons/Gear'; import { GPSIcon } from '../../ui/icons'; -import { useConfig } from '../../ConfigContext'; +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'; - -interface ExtensionConfig { - args?: string[]; - cmd?: string; - enabled: boolean; - envs?: Record; - name: string; - type: 'stdio' | 'sse' | 'builtin'; -} - -interface ExtensionItem { - id: string; - title: string; - subtitle: string; - enabled: boolean; - canConfigure: boolean; - config: ExtensionConfig; -} - -interface EnvVar { - key: string; - value: string; -} - -// 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'; - } - return `${config.type.toUpperCase()} extension${config.cmd ? ` (${config.cmd})` : ''}`; -}; +import { ExtensionConfig } from '../../../api/types.gen'; export default function ExtensionsSection() { - const { config, read, updateExtension, addExtension } = useConfig(); - const [extensions, setExtensions] = useState([]); - const [selectedExtension, setSelectedExtension] = useState(null); + const { toggleExtension, getExtensions, addExtension } = useConfig(); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [extensions, setExtensions] = useState([]); + const [selectedExtension, setSelectedExtension] = useState(null); const [isModalOpen, setIsModalOpen] = useState(false); const [isAddModalOpen, setIsAddModalOpen] = useState(false); const [formData, setFormData] = useState<{ name: string; - type: 'stdio' | 'sse'; + type: 'stdio' | 'sse' | 'builtin'; cmd?: string; args?: string[]; endpoint?: string; enabled: boolean; - envVars: EnvVar[]; + envVars: { key: string; value: string }[]; }>({ name: '', type: 'stdio', @@ -73,63 +37,82 @@ export default function ExtensionsSection() { envVars: [], }); - useEffect(() => { - const extensions = read('extensions', false); - if (extensions) { - const extensionItems: ExtensionItem[] = Object.entries(extensions).map(([name, ext]) => { - const extensionConfig = ext as ExtensionConfig; - return { - id: name, - title: getFriendlyTitle(name), - subtitle: getSubtitle(extensionConfig), - enabled: extensionConfig.enabled, - canConfigure: extensionConfig.type === 'stdio' && !!extensionConfig.envs, - config: extensionConfig, - }; - }); - setExtensions(extensionItems); + // 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})` : ''}`; } - }, [read]); + if (config.type === 'sse') { + return `SSE extension${config.uri ? ` (${config.uri})` : ''}`; + } + return `Unknown type of extension`; + }; + const fetchExtensions = async () => { + setLoading(true); + try { + const extensionsList = await getExtensions(true); // Force refresh + // Sort extensions by name to maintain consistent order + const sortedExtensions = [...extensionsList].sort((a, b) => a.name.localeCompare(b.name)); + setExtensions(sortedExtensions); + setError(null); + } catch (err) { + setError('Failed to load extensions'); + console.error('Error loading extensions:', err); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchExtensions(); + }, []); useEffect(() => { if (selectedExtension) { - const envVars = selectedExtension.config.envs - ? Object.entries(selectedExtension.config.envs).map(([key, value]) => ({ - key, - value: value as string, - })) - : []; + // 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.config.name, - type: selectedExtension.config.type as 'stdio' | 'sse', - cmd: selectedExtension.config.type === 'stdio' ? selectedExtension.config.cmd : undefined, - args: selectedExtension.config.args || [], - endpoint: - selectedExtension.config.type === 'sse' ? selectedExtension.config.cmd : undefined, - enabled: selectedExtension.config.enabled, + 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 (id: string) => { - const extension = extensions.find((ext) => ext.id === id); - if (extension) { - const updatedConfig = { - ...extension.config, - enabled: !extension.config.enabled, - }; - - try { - await updateExtension(id, updatedConfig); - } catch (error) { - console.error('Failed to update extension:', error); - } + const handleExtensionToggle = async (name: string) => { + try { + await toggleExtension(name); + fetchExtensions(); // Refresh the list after toggling + } catch (error) { + console.error('Failed to toggle extension:', error); } }; - const handleConfigureClick = (extension: ExtensionItem) => { + const handleConfigureClick = (extension: FixedExtensionEntry) => { setSelectedExtension(extension); setIsModalOpen(true); }; @@ -145,24 +128,35 @@ export default function ExtensionsSection() { {} as Record ); - const extensionConfig = { - name: formData.name, - type: formData.type, - enabled: formData.enabled, - envs, - ...(formData.type === 'stdio' - ? { - cmd: formData.cmd, - args: formData.args, - } - : { - cmd: formData.endpoint, - }), - }; + 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, + }; + } try { - await addExtension(formData.name, extensionConfig); + await addExtension(formData.name, extensionConfig, formData.enabled); handleModalClose(); + fetchExtensions(); // Refresh the list after adding } catch (error) { console.error('Failed to add extension:', error); } @@ -207,7 +201,6 @@ export default function ExtensionsSection() { envVars: newEnvVars, }); }; - const handleSaveConfig = async () => { if (!selectedExtension) return; @@ -221,24 +214,36 @@ export default function ExtensionsSection() { {} as Record ); - const updatedConfig = { - name: formData.name, - type: formData.type, - enabled: formData.enabled, - envs, - ...(formData.type === 'stdio' - ? { - cmd: formData.cmd, - args: formData.args, - } - : { - cmd: formData.endpoint, - }), - }; + 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, + }; + } try { - await updateExtension(selectedExtension.id, updatedConfig); + // CHANGE: Use addExtension instead of updateExtension + await addExtension(formData.name, extensionConfig, formData.enabled); handleModalClose(); + fetchExtensions(); // Refresh the list after updating } catch (error) { console.error('Failed to update extension configuration:', error); } @@ -256,14 +261,17 @@ export default function ExtensionsSection() {

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

{extension.title}

-

{extension.subtitle}

+

+ {getFriendlyTitle(extension.name)} +

+

{getSubtitle(extension)}

- {extension.canConfigure && ( + {/* Only show config button for non-builtin extensions */} + {extension.type !== 'builtin' && (
@@ -329,10 +337,10 @@ export default function ExtensionsSection() { + onChange={(option: { value: string; label: string } | null) => setFormData({ ...formData, type: (option?.value as 'stdio' | 'sse') || 'stdio',