-
Notifications
You must be signed in to change notification settings - Fork 2.7k
ui: remove and update extensions #1843
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,136 @@ | ||
| import { replaceWithShims } from '../../../agent/utils'; | ||
| import { ExtensionConfig } from '../../../api'; | ||
| import { toast } from 'react-toastify'; | ||
| import { getApiUrl, getSecretKey } from '../../../config'; | ||
| import React from 'react'; | ||
|
|
||
| // Error message component | ||
| const ErrorMsg = ({ | ||
| name, | ||
| message, | ||
| closeToast, | ||
| }: { | ||
| name: string; | ||
| message?: string; | ||
| closeToast?: () => void; | ||
| }) => ( | ||
| <div className="flex flex-col gap-1"> | ||
| <div> | ||
| Error {message?.includes('adding') ? 'adding' : 'removing'} {name} extension | ||
| </div> | ||
| <div> | ||
| <button | ||
| className="text-sm rounded px-2 py-1 bg-gray-400 hover:bg-gray-300 text-white cursor-pointer" | ||
| onClick={() => { | ||
| navigator.clipboard.writeText(message || 'Unknown error'); | ||
| closeToast?.(); | ||
| }} | ||
| > | ||
| Copy error message | ||
| </button> | ||
| </div> | ||
| </div> | ||
| ); | ||
|
|
||
| // Core API call function | ||
| async function extensionApiCall<T>( | ||
| endpoint: string, | ||
| payload: any, | ||
| actionType: 'adding' | 'removing', | ||
| extensionName: string | ||
| ): Promise<Response> { | ||
| let toastId; | ||
| const actionVerb = actionType === 'adding' ? 'Adding' : 'Removing'; | ||
| const pastVerb = actionType === 'adding' ? 'added' : 'removed'; | ||
|
|
||
| try { | ||
| toastId = toast.loading(`${actionVerb} ${extensionName} extension...`, { | ||
| position: 'top-center', | ||
| }); | ||
|
|
||
| if (actionType === 'adding') { | ||
| toast.info( | ||
| 'Press the ESC key on your keyboard to continue using goose while extension loads' | ||
| ); | ||
| } | ||
|
|
||
| const response = await fetch(getApiUrl(endpoint), { | ||
| method: 'POST', | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| 'X-Secret-Key': getSecretKey(), | ||
| }, | ||
| body: JSON.stringify(payload), | ||
| }); | ||
|
|
||
| // 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 && actionType === 'adding') { | ||
| if (toastId) toast.dismiss(toastId); | ||
| toast.error('Agent is not initialized. Please initialize the agent first.'); | ||
| return response; | ||
| } | ||
|
|
||
| if (toastId) toast.dismiss(toastId); | ||
| toast.error( | ||
| `Failed to ${actionType === 'adding' ? 'add' : 'remove'} ${extensionName} 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 }; | ||
| } | ||
|
|
||
| if (!data.error) { | ||
| if (toastId) toast.dismiss(toastId); | ||
| toast.success( | ||
| `Successfully ${actionType === 'adding' ? 'enabled' : 'disabled'} ${extensionName} extension` | ||
| ); | ||
| return response; | ||
| } | ||
|
|
||
| const errorMessage = `Error ${actionType} ${extensionName} extension${data.message ? `. ${data.message}` : ''}`; | ||
| console.error(errorMessage); | ||
|
|
||
| if (toastId) toast.dismiss(toastId); | ||
| toast(<ErrorMsg name={extensionName} message={data.message} />, { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Now that we have |
||
| type: 'error', | ||
| autoClose: false, | ||
| }); | ||
|
|
||
| return response; | ||
| } catch (error) { | ||
| console.log('Got some other error'); | ||
| const errorMessage = `Failed to ${actionType === 'adding' ? 'add' : 'remove'} ${extensionName} extension: ${error instanceof Error ? error.message : 'Unknown error'}`; | ||
| console.error(errorMessage); | ||
| if (toastId) toast.dismiss(toastId); | ||
| toast.error(errorMessage, { autoClose: false }); | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This can also nicely be |
||
| throw error; | ||
| } | ||
| } | ||
|
|
||
| // Public functions | ||
| export async function AddToAgent(extension: ExtensionConfig): Promise<Response> { | ||
| if (extension.type === 'stdio') { | ||
| console.log('extension command', extension.cmd); | ||
| extension.cmd = await replaceWithShims(extension.cmd); | ||
| console.log('next ext command', extension.cmd); | ||
| } | ||
|
|
||
| return extensionApiCall('/extensions/add', extension, 'adding', extension.name); | ||
| } | ||
|
|
||
| export async function RemoveFromAgent(name: string): Promise<Response> { | ||
| return extensionApiCall('/extensions/remove', name, 'removing', name); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,6 +6,7 @@ import { toast } from 'react-toastify'; | |
| import { ToastError, ToastLoading, ToastSuccess } from '../../settings/models/toasts'; | ||
|
|
||
| // Default extension timeout in seconds | ||
| // TODO: keep in sync with rust better | ||
| export const DEFAULT_EXTENSION_TIMEOUT = 300; | ||
|
|
||
| // Type definition for built-in extensions from JSON | ||
|
|
@@ -77,6 +78,11 @@ export async function activateExtension( | |
| // First add to the config system | ||
| await addExtensionFn(nameToKey(name), config, true); | ||
|
|
||
| if (config.type != 'stdio') { | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The V1 implementation supported the other kinds as well so this is a reminder to myself to go in very quickly after this and refactor to support HTTP+SSE ones as well. |
||
| console.error('only stdio is supported'); | ||
| throw Error('Only STDIO extensions are currently supported'); | ||
| } | ||
|
|
||
| // Then call the API endpoint | ||
| const response = await fetch(getApiUrl('/extensions/add'), { | ||
| method: 'POST', | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we want to go with
top-centerfor more things? I'm just thinking it might feel inconsistent as other things use top right. I am good with either - perhaps we should just be consistent across the board!There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The top right was annoying and covered the more menu :D top left covered the exit button lol