/** * An internal context to separate the messages state (which is constantly changing) from the rest of CopilotKit context */ import { ReactNode, useEffect, useState, useRef, useCallback, useMemo, createContext, useContext, } from "react"; import { CopilotMessagesContext } from "../../context/copilot-messages-context"; import { loadMessagesFromJsonRepresentation, Message, GraphQLError, } from "@copilotkit/runtime-client-gql"; import { useCopilotContext } from "../../context/copilot-context"; import { useToast } from "../toast/toast-provider"; import { shouldShowDevConsole } from "../../utils/dev-console"; import { ErrorVisibility, CopilotKitApiDiscoveryError, CopilotKitRemoteEndpointDiscoveryError, CopilotKitAgentDiscoveryError, CopilotKitError, CopilotKitErrorCode, } from "@copilotkit/shared"; import { Suggestion } from "@copilotkit/core"; /** * Determine whether a GraphQL error should be suppressed based on its visibility * and whether the dev console is active. * * Returns `null` when the error should be surfaced to the UI, or a log prefix * string when the error should be suppressed (logged to console only). * * Exported for unit testing. */ export function getErrorSuppression( visibility: ErrorVisibility | undefined, isDev: boolean, ): string | null { // Silent errors are always suppressed if (visibility === ErrorVisibility.SILENT) { return "CopilotKit Silent Error:"; } // DEV_ONLY errors are suppressed in production if (!isDev && visibility === ErrorVisibility.DEV_ONLY) { return "CopilotKit Error (hidden in production):"; } // All other visibilities (TOAST, BANNER, undefined) are always surfaced return null; } // Helper to determine if error should show as banner based on visibility and legacy patterns function shouldShowAsBanner(gqlError: GraphQLError): boolean { const extensions = gqlError.extensions; if (!extensions) return false; // Priority 1: Check error code for discovery errors (these should always be banners) const code = extensions.code as CopilotKitErrorCode; if ( code === CopilotKitErrorCode.AGENT_NOT_FOUND || code === CopilotKitErrorCode.API_NOT_FOUND || code === CopilotKitErrorCode.REMOTE_ENDPOINT_NOT_FOUND || code === CopilotKitErrorCode.CONFIGURATION_ERROR || code === CopilotKitErrorCode.MISSING_PUBLIC_API_KEY_ERROR || code === CopilotKitErrorCode.UPGRADE_REQUIRED_ERROR ) { return true; } // Priority 2: Check banner visibility if (extensions.visibility === ErrorVisibility.BANNER) { return true; } // Priority 3: Check for critical errors that should be banners regardless of formal classification const errorMessage = gqlError.message.toLowerCase(); if ( errorMessage.includes("api key") || errorMessage.includes("401") || errorMessage.includes("unauthorized") || errorMessage.includes("authentication") || errorMessage.includes("incorrect api key") ) { return true; } // Priority 4: Legacy stack trace detection for discovery errors const originalError = extensions.originalError as any; if (originalError?.stack) { return ( originalError.stack.includes("CopilotApiDiscoveryError") || originalError.stack.includes("CopilotKitRemoteEndpointDiscoveryError") || originalError.stack.includes("CopilotKitAgentDiscoveryError") ); } return false; } /** * MessagesTap is used to mitigate performance issues when we only need * a snapshot of the messages, not a continuously updating stream of messages. */ export type MessagesTap = { getMessagesFromTap: () => Message[]; updateTapMessages: (messages: Message[]) => void; }; const MessagesTapContext = createContext(null); export function useMessagesTap() { const tap = useContext(MessagesTapContext); if (!tap) throw new Error("useMessagesTap must be used inside "); return tap; } export function MessagesTapProvider({ children, }: { children: React.ReactNode; }) { const messagesRef = useRef([]); const tapRef = useRef({ getMessagesFromTap: () => messagesRef.current, updateTapMessages: (messages: Message[]) => { messagesRef.current = messages; }, }); return ( {children} ); } /** * CopilotKit messages context. */ export function CopilotMessages({ children }: { children: ReactNode }) { const [messages, setMessages] = useState([]); const lastLoadedThreadId = useRef(undefined!); const lastLoadedAgentName = useRef(undefined!); const lastLoadedMessages = useRef(undefined!); const { updateTapMessages } = useMessagesTap(); const { threadId, agentSession, showDevConsole, onError, copilotApiConfig } = useCopilotContext(); const { setBannerError } = useToast(); // Helper function to trace UI errors (similar to useCopilotRuntimeClient) const traceUIError = useCallback( async (error: CopilotKitError, originalError?: any) => { // Just check if onError and publicApiKey are defined if (!onError || !copilotApiConfig.publicApiKey) return; try { const traceEvent = { type: "error" as const, timestamp: Date.now(), context: { source: "ui" as const, request: { operation: "loadAgentState", url: copilotApiConfig.chatApiEndpoint, startTime: Date.now(), }, technical: { environment: "browser", userAgent: typeof navigator !== "undefined" ? navigator.userAgent : undefined, stackTrace: originalError instanceof Error ? originalError.stack : undefined, }, }, error, }; await onError(traceEvent); } catch (traceError) { console.error("Error in CopilotMessages onError handler:", traceError); } }, [onError, copilotApiConfig.publicApiKey, copilotApiConfig.chatApiEndpoint], ); const createStructuredError = ( gqlError: GraphQLError, ): CopilotKitError | null => { const extensions = gqlError.extensions; const originalError = extensions?.originalError as any; // Priority: Check stack trace for discovery errors first if (originalError?.stack) { if (originalError.stack.includes("CopilotApiDiscoveryError")) { return new CopilotKitApiDiscoveryError({ message: originalError.message, }); } if ( originalError.stack.includes("CopilotKitRemoteEndpointDiscoveryError") ) { return new CopilotKitRemoteEndpointDiscoveryError({ message: originalError.message, }); } if (originalError.stack.includes("CopilotKitAgentDiscoveryError")) { return new CopilotKitAgentDiscoveryError({ agentName: "", availableAgents: [], }); } } // Fallback: Use the formal error code if available const message = originalError?.message || gqlError.message; const code = extensions?.code as CopilotKitErrorCode; if (code) { return new CopilotKitError({ message, code }); } return null; }; const handleGraphQLErrors = useCallback( (error: any) => { if (error.graphQLErrors?.length) { const graphQLErrors = error.graphQLErrors as GraphQLError[]; // Route all errors to banners for consistent UI const routeError = (gqlError: GraphQLError) => { const extensions = gqlError.extensions; const visibility = extensions?.visibility as ErrorVisibility; const isDev = shouldShowDevConsole(showDevConsole); const suppression = getErrorSuppression(visibility, isDev); if (suppression) { console.error(suppression, gqlError.message); return; } // TOAST and BANNER errors are always surfaced, even in production // All other errors (including DEV_ONLY) show as banners for consistency const ckError = createStructuredError(gqlError); if (ckError) { setBannerError(ckError); // Trace the structured error traceUIError(ckError, gqlError); } else { // Fallback: create a generic error for unstructured GraphQL errors const fallbackError = new CopilotKitError({ message: gqlError.message, code: CopilotKitErrorCode.UNKNOWN, }); setBannerError(fallbackError); // Trace the fallback error traceUIError(fallbackError, gqlError); } }; // Process all errors as banners graphQLErrors.forEach(routeError); } else { // Non-GraphQL errors are always surfaced to the user const fallbackError = new CopilotKitError({ message: error?.message || String(error), code: CopilotKitErrorCode.UNKNOWN, }); setBannerError(fallbackError); // Trace the non-GraphQL error traceUIError(fallbackError, error); } }, [setBannerError, showDevConsole, traceUIError], ); useEffect(() => { updateTapMessages(messages); }, [messages, updateTapMessages]); const memoizedChildren = useMemo(() => children, [children]); const [suggestions, setSuggestions] = useState([]); return ( {memoizedChildren} ); }