Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,10 @@ import {
createExtensionConfig,
ExtensionFormData,
extensionToFormData,
extractExtensionConfig,
getDefaultFormData,
} from './utils';
import { useAgent } from '../../../agent/UpdateAgent';
import { activateExtension } from '.';
import { AddNewExtension, DeleteExtension, ToggleExtension, UpdateExtension } from './temp';

export default function ExtensionsSection() {
const { toggleExtension, getExtensions, addExtension, removeExtension } = useConfig();
Expand All @@ -22,7 +22,6 @@ export default function ExtensionsSection() {
const [selectedExtension, setSelectedExtension] = useState<FixedExtensionEntry | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
const { updateAgent, addExtensionToAgent } = useAgent();

const fetchExtensions = async () => {
setLoading(true);
Expand All @@ -44,13 +43,17 @@ export default function ExtensionsSection() {
fetchExtensions();
}, []);

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 handleExtensionToggle = async (extension: FixedExtensionEntry) => {
// If extension is enabled, we are trying to toggle if off, otherwise on
const toggleDirection = extension.enabled ? 'toggleOff' : 'toggleOn';
const extensionConfig = extractExtensionConfig(extension);
await ToggleExtension({
toggle: toggleDirection,
extensionConfig: extensionConfig,
addToConfig: addExtension,
removeFromConfig: removeExtension,
});
await fetchExtensions(); // Refresh the list after toggling
};

const handleConfigureClick = (extension: FixedExtensionEntry) => {
Expand All @@ -60,38 +63,29 @@ export default function ExtensionsSection() {

const handleAddExtension = async (formData: ExtensionFormData) => {
const extensionConfig = createExtensionConfig(formData);

try {
await activateExtension(formData.name, extensionConfig, addExtension);
console.log('attempting to add extension');
await updateAgent(extensionConfig);
handleModalClose();
await fetchExtensions(); // Refresh the list after adding
} catch (error) {
console.error('Failed to add extension:', error);
}
// TODO: replace activateExtension in index
// TODO: make sure error handling works
await AddNewExtension({ addToConfig: addExtension, extensionConfig: extensionConfig });
handleModalClose();
await fetchExtensions();
};

const handleUpdateExtension = async (formData: ExtensionFormData) => {
const extensionConfig = createExtensionConfig(formData);

try {
await activateExtension(formData.name, extensionConfig, addExtension);
handleModalClose();
fetchExtensions(); // Refresh the list after updating
} catch (error) {
console.error('Failed to update extension configuration:', error);
}
await UpdateExtension({
enabled: formData.enabled,
extensionConfig: extensionConfig,
addToConfig: addExtension,
});
handleModalClose();
await fetchExtensions();
};

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);
}
await DeleteExtension({ name, removeFromConfig: removeExtension });
handleModalClose();
await fetchExtensions();
};

const handleModalClose = () => {
Expand Down
136 changes: 136 additions & 0 deletions ui/desktop/src/components/settings_v2/extensions/agent_stuff.tsx
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',
Copy link
Collaborator

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-center for 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!

Copy link
Contributor Author

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

});

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} />, {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now that we have ToastError I think you can remove the custom ErrorMsg component and just use the reusable version.

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 });
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can also nicely be ToastError

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);
}
6 changes: 6 additions & 0 deletions ui/desktop/src/components/settings_v2/extensions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -77,6 +78,11 @@ export async function activateExtension(
// First add to the config system
await addExtensionFn(nameToKey(name), config, true);

if (config.type != 'stdio') {
Copy link
Collaborator

Choose a reason for hiding this comment

The 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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { getSubtitle, getFriendlyTitle } from './ExtensionList';

interface ExtensionItemProps {
extension: FixedExtensionEntry;
onToggle: (name: string) => void;
onToggle: (extension: FixedExtensionEntry) => void;
onConfigure: (extension: FixedExtensionEntry) => void;
}

Expand Down Expand Up @@ -37,7 +37,7 @@ export default function ExtensionItem({ extension, onToggle, onConfigure }: Exte
)}
<Switch
checked={extension.enabled}
onCheckedChange={() => onToggle(extension.name)}
onCheckedChange={() => onToggle(extension)}
variant="mono"
/>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { combineCmdAndArgs } from '../utils';

interface ExtensionListProps {
extensions: FixedExtensionEntry[];
onToggle: (name: string) => void;
onToggle: (extension: FixedExtensionEntry) => void;
onConfigure: (extension: FixedExtensionEntry) => void;
}

Expand Down
Loading
Loading