New toasts (#1777)
This commit is contained in:
@@ -398,6 +398,14 @@ export default function App() {
|
||||
<>
|
||||
<ToastContainer
|
||||
aria-label="Toast notifications"
|
||||
toastClassName={() =>
|
||||
`relative min-h-16 mb-4 p-2 rounded-lg
|
||||
flex justify-between overflow-hidden cursor-pointer
|
||||
text-textProminentInverse bg-bgStandardInverse dark:bg-bgAppInverse
|
||||
`
|
||||
}
|
||||
style={{ width: '380px' }}
|
||||
className="mt-6"
|
||||
position="top-right"
|
||||
autoClose={3000}
|
||||
closeOnClick
|
||||
|
||||
@@ -4,6 +4,12 @@ import { ExtensionConfig } from '../api';
|
||||
import { toast } from 'react-toastify';
|
||||
import React, { useState } from 'react';
|
||||
import { initializeAgent as startAgent, replaceWithShims } from './utils';
|
||||
import {
|
||||
ToastError,
|
||||
ToastInfo,
|
||||
ToastLoading,
|
||||
ToastSuccess,
|
||||
} from '../components/settings/models/toasts';
|
||||
|
||||
// extensionUpdate = an extension was newly added or updated so we should attempt to add it
|
||||
|
||||
@@ -25,9 +31,10 @@ export const useAgent = () => {
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Failed to initialize agent:', error);
|
||||
toast.error(
|
||||
`Failed to initialize agent: ${error instanceof Error ? error.message : 'Unknown error'}`
|
||||
);
|
||||
ToastError({
|
||||
title: 'Failed to initialize agent',
|
||||
errorMessage: error instanceof Error ? error.message : 'Unknown error',
|
||||
});
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -84,10 +91,14 @@ export const useAgent = () => {
|
||||
try {
|
||||
let toastId;
|
||||
if (!silent) {
|
||||
toastId = toast.loading(`Adding ${extension.name} extension...`, {
|
||||
position: 'top-center',
|
||||
toastId = ToastLoading({
|
||||
title: extension.name,
|
||||
msg: 'Adding extension...',
|
||||
toastOptions: { position: 'top-center' },
|
||||
});
|
||||
ToastInfo({
|
||||
msg: 'Press the escape key to continue using goose while extension loads',
|
||||
});
|
||||
toast.info('Press the escape key to continue using goose while extension loads');
|
||||
}
|
||||
|
||||
const response = await fetch(getApiUrl('/extensions/add'), {
|
||||
@@ -108,14 +119,20 @@ export const useAgent = () => {
|
||||
if (response.status === 428) {
|
||||
if (!silent) {
|
||||
if (toastId) toast.dismiss(toastId);
|
||||
toast.error('Agent is not initialized. Please initialize the agent first.');
|
||||
ToastError({
|
||||
msg: '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}`);
|
||||
ToastError({
|
||||
title: extension.name,
|
||||
msg: 'Failed to add extension',
|
||||
errorMessage: errorMsg,
|
||||
});
|
||||
}
|
||||
return response;
|
||||
}
|
||||
@@ -135,40 +152,34 @@ export const useAgent = () => {
|
||||
if (!data.error) {
|
||||
if (!silent) {
|
||||
if (toastId) toast.dismiss(toastId);
|
||||
toast.success(`Successfully enabled ${extension.name} extension`);
|
||||
ToastSuccess({
|
||||
title: extension.name,
|
||||
msg: 'Successfully added 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 });
|
||||
ToastError({
|
||||
title: extension.name,
|
||||
msg: 'Failed to add extension',
|
||||
errorMessage: data.message,
|
||||
});
|
||||
|
||||
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 });
|
||||
ToastError({
|
||||
title: extension.name,
|
||||
msg: 'Failed to add extension',
|
||||
errorMessage: error.message,
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
getTextContent,
|
||||
createAssistantMessage,
|
||||
} from '../types/message';
|
||||
import { ToastSuccess } from './settings/models/toasts';
|
||||
|
||||
export interface ChatType {
|
||||
id: string;
|
||||
|
||||
@@ -12,9 +12,9 @@ import { createSelectedModel } from './settings/models/utils';
|
||||
import { getDefaultModel } from './settings/models/hardcoded_stuff';
|
||||
import { initializeSystem } from '../utils/providerUtils';
|
||||
import { getApiUrl, getSecretKey } from '../config';
|
||||
import { toast } from 'react-toastify';
|
||||
import { getActiveProviders, isSecretKey } from './settings/api_keys/utils';
|
||||
import { BaseProviderGrid, getProviderDescription } from './settings/providers/BaseProviderGrid';
|
||||
import { ToastError, ToastSuccess } from './settings/models/toasts';
|
||||
|
||||
interface ProviderGridProps {
|
||||
onSubmit?: () => void;
|
||||
@@ -55,9 +55,10 @@ export function ProviderGrid({ onSubmit }: ProviderGridProps) {
|
||||
addRecentModel(model);
|
||||
localStorage.setItem('GOOSE_PROVIDER', providerId);
|
||||
|
||||
toast.success(
|
||||
`Selected ${provider.name} provider. Starting Goose with default model: ${getDefaultModel(provider.name.toLowerCase().replace(/ /g, '_'))}.`
|
||||
);
|
||||
ToastSuccess({
|
||||
title: provider.name,
|
||||
msg: `Starting Goose with default model: ${getDefaultModel(provider.name.toLowerCase().replace(/ /g, '_'))}.`,
|
||||
});
|
||||
|
||||
onSubmit?.();
|
||||
};
|
||||
@@ -134,11 +135,10 @@ export function ProviderGrid({ onSubmit }: ProviderGridProps) {
|
||||
}
|
||||
}
|
||||
|
||||
toast.success(
|
||||
isUpdate
|
||||
? `Successfully updated configuration for ${provider}`
|
||||
: `Successfully added configuration for ${provider}`
|
||||
);
|
||||
ToastSuccess({
|
||||
title: provider,
|
||||
msg: isUpdate ? `Successfully updated configuration` : `Successfully added configuration`,
|
||||
});
|
||||
|
||||
const updatedKeys = await getActiveProviders();
|
||||
setActiveKeys(updatedKeys);
|
||||
@@ -147,9 +147,11 @@ export function ProviderGrid({ onSubmit }: ProviderGridProps) {
|
||||
setSelectedId(null);
|
||||
} catch (error) {
|
||||
console.error('Error handling modal submit:', error);
|
||||
toast.error(
|
||||
`Failed to ${providers.find((p) => p.id === selectedId)?.isConfigured ? 'update' : 'add'} configuration for ${provider}`
|
||||
);
|
||||
ToastError({
|
||||
title: provider,
|
||||
msg: `Failed to ${providers.find((p) => p.id === selectedId)?.isConfigured ? 'update' : 'add'} configuration`,
|
||||
errorMessage: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ScrollArea } from '../ui/scroll-area';
|
||||
import { toast } from 'react-toastify';
|
||||
import { Settings as SettingsType } from './types';
|
||||
import {
|
||||
FullExtensionConfig,
|
||||
@@ -16,6 +15,7 @@ import { RecentModelsRadio } from './models/RecentModels';
|
||||
import { ExtensionItem } from './extensions/ExtensionItem';
|
||||
import type { View } from '../../App';
|
||||
import { ModeSelection } from './basic/ModeSelection';
|
||||
import { ToastSuccess } from './models/toasts';
|
||||
|
||||
const EXTENSIONS_DESCRIPTION =
|
||||
'The Model Context Protocol (MCP) is a system that allows AI models to securely connect with local or remote resources using standard server setups. It works like a client-server setup and expands AI capabilities using three main components: Prompts, Resources, and Tools.';
|
||||
@@ -164,7 +164,10 @@ export default function SettingsView({
|
||||
const response = await removeExtension(extensionBeingConfigured.name, true);
|
||||
|
||||
if (response.ok) {
|
||||
toast.success(`Successfully removed ${extensionBeingConfigured.name} extension`);
|
||||
ToastSuccess({
|
||||
title: extensionBeingConfigured.name,
|
||||
msg: `Successfully removed extension`,
|
||||
});
|
||||
|
||||
// Remove from localstorage
|
||||
setSettings((prev) => ({
|
||||
|
||||
@@ -6,6 +6,7 @@ import { FullExtensionConfig } from '../../../extensions';
|
||||
import { getApiUrl, getSecretKey } from '../../../config';
|
||||
import { addExtension } from '../../../extensions';
|
||||
import { toast } from 'react-toastify';
|
||||
import { ToastError, ToastSuccess } from '../models/toasts';
|
||||
|
||||
interface ConfigureExtensionModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -68,12 +69,19 @@ export function ConfigureBuiltInExtensionModal({
|
||||
throw new Error('Failed to add system configuration');
|
||||
}
|
||||
|
||||
toast.success(`Successfully configured the ${extension.name} extension`);
|
||||
ToastSuccess({
|
||||
title: extension.name,
|
||||
msg: `Successfully configured extension`,
|
||||
});
|
||||
onSubmit();
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error('Error configuring extension:', error);
|
||||
toast.error('Failed to configure extension');
|
||||
ToastError({
|
||||
title: extension.name,
|
||||
msg: `Failed to configure the extension`,
|
||||
errorMessage: error.message,
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import { FullExtensionConfig } from '../../../extensions';
|
||||
import { getApiUrl, getSecretKey } from '../../../config';
|
||||
import { addExtension } from '../../../extensions';
|
||||
import { toast } from 'react-toastify';
|
||||
import { ToastError, ToastSuccess } from '../models/toasts';
|
||||
|
||||
interface ConfigureExtensionModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -70,12 +71,19 @@ export function ConfigureExtensionModal({
|
||||
throw new Error('Failed to add system configuration');
|
||||
}
|
||||
|
||||
toast.success(`Successfully configured the ${extension.name} extension`);
|
||||
ToastSuccess({
|
||||
title: extension.name,
|
||||
msg: `Successfully configured extension`,
|
||||
});
|
||||
onSubmit();
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error('Error configuring extension:', error);
|
||||
toast.error('Failed to configure extension');
|
||||
ToastError({
|
||||
title: extension.name,
|
||||
msg: `Failed to configure extension`,
|
||||
errorMessage: error.message,
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { toast } from 'react-toastify';
|
||||
import Select from 'react-select';
|
||||
import { createDarkSelectStyles, darkSelectTheme } from '../../ui/select-styles';
|
||||
import { getApiUrl, getSecretKey } from '../../../config';
|
||||
import { ToastError } from '../models/toasts';
|
||||
|
||||
interface ManualExtensionModalProps {
|
||||
isOpen: boolean;
|
||||
@@ -38,22 +39,22 @@ export function ManualExtensionModal({ isOpen, onClose, onSubmit }: ManualExtens
|
||||
e.preventDefault();
|
||||
|
||||
if (!formData.id || !formData.name || !formData.description) {
|
||||
toast.error('Please fill in all required fields');
|
||||
ToastError({ title: 'Please fill in all required fields' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (formData.type === 'stdio' && !formData.commandInput) {
|
||||
toast.error('Command is required for stdio type');
|
||||
ToastError({ title: 'Command is required for stdio type' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (formData.type === 'sse' && !formData.uri) {
|
||||
toast.error('URI is required for SSE type');
|
||||
ToastError({ title: 'URI is required for SSE type' });
|
||||
return;
|
||||
}
|
||||
|
||||
if (formData.type === 'builtin' && !formData.name) {
|
||||
toast.error('Name is required for builtin type');
|
||||
ToastError({ title: 'Name is required for builtin type' });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -98,7 +99,7 @@ export function ManualExtensionModal({ isOpen, onClose, onSubmit }: ManualExtens
|
||||
resetForm();
|
||||
} catch (error) {
|
||||
console.error('Error configuring extension:', error);
|
||||
toast.error('Failed to configure extension');
|
||||
ToastError({ title: 'Failed to configure extension', errorMessage: error.message });
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -1,37 +1,72 @@
|
||||
import { toast } from 'react-toastify';
|
||||
import { toast, ToastOptions } from 'react-toastify';
|
||||
import React from 'react';
|
||||
import { Model } from './ModelContext';
|
||||
|
||||
export function ToastSuccessModelSwitch(model: Model) {
|
||||
const commonToastOptions: ToastOptions = {
|
||||
position: 'top-right',
|
||||
closeButton: false,
|
||||
hideProgressBar: true,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
};
|
||||
|
||||
type ToastSuccessProps = { title?: string; msg?: string; toastOptions?: ToastOptions };
|
||||
export function ToastSuccess({ title, msg, toastOptions = {} }: ToastSuccessProps) {
|
||||
return toast.success(
|
||||
<div>
|
||||
<strong>Model Changed</strong>
|
||||
<div>Switched to {model.alias ?? model.name}</div>
|
||||
{title ? <strong className="font-medium">{title}</strong> : null}
|
||||
{title ? <div>{msg}</div> : null}
|
||||
</div>,
|
||||
{
|
||||
position: 'top-right',
|
||||
autoClose: 5000,
|
||||
hideProgressBar: true,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
}
|
||||
{ ...commonToastOptions, autoClose: 3000, ...toastOptions }
|
||||
);
|
||||
}
|
||||
|
||||
export function ToastFailureGeneral(msg?: string) {
|
||||
type ToastErrorProps = {
|
||||
title?: string;
|
||||
msg?: string;
|
||||
errorMessage?: string;
|
||||
toastOptions?: ToastOptions;
|
||||
};
|
||||
export function ToastError({ title, msg, errorMessage, toastOptions }: ToastErrorProps) {
|
||||
return toast.error(
|
||||
<div>
|
||||
<strong>Error</strong>
|
||||
<div>{msg}</div>
|
||||
<div className="flex gap-4">
|
||||
<div className="flex-grow">
|
||||
{title ? <strong className="font-medium">{title}</strong> : null}
|
||||
{msg ? <div>{msg}</div> : null}
|
||||
</div>
|
||||
<div className="flex-none flex items-center">
|
||||
{errorMessage ? (
|
||||
<button
|
||||
className="text-textProminentInverse font-medium"
|
||||
onClick={() => navigator.clipboard.writeText(errorMessage)}
|
||||
>
|
||||
Copy error
|
||||
</button>
|
||||
) : null}
|
||||
</div>
|
||||
</div>,
|
||||
{
|
||||
position: 'top-right',
|
||||
autoClose: 3000,
|
||||
hideProgressBar: true,
|
||||
closeOnClick: true,
|
||||
pauseOnHover: true,
|
||||
draggable: true,
|
||||
}
|
||||
{ ...commonToastOptions, autoClose: errorMessage ? false : 5000, ...toastOptions }
|
||||
);
|
||||
}
|
||||
|
||||
type ToastLoadingProps = { title?: string; msg?: string; toastOptions?: ToastOptions };
|
||||
export function ToastLoading({ title, msg, toastOptions }: ToastLoadingProps) {
|
||||
return toast.loading(
|
||||
<div>
|
||||
{title ? <strong className="font-medium">{title}</strong> : null}
|
||||
{title ? <div>{msg}</div> : null}
|
||||
</div>,
|
||||
{ ...commonToastOptions, autoClose: false, ...toastOptions }
|
||||
);
|
||||
}
|
||||
|
||||
type ToastInfoProps = { title?: string; msg?: string; toastOptions?: ToastOptions };
|
||||
export function ToastInfo({ title, msg, toastOptions }: ToastInfoProps) {
|
||||
return toast.info(
|
||||
<div>
|
||||
{title ? <strong className="font-medium">{title}</strong> : null}
|
||||
{msg ? <div>{msg}</div> : null}
|
||||
</div>,
|
||||
{ ...commonToastOptions, ...toastOptions }
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useModel } from './ModelContext'; // Import the useModel hook
|
||||
import { Model } from './ModelContext';
|
||||
import { useMemo } from 'react';
|
||||
import { gooseModels } from './GooseModels';
|
||||
import { ToastFailureGeneral, ToastSuccessModelSwitch } from './toasts';
|
||||
import { ToastError, ToastSuccess } from './toasts';
|
||||
import { initializeSystem } from '../../../utils/providerUtils';
|
||||
import { useRecentModels } from './RecentModels';
|
||||
|
||||
@@ -32,12 +32,19 @@ export function useHandleModelSelection() {
|
||||
console.log(`[${componentName}] Switched to model: ${model.name} (${model.provider})`);
|
||||
|
||||
// Display a success toast notification
|
||||
ToastSuccessModelSwitch(model);
|
||||
ToastSuccess({
|
||||
title: 'Model changed',
|
||||
msg: `Switched to ${model.alias ?? model.name}`,
|
||||
});
|
||||
} catch (error) {
|
||||
// Handle errors gracefully
|
||||
console.error(`[${componentName}] Failed to switch model:`, error);
|
||||
// Display an error toast notification
|
||||
ToastFailureGeneral(`Failed to switch to model: ${model.name}`);
|
||||
ToastError({
|
||||
title: model.name,
|
||||
msg: `Failed to switch to model`,
|
||||
errorMessage: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { toast } from 'react-toastify';
|
||||
import { getActiveProviders, isSecretKey } from '../api_keys/utils';
|
||||
import { useModel } from '../models/ModelContext';
|
||||
import { Button } from '../../ui/button';
|
||||
import { ToastError, ToastSuccess } from '../models/toasts';
|
||||
|
||||
function ConfirmationModal({ message, onConfirm, onCancel }) {
|
||||
return (
|
||||
@@ -141,11 +142,10 @@ export function ConfigureProvidersGrid() {
|
||||
}
|
||||
}
|
||||
|
||||
toast.success(
|
||||
isUpdate
|
||||
? `Successfully updated configuration for ${provider}`
|
||||
: `Successfully added configuration for ${provider}`
|
||||
);
|
||||
ToastSuccess({
|
||||
title: provider,
|
||||
msg: isUpdate ? `Successfully updated configuration` : `Successfully added configuration`,
|
||||
});
|
||||
|
||||
const updatedKeys = await getActiveProviders();
|
||||
setActiveKeys(updatedKeys);
|
||||
@@ -155,9 +155,11 @@ export function ConfigureProvidersGrid() {
|
||||
setModalMode('setup');
|
||||
} catch (error) {
|
||||
console.error('Error handling modal submit:', error);
|
||||
toast.error(
|
||||
`Failed to ${providers.find((p) => p.id === selectedForSetup)?.isConfigured ? 'update' : 'add'} configuration for ${provider}`
|
||||
);
|
||||
ToastError({
|
||||
title: provider,
|
||||
msg: `Failed to ${providers.find((p) => p.id === selectedForSetup)?.isConfigured ? 'update' : 'add'} configuration`,
|
||||
errorMessage: error.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -178,9 +180,8 @@ export function ConfigureProvidersGrid() {
|
||||
try {
|
||||
// Check if the selected provider is currently active
|
||||
if (currentModel?.provider === providerToDelete.name) {
|
||||
toast.error(
|
||||
`Cannot delete the configuration for ${providerToDelete.name} because it's the provider of the current model (${currentModel.name}). Please switch to a different model first.`
|
||||
);
|
||||
const msg = `Cannot delete the configuration because it's the provider of the current model (${currentModel.name}). Please switch to a different model first.`;
|
||||
ToastError({ title: providerToDelete.name, msg, errorMessage: msg });
|
||||
setIsConfirmationOpen(false);
|
||||
return;
|
||||
}
|
||||
@@ -208,13 +209,20 @@ export function ConfigureProvidersGrid() {
|
||||
}
|
||||
|
||||
console.log('Configuration deleted successfully.');
|
||||
toast.success(`Successfully deleted configuration for ${providerToDelete.name}`);
|
||||
ToastSuccess({
|
||||
title: providerToDelete.name,
|
||||
msg: 'Successfully deleted configuration',
|
||||
});
|
||||
|
||||
const updatedKeys = await getActiveProviders();
|
||||
setActiveKeys(updatedKeys);
|
||||
} catch (error) {
|
||||
console.error('Error deleting configuration:', error);
|
||||
toast.error(`Unable to delete configuration for ${providerToDelete.name}`);
|
||||
ToastError({
|
||||
title: providerToDelete.name,
|
||||
msg: 'Failed to delete configuration',
|
||||
errorMessage: error.message,
|
||||
});
|
||||
}
|
||||
setIsConfirmationOpen(false);
|
||||
};
|
||||
|
||||
@@ -7,7 +7,7 @@ import { QUICKSTART_GUIDE_URL } from '../providers/modal/constants';
|
||||
import { Input } from '../../ui/input';
|
||||
import { Select } from '../../ui/Select';
|
||||
import { useConfig } from '../../ConfigContext';
|
||||
import { ToastFailureGeneral, ToastSuccessModelSwitch } from '../../settings/models/toasts';
|
||||
import { ToastError, ToastSuccess } from '../../settings/models/toasts';
|
||||
import { initializeSystem } from '../../../../src/utils/providerUtils';
|
||||
|
||||
const ModalButtons = ({ onSubmit, onCancel }) => (
|
||||
@@ -43,10 +43,16 @@ export const AddModelModal = ({ onClose }: AddModelModalProps) => {
|
||||
await upsert('GOOSE_PROVIDER', provider, false);
|
||||
await upsert('GOOSE_MODEL', modelName, false);
|
||||
await initializeSystem(provider, modelName);
|
||||
ToastSuccessModelSwitch({ provider, name: modelName });
|
||||
ToastSuccess({
|
||||
title: 'Model changed',
|
||||
msg: `Switched to ${modelName}.`,
|
||||
});
|
||||
onClose();
|
||||
} catch (e) {
|
||||
ToastFailureGeneral(e.message);
|
||||
ToastError({
|
||||
title: 'Failed to add model',
|
||||
errorMessage: e.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -5,6 +5,8 @@ import { type SettingsViewOptions } from './components/settings/SettingsView';
|
||||
import { toast } from 'react-toastify';
|
||||
|
||||
import builtInExtensionsData from './built-in-extensions.json';
|
||||
import { ToastError, ToastLoading, ToastSuccess } from './components/settings/models/toasts';
|
||||
import { Toast } from 'react-toastify/dist/components';
|
||||
|
||||
// Hardcoded default extension timeout in seconds
|
||||
export const DEFAULT_EXTENSION_TIMEOUT = 300;
|
||||
@@ -82,7 +84,7 @@ export async function addExtension(
|
||||
};
|
||||
|
||||
let toastId;
|
||||
if (!silent) toastId = toast.loading(`Adding ${extension.name} extension...`);
|
||||
if (!silent) toastId = ToastLoading({ title: extension.name, msg: 'Adding extension...' });
|
||||
|
||||
const response = await fetch(getApiUrl('/extensions/add'), {
|
||||
method: 'POST',
|
||||
@@ -98,38 +100,31 @@ export async function addExtension(
|
||||
if (!data.error) {
|
||||
if (!silent) {
|
||||
if (toastId) toast.dismiss(toastId);
|
||||
toast.success(`Successfully enabled ${extension.name} extension`);
|
||||
ToastSuccess({ title: extension.name, msg: `Successfully enabled extension` });
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
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);
|
||||
closeToast();
|
||||
}}
|
||||
>
|
||||
Copy error message
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const errorMessage = `Error adding extension`;
|
||||
console.error(errorMessage);
|
||||
if (toastId) toast.dismiss(toastId);
|
||||
toast(ErrorMsg, { type: 'error', autoClose: false });
|
||||
ToastError({
|
||||
title: extension.name,
|
||||
msg: errorMessage,
|
||||
errorMessage: data.message,
|
||||
toastOptions: { autoClose: false },
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
const errorMessage = `Failed to add ${extension.name} extension: ${error instanceof Error ? error.message : 'Unknown error'}`;
|
||||
console.error(errorMessage);
|
||||
toast.error(errorMessage, { autoClose: false });
|
||||
ToastError({
|
||||
title: extension.name,
|
||||
msg: 'Failed to add extension',
|
||||
errorMessage: error.message,
|
||||
toastOptions: { autoClose: false },
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -149,19 +144,29 @@ export async function removeExtension(name: string, silent: boolean = false): Pr
|
||||
|
||||
if (!data.error) {
|
||||
if (!silent) {
|
||||
toast.success(`Successfully disabled ${name} extension`);
|
||||
ToastSuccess({ title: name, msg: 'Successfully disabled extension' });
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
const errorMessage = `Error removing ${name} extension${data.message ? `. ${data.message}` : ''}`;
|
||||
console.error(errorMessage);
|
||||
toast.error(errorMessage, { autoClose: false });
|
||||
ToastError({
|
||||
title: name,
|
||||
msg: 'Error removing extension',
|
||||
errorMessage: data.message,
|
||||
toastOptions: { autoClose: false },
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
const errorMessage = `Failed to remove ${name} extension: ${error instanceof Error ? error.message : 'Unknown error'}`;
|
||||
console.error(errorMessage);
|
||||
toast.error(errorMessage, { autoClose: false });
|
||||
ToastError({
|
||||
title: name,
|
||||
msg: 'Error removing extension',
|
||||
errorMessage: error.message,
|
||||
toastOptions: { autoClose: false },
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -241,7 +246,12 @@ function envVarsRequired(config: ExtensionConfig) {
|
||||
}
|
||||
|
||||
function handleError(message: string, shouldThrow = false): void {
|
||||
toast.error(message, { autoClose: false });
|
||||
ToastError({
|
||||
title: 'Failed to install extension',
|
||||
msg: message,
|
||||
errorMessage: message,
|
||||
toastOptions: { autoClose: false },
|
||||
});
|
||||
console.error(message);
|
||||
if (shouldThrow) {
|
||||
throw new Error(message);
|
||||
@@ -253,19 +263,14 @@ export async function addExtensionFromDeepLink(
|
||||
setView: (view: View, options: SettingsViewOptions) => void
|
||||
) {
|
||||
if (!url.startsWith('goose://extension')) {
|
||||
handleError(
|
||||
'Failed to install extension: Invalid URL: URL must use the goose://extension scheme'
|
||||
);
|
||||
handleError('Invalid URL: URL must use the goose://extension scheme');
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedUrl = new URL(url);
|
||||
|
||||
if (parsedUrl.protocol !== 'goose:') {
|
||||
handleError(
|
||||
'Failed to install extension: Invalid protocol: URL must use the goose:// scheme',
|
||||
true
|
||||
);
|
||||
handleError('Invalid protocol: URL must use the goose:// scheme', true);
|
||||
}
|
||||
|
||||
// Check that all required fields are present and not empty
|
||||
@@ -274,10 +279,7 @@ export async function addExtensionFromDeepLink(
|
||||
for (const field of requiredFields) {
|
||||
const value = parsedUrl.searchParams.get(field);
|
||||
if (!value || value.trim() === '') {
|
||||
handleError(
|
||||
`Failed to install extension: The link is missing required field '${field}'`,
|
||||
true
|
||||
);
|
||||
handleError(`The link is missing required field '${field}'`, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,10 +300,7 @@ export async function addExtensionFromDeepLink(
|
||||
// Check for security risk with npx -c command
|
||||
const args = parsedUrl.searchParams.getAll('arg');
|
||||
if (cmd === 'npx' && args.includes('-c')) {
|
||||
handleError(
|
||||
'Failed to install extension: npx with -c argument can lead to code injection',
|
||||
true
|
||||
);
|
||||
handleError('npx with -c argument can lead to code injection', true);
|
||||
}
|
||||
|
||||
const envList = parsedUrl.searchParams.getAll('env');
|
||||
|
||||
@@ -98,6 +98,8 @@
|
||||
--background-prominent: var(--grey-80);
|
||||
--background-standard: var(--grey-90);
|
||||
--background-subtle: var(--grey-95);
|
||||
--background-app-inverse: var(--constant-black);
|
||||
--background-standard-inverse: var(--dark-grey-25);
|
||||
|
||||
--border-divider: var(--grey-90);
|
||||
--border-inverse: var(--constant-white);
|
||||
@@ -116,12 +118,15 @@
|
||||
--text-prominent: var(--grey-10);
|
||||
--text-standard: var(--grey-20);
|
||||
--text-subtle: var(--grey-50);
|
||||
--text-prominent-inverse: var(--constant-white);
|
||||
|
||||
&.dark {
|
||||
--background-app: var(--constant-black);
|
||||
--background-prominent: var(--dark-grey-40);
|
||||
--background-standard: var(--dark-grey-25);
|
||||
--background-subtle: var(--dark-grey-15);
|
||||
--background-app-inverse: var(--constant-white);
|
||||
--background-prominent-inverse: var(--grey-80);
|
||||
|
||||
--border-divider: var(--dark-grey-25);
|
||||
--border-inverse: var(--constant-black);
|
||||
@@ -140,6 +145,7 @@
|
||||
--text-prominent: var(--constant-white);
|
||||
--text-standard: var(--dark-grey-90);
|
||||
--text-subtle: var(--dark-grey-60);
|
||||
--text-prominent-inverse: var(--grey-20);
|
||||
}
|
||||
/* end arcade colors */
|
||||
}
|
||||
|
||||
@@ -53,6 +53,8 @@ export default {
|
||||
bgSubtle: 'var(--background-subtle)',
|
||||
bgStandard: 'var(--background-standard)',
|
||||
bgProminent: 'var(--background-prominent)',
|
||||
bgAppInverse: 'var(--background-app-inverse)',
|
||||
bgStandardInverse: 'var(--background-standard-inverse)',
|
||||
|
||||
borderSubtle: 'var(--border-subtle)',
|
||||
borderStandard: 'var(--border-standard)',
|
||||
@@ -61,6 +63,7 @@ export default {
|
||||
textStandard: 'var(--text-standard)',
|
||||
textSubtle: 'var(--text-subtle)',
|
||||
textPlaceholder: 'var(--text-placeholder)',
|
||||
textProminentInverse: 'var(--text-prominent-inverse)',
|
||||
|
||||
iconProminent: 'var(--icon-prominent)',
|
||||
iconStandard: 'var(--icon-standard)',
|
||||
|
||||
Reference in New Issue
Block a user