From e3c085e26082d95cd4d8360c3f8057662f65ab10 Mon Sep 17 00:00:00 2001 From: Ben Walding Date: Mon, 17 Mar 2025 13:46:26 +0000 Subject: [PATCH 1/2] fix: update the mcp client protocol version to 2024-11-05 (#1690) docs: Add Filesystem Short Video to Tutorial (#1723) separate args and command get rid of args add delete button make scrollable and fix bottom button spacing add button form validation allow esc key + click background to close modal; update providers modal fix providers modal add padding to add button add button styling move standalone functions to utils update the import tweaks add some update agent functionality Delete ui/desktop/src/agent/extensions.tsx add shim improve load experience --- crates/mcp-client/src/client.rs | 2 +- .../docs/tutorials/filesystem-mcp.md | 1 + ui/desktop/src/agent/UpdateAgent.tsx | 182 +++++++++++++ ui/desktop/src/agent/utils.tsx | 32 +++ ui/desktop/src/components/Modal.tsx | 56 +++- .../extensions/ExtensionsSection.tsx | 110 ++------ .../extensions/modal/EnvVarsSection.tsx | 83 ++++-- .../modal/ExtensionConfigFields.tsx | 62 +++-- .../extensions/modal/ExtensionModal.tsx | 249 +++++++++++++----- .../subcomponents/ExtensionItem.tsx | 11 +- .../subcomponents/ExtensionList.tsx | 4 +- .../settings_v2/extensions/utils.ts | 97 +++++++ .../providers/ProviderSettingsPage.tsx | 24 +- .../modal/ProviderConfiguationModal.tsx | 2 +- 14 files changed, 697 insertions(+), 218 deletions(-) create mode 100644 ui/desktop/src/agent/UpdateAgent.tsx create mode 100644 ui/desktop/src/agent/utils.tsx create mode 100644 ui/desktop/src/components/settings_v2/extensions/utils.ts diff --git a/crates/mcp-client/src/client.rs b/crates/mcp-client/src/client.rs index 0d722e558566..6342ba1d2476 100644 --- a/crates/mcp-client/src/client.rs +++ b/crates/mcp-client/src/client.rs @@ -245,7 +245,7 @@ where capabilities: ClientCapabilities, ) -> Result { let params = InitializeParams { - protocol_version: "1.0.0".into(), + protocol_version: "2024-11-05".to_string(), client_info: info, capabilities, }; diff --git a/documentation/docs/tutorials/filesystem-mcp.md b/documentation/docs/tutorials/filesystem-mcp.md index 609defdbe10f..6b28b863eee4 100644 --- a/documentation/docs/tutorials/filesystem-mcp.md +++ b/documentation/docs/tutorials/filesystem-mcp.md @@ -7,6 +7,7 @@ import Tabs from '@theme/Tabs'; import TabItem from '@theme/TabItem'; import YouTubeShortEmbed from '@site/src/components/YouTubeShortEmbed'; + This tutorial covers how to add the [Filesystem MCP server](https://github.com/modelcontextprotocol/servers/tree/HEAD/src/filesystem) as a Goose extension, enabling powerful code analysis and file management. With this extension, Goose can analyze project structures, edit and organize files, detect unused dependencies, and generate documentation to improve software maintainability. diff --git a/ui/desktop/src/agent/UpdateAgent.tsx b/ui/desktop/src/agent/UpdateAgent.tsx new file mode 100644 index 000000000000..fa3cb6432d70 --- /dev/null +++ b/ui/desktop/src/agent/UpdateAgent.tsx @@ -0,0 +1,182 @@ +import { useConfig, FixedExtensionEntry } from '../components/ConfigContext'; +import { getApiUrl, getSecretKey } from '../config'; +import { ExtensionConfig } from '../api'; +import { toast } from 'react-toastify'; +import React, { useState } from 'react'; +import { initializeAgent as startAgent, replaceWithShims } from './utils'; + +// extensionUpdate = an extension was newly added or updated so we should attempt to add it + +export const useAgent = () => { + const { getExtensions, read } = useConfig(); + const [isUpdating, setIsUpdating] = useState(false); + + // whenever we change the model, we must call this + const initializeAgent = async (provider: string, model: string) => { + try { + console.log('Initializing agent with provider', provider, 'model', model); + + const response = await startAgent(model, provider); + + if (!response.ok) { + throw new Error(`Failed to initialize agent: ${response.statusText}`); + } + + return true; + } catch (error) { + console.error('Failed to initialize agent:', error); + toast.error( + `Failed to initialize agent: ${error instanceof Error ? error.message : 'Unknown error'}` + ); + return false; + } + }; + + const updateAgent = async (extensionUpdate?: ExtensionConfig) => { + setIsUpdating(true); + + try { + // need to initialize agent first (i dont get why but if we dont do this, we get a 428) + // note: we must write the value for GOOSE_MODEL and GOOSE_PROVIDER in the config before updating agent + const goose_model = (await read('GOOSE_MODEL', false)) as string; + const goose_provider = (await read('GOOSE_PROVIDER', false)) as string; + + console.log( + `Starting agent with GOOSE_MODEL=${goose_model} and GOOSE_PROVIDER=${goose_provider}` + ); + + // Initialize the agent if it's a model change + if (goose_model && goose_provider) { + const success = await initializeAgent(goose_provider, goose_model); + if (!success) { + console.error('Failed to initialize agent during model change'); + return false; + } + } + + if (extensionUpdate) { + await addExtensionToAgent(extensionUpdate); + } + + return true; + } catch (error) { + console.error('Error updating agent:', error); + return false; + } finally { + setIsUpdating(false); + } + }; + + // TODO: set 'enabled' to false if we fail to start / add the extension + // only for non-builtins + + // TODO: try to add some descriptive error messages for common failure modes + const addExtensionToAgent = async ( + extension: ExtensionConfig, + silent: boolean = false + ): Promise => { + if (extension.type == 'stdio') { + console.log('extension command', extension.cmd); + extension.cmd = await replaceWithShims(extension.cmd); + console.log('next ext command', extension.cmd); + } + + try { + let toastId; + if (!silent) { + toastId = toast.loading(`Adding ${extension.name} extension...`, { + position: 'top-center', + }); + toast.info('Press the escape key to continue using goose while extension loads'); + } + + const response = await fetch(getApiUrl('/extensions/add'), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Secret-Key': getSecretKey(), + }, + body: JSON.stringify(extension), + }); + + // Handle non-OK responses + if (!response.ok) { + const errorMsg = `Server returned ${response.status}: ${response.statusText}`; + console.error(errorMsg); + + // Special handling for 428 Precondition Required (agent not initialized) + if (response.status === 428) { + if (!silent) { + if (toastId) toast.dismiss(toastId); + toast.error('Agent is not initialized. Please initialize the agent first.'); + } + return response; + } + + if (!silent) { + if (toastId) toast.dismiss(toastId); + toast.error(`Failed to add ${extension.name} extension: ${errorMsg}`); + } + return response; + } + + // Parse response JSON safely + let data; + try { + const text = await response.text(); + data = text ? JSON.parse(text) : { error: false }; + } catch (error) { + console.warn('Could not parse response as JSON, assuming success', error); + data = { error: false }; + } + + console.log('Response data:', data); + + if (!data.error) { + if (!silent) { + if (toastId) toast.dismiss(toastId); + toast.success(`Successfully enabled ${extension.name} extension`); + } + return response; + } + + console.log('Error trying to send a request to the extensions endpoint'); + const errorMessage = `Error adding ${extension.name} extension${data.message ? `. ${data.message}` : ''}`; + const ErrorMsg = ({ closeToast }: { closeToast?: () => void }) => ( +
+
Error adding {extension.name} extension
+
+ +
+
+ ); + + console.error(errorMessage); + if (toastId) toast.dismiss(toastId); + toast(ErrorMsg, { type: 'error', autoClose: false }); + + return response; + } catch (error) { + console.log('Got some other error'); + const errorMessage = `Failed to add ${extension.name} extension: ${error instanceof Error ? error.message : 'Unknown error'}`; + console.error(errorMessage); + toast.error(errorMessage, { autoClose: false }); + throw error; + } + }; + + return { + updateAgent, + addExtensionToAgent, + initializeAgent, + isUpdating, + }; +}; diff --git a/ui/desktop/src/agent/utils.tsx b/ui/desktop/src/agent/utils.tsx new file mode 100644 index 000000000000..a834126fb752 --- /dev/null +++ b/ui/desktop/src/agent/utils.tsx @@ -0,0 +1,32 @@ +import { getApiUrl, getSecretKey } from '../config'; + +export async function initializeAgent(model: string, provider: string) { + console.log('fetching...', provider, model); + const response = await fetch(getApiUrl('/agent'), { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-Secret-Key': getSecretKey(), + }, + body: JSON.stringify({ + provider: provider.toLowerCase().replace(/ /g, '_'), + model: model, + }), + }); + return response; +} + +export async function replaceWithShims(cmd: string) { + const binaryPathMap: Record = { + goosed: await window.electron.getBinaryPath('goosed'), + npx: await window.electron.getBinaryPath('npx'), + uvx: await window.electron.getBinaryPath('uvx'), + }; + + if (binaryPathMap[cmd]) { + console.log('--------> Replacing command with shim ------>', cmd, binaryPathMap[cmd]); + cmd = binaryPathMap[cmd]; + } + + return cmd; +} diff --git a/ui/desktop/src/components/Modal.tsx b/ui/desktop/src/components/Modal.tsx index 2f9cfb51cd60..0a6a2374ab2a 100644 --- a/ui/desktop/src/components/Modal.tsx +++ b/ui/desktop/src/components/Modal.tsx @@ -1,18 +1,64 @@ -import React from 'react'; +import React, { useEffect, useRef } from 'react'; import { Card } from './ui/card'; interface ModalProps { children: React.ReactNode; + footer?: React.ReactNode; // Optional footer + onClose: () => void; // Function to call when modal should close + preventBackdropClose?: boolean; // Optional prop to prevent closing on backdrop click } /** * A reusable modal component that renders content with a semi-transparent backdrop and blur effect. + * Closes when clicking outside the modal or pressing Esc key. */ -export default function Modal({ children }: ModalProps) { +export default function Modal({ + children, + footer, + onClose, + preventBackdropClose = false, +}: ModalProps) { + const modalRef = useRef(null); + + // Handle click outside the modal content + const handleBackdropClick = (e: React.MouseEvent) => { + if (preventBackdropClose) return; + // Check if the click was on the backdrop and not on the modal content + if (modalRef.current && !modalRef.current.contains(e.target as Node)) { + onClose(); + } + }; + + // Handle Esc key press + useEffect(() => { + const handleEscKey = (e: KeyboardEvent) => { + if (e.key === 'Escape') { + onClose(); + } + }; + + // Add event listener + document.addEventListener('keydown', handleEscKey); + + // Clean up + return () => { + document.removeEventListener('keydown', handleEscKey); + }; + }, [onClose]); + return ( -
- -
{children}
+
+ +
{children}
+ {footer && ( +
{footer}
+ )}
); diff --git a/ui/desktop/src/components/settings_v2/extensions/ExtensionsSection.tsx b/ui/desktop/src/components/settings_v2/extensions/ExtensionsSection.tsx index 45ea2be1298d..60b10540749a 100644 --- a/ui/desktop/src/components/settings_v2/extensions/ExtensionsSection.tsx +++ b/ui/desktop/src/components/settings_v2/extensions/ExtensionsSection.tsx @@ -3,18 +3,25 @@ import { Button } from '../../ui/button'; import { Plus } from 'lucide-react'; import { GPSIcon } from '../../ui/icons'; import { useConfig, FixedExtensionEntry } from '../../ConfigContext'; -import { ExtensionConfig } from '../../../api/types.gen'; import ExtensionList from './subcomponents/ExtensionList'; import ExtensionModal from './modal/ExtensionModal'; +import { + createExtensionConfig, + ExtensionFormData, + extensionToFormData, + getDefaultFormData, +} from './utils'; +import { useAgent } from '../../../agent/UpdateAgent'; export default function ExtensionsSection() { - const { toggleExtension, getExtensions, addExtension } = useConfig(); + const { toggleExtension, getExtensions, addExtension, removeExtension } = 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 { updateAgent, addExtensionToAgent } = useAgent(); const fetchExtensions = async () => { setLoading(true); @@ -55,8 +62,10 @@ export default function ExtensionsSection() { try { await addExtension(formData.name, extensionConfig, formData.enabled); + console.log('attempting to add extension'); + await updateAgent(extensionConfig); handleModalClose(); - fetchExtensions(); // Refresh the list after adding + await fetchExtensions(); // Refresh the list after adding } catch (error) { console.error('Failed to add extension:', error); } @@ -74,6 +83,16 @@ export default function ExtensionsSection() { } }; + const handleDeleteExtension = async (name: string) => { + try { + await removeExtension(name); + handleModalClose(); + fetchExtensions(); // Refresh the list after deleting + } catch (error) { + console.error('Failed to delete extension:', error); + } + }; + const handleModalClose = () => { setIsModalOpen(false); setIsAddModalOpen(false); @@ -122,7 +141,9 @@ export default function ExtensionsSection() { initialData={extensionToFormData(selectedExtension)} onClose={handleModalClose} onSubmit={handleUpdateExtension} + onDelete={handleDeleteExtension} submitLabel="Save Changes" + modalType={'edit'} /> )} @@ -134,90 +155,9 @@ export default function ExtensionsSection() { onClose={handleModalClose} onSubmit={handleAddExtension} submitLabel="Add Extension" + modalType={'add'} /> )} ); } - -// 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 index 7e064b66eeab..d79cb3a60166 100644 --- a/ui/desktop/src/components/settings_v2/extensions/modal/EnvVarsSection.tsx +++ b/ui/desktop/src/components/settings_v2/extensions/modal/EnvVarsSection.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Button } from '../../../ui/button'; -import { X } from 'lucide-react'; +import { Plus, X } from 'lucide-react'; import { Input } from '../../../ui/input'; interface EnvVarsSectionProps { @@ -8,6 +8,8 @@ interface EnvVarsSectionProps { onAdd: () => void; onRemove: (index: number) => void; onChange: (index: number, field: 'key' | 'value', value: string) => void; + submitAttempted: boolean; + isValid: boolean; } export default function EnvVarsSection({ @@ -15,40 +17,73 @@ export default function EnvVarsSection({ onAdd, onRemove, onChange, + submitAttempted, + isValid, }: EnvVarsSectionProps) { return (
-
- - +
+ {' '} + {/* Added relative positioning with minimal margin */} + + {submitAttempted && !isValid && ( +
+ {' '} + {/* Removed absolute positioning */} + Environment variables must consist of sets of variable names and values +
+ )}
- -
+
+ {/* Existing environment variables */} {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" - /> + +
+ onChange(index, 'key', e.target.value)} + placeholder="Variable name" + className={`w-full bg-bgSubtle border-borderSubtle text-textStandard`} + /> +
+
+ onChange(index, 'value', e.target.value)} + placeholder="Value" + className={`w-full bg-bgSubtle border-borderSubtle text-textStandard`} + /> +
-
+ ))} + + {/* Empty row with Add button */} + + +
); diff --git a/ui/desktop/src/components/settings_v2/extensions/modal/ExtensionConfigFields.tsx b/ui/desktop/src/components/settings_v2/extensions/modal/ExtensionConfigFields.tsx index 2837c1cd11f5..4f9bd7f46e80 100644 --- a/ui/desktop/src/components/settings_v2/extensions/modal/ExtensionConfigFields.tsx +++ b/ui/desktop/src/components/settings_v2/extensions/modal/ExtensionConfigFields.tsx @@ -3,57 +3,55 @@ import React from 'react'; interface ExtensionConfigFieldsProps { type: 'stdio' | 'sse' | 'builtin'; - cmd: string; - args: string; + full_cmd: string; endpoint: string; onChange: (key: string, value: any) => void; + submitAttempted?: boolean; + isValid?: boolean; } export default function ExtensionConfigFields({ type, - cmd, - args, + full_cmd, endpoint, onChange, + submitAttempted = false, + isValid, }: 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" - /> + +
+ onChange('cmd', e.target.value)} + placeholder="e.g. npx -y @modelcontextprotocol/my-extension " + className={`w-full ${!submitAttempted || isValid ? 'border-borderSubtle' : 'border-red-500'} text-textStandard`} + /> + {submitAttempted && !isValid && ( +
Command is required
+ )} +
); } else { return (
- - onChange('endpoint', e.target.value)} - placeholder="Enter endpoint URL..." - className="w-full" - /> + +
+ onChange('endpoint', e.target.value)} + placeholder="Enter endpoint URL..." + className={`w-full ${!submitAttempted || isValid ? 'border-borderSubtle' : 'border-red-500'} text-textStandard`} + /> + {submitAttempted && !isValid && ( +
Endpoint URL is required
+ )} +
); } diff --git a/ui/desktop/src/components/settings_v2/extensions/modal/ExtensionModal.tsx b/ui/desktop/src/components/settings_v2/extensions/modal/ExtensionModal.tsx index f3614ec238a8..3e853de5daad 100644 --- a/ui/desktop/src/components/settings_v2/extensions/modal/ExtensionModal.tsx +++ b/ui/desktop/src/components/settings_v2/extensions/modal/ExtensionModal.tsx @@ -1,20 +1,22 @@ -// 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 { ExtensionFormData } from '../utils'; import EnvVarsSection from './EnvVarsSection'; import ExtensionConfigFields from './ExtensionConfigFields'; +import { PlusIcon, Edit, Trash2, AlertTriangle } from 'lucide-react'; interface ExtensionModalProps { title: string; initialData: ExtensionFormData; onClose: () => void; onSubmit: (formData: ExtensionFormData) => void; + onDelete?: (name: string) => void; submitLabel: string; + modalType: 'add' | 'edit'; } export default function ExtensionModal({ @@ -22,9 +24,13 @@ export default function ExtensionModal({ initialData, onClose, onSubmit, + onDelete, submitLabel, + modalType, }: ExtensionModalProps) { const [formData, setFormData] = useState(initialData); + const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false); + const [submitAttempted, setSubmitAttempted] = useState(false); const handleAddEnvVar = () => { setFormData({ @@ -51,69 +57,192 @@ export default function ExtensionModal({ }); }; + // Function to determine which icon to display with proper styling + const getModalIcon = () => { + if (showDeleteConfirmation) { + return ; + } + return modalType === 'add' ? ( + + ) : ( + + ); + }; + + const isNameValid = () => { + return formData.name.trim() !== ''; + }; + + const isConfigValid = () => { + return ( + (formData.type === 'stdio' && formData.cmd && formData.cmd.trim() !== '') || + (formData.type === 'sse' && formData.endpoint && formData.endpoint.trim() !== '') + ); + }; + + const isEnvVarsValid = () => { + return formData.envVars.every( + ({ key, value }) => (key === '' && value === '') || (key !== '' && value !== '') + ); + }; + + // Form validation + const isFormValid = () => { + return isNameValid() && isConfigValid() && isEnvVarsValid(); + }; + + // Handle submit with validation + const handleSubmit = () => { + setSubmitAttempted(true); + + if (isFormValid()) { + onSubmit(formData); + } + }; + + // Create footer buttons based on current state + const footerContent = showDeleteConfirmation ? ( + // Delete confirmation footer + <> +
+

