-
Notifications
You must be signed in to change notification settings - Fork 2.7k
ui: start extensions on add #1714
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,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) => { | ||
|
Contributor
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. nit: Maybe we should call this
Contributor
Author
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. i think i will refactor a bit so will just leave like this for now :D will get rid of log statements then too |
||
| 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<Response> => { | ||
| 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 }) => ( | ||
| <div className="flex flex-col gap-1"> | ||
| <div>Error adding {extension.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(data.message || 'Unknown error'); | ||
| closeToast?.(); | ||
| }} | ||
| > | ||
| Copy error message | ||
| </button> | ||
| </div> | ||
| </div> | ||
| ); | ||
|
|
||
| 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, | ||
| }; | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<string, string> = { | ||
| 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; | ||
| } |
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.
Should this file also be called
useAgent.tsxinstead ofUpdateAgent?