import { toast, ToastOptions } from 'react-toastify'; import { Button } from './components/ui/button'; import { Tooltip, TooltipContent, TooltipTrigger } from './components/ui/Tooltip'; import Copy from './components/icons/Copy'; import { startNewSession } from './sessions'; import { useNavigation } from './hooks/useNavigation'; import { GroupedExtensionLoadingToast, ExtensionLoadingStatus, } from './components/GroupedExtensionLoadingToast'; import { getInitialWorkingDir } from './utils/workingDir'; export interface ToastServiceOptions { silent?: boolean; shouldThrow?: boolean; } class ToastService { private silent: boolean = false; private shouldThrow: boolean = false; // Create a singleton instance private static instance: ToastService; public static getInstance(): ToastService { if (!ToastService.instance) { ToastService.instance = new ToastService(); } return ToastService.instance; } configure(options: ToastServiceOptions = {}): void { if (options.silent !== undefined) { this.silent = options.silent; } if (options.shouldThrow !== undefined) { this.shouldThrow = options.shouldThrow; } } error(props: ToastErrorProps): void { if (!this.silent) { toastError(props); } if (this.shouldThrow) { throw new Error(props.msg); } } loading({ title, msg }: { title: string; msg: string }): string | number | undefined { if (this.silent) { return undefined; } const toastId = toastLoading({ title, msg }); return toastId; } success({ title, msg }: { title: string; msg: string }): void { if (this.silent) { return; } toastSuccess({ title, msg }); } dismiss(toastId?: string | number): void { if (toastId) toast.dismiss(toastId); } /** * Create a grouped extension loading toast that can be updated as extensions load */ extensionLoading( extensions: ExtensionLoadingStatus[], totalCount: number, isComplete: boolean = false ): string | number { if (this.silent) { return 'silent'; } const toastId = 'extension-loading'; const hasErrors = extensions.some((ext) => ext.status === 'error'); const autoClose = isComplete && !hasErrors ? 5000 : false; // Check if toast already exists if (toast.isActive(toastId)) { // Update existing toast toast.update(toastId, { render: ( ), autoClose, closeButton: true, closeOnClick: false, }); } else { // Create new toast toast( , { ...commonToastOptions, toastId, autoClose, closeButton: true, closeOnClick: false, // Prevent closing when clicking to expand/collapse } ); } return toastId; } /** * Handle errors with consistent logging and toast notifications * Consolidates the functionality of the original handleError function */ handleError(title: string, message: string, options: ToastServiceOptions = {}): void { this.configure(options); this.error({ title: title, msg: message, traceback: message, }); } } // Export a singleton instance for use throughout the app export const toastService = ToastService.getInstance(); // Re-export ExtensionLoadingStatus for convenience export type { ExtensionLoadingStatus }; const commonToastOptions: ToastOptions = { position: 'top-right', closeButton: true, 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(
{title ? {title} : null} {title ?
{msg}
: null}
, { ...commonToastOptions, autoClose: 3000, ...toastOptions } ); } type ToastErrorProps = { title: string; msg: string; traceback?: string; recoverHints?: string; }; function ToastErrorContent({ title, msg, traceback, recoverHints, }: Omit) { const setView = useNavigation(); const showRecovery = recoverHints && setView; const hasBoth = traceback && showRecovery; const handleCopyError = async () => { if (traceback) { try { await navigator.clipboard.writeText(traceback); } catch (error) { console.error('Failed to copy error:', error); } } }; return (
{title && {title}} {msg &&
{msg}
}
{showRecovery && ( )} {hasBoth && ( Copy error )} {traceback && !hasBoth && }
); } export function toastError({ title, msg, traceback, recoverHints }: ToastErrorProps) { return toast.error( , { ...commonToastOptions, autoClose: traceback ? false : 5000 } ); } type ToastLoadingProps = { title?: string; msg?: string; toastOptions?: ToastOptions; }; export function toastLoading({ title, msg, toastOptions }: ToastLoadingProps) { return toast.loading(
{title ? {title} : null} {title ?
{msg}
: null}
, { ...commonToastOptions, autoClose: false, ...toastOptions } ); }