+ Are you sure you want to delete "{formData.name}"? This action cannot be undone. +

+
+ + + + ) : ( + // Normal footer + <> + {modalType === 'edit' && onDelete && ( + + )} + + + + ); + + // Update title based on current state + const modalTitle = showDeleteConfirmation ? `Delete Extension "${formData.name}"` : title; + return ( - -
-

{title}

- -
-
- - setFormData({ ...formData, name: e.target.value })} - placeholder="Enter extension name..." - /> + + {/* Title and Icon */} +
+
{getModalIcon()}
+
+

{modalTitle}

+
+
+ + {showDeleteConfirmation ? ( +
+

+ This will permanently remove this extension and all of its settings. +

+
+ ) : ( + <> + {/* Form Fields */} + {/* Name */} +
+
+ +
+ setFormData({ ...formData, name: e.target.value })} + placeholder="Enter extension name..." + className={`${!submitAttempted || formData.name.trim() !== '' ? 'border-borderSubtle' : 'border-red-500'} text-textStandard focus:border-borderStandard`} + /> + {submitAttempted && !isNameValid() && ( +
Name is required
+ )} +
+
+ {/*Type Dropdown */} +
+ + - setFormData({ - ...formData, - type: (option?.value as 'stdio' | 'sse' | 'builtin') || 'stdio', - }) - } - options={[ - { value: 'stdio', label: 'STDIO' }, - { value: 'sse', label: 'SSE' }, - ]} - styles={createDarkSelectStyles('200px')} - theme={darkSelectTheme} - isSearchable={false} + + {/* Divider */} +
+ + {/* Config Fields */} +
+ setFormData({ ...formData, [key]: value })} + submitAttempted={submitAttempted} + isValid={isConfigValid()} />
-
- setFormData({ ...formData, [key]: value })} - /> - - - -
- - -
-
+ {/* Divider */} +
+ + {/* Environment Variables */} +
+ +
+ + )}
); } - -// ExtensionConfigFields.tsx - -// EnvVarsSection.tsx diff --git a/ui/desktop/src/components/settings_v2/extensions/subcomponents/ExtensionItem.tsx b/ui/desktop/src/components/settings_v2/extensions/subcomponents/ExtensionItem.tsx index 3bf772bb677f..e5a8d14a5103 100644 --- a/ui/desktop/src/components/settings_v2/extensions/subcomponents/ExtensionItem.tsx +++ b/ui/desktop/src/components/settings_v2/extensions/subcomponents/ExtensionItem.tsx @@ -12,6 +12,15 @@ interface ExtensionItemProps { } export default function ExtensionItem({ extension, onToggle, onConfigure }: ExtensionItemProps) { + const renderFormattedSubtitle = () => { + const subtitle = getSubtitle(extension); + return subtitle.split('\n').map((part, index) => ( + + {index === 0 ? part : {part}} + {index < subtitle.split('\n').length - 1 &&
} +
+ )); + }; return (
@@ -33,7 +42,7 @@ export default function ExtensionItem({ extension, onToggle, onConfigure }: Exte />
-

{getSubtitle(extension)}

+

{renderFormattedSubtitle()}

); } diff --git a/ui/desktop/src/components/settings_v2/extensions/subcomponents/ExtensionList.tsx b/ui/desktop/src/components/settings_v2/extensions/subcomponents/ExtensionList.tsx index d588926571df..5e7dc69f74bf 100644 --- a/ui/desktop/src/components/settings_v2/extensions/subcomponents/ExtensionList.tsx +++ b/ui/desktop/src/components/settings_v2/extensions/subcomponents/ExtensionList.tsx @@ -3,6 +3,7 @@ import { FixedExtensionEntry } from '../../../ConfigContext'; import { ExtensionConfig } from '../../../../api/types.gen'; import ExtensionItem from './ExtensionItem'; import builtInExtensionsData from '../../../../built-in-extensions.json'; +import { combineCmdAndArgs } from '../utils'; interface ExtensionListProps { extensions: FixedExtensionEntry[]; @@ -45,7 +46,8 @@ export function getSubtitle(config: ExtensionConfig): string { return 'Built-in extension'; } if (config.type === 'stdio') { - return `STDIO extension${config.cmd ? ` (${config.cmd})` : ''}`; + const full_command = combineCmdAndArgs(config.cmd, config.args); + return `STDIO extension${full_command ? `\n${full_command}` : ''}`; } if (config.type === 'sse') { return `SSE extension${config.uri ? ` (${config.uri})` : ''}`; diff --git a/ui/desktop/src/components/settings_v2/extensions/utils.ts b/ui/desktop/src/components/settings_v2/extensions/utils.ts new file mode 100644 index 000000000000..000f84f011be --- /dev/null +++ b/ui/desktop/src/components/settings_v2/extensions/utils.ts @@ -0,0 +1,97 @@ +import { FixedExtensionEntry } from '../../ConfigContext'; +import { ExtensionConfig } from '../../../api/types.gen'; + +export interface ExtensionFormData { + name: string; + type: 'stdio' | 'sse' | 'builtin'; + cmd?: string; + endpoint?: string; + enabled: boolean; + envVars: { key: string; value: string }[]; +} + +export function getDefaultFormData(): ExtensionFormData { + return { + name: '', + type: 'stdio', + cmd: '', + endpoint: '', + enabled: true, + envVars: [], + }; +} + +export 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' ? combineCmdAndArgs(extension.cmd, extension.args) : undefined, + endpoint: extension.type === 'sse' ? extension.uri : undefined, + enabled: extension.enabled, + envVars, + }; +} + +export 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') { + // we put the cmd + args all in the form cmd field but need to split out into cmd + args + const { cmd, args } = splitCmdAndArgs(formData.cmd); + + return { + type: 'stdio', + name: formData.name, + cmd: cmd, + args: 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, + }; + } +} + +export function splitCmdAndArgs(str: string): { cmd: string; args: string[] } { + const words = str.trim().split(/\s+/); + const cmd = words[0] || ''; + const args = words.slice(1); + + return { + cmd, + args, + }; +} + +export function combineCmdAndArgs(cmd: string, args: string[]): string { + return [cmd, ...args].join(' '); +} diff --git a/ui/desktop/src/components/settings_v2/providers/ProviderSettingsPage.tsx b/ui/desktop/src/components/settings_v2/providers/ProviderSettingsPage.tsx index 121d0247ca00..2c77d1dca991 100644 --- a/ui/desktop/src/components/settings_v2/providers/ProviderSettingsPage.tsx +++ b/ui/desktop/src/components/settings_v2/providers/ProviderSettingsPage.tsx @@ -4,6 +4,7 @@ import BackButton from '../../ui/BackButton'; import ProviderGrid from './ProviderGrid'; import { useConfig } from '../../ConfigContext'; import { ProviderDetails } from '../../../api/types.gen'; +import { useAgent } from '../../../agent/UpdateAgent'; interface ProviderSettingsProps { onClose: () => void; @@ -12,6 +13,7 @@ interface ProviderSettingsProps { export default function ProviderSettings({ onClose, isOnboarding }: ProviderSettingsProps) { const { getProviders, upsert } = useConfig(); + const { initializeAgent } = useAgent(); const [loading, setLoading] = useState(true); const [providers, setProviders] = useState([]); const initialLoadDone = useRef(false); @@ -50,24 +52,30 @@ export default function ProviderSettings({ onClose, isOnboarding }: ProviderSett // Handler for when a provider is launched if this component is used as part of onboarding page const handleProviderLaunch = useCallback( - (provider: ProviderDetails) => { + async (provider: ProviderDetails) => { + const provider_name = provider.name; + const model = provider.metadata.default_model; + console.log(`Launching with provider: ${provider.name}`); try { + // update the config // set GOOSE_PROVIDER in the config file - // @lily-de: leaving as test for now to avoid messing with my config directly - upsert('GOOSE_PROVIDER_TEST', provider.name, false).then((_) => - console.log('Setting GOOSE_PROVIDER to', provider.name) + upsert('GOOSE_PROVIDER', provider_name, false).then((_) => + console.log('Setting GOOSE_PROVIDER to', provider_name) ); // set GOOSE_MODEL in the config file - upsert('GOOSE_MODEL_TEST', provider.metadata.default_model, false).then((_) => - console.log('Setting GOOSE_MODEL to', provider.metadata.default_model) + upsert('GOOSE_MODEL', model, false).then((_) => + console.log('Setting GOOSE_MODEL to', model) ); + + // initialize agent + await initializeAgent(provider_name, model); } catch (error) { - console.error(`Failed to initialize with provider ${provider.name}:`, error); + console.error(`Failed to initialize with provider ${provider_name}:`, error); } onClose(); }, - [onClose, upsert] + [initializeAgent, onClose, upsert] ); return ( diff --git a/ui/desktop/src/components/settings_v2/providers/modal/ProviderConfiguationModal.tsx b/ui/desktop/src/components/settings_v2/providers/modal/ProviderConfiguationModal.tsx index d2651fbef5f7..b09451557059 100644 --- a/ui/desktop/src/components/settings_v2/providers/modal/ProviderConfiguationModal.tsx +++ b/ui/desktop/src/components/settings_v2/providers/modal/ProviderConfiguationModal.tsx @@ -63,7 +63,7 @@ export default function ProviderConfigurationModal() { }; return ( - +
{/* Logo area - centered above title */} From 4a19661c6ee83f5fe77cf8f64f284c67635cbd28 Mon Sep 17 00:00:00 2001 From: Lily Delalande Date: Mon, 17 Mar 2025 09:27:44 -0700 Subject: [PATCH 2/2] fix import --- .../src/components/settings_v2/extensions/ExtensionsSection.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/desktop/src/components/settings_v2/extensions/ExtensionsSection.tsx b/ui/desktop/src/components/settings_v2/extensions/ExtensionsSection.tsx index f74a4f204aad..60b10540749a 100644 --- a/ui/desktop/src/components/settings_v2/extensions/ExtensionsSection.tsx +++ b/ui/desktop/src/components/settings_v2/extensions/ExtensionsSection.tsx @@ -11,6 +11,7 @@ import { extensionToFormData, getDefaultFormData, } from './utils'; +import { useAgent } from '../../../agent/UpdateAgent'; export default function ExtensionsSection() { const { toggleExtension, getExtensions, addExtension, removeExtension } = useConfig();